spg_sql/parser.rs
1//! Recursive-descent parser with a Pratt (precedence-climbing) sub-parser for
2//! expressions.
3//!
4//! Precedence (lowest → highest binding):
5//! `OR` (1) `<` `AND` (2) `<` `NOT` unary (3) `<`
6//! comparisons `=` `<>` `<` `<=` `>` `>=` (4) `<`
7//! `+` `-` (5) `<` `*` `/` (6) `<` unary `-` (7) `<` parens / atom.
8//!
9//! This matches PG's behaviour for the operators we support — e.g. `NOT a = b`
10//! parses as `NOT (a = b)` and `-a * b` as `(-a) * b`.
11
12use alloc::boxed::Box;
13use alloc::format;
14use alloc::string::{String, ToString};
15use alloc::vec;
16use alloc::vec::Vec;
17use core::fmt;
18use core::mem;
19
20use crate::ast::{
21 AssignTarget, BinOp, CastTarget, Collation, ColumnDef, ColumnName, ColumnTypeName,
22 CreateFunctionStatement, CreateIndexStatement, CreatePublicationStatement,
23 CreateSubscriptionStatement, CreateTableStatement, CreateTriggerStatement, DiscardTarget, Expr,
24 ExtractField, FkAction, ForeignKeyConstraint, FrameBound, FrameExclusion, FrameKind,
25 FromClause, FromJoin, FunctionArg, FunctionArgMode, FunctionArgType, FunctionAttrs,
26 FunctionBody, FunctionParallel, FunctionReturn, FunctionVolatility, GrantObject, GrantPriv,
27 GrantStatement, IndexMethod, InsertStatement, IsolationLevel, JoinKind, Literal, MysqlIntWidth,
28 NullTreatment, OrderBy, Overriding, PlPgSqlBlock, PlPgSqlDeclare, PlPgSqlStmt,
29 PublicationScope, RaiseLevel, RangeKindAst, ReturnTarget, SelectItem, SelectStatement,
30 Statement, TableRef, TriggerEvent, TriggerForEach, TriggerTiming, UnOp, UnionKind, VecEncoding,
31 WindowFrame,
32};
33use crate::lexer::{self, LexError, Token};
34
35/// v7.38 — a `WINDOW w AS (…)` definition body:
36/// `(PARTITION BY exprs, ORDER BY (expr, desc, nulls_first), frame)`.
37type WindowDef = (
38 Vec<Expr>,
39 Vec<(Expr, bool, Option<bool>)>,
40 Option<WindowFrame>,
41);
42
43/// v7.14.0 — true when the leading keyword of a top-level
44/// statement is one of the dump-emitted DDL forms SPG accepts
45/// as a no-op (no behavioural effect on the single-schema /
46/// single-database model). These statements are consumed up to
47/// the next `;` / EOF and returned as `Statement::Empty`.
48/// v7.39 (read01 round 57) — wrap a parsed GRANT body in the right statement.
49fn finish_grant(grant: bool, g: GrantStatement) -> Statement {
50 if grant {
51 Statement::Grant(g)
52 } else {
53 Statement::Revoke(g)
54 }
55}
56
57fn is_dump_noise_statement(lc: &str) -> bool {
58 matches!(
59 lc,
60 // v7.39 (read01 round 50): "comment" moved OUT — COMMENT ON is now a
61 // real statement with a real store. v7.39 (read01 round 57): "grant" /
62 // "revoke" moved OUT — table privileges are now REAL (stored in
63 // `relacl`, enforced against the session role); a grant on any other
64 // object class still parses and no-ops so dumps restore.
65 // MySQL bulk-load brackets.
66 "unlock"
67 // MySQL OPTIMIZE / ANALYZE TABLE / CHECK TABLE
68 // diagnostics that pg_dump-style tools also emit
69 // post-restore.
70 | "optimize"
71 | "check"
72 // PG psql backslash meta-commands that newer
73 // pg_dump versions emit unescaped (\restrict /
74 // \unrestrict). Real psql intercepts these; SPG's
75 // PG-wire sees them as raw text.
76 | "\\restrict"
77 | "\\unrestrict"
78 // v7.17.0 Phase 4.1 — MySQL `DELIMITER //` and
79 // `DELIMITER ;` directives. Technically client-side
80 // (the `mysql` CLI uses them to set the statement
81 // terminator), not SQL — but mysqldump and stored-
82 // procedure scripts emit them inline. SPG's parser
83 // sees one statement at a time and doesn't care
84 // about the terminator, so consume DELIMITER lines
85 // as Empty.
86 | "delimiter"
87 // v7.37.17 (17.6 siblings) — additional PG maintenance /
88 // session-state statements pg_dump + application startup
89 // scripts emit. SPG has no matching session-state to
90 // discard (no prepared-plan cache surface, no temp
91 // sequences), no matching security-label / storage-
92 // option to apply, no separate CREATE/DROP CAST that
93 // affects execution.
94 // v7.37.17 (17.6 siblings) — PG role-cleanup statements
95 // pg_dump / pg_dumpall emit around DROP ROLE:
96 // REASSIGN OWNED BY <role> [, ...] TO <newrole>
97 // DROP OWNED BY <role> [, ...] [CASCADE | RESTRICT]
98 // Both operate on the role's owned objects; SPG has no
99 // role-owner model, so accept-and-no-op.
100 // v7.37.17 (17.6 sibling) — LOAD '<library>'. pg_dump
101 // + extension scripts use LOAD to preload shared
102 // libraries. SPG doesn't have a shared-library extension
103 // point today (extensions ship as first-class crates
104 // linked at build time); accept as a no-op.
105 | "load"
106 )
107}
108
109/// v7.37.43-T4 — PG-unreserved keywords that are legal identifiers
110/// per `pg_get_keywords()`. SPG tokenizes these as named variants
111/// so the parser can dispatch on them in their owning contexts
112/// (`RELEASE SAVEPOINT`, `SHOW name`, `BEGIN`/`COMMIT`/`ROLLBACK`,
113/// `CREATE INDEX`, etc.), but they MUST stay usable as table /
114/// column / alias names — that's the PG contract for unreserved
115/// keywords (see PG docs Appendix C.1).
116///
117/// Before this generalisation, sentori migration 0001_init.sql
118/// `release TEXT NOT NULL` blew up the parser with "expected
119/// identifier, got Release", and the same gap stalked every
120/// SPG drop-in user whose schema had a column / alias named
121/// `release` / `index` / `tables` / `show` / `savepoint` /
122/// `begin` / `commit` / `rollback` / `drop` / `insert` / `values`
123/// / `limit` / `partition`. PG accepts all of them as identifiers
124/// when unquoted, so SPG must too.
125///
126/// Returns the canonical lowercase identifier text when the token
127/// belongs to PG's unreserved class, `None` otherwise. Used by
128/// `expect_ident_like` (column / table / alias names) so the
129/// generalisation applies everywhere an identifier may appear,
130/// not just in the contexts these tokens were introduced for.
131fn unreserved_keyword_text(tok: &Token) -> Option<String> {
132 let s = match tok {
133 // PG keyword class: unreserved or col_name.
134 //
135 Token::Release => "release",
136 Token::Savepoint => "savepoint",
137 Token::Show => "show",
138 Token::Index => "index",
139 Token::Begin => "begin",
140 Token::Commit => "commit",
141 Token::Rollback => "rollback",
142 Token::Drop => "drop",
143 Token::Insert => "insert",
144 Token::Values => "values",
145 Token::Limit => "limit",
146 Token::Partition => "partition",
147 Token::Tables => "tables",
148 Token::Connection => "connection",
149 Token::Publication => "publication",
150 Token::Subscription => "subscription",
151 Token::Interval => "interval",
152 // `extract` is non-reserved in PG too (it's a function the
153 // parser dispatches via context — outside that context it's
154 // a plain identifier).
155 Token::Extract => "extract",
156 Token::Offset => "offset",
157 // `to` is reserved in PG (used in many "AS … TO …" forms), so
158 // it is NOT relaxed here. Same for `from`, `where`, `as`,
159 // `select`, `not`, `and`, `or`, `null`, `true`, `false`,
160 // `create`, `table`, `into`, `on`, `order`, `by`, `having`,
161 // `group`, `distinct`, `union`, `all`, `join`, `inner`,
162 // `left`, `cross`, `outer`, `default`, `is`, `between`,
163 // `in`, `like`, `for`, `except`, `desc`, `asc`, `partition`
164 // (partial — keep partition as unreserved per modern PG).
165 _ => return None,
166 };
167 Some(s.to_string())
168}
169
170/// v7.9.22 — recognise pgvector / SPG vector-index opclass names
171/// in CREATE INDEX. SPG's HNSW already routes by query operator;
172/// the opclass is accepted for `pg_dump` compatibility (mailrs
173/// migration follow-up G5).
174/// v7.13.0 — extended to recognise PG built-in / pg_trgm opclasses
175/// (mailrs round-5 G5). These are tokens-only acceptance — SPG
176/// doesn't change index behaviour based on them.
177/// v7.37.17 (17.6 siblings) — the four PG `each` SRFs share one
178/// FROM-clause pipeline; the stored name tells the executor whether
179/// the value column keeps JSON rendering (`jsonb_each` / `json_each`)
180/// or unwraps to text (`*_each_text`).
181fn is_json_each_name(s: &str) -> bool {
182 s.eq_ignore_ascii_case("jsonb_each_text")
183 || s.eq_ignore_ascii_case("jsonb_each")
184 || s.eq_ignore_ascii_case("json_each_text")
185 || s.eq_ignore_ascii_case("json_each")
186}
187
188/// v7.38 (read01, T14) — resolve named function arguments (`argname => value`)
189/// to positional order for the `make_*` family (the AST stays positional).
190/// Positional args fill slots left-to-right; a named arg goes to its registered
191/// slot; unfilled slots default to integer 0 (PG's optional make_interval
192/// fields — the make_date/time arity is still checked at eval time).
193fn reorder_named_args(
194 fname: &str,
195 args: Vec<Expr>,
196 names: &[Option<String>],
197) -> Result<Vec<Expr>, String> {
198 let params: &[&str] = match fname.to_ascii_lowercase().as_str() {
199 "make_date" => &["year", "month", "day"],
200 "make_time" => &["hour", "min", "sec"],
201 "make_timestamp" | "make_timestamptz" => &["year", "month", "mday", "hour", "min", "sec"],
202 "make_interval" => &["years", "months", "weeks", "days", "hours", "mins", "secs"],
203 other => {
204 return Err(alloc::format!(
205 "function {other}(...) does not support named arguments"
206 ));
207 }
208 };
209 let mut slots: Vec<Option<Expr>> = (0..params.len()).map(|_| None).collect();
210 let mut next_positional = 0usize;
211 for (arg, name) in args.into_iter().zip(names.iter()) {
212 let idx = match name {
213 Some(n) => params
214 .iter()
215 .position(|p| p.eq_ignore_ascii_case(n))
216 .ok_or_else(|| alloc::format!("{fname}(...) has no argument named \"{n}\""))?,
217 None => {
218 let i = next_positional;
219 next_positional += 1;
220 i
221 }
222 };
223 if idx >= slots.len() {
224 return Err(alloc::format!("too many arguments for {fname}(...)"));
225 }
226 if slots[idx].is_some() {
227 return Err(alloc::format!(
228 "argument \"{}\" specified more than once",
229 params[idx]
230 ));
231 }
232 slots[idx] = Some(arg);
233 }
234 Ok(slots
235 .into_iter()
236 .map(|s| s.unwrap_or(Expr::Literal(Literal::Integer(0))))
237 .collect())
238}
239
240/// v7.38 (read01) — parse a lexer `Token::Numeric` source string (digits with
241/// an optional single `.`, no sign, no exponent) into `(unscaled, scale)` for
242/// `Literal::Numeric`. Returns `None` if the mantissa overflows i128.
243/// v7.39 (read01 numeric.c) — the result of expanding an `1.5e3`-style
244/// scientific literal into PG's plain NUMERIC decimal form.
245#[derive(Debug)]
246pub enum SciExpanded {
247 /// Plain decimal string ("1.5e3" → "1500", "1e-5" → "0.00001").
248 Expanded(String),
249 /// Exponent pushes the value outside PG's numeric format
250 /// (more than 131072 integer digits or 16383 fractional digits).
251 Overflow,
252 /// Not a `[±]digits[.digits]e[±]digits` literal at all.
253 NotScientific,
254}
255
256/// Expand scientific notation into a plain decimal string by moving the
257/// decimal point — no float round-trip, so the value stays exact. PG treats
258/// such literals as NUMERIC; the digit-count caps mirror PG's numeric format
259/// limits ("value overflows numeric format").
260pub fn expand_scientific_literal(s: &str) -> SciExpanded {
261 let s = s.trim();
262 let Some(epos) = s.find(['e', 'E']) else {
263 return SciExpanded::NotScientific;
264 };
265 let (mant, exp_str) = (&s[..epos], &s[epos + 1..]);
266 let Ok(exp) = exp_str.parse::<i64>() else {
267 return SciExpanded::NotScientific;
268 };
269 let (neg, mant) = match mant.strip_prefix('-') {
270 Some(r) => (true, r),
271 None => (false, mant.strip_prefix('+').unwrap_or(mant)),
272 };
273 let (int_part, frac_part) = match mant.split_once('.') {
274 Some((i, f)) => (i, f),
275 None => (mant, ""),
276 };
277 if (int_part.is_empty() && frac_part.is_empty())
278 || !int_part.bytes().all(|b| b.is_ascii_digit())
279 || !frac_part.bytes().all(|b| b.is_ascii_digit())
280 {
281 return SciExpanded::NotScientific;
282 }
283 let mut digits = String::with_capacity(int_part.len() + frac_part.len());
284 digits.push_str(int_part);
285 digits.push_str(frac_part);
286 // Decimal point position within `digits` after applying the exponent.
287 let new_point = int_part.len() as i64 + exp;
288 // PG's numeric format: up to 131072 digits before the point, 16383 after.
289 if new_point > 131_072 {
290 return SciExpanded::Overflow;
291 }
292 if (digits.len() as i64 - new_point) > 16_383 {
293 return SciExpanded::Overflow;
294 }
295 let sign = if neg { "-" } else { "" };
296 let plain = if new_point <= 0 {
297 let mut out = String::with_capacity(digits.len() + 2 + (-new_point) as usize);
298 out.push_str("0.");
299 for _ in 0..(-new_point) {
300 out.push('0');
301 }
302 out.push_str(&digits);
303 out
304 } else if (new_point as usize) >= digits.len() {
305 let mut out = digits;
306 for _ in 0..(new_point as usize - out.len()) {
307 out.push('0');
308 }
309 out
310 } else {
311 let mut out = String::with_capacity(digits.len() + 1);
312 out.push_str(&digits[..new_point as usize]);
313 out.push('.');
314 out.push_str(&digits[new_point as usize..]);
315 out
316 };
317 SciExpanded::Expanded(alloc::format!("{sign}{plain}"))
318}
319
320/// v7.39 (round 367, M20) — lower a MySQL hexadecimal binary-string
321/// literal (`0x…` / `X'…'`) onto the existing bytea cast. The hex digits
322/// are left-padded to an even count (`0x123` → byte string `01 23`, per
323/// MariaDB) and handed to the PG bytea input format (`\x…`).
324#[inline(never)]
325fn hex_literal_to_bytea_expr(hex: &str) -> Expr {
326 let padded = if hex.len() % 2 == 1 {
327 alloc::format!("0{hex}")
328 } else {
329 hex.to_string()
330 };
331 Expr::Cast {
332 expr: alloc::boxed::Box::new(Expr::Literal(Literal::String(alloc::format!(
333 "\\x{padded}"
334 )))),
335 target: CastTarget::Named("bytea".to_string()),
336 }
337}
338
339/// v7.39 (round 367, M20) — lower a MySQL bit-value literal (`b'1010'`)
340/// onto the bytea cast. The bits are read big-endian and left-padded to a
341/// whole number of bytes (`b'1010'` → one byte `0x0A`, per MariaDB).
342#[inline(never)]
343fn bits_literal_to_bytea_expr(bits: &str) -> Expr {
344 let pad = (8 - bits.len() % 8) % 8;
345 let mut hex = String::with_capacity((bits.len() + pad).div_ceil(4));
346 let padded: String = core::iter::repeat_n('0', pad).chain(bits.chars()).collect();
347 for nibble in padded.as_bytes().chunks(4) {
348 let mut v = 0u8;
349 for &b in nibble {
350 v = (v << 1) | (b - b'0');
351 }
352 hex.push(char::from_digit(u32::from(v), 16).unwrap_or('0'));
353 }
354 hex_literal_to_bytea_expr(&hex)
355}
356
357/// Resolve a lexer `Token::Numeric` into its literal. PG semantics: a dotted
358/// or over-i64 literal is exact NUMERIC; scientific notation is NUMERIC too
359/// (expanded to the plain decimal form); only a fractional depth beyond SPG's
360/// scale width (u8) falls back to double precision.
361///
362/// v7.38.19 — that sentence used to end "(recorded delta)". Measured
363/// against PG 18.4: a literal with 300 fractional digits round-trips
364/// identically on both engines, so whatever the note described is gone.
365/// It is RD-7 in `docs/RECORDED_DELTAS.md`, under "corrected by
366/// measurement" rather than under "open".
367/// Kept out of the parse_expr recursion frame — see the call site.
368#[inline(never)]
369fn numeric_token_to_literal(s: String) -> Result<Literal, String> {
370 match parse_decimal_literal(&s) {
371 Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
372 // v7.38 (read01, T3.C3) — a plain decimal too wide for i128 keeps
373 // its exact value as a NumericBig.
374 None if !s.contains(['e', 'E']) => Ok(Literal::NumericBig(s)),
375 // v7.39 (read01 numeric.c) — expand the exponent form.
376 None => match expand_scientific_literal(&s) {
377 SciExpanded::Expanded(plain) => match parse_decimal_literal(&plain) {
378 Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
379 None if plain
380 .split_once('.')
381 .is_none_or(|(_, f)| u8::try_from(f.len()).is_ok()) =>
382 {
383 Ok(Literal::NumericBig(plain))
384 }
385 None => s
386 .parse::<f64>()
387 .map(Literal::Float)
388 .map_err(|_| format!("invalid numeric literal {s:?}")),
389 },
390 SciExpanded::Overflow => Err("value overflows numeric format".to_string()),
391 SciExpanded::NotScientific => s
392 .parse::<f64>()
393 .map(Literal::Float)
394 .map_err(|_| format!("invalid numeric literal {s:?}")),
395 },
396 }
397}
398
399fn parse_decimal_literal(s: &str) -> Option<(i128, u16)> {
400 let (int_part, frac_part) = match s.split_once('.') {
401 Some((i, f)) => (i, f),
402 None => (s, ""),
403 };
404 // v7.39 (round 271) — was u8::MAX. A literal with 256 decimal
405 // places fell out of the numeric path here, which is why
406 // `pg_typeof(1e-256)` answered double precision and a plain
407 // 256-place decimal aborted the query in the big-decimal converter.
408 if frac_part.len() > u16::MAX as usize {
409 return None;
410 }
411 let mut digits = String::with_capacity(int_part.len() + frac_part.len());
412 digits.push_str(int_part);
413 digits.push_str(frac_part);
414 let mantissa: i128 = digits.parse().ok()?;
415 #[allow(clippy::cast_possible_truncation)]
416 Some((mantissa, frac_part.len() as u16))
417}
418
419/// `jsonb_to_record` / `jsonb_to_recordset` (+ `json_` variants) — the
420/// record-returning JSON functions that take a `AS alias(col type, …)`
421/// column-definition list in FROM position.
422fn is_json_to_record_name(s: &str) -> bool {
423 s.eq_ignore_ascii_case("jsonb_to_recordset")
424 || s.eq_ignore_ascii_case("jsonb_to_record")
425 // v7.39 (read01 jsonfuncs.c) — the populate family with an AS
426 // column-definition list desugars identically (the record base
427 // argument only carries the type; a non-NULL base's field
428 // defaults are a recorded delta, RD-6).
429 || s.eq_ignore_ascii_case("json_populate_record")
430 || s.eq_ignore_ascii_case("jsonb_populate_record")
431 || s.eq_ignore_ascii_case("json_populate_recordset")
432 || s.eq_ignore_ascii_case("jsonb_populate_recordset")
433 || s.eq_ignore_ascii_case("json_to_recordset")
434 || s.eq_ignore_ascii_case("json_to_record")
435}
436
437impl Parser {
438 /// Whether what follows an identifier ends an index key, which is how
439 /// an operator class is told from anything else in that position.
440 fn opclass_position_follows(next: Option<&Token>) -> bool {
441 match next {
442 // `ASC` / `DESC` have their own tokens; matching them as
443 // identifiers named "asc" / "desc" — which the first version of
444 // this did — never fires, and `(c text_pattern_ops DESC)` (which
445 // PG18.4 accepts, verified) went on failing to parse.
446 Some(Token::Comma | Token::RParen | Token::Asc | Token::Desc) => true,
447 Some(Token::Ident(w)) => {
448 w.eq_ignore_ascii_case("nulls") || w.eq_ignore_ascii_case("collate")
449 }
450 _ => false,
451 }
452 }
453}
454
455fn is_vector_opclass_name(name: &str) -> bool {
456 let lc = name.to_ascii_lowercase();
457 matches!(
458 lc.as_str(),
459 "vector_cosine_ops"
460 | "vector_l2_ops"
461 | "vector_ip_ops"
462 | "halfvec_cosine_ops"
463 | "halfvec_l2_ops"
464 | "halfvec_ip_ops"
465 | "sq8_cosine_ops"
466 | "sq8_l2_ops"
467 | "sq8_ip_ops"
468 // pg_trgm — trigram operator class. SPG's GIN index
469 // already uses tsvector tokens; trigram-style LIKE
470 // pattern matching still routes through a sequential
471 // scan, but the opclass name is accepted so PG schemas
472 // load.
473 | "gin_trgm_ops"
474 | "gist_trgm_ops"
475 // PG built-in btree opclasses occasionally appear in
476 // pg_dump output for column types with multiple
477 // sort orders (text_pattern_ops, varchar_pattern_ops,
478 // bpchar_pattern_ops).
479 | "text_pattern_ops"
480 | "varchar_pattern_ops"
481 | "bpchar_pattern_ops"
482 | "int4_ops"
483 | "int8_ops"
484 | "text_ops"
485 )
486}
487
488#[derive(Debug, Clone, PartialEq, Eq)]
489pub struct ParseError {
490 pub message: String,
491 /// Index into the token stream where parsing tripped. Not a byte offset.
492 /// v7.39 (read01 round 95) — the byte/char position is NOT stored here: a
493 /// field would grow every `Result<_, ParseError>` slot on the deeply
494 /// recursive parse stack and tip the nesting-budget frame cliff. PG's
495 /// 1-based char position is recovered on the cold error path by
496 /// [`syntax_error_position`], which re-tokenizes to map this token index.
497 pub token_pos: usize,
498}
499
500impl fmt::Display for ParseError {
501 /// v7.39 (round 322/V24) — the message ALONE. It used to be prefixed
502 /// with `parse error at token #N: `, which PG has no equivalent of:
503 /// the message bodies are already PG's verbatim (`LIMIT must not be
504 /// negative`, `invalid input syntax for type bigint: "abc"`), and the
505 /// prefix was SPG's internal token index leaking into every one of
506 /// them. `token_pos` stays a field — the wire recovers PG's 1-based
507 /// character position from it for the ErrorResponse `P`.
508 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
509 f.write_str(&self.message)
510 }
511}
512
513impl From<LexError> for ParseError {
514 fn from(e: LexError) -> Self {
515 Self {
516 message: format!("lex: {e}"),
517 token_pos: 0,
518 }
519 }
520}
521
522/// v7.9.30 — parse a single expression (no trailing junk). Used by
523/// the engine to re-hydrate stored partial-index / unique-index
524/// predicates from their canonical Display form. The same Pratt
525/// parser the statement path uses; this entry point just skips the
526/// statement dispatch.
527pub fn parse_expression(input: &str) -> Result<Expr, ParseError> {
528 let (tokens, offsets) = lexer::tokenize_with_offsets(input, lexer::Dialect::PG)
529 .map_err(|e| shape_lex_error(&e, input))?;
530 let mut p = Parser::new(tokens);
531 let expr = p
532 .parse_expr(0)
533 .and_then(|e| p.expect_eof().map(|()| e))
534 .map_err(|e| shape_syntax_error(e, input, &offsets))?;
535 Ok(expr)
536}
537
538/// Parse exactly one statement, swallow an optional trailing `;`, and require
539/// the token stream to end there. PG string semantics.
540pub fn parse_statement(input: &str) -> Result<Statement, ParseError> {
541 parse_statement_with(input, lexer::Dialect::PG)
542}
543
544/// v7.22 (round-13 T3) — dialect-aware entry: `backslash_escapes`
545/// selects MySQL-style string lexing (see `lexer::tokenize_with`).
546/// The engine threads its session flag through here.
547pub fn parse_statement_with(input: &str, dialect: lexer::Dialect) -> Result<Statement, ParseError> {
548 let (tokens, offsets, merges) =
549 lexer::tokenize_with_merges(input, dialect).map_err(|e| shape_lex_error(&e, input))?;
550 // v7.39.2 — the grammar follows "is this MySQL", the lexer follows
551 // "does backslash escape". They used to be one flag, and a session
552 // that turned escapes off lost the grammar with them.
553 let mut p = Parser::new_with_dialect(tokens, dialect.speaks_mysql)
554 .with_source(input, &offsets)
555 .with_merges(merges);
556 let stmt = (|| {
557 let stmt = p.parse_one_statement()?;
558 if matches!(p.peek(), Token::Semicolon) {
559 p.advance();
560 }
561 p.expect_eof()?;
562 Ok(stmt)
563 })()
564 .map_err(|e: ParseError| shape_syntax_error(e, input, &offsets))?;
565 Ok(stmt)
566}
567
568/// v7.39 (round 340, V56) — PG has exactly two syntax-error wordings:
569/// `syntax error at or near "<token>"` and `syntax error at end of input`
570/// (measured on 18.4 across a dozen shapes). SPG wrote its own per-site
571/// prose — `expected identifier, got Eof`, `unexpected token From in
572/// expression`, `expected end of input, got Ident("with")` — which named
573/// internal token types and, in the Debug forms, leaked the parser's own
574/// enum into a message clients read.
575///
576/// Applied once on the way out, so every construction site is covered and
577/// the token named is the one the error itself points at. Messages whose
578/// bodies are already PG's verbatim (`LIMIT must not be negative`,
579/// `invalid input syntax for type bigint: "abc"`) are left alone — those
580/// are PG's own errors, not its syntax error.
581fn shape_syntax_error(e: ParseError, input: &str, offsets: &[usize]) -> ParseError {
582 if !(e.message.starts_with("expected ") || e.message.starts_with("unexpected token ")) {
583 return e;
584 }
585 let message = match offending_lexeme(input, offsets, e.token_pos) {
586 Some(tok) => alloc::format!("syntax error at or near \"{tok}\""),
587 None => "syntax error at end of input".into(),
588 };
589 ParseError {
590 message,
591 token_pos: e.token_pos,
592 }
593}
594
595/// v7.39 (round 340, V56) — a lexer-level failure the way PG words it.
596/// Measured on 18.4: `unterminated quoted string at or near "'abc"`,
597/// `unterminated quoted identifier at or near ""abc"`, `unterminated /*
598/// comment at or near "/* x"` — the quoted part runs from the opening
599/// delimiter to the end of the input. SPG reported its own internal
600/// shape instead (`unterminated string literal at byte 7`), which named
601/// a byte offset no client can use.
602fn shape_lex_error(e: &lexer::LexError, input: &str) -> ParseError {
603 use lexer::LexErrorKind as K;
604 let from_here = input.get(e.pos..).map(str::trim_end).unwrap_or("");
605 let message = match &e.kind {
606 K::UnterminatedString => {
607 alloc::format!("unterminated quoted string at or near \"{from_here}\"")
608 }
609 K::UnterminatedQuotedIdent => {
610 alloc::format!("unterminated quoted identifier at or near \"{from_here}\"")
611 }
612 K::UnterminatedBlockComment => {
613 alloc::format!("unterminated /* comment at or near \"{from_here}\"")
614 }
615 // PG has no "unknown character" error of its own — the character
616 // is skipped and the parser reports the next token. SPG stops at
617 // the character itself and names it, which is the same shape.
618 K::UnknownChar(c) => alloc::format!("syntax error at or near \"{c}\""),
619 // The number-literal kinds already carry PG's `at or near` form.
620 other => alloc::format!(
621 "{}",
622 lexer::LexError {
623 kind: other.clone(),
624 pos: e.pos,
625 }
626 ),
627 };
628 ParseError {
629 message,
630 token_pos: 0,
631 }
632}
633
634/// The offending token exactly as it appears in the input, or `None` at
635/// end of input. PG echoes the source spelling — a lower-case `frm`
636/// reports as `frm`, not as a canonicalised keyword.
637fn offending_lexeme<'a>(input: &'a str, offsets: &[usize], token_pos: usize) -> Option<&'a str> {
638 let start = *offsets.get(token_pos)?;
639 if start >= input.len() {
640 return None;
641 }
642 let end = offsets
643 .get(token_pos + 1)
644 .copied()
645 .unwrap_or(input.len())
646 .min(input.len());
647 let seg = input.get(start..end)?.trim();
648 if seg.is_empty() {
649 return None;
650 }
651 // A quoted literal / identifier keeps its inner spaces; anything else
652 // ends at the first whitespace (the segment runs to the NEXT token's
653 // start, which may swallow a comment).
654 if seg.starts_with('\'') || seg.starts_with('"') || seg.starts_with('`') {
655 Some(seg)
656 } else {
657 seg.split_whitespace().next()
658 }
659}
660
661/// v7.39 (read01 round 95) — recover PG's 1-based CHARACTER error position for
662/// a [`ParseError::token_pos`]. Kept off the `ParseError` struct (and so off
663/// every recursive `Result` slot) to protect the nesting-budget frame cliff:
664/// this re-tokenizes `input` on the cold error path to map the failing token
665/// index to its start byte, then to a character offset. The dialect
666/// must match the parse that produced `token_pos` (it barely shifts offsets,
667/// but stay consistent). Returns `None` when the index has no offset or the
668/// byte isn't a char boundary. The wire attaches it as the ErrorResponse `P`.
669#[must_use]
670pub fn syntax_error_position(
671 input: &str,
672 dialect: lexer::Dialect,
673 token_pos: usize,
674) -> Option<usize> {
675 let (_, offsets) = lexer::tokenize_with_offsets(input, dialect).ok()?;
676 let byte_off = *offsets.get(token_pos)?;
677 if byte_off > input.len() || !input.is_char_boundary(byte_off) {
678 return None;
679 }
680 Some(input[..byte_off].chars().count() + 1)
681}
682
683struct Parser {
684 tokens: Vec<Token>,
685 pos: usize,
686 /// v7.39 (round 274) — the session's dialect, carried by the same
687 /// signal that drives string-literal escaping: `SET sql_mode` (only
688 /// MySQL clients and mysqldump preambles emit it) turns it on,
689 /// `SET standard_conforming_strings` (every pg_dump preamble) turns
690 /// it off. Needed here because the two dialects disagree about what
691 /// `REAL` means — see the type mapping below.
692 mysql_dialect: bool,
693 /// v7.30.2 (mailrs round-25 ask 2) — live nesting depth of the
694 /// mutually recursive expr/select parsers. Bounded so a deeply
695 /// nested input returns a parse error instead of overflowing
696 /// the stack (embed hosts die on overflow — it is an abort,
697 /// not a catchable error).
698 nest_depth: usize,
699 /// TABLESAMPLE lowering channel: the table-ref parser pushes a
700 /// `random() < p/100` predicate here; the enclosing SELECT
701 /// drains the list after its WHERE parses and ANDs the
702 /// predicates in. parse_bare_select save/restores around its
703 /// FROM+WHERE so nested selects only drain their own.
704 pending_sample_preds: Vec<Expr>,
705 /// v7.38.19 — the target of a `SELECT … INTO <table>`, carried out
706 /// of `parse_bare_select` (which returns a `SelectStatement` and has
707 /// nowhere to put it) to the caller that lowers the pair to the CTAS
708 /// node. `bool` is `TEMP`.
709 pending_select_into: Option<(String, bool)>,
710 /// v7.39 (round 691) — collation lowering channel, the same shape as
711 /// `pending_sample_preds` above. `expr COLLATE "name"` is ORDERING
712 /// information, and `ast::OrderBy` is where this parser keeps ordering
713 /// information (`desc`, `nulls_first`); the alternative — a new `Expr`
714 /// variant — puts a new arm on `eval_expr`, which this repo has
715 /// measured to overflow the debug stack. So while an ORDER BY KEY is
716 /// being parsed the postfix loop drops the name here instead of
717 /// refusing it, and the key's parser takes it.
718 ///
719 /// Only inside an ORDER BY key: everywhere else an unperformable
720 /// collation still errors, because accepting one at a COMPARISON and
721 /// ignoring it is the defect F36 exists to close.
722 in_order_by_key: bool,
723 order_key_collation: Option<String>,
724 /// POSITION(sub IN str) — while parsing the needle, the IN
725 /// keyword is the argument separator, not a membership test.
726 /// The postfix loop leaves IN unconsumed when this is set.
727 suppress_in_tail: bool,
728 /// Index of the token the last `advance()` returned — see
729 /// [`Parser::consumed_pos`].
730 last_consumed: usize,
731 /// v7.39 (round 506) — the statement's own text and the byte each token
732 /// starts at, so a MySQL projection item can report the SOURCE TEXT
733 /// MariaDB reports: `SELECT a + b` names its column `a + b`,
734 /// spacing and all. Only filled for a MySQL session — a PG one names
735 /// columns from the parsed shape and pays nothing for this.
736 src: Option<(String, Vec<usize>)>,
737 /// v7.39.3 — (token index, first-segment byte length) for every
738 /// string literal the lexer built by implicit concatenation.
739 merges: Vec<(usize, usize)>,
740}
741
742/// Max expr/select parser nesting (parens, subqueries, CASE, …).
743/// Real SQL nests a few dozen levels at the extreme. Each nesting level
744/// costs a parse_expr→parse_unary→parse_atom frame chain, so the budget
745/// exists to turn a deep statement into a catchable parse ERROR: a stack
746/// overflow is an abort, and in the server it does not fail one query, it
747/// takes the process down and every other connection with it.
748///
749/// v7.39 (round 507) — measured, because the figure here used to be a
750/// guess ("over 10 KiB in debug … comfortably inside a 2 MiB worker stack
751/// in BOTH debug and release"), and the debug half of that is wrong by
752/// more than an order of magnitude:
753///
754/// * RELEASE, on a 2 MiB worker stack: every recursive shape reaches
755/// this budget and errors. Verified against a live server for nested
756/// derived tables, parens, calls, CASE, IN-subqueries, scalar
757/// subqueries, NOT and unary minus — the server stayed up through all
758/// of them. This is the contract that matters, and it holds.
759/// * DEBUG: nested derived tables cost roughly 235 KiB of stack PER
760/// LEVEL, so parsing alone aborts around 35 levels on an 8 MiB stack
761/// and executing aborts around 8 inside a test thread. The budget is
762/// simply unreachable there, which is why a deep-nesting test has to
763/// ask for a large stack of its own — see `nesting_budget_errors_at`
764/// in the parser tests.
765/// v7.39 (round 541) — the pg_catalog relations SPG synthesises, in
766/// one place.
767///
768/// There were two copies of this fact: a curated list, used for BARE
769/// names, and — in `try_peek_meta_qualified` — no list at all, which
770/// rewrote `pg_catalog.<anything>` to `__spg_pg_<anything>` and left
771/// the engine to complain about a view it could not materialise. So
772/// writing the schema qualifier CHANGED THE ANSWER: `pg_stat_activity`
773/// had rows, `pg_catalog.pg_stat_activity` was an error.
774///
775/// PG puts `pg_catalog` at the implicit front of every search_path, so
776/// the two spellings name the same relation and must resolve the same
777/// way. Names NOT here (`pg_stat_activity`, `pg_locks`,
778/// `pg_stat_statements`, `pg_statio_user_tables`) route through the
779/// meta_view_result path under their own names and must not be
780/// rewritten; a name that is neither reaches the ordinary resolver,
781/// which reports that the relation does not exist — PG's answer.
782const SYNTHESISED_PG_CATALOGS: &[&str] = &[
783 "pg_am",
784 "pg_attrdef",
785 "pg_attribute",
786 "pg_cast",
787 "pg_db_role_setting",
788 "pg_conversion",
789 "pg_default_acl",
790 "pg_shadow",
791 "pg_sequences",
792 "pg_range",
793 "pg_partitioned_table",
794 "pg_language",
795 "pg_group",
796 "pg_authid",
797 "pg_class",
798 "pg_collation",
799 "pg_constraint",
800 "pg_database",
801 "pg_depend",
802 "pg_amop",
803 "pg_amproc",
804 "pg_opclass",
805 "pg_opfamily",
806 // v7.39 (read01 round 50) — COMMENT ON store, PG's pg_description.
807 "pg_description",
808 "pg_enum",
809 "pg_extension",
810 // v7.39 (round 541) — pg_dump reads it for every relation of kind
811 // 'f'. SPG has no foreign tables, so it is empty, which is also
812 // what PG reports on a database that has none.
813 "pg_foreign_table",
814 // v7.39 (round 541) — the empty-by-truth family; see
815 // EMPTY_PG_CATALOGS in spg-engine::system_catalog.
816 "pg_event_trigger",
817 "pg_file_settings",
818 "pg_foreign_data_wrapper",
819 "pg_foreign_server",
820 "pg_hba_file_rules",
821 "pg_ident_file_mappings",
822 "pg_init_privs",
823 "pg_parameter_acl",
824 "pg_prepared_xacts",
825 "pg_publication_namespace",
826 "pg_publication_rel",
827 "pg_publication_tables",
828 "pg_replication_origin",
829 "pg_replication_origin_status",
830 "pg_seclabel",
831 "pg_seclabels",
832 "pg_shdepend",
833 "pg_shdescription",
834 "pg_shmem_allocations",
835 "pg_shmem_allocations_numa",
836 "pg_shseclabel",
837 "pg_statistic_ext_data",
838 "pg_stats_ext",
839 "pg_stats_ext_exprs",
840 "pg_subscription_rel",
841 "pg_transform",
842 "pg_user_mapping",
843 "pg_user_mappings",
844 "pg_index",
845 "pg_indexes",
846 "pg_inherits",
847 // v7.39 (round 650) — the text-search catalogs SPG can fill
848 // honestly. `pg_ts_config_map` is deliberately NOT here: it maps
849 // token types to dictionaries and SPG has no token-type model,
850 // the same gap that leaves `ts_token_type` / `ts_debug` unbuilt.
851 "pg_ts_config",
852 "pg_ts_config_map",
853 "pg_ts_dict",
854 "pg_ts_parser",
855 "pg_ts_template",
856 "pg_matviews",
857 "pg_namespace",
858 // v7.39 (round 621)
859 "pg_operator",
860 "pg_policies",
861 "pg_policy",
862 "pg_proc",
863 "pg_publication",
864 "pg_replication_slots",
865 "pg_roles",
866 // v7.39 (round 143) — the rewrite-rule listing view.
867 // v7.39 (round 312) — and the rule catalogue itself, which
868 // `pg_get_ruledef(oid)` resolves against.
869 "pg_rewrite",
870 "pg_rules",
871 "pg_sequence",
872 "pg_settings",
873 "pg_stat_archiver",
874 "pg_stat_bgwriter",
875 "pg_stat_checkpointer",
876 "pg_stat_database",
877 "pg_stat_io",
878 "pg_stat_progress_analyze",
879 "pg_auth_members",
880 "pg_stat_progress_create_index",
881 "pg_stat_progress_vacuum",
882 "pg_stat_replication",
883 "pg_stat_slru",
884 "pg_stat_subscription_stats",
885 "pg_stat_user_functions",
886 "pg_stat_user_indexes",
887 "pg_stat_user_tables",
888 "pg_stat_wal",
889 "pg_prepared_statements",
890 "pg_largeobject",
891 "pg_largeobject_metadata",
892 "pg_statistic",
893 "pg_statistic_ext",
894 // v7.38.18 — the readable view over pg_statistic.
895 "pg_stats",
896 "pg_subscription",
897 "pg_tables",
898 "pg_tablespace",
899 // v7.39 (round 502) — the timezone catalogues. SPG resolved
900 // named zones correctly but could not list them, so a client
901 // populating a timezone picker got "relation does not exist".
902 "pg_timezone_abbrevs",
903 "pg_timezone_names",
904 "pg_trigger",
905 "pg_type",
906 "pg_user",
907 "pg_views",
908];
909
910const MAX_NEST_DEPTH: usize = 64;
911
912/// Stack accounting for the nesting budget, test-only.
913///
914/// `MAX_NEST_DEPTH` is a fixed count calibrated against a frame size
915/// that MOVES: a compiler upgrade grew the parser's debug frames and
916/// silently ate the margin until `nesting_budget_errors_cleanly` went
917/// from erroring cleanly to aborting on a stack overflow. A count
918/// cannot notice that on its own, so the budget is measured here and
919/// held to a ceiling.
920///
921/// The reading has to come from a helper whose OWN frame is the same at
922/// every call: debug slot placement does not follow source order, so a
923/// local's address inside the function under test is not that
924/// function's frame boundary. Two earlier probes were wrong that way —
925/// one read `&self.nest_depth`, which is the `Parser`'s address and
926/// never moves at all.
927#[cfg(test)]
928mod frame_meter {
929 extern crate std;
930 use std::cell::Cell;
931
932 // Per-THREAD, not global. `cargo test` runs tests in parallel and
933 // plenty of them parse nested expressions, so shared statics get
934 // stack addresses from several threads at once and the subtraction
935 // below turns into noise — it read 229,772 bytes per level that way,
936 // while passing when the test was run on its own.
937 std::thread_local! {
938 static AT_LO: Cell<usize> = const { Cell::new(0) };
939 static AT_HI: Cell<usize> = const { Cell::new(0) };
940 }
941
942 pub(super) const SAMPLE_LO: usize = 4;
943 pub(super) const SAMPLE_HI: usize = 24;
944
945 #[inline(never)]
946 pub(super) fn record(depth: usize) {
947 let anchor = 0u8;
948 let at = core::ptr::from_ref(&anchor) as usize;
949 if depth == SAMPLE_LO {
950 AT_LO.with(|c| c.set(at));
951 } else if depth == SAMPLE_HI {
952 AT_HI.with(|c| c.set(at));
953 }
954 }
955
956 /// Bytes of stack one nesting level costs, averaged over the span.
957 pub(super) fn bytes_per_level() -> usize {
958 let lo = AT_LO.with(Cell::get);
959 let hi = AT_HI.with(Cell::get);
960 assert!(lo > 0 && hi > 0, "meter never sampled: lo={lo} hi={hi}");
961 assert!(lo > hi, "stack grew upwards? lo={lo} hi={hi}");
962 (lo - hi) / (SAMPLE_HI - SAMPLE_LO)
963 }
964
965 pub(super) fn reset() {
966 AT_LO.with(|c| c.set(0));
967 AT_HI.with(|c| c.set(0));
968 }
969}
970
971/// v7.39 (read01 geo_ops.c) — prefix `@@` desugar target, out-of-line so
972/// the constructor's temporaries stay off `parse_unary`'s recursion frame.
973#[inline(never)]
974fn build_center_call(e: Expr) -> Expr {
975 Expr::FunctionCall {
976 name: alloc::string::String::from("center"),
977 args: alloc::vec![e],
978 }
979}
980
981/// Max consecutive binary operators at ONE precedence level
982/// (`a OR b OR c …`, `1+1+1…`). The chain builds iteratively at
983/// parse time but evaluates and drops recursively — depth beyond
984/// this overflows 2 MiB worker stacks (debug eval frames run
985/// multiple KiB). `IN (…)` lists are flat and unaffected.
986const MAX_BINARY_CHAIN: usize = 256;
987
988/// v7.22 (round-13 gap 5) — the kind keyword after `CONSTRAINT
989/// <name>` in a CREATE TABLE column list. FOREIGN KEY is not here:
990/// it keeps its dedicated path (`parse_table_level_fk`).
991enum NamedTableConstraintKind {
992 Check,
993 Unique,
994 PrimaryKey,
995 Exclude,
996}
997
998impl Parser {
999 fn new(tokens: Vec<Token>) -> Self {
1000 Self::new_with_dialect(tokens, false)
1001 }
1002
1003 fn new_with_dialect(tokens: Vec<Token>, mysql_dialect: bool) -> Self {
1004 Self {
1005 tokens,
1006 mysql_dialect,
1007 in_order_by_key: false,
1008 order_key_collation: None,
1009 pos: 0,
1010 nest_depth: 0,
1011 pending_sample_preds: Vec::new(),
1012 pending_select_into: None,
1013 suppress_in_tail: false,
1014 last_consumed: 0,
1015 src: None,
1016 merges: Vec::new(),
1017 }
1018 }
1019
1020 /// Hand the parser the text it is parsing, for [`Parser::source_span`].
1021 fn with_source(mut self, input: &str, offsets: &[usize]) -> Self {
1022 if self.mysql_dialect {
1023 self.src = Some((input.to_string(), offsets.to_vec()));
1024 }
1025 self
1026 }
1027
1028 /// v7.39.3 — the implicit-concatenation log from the lexer, so a
1029 /// merged literal can still be LABELLED by its first segment the way
1030 /// MySQL 9.7.2 labels it.
1031 fn with_merges(mut self, merges: Vec<(usize, usize)>) -> Self {
1032 if self.mysql_dialect {
1033 self.merges = merges;
1034 }
1035 self
1036 }
1037
1038 /// The byte length of the first segment of the literal at `tok`, when
1039 /// that literal was built by implicit concatenation.
1040 fn merged_first_len(&self, tok: usize) -> Option<usize> {
1041 self.merges
1042 .iter()
1043 .find(|(k, _)| *k == tok)
1044 .map(|(_, len)| *len)
1045 }
1046
1047 /// The source text spanning tokens `start ..= end`, trimmed.
1048 ///
1049 /// The offsets are token STARTS, so the span runs to the start of the
1050 /// token after `end` and gives back the whitespace between them —
1051 /// trimming is what makes `a + b FROM t` end at `b`.
1052 fn source_span(&self, start: usize, end: usize) -> Option<&str> {
1053 let (text, offsets) = self.src.as_ref()?;
1054 let from = *offsets.get(start)?;
1055 let to = *offsets.get(end + 1)?;
1056 text.get(from..to).map(str::trim_end)
1057 }
1058
1059 /// v7.30.2 (mailrs round-25 ask 2) — bump the expr/select
1060 /// nesting depth, erroring out cleanly past the budget.
1061 fn enter_nested(&mut self) -> Result<(), ParseError> {
1062 self.nest_depth += 1;
1063 #[cfg(test)]
1064 frame_meter::record(self.nest_depth);
1065 if self.nest_depth > MAX_NEST_DEPTH {
1066 self.nest_depth -= 1;
1067 return Err(self.err(alloc::format!(
1068 "statement nests deeper than {MAX_NEST_DEPTH} levels"
1069 )));
1070 }
1071 Ok(())
1072 }
1073
1074 fn peek(&self) -> &Token {
1075 // tokens always ends with Eof; pos is clamped in advance().
1076 &self.tokens[self.pos]
1077 }
1078
1079 fn advance(&mut self) -> Token {
1080 let t = mem::replace(&mut self.tokens[self.pos], Token::Eof);
1081 self.last_consumed = self.pos;
1082 if self.pos + 1 < self.tokens.len() {
1083 self.pos += 1;
1084 }
1085 t
1086 }
1087
1088 /// v7.39 (round 340, V56) — the index of the token `advance()` just
1089 /// returned. It was computed as `pos - 1`, which is wrong at both
1090 /// ends: `advance()` parks on the final Eof rather than running off
1091 /// the end (so `SELECT * FROM` named `FROM` where PG says `at end of
1092 /// input`), and after backtracking `pos` is no longer one past the
1093 /// token that failed. Recorded by `advance()` itself instead.
1094 const fn consumed_pos(&self) -> usize {
1095 self.last_consumed
1096 }
1097
1098 fn err(&self, message: String) -> ParseError {
1099 ParseError {
1100 message,
1101 token_pos: self.pos,
1102 }
1103 }
1104
1105 /// v7.39.3 — like [`Parser::err`] but pointing at a token the caller
1106 /// names rather than at the current one.
1107 ///
1108 /// The position is not decoration on the MySQL wire: its syntax-error
1109 /// sentence quotes the source from there to the end of the statement,
1110 /// so an error raised after the construct it is about quotes nothing.
1111 fn err_at(&self, token_pos: usize, message: String) -> ParseError {
1112 ParseError { message, token_pos }
1113 }
1114
1115 fn expect_eof(&self) -> Result<(), ParseError> {
1116 if matches!(self.peek(), Token::Eof) {
1117 Ok(())
1118 } else {
1119 Err(self.err(format!("expected end of input, got {:?}", self.peek())))
1120 }
1121 }
1122
1123 /// v7.14.0 — swallow every token up to (but not including) the
1124 /// next semicolon / EOF. Used by the dump-noise dispatcher
1125 /// to consume `COMMENT ON …`, `GRANT …`, `LOCK TABLES …`,
1126 /// etc. without modeling each grammar.
1127 /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
1128 /// Kinds SPG stores by name: TABLE / COLUMN / INDEX / VIEW / SEQUENCE /
1129 /// SCHEMA / TYPE / DATABASE / FUNCTION. Anything else (and the multi-word
1130 /// `CONSTRAINT c ON t` / `MATERIALIZED VIEW` forms) keeps the old
1131 /// swallow-as-no-op behaviour so a pg_dump tail still loads.
1132 fn parse_comment_on(&mut self) -> Result<Statement, ParseError> {
1133 let start = self.pos;
1134 self.advance(); // COMMENT
1135 if !matches!(self.peek(), Token::On) {
1136 self.pos = start;
1137 self.consume_until_statement_boundary();
1138 return Ok(Statement::Empty);
1139 }
1140 self.advance(); // ON
1141 let kind = match self.peek() {
1142 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
1143 Token::Table => "table".into(),
1144 _ => {
1145 self.consume_until_statement_boundary();
1146 return Ok(Statement::Empty);
1147 }
1148 };
1149 if !matches!(
1150 kind.as_str(),
1151 "table"
1152 | "column"
1153 | "index"
1154 | "view"
1155 | "sequence"
1156 | "schema"
1157 | "type"
1158 | "database"
1159 | "function"
1160 ) {
1161 self.consume_until_statement_boundary();
1162 return Ok(Statement::Empty);
1163 }
1164 self.advance(); // the kind keyword
1165 // The object name. ⚠️ `expect_ident_like` strips a leading
1166 // `<schema>.` qualifier and returns only the trailing ident (SPG is
1167 // single-schema), which would turn `COMMENT ON COLUMN t.c` into just
1168 // `c`. Read the dotted parts from raw tokens instead, then let a
1169 // 3-part `schema.t.c` drop its leading schema like everywhere else.
1170 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
1171 loop {
1172 match self.advance() {
1173 Token::Ident(s) | Token::QuotedIdent(s) => parts.push(s),
1174 other if unreserved_keyword_text(&other).is_some() => {
1175 parts.push(unreserved_keyword_text(&other).unwrap());
1176 }
1177 other => {
1178 return Err(ParseError {
1179 message: alloc::format!("expected identifier, got {other:?}"),
1180 token_pos: self.consumed_pos(),
1181 });
1182 }
1183 }
1184 if matches!(self.peek(), Token::Dot) {
1185 self.advance();
1186 } else {
1187 break;
1188 }
1189 }
1190 // COLUMN wants `table.column`; every other kind wants a bare name.
1191 let want = if kind == "column" { 2 } else { 1 };
1192 while parts.len() > want {
1193 parts.remove(0);
1194 }
1195 let name = parts.join(".");
1196 // v7.39 (round 710) — `COMMENT ON FUNCTION f(int, text) IS …`.
1197 // pg_dump writes the SIGNATURE, and the paren list was a syntax
1198 // error here — a dump carrying one function comment failed to
1199 // restore. The list is consumed (the comment store keys by name;
1200 // overload-precise comments are the function-predicate follow-up).
1201 if matches!(self.peek(), Token::LParen)
1202 && matches!(
1203 kind.as_str(),
1204 "function" | "procedure" | "aggregate" | "routine"
1205 )
1206 {
1207 let mut depth = 0usize;
1208 loop {
1209 match self.advance() {
1210 Token::LParen => depth += 1,
1211 Token::RParen => {
1212 depth -= 1;
1213 if depth == 0 {
1214 break;
1215 }
1216 }
1217 Token::Eof => {
1218 return Err(self.err(alloc::string::String::from(
1219 "unterminated argument list in COMMENT ON",
1220 )));
1221 }
1222 _ => {}
1223 }
1224 }
1225 }
1226 // `IS`
1227 if !matches!(self.peek(), Token::Is) {
1228 self.expect_keyword_ident("is")?;
1229 } else {
1230 self.advance();
1231 }
1232 let comment = match self.peek() {
1233 Token::Null => {
1234 self.advance();
1235 None
1236 }
1237 _ => Some(self.expect_string_literal()?),
1238 };
1239 Ok(Statement::CommentOn {
1240 kind,
1241 name,
1242 comment,
1243 })
1244 }
1245
1246 /// v7.39 (read01 round 57) — `GRANT <privs> ON <obj> TO <roles> [WITH GRANT
1247 /// OPTION]` / `REVOKE [GRANT OPTION FOR] <privs> ON <obj> FROM <roles>
1248 /// [CASCADE|RESTRICT]`.
1249 ///
1250 /// TABLE privileges are the real ones (stored, enforced, introspectable).
1251 /// Every other object class — SCHEMA / SEQUENCE / DATABASE / FUNCTION / …,
1252 /// and the no-ON `GRANT role TO role` membership form — parses into
1253 /// `GrantObject::Other` and no-ops in the engine, so a pg_dump that grants
1254 /// on them still restores.
1255 fn parse_grant_or_revoke(&mut self, grant: bool) -> Result<Statement, ParseError> {
1256 self.advance(); // GRANT / REVOKE
1257 // REVOKE's optional `GRANT OPTION FOR` prefix.
1258 let mut grant_option = false;
1259 if !grant
1260 && self.peek_keyword_ident("grant")
1261 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("option"))
1262 {
1263 self.advance(); // GRANT
1264 self.advance(); // OPTION
1265 self.expect_keyword_ident("for")?;
1266 grant_option = true;
1267 }
1268 // The privilege list: `ALL [PRIVILEGES] [(cols)]`, or comma-separated
1269 // words each with an optional COLUMN list.
1270 let mut privileges: Vec<GrantPriv> = Vec::new();
1271 if matches!(self.peek(), Token::All) {
1272 self.advance();
1273 if self.peek_keyword_ident("privileges") {
1274 self.advance();
1275 }
1276 // `GRANT ALL (col) ON t TO r` — every column privilege, on that
1277 // column only.
1278 if matches!(self.peek(), Token::LParen) {
1279 let columns = self.parse_grant_column_list()?;
1280 privileges.push(GrantPriv {
1281 word: "ALL".into(),
1282 columns,
1283 });
1284 }
1285 } else {
1286 loop {
1287 // SELECT and INSERT lex as reserved tokens, so they never
1288 // reach `expect_ident_like` as plain idents; the rest
1289 // (UPDATE / DELETE / TRUNCATE / REFERENCES / TRIGGER /
1290 // MAINTAIN) are ordinary identifiers.
1291 let w = match self.peek() {
1292 Token::Select => {
1293 self.advance();
1294 "SELECT".to_string()
1295 }
1296 Token::Insert => {
1297 self.advance();
1298 "INSERT".to_string()
1299 }
1300 // v7.39 (read01 round 60) — CREATE is a privilege word on a
1301 // schema / database, and it lexes as a reserved token.
1302 Token::Create => {
1303 self.advance();
1304 "CREATE".to_string()
1305 }
1306 // NOT upper-cased: in the no-ON shape (`GRANT devs TO
1307 // alice`) these "privilege words" are ROLE NAMES, and a
1308 // role name is case-sensitive. `priv_from_word` folds case
1309 // itself when they really are privileges.
1310 _ => self.expect_ident_like()?,
1311 };
1312 // v7.39 (read01 round 59) — the optional per-privilege COLUMN
1313 // list: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
1314 let columns = if matches!(self.peek(), Token::LParen) {
1315 self.parse_grant_column_list()?
1316 } else {
1317 Vec::new()
1318 };
1319 privileges.push(GrantPriv { word: w, columns });
1320 if matches!(self.peek(), Token::Comma) {
1321 self.advance();
1322 } else {
1323 break;
1324 }
1325 }
1326 }
1327 // v7.39 (read01 round 58) — no ON clause at all = `GRANT devs TO alice`:
1328 // role MEMBERSHIP. The words parsed as "privileges" are the role names.
1329 if !matches!(self.peek(), Token::On) {
1330 let roles: Vec<String> = core::mem::take(&mut privileges)
1331 .into_iter()
1332 .map(|p| p.word)
1333 .collect();
1334 let grantees = self.parse_grantee_list(grant)?;
1335 // `WITH ADMIN OPTION` / `GRANTED BY x` — accepted, ignored (SPG has
1336 // no admin-option layer: a member cannot re-grant).
1337 self.consume_until_statement_boundary();
1338 return Ok(finish_grant(
1339 grant,
1340 GrantStatement {
1341 privileges: Vec::new(),
1342 object: GrantObject::Roles(roles),
1343 grantees,
1344 grant_option,
1345 },
1346 ));
1347 }
1348 self.advance(); // ON
1349 // An optional object-class keyword. `TABLE` (or no keyword at all) is
1350 // the enforced case; anything else parses and no-ops.
1351 let mut class = "TABLE";
1352 match self.peek() {
1353 Token::Table => {
1354 self.advance();
1355 }
1356 Token::All => {
1357 // v7.39 (read01 round 61) — `ALL TABLES IN SCHEMA x` expands to
1358 // every table at GRANT time, like PG. `ALL SEQUENCES/FUNCTIONS
1359 // IN SCHEMA` stay no-ops and keep their own object class.
1360 self.advance(); // ALL
1361 let kind = match self.peek() {
1362 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
1363 // TABLES has its own token (SHOW TABLES owns it).
1364 Token::Tables | Token::Table => "tables".to_string(),
1365 _ => String::new(),
1366 };
1367 if !kind.is_empty() {
1368 self.advance();
1369 }
1370 // `IN SCHEMA <name>`
1371 if matches!(self.peek(), Token::In) {
1372 self.advance();
1373 if self.peek_keyword_ident("schema") {
1374 self.advance();
1375 let _schema = self.expect_ident_like()?;
1376 }
1377 }
1378 if kind != "tables" {
1379 self.consume_until_statement_boundary();
1380 return Ok(finish_grant(
1381 grant,
1382 GrantStatement {
1383 privileges,
1384 object: GrantObject::Other("ALL … IN SCHEMA".into()),
1385 grantees: Vec::new(),
1386 grant_option,
1387 },
1388 ));
1389 }
1390 let grantees = self.parse_grantee_list(grant)?;
1391 self.consume_until_statement_boundary();
1392 return Ok(finish_grant(
1393 grant,
1394 GrantStatement {
1395 privileges,
1396 object: GrantObject::AllTablesInSchema,
1397 grantees,
1398 grant_option,
1399 },
1400 ));
1401 }
1402 Token::Ident(w) | Token::QuotedIdent(w) => {
1403 let lc = w.to_ascii_lowercase();
1404 // v7.39 (read01 round 60) — SEQUENCE / SCHEMA / DATABASE are
1405 // real objects with real ACLs now.
1406 if matches!(lc.as_str(), "sequence" | "schema" | "database") {
1407 self.advance();
1408 let mut names: Vec<String> = Vec::new();
1409 loop {
1410 let mut parts: Vec<String> = Vec::new();
1411 loop {
1412 parts.push(self.expect_ident_like()?);
1413 if matches!(self.peek(), Token::Dot) {
1414 self.advance();
1415 } else {
1416 break;
1417 }
1418 }
1419 names.push(parts.pop().expect("at least one part"));
1420 if matches!(self.peek(), Token::Comma) {
1421 self.advance();
1422 } else {
1423 break;
1424 }
1425 }
1426 let grantees = self.parse_grantee_list(grant)?;
1427 let mut grant_option = grant_option;
1428 if grant && self.peek_keyword_ident("with") {
1429 self.advance();
1430 self.expect_keyword_ident("grant")?;
1431 self.expect_keyword_ident("option")?;
1432 grant_option = true;
1433 }
1434 self.consume_until_statement_boundary();
1435 let object = match lc.as_str() {
1436 "sequence" => GrantObject::Sequences(names),
1437 "schema" => GrantObject::Schemas(names),
1438 _ => GrantObject::Databases(names),
1439 };
1440 return Ok(finish_grant(
1441 grant,
1442 GrantStatement {
1443 privileges,
1444 object,
1445 grantees,
1446 grant_option,
1447 },
1448 ));
1449 }
1450 // v7.39 (read01 round 61) — `ON FUNCTION f(int)` is real. SPG
1451 // keys functions by NAME, so the argument list parses and is
1452 // dropped (an overload set shares one ACL — recorded residual).
1453 if matches!(lc.as_str(), "function" | "procedure" | "routine") {
1454 self.advance();
1455 let mut names: Vec<(String, Option<Vec<String>>)> = Vec::new();
1456 loop {
1457 let mut parts: Vec<String> = Vec::new();
1458 loop {
1459 parts.push(self.expect_ident_like()?);
1460 if matches!(self.peek(), Token::Dot) {
1461 self.advance();
1462 } else {
1463 break;
1464 }
1465 }
1466 let fname = parts.pop().expect("at least one part");
1467 // v7.39 (read01 round 62) — the signature picks the
1468 // overload, so it is captured.
1469 let sig = if matches!(self.peek(), Token::LParen) {
1470 Some(self.parse_function_signature_types()?)
1471 } else {
1472 None
1473 };
1474 names.push((fname, sig));
1475 if matches!(self.peek(), Token::Comma) {
1476 self.advance();
1477 } else {
1478 break;
1479 }
1480 }
1481 let grantees = self.parse_grantee_list(grant)?;
1482 self.consume_until_statement_boundary();
1483 return Ok(finish_grant(
1484 grant,
1485 GrantStatement {
1486 privileges,
1487 object: GrantObject::Functions(names),
1488 grantees,
1489 grant_option,
1490 },
1491 ));
1492 }
1493 if matches!(
1494 lc.as_str(),
1495 "type"
1496 | "domain"
1497 | "language"
1498 | "tablespace"
1499 | "large"
1500 | "foreign"
1501 | "parameter"
1502 ) {
1503 self.consume_until_statement_boundary();
1504 return Ok(finish_grant(
1505 grant,
1506 GrantStatement {
1507 privileges,
1508 object: GrantObject::Other(lc.to_ascii_uppercase()),
1509 grantees: Vec::new(),
1510 grant_option,
1511 },
1512 ));
1513 }
1514 class = "TABLE";
1515 }
1516 _ => {}
1517 }
1518 let _ = class;
1519 // The table list. Schema-qualified names drop their qualifier (SPG is
1520 // single-schema) — but read the dotted parts from raw tokens, since
1521 // `expect_ident_like` would silently swallow the leading part.
1522 let mut tables: Vec<String> = Vec::new();
1523 loop {
1524 let mut parts: Vec<String> = Vec::new();
1525 loop {
1526 parts.push(self.expect_ident_like()?);
1527 if matches!(self.peek(), Token::Dot) {
1528 self.advance();
1529 } else {
1530 break;
1531 }
1532 }
1533 tables.push(parts.pop().expect("at least one part"));
1534 if matches!(self.peek(), Token::Comma) {
1535 self.advance();
1536 } else {
1537 break;
1538 }
1539 }
1540 let grantees = self.parse_grantee_list(grant)?;
1541 if grant && self.peek_keyword_ident("with") {
1542 self.advance();
1543 self.expect_keyword_ident("grant")?;
1544 self.expect_keyword_ident("option")?;
1545 grant_option = true;
1546 }
1547 // REVOKE's trailing CASCADE / RESTRICT — SPG has no dependent grants
1548 // to cascade to (no re-granting), so both are accepted and ignored.
1549 if !grant && (self.peek_keyword_ident("cascade") || self.peek_keyword_ident("restrict")) {
1550 self.advance();
1551 }
1552 Ok(finish_grant(
1553 grant,
1554 GrantStatement {
1555 privileges,
1556 object: GrantObject::Tables(tables),
1557 grantees,
1558 grant_option,
1559 },
1560 ))
1561 }
1562
1563 /// v7.39 (read01 round 62) — the argument TYPES in a function signature:
1564 /// `(int, text)` or `(x int, y text)` (PG accepts either). Returns the type
1565 /// words; the caller normalises them into a signature key.
1566 fn parse_function_signature_types(&mut self) -> Result<Vec<String>, ParseError> {
1567 self.advance(); // (
1568 let mut types: Vec<String> = Vec::new();
1569 if matches!(self.peek(), Token::RParen) {
1570 self.advance();
1571 return Ok(types);
1572 }
1573 loop {
1574 // Collect the words of one argument up to a comma / close paren.
1575 let mut words: Vec<String> = Vec::new();
1576 loop {
1577 match self.peek() {
1578 Token::Comma | Token::RParen | Token::Eof => break,
1579 _ => {}
1580 }
1581 let tok = self.advance();
1582 match tok {
1583 Token::Ident(w) | Token::QuotedIdent(w) => words.push(w),
1584 other => {
1585 if let Some(w) = unreserved_keyword_text(&other) {
1586 words.push(w);
1587 }
1588 }
1589 }
1590 }
1591 // `name TYPE` or a bare `TYPE`. Several of PG's type names are
1592 // themselves several words (`double precision`, `character
1593 // varying`, `timestamp with time zone`), so "two words means the
1594 // first is a parameter name" reads the type off `f(double
1595 // precision)` as `precision`. v7.39 (round 282): recognise the
1596 // multi-word spellings first — a leading word that STARTS one of
1597 // them is part of the type, not a name.
1598 let joined = words.join(" ");
1599 let ty = if words.len() >= 2 && is_multiword_type_phrase(&joined) {
1600 joined
1601 } else if words.len() >= 2 {
1602 words[1..].join(" ")
1603 } else {
1604 words.first().cloned().unwrap_or_default()
1605 };
1606 types.push(ty);
1607 if matches!(self.peek(), Token::Comma) {
1608 self.advance();
1609 } else {
1610 break;
1611 }
1612 }
1613 if matches!(self.peek(), Token::RParen) {
1614 self.advance();
1615 }
1616 Ok(types)
1617 }
1618
1619 /// v7.39 (read01 round 59) — `( col, col, … )` after a privilege word.
1620 fn parse_grant_column_list(&mut self) -> Result<Vec<String>, ParseError> {
1621 self.advance(); // (
1622 let mut cols = Vec::new();
1623 loop {
1624 cols.push(self.expect_ident_like()?);
1625 if matches!(self.peek(), Token::Comma) {
1626 self.advance();
1627 } else {
1628 break;
1629 }
1630 }
1631 if !matches!(self.peek(), Token::RParen) {
1632 return Err(self.err(alloc::format!(
1633 "expected ')' to close the column list, got {:?}",
1634 self.peek()
1635 )));
1636 }
1637 self.advance(); // )
1638 Ok(cols)
1639 }
1640
1641 /// `TO <roles>` (grant) / `FROM <roles>` (revoke). An empty-string entry is
1642 /// PUBLIC.
1643 fn parse_grantee_list(&mut self, grant: bool) -> Result<Vec<String>, ParseError> {
1644 if grant {
1645 if matches!(self.peek(), Token::To) {
1646 self.advance();
1647 } else {
1648 self.expect_keyword_ident("to")?;
1649 }
1650 } else if matches!(self.peek(), Token::From) {
1651 self.advance();
1652 } else {
1653 self.expect_keyword_ident("from")?;
1654 }
1655 let mut grantees: Vec<String> = Vec::new();
1656 loop {
1657 // `GROUP name` is the legacy spelling of a plain role name.
1658 if self.peek_keyword_ident("group") {
1659 self.advance();
1660 }
1661 if self.peek_keyword_ident("public") {
1662 self.advance();
1663 grantees.push(String::new()); // PUBLIC
1664 } else {
1665 grantees.push(self.expect_ident_like()?);
1666 }
1667 if matches!(self.peek(), Token::Comma) {
1668 self.advance();
1669 } else {
1670 break;
1671 }
1672 }
1673 Ok(grantees)
1674 }
1675
1676 /// v7.39 (round 277) — `PREPARE <name> [(type, …)] AS <statement>`.
1677 /// The body keeps its `$N` placeholders; substitution happens at
1678 /// EXECUTE. The declared types are recorded for
1679 /// `pg_prepared_statements.parameter_types` but are not enforced —
1680 /// PG infers when the list is omitted, and SPG resolves the values
1681 /// at substitution time either way.
1682 fn parse_prepare(&mut self) -> Result<Statement, ParseError> {
1683 let start = self.pos;
1684 self.advance(); // PREPARE
1685 // v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'` is 2PC, a
1686 // different statement that happens to share the keyword. PG
1687 // ships with `max_prepared_transactions = 0` and reports it
1688 // this way; SPG has no prepared-transaction registry, so the
1689 // same wording is the accurate answer rather than a dodge.
1690 // Round 277 turned this from a silent no-op into a confusing
1691 // "expected AS in PREPARE" parse error.
1692 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
1693 self.advance();
1694 let gid = match self.advance() {
1695 Token::String(g) => g,
1696 other => {
1697 return Err(self.err(alloc::format!(
1698 "expected a transaction identifier after PREPARE TRANSACTION, got {other:?}"
1699 )));
1700 }
1701 };
1702 return Ok(Statement::PrepareTransaction(gid));
1703 }
1704 let name = self.expect_ident_like()?;
1705 let mut param_types = Vec::new();
1706 if matches!(self.peek(), Token::LParen) {
1707 self.advance();
1708 loop {
1709 let mut ty = self.expect_ident_like()?;
1710 // A parameterised type name (`numeric(10,2)`,
1711 // `varchar(20)`) keeps its argument list in the text.
1712 if matches!(self.peek(), Token::LParen) {
1713 let mut depth = 0usize;
1714 let mut buf = String::from("(");
1715 loop {
1716 match self.advance() {
1717 Token::LParen => {
1718 depth += 1;
1719 if depth > 1 {
1720 buf.push('(');
1721 }
1722 }
1723 Token::RParen => {
1724 depth -= 1;
1725 buf.push(')');
1726 if depth == 0 {
1727 break;
1728 }
1729 }
1730 Token::Comma => buf.push(','),
1731 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
1732 Token::Eof => break,
1733 _ => {}
1734 }
1735 }
1736 ty.push_str(&buf);
1737 }
1738 // r1049 — `PREPARE p(bigint[]) AS …`: the sixth `[]`
1739 // position, same family as the parameter list above.
1740 let array_suffix = self.consume_array_suffix();
1741 ty.push_str(&array_suffix);
1742 param_types.push(ty);
1743 match self.peek() {
1744 Token::Comma => {
1745 self.advance();
1746 }
1747 Token::RParen => {
1748 self.advance();
1749 break;
1750 }
1751 other => {
1752 return Err(self.err(alloc::format!(
1753 "expected ',' or ')' in PREPARE parameter list, got {other:?}"
1754 )));
1755 }
1756 }
1757 }
1758 }
1759 if !matches!(self.peek(), Token::As) {
1760 return Err(self.err(alloc::format!(
1761 "expected AS in PREPARE, got {:?}",
1762 self.peek()
1763 )));
1764 }
1765 self.advance();
1766 let body = self.parse_one_statement()?;
1767 // The Parser holds tokens, not the source text, so the
1768 // statement PG reports in `pg_prepared_statements.statement`
1769 // is rebuilt from the AST rather than sliced from the input.
1770 let _ = start;
1771 let mut source = alloc::format!("PREPARE {}", crate::ast::quote_ident(&name));
1772 if !param_types.is_empty() {
1773 source.push_str(" (");
1774 source.push_str(¶m_types.join(", "));
1775 source.push(')');
1776 }
1777 source.push_str(" AS ");
1778 source.push_str(&alloc::format!("{body}"));
1779 Ok(Statement::Prepare {
1780 name,
1781 param_types,
1782 body: alloc::boxed::Box::new(body),
1783 source,
1784 })
1785 }
1786
1787 /// v7.39 (round 277) — `EXECUTE <name> [(<expr>, …)]`.
1788 fn parse_execute(&mut self) -> Result<Statement, ParseError> {
1789 self.advance(); // EXECUTE
1790 let name = self.expect_ident_like()?;
1791 let mut args = Vec::new();
1792 if matches!(self.peek(), Token::LParen) {
1793 self.advance();
1794 if matches!(self.peek(), Token::RParen) {
1795 self.advance();
1796 } else {
1797 loop {
1798 args.push(self.parse_expr(0)?);
1799 match self.advance() {
1800 Token::Comma => {}
1801 Token::RParen => break,
1802 other => {
1803 return Err(self.err(alloc::format!(
1804 "expected ',' or ')' in EXECUTE arguments, got {other:?}"
1805 )));
1806 }
1807 }
1808 }
1809 }
1810 }
1811 Ok(Statement::Execute { name, args })
1812 }
1813
1814 /// v7.39 (round 277) — `DEALLOCATE {[PREPARE] <name> | ALL}`.
1815 /// v7.39 (round 278) — `CALL <proc>([args])`. There is no
1816 /// procedure catalog yet, so this reports PG's not-found error
1817 /// (with its HINT) rather than pretending the call ran.
1818 /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
1819 /// Bare `DISCARD` is a syntax error in PG; so it is here.
1820 fn parse_discard(&mut self) -> Result<Statement, ParseError> {
1821 self.advance(); // DISCARD
1822 let target = match self.advance() {
1823 Token::All => DiscardTarget::All,
1824 Token::Ident(w) | Token::QuotedIdent(w) => match w.to_ascii_lowercase().as_str() {
1825 "all" => DiscardTarget::All,
1826 "plans" => DiscardTarget::Plans,
1827 "sequences" => DiscardTarget::Sequences,
1828 "temp" | "temporary" => DiscardTarget::Temp,
1829 other => {
1830 return Err(self.err(format!(
1831 "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1832 )));
1833 }
1834 },
1835 other => {
1836 return Err(self.err(format!(
1837 "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1838 )));
1839 }
1840 };
1841 Ok(Statement::Discard(target))
1842 }
1843
1844 /// v7.39 (round 318, V51) — MySQL `KILL [HARD|SOFT] [CONNECTION|QUERY]
1845 /// <expr>`. MariaDB accepts an expression for the id (its own docs use
1846 /// `KILL connection_id()`), and the HARD / SOFT prefixes only pick how
1847 /// aggressively the server interrupts, which SPG does not distinguish.
1848 /// Bare `KILL <id>` means CONNECTION.
1849 fn parse_kill(&mut self) -> Result<Statement, ParseError> {
1850 self.advance(); // KILL
1851 let mut query_only = false;
1852 loop {
1853 // CONNECTION is a reserved keyword token (it also opens
1854 // `CREATE SUBSCRIPTION … CONNECTION '…'`), so it arrives as
1855 // `Token::Connection` rather than a bare ident.
1856 if matches!(self.peek(), Token::Connection) {
1857 self.advance();
1858 break;
1859 }
1860 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
1861 break;
1862 };
1863 match w.to_ascii_lowercase().as_str() {
1864 "hard" | "soft" => {
1865 self.advance();
1866 }
1867 "query" => {
1868 self.advance();
1869 query_only = true;
1870 break;
1871 }
1872 _ => break,
1873 }
1874 }
1875 let id = self.parse_expr(0)?;
1876 Ok(Statement::Kill {
1877 query_only,
1878 id: Box::new(id),
1879 })
1880 }
1881
1882 fn parse_call(&mut self) -> Result<Statement, ParseError> {
1883 self.advance(); // CALL
1884 let name = self.expect_ident_like()?;
1885 self.consume_until_statement_boundary();
1886 Ok(Statement::Call(name))
1887 }
1888
1889 fn parse_deallocate(&mut self) -> Result<Statement, ParseError> {
1890 self.advance(); // DEALLOCATE
1891 // PG accepts an optional noise `PREPARE` keyword here.
1892 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("prepare")) {
1893 self.advance();
1894 }
1895 if matches!(self.peek(), Token::All) {
1896 self.advance();
1897 return Ok(Statement::Deallocate(None));
1898 }
1899 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("all")) {
1900 self.advance();
1901 return Ok(Statement::Deallocate(None));
1902 }
1903 let name = self.expect_ident_like()?;
1904 Ok(Statement::Deallocate(Some(name)))
1905 }
1906
1907 fn consume_until_statement_boundary(&mut self) {
1908 loop {
1909 match self.peek() {
1910 Token::Semicolon | Token::Eof => return,
1911 _ => self.advance(),
1912 };
1913 }
1914 }
1915
1916 /// v7.38.19 — the database name a `CREATE DATABASE` names, skipping
1917 /// an `IF NOT EXISTS`. Consumes only the name; the collation scanner
1918 /// runs after it and eats the rest.
1919 fn scan_database_name(&mut self) -> Option<String> {
1920 // The caller has only PEEKED at `DATABASE`; step past it, or the
1921 // first identifier found is the keyword itself. It was, and
1922 // `pg_database` listed a database called `database`.
1923 if matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w) if w.eq_ignore_ascii_case("database"))
1924 {
1925 self.advance();
1926 }
1927 for kw in ["if", "not", "exists"] {
1928 if matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w) if w.eq_ignore_ascii_case(kw))
1929 {
1930 self.advance();
1931 }
1932 }
1933 match self.peek().clone() {
1934 Token::Ident(w) | Token::QuotedIdent(w) => {
1935 self.advance();
1936 Some(w)
1937 }
1938 _ => None,
1939 }
1940 }
1941
1942 /// v7.38.18 — consume to the statement boundary like
1943 /// `consume_until_statement_boundary`, but pick out the collation a
1944 /// `CREATE DATABASE` asked for on the way.
1945 ///
1946 /// `LC_COLLATE 'de_DE.utf8'` and `LOCALE 'de_DE.utf8'` both count;
1947 /// `LC_CTYPE` does not, because SPG has no separate ctype and
1948 /// pretending to honour it would be the more misleading answer. An
1949 /// `=` between the keyword and the value is optional, as in PG.
1950 ///
1951 /// The whole statement used to be thrown away. Being single-database
1952 /// makes the NAME a no-op; it does not make the collation one.
1953 fn scan_database_collation_until_boundary(&mut self) -> Option<String> {
1954 let mut want_value = false;
1955 let mut found: Option<String> = None;
1956 loop {
1957 let tok = self.peek().clone();
1958 match &tok {
1959 Token::Semicolon | Token::Eof => break,
1960 Token::Ident(w) | Token::QuotedIdent(w)
1961 if w.eq_ignore_ascii_case("lc_collate") || w.eq_ignore_ascii_case("locale") =>
1962 {
1963 want_value = true;
1964 }
1965 Token::Eq if want_value => {}
1966 Token::String(v) if want_value => {
1967 found = Some(v.clone());
1968 want_value = false;
1969 }
1970 Token::Ident(v) | Token::QuotedIdent(v) if want_value => {
1971 found = Some(v.clone());
1972 want_value = false;
1973 }
1974 _ => want_value = false,
1975 }
1976 self.advance();
1977 }
1978 found
1979 }
1980
1981 /// v7.22 (round-13 T2) — consume to the statement boundary like
1982 /// `consume_until_statement_boundary`, but pick out the sequence
1983 /// name on the way: either `SEQUENCE NAME <ident>` (identity
1984 /// columns) or the first string literal (`nextval('<seq>')`).
1985 /// Schema qualifiers and `::regclass` casts are stripped.
1986 fn scan_sequence_name_until_boundary(&mut self) -> Option<String> {
1987 let mut seq: Option<String> = None;
1988 let mut after_sequence_kw = false;
1989 let mut after_name_kw = false;
1990 loop {
1991 match self.peek().clone() {
1992 Token::Semicolon | Token::Eof => break,
1993 Token::Ident(s) | Token::QuotedIdent(s) => {
1994 if after_name_kw && seq.is_none() {
1995 self.advance();
1996 let mut name = s;
1997 // `SEQUENCE NAME public.groups_id_seq` — keep
1998 // the bare name, drop qualifiers.
1999 while matches!(self.peek(), Token::Dot) {
2000 self.advance();
2001 if let Token::Ident(n) | Token::QuotedIdent(n) = self.advance() {
2002 name = n;
2003 }
2004 }
2005 seq = Some(name);
2006 after_name_kw = false;
2007 continue;
2008 }
2009 if after_sequence_kw && s.eq_ignore_ascii_case("name") {
2010 after_name_kw = true;
2011 after_sequence_kw = false;
2012 } else {
2013 after_sequence_kw = s.eq_ignore_ascii_case("sequence");
2014 }
2015 self.advance();
2016 }
2017 Token::String(s) => {
2018 if seq.is_none() {
2019 // `nextval('public.groups_id_seq'::regclass)`
2020 let bare = s
2021 .rsplit_once('.')
2022 .map_or_else(|| s.clone(), |(_, b)| b.to_string());
2023 seq = Some(bare);
2024 }
2025 self.advance();
2026 }
2027 _ => {
2028 after_sequence_kw = false;
2029 after_name_kw = false;
2030 self.advance();
2031 }
2032 }
2033 }
2034 seq
2035 }
2036
2037 /// v7.39 (round 621) — is the next token the keyword `BY`?
2038 ///
2039 /// `pg_get_keywords()` classes `by` as `U` (unreserved), so it is a legal
2040 /// column, table and alias name — and SPG lexed it into a dedicated
2041 /// `Token::By`, which made it unusable as a name ANYWHERE. Of the seven
2042 /// two-letter keywords the lexer knew, this was the only one PG leaves
2043 /// unreserved (`as`, `in`, `on`, `or`, `to` are reserved and `is` is `T`).
2044 ///
2045 /// The token is gone; the three clauses that own the word — GROUP BY,
2046 /// ORDER BY, PARTITION BY — and the handful of other places that expect it
2047 /// ask this instead. Adding it to the unreserved-identifier table was not
2048 /// enough on its own: identifier positions that match the token shape
2049 /// directly (an index's column list, a table alias) never consult that
2050 /// table, so `CREATE INDEX … ON t(by)` and `FROM t AS by` still failed.
2051 /// Not lexing it as a keyword closes the whole class rather than the two
2052 /// positions that happened to be noticed.
2053 fn peek_is_by(&self) -> bool {
2054 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by"))
2055 }
2056
2057 /// v7.39 (round 621) — the optional `CASCADE` / `RESTRICT` trailer a DROP
2058 /// takes. Accepted and dropped: SPG tracks no dependents to cascade to,
2059 /// which is what `DROP TABLE` and `DROP INDEX` have done since v7.14.
2060 fn consume_drop_behaviour(&mut self) {
2061 if matches!(
2062 self.peek(),
2063 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") || s.eq_ignore_ascii_case("restrict")
2064 ) {
2065 self.advance();
2066 }
2067 }
2068
2069 fn expect_ident_like(&mut self) -> Result<String, ParseError> {
2070 let first = match self.advance() {
2071 Token::Ident(s) | Token::QuotedIdent(s) => s,
2072 // v7.37.43-T4 — PG-unreserved keywords are legal identifiers
2073 // per PG's `pg_get_keywords()` classification. SPG tokenizes
2074 // these as named variants for parsing leverage in the
2075 // contexts that own them (`RELEASE SAVEPOINT`, `SHOW name`,
2076 // `BEGIN`, etc.), but they MUST still be usable as table /
2077 // column / alias names in DDL+DML. Sentori migrations like
2078 // 0001_init.sql ship `release TEXT NOT NULL` in the events
2079 // table — the `events.release` column carries the release
2080 // identifier string. Pre-T4 this triggered "expected
2081 // identifier, got Release" and blocked every drop-in user
2082 // whose schema had a column / alias with one of these names.
2083 other if unreserved_keyword_text(&other).is_some() => {
2084 unreserved_keyword_text(&other).unwrap()
2085 }
2086 other => {
2087 return Err(ParseError {
2088 message: format!("expected identifier, got {other:?}"),
2089 token_pos: self.consumed_pos(),
2090 });
2091 }
2092 };
2093 // v7.14.0 — strip optional `<schema>.` prefix. PG dumps
2094 // qualify every name with `public.` (and pg_catalog.* for
2095 // functions); SPG is single-schema so we discard the
2096 // prefix and return only the trailing ident. Same shape
2097 // also handles MySQL `db.tbl` cross-database refs (SPG
2098 // ignores the db part).
2099 if matches!(self.peek(), Token::Dot) {
2100 self.advance();
2101 match self.advance() {
2102 Token::Ident(s) | Token::QuotedIdent(s) => return Ok(s),
2103 other if unreserved_keyword_text(&other).is_some() => {
2104 return Ok(unreserved_keyword_text(&other).unwrap());
2105 }
2106 other => {
2107 return Err(ParseError {
2108 message: format!("expected identifier after '{first}.', got {other:?}"),
2109 token_pos: self.consumed_pos(),
2110 });
2111 }
2112 }
2113 }
2114 Ok(first)
2115 }
2116
2117 #[allow(clippy::too_many_lines)]
2118 fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
2119 // v7.14.0 — empty / comment-only / semicolon-only input
2120 // (after the lexer strips line + block + MySQL
2121 // conditional comments) lands as Statement::Empty.
2122 // pg_dump and mysqldump emit several wrappers that
2123 // collapse to nothing after stripping (`/*!40101 SET …
2124 // */;`, blank lines between statements); the engine
2125 // returns CommandOk no-op so the dump loads cleanly.
2126 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
2127 return Ok(Statement::Empty);
2128 }
2129 // v7.14.0 — pg_dump / mysqldump "noise" statements:
2130 // catalog / metadata DDL that has no behavioural effect
2131 // on SPG's single-schema, single-database, single-user
2132 // model. Consume the whole statement up to the next
2133 // semicolon / EOF and return Empty. This is broader than
2134 // the per-keyword DROP / SET / COMMENT arms but lets the
2135 // long tail of `LOCK TABLES`, `UNLOCK TABLES`, `GRANT`,
2136 // `REVOKE`, `ALTER OWNER TO`, `\restrict`, `\unrestrict`,
2137 // `BEGIN; COMMIT;` wrappers, etc. all pass through.
2138 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
2139 let lc = s.to_ascii_lowercase();
2140 // v7.39 (read01 round 50) — COMMENT ON is a real statement now.
2141 if lc == "comment" {
2142 return self.parse_comment_on();
2143 }
2144 // v7.39 (read01 round 57) — so is GRANT / REVOKE.
2145 if lc == "grant" || lc == "revoke" {
2146 return self.parse_grant_or_revoke(lc == "grant");
2147 }
2148 // v7.39 (round 277) — the SQL-level prepared-statement
2149 // surface is REAL now. It used to be accepted and dropped
2150 // on the theory that "real execution still happens via the
2151 // extended-query flow" — true only for a driver that uses
2152 // that flow; a plain SQL PREPARE / EXECUTE returned no
2153 // rows at all.
2154 if lc == "prepare" {
2155 return self.parse_prepare();
2156 }
2157 if lc == "execute" {
2158 return self.parse_execute();
2159 }
2160 if lc == "deallocate" {
2161 return self.parse_deallocate();
2162 }
2163 // v7.39 (round 278) — `CALL <proc>(<args>)` used to be
2164 // accepted and dropped, so an application's stored-procedure
2165 // invocation reported success and did nothing. SPG has no
2166 // procedure catalog, so every CALL names a procedure that
2167 // does not exist — which is exactly what PG says.
2168 if lc == "call" {
2169 return self.parse_call();
2170 }
2171 // v7.39 (round 318, V51) — MySQL `KILL`. Not dump noise: it
2172 // names one connection and acts on it.
2173 if lc == "kill" {
2174 return self.parse_kill();
2175 }
2176 if lc == "discard" {
2177 return self.parse_discard();
2178 }
2179 // v7.39 (round 696) — REASSIGN OWNED BY <role> [, …] TO <role>.
2180 // Still performs nothing; the roles are carried out so a name
2181 // that does not exist is refused, as PG18 refuses it.
2182 if lc == "reassign" {
2183 self.advance();
2184 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("owned")) {
2185 self.advance();
2186 }
2187 if self.peek_is_by() {
2188 self.advance();
2189 }
2190 // Only the roles BEFORE the TO are the ones that must
2191 // exist — `TO` names the new owner, which PG checks as
2192 // well, so both lists are collected.
2193 let mut names = self.take_comma_separated_names();
2194 if matches!(self.peek(), Token::To) {
2195 self.advance();
2196 names.extend(self.take_comma_separated_names());
2197 }
2198 self.consume_until_statement_boundary();
2199 return Ok(Statement::ValidateOnly {
2200 kind: crate::ast::ValidateOnlyKind::RoleName,
2201 names,
2202 });
2203 }
2204 // v7.39 (round 696) — SECURITY LABEL. PG18 refuses it
2205 // unconditionally with `no security label providers have been
2206 // loaded`, whatever object it names, because none is loaded.
2207 // SPG has none either; accepting it told the caller a label had
2208 // been applied when nothing anywhere records one.
2209 if lc == "security" {
2210 self.consume_until_statement_boundary();
2211 return Ok(Statement::ValidateOnly {
2212 kind: crate::ast::ValidateOnlyKind::SecurityLabel,
2213 names: Vec::new(),
2214 });
2215 }
2216 // v7.39.2 — `USE <db>` is a real statement now, and only in
2217 // the MySQL dialect. It used to be swallowed here with the
2218 // dump noise, so `USE myapp; SELECT DATABASE()` answered the
2219 // same constant it answered before — MySQL 9.7.2 answers
2220 // `myapp`. PostgreSQL has no USE at all, and pg_dump does not
2221 // emit one, but the swallow stays on that side: it was put
2222 // there for restores and taking it away is not this defect.
2223 if lc == "use" {
2224 if self.mysql_dialect {
2225 self.advance();
2226 let name = self.expect_ident_like()?;
2227 return Ok(Statement::UseDatabase(name));
2228 }
2229 self.consume_until_statement_boundary();
2230 return Ok(Statement::Empty);
2231 }
2232 if is_dump_noise_statement(&lc) {
2233 self.consume_until_statement_boundary();
2234 return Ok(Statement::Empty);
2235 }
2236 }
2237 match self.peek() {
2238 Token::Select => self.parse_select_stmt(),
2239 // v7.37.17 (17.6 siblings) — a statement opening with a
2240 // parenthesized query group: `(SELECT … UNION …)
2241 // INTERSECT …`. parse_bare_select's group arm consumes
2242 // the parens; the select parser handles the outer chain
2243 // and tail.
2244 Token::LParen
2245 if matches!(
2246 self.tokens.get(self.pos + 1),
2247 Some(Token::Select | Token::LParen | Token::Values)
2248 ) =>
2249 {
2250 self.parse_select_stmt()
2251 }
2252 // v7.37.17 (17.6 siblings) — top-level bare VALUES
2253 // statement (`VALUES (1), (2) [ORDER BY …] [LIMIT …]`).
2254 // Lowers to the same UNION ALL chain the FROM-position
2255 // form uses, then reuses the shared SELECT tail.
2256 Token::Values => {
2257 self.advance(); // VALUES
2258 let mut head = self.parse_values_rows_body()?;
2259 self.parse_select_tail_into(&mut head)?;
2260 Ok(Statement::Select(head))
2261 }
2262 // SQL-standard `TABLE name` shorthand for
2263 // `SELECT * FROM name` — pg_dump never emits it, but
2264 // psql users and PG docs use it constantly. Set-op
2265 // chains and the ORDER BY/LIMIT tail compose like any
2266 // SELECT head.
2267 Token::Table
2268 if matches!(
2269 self.tokens.get(self.pos + 1),
2270 Some(Token::Ident(_) | Token::QuotedIdent(_))
2271 ) =>
2272 {
2273 let mut head = self.parse_table_shorthand()?;
2274 self.parse_setop_chain_into(&mut head)?;
2275 self.parse_select_tail_into(&mut head)?;
2276 Ok(Statement::Select(head))
2277 }
2278 // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2279 // body is a dollar-quoted plpgsql block (lexer already
2280 // collapsed `$$…$$` into a single Token::String).
2281 // v7.16.2 — mailrs round-10 A.2: parse the body as a
2282 // real PlPgSqlBlock so the engine can EXECUTE it at
2283 // top level instead of silently swallowing. Pre-
2284 // v7.16.2 the parser threw the body away and the
2285 // engine returned CommandOk for the entire DO; that
2286 // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2287 // $$` into a SEV-1 silent no-op (the IF + the rename
2288 // were both invisible — mailrs's migrate-042 didn't
2289 // actually run). Now the body parses + executes;
2290 // EmbeddedSql inside the block runs immediately
2291 // against the engine (not deferred — we're at top
2292 // level, not inside a trigger row-write loop).
2293 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2294 self.advance();
2295 let body_text = match self.advance() {
2296 Token::String(s) => s,
2297 other => {
2298 return Err(self.err(alloc::format!(
2299 "expected dollar-quoted body after DO, got {other:?}"
2300 )));
2301 }
2302 };
2303 // Optional `LANGUAGE <name>` trailer (idents only).
2304 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2305 self.advance();
2306 let _ = self.expect_ident_like()?;
2307 }
2308 // Parse the body — same shape CREATE FUNCTION
2309 // uses for trigger function bodies. If the body
2310 // doesn't parse cleanly we surface the error
2311 // (better than silent no-op).
2312 let block = parse_plpgsql_body(&body_text)?;
2313 Ok(Statement::DoBlock(block))
2314 }
2315 // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2316 // WITH isn't a reserved token in our lexer — comes through
2317 // as `Token::Ident("with")` (case-insensitive).
2318 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2319 self.advance();
2320 self.parse_with_cte_then_select()
2321 }
2322 // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2323 // an identifier — not a reserved keyword.
2324 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2325 self.advance();
2326 let mut analyze = false;
2327 let mut suggest = false;
2328 let mut costs_off = false;
2329 let mut buffers = false;
2330 let mut timing_off = false;
2331 let mut settings = false;
2332 let mut wal = false;
2333 let mut summary_off = false;
2334 let mut format = crate::ast::ExplainFormat::Text;
2335 // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2336 // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2337 // options are comma-separated. Booleans default to ON
2338 // when the value token is omitted (matches PG).
2339 if matches!(self.peek(), Token::LParen) {
2340 self.advance();
2341 loop {
2342 let opt = match self.peek().clone() {
2343 Token::Ident(s) | Token::QuotedIdent(s) => s,
2344 other => {
2345 return Err(self.err(format!(
2346 "expected option keyword inside EXPLAIN (…), got {other:?}"
2347 )));
2348 }
2349 };
2350 self.advance();
2351 if opt.eq_ignore_ascii_case("suggest") {
2352 suggest = true;
2353 // SUGGEST takes no explicit value today.
2354 } else if opt.eq_ignore_ascii_case("costs") {
2355 // PG syntax: `COSTS [ON | OFF]`. Default
2356 // when value omitted is ON, so plain
2357 // `COSTS` is a no-op. `COSTS OFF` flips.
2358 // `ON` lexes to `Token::On` (reserved
2359 // keyword in JOIN ... ON contexts); accept
2360 // it alongside the bare Ident form so the
2361 // grammar matches PG verbatim.
2362 let value = match self.peek().clone() {
2363 Token::On => {
2364 self.advance();
2365 true
2366 }
2367 Token::Ident(v) | Token::QuotedIdent(v)
2368 if v.eq_ignore_ascii_case("off") =>
2369 {
2370 self.advance();
2371 false
2372 }
2373 Token::Ident(v) | Token::QuotedIdent(v)
2374 if v.eq_ignore_ascii_case("true") =>
2375 {
2376 self.advance();
2377 true
2378 }
2379 _ => true,
2380 };
2381 costs_off = !value;
2382 } else if opt.eq_ignore_ascii_case("analyze")
2383 || opt.eq_ignore_ascii_case("analyse")
2384 {
2385 // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2386 // Same default-ON rule as ANALYZE keyword form.
2387 let value = match self.peek().clone() {
2388 Token::On => {
2389 self.advance();
2390 true
2391 }
2392 Token::Ident(v) | Token::QuotedIdent(v)
2393 if v.eq_ignore_ascii_case("off") =>
2394 {
2395 self.advance();
2396 false
2397 }
2398 Token::Ident(v) | Token::QuotedIdent(v)
2399 if v.eq_ignore_ascii_case("true") =>
2400 {
2401 self.advance();
2402 true
2403 }
2404 _ => true,
2405 };
2406 analyze = value;
2407 } else if opt.eq_ignore_ascii_case("buffers") {
2408 // v7.37.22 — `BUFFERS [ON|OFF]`.
2409 let value = match self.peek().clone() {
2410 Token::On => {
2411 self.advance();
2412 true
2413 }
2414 Token::Ident(v) | Token::QuotedIdent(v)
2415 if v.eq_ignore_ascii_case("off") =>
2416 {
2417 self.advance();
2418 false
2419 }
2420 Token::Ident(v) | Token::QuotedIdent(v)
2421 if v.eq_ignore_ascii_case("true") =>
2422 {
2423 self.advance();
2424 true
2425 }
2426 _ => true,
2427 };
2428 buffers = value;
2429 } else if opt.eq_ignore_ascii_case("timing") {
2430 // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2431 // the measured wall-clock annotation.
2432 let value = match self.peek().clone() {
2433 Token::On => {
2434 self.advance();
2435 true
2436 }
2437 Token::Ident(v) | Token::QuotedIdent(v)
2438 if v.eq_ignore_ascii_case("off") =>
2439 {
2440 self.advance();
2441 false
2442 }
2443 Token::Ident(v) | Token::QuotedIdent(v)
2444 if v.eq_ignore_ascii_case("true") =>
2445 {
2446 self.advance();
2447 true
2448 }
2449 _ => true,
2450 };
2451 timing_off = !value;
2452 } else if opt.eq_ignore_ascii_case("settings") {
2453 settings = true;
2454 } else if opt.eq_ignore_ascii_case("wal") {
2455 wal = true;
2456 } else if opt.eq_ignore_ascii_case("summary") {
2457 // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2458 // gates the trailing Planning/Execution Time
2459 // lines now (was accept-and-no-op).
2460 let value = match self.peek().clone() {
2461 Token::On => {
2462 self.advance();
2463 true
2464 }
2465 Token::Ident(v) | Token::QuotedIdent(v)
2466 if v.eq_ignore_ascii_case("off") =>
2467 {
2468 self.advance();
2469 false
2470 }
2471 Token::Ident(v) | Token::QuotedIdent(v)
2472 if v.eq_ignore_ascii_case("true") =>
2473 {
2474 self.advance();
2475 true
2476 }
2477 _ => true,
2478 };
2479 summary_off = !value;
2480 } else if opt.eq_ignore_ascii_case("verbose")
2481 || opt.eq_ignore_ascii_case("format")
2482 {
2483 // v7.37.22 — accept-but-no-op the remaining
2484 // PG options so EXPLAIN-using clients
2485 // (pgAdmin / DataGrip) don't see syntax
2486 // errors. FORMAT takes a value (text /
2487 // json / yaml / xml); skip the next token
2488 // if it's an ident.
2489 if opt.eq_ignore_ascii_case("format") {
2490 if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2491 {
2492 self.advance();
2493 format = match v.to_ascii_lowercase().as_str() {
2494 "text" => crate::ast::ExplainFormat::Text,
2495 "json" => crate::ast::ExplainFormat::Json,
2496 "xml" => crate::ast::ExplainFormat::Xml,
2497 "yaml" => crate::ast::ExplainFormat::Yaml,
2498 other => {
2499 return Err(self.err(format!(
2500 "EXPLAIN (FORMAT …): unknown format {other:?}; \
2501 supports text, json, xml, yaml"
2502 )));
2503 }
2504 };
2505 }
2506 } else {
2507 // VERBOSE / SUMMARY take optional ON/OFF;
2508 // consume if present.
2509 if matches!(self.peek(), Token::On) {
2510 self.advance();
2511 } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2512 self.peek().clone()
2513 && (v.eq_ignore_ascii_case("off")
2514 || v.eq_ignore_ascii_case("true"))
2515 {
2516 self.advance();
2517 let _ = v;
2518 }
2519 }
2520 } else {
2521 return Err(self.err(format!(
2522 "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2523 )));
2524 }
2525 if matches!(self.peek(), Token::Comma) {
2526 self.advance();
2527 continue;
2528 }
2529 break;
2530 }
2531 if !matches!(self.peek(), Token::RParen) {
2532 return Err(self.err(format!(
2533 "expected ')' after EXPLAIN options, got {:?}",
2534 self.peek()
2535 )));
2536 }
2537 self.advance();
2538 } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2539 && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2540 {
2541 self.advance();
2542 analyze = true;
2543 }
2544 // v7.39 (round 224) — the body may open with WITH (CTEs);
2545 // route through the same CTE-then-SELECT path the top-level
2546 // WITH statement uses. v7.39 (round 225) — DML bodies parse
2547 // too (PG explains INSERT / UPDATE / DELETE).
2548 let inner = match self.peek().clone() {
2549 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2550 self.advance();
2551 self.parse_with_cte_then_select()?
2552 }
2553 Token::Insert => self.parse_insert_stmt(false)?,
2554 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2555 self.advance();
2556 self.parse_update_after_keyword()?
2557 }
2558 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2559 self.advance();
2560 self.parse_delete_after_keyword()?
2561 }
2562 _ => self.parse_select_stmt()?,
2563 };
2564 if !matches!(
2565 inner,
2566 Statement::Select(_)
2567 | Statement::Insert(_)
2568 | Statement::Update(_)
2569 | Statement::Delete(_)
2570 ) {
2571 return Err(self.err(format!(
2572 "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2573 )));
2574 }
2575 Ok(Statement::Explain(crate::ast::ExplainStatement {
2576 analyze,
2577 inner: Box::new(inner),
2578 suggest,
2579 costs_off,
2580 buffers,
2581 timing_off,
2582 settings,
2583 wal,
2584 summary_off,
2585 format,
2586 }))
2587 }
2588 Token::Create => self.parse_create_stmt(),
2589 Token::Insert => self.parse_insert_stmt(false),
2590 // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2591 // spelling; route to the same handler. DESC is the
2592 // reserved ORDER BY token, so it gets its own arm.
2593 Token::Ident(s)
2594 if s.eq_ignore_ascii_case("describe")
2595 && matches!(
2596 self.tokens.get(self.pos + 1),
2597 Some(Token::Ident(_) | Token::QuotedIdent(_))
2598 ) =>
2599 {
2600 self.advance();
2601 let table = self.expect_ident_like()?;
2602 Ok(Statement::ShowColumns(table))
2603 }
2604 Token::Desc
2605 if matches!(
2606 self.tokens.get(self.pos + 1),
2607 Some(Token::Ident(_) | Token::QuotedIdent(_))
2608 ) =>
2609 {
2610 self.advance();
2611 let table = self.expect_ident_like()?;
2612 Ok(Statement::ShowColumns(table))
2613 }
2614 // `COPY table [(cols)] TO STDOUT` — the export half of
2615 // pg_dump's COPY pair (the FROM stdin half rides the
2616 // embed import path). Options need a format design and
2617 // error honestly.
2618 Token::Ident(s)
2619 if s.eq_ignore_ascii_case("copy")
2620 && matches!(
2621 self.tokens.get(self.pos + 1),
2622 Some(Token::Ident(_) | Token::QuotedIdent(_))
2623 ) =>
2624 {
2625 self.advance(); // COPY
2626 let table = self.expect_ident_like()?;
2627 let columns = if matches!(self.peek(), Token::LParen) {
2628 self.advance();
2629 let mut cols = alloc::vec![self.expect_ident_like()?];
2630 while matches!(self.peek(), Token::Comma) {
2631 self.advance();
2632 cols.push(self.expect_ident_like()?);
2633 }
2634 if !matches!(self.peek(), Token::RParen) {
2635 return Err(self.err(format!(
2636 "expected ')' after COPY column list, got {:?}",
2637 self.peek()
2638 )));
2639 }
2640 self.advance();
2641 Some(cols)
2642 } else {
2643 None
2644 };
2645 // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2646 // endpoint. (FROM STDIN still rides the wire/import path —
2647 // its data arrives out of band.)
2648 if matches!(self.peek(), Token::From)
2649 && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2650 {
2651 self.advance(); // FROM
2652 let Token::String(path) = self.advance() else {
2653 unreachable!()
2654 };
2655 let options = self.parse_copy_to_options()?;
2656 return Ok(Statement::CopyFromFile {
2657 table,
2658 columns,
2659 path,
2660 options,
2661 });
2662 }
2663 if !matches!(self.peek(), Token::To) {
2664 return Err(self.err(format!(
2665 "COPY: only TO STDOUT is supported here (FROM stdin \
2666 rides the import path); got {:?}",
2667 self.peek()
2668 )));
2669 }
2670 self.advance();
2671 if matches!(self.peek(), Token::String(_)) {
2672 let Token::String(path) = self.advance() else { unreachable!() };
2673 let options = self.parse_copy_to_options()?;
2674 return Ok(Statement::CopyToFile {
2675 table,
2676 columns,
2677 query: None,
2678 path,
2679 options,
2680 });
2681 }
2682 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2683 return Err(self.err(format!(
2684 "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2685 self.peek()
2686 )));
2687 }
2688 self.advance();
2689 let options = self.parse_copy_to_options()?;
2690 Ok(Statement::CopyTo {
2691 table,
2692 columns,
2693 query: None,
2694 options,
2695 })
2696 }
2697 // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2698 // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2699 // result set is streamed in COPY format (PG's query form).
2700 Token::Ident(s)
2701 if s.eq_ignore_ascii_case("copy")
2702 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2703 {
2704 self.advance(); // COPY
2705 self.advance(); // (
2706 let query = self.parse_select_stmt()?;
2707 if !matches!(self.peek(), Token::RParen) {
2708 return Err(self.err(format!(
2709 "expected ')' after COPY query, got {:?}",
2710 self.peek()
2711 )));
2712 }
2713 self.advance(); // )
2714 if !matches!(self.peek(), Token::To) {
2715 return Err(self.err(format!(
2716 "COPY (query): only TO STDOUT is supported, got {:?}",
2717 self.peek()
2718 )));
2719 }
2720 self.advance();
2721 if matches!(self.peek(), Token::String(_)) {
2722 let Token::String(path) = self.advance() else { unreachable!() };
2723 let options = self.parse_copy_to_options()?;
2724 return Ok(Statement::CopyToFile {
2725 table: String::new(),
2726 columns: None,
2727 query: Some(alloc::boxed::Box::new(query)),
2728 path,
2729 options,
2730 });
2731 }
2732 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2733 return Err(self.err(format!(
2734 "COPY (query): TO supports STDOUT only, got {:?}",
2735 self.peek()
2736 )));
2737 }
2738 self.advance();
2739 let options = self.parse_copy_to_options()?;
2740 Ok(Statement::CopyTo {
2741 table: String::new(),
2742 columns: None,
2743 query: Some(alloc::boxed::Box::new(query)),
2744 options,
2745 })
2746 }
2747 // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2748 // Shares the INSERT body; the replace flag lowers it
2749 // onto ON CONFLICT DO UPDATE with an empty assignment
2750 // list (engine: replace the whole row).
2751 Token::Ident(s)
2752 if s.eq_ignore_ascii_case("replace")
2753 && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2754 {
2755 self.parse_insert_stmt(true)
2756 }
2757 Token::Begin => {
2758 self.advance();
2759 // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2760 // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2761 // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2762 // is consumed first, then the trailing modes — including the
2763 // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2764 // WORK/TRANSACTION). The explicit level, when present, rides the
2765 // statement so `exec_begin` applies it for this transaction.
2766 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2767 {
2768 self.advance();
2769 }
2770 let iso = self.parse_isolation_level_clauses()?;
2771 Ok(Statement::Begin(iso))
2772 }
2773 // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2774 // for BEGIN. START is contextual in PG too; pattern-match
2775 // on the ident here. Iso clauses are parse-and-ignored,
2776 // same as BEGIN above.
2777 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2778 self.advance();
2779 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2780 {
2781 return Err(self.err(alloc::format!(
2782 "expected TRANSACTION after START, got {:?}",
2783 self.peek()
2784 )));
2785 }
2786 self.advance();
2787 let iso = self.parse_isolation_level_clauses()?;
2788 Ok(Statement::Begin(iso))
2789 }
2790 Token::Commit => {
2791 self.advance();
2792 // PG: `COMMIT [WORK | TRANSACTION]`.
2793 if let Token::Ident(w) = self.peek()
2794 && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2795 {
2796 self.advance();
2797 }
2798 Ok(Statement::Commit)
2799 }
2800 // r1066 (7.38 S5.1) — `END [WORK | TRANSACTION]` is PG's
2801 // COMMIT synonym; pgbench's builtin tpcb-like script closes
2802 // every transaction with `END;` and the drop-in aborted on
2803 // it. Only reachable at statement start (CASE … END lives
2804 // inside expressions), so no ambiguity.
2805 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
2806 self.advance();
2807 if let Token::Ident(w) = self.peek()
2808 && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2809 {
2810 self.advance();
2811 }
2812 Ok(Statement::Commit)
2813 }
2814 Token::Rollback => {
2815 self.advance();
2816 // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2817 // savepoint without ending the transaction. Bare
2818 // `ROLLBACK` drops the whole TX.
2819 if matches!(self.peek(), Token::To) {
2820 self.advance();
2821 if matches!(self.peek(), Token::Savepoint) {
2822 self.advance();
2823 }
2824 let name = self.expect_ident_like()?;
2825 Ok(Statement::RollbackToSavepoint(name))
2826 } else {
2827 Ok(Statement::Rollback)
2828 }
2829 }
2830 Token::Savepoint => {
2831 self.advance();
2832 let name = self.expect_ident_like()?;
2833 Ok(Statement::Savepoint(name))
2834 }
2835 Token::Release => {
2836 self.advance();
2837 // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2838 // is optional in standard SQL.
2839 if matches!(self.peek(), Token::Savepoint) {
2840 self.advance();
2841 }
2842 let name = self.expect_ident_like()?;
2843 Ok(Statement::ReleaseSavepoint(name))
2844 }
2845 Token::Show => {
2846 self.advance();
2847 // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2848 // v6.1.2 promoted TABLES to a reserved keyword (for
2849 // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2850 // arrives as `Token::Tables` rather than a bare ident.
2851 // USERS / COLUMNS remain bare idents.
2852 let target = match self.advance() {
2853 Token::Tables => "tables".to_string(),
2854 // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2855 // keyword token; recognise it as the SHOW CREATE
2856 // dispatch keyword too.
2857 Token::Create => "create".to_string(),
2858 // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2859 // keyword too; let SHOW INDEX FROM parse.
2860 Token::Index => "index".to_string(),
2861 // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2862 // reserved (used in aggregate function calls);
2863 // recognise it here so the parser dispatches
2864 // to ShowParameter("all") — the engine returns
2865 // the curated parameter inventory.
2866 Token::All => "all".to_string(),
2867 // v7.38.18 (C12) — `SHOW COUNT(*) WARNINGS`, MySQL's
2868 // spelling for the size of the diagnostics area.
2869 // MySQL-dialect only: PostgreSQL 18.4 answers this
2870 // phrase with `syntax error at or near "("`, and a
2871 // PG session must keep getting exactly that rather
2872 // than a message about an unknown parameter.
2873 // `COUNT` arrives as a bare ident; the `(*)` and the
2874 // trailing keyword are consumed here so the whole
2875 // form reaches the engine as one parameter name.
2876 Token::Ident(ref c)
2877 if self.mysql_dialect
2878 && c.eq_ignore_ascii_case("count")
2879 && matches!(self.peek(), Token::LParen) =>
2880 {
2881 self.advance();
2882 if matches!(self.peek(), Token::Star) {
2883 self.advance();
2884 }
2885 if matches!(self.peek(), Token::RParen) {
2886 self.advance();
2887 }
2888 match self.advance() {
2889 Token::Ident(w) if w.eq_ignore_ascii_case("warnings") => {
2890 return Ok(Statement::ShowParameter(
2891 "count(*) warnings".to_string(),
2892 ));
2893 }
2894 other => {
2895 return Err(self.err(format!(
2896 "expected WARNINGS after SHOW COUNT(*), got {other:?}"
2897 )));
2898 }
2899 }
2900 }
2901 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2902 other => {
2903 return Err(self.err(format!(
2904 "expected SHOW target, got {other:?}"
2905 )));
2906 }
2907 };
2908 match target.as_str() {
2909 "tables" => Ok(Statement::ShowTables),
2910 "users" => Ok(Statement::ShowUsers),
2911 // v7.38 轴 4 — `SHOW transaction_isolation`
2912 // returns the currently-selected isolation level.
2913 "transaction_isolation" => Ok(Statement::ShowParameter(
2914 "transaction_isolation".to_string(),
2915 )),
2916 // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2917 // TABLE <t>` returns a 2-column row: (Table,
2918 // Create Table). mysqldump emits this for every
2919 // table at scrape time; without it the dump
2920 // round-trip stalls.
2921 // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2922 // FROM <t>` (also spelled `SHOW INDEX` and
2923 // `SHOW KEYS`). admin / mysqldump probes use
2924 // it to list per-table indexes.
2925 "indexes" | "index" | "keys" => {
2926 if !matches!(self.peek(), Token::From) {
2927 return Err(self.err(format!(
2928 "expected FROM after SHOW INDEXES, got {:?}",
2929 self.peek()
2930 )));
2931 }
2932 self.advance();
2933 let table = self.expect_ident_like()?;
2934 Ok(Statement::ShowIndexes(table))
2935 }
2936 // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2937 // `SHOW VARIABLES`. Both return a 2-column row
2938 // set listing server-side state; clients probe
2939 // them at connect time.
2940 "status" => Ok(Statement::ShowStatus),
2941 "variables" => {
2942 // r1067 — `SHOW VARIABLES LIKE 'pat'`.
2943 if matches!(self.peek(), Token::Like) {
2944 self.advance();
2945 let pat = match self.advance() {
2946 Token::String(p) => p,
2947 other => {
2948 return Err(self.err(format!(
2949 "SHOW VARIABLES LIKE expects a quoted pattern, got {other:?}"
2950 )));
2951 }
2952 };
2953 return Ok(Statement::ShowVariablesLike(pat));
2954 }
2955 Ok(Statement::ShowVariables)
2956 }
2957 // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2958 "processlist" => Ok(Statement::ShowProcesslist),
2959 "create" => {
2960 // SHOW CREATE TABLE / VIEW / DATABASE — only
2961 // TABLE is supported in v7.17.
2962 let kind = match self.advance() {
2963 Token::Ident(s) | Token::QuotedIdent(s) => s,
2964 Token::Table => "table".to_string(),
2965 other => {
2966 return Err(self.err(format!(
2967 "expected TABLE after SHOW CREATE, got {other:?}"
2968 )));
2969 }
2970 };
2971 if !kind.eq_ignore_ascii_case("table") {
2972 return Err(self.err(format!(
2973 "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2974 )));
2975 }
2976 let name = self.expect_ident_like()?;
2977 Ok(Statement::ShowCreateTable(name))
2978 }
2979 // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2980 // (and `SHOW SCHEMAS` alias). The mysql client uses
2981 // it to populate the database selector at connect
2982 // time; without it `mysql -p` errors before the
2983 // first user query.
2984 "databases" | "schemas" => Ok(Statement::ShowDatabases),
2985 // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2986 // keyword on its own; it lands here as a bare
2987 // ident. Returning all publications + their
2988 // scope summary.
2989 "publications" => Ok(Statement::ShowPublications),
2990 // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2991 "subscriptions" => Ok(Statement::ShowSubscriptions),
2992 "columns" => {
2993 if !matches!(self.peek(), Token::From) {
2994 return Err(self.err(format!(
2995 "expected FROM after SHOW COLUMNS, got {:?}",
2996 self.peek()
2997 )));
2998 }
2999 self.advance();
3000 let table = self.expect_ident_like()?;
3001 Ok(Statement::ShowColumns(table))
3002 }
3003 // v7.38 轴 4 surface — `SHOW <param>` for any
3004 // remaining session / preset parameter name
3005 // (server_version, search_path, client_encoding,
3006 // …). The engine's ShowParameter handler does the
3007 // dispatch; unrecognised names error there with
3008 // a pointer to pg_settings, not at parse time —
3009 // so a driver that issues `SHOW spam_setting`
3010 // gets a clear runtime error instead of a
3011 // confusing "unknown SHOW target".
3012 other => {
3013 // v7.38 (read01 P3.20) — a custom namespaced GUC
3014 // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
3015 // consume the dotted tail so it round-trips with
3016 // `SET app.foo` / `current_setting('app.foo')`.
3017 let mut full = other.to_string();
3018 while matches!(self.peek(), Token::Dot) {
3019 self.advance();
3020 let seg = self.expect_ident_like()?;
3021 full.push('.');
3022 full.push_str(&seg.to_ascii_lowercase());
3023 }
3024 Ok(Statement::ShowParameter(full))
3025 }
3026 }
3027 }
3028 // v6.1.2: `DROP` is now a reserved keyword (it dispatches
3029 // to DROP USER and DROP PUBLICATION today; DROP TABLE /
3030 // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
3031 // arrived as a bare ident; tokenising it dedicatedly
3032 // keeps the dispatch tree small.
3033 Token::Drop => {
3034 self.advance();
3035 match self.peek() {
3036 // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
3037 // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
3038 // around DROP ROLE cleanup. SPG has no role-owner
3039 // model, so consume to boundary as a no-op.
3040 Token::Ident(s) | Token::QuotedIdent(s)
3041 if s.eq_ignore_ascii_case("owned") =>
3042 {
3043 // v7.39 (round 696) — still a no-op (SPG has no
3044 // role-owner model), but the ROLE is carried out so
3045 // the engine can refuse one that does not exist,
3046 // which is what PG18 does.
3047 self.advance();
3048 if self.peek_is_by() {
3049 self.advance();
3050 }
3051 let names = self.take_comma_separated_names();
3052 self.consume_until_statement_boundary();
3053 Ok(Statement::ValidateOnly {
3054 kind: crate::ast::ValidateOnlyKind::RoleName,
3055 names,
3056 })
3057 }
3058 // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
3059 // It drops only a TEMPORARY table, and name resolution
3060 // already prefers the session's own, so the keyword is
3061 // consumed and the ordinary DROP TABLE path runs.
3062 Token::Ident(s) | Token::QuotedIdent(s)
3063 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
3064 {
3065 self.advance();
3066 if !matches!(self.peek(), Token::Table) {
3067 return Err(self.err(alloc::format!(
3068 "expected TABLE after DROP TEMPORARY, got {:?}",
3069 self.peek()
3070 )));
3071 }
3072 self.parse_drop_table_after_keyword()
3073 }
3074 Token::Publication => {
3075 self.advance();
3076 // v7.39 (round 754, F31-B4) — the round-753
3077 // audit probe tripped over the missing
3078 // `IF EXISTS` here (syntax error).
3079 let if_exists = self.consume_if_exists();
3080 let name = self.expect_ident_or_string()?;
3081 Ok(Statement::DropPublication { name, if_exists })
3082 }
3083 Token::Subscription => {
3084 self.advance();
3085 let if_exists = self.consume_if_exists();
3086 let name = self.expect_ident_or_string()?;
3087 Ok(Statement::DropSubscription { name, if_exists })
3088 }
3089 Token::Ident(s) | Token::QuotedIdent(s)
3090 if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
3091 {
3092 self.advance();
3093 // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
3094 // login user IS a role in PG, and SPG's store holds
3095 // both. `IF EXISTS` is accepted on either spelling.
3096 let if_exists = self.consume_if_exists();
3097 let name = self.expect_ident_or_string()?;
3098 Ok(Statement::DropUser { name, if_exists })
3099 }
3100 // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
3101 // CREATE DATABASE has parsed since v7.14 and this did
3102 // not, so `DROP DATABASE IF EXISTS x` — what every
3103 // teardown script and pg_dumpall preamble opens with —
3104 // came back as a syntax error, which IF EXISTS cannot
3105 // soften. The name is carried so the engine can answer
3106 // the way PG does; PG never lets this succeed on a
3107 // single-database server, since the name is either
3108 // unknown ("database … does not exist", or a notice
3109 // under IF EXISTS) or the one you are connected to
3110 // ("cannot drop the currently open database").
3111 Token::Ident(s) | Token::QuotedIdent(s)
3112 if s.eq_ignore_ascii_case("database") =>
3113 {
3114 self.advance();
3115 let if_exists = self.consume_if_exists();
3116 let name = self.expect_ident_or_string()?;
3117 self.consume_until_statement_boundary();
3118 Ok(Statement::DropDatabase { name, if_exists })
3119 }
3120 // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
3121 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
3122 self.advance();
3123 let if_exists = self.consume_if_exists();
3124 let name = self.expect_ident_like()?;
3125 // ON <table>
3126 if !matches!(self.peek(), Token::On) {
3127 return Err(self.err(alloc::format!(
3128 "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
3129 self.peek()
3130 )));
3131 }
3132 self.advance();
3133 let table = self.expect_ident_like()?;
3134 Ok(Statement::DropTrigger {
3135 name,
3136 table,
3137 if_exists,
3138 })
3139 }
3140 // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
3141 // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
3142 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
3143 self.advance();
3144 let if_exists = self.consume_if_exists();
3145 let name = self.expect_ident_like()?;
3146 if !matches!(self.peek(), Token::On) {
3147 return Err(self.err(alloc::format!(
3148 "expected ON <table> after DROP RULE {name:?}, got {:?}",
3149 self.peek()
3150 )));
3151 }
3152 self.advance();
3153 let table = self.expect_ident_like()?;
3154 // Optional CASCADE / RESTRICT — accepted, no effect.
3155 self.consume_until_statement_boundary();
3156 Ok(Statement::DropRule {
3157 name,
3158 table,
3159 if_exists,
3160 })
3161 }
3162 // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
3163 // v7.12.4 ignores any optional arg-list (signature-
3164 // based overload disambiguation lands in v7.12.5+).
3165 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
3166 self.advance();
3167 let if_exists = self.consume_if_exists();
3168 let name = self.expect_ident_like()?;
3169 // v7.39 (read01 round 62) — the argument list identifies
3170 // WHICH overload to drop, so it is captured, not
3171 // discarded. `DROP FUNCTION f` (no list) is legal when
3172 // the name is unambiguous; the engine enforces that.
3173 let args = if matches!(self.peek(), Token::LParen) {
3174 Some(self.parse_function_signature_types()?)
3175 } else {
3176 None
3177 };
3178 // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
3179 // trailer, which `DROP TABLE` and `DROP INDEX` have
3180 // accepted since v7.14 and this one refused outright.
3181 // pg_dump writes it, so refusing was a parse error in
3182 // the middle of a restore. SPG drops the function
3183 // either way — it tracks no dependents to cascade to —
3184 // which is the same reading the other two give it.
3185 self.consume_drop_behaviour();
3186 Ok(Statement::DropFunction {
3187 name,
3188 args,
3189 if_exists,
3190 })
3191 }
3192 // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
3193 // [CASCADE|RESTRICT]. pg_dump and mysqldump both
3194 // emit DROP TABLE IF EXISTS at the head of every
3195 // CREATE TABLE block so re-importing a dump
3196 // overwrites prior state. SPG accepts and removes
3197 // matching tables; CASCADE/RESTRICT trailers
3198 // accepted silently.
3199 Token::Table => self.parse_drop_table_after_keyword(),
3200 // v7.14.0 — DROP INDEX [IF EXISTS] name
3201 // [CASCADE|RESTRICT]. PG / mysqldump emit this
3202 // for partial-index renames and pgvector
3203 // migrations. SPG removes the matching index;
3204 // IF EXISTS makes the drop idempotent.
3205 Token::Index => {
3206 self.advance();
3207 let if_exists_at = self.pos;
3208 let if_exists = self.consume_if_exists();
3209 let name = self.expect_ident_like()?;
3210 // v7.39.7 — MySQL's own spelling, which SPG
3211 // refused.
3212 //
3213 // `DROP INDEX i ON t` is how MySQL drops an
3214 // index; its names live inside a table, so the
3215 // statement names the table. Measured against
3216 // MySQL 9.7.2: the form above works, and the
3217 // bare `DROP INDEX i` PostgreSQL uses is a 1064
3218 // there. SPG had it exactly backwards on the
3219 // MySQL wire — the bare form accepted, MySQL's
3220 // own a syntax error — so a migration that drops
3221 // an index failed against the drop-in and not
3222 // against the thing it replaces.
3223 let table = if matches!(self.peek(), Token::On) {
3224 self.advance();
3225 Some(self.expect_ident_like()?)
3226 } else {
3227 None
3228 };
3229 if self.mysql_dialect {
3230 // MySQL has no `IF EXISTS` here either:
3231 // `DROP INDEX IF EXISTS i ON t` is a 1064.
3232 if if_exists {
3233 return Err(self.err_at(
3234 if_exists_at,
3235 "MySQL has no IF EXISTS on DROP INDEX".into(),
3236 ));
3237 }
3238 if table.is_none() {
3239 return Err(self.err("expected ON after the index name".into()));
3240 }
3241 }
3242 if matches!(
3243 self.peek(),
3244 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3245 || s.eq_ignore_ascii_case("restrict")
3246 ) {
3247 self.advance();
3248 }
3249 Ok(Statement::DropIndex {
3250 name,
3251 if_exists,
3252 table,
3253 })
3254 }
3255 // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3256 // [CASCADE|RESTRICT]. SPG is single-database;
3257 // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3258 // name [, name…] [CASCADE | RESTRICT]. Real
3259 // unregister (was silent no-op pre-v7.17).
3260 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3261 self.advance();
3262 let if_exists = self.consume_if_exists();
3263 let mut names = vec![self.expect_ident_like()?];
3264 while matches!(self.peek(), Token::Comma) {
3265 self.advance();
3266 names.push(self.expect_ident_like()?);
3267 }
3268 if matches!(
3269 self.peek(),
3270 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3271 || s.eq_ignore_ascii_case("restrict")
3272 ) {
3273 self.advance();
3274 }
3275 Ok(Statement::DropSchema { names, if_exists })
3276 }
3277 // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3278 // name [, name…] [CASCADE|RESTRICT].
3279 Token::Ident(s) | Token::QuotedIdent(s)
3280 if s.eq_ignore_ascii_case("type") =>
3281 {
3282 self.advance();
3283 let if_exists = self.consume_if_exists();
3284 let mut names = vec![self.expect_ident_like()?];
3285 while matches!(self.peek(), Token::Comma) {
3286 self.advance();
3287 names.push(self.expect_ident_like()?);
3288 }
3289 if matches!(
3290 self.peek(),
3291 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3292 || s.eq_ignore_ascii_case("restrict")
3293 ) {
3294 self.advance();
3295 }
3296 Ok(Statement::DropType { names, if_exists })
3297 }
3298 // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3299 // name [, name…] [CASCADE|RESTRICT].
3300 Token::Ident(s) | Token::QuotedIdent(s)
3301 if s.eq_ignore_ascii_case("domain") =>
3302 {
3303 self.advance();
3304 let if_exists = self.consume_if_exists();
3305 let mut names = vec![self.expect_ident_like()?];
3306 while matches!(self.peek(), Token::Comma) {
3307 self.advance();
3308 names.push(self.expect_ident_like()?);
3309 }
3310 if matches!(
3311 self.peek(),
3312 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3313 || s.eq_ignore_ascii_case("restrict")
3314 ) {
3315 self.advance();
3316 }
3317 Ok(Statement::DropDomain { names, if_exists })
3318 }
3319 // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3320 // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3321 Token::Ident(s) | Token::QuotedIdent(s)
3322 if s.eq_ignore_ascii_case("materialized") =>
3323 {
3324 self.advance();
3325 let nxt = self.peek().clone();
3326 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3327 {
3328 return Err(self.err(alloc::format!(
3329 "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3330 )));
3331 }
3332 self.advance();
3333 let if_exists = self.consume_if_exists();
3334 let mut names = vec![self.expect_ident_like()?];
3335 while matches!(self.peek(), Token::Comma) {
3336 self.advance();
3337 names.push(self.expect_ident_like()?);
3338 }
3339 if matches!(
3340 self.peek(),
3341 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3342 || s.eq_ignore_ascii_case("restrict")
3343 ) {
3344 self.advance();
3345 }
3346 Ok(Statement::DropMaterializedView { names, if_exists })
3347 }
3348 // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3349 // name [, name…] [CASCADE|RESTRICT].
3350 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3351 self.advance();
3352 let if_exists = self.consume_if_exists();
3353 let mut names = vec![self.expect_ident_like()?];
3354 while matches!(self.peek(), Token::Comma) {
3355 self.advance();
3356 names.push(self.expect_ident_like()?);
3357 }
3358 if matches!(
3359 self.peek(),
3360 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3361 || s.eq_ignore_ascii_case("restrict")
3362 ) {
3363 self.advance();
3364 }
3365 Ok(Statement::DropView { names, if_exists })
3366 }
3367 // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3368 // [CASCADE|RESTRICT]. Real removal from catalog
3369 // (was a silent no-op pre-v7.17).
3370 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3371 self.advance();
3372 let if_exists = self.consume_if_exists();
3373 let mut names = vec![self.expect_ident_like()?];
3374 while matches!(self.peek(), Token::Comma) {
3375 self.advance();
3376 names.push(self.expect_ident_like()?);
3377 }
3378 if matches!(
3379 self.peek(),
3380 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3381 || s.eq_ignore_ascii_case("restrict")
3382 ) {
3383 self.advance();
3384 }
3385 Ok(Statement::DropSequence { names, if_exists })
3386 }
3387 // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3388 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3389 self.advance();
3390 self.parse_drop_policy_after_keyword()
3391 }
3392 // v7.37.17 (17.6 siblings) — DROP <target> for
3393 // targets SPG doesn't natively track. pg_dump
3394 // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3395 // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3396 // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3397 // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3398 // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3399 // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3400 // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3401 // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3402 // etc. — accept + Empty-return so pg_dump tails
3403 // load through. Materialized-view drop dispatches
3404 // to the existing DropTable path when the token
3405 // is Materialized-View-shaped (elsewhere in
3406 // this parser).
3407 Token::Ident(s) | Token::QuotedIdent(s)
3408 if s.eq_ignore_ascii_case("text")
3409 // The DROP dispatch matches on PEEK — `text` is
3410 // not yet consumed, so SEARCH/CONFIGURATION sit
3411 // at pos+1/pos+2 (the round-695 trap's mirror).
3412 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3413 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3414 {
3415 // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3416 // validates the name; DICTIONARY / PARSER / TEMPLATE
3417 // stay in the noise arm below.
3418 self.advance(); // TEXT
3419 self.advance(); // SEARCH
3420 self.advance(); // CONFIGURATION
3421 let if_exists = self.consume_if_exists();
3422 let names = self.take_comma_separated_names();
3423 self.consume_until_statement_boundary();
3424 if if_exists {
3425 return Ok(Statement::Empty);
3426 }
3427 Ok(Statement::ValidateOnly {
3428 kind: crate::ast::ValidateOnlyKind::TsConfigName,
3429 names,
3430 })
3431 }
3432 Token::Ident(s) | Token::QuotedIdent(s)
3433 if matches!(
3434 s.to_ascii_lowercase().as_str(),
3435 "type"
3436 | "domain"
3437 | "operator"
3438 | "cast"
3439 // `text` = TEXT SEARCH DICTIONARY / PARSER /
3440 // TEMPLATE (CONFIGURATION intercepted above).
3441 | "text"
3442 | "materialized"
3443 | "large"
3444 | "role"
3445 | "access"
3446 | "procedure"
3447 | "routine"
3448 ) =>
3449 {
3450 self.consume_until_statement_boundary();
3451 Ok(Statement::Empty)
3452 }
3453 // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3454 // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3455 // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3456 // foreign-data warning family (round 706) so a
3457 // CREATE→DROP sequence in a dump stays consistent.
3458 Token::Ident(s) | Token::QuotedIdent(s)
3459 if s.eq_ignore_ascii_case("server")
3460 || s.eq_ignore_ascii_case("foreign") =>
3461 {
3462 self.advance();
3463 self.consume_until_statement_boundary();
3464 Ok(Statement::ValidateOnly {
3465 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3466 names: Vec::new(),
3467 })
3468 }
3469 Token::Ident(s) | Token::QuotedIdent(s)
3470 if s.eq_ignore_ascii_case("collation")
3471 || s.eq_ignore_ascii_case("tablespace") =>
3472 {
3473 let kind = if s.eq_ignore_ascii_case("collation") {
3474 crate::ast::ValidateOnlyKind::CollationName
3475 } else {
3476 crate::ast::ValidateOnlyKind::TablespaceName
3477 };
3478 self.advance();
3479 let if_exists = self.consume_if_exists();
3480 let names = self.take_comma_separated_names();
3481 self.consume_until_statement_boundary();
3482 if if_exists {
3483 return Ok(Statement::Empty);
3484 }
3485 Ok(Statement::ValidateOnly { kind, names })
3486 }
3487 Token::Ident(s) | Token::QuotedIdent(s)
3488 if s.eq_ignore_ascii_case("event") =>
3489 {
3490 self.advance();
3491 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3492 {
3493 self.advance();
3494 }
3495 let if_exists = self.consume_if_exists();
3496 let names = self.take_comma_separated_names();
3497 self.consume_until_statement_boundary();
3498 if if_exists {
3499 return Ok(Statement::Empty);
3500 }
3501 Ok(Statement::ValidateOnly {
3502 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3503 names,
3504 })
3505 }
3506 // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3507 // leave the noise list; see the ValidateOnly kinds.
3508 Token::Ident(s) | Token::QuotedIdent(s)
3509 if s.eq_ignore_ascii_case("conversion")
3510 || s.eq_ignore_ascii_case("language")
3511 // `DROP PROCEDURAL LANGUAGE` puts the modifier
3512 // FIRST — the first draft looked for it after.
3513 || s.eq_ignore_ascii_case("procedural") =>
3514 {
3515 let kind = if s.eq_ignore_ascii_case("conversion") {
3516 crate::ast::ValidateOnlyKind::ConversionName
3517 } else {
3518 crate::ast::ValidateOnlyKind::LanguageName
3519 };
3520 self.advance();
3521 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3522 {
3523 self.advance();
3524 }
3525 let if_exists = self.consume_if_exists();
3526 let names = self.take_comma_separated_names();
3527 self.consume_until_statement_boundary();
3528 if if_exists {
3529 return Ok(Statement::Empty);
3530 }
3531 Ok(Statement::ValidateOnly { kind, names })
3532 }
3533 // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3534 // name(argtypes)[, …]`. Parsed for real so the engine
3535 // can answer as PG does; see Statement::DropAggregate.
3536 Token::Ident(s) | Token::QuotedIdent(s)
3537 if s.eq_ignore_ascii_case("aggregate") =>
3538 {
3539 self.advance();
3540 let if_exists = self.consume_if_exists();
3541 let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3542 loop {
3543 let name = self.expect_ident_like()?;
3544 if !matches!(self.peek(), Token::LParen) {
3545 return Err(self.err(alloc::format!(
3546 "expected argument list after DROP AGGREGATE {name}"
3547 )));
3548 }
3549 self.advance();
3550 let mut args: Vec<String> = Vec::new();
3551 let mut star = false;
3552 loop {
3553 match self.peek().clone() {
3554 Token::RParen => {
3555 self.advance();
3556 break;
3557 }
3558 Token::Star => {
3559 self.advance();
3560 star = true;
3561 }
3562 Token::Comma => {
3563 self.advance();
3564 }
3565 _ => {
3566 // A type name may be multi-token
3567 // (`double precision`); glue idents
3568 // until , or ).
3569 let mut t = self.expect_ident_like()?;
3570 while let Token::Ident(nx) = self.peek() {
3571 let nx = nx.clone();
3572 self.advance();
3573 t.push(' ');
3574 t.push_str(&nx);
3575 }
3576 args.push(t);
3577 }
3578 }
3579 }
3580 items.push((name, if star { None } else { Some(args) }));
3581 if matches!(self.peek(), Token::Comma) {
3582 self.advance();
3583 } else {
3584 break;
3585 }
3586 }
3587 self.consume_until_statement_boundary();
3588 Ok(Statement::DropAggregate { if_exists, items })
3589 }
3590 // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3591 // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3592 // installed; `IF EXISTS` is the spelling that says do
3593 // not, and it keeps the no-op.
3594 Token::Ident(s) | Token::QuotedIdent(s)
3595 if s.eq_ignore_ascii_case("extension") =>
3596 {
3597 self.advance();
3598 let if_exists = self.consume_if_exists();
3599 let names = self.take_comma_separated_names();
3600 self.consume_until_statement_boundary();
3601 if if_exists {
3602 return Ok(Statement::Empty);
3603 }
3604 Ok(Statement::ValidateOnly {
3605 kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3606 names,
3607 })
3608 }
3609 Token::Ident(s) | Token::QuotedIdent(s)
3610 if s.eq_ignore_ascii_case("statistics") =>
3611 {
3612 self.parse_drop_statistics_after_drop()
3613 }
3614 other => Err(self.err(format!(
3615 "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3616 SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3617 ))),
3618 }
3619 }
3620 // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3621 // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3622 // and accepted before the view name. SPG materialised
3623 // views re-evaluate on read (always-fresh semantics), so
3624 // the CONCURRENTLY-vs-serial distinction has no runtime
3625 // effect — the refresh body does not block readers either
3626 // way. Same accept-and-no-op pattern as DETACH PARTITION
3627 // CONCURRENTLY (16.5).
3628 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3629 self.advance();
3630 let nxt = self.peek().clone();
3631 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3632 {
3633 return Err(self.err(alloc::format!(
3634 "expected MATERIALIZED after REFRESH, got {nxt:?}"
3635 )));
3636 }
3637 self.advance();
3638 let nxt2 = self.peek().clone();
3639 if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3640 {
3641 return Err(self.err(alloc::format!(
3642 "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3643 )));
3644 }
3645 self.advance();
3646 // Optional CONCURRENTLY noise word — consumed without
3647 // changing semantics.
3648 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3649 {
3650 self.advance();
3651 }
3652 let name = self.expect_ident_like()?;
3653 let with_data = self.parse_optional_with_data(true)?;
3654 Ok(Statement::RefreshMaterializedView { name, with_data })
3655 }
3656 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3657 self.advance();
3658 self.parse_update_after_keyword()
3659 }
3660 // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3661 // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3662 // [CASCADE | RESTRICT]. Clears every row from each named
3663 // table. Parses at the top level; the engine dispatcher
3664 // walks Statement::Truncate.
3665 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3666 self.advance();
3667 // Optional TABLE noise word — PG accepts both the reserved
3668 // token and the bare identifier spelling.
3669 if matches!(self.peek(), Token::Table)
3670 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3671 {
3672 self.advance();
3673 }
3674 // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3675 // not absorbed. The lookahead keeps a table genuinely
3676 // called `only` working: the keyword is a keyword only
3677 // when a name follows it.
3678 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3679 if s.eq_ignore_ascii_case("only"))
3680 && matches!(
3681 self.tokens.get(self.pos + 1),
3682 Some(Token::Ident(_) | Token::QuotedIdent(_))
3683 );
3684 if only {
3685 self.advance();
3686 }
3687 // Table names (comma-separated).
3688 let mut tables = Vec::new();
3689 loop {
3690 tables.push(self.expect_ident_like()?);
3691 if matches!(self.peek(), Token::Comma) {
3692 self.advance();
3693 continue;
3694 }
3695 break;
3696 }
3697 // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3698 let mut restart_identity = false;
3699 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3700 {
3701 self.advance();
3702 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3703 {
3704 self.advance();
3705 restart_identity = true;
3706 }
3707 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3708 {
3709 self.advance();
3710 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3711 {
3712 self.advance();
3713 }
3714 }
3715 // Optional CASCADE / RESTRICT.
3716 let mut cascade = false;
3717 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3718 {
3719 self.advance();
3720 cascade = true;
3721 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3722 {
3723 self.advance();
3724 }
3725 Ok(Statement::Truncate {
3726 tables,
3727 restart_identity,
3728 cascade,
3729 only,
3730 })
3731 }
3732 // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3733 // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3734 // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3735 // rows change so the index tree is always up-to-date;
3736 // REINDEX is a strict no-op. Accept the whole statement
3737 // shape to boundary for pg_dump round-trip compatibility.
3738 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3739 // v7.39 (round 535) — the target is CARRIED now. SPG has no
3740 // index bloat to rebuild, so the work stays a no-op, but PG
3741 // validates what it was pointed at and this swallowed the
3742 // name at parse time — `REINDEX TABLE typo` reported
3743 // success. Measured on PG18: INDEX / TABLE name a relation,
3744 // SCHEMA a schema, SYSTEM nothing.
3745 self.advance();
3746 self.parse_reindex_tail()
3747 }
3748 // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3749 // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3750 // SPG has no MVCC bloat today (Phase D visibility map
3751 // queues with v7.38); the freezer collapses hot-tier
3752 // rows into cold segments automatically. VACUUM is a
3753 // no-op — pg_dump maintenance scripts and Discourse's
3754 // periodic-maintenance path both emit it.
3755 // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3756 // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3757 // actual bloat, so the pre-MVCC accept-and-ignore posture
3758 // became a silent no-op on a customer's manual reclaim.
3759 // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3760 // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3761 // ANALYZE is captured, the optional table name is captured.
3762 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3763 self.advance();
3764 // Parenthesised option list: absorb it.
3765 if matches!(self.peek(), Token::LParen) {
3766 let mut depth = 0usize;
3767 loop {
3768 match self.advance() {
3769 Token::LParen => depth += 1,
3770 Token::RParen => {
3771 depth -= 1;
3772 if depth == 0 {
3773 break;
3774 }
3775 }
3776 Token::Eof => break,
3777 _ => {}
3778 }
3779 }
3780 }
3781 let mut analyze = false;
3782 let mut table: Option<String> = None;
3783 loop {
3784 match self.peek() {
3785 // v7.39 (round 535) — `FULL` lexes as a keyword, not
3786 // an identifier, so the loop below broke out on it and
3787 // dropped the table name: `VACUUM FULL nosuch` was
3788 // accepted where `VACUUM nosuch` was refused.
3789 Token::Full => {
3790 self.advance();
3791 }
3792 Token::Ident(w) | Token::QuotedIdent(w) => {
3793 let wl = w.to_ascii_lowercase();
3794 match wl.as_str() {
3795 "full" | "freeze" | "verbose" => {
3796 self.advance();
3797 }
3798 "analyze" | "analyse" => {
3799 analyze = true;
3800 self.advance();
3801 }
3802 _ => {
3803 table = Some(self.expect_ident_like()?);
3804 break;
3805 }
3806 }
3807 }
3808 _ => break,
3809 }
3810 }
3811 // Optional trailing column list / anything else to the
3812 // statement boundary (PG accepts per-column ANALYZE).
3813 self.consume_until_statement_boundary();
3814 Ok(Statement::Vacuum { table, analyze })
3815 }
3816 // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3817 // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3818 // <index>. PG stores rows in physical order matching
3819 // an index; SPG's hot-tier is append-only + cold-tier
3820 // is segment-frozen, so clustering has no persistent
3821 // effect. Accept-and-no-op for pg_dump compat.
3822 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3823 // v7.39 (round 535) — same as REINDEX above: the relation is
3824 // carried so the engine can refuse one that does not exist.
3825 // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3826 self.advance();
3827 self.parse_cluster_tail()
3828 }
3829 // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3830 // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3831 // optional string payload; UNLISTEN takes a channel or `*`.
3832 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3833 self.advance();
3834 let ch = match self.advance() {
3835 Token::Ident(c) | Token::QuotedIdent(c) => c,
3836 other => {
3837 return Err(self.err(format!(
3838 "expected channel name after LISTEN, got {other:?}"
3839 )));
3840 }
3841 };
3842 Ok(Statement::Listen(ch))
3843 }
3844 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3845 self.advance();
3846 let channel = match self.advance() {
3847 Token::Ident(c) | Token::QuotedIdent(c) => c,
3848 other => {
3849 return Err(self.err(format!(
3850 "expected channel name after NOTIFY, got {other:?}"
3851 )));
3852 }
3853 };
3854 let payload = if matches!(self.peek(), Token::Comma) {
3855 self.advance();
3856 match self.advance() {
3857 Token::String(p) => Some(p),
3858 other => {
3859 return Err(self.err(format!(
3860 "expected string payload after NOTIFY <channel>, got {other:?}"
3861 )));
3862 }
3863 }
3864 } else {
3865 None
3866 };
3867 Ok(Statement::Notify { channel, payload })
3868 }
3869 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3870 self.advance();
3871 match self.advance() {
3872 Token::Star => Ok(Statement::Unlisten(None)),
3873 Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3874 other => Err(self.err(format!(
3875 "expected channel name or * after UNLISTEN, got {other:?}"
3876 ))),
3877 }
3878 }
3879 // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3880 // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3881 // process-wide write lock today; explicit LOCK has no
3882 // effect. Accept-and-no-op for pg_dump / migration
3883 // compat.
3884 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3885 self.advance();
3886 // v7.39 (round 696) — the LOCK still has no effect (SPG's
3887 // engine holds a process-wide write lock), but the TABLE
3888 // NAME is now carried out so the engine can refuse one that
3889 // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3890 // READ|WRITE` is a different statement with the same first
3891 // word; it keeps the old no-op, because a MySQL dump's
3892 // bracket names tables it is about to create.
3893 let mysql_tables = matches!(self.peek(), Token::Ident(k)
3894 if k.eq_ignore_ascii_case("tables"));
3895 if mysql_tables {
3896 self.consume_until_statement_boundary();
3897 return Ok(Statement::Empty);
3898 }
3899 if matches!(self.peek(), Token::Table) {
3900 self.advance();
3901 }
3902 let names = self.take_comma_separated_names();
3903 self.consume_until_statement_boundary();
3904 Ok(Statement::ValidateOnly {
3905 kind: crate::ast::ValidateOnlyKind::LockTable,
3906 names,
3907 })
3908 }
3909 // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3910 // durability marker + snapshot in PG. SPG has WAL
3911 // checkpointing on a byte / time schedule (v7.37.10
3912 // 60s / 4 MiB defaults). The bare statement parses to
3913 // `Statement::Empty` here (the no_std engine owns no
3914 // WAL / snapshot); v7.37 Epic Du wires the HOST
3915 // (embedded `Database::execute_buffered`, via
3916 // `sql_is_checkpoint`) to force an immediate synchronous
3917 // checkpoint through `Database::checkpoint` — a real
3918 // durability barrier, matching PG.
3919 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3920 self.advance();
3921 self.consume_until_statement_boundary();
3922 Ok(Statement::Empty)
3923 }
3924 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3925 self.advance();
3926 self.parse_delete_after_keyword()
3927 }
3928 // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3929 // ALTER is not a reserved keyword in the lexer — handled
3930 // as a bare ident here.
3931 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3932 self.advance();
3933 self.parse_alter_after_keyword()
3934 }
3935 // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3936 // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3937 // additions needed.
3938 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3939 self.advance();
3940 self.parse_wait_after_keyword()
3941 }
3942 // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3943 // Bare ANALYZE → analyse every user table; ANALYZE
3944 // <name> → re-stats one. The argument is an optional
3945 // ident (or quoted ident); anything else is a parse
3946 // error.
3947 // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3948 // `WHERE` filter (carved out per V6_7_DESIGN.md
3949 // STABILITY). Lex order: identifier "compact" → "cold"
3950 // → "segments". Anything else after `COMPACT` is a
3951 // parse error.
3952 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3953 self.advance();
3954 let next = self.peek().clone();
3955 let cold = match next {
3956 Token::Ident(s) | Token::QuotedIdent(s) => s,
3957 _ => {
3958 return Err(
3959 self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3960 );
3961 }
3962 };
3963 if !cold.eq_ignore_ascii_case("cold") {
3964 return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3965 }
3966 self.advance();
3967 let next = self.peek().clone();
3968 let segments = match next {
3969 Token::Ident(s) | Token::QuotedIdent(s) => s,
3970 _ => {
3971 return Err(self.err(format!(
3972 "expected SEGMENTS after COMPACT COLD, got {:?}",
3973 self.peek()
3974 )));
3975 }
3976 };
3977 if !segments.eq_ignore_ascii_case("segments") {
3978 return Err(self.err(format!(
3979 "expected SEGMENTS after COMPACT COLD, got {segments:?}"
3980 )));
3981 }
3982 self.advance();
3983 Ok(Statement::CompactColdSegments)
3984 }
3985 // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
3986 // Parsed as a case-insensitive identifier since MERGE
3987 // isn't a reserved lexer keyword (collides with the
3988 // mysqldump `ALGORITHM = MERGE` view clause if it
3989 // were); the inner parser drives the rest of the
3990 // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
3991 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
3992 self.advance();
3993 self.parse_merge_after_keyword()
3994 }
3995 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
3996 self.advance();
3997 let target = match self.peek() {
3998 Token::Eof | Token::Semicolon => None,
3999 Token::Ident(_) | Token::QuotedIdent(_) => {
4000 Some(self.expect_ident_like()?)
4001 }
4002 other => {
4003 return Err(self.err(format!(
4004 "expected table name or end of statement after ANALYZE, got {other:?}"
4005 )));
4006 }
4007 };
4008 // v7.39 (round 776, F31 J7) — the per-column form
4009 // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
4010 // here while the VACUUM arm already consumed it; SPG
4011 // analyzes whole tables, so the list parses and is
4012 // accepted like the VACUUM path's.
4013 if target.is_some() && matches!(self.peek(), Token::LParen) {
4014 self.advance();
4015 loop {
4016 let _ = self.expect_ident_like()?;
4017 match self.peek() {
4018 Token::Comma => {
4019 self.advance();
4020 }
4021 Token::RParen => {
4022 self.advance();
4023 break;
4024 }
4025 other => {
4026 return Err(self.err(format!(
4027 "expected ',' or ')' in ANALYZE column list, got {other:?}"
4028 )));
4029 }
4030 }
4031 }
4032 }
4033 Ok(Statement::Analyze(target))
4034 }
4035 // v7.12.1 — `SET <name> [TO|=] <value>`. The
4036 // `default_text_search_config` parameter is consumed
4037 // by the FTS function dispatcher; other parameter
4038 // names are recorded but treated as a no-op so PG
4039 // dump output loads.
4040 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
4041 self.advance();
4042 // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
4043 // adds `SET GLOBAL` too (and the alias `SET @@global.name =
4044 // …` which the SessionVar path handles). `LOCAL` is the only
4045 // one that changes semantics — it scopes the change to the
4046 // current transaction — so capture it; SESSION / GLOBAL are
4047 // accepted and treated as the default session scope.
4048 let mut set_local = false;
4049 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
4050 let q = s.to_ascii_lowercase();
4051 if q == "local" || q == "session" || q == "global" {
4052 set_local = q == "local";
4053 self.advance();
4054 }
4055 }
4056 // 7.38.1 S5.2 — PG `SET [SESSION] AUTHORIZATION
4057 // { DEFAULT | <role> }`. pg_dump's ACL section switches
4058 // to the object owner with it. SPG maps it onto the
4059 // session-role machinery (recorded delta RD-10: PG moves
4060 // session_user too; SPG moves the effective role).
4061 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4062 if s.eq_ignore_ascii_case("authorization"))
4063 {
4064 self.advance(); // AUTHORIZATION
4065 let role = match self.peek().clone() {
4066 Token::Default => {
4067 self.advance();
4068 None
4069 }
4070 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4071 self.advance();
4072 Some(s)
4073 }
4074 _ => None,
4075 };
4076 return Ok(Statement::SetRole(role));
4077 }
4078 // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
4079 // <collation>]` — change the connection client
4080 // charset. SPG stores UTF-8 always and orders
4081 // bytewise; accept as a no-op.
4082 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
4083 {
4084 self.advance();
4085 // v7.39 — this used to parse the clause and throw it
4086 // away ("SPG stores UTF-8 always and orders
4087 // bytewise; accept as a no-op"). That sentence
4088 // stopped being true when collations arrived, and
4089 // once `collation_connection` began driving literal
4090 // comparison, dropping the COLLATE clause became a
4091 // silently wrong answer: `SET NAMES utf8mb4 COLLATE
4092 // utf8mb4_general_ci` reported back
4093 // `utf8mb4_0900_ai_ci` and compared as NO PAD.
4094 //
4095 // The charset name is emitted as `names` and the
4096 // ENGINE expands it, because which collation a
4097 // charset defaults to is MySQL semantics and belongs
4098 // beside the rest of them, not in the parser.
4099 let mut pairs = alloc::vec::Vec::new();
4100 if matches!(
4101 self.peek(),
4102 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4103 ) {
4104 let charset = match self.advance() {
4105 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4106 _ => unreachable!("peeked an ident-or-string"),
4107 };
4108 pairs.push((String::from("names"), crate::ast::SetValue::Ident(charset)));
4109 }
4110 // Optional `COLLATE <name>` — emitted AFTER `names`
4111 // so it overrides the charset's default, which is
4112 // what MySQL does.
4113 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
4114 {
4115 self.advance();
4116 if matches!(
4117 self.peek(),
4118 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4119 ) {
4120 let coll = match self.advance() {
4121 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4122 _ => unreachable!("peeked an ident-or-string"),
4123 };
4124 pairs.push((
4125 String::from("collation_connection"),
4126 crate::ast::SetValue::Ident(coll),
4127 ));
4128 }
4129 }
4130 if pairs.is_empty() {
4131 return Ok(Statement::Empty);
4132 }
4133 return Ok(Statement::SetParameterList(pairs));
4134 }
4135 // v7.37.17 (17.6 sibling) — PG `SET ROLE
4136 // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
4137 // uses this to switch to the object owner before
4138 // recreating tables. SPG has no role system so this
4139 // is a no-op.
4140 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
4141 {
4142 self.advance(); // ROLE
4143 // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
4144 // reset to the login identity; a name / string sets the
4145 // effective role that drives current_user + RLS.
4146 let role = match self.peek().clone() {
4147 Token::Default => {
4148 self.advance();
4149 None
4150 }
4151 Token::Ident(s) | Token::QuotedIdent(s)
4152 if s.eq_ignore_ascii_case("none") =>
4153 {
4154 self.advance();
4155 None
4156 }
4157 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4158 self.advance();
4159 Some(s)
4160 }
4161 _ => None,
4162 };
4163 return Ok(Statement::SetRole(role));
4164 }
4165 // v7.37.17 (17.6 sibling) — PG `SET SESSION
4166 // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
4167 // ISO SQL surface). pg_dump prepends this to fix
4168 // the isolation level for the restore session. SPG
4169 // defaults to READ COMMITTED and doesn't yet honor
4170 // session-set isolation across statements — accept
4171 // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
4172 // per-tx form is handled elsewhere.
4173 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
4174 {
4175 self.advance(); // CHARACTERISTICS
4176 if matches!(self.peek(), Token::As) {
4177 self.advance();
4178 }
4179 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
4180 self.advance();
4181 }
4182 // v7.39 — no longer a no-op. The note above said SPG
4183 // "doesn't yet honor session-set isolation across
4184 // statements"; it does now, through
4185 // `default_transaction_isolation`, and measured on
4186 // PG 18.6 this statement is exactly a way to set it:
4187 //
4188 // SET SESSION CHARACTERISTICS AS TRANSACTION
4189 // ISOLATION LEVEL REPEATABLE READ;
4190 // current_setting('default_transaction_isolation')
4191 // -> repeatable read
4192 //
4193 // pg_dump prepends this to fix the level for a
4194 // restore session, so accepting it and doing nothing
4195 // meant the restore ran at a level nobody chose.
4196 //
4197 // The trailing READ ONLY / [NOT] DEFERRABLE modes are
4198 // still consumed and dropped. `default_transaction_read_only`
4199 // exists in the GUC inventory but nothing enforces it,
4200 // and setting a value no code honours is the very
4201 // defect this version is about — a session told it
4202 // holds a guarantee it does not.
4203 let modes = self.parse_isolation_level_clauses()?;
4204 self.consume_until_statement_boundary();
4205 let mut pairs: alloc::vec::Vec<(
4206 alloc::string::String,
4207 crate::ast::SetValue,
4208 )> = alloc::vec::Vec::new();
4209 if let Some(level) = modes.isolation {
4210 pairs.push((
4211 alloc::string::String::from("default_transaction_isolation"),
4212 crate::ast::SetValue::String(alloc::string::String::from(
4213 level.as_pg_str(),
4214 )),
4215 ));
4216 }
4217 if let Some(ro) = modes.read_only {
4218 pairs.push((
4219 alloc::string::String::from("default_transaction_read_only"),
4220 crate::ast::SetValue::Ident(alloc::string::String::from(if ro {
4221 "on"
4222 } else {
4223 "off"
4224 })),
4225 ));
4226 }
4227 return Ok(if pairs.is_empty() {
4228 Statement::Empty
4229 } else {
4230 Statement::SetParameterList(pairs)
4231 });
4232 }
4233 // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
4234 // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
4235 // pg_dump emits this to control the deferrability of
4236 // FK / UNIQUE constraints across a bulk restore. SPG
4237 // has no deferrable-constraint machinery today; the
4238 // FK checker is strict-immediate. Accept-and-no-op
4239 // for pg_dump round-trip compatibility.
4240 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
4241 {
4242 self.advance(); // CONSTRAINTS
4243 // v7.39 (round 288) — no longer a no-op: the trailing
4244 // DEFERRED / IMMEDIATE sets the transaction's timing.
4245 // v7.39 (round 308, V29) — and the names are kept.
4246 // They used to be skipped over on the way to the
4247 // DEFERRED keyword, so a named form silently behaved
4248 // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
4249 // every deferrable constraint in the transaction.
4250 let mut names: alloc::vec::Vec<alloc::string::String> =
4251 alloc::vec::Vec::new();
4252 if matches!(self.peek(), Token::All) {
4253 self.advance();
4254 } else {
4255 loop {
4256 let mut n = self.expect_ident_like()?;
4257 // A schema-qualified name (`public.fk_a`)
4258 // identifies the same constraint; PG resolves
4259 // it by the trailing segment.
4260 while matches!(self.peek(), Token::Dot) {
4261 self.advance();
4262 n = self.expect_ident_like()?;
4263 }
4264 names.push(n);
4265 if matches!(self.peek(), Token::Comma) {
4266 self.advance();
4267 } else {
4268 break;
4269 }
4270 }
4271 }
4272 let deferred = match self.peek() {
4273 Token::Ident(s) | Token::QuotedIdent(s)
4274 if s.eq_ignore_ascii_case("deferred") =>
4275 {
4276 true
4277 }
4278 Token::Ident(s) | Token::QuotedIdent(s)
4279 if s.eq_ignore_ascii_case("immediate") =>
4280 {
4281 false
4282 }
4283 other => {
4284 return Err(self.err(alloc::format!(
4285 "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
4286 )));
4287 }
4288 };
4289 self.advance();
4290 return Ok(Statement::SetConstraints { names, deferred });
4291 }
4292 // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
4293 // { DEFAULT | '<role>' | <ident> }` (mailrs
4294 // round-10 A.1). pg_dump preamble emits the
4295 // `DEFAULT` form to reset session authorization.
4296 //
4297 // v7.39 (round 697) — this said "SPG has no role system so
4298 // this is a strict no-op". SPG has had one since round 58;
4299 // the comment outlived it, and with it the reason a name
4300 // that is not a role was accepted here. It still switches
4301 // no authorization — what it does now is refuse a role
4302 // that does not exist, as PG18 does. PG also accepts `RESET SESSION
4303 // AUTHORIZATION` (handled by the RESET parser
4304 // elsewhere). Reference:
4305 // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
4306 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
4307 {
4308 self.advance(); // AUTHORIZATION
4309 match self.peek().clone() {
4310 Token::Default => {
4311 self.advance();
4312 }
4313 Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4314 self.advance();
4315 return Ok(Statement::ValidateOnly {
4316 kind: crate::ast::ValidateOnlyKind::RoleName,
4317 names: alloc::vec![r],
4318 });
4319 }
4320 other => {
4321 return Err(self.err(alloc::format!(
4322 "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4323 )));
4324 }
4325 }
4326 return Ok(Statement::Empty);
4327 }
4328 // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4329 // ISOLATION LEVEL { READ COMMITTED | READ
4330 // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4331 // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4332 // PG-standard surface. v7.37.8 accepts the syntax
4333 // and tracks the selected level on
4334 // `Engine::current_isolation_level()`; the actual
4335 // MVCC / SSI semantics implementation lands in
4336 // the 轴 4 isolation framework (separate train).
4337 // PG itself maps READ UNCOMMITTED to READ COMMITTED
4338 // internally; SPG behaves the same (effectively
4339 // READ COMMITTED at every level today).
4340 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4341 {
4342 self.advance(); // TRANSACTION
4343 let modes = self.parse_isolation_level_clauses()?;
4344 return Ok(Statement::SetTransaction { modes });
4345 }
4346 // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4347 // alias — same accept-as-no-op as SET NAMES.
4348 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4349 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4350 {
4351 self.advance(); // CHARACTER
4352 self.advance(); // SET
4353 if matches!(
4354 self.peek(),
4355 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4356 ) {
4357 self.advance();
4358 }
4359 return Ok(Statement::Empty);
4360 }
4361 // v7.39 (GUC) — PG spells the timezone GUC as two
4362 // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4363 // where <value> is a string/ident or the LOCAL /
4364 // DEFAULT keyword (both mean "back to the default").
4365 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4366 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4367 {
4368 self.advance(); // TIME
4369 self.advance(); // ZONE
4370 let value = match self.peek().clone() {
4371 Token::Ident(s)
4372 if s.eq_ignore_ascii_case("local")
4373 || s.eq_ignore_ascii_case("default") =>
4374 {
4375 self.advance();
4376 crate::ast::SetValue::Default
4377 }
4378 Token::Default => {
4379 self.advance();
4380 crate::ast::SetValue::Default
4381 }
4382 _ => self.parse_set_value()?,
4383 };
4384 return Ok(Statement::SetParameter {
4385 name: "timezone".into(),
4386 value,
4387 local: set_local,
4388 });
4389 }
4390 // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4391 // MySQL USER-variable assignment: its own per-session
4392 // namespace, an arbitrary expression on the right, and `:=`
4393 // as a second spelling of `=`. It used to fall into the
4394 // session-PARAMETER list below, whose values are literals and
4395 // whose store nothing reads back under a `@` name — so the
4396 // assignment reported success and vanished.
4397 //
4398 // A `@@`-prefixed LHS is a real engine setting and keeps the
4399 // old path.
4400 if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4401 return self.parse_set_user_vars();
4402 }
4403 // v7.14.0 — multi-assignment form
4404 // `SET a = 1, b = 2, …`. Single-assignment is the
4405 // 1-element case. Each LHS may be a regular ident
4406 // or a SessionVar (`@VAR` / `@@VAR`).
4407 let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4408 loop {
4409 let lhs = match self.peek().clone() {
4410 Token::SessionVar(s) => {
4411 self.advance();
4412 s
4413 }
4414 Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4415 other => {
4416 return Err(self.err(format!(
4417 "expected parameter name after SET, got {other:?}"
4418 )));
4419 }
4420 };
4421 // Accept either `=` or the bare `TO` keyword.
4422 match self.peek() {
4423 Token::Eq => {
4424 self.advance();
4425 }
4426 Token::To => {
4427 self.advance();
4428 }
4429 other => {
4430 return Err(self.err(format!(
4431 "expected `=` or TO after SET {lhs}, got {other:?}"
4432 )));
4433 }
4434 }
4435 let mut value = self.parse_set_value()?;
4436 // v7.39 (GUC) — disambiguate the comma: `, name =` /
4437 // `, name TO` continues a MySQL-style multi-assign,
4438 // anything else is a PG list VALUE
4439 // (`SET search_path = myschema, public`) folded into
4440 // one comma-joined string.
4441 while matches!(self.peek(), Token::Comma) {
4442 let is_assign = matches!(
4443 self.tokens.get(self.pos + 1),
4444 Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4445 ) && matches!(
4446 self.tokens.get(self.pos + 2),
4447 Some(Token::Eq | Token::To)
4448 );
4449 if is_assign {
4450 break;
4451 }
4452 self.advance(); // comma
4453 let next = self.parse_set_value()?;
4454 let joined = alloc::format!(
4455 "{}, {}",
4456 set_value_text(&value),
4457 set_value_text(&next)
4458 );
4459 value = crate::ast::SetValue::String(joined);
4460 }
4461 pairs.push((lhs, value));
4462 if matches!(self.peek(), Token::Comma) {
4463 self.advance();
4464 continue;
4465 }
4466 break;
4467 }
4468 if pairs.len() == 1 {
4469 let (name, value) = pairs.into_iter().next().unwrap();
4470 Ok(Statement::SetParameter {
4471 name,
4472 value,
4473 local: set_local,
4474 })
4475 } else {
4476 Ok(Statement::SetParameterList(pairs))
4477 }
4478 }
4479 // v7.12.1 — `RESET <name>` / `RESET ALL`.
4480 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4481 self.advance();
4482 match self.peek().clone() {
4483 Token::All => {
4484 self.advance();
4485 Ok(Statement::ResetParameter(None))
4486 }
4487 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4488 self.advance();
4489 Ok(Statement::ResetParameter(None))
4490 }
4491 // v7.39 (RLS) — `RESET ROLE` clears the session role.
4492 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4493 self.advance();
4494 Ok(Statement::SetRole(None))
4495 }
4496 // 7.38.1 S5.2 — `RESET SESSION AUTHORIZATION`
4497 // (pg_dump's return from the owner switch).
4498 Token::Ident(s) | Token::QuotedIdent(s)
4499 if s.eq_ignore_ascii_case("session")
4500 && matches!(
4501 self.tokens.get(self.pos + 1),
4502 Some(Token::Ident(a) | Token::QuotedIdent(a))
4503 if a.eq_ignore_ascii_case("authorization")
4504 ) =>
4505 {
4506 self.advance(); // SESSION
4507 self.advance(); // AUTHORIZATION
4508 Ok(Statement::SetRole(None))
4509 }
4510 _ => {
4511 let name = self.parse_set_param_name()?;
4512 Ok(Statement::ResetParameter(Some(name)))
4513 }
4514 }
4515 }
4516 // v7.39 (round 218) — server-side cursors.
4517 Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4518 Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4519 Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4520 Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4521 self.advance();
4522 match self.peek().clone() {
4523 Token::All => {
4524 self.advance();
4525 Ok(Statement::CloseCursor { name: None })
4526 }
4527 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4528 self.advance();
4529 Ok(Statement::CloseCursor { name: None })
4530 }
4531 Token::Ident(n) | Token::QuotedIdent(n) => {
4532 self.advance();
4533 Ok(Statement::CloseCursor { name: Some(n) })
4534 }
4535 other => Err(self.err(format!(
4536 "expected cursor name or ALL after CLOSE, got {other:?}"
4537 ))),
4538 }
4539 }
4540 other => Err(self.err(format!(
4541 "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4542 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4543 ))),
4544 }
4545 }
4546
4547 /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4548 /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4549 /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4550 /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4551 fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4552 self.advance(); // DECLARE
4553 let name = match self.advance() {
4554 Token::Ident(n) | Token::QuotedIdent(n) => n,
4555 other => {
4556 return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4557 }
4558 };
4559 let mut scroll: Option<bool> = None;
4560 loop {
4561 match self.peek() {
4562 Token::Ident(s)
4563 if s.eq_ignore_ascii_case("binary")
4564 || s.eq_ignore_ascii_case("insensitive")
4565 || s.eq_ignore_ascii_case("asensitive") =>
4566 {
4567 self.advance();
4568 }
4569 Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4570 self.advance();
4571 scroll = Some(true);
4572 }
4573 Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4574 {
4575 self.advance(); // NO
4576 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4577 return Err(self.err(format!(
4578 "expected SCROLL after NO in DECLARE, got {:?}",
4579 self.peek()
4580 )));
4581 }
4582 self.advance();
4583 scroll = Some(false);
4584 }
4585 _ => break,
4586 }
4587 }
4588 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4589 return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4590 }
4591 self.advance();
4592 let mut hold = false;
4593 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4594 self.advance();
4595 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4596 return Err(self.err(format!(
4597 "expected HOLD after WITH in DECLARE, got {:?}",
4598 self.peek()
4599 )));
4600 }
4601 self.advance();
4602 hold = true;
4603 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4604 self.advance();
4605 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4606 return Err(self.err(format!(
4607 "expected HOLD after WITHOUT in DECLARE, got {:?}",
4608 self.peek()
4609 )));
4610 }
4611 self.advance();
4612 }
4613 if !matches!(self.peek(), Token::For) {
4614 return Err(self.err(format!(
4615 "expected FOR before the cursor query, got {:?}",
4616 self.peek()
4617 )));
4618 }
4619 self.advance();
4620 let query = self.parse_one_statement()?;
4621 Ok(Statement::DeclareCursor {
4622 name,
4623 scroll,
4624 hold,
4625 query: alloc::boxed::Box::new(query),
4626 })
4627 }
4628
4629 /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4630 /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4631 /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4632 fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4633 use crate::ast::CursorDirection as D;
4634 self.advance(); // FETCH / MOVE
4635 let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4636 let neg = if matches!(this.peek(), Token::Minus) {
4637 this.advance();
4638 true
4639 } else {
4640 false
4641 };
4642 match this.advance() {
4643 Token::Integer(v) => Ok(if neg { -v } else { v }),
4644 other => Err(this.err(format!("expected count, got {other:?}"))),
4645 }
4646 };
4647 let direction = match self.peek().clone() {
4648 Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4649 self.advance();
4650 D::Next
4651 }
4652 Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4653 self.advance();
4654 D::Prior
4655 }
4656 Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4657 self.advance();
4658 D::First
4659 }
4660 Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4661 self.advance();
4662 D::Last
4663 }
4664 Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4665 self.advance();
4666 D::Absolute(signed_count(self)?)
4667 }
4668 Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4669 self.advance();
4670 D::Relative(signed_count(self)?)
4671 }
4672 Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4673 self.advance();
4674 match self.peek().clone() {
4675 Token::All => {
4676 self.advance();
4677 D::All
4678 }
4679 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4680 self.advance();
4681 D::All
4682 }
4683 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4684 _ => D::Next, // bare FORWARD = FORWARD 1
4685 }
4686 }
4687 Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4688 self.advance();
4689 match self.peek().clone() {
4690 Token::All => {
4691 self.advance();
4692 D::BackwardAll
4693 }
4694 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4695 self.advance();
4696 D::BackwardAll
4697 }
4698 Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4699 _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4700 }
4701 }
4702 Token::All => {
4703 self.advance();
4704 D::All
4705 }
4706 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4707 self.advance();
4708 D::All
4709 }
4710 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4711 // Bare `FETCH <name>` — direction defaults to NEXT.
4712 _ => D::Next,
4713 };
4714 // Optional FROM / IN.
4715 if matches!(self.peek(), Token::From)
4716 || matches!(self.peek(), Token::In)
4717 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4718 {
4719 self.advance();
4720 }
4721 let name = match self.advance() {
4722 Token::Ident(n) | Token::QuotedIdent(n) => n,
4723 other => {
4724 return Err(self.err(format!("expected cursor name, got {other:?}")));
4725 }
4726 };
4727 Ok(if is_move {
4728 Statement::MoveCursor { name, direction }
4729 } else {
4730 Statement::FetchCursor { name, direction }
4731 })
4732 }
4733
4734 /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4735 /// [(kind, …)] ON <col>, … FROM <table>`.
4736 fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4737 self.advance(); // STATISTICS
4738 // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4739 let mut if_not_exists = false;
4740 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4741 && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4742 {
4743 self.advance();
4744 self.advance();
4745 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4746 self.advance();
4747 if_not_exists = true;
4748 }
4749 }
4750 let name = self.expect_ident_like()?;
4751 let mut kinds = Vec::new();
4752 if matches!(self.peek(), Token::LParen) {
4753 self.advance();
4754 loop {
4755 let k = self.expect_ident_like()?;
4756 // PG stores the single letters; accept the spelled-out
4757 // names the SQL uses and record what PG records.
4758 kinds.push(match k.to_ascii_lowercase().as_str() {
4759 "ndistinct" => String::from("d"),
4760 "dependencies" => String::from("f"),
4761 "mcv" => String::from("m"),
4762 other => {
4763 return Err(
4764 self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4765 );
4766 }
4767 });
4768 match self.advance() {
4769 Token::Comma => {}
4770 Token::RParen => break,
4771 other => {
4772 return Err(self.err(alloc::format!(
4773 "expected ',' or ')' in statistics kind list, got {other:?}"
4774 )));
4775 }
4776 }
4777 }
4778 }
4779 if !matches!(self.peek(), Token::On) {
4780 return Err(self.err(alloc::format!(
4781 "expected ON in CREATE STATISTICS, got {:?}",
4782 self.peek()
4783 )));
4784 }
4785 self.advance();
4786 let mut columns = Vec::new();
4787 loop {
4788 columns.push(self.expect_ident_like()?);
4789 if matches!(self.peek(), Token::Comma) {
4790 self.advance();
4791 } else {
4792 break;
4793 }
4794 }
4795 if !matches!(self.peek(), Token::From) {
4796 return Err(self.err(alloc::format!(
4797 "expected FROM in CREATE STATISTICS, got {:?}",
4798 self.peek()
4799 )));
4800 }
4801 self.advance();
4802 let table = self.expect_ident_like()?;
4803 Ok(Statement::CreateStatistics {
4804 name,
4805 if_not_exists,
4806 kinds,
4807 columns,
4808 table,
4809 })
4810 }
4811
4812 /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4813 /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4814 /// entered with the `TABLE` keyword still unconsumed. Extracted so
4815 /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4816 /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4817 /// forward call.
4818 fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4819 self.advance(); // TABLE
4820 let if_exists = self.consume_if_exists();
4821 let mut names: Vec<String> = Vec::new();
4822 loop {
4823 names.push(self.expect_ident_like()?);
4824 if matches!(self.peek(), Token::Comma) {
4825 self.advance();
4826 continue;
4827 }
4828 break;
4829 }
4830 if matches!(
4831 self.peek(),
4832 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4833 || s.eq_ignore_ascii_case("restrict")
4834 ) {
4835 self.advance();
4836 }
4837 Ok(Statement::DropTable { names, if_exists })
4838 }
4839
4840 fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4841 self.advance(); // STATISTICS
4842 let mut if_exists = false;
4843 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4844 && matches!(self.tokens.get(self.pos + 1),
4845 Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4846 {
4847 self.advance();
4848 self.advance();
4849 if_exists = true;
4850 }
4851 let name = self.expect_ident_like()?;
4852 Ok(Statement::DropStatistics { name, if_exists })
4853 }
4854
4855 fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4856 debug_assert!(matches!(self.peek(), Token::Create));
4857 self.advance();
4858 match self.peek() {
4859 Token::Table => self.parse_create_table_stmt_after_create(),
4860 Token::Index => self.parse_create_index_stmt_after_create(false),
4861 // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4862 // object now. It used to be consumed by the CREATE-noise
4863 // arm, so a pg_dump that declares extended statistics
4864 // restored silently without them and reflection showed
4865 // nothing.
4866 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4867 self.parse_create_statistics_after_create()
4868 }
4869 // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4870 // The `UNIQUE` modifier turns a partial index into a
4871 // partial-uniqueness invariant (only rows matching the
4872 // WHERE predicate are checked for duplicates). mailrs
4873 // K1 (3 hits: email_templates default, calendar_events
4874 // master, calendar_events instance).
4875 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4876 self.advance();
4877 if !matches!(self.peek(), Token::Index) {
4878 return Err(self.err(alloc::format!(
4879 "expected INDEX after CREATE UNIQUE, got {:?}",
4880 self.peek()
4881 )));
4882 }
4883 self.parse_create_index_stmt_after_create(true)
4884 }
4885 Token::Publication => {
4886 self.advance();
4887 self.parse_create_publication_after_keyword()
4888 }
4889 Token::Subscription => {
4890 self.advance();
4891 self.parse_create_subscription_after_keyword()
4892 }
4893 // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4894 // USER isn't a reserved keyword — we look for the bare
4895 // identifier so the lexer doesn't have to grow a token.
4896 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4897 self.advance();
4898 self.parse_create_user_after_keyword(true)
4899 }
4900 // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4901 // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4902 // the default of the LOGIN attribute.
4903 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4904 self.advance();
4905 self.parse_create_user_after_keyword(false)
4906 }
4907 // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4908 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4909 self.advance();
4910 self.parse_create_policy_after_keyword()
4911 }
4912 // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4913 // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4914 // no-op. mailrs follow-up F3.
4915 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4916 self.advance();
4917 self.parse_create_extension_after_keyword()
4918 }
4919 // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4920 // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4921 // optional; absorb it here and forward to the
4922 // per-kind parsers with the flag. OR is a reserved
4923 // keyword token.
4924 Token::Or => {
4925 self.advance();
4926 let next = self.peek();
4927 let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4928 return Err(self.err(alloc::format!(
4929 "expected REPLACE after CREATE OR, got {next:?}"
4930 )));
4931 };
4932 if !s2.eq_ignore_ascii_case("replace") {
4933 return Err(self.err(alloc::format!(
4934 "expected REPLACE after CREATE OR, got {s2:?}"
4935 )));
4936 }
4937 self.advance();
4938 self.parse_create_function_or_trigger_after_or_replace(true)
4939 }
4940 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4941 self.advance();
4942 self.parse_create_function_after_keyword(false)
4943 }
4944 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4945 self.advance();
4946 self.parse_create_trigger_after_keyword(false)
4947 }
4948 // v7.39 (round 139) — CREATE RULE …
4949 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4950 self.advance();
4951 self.parse_create_rule_after_keyword(false)
4952 }
4953 // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4954 // trigger is a row-level AFTER trigger that additionally carries
4955 // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4956 // path already tolerates and skips those clauses, so consuming the
4957 // CONSTRAINT keyword and reusing it makes the statement parse and the
4958 // trigger fire. (The deferral timing itself is not yet honoured —
4959 // SPG fires it as a plain AFTER trigger, which is correct behaviour
4960 // for every non-deferred use.)
4961 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4962 self.advance();
4963 if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4964 if t.eq_ignore_ascii_case("trigger"))
4965 {
4966 return Err(self.err(alloc::format!(
4967 "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4968 self.peek()
4969 )));
4970 }
4971 self.advance();
4972 self.parse_create_trigger_after_keyword(false)
4973 }
4974 // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4975 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4976 self.advance();
4977 self.parse_create_sequence_after_keyword(false)
4978 }
4979 // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4980 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4981 self.advance();
4982 self.parse_create_view_after_keyword(false, false, false)
4983 }
4984 // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4985 // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4986 // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4987 // appear (in any order) between `CREATE` and `VIEW` in
4988 // every mysqldump-emitted view. Pre-2.6 the parser
4989 // rejected the prefix and the customer's whole view
4990 // backup failed on the first view. The hints are pure
4991 // planner / permission metadata; SPG's view-rewrite
4992 // path is semantically equivalent for all three
4993 // algorithms in v7.17 (TEMPTABLE differs only in
4994 // perf for huge views — out of v7.17 scope), and
4995 // DEFINER / SQL SECURITY are pure single-user
4996 // permissioning that SPG ignores by design.
4997 Token::Ident(s) | Token::QuotedIdent(s)
4998 if s.eq_ignore_ascii_case("algorithm")
4999 || s.eq_ignore_ascii_case("definer")
5000 || s.eq_ignore_ascii_case("sql") =>
5001 {
5002 self.consume_mysql_view_prefix()?;
5003 // After absorbing ALGORITHM / DEFINER / SQL SECURITY
5004 // (in any order, in any combination), the next
5005 // keyword must be VIEW. mysqldump never emits these
5006 // prefixes on non-view statements.
5007 let next = self.peek().clone();
5008 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
5009 if s2.eq_ignore_ascii_case("view"))
5010 {
5011 self.advance();
5012 self.parse_create_view_after_keyword(false, false, false)
5013 } else {
5014 Err(self.err(alloc::format!(
5015 "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
5016 )))
5017 }
5018 }
5019 // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
5020 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
5021 self.advance();
5022 self.parse_create_type_after_keyword()
5023 }
5024 // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
5025 // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
5026 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
5027 self.advance();
5028 self.parse_create_domain_after_keyword()
5029 }
5030 // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
5031 // name [AUTHORIZATION user]. Real catalog registry
5032 // (was silent-no-op'd pre-v7.17).
5033 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
5034 self.advance();
5035 let if_not_exists = self.parse_if_not_exists();
5036 let name = self.expect_ident_like()?;
5037 // Optional `AUTHORIZATION <user>` trailer — accepted,
5038 // ignored (single-user catalog).
5039 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
5040 if s.eq_ignore_ascii_case("authorization"))
5041 {
5042 self.advance();
5043 let _ = self.expect_ident_like()?;
5044 }
5045 Ok(Statement::CreateSchema { name, if_not_exists })
5046 }
5047 // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
5048 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
5049 self.advance();
5050 let next = self.peek().clone();
5051 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5052 {
5053 self.advance();
5054 self.parse_create_materialized_view_after_keyword()
5055 } else {
5056 Err(self.err(alloc::format!(
5057 "expected VIEW after CREATE MATERIALIZED, got {next:?}"
5058 )))
5059 }
5060 }
5061 // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
5062 // no-op below), an UNLOGGED table is a real, fully-usable table in
5063 // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
5064 // durability optimisation is a follow-up), so a dump / app that
5065 // declares UNLOGGED tables works instead of failing to parse.
5066 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
5067 self.advance(); // UNLOGGED
5068 if matches!(self.peek(), Token::Table) {
5069 self.parse_create_table_stmt_after_create()
5070 } else {
5071 Err(self.err(format!(
5072 "expected TABLE after CREATE UNLOGGED, got {:?}",
5073 self.peek()
5074 )))
5075 }
5076 }
5077 Token::Ident(s) | Token::QuotedIdent(s)
5078 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
5079 {
5080 self.advance();
5081 // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
5082 let next = self.peek().clone();
5083 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
5084 {
5085 self.advance();
5086 self.parse_create_sequence_after_keyword(true)
5087 } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5088 {
5089 self.advance();
5090 self.parse_create_view_after_keyword(false, false, true)
5091 } else {
5092 // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
5093 // consumed and answered OK while creating nothing, so
5094 // every statement that touched the table afterwards failed
5095 // with "table not found" — the DDL itself lied. It is a
5096 // real CREATE TABLE now, marked temporary so the executor
5097 // puts it in the session's own namespace. An optional
5098 // TABLE keyword may or may not be present (`CREATE TEMP t`
5099 // is not legal, but the keyword is consumed by the
5100 // CREATE TABLE parser itself).
5101 let stmt = self.parse_create_table_stmt_after_create()?;
5102 match stmt {
5103 Statement::CreateTable(mut c) => {
5104 c.temporary = true;
5105 Ok(Statement::CreateTable(c))
5106 }
5107 // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
5108 // CTAS node, which needs the same session namespace.
5109 Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
5110 m.temporary = true;
5111 Ok(Statement::CreateMaterializedView(m))
5112 }
5113 other => Ok(other),
5114 }
5115 }
5116 }
5117 // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
5118 // BEGIN <body> END`. The body may reference `@var`
5119 // session variables, SET statements, internal `;`
5120 // terminators, etc. SPG has no procedure runtime, so
5121 // consume the whole `CREATE PROCEDURE … END` block as
5122 // a no-op so mysqldump scripts that include stored
5123 // routines load through. The matching-END consumer
5124 // tracks BEGIN/END nesting depth to handle nested
5125 // BEGIN blocks correctly.
5126 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
5127 self.consume_mysql_routine_body();
5128 Ok(Statement::Empty)
5129 }
5130 // v7.14.0 — pg_dump / mysqldump emit
5131 // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
5132 // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
5133 // SPG is single-schema / single-database; these have
5134 // no behavioural effect, so consume + return Empty.
5135 // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
5136 // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
5137 // moved up to real parser branches. DATABASE / ROLE /
5138 // POLICY / OPERATOR stay no-op forever
5139 // (single-database, hardcoded roles).
5140 Token::Ident(s) | Token::QuotedIdent(s)
5141 if matches!(
5142 s.to_ascii_lowercase().as_str(),
5143 "database"
5144 | "role"
5145 | "operator"
5146 | "cast"
5147 | "aggregate"
5148 | "language"
5149 | "collation"
5150 | "conversion"
5151 // v7.17.0 Phase 8 (audit N6) — rarely-
5152 // emitted pg_dump shapes that should
5153 // load through without a parser error.
5154 // SPG has no planner statistics catalog,
5155 // no event-trigger hooks, no foreign-
5156 // data-wrapper infrastructure; consume
5157 // + return Empty.
5158 | "statistics"
5159 | "event"
5160 // v7.37.17 (17.6 siblings) — additional CREATE
5161 // targets pg_dump / operator install scripts
5162 // may emit that SPG has no matching machinery
5163 // for. Consume + Empty-return.
5164 | "text"
5165 | "tablespace"
5166 | "access"
5167 | "large"
5168 ) =>
5169 {
5170 // DATABASE is the one member of this list PG refuses
5171 // inside a transaction block; the rest (ROLE, CAST,
5172 // TABLESPACE, …) it runs there quite happily, so only
5173 // this one is named. Still a no-op otherwise — SPG is
5174 // single-database.
5175 let is_database = s.eq_ignore_ascii_case("database");
5176 // The name is the first token after DATABASE, past an
5177 // `IF NOT EXISTS`.
5178 let name = if is_database {
5179 self.scan_database_name()
5180 } else {
5181 None
5182 };
5183 let collation = if is_database {
5184 self.scan_database_collation_until_boundary()
5185 } else {
5186 self.consume_until_statement_boundary();
5187 None
5188 };
5189 if is_database {
5190 return Ok(Statement::NoOpPreventedInTransaction {
5191 what: String::from("CREATE DATABASE"),
5192 collation,
5193 name,
5194 });
5195 }
5196 Ok(Statement::Empty)
5197 }
5198 // v7.39 (round 706) — the foreign-data family leaves the silent
5199 // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
5200 // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
5201 // FDW machinery), but the ENGINE now warns, so a restore log
5202 // says what will not function instead of reporting success.
5203 Token::Ident(s) | Token::QuotedIdent(s)
5204 if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
5205 {
5206 self.consume_until_statement_boundary();
5207 Ok(Statement::ValidateOnly {
5208 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
5209 names: Vec::new(),
5210 })
5211 }
5212 other => Err(self.err(format!(
5213 "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
5214 ))),
5215 }
5216 }
5217
5218 /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
5219 /// keyword decides whether we parse a function or trigger
5220 /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
5221 /// PROCEDURE) — those land in later releases.
5222 fn parse_create_function_or_trigger_after_or_replace(
5223 &mut self,
5224 or_replace: bool,
5225 ) -> Result<Statement, ParseError> {
5226 let tok = self.peek();
5227 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5228 return Err(self.err(alloc::format!(
5229 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
5230 )));
5231 };
5232 if s.eq_ignore_ascii_case("function") {
5233 self.advance();
5234 self.parse_create_function_after_keyword(or_replace)
5235 } else if s.eq_ignore_ascii_case("trigger") {
5236 self.advance();
5237 self.parse_create_trigger_after_keyword(or_replace)
5238 } else if s.eq_ignore_ascii_case("rule") {
5239 // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
5240 self.advance();
5241 self.parse_create_rule_after_keyword(or_replace)
5242 } else if s.eq_ignore_ascii_case("view") {
5243 // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
5244 self.advance();
5245 self.parse_create_view_after_keyword(or_replace, false, false)
5246 } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
5247 // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
5248 self.advance();
5249 let nxt = self.peek().clone();
5250 if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
5251 {
5252 self.advance();
5253 self.parse_create_view_after_keyword(or_replace, false, true)
5254 } else {
5255 Err(self.err(alloc::format!(
5256 "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
5257 )))
5258 }
5259 } else {
5260 Err(self.err(alloc::format!(
5261 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
5262 )))
5263 }
5264 }
5265
5266 /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
5267 /// SPG doesn't have a registry; pgvector / similar are
5268 /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
5269 /// the syntax lets dual-target schemas keep the line.
5270 fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
5271 // Optional `IF NOT EXISTS`.
5272 self.consume_if_not_exists();
5273 let name = self.expect_ident_like()?;
5274 // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
5275 // CASCADE / FROM '<v>' clauses; we don't model them.
5276 loop {
5277 match self.peek() {
5278 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
5279 self.advance();
5280 continue;
5281 }
5282 Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
5283 self.advance();
5284 let _ = self.expect_ident_like()?;
5285 continue;
5286 }
5287 Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
5288 self.advance();
5289 // String or ident literal.
5290 let _ = self.advance();
5291 continue;
5292 }
5293 Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
5294 self.advance();
5295 let _ = self.advance();
5296 continue;
5297 }
5298 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
5299 self.advance();
5300 continue;
5301 }
5302 _ => break,
5303 }
5304 }
5305 // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
5306 // nosuch` reported success and `pg_extension` then did not list it,
5307 // which is the accept-and-do-nothing shape F31 exists to find.
5308 Ok(Statement::ValidateOnly {
5309 kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
5310 names: alloc::vec![name],
5311 })
5312 }
5313
5314 /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
5315 /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
5316 /// already been consumed by the caller. Grammar accepted:
5317 ///
5318 /// name `(` arg-list `)`
5319 /// `RETURNS` return-type
5320 /// [ `LANGUAGE` ident ]
5321 /// `AS` $$ body $$
5322 /// [ `LANGUAGE` ident ]
5323 ///
5324 /// Either `LANGUAGE` position is allowed; PG accepts both.
5325 fn parse_create_function_after_keyword(
5326 &mut self,
5327 or_replace: bool,
5328 ) -> Result<Statement, ParseError> {
5329 let name = self.expect_ident_like()?;
5330 // Argument list. v7.12.4 commonly sees the empty `()`
5331 // (trigger functions); typed args parse and round-trip
5332 // but the executor only invokes nullary functions.
5333 if !matches!(self.peek(), Token::LParen) {
5334 return Err(self.err(alloc::format!(
5335 "expected '(' after function name {name:?}, got {:?}",
5336 self.peek()
5337 )));
5338 }
5339 self.advance();
5340 let args = self.parse_function_arg_list()?;
5341 // RETURNS clause.
5342 let tok = self.peek();
5343 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5344 return Err(self.err(alloc::format!(
5345 "expected RETURNS after function arg list, got {tok:?}"
5346 )));
5347 };
5348 if !s.eq_ignore_ascii_case("returns") {
5349 return Err(self.err(alloc::format!(
5350 "expected RETURNS after function arg list, got {s:?}"
5351 )));
5352 }
5353 self.advance();
5354 let returns = self.parse_function_return()?;
5355 // Optional LANGUAGE clause (PG also accepts after AS — we'll
5356 // re-check after the body too).
5357 let mut language: Option<String> = self.parse_optional_language()?;
5358 // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5359 // either side of the body and in any order, interleaved with
5360 // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5361 // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5362 // PG's own pg_dump output did not restore.
5363 let mut attrs = FunctionAttrs::default();
5364 loop {
5365 let before = self.pos;
5366 self.parse_function_attrs_into(&mut attrs)?;
5367 if language.is_none() {
5368 language = self.parse_optional_language()?;
5369 }
5370 if self.pos == before {
5371 break;
5372 }
5373 }
5374 // `AS` followed by a $$-quoted body (lexer already
5375 // collapses both `$$…$$` and `$tag$…$tag$` to a single
5376 // Token::String). AS is a reserved keyword (Token::As).
5377 if !matches!(self.peek(), Token::As) {
5378 return Err(self.err(alloc::format!(
5379 "expected AS before function body, got {:?}",
5380 self.peek()
5381 )));
5382 }
5383 self.advance();
5384 let body_text = match self.peek() {
5385 Token::String(s) => {
5386 let body = s.clone();
5387 self.advance();
5388 body
5389 }
5390 other => {
5391 return Err(self.err(alloc::format!(
5392 "expected $$-quoted function body after AS, got {other:?}"
5393 )));
5394 }
5395 };
5396 // Trailing clauses — PG's other accepted position for both the
5397 // LANGUAGE and the attributes.
5398 loop {
5399 let before = self.pos;
5400 self.parse_function_attrs_into(&mut attrs)?;
5401 if language.is_none() {
5402 language = self.parse_optional_language()?;
5403 }
5404 if self.pos == before {
5405 break;
5406 }
5407 }
5408 let language = language.unwrap_or_else(|| String::from("sql"));
5409 // PL/pgSQL bodies get structure-parsed. Other languages
5410 // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5411 // recognise) round-trip as Raw text — the executor errors
5412 // when invoked with a clear unsupported message.
5413 let body = if language.eq_ignore_ascii_case("plpgsql") {
5414 match parse_plpgsql_body(&body_text) {
5415 Ok(block) => FunctionBody::PlPgSql(block),
5416 // Best-effort: if the body parser doesn't yet
5417 // support a construct used inside, fall back to
5418 // raw — keeps `CREATE FUNCTION` itself working
5419 // (catalogue accepts), executor errors on
5420 // invocation only.
5421 Err(_) => FunctionBody::Raw(body_text),
5422 }
5423 } else {
5424 FunctionBody::Raw(body_text)
5425 };
5426 Ok(Statement::CreateFunction(CreateFunctionStatement {
5427 name,
5428 or_replace,
5429 args,
5430 returns,
5431 language,
5432 body,
5433 attrs,
5434 }))
5435 }
5436
5437 /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5438 /// attribute clauses into `attrs`, stopping at the first token that
5439 /// is not one. Measured against PG 18.4, which accepts them in any
5440 /// order and on either side of the body.
5441 fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5442 loop {
5443 let word = match self.peek() {
5444 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5445 // NOT LEAKPROOF — NOT is a reserved keyword token.
5446 Token::Not
5447 if matches!(
5448 self.tokens.get(self.pos + 1),
5449 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5450 ) =>
5451 {
5452 self.advance();
5453 self.advance();
5454 attrs.leakproof = false;
5455 continue;
5456 }
5457 _ => return Ok(()),
5458 };
5459 match word.as_str() {
5460 "immutable" => {
5461 self.advance();
5462 attrs.volatility = FunctionVolatility::Immutable;
5463 }
5464 "stable" => {
5465 self.advance();
5466 attrs.volatility = FunctionVolatility::Stable;
5467 }
5468 "volatile" => {
5469 self.advance();
5470 attrs.volatility = FunctionVolatility::Volatile;
5471 }
5472 "strict" => {
5473 self.advance();
5474 attrs.strict = true;
5475 }
5476 "leakproof" => {
5477 self.advance();
5478 attrs.leakproof = true;
5479 }
5480 // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5481 // spelled-out forms of STRICT and its opposite.
5482 "returns" | "called" => {
5483 let strict = word == "returns";
5484 let mut probe = self.pos + 1;
5485 if strict {
5486 // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5487 // is not ours.
5488 match self.tokens.get(probe) {
5489 Some(Token::Null) => probe += 1,
5490 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5491 _ => return Ok(()),
5492 }
5493 }
5494 let ok = matches!(self.tokens.get(probe), Some(Token::On))
5495 || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5496 if !ok {
5497 return Ok(());
5498 }
5499 probe += 1;
5500 match self.tokens.get(probe) {
5501 Some(Token::Null) => probe += 1,
5502 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5503 _ => return Ok(()),
5504 }
5505 match self.tokens.get(probe) {
5506 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5507 _ => return Ok(()),
5508 }
5509 self.pos = probe;
5510 attrs.strict = strict;
5511 }
5512 "security" | "external" => {
5513 // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5514 let mut probe = self.pos + 1;
5515 if word == "external" {
5516 match self.tokens.get(probe) {
5517 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5518 probe += 1;
5519 }
5520 _ => return Ok(()),
5521 }
5522 }
5523 let definer = match self.tokens.get(probe) {
5524 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5525 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5526 _ => return Ok(()),
5527 };
5528 self.pos = probe + 1;
5529 attrs.security_definer = definer;
5530 }
5531 "parallel" => {
5532 let level = match self.tokens.get(self.pos + 1) {
5533 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5534 FunctionParallel::Safe
5535 }
5536 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5537 FunctionParallel::Restricted
5538 }
5539 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5540 FunctionParallel::Unsafe
5541 }
5542 _ => return Ok(()),
5543 };
5544 self.pos += 2;
5545 attrs.parallel = level;
5546 }
5547 "cost" | "rows" => {
5548 let Some(n) = self.peek_number_at(self.pos + 1) else {
5549 return Ok(());
5550 };
5551 self.pos += 2;
5552 if word == "cost" {
5553 attrs.cost = Some(n);
5554 } else {
5555 attrs.rows = Some(n);
5556 }
5557 }
5558 _ => return Ok(()),
5559 }
5560 }
5561 }
5562
5563 /// The numeric literal at `idx`, if there is one.
5564 fn peek_number_at(&self, idx: usize) -> Option<f64> {
5565 match self.tokens.get(idx)? {
5566 Token::Integer(n) => Some(*n as f64),
5567 Token::Float(f) => Some(*f),
5568 Token::Numeric(t) => t.parse::<f64>().ok(),
5569 _ => None,
5570 }
5571 }
5572
5573 /// Closing `)`-terminated argument list. v7.12.4 commonly
5574 /// sees the empty `()`; typed args round-trip but the
5575 /// executor (yet) doesn't invoke them.
5576 /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5577 /// it away, which is what PG does with one on a function parameter.
5578 fn skip_type_modifier(&mut self) {
5579 if !matches!(self.peek(), Token::LParen) {
5580 return;
5581 }
5582 // Only a numeric modifier — anything else is not one, and eating
5583 // it would swallow real grammar.
5584 let mut i = self.pos + 1;
5585 let mut seen_number = false;
5586 loop {
5587 match self.tokens.get(i) {
5588 Some(Token::Integer(_)) => seen_number = true,
5589 Some(Token::Comma) => {}
5590 Some(Token::RParen) => break,
5591 _ => return,
5592 }
5593 i += 1;
5594 }
5595 if !seen_number {
5596 return;
5597 }
5598 while self.pos <= i {
5599 self.advance();
5600 }
5601 }
5602
5603 fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5604 let mut args: Vec<FunctionArg> = Vec::new();
5605 if matches!(self.peek(), Token::RParen) {
5606 self.advance();
5607 return Ok(args);
5608 }
5609 loop {
5610 // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5611 // a reserved token; OUT / INOUT are bare idents.
5612 let mode = if matches!(self.peek(), Token::In) {
5613 self.advance();
5614 FunctionArgMode::In
5615 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5616 {
5617 self.advance();
5618 FunctionArgMode::Out
5619 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5620 {
5621 self.advance();
5622 FunctionArgMode::InOut
5623 } else {
5624 FunctionArgMode::In
5625 };
5626 // Optional name. The next token is either a name
5627 // (followed by a type ident) or the type itself.
5628 // Disambiguate by peeking ahead: if the token after
5629 // the next ident is also an ident, we treat the
5630 // first as the name.
5631 // v7.39 (round 315, V19) — take EVERY ident-like word up to
5632 // the comma or paren, then decide. Reading at most two of
5633 // them could not spell `x double precision` at all, and
5634 // silently mis-read the bare `double precision` as a
5635 // parameter named "double" — which is what made the same
5636 // signature key two different ways.
5637 let (name, ty_token) = {
5638 let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5639 while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5640 words.push(self.expect_ident_like()?);
5641 }
5642 // v7.39 (round 344) — a length / precision modifier on the
5643 // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5644 // accepts it and DROPS it — `pg_get_function_arguments`
5645 // reports plain `character varying` / `numeric`, measured on
5646 // 18.4 — but SPG raised `syntax error at or near "("`,
5647 // because the modifier's parens were never consumed.
5648 self.skip_type_modifier();
5649 // r1049 — `f(v bigint[])`. The array suffix parsed in
5650 // the column position, the cast position and (r1038)
5651 // the RETURNS position, but not here: the fifth
5652 // member of the same family, reported by sentori as
5653 // presumably the same code. It is now.
5654 let array_suffix = self.consume_array_suffix();
5655 let whole = words.join(" ");
5656 let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5657 {
5658 (Some(words[0].clone()), words[1..].join(" "))
5659 } else {
5660 (None, whole)
5661 };
5662 ty_token.push_str(&array_suffix);
5663 (name, ty_token)
5664 };
5665 // Type — try to map to ColumnTypeName, else Raw.
5666 let ty = match map_type_ident_to_column_type_name(&ty_token) {
5667 Some(t) => FunctionArgType::Typed(t),
5668 None => FunctionArgType::Raw(ty_token),
5669 };
5670 args.push(FunctionArg { mode, name, ty });
5671 match self.peek() {
5672 Token::Comma => {
5673 self.advance();
5674 continue;
5675 }
5676 Token::RParen => {
5677 self.advance();
5678 return Ok(args);
5679 }
5680 other => {
5681 return Err(self.err(alloc::format!(
5682 "expected , or ) in function arg list, got {other:?}"
5683 )));
5684 }
5685 }
5686 }
5687 }
5688
5689 fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5690 // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5691 // function whose row shape is named inline.
5692 if matches!(self.peek(), Token::Table)
5693 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5694 {
5695 self.advance(); // TABLE
5696 self.advance(); // (
5697 let mut cols: Vec<String> = Vec::new();
5698 loop {
5699 let cname = self.expect_ident_like()?;
5700 let mut ty: Vec<String> = Vec::new();
5701 loop {
5702 match self.peek() {
5703 Token::Comma | Token::RParen | Token::Eof => break,
5704 _ => {}
5705 }
5706 match self.advance() {
5707 Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5708 other => {
5709 if let Some(w) = unreserved_keyword_text(&other) {
5710 ty.push(w);
5711 }
5712 }
5713 }
5714 }
5715 cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5716 if matches!(self.peek(), Token::Comma) {
5717 self.advance();
5718 } else {
5719 break;
5720 }
5721 }
5722 if matches!(self.peek(), Token::RParen) {
5723 self.advance();
5724 }
5725 return Ok(FunctionReturn::Other(alloc::format!(
5726 "TABLE({})",
5727 cols.join(", ")
5728 )));
5729 }
5730 let ident = self.expect_ident_like()?;
5731 // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5732 if ident.eq_ignore_ascii_case("setof") {
5733 let inner = self.expect_ident_like()?;
5734 let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5735 return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5736 }
5737 if ident.eq_ignore_ascii_case("trigger") {
5738 return Ok(FunctionReturn::Trigger);
5739 }
5740 if ident.eq_ignore_ascii_case("void") {
5741 return Ok(FunctionReturn::Void);
5742 }
5743 // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5744 // RETURN position did not, so the `[` was a syntax error and the
5745 // whole migration stopped. sentori worked around it by returning
5746 // zero-padded text.
5747 let suffix = self.consume_array_suffix();
5748 if !suffix.is_empty() {
5749 return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5750 }
5751 match map_type_ident_to_column_type_name(&ident) {
5752 Some(t) => Ok(FunctionReturn::Type(t)),
5753 None => Ok(FunctionReturn::Other(ident)),
5754 }
5755 }
5756
5757 /// Consume any `[]` / `[N]` array markers after a type name and give
5758 /// back their text. Empty when there are none.
5759 fn consume_array_suffix(&mut self) -> String {
5760 let mut out = String::new();
5761 while matches!(self.peek(), Token::LBracket) {
5762 self.advance();
5763 // `[N]` is accepted and, as in PG, the length is not enforced.
5764 if let Token::Integer(n) = self.peek().clone() {
5765 self.advance();
5766 out.push_str(&alloc::format!("[{n}]"));
5767 } else {
5768 out.push_str("[]");
5769 }
5770 if matches!(self.peek(), Token::RBracket) {
5771 self.advance();
5772 }
5773 }
5774 out
5775 }
5776
5777 fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5778 match self.peek() {
5779 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5780 self.advance();
5781 let lang = self.expect_ident_like()?;
5782 Ok(Some(lang.to_ascii_lowercase()))
5783 }
5784 _ => Ok(None),
5785 }
5786 }
5787
5788 /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5789 /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5790 /// (expr)]*`. The `DOMAIN` keyword has already been
5791 /// consumed. PG allows the trailing constraints in any
5792 /// order; we approximate with a small loop.
5793 fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5794 let name = self.expect_ident_like()?;
5795 // Optional `AS`.
5796 if matches!(self.peek(), Token::As) {
5797 self.advance();
5798 }
5799 // v7.39 (round 259) — keep the raw type NAME when the base is not
5800 // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5801 // parent domain.
5802 let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _, _, _) =
5803 self.parse_type_with_implied_flags()?;
5804 let mut default: Option<Expr> = None;
5805 let mut not_null = false;
5806 let mut checks: Vec<Expr> = Vec::new();
5807 loop {
5808 match self.peek() {
5809 Token::Default => {
5810 if default.is_some() {
5811 return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5812 }
5813 self.advance();
5814 default = Some(self.parse_expr(0)?);
5815 }
5816 Token::Not => {
5817 self.advance();
5818 if !matches!(self.peek(), Token::Null) {
5819 return Err(self.err(alloc::format!(
5820 "expected NULL after NOT in DOMAIN, got {:?}",
5821 self.peek()
5822 )));
5823 }
5824 self.advance();
5825 not_null = true;
5826 }
5827 Token::Null => {
5828 self.advance();
5829 // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5830 // is the default-nullable marker (PG accepts it),
5831 // but AFTER a NOT NULL it is a conflict PG refuses
5832 // (`conflicting NULL/NOT NULL constraints`,
5833 // PG18-measured); the old arm no-opped both ways.
5834 if not_null {
5835 return Err(self.err(alloc::string::String::from(
5836 "conflicting NULL/NOT NULL constraints",
5837 )));
5838 }
5839 }
5840 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5841 self.advance();
5842 if !matches!(self.peek(), Token::LParen) {
5843 return Err(self.err(alloc::format!(
5844 "expected '(' after CHECK in DOMAIN, got {:?}",
5845 self.peek()
5846 )));
5847 }
5848 self.advance();
5849 let expr = self.parse_expr(0)?;
5850 if !matches!(self.peek(), Token::RParen) {
5851 return Err(self.err(alloc::format!(
5852 "expected ')' after CHECK expr, got {:?}",
5853 self.peek()
5854 )));
5855 }
5856 self.advance();
5857 checks.push(expr);
5858 }
5859 // CONSTRAINT <name> CHECK (…) — PG accepts a name
5860 // prefix on the constraint; we drop the name and
5861 // recurse into the constraint parsing.
5862 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5863 self.advance();
5864 let _ = self.expect_ident_like()?;
5865 }
5866 _ => break,
5867 }
5868 }
5869 Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5870 name,
5871 base_type,
5872 base_domain: base_user_ref,
5873 default,
5874 not_null,
5875 checks,
5876 }))
5877 }
5878
5879 /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5880 /// ('a', 'b', …)`. The `TYPE` keyword has already been
5881 /// consumed.
5882 fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5883 let name = self.expect_ident_like()?;
5884 // Required `AS`.
5885 if !matches!(self.peek(), Token::As) {
5886 return Err(self.err(alloc::format!(
5887 "expected AS after CREATE TYPE {name:?}, got {:?}",
5888 self.peek()
5889 )));
5890 }
5891 self.advance();
5892 // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5893 // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5894 // on the next token: `(` = composite, ident `ENUM` = enum.
5895 if matches!(self.peek(), Token::LParen) {
5896 self.advance();
5897 let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5898 let mut field_user_types: Vec<Option<String>> = Vec::new();
5899 // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5900 // is legal PG (an attribute-less composite; measured — the old
5901 // e2e note claimed PG requires at least one attribute).
5902 if matches!(self.peek(), Token::RParen) {
5903 self.advance();
5904 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5905 name,
5906 kind: crate::ast::TypeKind::Composite {
5907 fields,
5908 field_user_types,
5909 },
5910 }));
5911 }
5912 loop {
5913 let field_name = self.expect_ident_like()?;
5914 // v7.39 (round 264) — keep the raw type name when it is not
5915 // a builtin: that is how a NESTED composite field records
5916 // which composite it holds.
5917 let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _, _, _) =
5918 self.parse_type_with_implied_flags()?;
5919 fields.push((field_name, field_type));
5920 field_user_types.push(field_user_ref);
5921 if matches!(self.peek(), Token::Comma) {
5922 self.advance();
5923 continue;
5924 }
5925 if matches!(self.peek(), Token::RParen) {
5926 self.advance();
5927 break;
5928 }
5929 return Err(self.err(alloc::format!(
5930 "expected , or ) in composite field list, got {:?}",
5931 self.peek()
5932 )));
5933 }
5934 if fields.is_empty() {
5935 return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5936 }
5937 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5938 name,
5939 kind: crate::ast::TypeKind::Composite {
5940 fields,
5941 field_user_types,
5942 },
5943 }));
5944 }
5945 // Required `ENUM` ident.
5946 let kind_ident = match self.peek().clone() {
5947 Token::Ident(s) | Token::QuotedIdent(s) => s,
5948 other => {
5949 return Err(self.err(alloc::format!(
5950 "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5951 )));
5952 }
5953 };
5954 if !kind_ident.eq_ignore_ascii_case("enum") {
5955 return Err(self.err(alloc::format!(
5956 "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5957 )));
5958 }
5959 self.advance();
5960 if !matches!(self.peek(), Token::LParen) {
5961 return Err(self.err(alloc::format!(
5962 "expected '(' after ENUM, got {:?}",
5963 self.peek()
5964 )));
5965 }
5966 self.advance();
5967 let mut labels: Vec<String> = Vec::new();
5968 loop {
5969 match self.peek().clone() {
5970 Token::String(s) => {
5971 self.advance();
5972 labels.push(s);
5973 }
5974 other => {
5975 return Err(
5976 self.err(alloc::format!("expected enum label string, got {other:?}"))
5977 );
5978 }
5979 }
5980 if matches!(self.peek(), Token::Comma) {
5981 self.advance();
5982 continue;
5983 }
5984 if matches!(self.peek(), Token::RParen) {
5985 self.advance();
5986 break;
5987 }
5988 return Err(self.err(alloc::format!(
5989 "expected , or ) in ENUM label list, got {:?}",
5990 self.peek()
5991 )));
5992 }
5993 if labels.is_empty() {
5994 return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5995 }
5996 Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5997 name,
5998 kind: crate::ast::TypeKind::Enum { labels },
5999 }))
6000 }
6001
6002 /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
6003 /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
6004 /// The `CREATE MATERIALIZED VIEW` keywords have already been
6005 /// consumed.
6006 fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
6007 let if_not_exists = self.parse_if_not_exists();
6008 let name = self.expect_ident_like()?;
6009 let mut columns: Vec<String> = Vec::new();
6010 if matches!(self.peek(), Token::LParen) {
6011 self.advance();
6012 loop {
6013 let c = self.expect_ident_like()?;
6014 columns.push(c);
6015 if matches!(self.peek(), Token::Comma) {
6016 self.advance();
6017 continue;
6018 }
6019 if matches!(self.peek(), Token::RParen) {
6020 self.advance();
6021 break;
6022 }
6023 return Err(self.err(alloc::format!(
6024 "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
6025 self.peek()
6026 )));
6027 }
6028 }
6029 if !matches!(self.peek(), Token::As) {
6030 return Err(self.err(alloc::format!(
6031 "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
6032 self.peek()
6033 )));
6034 }
6035 self.advance();
6036 // v7.39 (round 151) — a WITH-headed body is legal (read-only
6037 // CTEs only; the engine rejects data-modifying ones with PG's
6038 // message). A trailing `WITH [NO] DATA` can't START the body,
6039 // so WITH here heads the query.
6040 let body = if self.peek_is_with_kw() {
6041 self.advance();
6042 self.parse_nested_with_select()?
6043 } else {
6044 let body_stmt = self.parse_select_stmt()?;
6045 let Statement::Select(body) = body_stmt else {
6046 return Err(self.err(alloc::format!(
6047 "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
6048 )));
6049 };
6050 body
6051 };
6052 // Optional trailing `WITH [NO] DATA`.
6053 let with_data = self.parse_optional_with_data(true)?;
6054 Ok(Statement::CreateMaterializedView(
6055 crate::ast::CreateMaterializedViewStatement {
6056 temporary: false,
6057 name,
6058 if_not_exists,
6059 columns,
6060 body,
6061 with_data,
6062 as_plain_table: false,
6063 },
6064 ))
6065 }
6066
6067 /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
6068 /// `default_when_absent` is what to return if the tail is
6069 /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
6070 /// WITH DATA).
6071 fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
6072 let save = self.pos;
6073 // `WITH` is an Ident (not reserved in the lexer).
6074 let is_with = match self.peek() {
6075 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
6076 _ => false,
6077 };
6078 if !is_with {
6079 return Ok(default_when_absent);
6080 }
6081 self.advance();
6082 // Optional `NO`.
6083 let mut with_data = true;
6084 let is_no = match self.peek() {
6085 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
6086 _ => false,
6087 };
6088 if is_no {
6089 self.advance();
6090 with_data = false;
6091 }
6092 // Required `DATA` ident.
6093 let is_data = match self.peek() {
6094 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
6095 _ => false,
6096 };
6097 if is_data {
6098 self.advance();
6099 Ok(with_data)
6100 } else {
6101 // Caller's WITH wasn't WITH-DATA — rewind so the outer
6102 // parser can interpret it.
6103 self.pos = save;
6104 Ok(default_when_absent)
6105 }
6106 }
6107
6108 /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
6109 /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
6110 /// All keyword prefixes have already been consumed; the flags
6111 /// say which were present.
6112 fn parse_create_view_after_keyword(
6113 &mut self,
6114 or_replace: bool,
6115 _materialized_unused: bool,
6116 temporary: bool,
6117 ) -> Result<Statement, ParseError> {
6118 let if_not_exists = self.parse_if_not_exists();
6119 let name = self.expect_ident_like()?;
6120 // Optional `(col, col, …)` rename list.
6121 let mut columns: Vec<String> = Vec::new();
6122 if matches!(self.peek(), Token::LParen) {
6123 self.advance();
6124 loop {
6125 let c = self.expect_ident_like()?;
6126 columns.push(c);
6127 if matches!(self.peek(), Token::Comma) {
6128 self.advance();
6129 continue;
6130 }
6131 if matches!(self.peek(), Token::RParen) {
6132 self.advance();
6133 break;
6134 }
6135 return Err(self.err(alloc::format!(
6136 "expected , or ) in VIEW column list, got {:?}",
6137 self.peek()
6138 )));
6139 }
6140 }
6141 // Required `AS`.
6142 if !matches!(self.peek(), Token::As) {
6143 return Err(self.err(alloc::format!(
6144 "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
6145 self.peek()
6146 )));
6147 }
6148 self.advance();
6149 // Body: a regular SELECT statement. v7.39 (round 151) — a
6150 // WITH-headed body is legal too (read-only CTEs only; the
6151 // engine rejects data-modifying ones with PG's message).
6152 // Disambiguation vs `WITH CHECK OPTION`: a body can't START
6153 // with the check-option clause, so WITH here heads the query.
6154 let body = if self.peek_is_with_kw() {
6155 self.advance();
6156 self.parse_nested_with_select()?
6157 } else {
6158 let body_stmt = self.parse_select_stmt()?;
6159 let Statement::Select(body) = body_stmt else {
6160 return Err(self.err(alloc::format!(
6161 "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
6162 )));
6163 };
6164 body
6165 };
6166 // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
6167 // The SELECT parser stops before a trailing WITH, so it lands here.
6168 let check_option = if matches!(self.peek(),
6169 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
6170 {
6171 self.advance(); // WITH
6172 let opt = match self.peek() {
6173 Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
6174 self.advance();
6175 crate::ast::ViewCheckOption::Local
6176 }
6177 Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
6178 self.advance();
6179 crate::ast::ViewCheckOption::Cascaded
6180 }
6181 // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
6182 _ => crate::ast::ViewCheckOption::Cascaded,
6183 };
6184 if !matches!(self.peek(),
6185 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
6186 {
6187 return Err(self.err(alloc::format!(
6188 "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
6189 self.peek()
6190 )));
6191 }
6192 self.advance(); // CHECK
6193 if !matches!(self.peek(),
6194 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
6195 {
6196 return Err(self.err(alloc::format!(
6197 "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
6198 self.peek()
6199 )));
6200 }
6201 self.advance(); // OPTION
6202 Some(opt)
6203 } else {
6204 None
6205 };
6206 Ok(Statement::CreateView(crate::ast::CreateViewStatement {
6207 name,
6208 or_replace,
6209 if_not_exists,
6210 temporary,
6211 columns,
6212 body,
6213 check_option,
6214 }))
6215 }
6216
6217 /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
6218 /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
6219 /// consumed; `temporary` carries whether TEMPORARY was seen.
6220 fn parse_create_sequence_after_keyword(
6221 &mut self,
6222 temporary: bool,
6223 ) -> Result<Statement, ParseError> {
6224 let if_not_exists = self.parse_if_not_exists();
6225 let name = self.expect_ident_like()?;
6226 // Optional `AS data_type`.
6227 let data_type = if matches!(self.peek(), Token::As) {
6228 self.advance();
6229 Some(self.parse_sequence_data_type()?)
6230 } else {
6231 None
6232 };
6233 let options = self.parse_sequence_options(/* allow_restart = */ false)?;
6234 Ok(Statement::CreateSequence(
6235 crate::ast::CreateSequenceStatement {
6236 name,
6237 if_not_exists,
6238 temporary,
6239 data_type,
6240 options,
6241 },
6242 ))
6243 }
6244
6245 /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
6246 /// already been consumed; this is reached after `SEQUENCE`.
6247 /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
6248 fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
6249 use crate::ast::AlterDomainAction as A;
6250 let name = self.expect_ident_like()?;
6251 // DROP / SET / ADD lex as reserved keyword tokens, not idents.
6252 let kw = match self.peek() {
6253 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6254 Token::Drop => alloc::string::String::from("drop"),
6255 Token::Default => alloc::string::String::from("default"),
6256 other => {
6257 return Err(self.err(alloc::format!(
6258 "expected an ALTER DOMAIN action, got {other:?}"
6259 )));
6260 }
6261 };
6262 let action = match kw.as_str() {
6263 "add" => {
6264 self.advance();
6265 let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
6266 {
6267 self.advance();
6268 Some(self.expect_ident_like()?)
6269 } else {
6270 None
6271 };
6272 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
6273 return Err(self.err(alloc::format!(
6274 "ALTER DOMAIN ADD supports CHECK only, got {:?}",
6275 self.peek()
6276 )));
6277 }
6278 self.advance();
6279 if !matches!(self.peek(), Token::LParen) {
6280 return Err(self.err("expected '(' after CHECK".into()));
6281 }
6282 self.advance();
6283 let check = self.parse_expr(0)?;
6284 if !matches!(self.peek(), Token::RParen) {
6285 return Err(self.err("expected ')' after CHECK expression".into()));
6286 }
6287 self.advance();
6288 A::AddConstraint { name: cname, check }
6289 }
6290 "drop" => {
6291 self.advance();
6292 match self.peek() {
6293 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
6294 self.advance();
6295 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
6296 {
6297 self.advance();
6298 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
6299 {
6300 return Err(self.err("expected EXISTS after IF".into()));
6301 }
6302 self.advance();
6303 true
6304 } else {
6305 false
6306 };
6307 let cn = self.expect_ident_like()?;
6308 A::DropConstraint {
6309 name: cn,
6310 if_exists,
6311 }
6312 }
6313 Token::Default => {
6314 self.advance();
6315 A::DropDefault
6316 }
6317 Token::Not => {
6318 self.advance();
6319 if !matches!(self.peek(), Token::Null) {
6320 return Err(self.err("expected NULL after NOT".into()));
6321 }
6322 self.advance();
6323 A::DropNotNull
6324 }
6325 other => {
6326 return Err(self.err(alloc::format!(
6327 "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
6328 )));
6329 }
6330 }
6331 }
6332 "set" => {
6333 self.advance();
6334 match self.peek() {
6335 Token::Default => {
6336 self.advance();
6337 A::SetDefault(self.parse_expr(0)?)
6338 }
6339 Token::Not => {
6340 self.advance();
6341 if !matches!(self.peek(), Token::Null) {
6342 return Err(self.err("expected NULL after NOT".into()));
6343 }
6344 self.advance();
6345 A::SetNotNull
6346 }
6347 other => {
6348 return Err(self.err(alloc::format!(
6349 "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6350 )));
6351 }
6352 }
6353 }
6354 "rename" => {
6355 self.advance();
6356 if !matches!(self.peek(), Token::To) {
6357 return Err(self.err("expected TO after RENAME".into()));
6358 }
6359 self.advance();
6360 A::RenameTo(self.expect_ident_like()?)
6361 }
6362 other => {
6363 return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6364 }
6365 };
6366 Ok(Statement::AlterDomain { name, action })
6367 }
6368
6369 fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6370 let if_exists = self.parse_if_exists();
6371 let name = self.expect_ident_like()?;
6372 // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6373 // the option list (PG allows only one or the other).
6374 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6375 self.advance();
6376 if matches!(self.peek(), Token::To) {
6377 self.advance();
6378 } else {
6379 self.expect_keyword_ident("to")?;
6380 }
6381 let new = self.expect_ident_like()?;
6382 return Ok(Statement::AlterSequence(
6383 crate::ast::AlterSequenceStatement {
6384 name,
6385 if_exists,
6386 options: crate::ast::SequenceOptions::default(),
6387 rename_to: Some(new),
6388 },
6389 ));
6390 }
6391 let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6392 Ok(Statement::AlterSequence(
6393 crate::ast::AlterSequenceStatement {
6394 name,
6395 if_exists,
6396 options,
6397 rename_to: None,
6398 },
6399 ))
6400 }
6401
6402 fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6403 let kw = self.expect_ident_like()?;
6404 match kw.to_ascii_lowercase().as_str() {
6405 "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6406 "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6407 "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6408 other => Err(self.err(alloc::format!(
6409 "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6410 ))),
6411 }
6412 }
6413
6414 fn parse_sequence_options(
6415 &mut self,
6416 allow_restart: bool,
6417 ) -> Result<crate::ast::SequenceOptions, ParseError> {
6418 use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6419 let mut opts = SequenceOptions::default();
6420 #[allow(clippy::while_let_loop)]
6421 loop {
6422 // Match an ident; stop at any non-ident token (sentinel,
6423 // semicolon, end of statement).
6424 let kw_lc = match self.peek() {
6425 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6426 _ => break,
6427 };
6428 match kw_lc.as_str() {
6429 "increment" => {
6430 self.advance();
6431 // Optional BY.
6432 if self.peek_is_by() {
6433 self.advance();
6434 }
6435 opts.increment = Some(self.expect_signed_int()?);
6436 }
6437 "minvalue" => {
6438 self.advance();
6439 opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6440 }
6441 "maxvalue" => {
6442 self.advance();
6443 opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6444 }
6445 "no" => {
6446 self.advance();
6447 let what = self.expect_ident_like()?;
6448 match what.to_ascii_lowercase().as_str() {
6449 "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6450 "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6451 "cycle" => opts.cycle = Some(false),
6452 other => {
6453 return Err(self.err(alloc::format!(
6454 "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6455 )));
6456 }
6457 }
6458 }
6459 "start" => {
6460 self.advance();
6461 // Optional WITH.
6462 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6463 if s.eq_ignore_ascii_case("with"))
6464 {
6465 self.advance();
6466 }
6467 opts.start = Some(self.expect_signed_int()?);
6468 }
6469 "restart" if allow_restart => {
6470 self.advance();
6471 // Optional WITH n; bare RESTART means restart at START.
6472 let mut with_val: Option<i64> = None;
6473 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6474 if s.eq_ignore_ascii_case("with"))
6475 {
6476 self.advance();
6477 with_val = Some(self.expect_signed_int()?);
6478 } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6479 with_val = Some(self.expect_signed_int()?);
6480 }
6481 opts.restart = Some(with_val);
6482 }
6483 "cache" => {
6484 self.advance();
6485 opts.cache = Some(self.expect_signed_int()?);
6486 }
6487 "cycle" => {
6488 self.advance();
6489 opts.cycle = Some(true);
6490 }
6491 "owned" => {
6492 self.advance();
6493 match self.peek() {
6494 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6495 self.advance();
6496 }
6497 other => {
6498 return Err(
6499 self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6500 );
6501 }
6502 }
6503 // OWNED BY {NONE | tab.col}. Read just one ident
6504 // (NOT expect_ident_like which would auto-strip
6505 // a schema prefix and consume the `.col` we need).
6506 let first = match self.advance() {
6507 Token::Ident(s) | Token::QuotedIdent(s) => s,
6508 other => {
6509 return Err(self.err(alloc::format!(
6510 "expected identifier or NONE after OWNED BY, got {other:?}"
6511 )));
6512 }
6513 };
6514 if first.eq_ignore_ascii_case("none") {
6515 opts.owned_by = Some(SequenceOwnedBy::None);
6516 } else if matches!(self.peek(), Token::Dot) {
6517 self.advance();
6518 let second = match self.advance() {
6519 Token::Ident(s) | Token::QuotedIdent(s) => s,
6520 other => {
6521 return Err(self.err(alloc::format!(
6522 "expected column name after OWNED BY {first}., got {other:?}"
6523 )));
6524 }
6525 };
6526 // v7.17 dump-compat fix — pg_dump emits
6527 // OWNED BY clauses as
6528 // `schema.table.column` (three segments).
6529 // If a third `.<ident>` follows, treat the
6530 // first ident as schema (drop it; SPG is
6531 // single-schema) and the middle / last
6532 // pair as table.column. Otherwise it's
6533 // the two-segment form table.column.
6534 if matches!(self.peek(), Token::Dot) {
6535 self.advance();
6536 let third = match self.advance() {
6537 Token::Ident(s) | Token::QuotedIdent(s) => s,
6538 other => {
6539 return Err(self.err(alloc::format!(
6540 "expected column name after OWNED BY {first}.{second}., got {other:?}"
6541 )));
6542 }
6543 };
6544 let _ = first; // schema prefix discarded
6545 opts.owned_by = Some(SequenceOwnedBy::Column {
6546 table: second,
6547 column: third,
6548 });
6549 } else {
6550 opts.owned_by = Some(SequenceOwnedBy::Column {
6551 table: first,
6552 column: second,
6553 });
6554 }
6555 } else {
6556 return Err(self.err(alloc::format!(
6557 "expected table.column or NONE after OWNED BY, got {first:?}"
6558 )));
6559 }
6560 }
6561 _ => break,
6562 }
6563 }
6564 Ok(opts)
6565 }
6566
6567 fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6568 let neg = if matches!(self.peek(), Token::Minus) {
6569 self.advance();
6570 true
6571 } else {
6572 false
6573 };
6574 match self.peek() {
6575 Token::Integer(n) => {
6576 let v = *n;
6577 self.advance();
6578 Ok(if neg { -v } else { v })
6579 }
6580 other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6581 }
6582 }
6583
6584 /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6585 /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6586 /// clause is fully accepted and discarded — SPG always runs
6587 /// constraint checks immediately (single-writer model). The
6588 /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6589 /// in either order (per the SQL spec they're independent),
6590 /// though pg_dump always emits them in the canonical
6591 /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6592 /// Stops at the first token that isn't part of the clause.
6593 fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6594 self.consume_deferrable_clauses_timed().map(|_| ())
6595 }
6596
6597 /// v7.39 (round 288) — the same scan, but reporting what it saw:
6598 /// `(deferrable, initially_deferred)`. The clauses were parsed and
6599 /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6600 /// NOT DEFERRABLE and a circular-FK migration could not load.
6601 fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6602 let mut deferrable = false;
6603 let mut initially_deferred = false;
6604 loop {
6605 // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6606 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6607 self.advance();
6608 deferrable = true;
6609 if self.consume_optional_initially_clause()? {
6610 initially_deferred = true;
6611 }
6612 continue;
6613 }
6614 // `NOT DEFERRABLE` — already worked pre-3.1.
6615 if matches!(self.peek(), Token::Not) {
6616 let look = self.tokens.get(self.pos + 1);
6617 if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6618 self.advance(); // NOT
6619 self.advance(); // DEFERRABLE
6620 deferrable = false;
6621 initially_deferred = false;
6622 let _ = self.consume_optional_initially_clause()?;
6623 continue;
6624 }
6625 break;
6626 }
6627 // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6628 // accepts this without a leading [NOT] DEFERRABLE
6629 // (the timing keyword alone). pg_dump occasionally
6630 // emits it on FK constraints that inherit timing.
6631 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6632 if self.consume_optional_initially_clause()? {
6633 initially_deferred = true;
6634 // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6635 deferrable = true;
6636 }
6637 continue;
6638 }
6639 break;
6640 }
6641 Ok((deferrable, initially_deferred))
6642 }
6643
6644 /// Helper for [`consume_optional_deferrable_clauses`]. When the
6645 /// next token is `INITIALLY`, consume it plus the required
6646 /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6647 /// Returns true when the timing seen was `DEFERRED`.
6648 fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6649 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6650 return Ok(false);
6651 }
6652 self.advance(); // INITIALLY
6653 match self.advance() {
6654 Token::Ident(s)
6655 if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6656 {
6657 Ok(s.eq_ignore_ascii_case("deferred"))
6658 }
6659 other => Err(self.err(alloc::format!(
6660 "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6661 ))),
6662 }
6663 }
6664
6665 /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6666 /// in its entirety so the parser returns Empty without
6667 /// touching the runtime. The CREATE+PROCEDURE keywords are
6668 /// already consumed; this swallows everything from the
6669 /// procedure name through the matching `END`, including
6670 /// nested `BEGIN`/`END` blocks, internal `;` terminators
6671 /// (DELIMITER `//` makes the script splitter forward the
6672 /// whole block as one statement), `@var` session-variable
6673 /// references, and the trailing terminator.
6674 ///
6675 /// Tracks nesting depth so:
6676 /// BEGIN
6677 /// IF cond THEN
6678 /// BEGIN ... END;
6679 /// END IF;
6680 /// END
6681 /// terminates at the outer END.
6682 fn consume_mysql_routine_body(&mut self) {
6683 // Outer skeleton: name, (...), optional clauses, BEGIN
6684 // <body> END [;]. Scan for the first BEGIN — anything
6685 // before it is signature decoration we don't care about.
6686 // Once inside BEGIN, count up on BEGIN, down on END.
6687 let mut depth: i32 = 0;
6688 let mut started = false;
6689 loop {
6690 match self.peek().clone() {
6691 Token::Begin => {
6692 self.advance();
6693 depth += 1;
6694 started = true;
6695 }
6696 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6697 self.advance();
6698 if started {
6699 depth -= 1;
6700 if depth <= 0 {
6701 // Optional trailing ident (`END IF`,
6702 // `END LOOP`, `END WHILE`, `END CASE`,
6703 // `END label_name`) — eat the next
6704 // ident if present so we don't
6705 // mistake `END IF;` for the outer
6706 // close.
6707 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6708 // If the next token is one of the
6709 // PL/SQL block-closer keywords,
6710 // the END belongs to an inner
6711 // block; bump depth back up.
6712 let is_inner_close = matches!(
6713 self.peek(),
6714 Token::Ident(s) | Token::QuotedIdent(s)
6715 if matches!(
6716 s.to_ascii_lowercase().as_str(),
6717 "if" | "loop" | "while" | "case" | "repeat"
6718 )
6719 );
6720 if is_inner_close {
6721 self.advance();
6722 depth += 1;
6723 continue;
6724 }
6725 }
6726 // Eat optional trailing `;`.
6727 if matches!(self.peek(), Token::Semicolon) {
6728 self.advance();
6729 }
6730 return;
6731 }
6732 }
6733 }
6734 Token::Eof => return,
6735 _ => {
6736 self.advance();
6737 }
6738 }
6739 }
6740 }
6741
6742 /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6743 /// that appear between `CREATE` and `VIEW` in mysqldump output:
6744 ///
6745 /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6746 /// * `DEFINER = <user>` (user may be a quoted string, a bare
6747 /// ident, or `ident @ ident-or-quoted-string` host form)
6748 /// * `SQL SECURITY {DEFINER|INVOKER}`
6749 ///
6750 /// Each clause may appear at most once but in any order.
6751 /// The hints are pure planner / permission metadata that
6752 /// SPG's view-rewrite engine handles uniformly; we accept
6753 /// and discard. Returns `Ok(())` once a non-clause token is
6754 /// peeked (the caller then checks for the `VIEW` keyword).
6755 fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6756 loop {
6757 match self.peek().clone() {
6758 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6759 self.advance(); // ALGORITHM
6760 // Optional `=`. MySQL spec requires it but be
6761 // generous.
6762 if matches!(self.peek(), Token::Eq) {
6763 self.advance();
6764 }
6765 // UNDEFINED / MERGE / TEMPTABLE — accept any
6766 // bare ident; unknown values still parse so
6767 // future MySQL versions don't break.
6768 if matches!(
6769 self.peek(),
6770 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6771 ) {
6772 self.advance();
6773 }
6774 }
6775 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6776 self.advance(); // DEFINER
6777 if matches!(self.peek(), Token::Eq) {
6778 self.advance();
6779 }
6780 // User: quoted string, ident, OR ident @ host
6781 // (host may itself be quoted or bare).
6782 match self.peek().clone() {
6783 Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6784 self.advance();
6785 // Optional `@host`.
6786 if matches!(self.peek(), Token::At) {
6787 self.advance();
6788 if matches!(
6789 self.peek(),
6790 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6791 ) {
6792 self.advance();
6793 }
6794 }
6795 }
6796 _ => {}
6797 }
6798 }
6799 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6800 // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6801 // when followed by SECURITY — the dispatcher must
6802 // not consume a bare `SQL` token (it's not a
6803 // legal CREATE prefix on its own).
6804 let save = self.pos;
6805 self.advance(); // SQL
6806 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6807 if s2.eq_ignore_ascii_case("security"))
6808 {
6809 self.advance(); // SECURITY
6810 // DEFINER / INVOKER trailing ident.
6811 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6812 self.advance();
6813 }
6814 } else {
6815 // Not a SQL SECURITY clause — roll back and
6816 // bail; the caller will error out cleanly.
6817 self.pos = save;
6818 return Ok(());
6819 }
6820 }
6821 _ => return Ok(()),
6822 }
6823 }
6824 }
6825
6826 fn parse_if_not_exists(&mut self) -> bool {
6827 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6828 {
6829 let save = self.pos;
6830 self.advance();
6831 if matches!(self.peek(), Token::Not) {
6832 self.advance();
6833 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6834 {
6835 self.advance();
6836 return true;
6837 }
6838 }
6839 self.pos = save;
6840 }
6841 false
6842 }
6843
6844 fn parse_if_exists(&mut self) -> bool {
6845 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6846 {
6847 let save = self.pos;
6848 self.advance();
6849 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6850 {
6851 self.advance();
6852 return true;
6853 }
6854 self.pos = save;
6855 }
6856 false
6857 }
6858
6859 /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6860 /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6861 /// been consumed.
6862 fn parse_create_trigger_after_keyword(
6863 &mut self,
6864 or_replace: bool,
6865 ) -> Result<Statement, ParseError> {
6866 let name = self.expect_ident_like()?;
6867 let timing = {
6868 let ident = self.expect_ident_like()?;
6869 if ident.eq_ignore_ascii_case("before") {
6870 TriggerTiming::Before
6871 } else if ident.eq_ignore_ascii_case("after") {
6872 TriggerTiming::After
6873 } else if ident.eq_ignore_ascii_case("instead") {
6874 let next = self.expect_ident_like()?;
6875 if !next.eq_ignore_ascii_case("of") {
6876 return Err(self.err(alloc::format!(
6877 "expected OF after INSTEAD in trigger timing, got {next:?}"
6878 )));
6879 }
6880 TriggerTiming::InsteadOf
6881 } else {
6882 return Err(self.err(alloc::format!(
6883 "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6884 )));
6885 }
6886 };
6887 // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6888 // OR is a reserved keyword token (Token::Or), not an Ident.
6889 // v7.13.0 — after an UPDATE event we may optionally see
6890 // `OF col, col, …` (mailrs round-5 G7). Columns are
6891 // captured into `update_columns` once across the whole
6892 // events list; multiple `UPDATE OF` clauses are rejected.
6893 let mut events: Vec<TriggerEvent> = Vec::new();
6894 let mut update_columns: Vec<String> = Vec::new();
6895 let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6896 events.push(first_ev);
6897 if !first_cols.is_empty() {
6898 update_columns = first_cols;
6899 }
6900 while matches!(self.peek(), Token::Or) {
6901 self.advance();
6902 let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6903 events.push(ev);
6904 if !cols.is_empty() {
6905 if !update_columns.is_empty() {
6906 return Err(
6907 self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6908 );
6909 }
6910 update_columns = cols;
6911 }
6912 }
6913 // ON <table>
6914 let tok = self.peek();
6915 let Token::On = tok else {
6916 return Err(self.err(alloc::format!(
6917 "expected ON after trigger events, got {tok:?}"
6918 )));
6919 };
6920 self.advance();
6921 let table = self.expect_ident_like()?;
6922 // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6923 // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6924 // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6925 // the trigger as a plain AFTER trigger (correct for every non-deferred
6926 // use; deferral timing is not yet honoured).
6927 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6928 if s.eq_ignore_ascii_case("from"))
6929 {
6930 self.advance();
6931 let _reftable = self.expect_ident_like()?;
6932 }
6933 self.consume_optional_deferrable_clauses()?;
6934 // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6935 // keyword (Token::For); EACH / ROW / STATEMENT are bare
6936 // idents.
6937 if !matches!(self.peek(), Token::For) {
6938 return Err(self.err(alloc::format!(
6939 "expected FOR EACH ROW / STATEMENT, got {:?}",
6940 self.peek()
6941 )));
6942 }
6943 self.advance();
6944 let for_each = {
6945 let e = self.expect_ident_like()?;
6946 if !e.eq_ignore_ascii_case("each") {
6947 return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6948 }
6949 let unit = self.expect_ident_like()?;
6950 if unit.eq_ignore_ascii_case("row") {
6951 TriggerForEach::Row
6952 } else if unit.eq_ignore_ascii_case("statement") {
6953 TriggerForEach::Statement
6954 } else {
6955 return Err(self.err(alloc::format!(
6956 "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6957 )));
6958 }
6959 };
6960 // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6961 let when_condition = if matches!(self.peek(),
6962 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6963 {
6964 self.advance();
6965 Some(self.parse_paren_expr("WHEN")?)
6966 } else {
6967 None
6968 };
6969 // EXECUTE FUNCTION/PROCEDURE name(...)
6970 let exec = self.expect_ident_like()?;
6971 if !exec.eq_ignore_ascii_case("execute") {
6972 return Err(self.err(alloc::format!(
6973 "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6974 )));
6975 }
6976 let fn_or_proc = self.expect_ident_like()?;
6977 if !(fn_or_proc.eq_ignore_ascii_case("function")
6978 || fn_or_proc.eq_ignore_ascii_case("procedure"))
6979 {
6980 return Err(self.err(alloc::format!(
6981 "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6982 )));
6983 }
6984 let function = self.expect_ident_like()?;
6985 // Optional empty arg list `()`.
6986 if matches!(self.peek(), Token::LParen) {
6987 self.advance();
6988 if !matches!(self.peek(), Token::RParen) {
6989 return Err(self.err(alloc::format!(
6990 "v7.12.4 trigger function calls take no args; got {:?}",
6991 self.peek()
6992 )));
6993 }
6994 self.advance();
6995 }
6996 Ok(Statement::CreateTrigger(CreateTriggerStatement {
6997 name,
6998 or_replace,
6999 timing,
7000 events,
7001 table,
7002 for_each,
7003 function,
7004 update_columns,
7005 when_condition,
7006 }))
7007 }
7008
7009 /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
7010 /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
7011 fn parse_create_rule_after_keyword(
7012 &mut self,
7013 or_replace: bool,
7014 ) -> Result<Statement, ParseError> {
7015 let name = self.expect_ident_like()?;
7016 if !matches!(self.peek(), Token::As) {
7017 return Err(self.err(alloc::format!(
7018 "expected AS in CREATE RULE, got {:?}",
7019 self.peek()
7020 )));
7021 }
7022 self.advance();
7023 if !matches!(self.peek(), Token::On) {
7024 return Err(self.err(alloc::format!(
7025 "expected ON in CREATE RULE, got {:?}",
7026 self.peek()
7027 )));
7028 }
7029 self.advance();
7030 let event = self.parse_rule_event()?;
7031 if !matches!(self.peek(), Token::To)
7032 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
7033 {
7034 return Err(self.err(alloc::format!(
7035 "expected TO after rule event, got {:?}",
7036 self.peek()
7037 )));
7038 }
7039 self.advance();
7040 let table = self.expect_ident_like()?;
7041 // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
7042 let when_condition = if matches!(self.peek(), Token::Where) {
7043 self.advance();
7044 Some(self.parse_expr(0)?)
7045 } else {
7046 None
7047 };
7048 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
7049 {
7050 return Err(self.err(alloc::format!(
7051 "expected DO in CREATE RULE, got {:?}",
7052 self.peek()
7053 )));
7054 }
7055 self.advance();
7056 // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
7057 let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
7058 {
7059 self.advance();
7060 true
7061 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
7062 self.advance();
7063 false
7064 } else {
7065 false
7066 };
7067 // `NOTHING` | `( cmd; … )` | `cmd`.
7068 let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
7069 {
7070 self.advance();
7071 Vec::new()
7072 } else if matches!(self.peek(), Token::LParen) {
7073 self.advance();
7074 let mut cmds = Vec::new();
7075 loop {
7076 cmds.push(self.parse_one_statement()?);
7077 if matches!(self.peek(), Token::Semicolon) {
7078 self.advance();
7079 if matches!(self.peek(), Token::RParen) {
7080 break;
7081 }
7082 continue;
7083 }
7084 break;
7085 }
7086 if !matches!(self.peek(), Token::RParen) {
7087 return Err(self.err(alloc::format!(
7088 "expected ) closing the CREATE RULE command list, got {:?}",
7089 self.peek()
7090 )));
7091 }
7092 self.advance();
7093 cmds
7094 } else {
7095 alloc::vec![self.parse_one_statement()?]
7096 };
7097 Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
7098 name,
7099 or_replace,
7100 event,
7101 table,
7102 instead,
7103 when_condition,
7104 commands,
7105 }))
7106 }
7107
7108 /// v7.39 (round 139) — a rule event keyword → uppercase string.
7109 fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
7110 if matches!(self.peek(), Token::Insert) {
7111 self.advance();
7112 return Ok(alloc::string::String::from("INSERT"));
7113 }
7114 if matches!(self.peek(), Token::Select) {
7115 self.advance();
7116 return Ok(alloc::string::String::from("SELECT"));
7117 }
7118 match self.peek() {
7119 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7120 self.advance();
7121 Ok(alloc::string::String::from("UPDATE"))
7122 }
7123 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7124 self.advance();
7125 Ok(alloc::string::String::from("DELETE"))
7126 }
7127 other => Err(self.err(alloc::format!(
7128 "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
7129 ))),
7130 }
7131 }
7132
7133 /// v7.13.0 — parse one trigger event, then optionally consume
7134 /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
7135 /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
7136 fn parse_trigger_event_with_optional_of(
7137 &mut self,
7138 ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
7139 let ev = self.parse_trigger_event()?;
7140 if !matches!(ev, TriggerEvent::Update) {
7141 return Ok((ev, Vec::new()));
7142 }
7143 // `OF` is a bare ident.
7144 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
7145 return Ok((ev, Vec::new()));
7146 }
7147 self.advance(); // OF
7148 let mut cols: Vec<String> = Vec::new();
7149 loop {
7150 cols.push(self.expect_ident_like()?);
7151 if matches!(self.peek(), Token::Comma) {
7152 self.advance();
7153 continue;
7154 }
7155 break;
7156 }
7157 if cols.is_empty() {
7158 return Err(
7159 self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
7160 );
7161 }
7162 Ok((ev, cols))
7163 }
7164
7165 /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
7166 /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
7167 /// before `BEGIN`, and IF / RAISE / embedded SQL statements
7168 /// inside the body.
7169 /// Called by [`parse_plpgsql_body`] after the body's tokens
7170 /// have been lexed into this temporary parser.
7171 pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
7172 // v7.12.6 — optional DECLARE prelude.
7173 let declarations = if matches!(
7174 self.peek(),
7175 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
7176 ) {
7177 self.advance();
7178 self.parse_plpgsql_declare_block()?
7179 } else {
7180 Vec::new()
7181 };
7182 // BEGIN keyword (PL/pgSQL — distinct from the SQL
7183 // `BEGIN` transaction-start, but we can reuse the
7184 // reserved Token::Begin since the body is a separate
7185 // lex/parse context).
7186 if !matches!(self.peek(), Token::Begin) {
7187 return Err(self.err(alloc::format!(
7188 "expected BEGIN at start of plpgsql block, got {:?}",
7189 self.peek()
7190 )));
7191 }
7192 self.advance();
7193 let statements = self.parse_plpgsql_stmt_list_until_end()?;
7194 // v7.37.20 (20.10) — optional EXCEPTION clause between the
7195 // body's last statement and the trailing END. When present
7196 // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
7197 // arms terminated by END.
7198 let exception_handlers = if matches!(
7199 self.peek(),
7200 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
7201 ) {
7202 self.advance();
7203 self.parse_plpgsql_exception_handlers()?
7204 } else {
7205 Vec::new()
7206 };
7207 Ok(PlPgSqlBlock {
7208 declarations,
7209 statements,
7210 exception_handlers,
7211 })
7212 }
7213
7214 /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
7215 /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
7216 fn parse_plpgsql_exception_handlers(
7217 &mut self,
7218 ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
7219 let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
7220 loop {
7221 // Stop at END — the block-level trailing END LOOP / END;
7222 // is handled by the caller.
7223 if matches!(
7224 self.peek(),
7225 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
7226 ) {
7227 return Ok(out);
7228 }
7229 // WHEN <cond> [OR <cond>]* THEN <body>
7230 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7231 {
7232 return Err(self.err(alloc::format!(
7233 "expected WHEN or END inside EXCEPTION clause, got {:?}",
7234 self.peek()
7235 )));
7236 }
7237 self.advance();
7238 let mut conditions: Vec<String> = Vec::new();
7239 conditions.push(self.expect_ident_like()?);
7240 while matches!(self.peek(), Token::Or) {
7241 self.advance();
7242 conditions.push(self.expect_ident_like()?);
7243 }
7244 let then_kw = self.expect_ident_like()?;
7245 if !then_kw.eq_ignore_ascii_case("then") {
7246 return Err(self.err(alloc::format!(
7247 "expected THEN after WHEN condition list, got {then_kw:?}"
7248 )));
7249 }
7250 let body = self.parse_plpgsql_stmt_list_until_end()?;
7251 out.push(crate::ast::ExceptionHandler { conditions, body });
7252 }
7253 }
7254
7255 /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
7256 /// prelude. Caller has already consumed `DECLARE`. We stop
7257 /// reading entries when we hit `BEGIN`.
7258 fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
7259 let mut out: Vec<PlPgSqlDeclare> = Vec::new();
7260 loop {
7261 if matches!(self.peek(), Token::Begin) {
7262 return Ok(out);
7263 }
7264 let name = self.expect_ident_like()?;
7265 // v7.37.20 (20.7) — type inference: if the next token is
7266 // `:=` or `=` (no explicit type), infer from the default
7267 // expression. Otherwise the ident that follows is the
7268 // declared type.
7269 //
7270 // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
7271 // (PG-standard). SPG parse-accepts and treats identically
7272 // to inference — the eventual runtime value determines
7273 // the local's type, which is faithful to how SPG handles
7274 // untyped locals today (see 20.7). Full compile-time
7275 // catalog lookup queues with v7.40 PL/pgSQL epic.
7276 let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
7277 // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
7278 // downstream declaration walker to type the local by
7279 // the runtime type of the default expression.
7280 FunctionArgType::Raw("_infer_".into())
7281 } else {
7282 let ty_token = self.expect_ident_like()?;
7283 // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
7284 // consume optional `.<ident>` qualifier + `%<KW>`
7285 // suffix. Both qualifier and suffix map to _infer_.
7286 if matches!(self.peek(), Token::Dot) {
7287 self.advance();
7288 let _ = self.expect_ident_like()?;
7289 }
7290 if matches!(self.peek(), Token::Percent) {
7291 self.advance();
7292 // Consume the trailing TYPE / ROWTYPE ident.
7293 let _ = self.expect_ident_like()?;
7294 FunctionArgType::Raw("_infer_".into())
7295 } else {
7296 match map_type_ident_to_column_type_name(&ty_token) {
7297 Some(t) => FunctionArgType::Typed(t),
7298 None => FunctionArgType::Raw(ty_token),
7299 }
7300 }
7301 };
7302 let default = match self.peek() {
7303 Token::ColonEq => {
7304 self.advance();
7305 Some(self.parse_expr(0)?)
7306 }
7307 Token::Eq => {
7308 // PL/pgSQL also accepts `=` for the
7309 // DECLARE default (PG treats them the same
7310 // in this position).
7311 self.advance();
7312 Some(self.parse_expr(0)?)
7313 }
7314 _ => None,
7315 };
7316 // Mandatory `;` between declarations.
7317 if !matches!(self.peek(), Token::Semicolon) {
7318 return Err(self.err(alloc::format!(
7319 "expected ; after DECLARE entry for {name:?}, got {:?}",
7320 self.peek()
7321 )));
7322 }
7323 self.advance();
7324 out.push(PlPgSqlDeclare { name, ty, default });
7325 }
7326 }
7327
7328 /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
7329 /// the terminating `END;` (or `END IF;` etc — handled by the
7330 /// per-construct sub-parsers). Used by both the outer block
7331 /// and the IF/ELSE branch bodies.
7332 fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
7333 let mut statements: Vec<PlPgSqlStmt> = Vec::new();
7334 loop {
7335 // Allow trailing semicolons + END.
7336 while matches!(self.peek(), Token::Semicolon) {
7337 self.advance();
7338 }
7339 // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7340 if matches!(
7341 self.peek(),
7342 Token::Ident(s) | Token::QuotedIdent(s)
7343 if s.eq_ignore_ascii_case("end")
7344 || s.eq_ignore_ascii_case("else")
7345 || s.eq_ignore_ascii_case("elsif")
7346 || s.eq_ignore_ascii_case("elseif")
7347 || s.eq_ignore_ascii_case("exception")
7348 || s.eq_ignore_ascii_case("when")
7349 ) {
7350 return Ok(statements);
7351 }
7352 // Otherwise: one statement, then expect `;` or
7353 // a block-terminator keyword.
7354 let stmt = self.parse_plpgsql_stmt()?;
7355 statements.push(stmt);
7356 match self.peek() {
7357 Token::Semicolon => {
7358 self.advance();
7359 }
7360 Token::Ident(s) | Token::QuotedIdent(s)
7361 if s.eq_ignore_ascii_case("end")
7362 || s.eq_ignore_ascii_case("else")
7363 || s.eq_ignore_ascii_case("elsif")
7364 || s.eq_ignore_ascii_case("elseif")
7365 || s.eq_ignore_ascii_case("exception")
7366 || s.eq_ignore_ascii_case("when") =>
7367 {
7368 // Final statement of the block without `;`.
7369 }
7370 other => {
7371 return Err(self.err(alloc::format!(
7372 "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7373 )));
7374 }
7375 }
7376 }
7377 }
7378
7379 fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7380 // RETURN keyword?
7381 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7382 {
7383 self.advance();
7384 return self.parse_plpgsql_return();
7385 }
7386 // v7.12.6 — IF block.
7387 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7388 {
7389 self.advance();
7390 return self.parse_plpgsql_if();
7391 }
7392 // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7393 // Detected by peeking that token pos+3 is Ident("execute").
7394 if matches!(self.peek(), Token::For)
7395 && matches!(
7396 self.tokens.get(self.pos + 1),
7397 Some(Token::Ident(_) | Token::QuotedIdent(_))
7398 )
7399 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7400 && matches!(
7401 self.tokens.get(self.pos + 3),
7402 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7403 )
7404 {
7405 self.advance(); // FOR
7406 let var = self.expect_ident_like()?;
7407 self.advance(); // IN
7408 self.advance(); // EXECUTE
7409 // Prescan for LOOP at paren depth 0 so parse_expr stops
7410 // before the LOOP keyword (same trick as the bare-SELECT
7411 // ForQuery arm).
7412 let mut depth: i32 = 0;
7413 let mut loop_pos: Option<usize> = None;
7414 let mut scan = self.pos;
7415 while scan < self.tokens.len() {
7416 match self.tokens.get(scan) {
7417 Some(Token::LParen) => depth += 1,
7418 Some(Token::RParen) => depth -= 1,
7419 Some(Token::Ident(s) | Token::QuotedIdent(s))
7420 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7421 {
7422 loop_pos = Some(scan);
7423 break;
7424 }
7425 _ => {}
7426 }
7427 scan += 1;
7428 }
7429 let loop_pos = loop_pos.ok_or_else(|| {
7430 self.err(alloc::format!(
7431 "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7432 ))
7433 })?;
7434 let saved_loop = self.tokens[loop_pos].clone();
7435 self.tokens[loop_pos] = Token::Semicolon;
7436 let expr_result = self.parse_expr(0);
7437 self.tokens[loop_pos] = saved_loop;
7438 let sql_expr = expr_result?;
7439 let loop_kw = self.expect_ident_like()?;
7440 if !loop_kw.eq_ignore_ascii_case("loop") {
7441 return Err(self.err(alloc::format!(
7442 "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7443 )));
7444 }
7445 let body = self.parse_plpgsql_stmt_list_until_end()?;
7446 let end_kw = self.expect_ident_like()?;
7447 if !end_kw.eq_ignore_ascii_case("end") {
7448 return Err(self.err(alloc::format!(
7449 "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7450 )));
7451 }
7452 let loop_kw2 = self.expect_ident_like()?;
7453 if !loop_kw2.eq_ignore_ascii_case("loop") {
7454 return Err(self.err(alloc::format!(
7455 "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7456 )));
7457 }
7458 return Ok(PlPgSqlStmt::ForExecute {
7459 var,
7460 sql_expr,
7461 body,
7462 });
7463 }
7464 // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7465 //
7466 // Two syntactic forms:
7467 // FOR var IN SELECT ... ORDER BY ... LOOP ...
7468 // FOR var IN (SELECT ...) LOOP ...
7469 //
7470 // Bare-SELECT form: to keep parse_select_stmt from swallowing
7471 // the trailing `LOOP` keyword as a table alias, we prescan
7472 // forward to find LOOP at paren depth 0, splice a fake
7473 // Semicolon at that position (so SELECT parses cleanly),
7474 // then re-splice LOOP back in.
7475 //
7476 // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7477 // LOOP directly — no scan required.
7478 if matches!(self.peek(), Token::For)
7479 && matches!(
7480 self.tokens.get(self.pos + 1),
7481 Some(Token::Ident(_) | Token::QuotedIdent(_))
7482 )
7483 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7484 && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7485 || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7486 {
7487 self.advance(); // FOR
7488 let var = self.expect_ident_like()?;
7489 // IN
7490 self.advance();
7491 let query = if matches!(self.peek(), Token::LParen) {
7492 // Paren-wrapped SELECT.
7493 self.advance();
7494 let inner = self.parse_select_stmt()?;
7495 let Statement::Select(q) = inner else {
7496 return Err(self.err(alloc::format!(
7497 "expected SELECT inside (…), got {:?}",
7498 self.peek()
7499 )));
7500 };
7501 if !matches!(self.peek(), Token::RParen) {
7502 return Err(self.err(alloc::format!(
7503 "expected ')' after FOR-IN-SELECT body, got {:?}",
7504 self.peek()
7505 )));
7506 }
7507 self.advance();
7508 q
7509 } else {
7510 // Bare SELECT: prescan to find the LOOP boundary.
7511 let mut depth: i32 = 0;
7512 let mut loop_pos: Option<usize> = None;
7513 let mut scan = self.pos;
7514 while scan < self.tokens.len() {
7515 match self.tokens.get(scan) {
7516 Some(Token::LParen) => depth += 1,
7517 Some(Token::RParen) => depth -= 1,
7518 Some(Token::Ident(s) | Token::QuotedIdent(s))
7519 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7520 {
7521 loop_pos = Some(scan);
7522 break;
7523 }
7524 _ => {}
7525 }
7526 scan += 1;
7527 }
7528 let loop_pos = loop_pos.ok_or_else(|| {
7529 self.err(alloc::format!(
7530 "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7531 ))
7532 })?;
7533 // Swap the LOOP token with a synthetic Semicolon so
7534 // parse_select_stmt stops there, then restore afterward.
7535 let saved_loop = self.tokens[loop_pos].clone();
7536 self.tokens[loop_pos] = Token::Semicolon;
7537 let parse_result = self.parse_select_stmt();
7538 self.tokens[loop_pos] = saved_loop;
7539 let inner = parse_result?;
7540 let Statement::Select(q) = inner else {
7541 return Err(self.err(alloc::format!(
7542 "expected SELECT after FOR <var> IN, got {:?}",
7543 self.peek()
7544 )));
7545 };
7546 q
7547 };
7548 let loop_kw = self.expect_ident_like()?;
7549 if !loop_kw.eq_ignore_ascii_case("loop") {
7550 return Err(self.err(alloc::format!(
7551 "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7552 )));
7553 }
7554 let body = self.parse_plpgsql_stmt_list_until_end()?;
7555 let end_kw = self.expect_ident_like()?;
7556 if !end_kw.eq_ignore_ascii_case("end") {
7557 return Err(self.err(alloc::format!(
7558 "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7559 )));
7560 }
7561 let loop_kw2 = self.expect_ident_like()?;
7562 if !loop_kw2.eq_ignore_ascii_case("loop") {
7563 return Err(self.err(alloc::format!(
7564 "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7565 )));
7566 }
7567 return Ok(PlPgSqlStmt::ForQuery {
7568 var,
7569 query: Box::new(query),
7570 body,
7571 });
7572 }
7573 // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7574 // FOR is a reserved keyword token (Token::For).
7575 if matches!(self.peek(), Token::For)
7576 && matches!(
7577 self.tokens.get(self.pos + 1),
7578 Some(Token::Ident(_) | Token::QuotedIdent(_))
7579 )
7580 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7581 {
7582 self.advance(); // FOR
7583 let var = self.expect_ident_like()?;
7584 if !matches!(self.peek(), Token::In) {
7585 return Err(self.err(alloc::format!(
7586 "expected IN after FOR <var>, got {:?}",
7587 self.peek()
7588 )));
7589 }
7590 self.advance();
7591 let reverse = matches!(
7592 self.peek(),
7593 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7594 );
7595 if reverse {
7596 self.advance();
7597 }
7598 let start = self.parse_expr(0)?;
7599 if !matches!(self.peek(), Token::DotDot) {
7600 return Err(self.err(alloc::format!(
7601 "expected '..' between FOR loop bounds, got {:?}",
7602 self.peek()
7603 )));
7604 }
7605 self.advance();
7606 let end = self.parse_expr(0)?;
7607 let loop_kw = self.expect_ident_like()?;
7608 if !loop_kw.eq_ignore_ascii_case("loop") {
7609 return Err(self.err(alloc::format!(
7610 "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7611 )));
7612 }
7613 let body = self.parse_plpgsql_stmt_list_until_end()?;
7614 let end_kw = self.expect_ident_like()?;
7615 if !end_kw.eq_ignore_ascii_case("end") {
7616 return Err(self.err(alloc::format!(
7617 "expected END LOOP after FOR body, got {end_kw:?}"
7618 )));
7619 }
7620 let loop_kw2 = self.expect_ident_like()?;
7621 if !loop_kw2.eq_ignore_ascii_case("loop") {
7622 return Err(self.err(alloc::format!(
7623 "expected END LOOP after FOR body, got END {loop_kw2:?}"
7624 )));
7625 }
7626 return Ok(PlPgSqlStmt::ForRange {
7627 var,
7628 start,
7629 end,
7630 reverse,
7631 body,
7632 });
7633 }
7634 // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7635 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7636 {
7637 self.advance();
7638 let body = self.parse_plpgsql_stmt_list_until_end()?;
7639 let end_kw = self.expect_ident_like()?;
7640 if !end_kw.eq_ignore_ascii_case("end") {
7641 return Err(self.err(alloc::format!(
7642 "expected END LOOP after LOOP body, got {end_kw:?}"
7643 )));
7644 }
7645 let loop_kw = self.expect_ident_like()?;
7646 if !loop_kw.eq_ignore_ascii_case("loop") {
7647 return Err(self.err(alloc::format!(
7648 "expected END LOOP after LOOP body, got END {loop_kw:?}"
7649 )));
7650 }
7651 return Ok(PlPgSqlStmt::Loop { body });
7652 }
7653 // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7654 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7655 {
7656 self.advance();
7657 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7658 {
7659 self.advance();
7660 Some(self.parse_expr(0)?)
7661 } else {
7662 None
7663 };
7664 return Ok(PlPgSqlStmt::Exit { when });
7665 }
7666 // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7667 // already-parsed Statement or a runtime-computed SQL string.
7668 // The disambiguator vs the extended-query-protocol `EXECUTE
7669 // <stmt_name>` (which is a top-level Statement, not a
7670 // plpgsql line) is that inside a DO block / trigger body the
7671 // EXECUTE keyword ALWAYS refers to dynamic SQL.
7672 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7673 {
7674 self.advance();
7675 let sql = self.parse_expr(0)?;
7676 return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7677 }
7678 // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7679 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7680 {
7681 self.advance();
7682 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7683 {
7684 self.advance();
7685 Some(self.parse_expr(0)?)
7686 } else {
7687 None
7688 };
7689 return Ok(PlPgSqlStmt::Continue { when });
7690 }
7691 // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7692 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7693 {
7694 self.advance();
7695 let condition = self.parse_expr(0)?;
7696 let loop_kw = self.expect_ident_like()?;
7697 if !loop_kw.eq_ignore_ascii_case("loop") {
7698 return Err(self.err(alloc::format!(
7699 "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7700 )));
7701 }
7702 let body = self.parse_plpgsql_stmt_list_until_end()?;
7703 // Expect END LOOP.
7704 let end_kw = self.expect_ident_like()?;
7705 if !end_kw.eq_ignore_ascii_case("end") {
7706 return Err(self.err(alloc::format!(
7707 "expected END LOOP after WHILE body, got {end_kw:?}"
7708 )));
7709 }
7710 let loop_kw2 = self.expect_ident_like()?;
7711 if !loop_kw2.eq_ignore_ascii_case("loop") {
7712 return Err(self.err(alloc::format!(
7713 "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7714 )));
7715 }
7716 return Ok(PlPgSqlStmt::While { condition, body });
7717 }
7718 // v7.12.6 — RAISE.
7719 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7720 {
7721 self.advance();
7722 return self.parse_plpgsql_raise();
7723 }
7724 // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7725 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7726 {
7727 self.advance();
7728 let condition = self.parse_expr(0)?;
7729 let message = if matches!(self.peek(), Token::Comma) {
7730 self.advance();
7731 Some(self.parse_expr(0)?)
7732 } else {
7733 None
7734 };
7735 return Ok(PlPgSqlStmt::Assert { condition, message });
7736 }
7737 // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7738 // "PERFORM is equivalent to SELECT but discards the
7739 // result." Side effects (function calls, RAISE inside
7740 // SQL functions, etc.) still execute. We desugar to
7741 // `SELECT <body>` and wrap in EmbeddedSql so the engine's
7742 // existing embedded-statement path handles execution +
7743 // result-discard cleanly. The result is naturally
7744 // discarded because EmbeddedSql doesn't propagate row
7745 // sets back to the plpgsql interpreter.
7746 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7747 {
7748 self.advance();
7749 // Splice a synthetic Token::Select into the stream at
7750 // the current position so parse_select_stmt parses the
7751 // remainder as a normal SELECT body. Token-stream
7752 // surgery mirrors the try_parse_plpgsql_select_into
7753 // pattern used for SELECT … INTO desugaring.
7754 self.tokens.insert(self.pos, Token::Select);
7755 let select = self.parse_select_stmt()?;
7756 let Statement::Select(s) = select else {
7757 return Err(self.err(alloc::format!(
7758 "expected SELECT body after PERFORM, got {:?}",
7759 self.peek()
7760 )));
7761 };
7762 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7763 }
7764 // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7765 // plpgsql-specific shape (mailrs round-10 migrate-042).
7766 // PG's SELECT INTO at top-level SQL would CREATE a new
7767 // table; inside plpgsql it ASSIGNS the query result to
7768 // a local variable. We detect the INTO at paren-depth
7769 // 0 between SELECT and the statement boundary; if
7770 // found, split the token stream into "pre-INTO
7771 // projection" + "var" + "post-INTO FROM/WHERE…" and
7772 // rebuild as a SelectInto with a regular SELECT body
7773 // (no INTO clause).
7774 if matches!(self.peek(), Token::Select)
7775 && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7776 {
7777 return Ok(PlPgSqlStmt::SelectInto {
7778 var: var_name,
7779 body: Box::new(select_body),
7780 });
7781 }
7782 // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7783 // SELECT can appear directly inside a trigger body; we
7784 // recurse into the regular Statement parser, which will
7785 // stop at the trailing `;` (which our caller then
7786 // consumes).
7787 // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7788 // also embed ALTER / CREATE / DROP statements; route
7789 // those through the same parser so the DO body parses
7790 // cleanly.
7791 if matches!(self.peek(), Token::Insert)
7792 || matches!(self.peek(), Token::Select)
7793 || matches!(self.peek(), Token::Create)
7794 || matches!(self.peek(), Token::Drop)
7795 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7796 if s.eq_ignore_ascii_case("update")
7797 || s.eq_ignore_ascii_case("delete")
7798 || s.eq_ignore_ascii_case("alter"))
7799 {
7800 let stmt = self.parse_one_statement()?;
7801 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7802 }
7803 // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7804 // followed by `:=` and an expression.
7805 let target = self.parse_plpgsql_assign_target()?;
7806 // PL/pgSQL assignment uses `:=`. The lexer represents
7807 // this as a colon followed by `=`; check both shapes.
7808 match self.peek() {
7809 Token::ColonEq => {
7810 self.advance();
7811 }
7812 Token::Colon => {
7813 self.advance();
7814 if !matches!(self.peek(), Token::Eq) {
7815 return Err(self.err(alloc::format!(
7816 "expected := after plpgsql assign target, got `:` then {:?}",
7817 self.peek()
7818 )));
7819 }
7820 self.advance();
7821 }
7822 other => {
7823 return Err(self.err(alloc::format!(
7824 "expected := after plpgsql assign target, got {other:?}"
7825 )));
7826 }
7827 }
7828 let value = self.parse_expr(0)?;
7829 Ok(PlPgSqlStmt::Assign { target, value })
7830 }
7831
7832 /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7833 /// [ELSE body] END IF`. `IF` keyword already consumed.
7834 fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7835 let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7836 let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7837 loop {
7838 // <expr> THEN
7839 let cond = self.parse_expr(0)?;
7840 let then_kw = self.expect_ident_like()?;
7841 if !then_kw.eq_ignore_ascii_case("then") {
7842 return Err(self.err(alloc::format!(
7843 "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7844 )));
7845 }
7846 let body = self.parse_plpgsql_stmt_list_until_end()?;
7847 branches.push((cond, body));
7848 // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7849 match self.peek() {
7850 Token::Ident(s) | Token::QuotedIdent(s)
7851 if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7852 {
7853 self.advance();
7854 continue;
7855 }
7856 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7857 self.advance();
7858 else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7859 break;
7860 }
7861 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7862 break;
7863 }
7864 other => {
7865 return Err(self.err(alloc::format!(
7866 "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7867 )));
7868 }
7869 }
7870 }
7871 // Expect `END IF` (the END keyword is the one we're
7872 // looking at right now).
7873 let end_kw = self.expect_ident_like()?;
7874 if !end_kw.eq_ignore_ascii_case("end") {
7875 return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7876 }
7877 let if_kw = self.expect_ident_like()?;
7878 if !if_kw.eq_ignore_ascii_case("if") {
7879 return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7880 }
7881 Ok(PlPgSqlStmt::If {
7882 branches,
7883 else_branch,
7884 })
7885 }
7886
7887 /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7888 /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7889 /// is already consumed.
7890 fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7891 let lvl_ident = self.expect_ident_like()?;
7892 let level = match lvl_ident.to_ascii_lowercase().as_str() {
7893 "notice" => RaiseLevel::Notice,
7894 "warning" => RaiseLevel::Warning,
7895 "info" => RaiseLevel::Info,
7896 "log" => RaiseLevel::Log,
7897 "debug" => RaiseLevel::Debug,
7898 "exception" => RaiseLevel::Exception,
7899 other => {
7900 return Err(self.err(alloc::format!(
7901 "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7902 )));
7903 }
7904 };
7905 // Message: required for v7.12.6. PG accepts a bare
7906 // RAISE-rethrow form (no message), reserved for future
7907 // RAISE-no-args support.
7908 let Token::String(msg) = self.peek() else {
7909 return Err(self.err(alloc::format!(
7910 "expected RAISE message string, got {:?}",
7911 self.peek()
7912 )));
7913 };
7914 let message = msg.clone();
7915 self.advance();
7916 // Optional comma-separated args (PG `%` format substitution).
7917 let mut args: Vec<Expr> = Vec::new();
7918 while matches!(self.peek(), Token::Comma) {
7919 self.advance();
7920 args.push(self.parse_expr(0)?);
7921 }
7922 Ok(PlPgSqlStmt::Raise {
7923 level,
7924 message,
7925 args,
7926 })
7927 }
7928
7929 /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7930 /// <projection> INTO <var> [FROM …]` (mailrs round-10
7931 /// migrate-042). Returns `(rebuilt_select_without_into,
7932 /// var_name)` when the pattern matches; `None` for
7933 /// regular SELECTs (those go through the embedded-SQL
7934 /// path). Token-stream surgery so the rebuilt SELECT
7935 /// parses through the regular `parse_select_stmt`.
7936 #[allow(clippy::too_many_lines)]
7937 fn try_parse_plpgsql_select_into(
7938 &mut self,
7939 ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7940 // Scan forward from `self.pos + 1` (past Token::Select)
7941 // for Token::Into at paren-depth 0, stopping at the
7942 // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7943 // end the plpgsql statement.
7944 let start = self.pos;
7945 let mut into_pos: Option<usize> = None;
7946 let mut depth: i32 = 0;
7947 let mut i = start + 1;
7948 while i < self.tokens.len() {
7949 match &self.tokens[i] {
7950 Token::LParen => depth += 1,
7951 Token::RParen => depth -= 1,
7952 Token::Semicolon if depth == 0 => break,
7953 Token::Ident(s)
7954 if depth == 0
7955 && (s.eq_ignore_ascii_case("end")
7956 || s.eq_ignore_ascii_case("else")
7957 || s.eq_ignore_ascii_case("elsif")) =>
7958 {
7959 break;
7960 }
7961 Token::Into if depth == 0 => {
7962 into_pos = Some(i);
7963 break;
7964 }
7965 _ => {}
7966 }
7967 i += 1;
7968 }
7969 let Some(into_at) = into_pos else {
7970 return Ok(None);
7971 };
7972 // The token immediately after INTO must be the target
7973 // var ident; anything else (e.g. INSERT INTO table)
7974 // ruled out by the depth-0 check above. Capture it.
7975 let var = match self.tokens.get(into_at + 1) {
7976 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7977 other => {
7978 return Err(self.err(alloc::format!(
7979 "expected variable name after SELECT … INTO, got {other:?}"
7980 )));
7981 }
7982 };
7983 // Find the end of the plpgsql SELECT INTO statement —
7984 // same boundary rules as the depth-0 scan above.
7985 let mut end = into_at + 2;
7986 let mut depth2: i32 = 0;
7987 while end < self.tokens.len() {
7988 match &self.tokens[end] {
7989 Token::LParen => depth2 += 1,
7990 Token::RParen => depth2 -= 1,
7991 Token::Semicolon if depth2 == 0 => break,
7992 Token::Ident(s)
7993 if depth2 == 0
7994 && (s.eq_ignore_ascii_case("end")
7995 || s.eq_ignore_ascii_case("else")
7996 || s.eq_ignore_ascii_case("elsif")) =>
7997 {
7998 break;
7999 }
8000 _ => {}
8001 }
8002 end += 1;
8003 }
8004 // Rebuild a token stream that represents the SELECT
8005 // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
8006 // post-var tokens up to statement end]. Run the
8007 // regular `parse_select_stmt` against it.
8008 let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
8009 for j in start..into_at {
8010 rebuilt.push(self.tokens[j].clone());
8011 }
8012 for j in (into_at + 2)..end {
8013 rebuilt.push(self.tokens[j].clone());
8014 }
8015 rebuilt.push(Token::Eof);
8016 let saved_pos = self.pos;
8017 let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
8018 self.pos = 0;
8019 // parse_select_stmt → parse_bare_select consumes Token::Select itself.
8020 if !matches!(self.peek(), Token::Select) {
8021 self.tokens = saved_tokens;
8022 self.pos = saved_pos;
8023 return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
8024 }
8025 let sel = self.parse_select_stmt();
8026 self.tokens = saved_tokens;
8027 self.pos = end;
8028 let sel = sel?;
8029 let Statement::Select(body) = sel else {
8030 return Err(self.err(alloc::format!(
8031 "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
8032 )));
8033 };
8034 Ok(Some((body, var)))
8035 }
8036
8037 fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
8038 // v7.16.1 — read the head token DIRECTLY rather than
8039 // via `expect_ident_like`. The v7.14.0 schema-qualifier
8040 // strip (`public.t` → `t`) inside `expect_ident_like`
8041 // greedily consumes any `ident . ident` pair, which
8042 // silently turned every `NEW.col := …` /
8043 // `OLD.col := …` plpgsql assignment into a Local("col")
8044 // assignment — the head "new"/"old" was eaten as if it
8045 // were a schema name and the Dot was consumed too, so
8046 // this function's own `peek() == Token::Dot` check
8047 // below never fired. Every BEFORE trigger that rewrote
8048 // a NEW cell was a silent no-op for two major releases
8049 // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
8050 // gate failures were investigated as v7.16.1 backlog.
8051 let head = match self.advance() {
8052 Token::Ident(s) | Token::QuotedIdent(s) => s,
8053 other => {
8054 return Err(self.err(alloc::format!(
8055 "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
8056 )));
8057 }
8058 };
8059 if matches!(self.peek(), Token::Dot) {
8060 self.advance();
8061 let col = self.expect_ident_like()?;
8062 if head.eq_ignore_ascii_case("new") {
8063 return Ok(AssignTarget::NewColumn(col));
8064 }
8065 if head.eq_ignore_ascii_case("old") {
8066 return Ok(AssignTarget::OldColumn(col));
8067 }
8068 return Err(self.err(alloc::format!(
8069 "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
8070 got {head:?}.<col>"
8071 )));
8072 }
8073 Ok(AssignTarget::Local(head))
8074 }
8075
8076 fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
8077 // RETURN NEW / OLD / NULL — bare-ident forms.
8078 match self.peek() {
8079 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
8080 self.advance();
8081 return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
8082 }
8083 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
8084 self.advance();
8085 return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
8086 }
8087 Token::Null => {
8088 self.advance();
8089 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8090 }
8091 // Bare `RETURN;` (no value) — treated as `RETURN NULL`
8092 // per PL/pgSQL convention.
8093 Token::Semicolon => {
8094 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8095 }
8096 _ => {}
8097 }
8098 // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
8099 // EXECUTE <expr>. In a DO block context RETURN QUERY has no
8100 // caller-visible effect (blocks don't return sets), so we
8101 // desugar it identically to PERFORM: parse the SELECT (or
8102 // EXECUTE dynamic) as embedded SQL that runs for side
8103 // effects and discards the result. RETURN NEXT <expr>
8104 // (single-row accumulator) queues with v7.40 SETOF function
8105 // infrastructure.
8106 // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
8107 // and keep going.
8108 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
8109 {
8110 self.advance();
8111 let e = self.parse_expr(0)?;
8112 return Ok(PlPgSqlStmt::ReturnNext(e));
8113 }
8114 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
8115 {
8116 self.advance();
8117 // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
8118 // rows go to the set, like the static form. It used to desugar to a
8119 // bare ExecuteDynamic, whose result was DISCARDED.
8120 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
8121 {
8122 self.advance();
8123 let sql = self.parse_expr(0)?;
8124 return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
8125 }
8126 // Bare RETURN QUERY <select>. If the current token is
8127 // not already SELECT (e.g., the user wrote `RETURN QUERY
8128 // <projection> FROM ...` in a shorthand — rare but PG
8129 // accepts a bare projection here), splice one in. Same
8130 // trick as PERFORM.
8131 if !matches!(self.peek(), Token::Select) {
8132 self.tokens.insert(self.pos, Token::Select);
8133 }
8134 let select = self.parse_select_stmt()?;
8135 let Statement::Select(s) = select else {
8136 return Err(self.err(alloc::format!(
8137 "expected SELECT body after RETURN QUERY, got {:?}",
8138 self.peek()
8139 )));
8140 };
8141 // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
8142 // to an embedded side-effect SELECT whose rows were DISCARDED, which
8143 // in a SETOF function is the entire answer thrown away.
8144 return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
8145 }
8146 // Fall through: parse a full expression.
8147 let e = self.parse_expr(0)?;
8148 Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
8149 }
8150
8151 fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
8152 // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
8153 // are ident-shaped (the parser keys off case-insensitive
8154 // match — same shape used by the top-level Update / Delete
8155 // dispatchers at parse_one_statement).
8156 if matches!(self.peek(), Token::Insert) {
8157 self.advance();
8158 return Ok(TriggerEvent::Insert);
8159 }
8160 match self.peek() {
8161 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
8162 self.advance();
8163 Ok(TriggerEvent::Update)
8164 }
8165 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
8166 self.advance();
8167 Ok(TriggerEvent::Delete)
8168 }
8169 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
8170 self.advance();
8171 Ok(TriggerEvent::Truncate)
8172 }
8173 other => Err(self.err(alloc::format!(
8174 "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
8175 ))),
8176 }
8177 }
8178
8179 /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
8180 /// - (no clause) → implicit `FOR ALL TABLES`
8181 /// - `FOR ALL TABLES`
8182 /// - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
8183 /// - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
8184 /// accepted as an SPG lenience. PG18-measured (round 753): PG
8185 /// REJECTS the bare plural (`invalid publication object list`,
8186 /// TABLES only pairs with IN SCHEMA); the old note claimed an
8187 /// unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
8188 fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
8189 let name = self.expect_ident_or_string()?;
8190 // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
8191 // shape so existing publications keep parsing identically.
8192 let scope = if matches!(self.peek(), Token::For) {
8193 self.advance();
8194 if matches!(self.peek(), Token::All) {
8195 self.advance();
8196 if !matches!(self.peek(), Token::Tables) {
8197 return Err(self.err(format!(
8198 "expected TABLES after FOR ALL, got {:?}",
8199 self.peek()
8200 )));
8201 }
8202 self.advance();
8203 if matches!(self.peek(), Token::Except) {
8204 self.advance();
8205 let tables = self.parse_publication_table_list()?;
8206 PublicationScope::AllTablesExcept(tables)
8207 } else {
8208 PublicationScope::AllTables
8209 }
8210 } else if matches!(self.peek(), Token::Table) {
8211 self.advance();
8212 let tables = self.parse_publication_table_list()?;
8213 PublicationScope::ForTables(tables)
8214 } else if matches!(self.peek(), Token::Tables) {
8215 // v7.39 (round 754, F31-B5) — PG18-measured: the bare
8216 // plural (`FOR TABLES t`) is REJECTED (`invalid
8217 // publication object list`); TABLES only pairs with
8218 // `IN SCHEMA`. The old arm accepted it on an
8219 // unverifiable "PG 19 accepts both" claim.
8220 self.advance();
8221 if !matches!(self.peek(), Token::In) {
8222 return Err(self.err(alloc::string::String::from(
8223 "invalid publication object list",
8224 )));
8225 }
8226 self.advance();
8227 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
8228 return Err(self.err(format!(
8229 "expected SCHEMA after FOR TABLES IN, got {:?}",
8230 self.peek()
8231 )));
8232 }
8233 self.advance();
8234 let schema = self.expect_ident_or_string()?;
8235 PublicationScope::TablesInSchema(schema)
8236 } else {
8237 return Err(self.err(format!(
8238 "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
8239 self.peek()
8240 )));
8241 }
8242 } else {
8243 PublicationScope::AllTables
8244 };
8245 Ok(Statement::CreatePublication(CreatePublicationStatement {
8246 name,
8247 scope,
8248 }))
8249 }
8250
8251 /// v6.1.3 — Comma-separated identifier list for the publication
8252 /// FOR-clause. Requires at least one entry; empty list is a
8253 /// parse error (PG behaviour). Quoted idents are accepted; the
8254 /// names round-trip through `Display` as `quote_ident(name)`.
8255 ///
8256 /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
8257 /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
8258 /// pg_dump output. SPG's publication state today is per-table
8259 /// only (matching the pre-PG-15 surface); the col list + WHERE
8260 /// are parsed so dumps load through and the table name reaches
8261 /// `PublicationScope::ForTables`, but the filter is not enforced
8262 /// at publish time. Re-open when a customer dogfood gate
8263 /// requires per-row-filter or column-subset publish semantics
8264 /// (which gates on persistent slot state landing first, 21.12).
8265 fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
8266 let first = self.parse_publication_table_entry()?;
8267 let mut out = alloc::vec![first];
8268 while matches!(self.peek(), Token::Comma) {
8269 self.advance();
8270 out.push(self.parse_publication_table_entry()?);
8271 }
8272 Ok(out)
8273 }
8274
8275 /// One table entry inside a FOR TABLE clause:
8276 /// tab_name [ (col, col, …) ] [ WHERE (predicate) ]
8277 /// Returns just the table name; the column list + WHERE predicate
8278 /// are consumed and discarded per the parse-accept-discard
8279 /// commitment above.
8280 fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
8281 let name = self.expect_ident_like()?;
8282 // Optional column list — `(col, col, …)`.
8283 if matches!(self.peek(), Token::LParen) {
8284 self.advance();
8285 // Empty parens are a PG error too; require ≥ 1 column.
8286 let _ = self.expect_ident_like()?;
8287 while matches!(self.peek(), Token::Comma) {
8288 self.advance();
8289 let _ = self.expect_ident_like()?;
8290 }
8291 if !matches!(self.peek(), Token::RParen) {
8292 return Err(self.err(alloc::format!(
8293 "expected ')' to close publication column list, got {:?}",
8294 self.peek()
8295 )));
8296 }
8297 self.advance();
8298 }
8299 // Optional row filter — `WHERE (predicate)`.
8300 if matches!(self.peek(), Token::Where) {
8301 self.advance();
8302 if !matches!(self.peek(), Token::LParen) {
8303 return Err(self.err(alloc::format!(
8304 "expected '(' after WHERE in publication row filter, got {:?}",
8305 self.peek()
8306 )));
8307 }
8308 self.advance();
8309 let _ = self.parse_expr(0)?;
8310 if !matches!(self.peek(), Token::RParen) {
8311 return Err(self.err(alloc::format!(
8312 "expected ')' to close publication WHERE filter, got {:?}",
8313 self.peek()
8314 )));
8315 }
8316 self.advance();
8317 }
8318 Ok(name)
8319 }
8320
8321 /// v6.1.4 — `CREATE SUBSCRIPTION <name>
8322 /// CONNECTION '<conn>'
8323 /// PUBLICATION <pub> [, <pub> ...]`.
8324 ///
8325 /// The clause order is fixed (CONNECTION first, then
8326 /// PUBLICATION) to match PG. No WITH-options accepted in
8327 /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
8328 fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
8329 let name = self.expect_ident_or_string()?;
8330 if !matches!(self.peek(), Token::Connection) {
8331 return Err(self.err(format!(
8332 "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
8333 self.peek()
8334 )));
8335 }
8336 self.advance();
8337 let conn_str = self.expect_string_literal()?;
8338 if !matches!(self.peek(), Token::Publication) {
8339 return Err(self.err(format!(
8340 "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8341 self.peek()
8342 )));
8343 }
8344 self.advance();
8345 // Reuse the publication FOR-list parser shape: at least one
8346 // identifier, comma-separated.
8347 let first = self.expect_ident_like()?;
8348 let mut publications = alloc::vec![first];
8349 while matches!(self.peek(), Token::Comma) {
8350 self.advance();
8351 publications.push(self.expect_ident_like()?);
8352 }
8353 Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8354 name,
8355 conn_str,
8356 publications,
8357 }))
8358 }
8359
8360 /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8361 /// All keywords after `WAIT` are bare idents in v6.1.x; no
8362 /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8363 /// that fit `u64`.
8364 /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8365 /// qualifier is a *namespace* the app owns (`app.user_id`,
8366 /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8367 /// to discard. So parse the raw segments here instead of
8368 /// `expect_ident_like`, which strips a leading `schema.` qualifier
8369 /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8370 /// a single segment and round-trip unchanged.
8371 fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8372 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8373 loop {
8374 let seg = match self.advance() {
8375 Token::Ident(s) | Token::QuotedIdent(s) => s,
8376 other if unreserved_keyword_text(&other).is_some() => {
8377 unreserved_keyword_text(&other).unwrap()
8378 }
8379 other => {
8380 return Err(ParseError {
8381 message: format!("expected parameter name, got {other:?}"),
8382 token_pos: self.consumed_pos(),
8383 });
8384 }
8385 };
8386 parts.push(seg);
8387 if matches!(self.peek(), Token::Dot) {
8388 self.advance();
8389 continue;
8390 }
8391 break;
8392 }
8393 Ok(parts.join(".").to_ascii_lowercase())
8394 }
8395
8396 fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8397 Self::parse_set_value_inner(self)
8398 }
8399
8400 fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8401 match self.advance() {
8402 Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8403 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8404 Ok(crate::ast::SetValue::Default)
8405 }
8406 Token::Ident(s) | Token::QuotedIdent(s) => {
8407 let mut accum = s;
8408 while matches!(self.peek(), Token::Dot) {
8409 self.advance();
8410 let next = self.expect_ident_like()?;
8411 accum.push('.');
8412 accum.push_str(&next);
8413 }
8414 Ok(crate::ast::SetValue::Ident(accum))
8415 }
8416 Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8417 Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8418 // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8419 // spellings that lex as keyword tokens, not idents:
8420 // `SET standard_conforming_strings = on` is in every
8421 // pg_dump preamble (`off` already lexes as an ident).
8422 // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8423 // DEFAULT lexes as its keyword token, so the ident arm above
8424 // never saw it and the everyday reset form was a syntax error.
8425 Token::Default => Ok(crate::ast::SetValue::Default),
8426 Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8427 Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8428 Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8429 // v7.14.0 — MySQL session/user variable RHS
8430 // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8431 // Wrap as Ident so the SET handler can record it; the
8432 // engine treats `@VAR` / `@@VAR` values as opaque
8433 // strings.
8434 Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8435 // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8436 // is the common MySQL preamble shape. Allow a `+` or
8437 // `-` prefix on negative numerics for parity with PG
8438 // (some param defaults are negative).
8439 Token::Minus => match self.advance() {
8440 Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8441 Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8442 other => Err(self.err(format!(
8443 "expected numeric after `-` in SET value, got {other:?}"
8444 ))),
8445 },
8446 other => Err(self.err(format!(
8447 "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8448 ))),
8449 }
8450 }
8451
8452 /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8453 /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8454 /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8455 /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8456 /// present). Modes are comma-separated per PG; SPG also
8457 /// accepts space-separated for tolerance. READ ONLY / WRITE
8458 /// / DEFERRABLE are parsed-and-ignored (recorded for future
8459 /// surface but not behaviorally honoured today).
8460 /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8461 /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8462 /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8463 /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8464 /// session default rather than forcing READ COMMITTED.
8465 fn parse_isolation_level_clauses(
8466 &mut self,
8467 ) -> Result<crate::ast::TransactionModes, ParseError> {
8468 let mut level = IsolationLevel::default();
8469 let mut have_level = false;
8470 // v7.39 — READ ONLY / READ WRITE used to be consumed and dropped,
8471 // so `BEGIN READ ONLY` opened an ordinary read-write transaction.
8472 let mut read_only: Option<bool> = None;
8473 loop {
8474 // ISOLATION LEVEL …
8475 let saw_isolation =
8476 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8477 if saw_isolation {
8478 self.advance(); // ISOLATION
8479 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8480 return Err(self.err(alloc::format!(
8481 "expected LEVEL after ISOLATION, got {:?}",
8482 self.peek()
8483 )));
8484 }
8485 self.advance(); // LEVEL
8486 // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8487 let w1 = self
8488 .expect_ident_like()
8489 .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8490 let lc = w1.to_ascii_lowercase();
8491 level = match lc.as_str() {
8492 "serializable" => IsolationLevel::Serializable,
8493 "repeatable" => {
8494 // Expect READ
8495 let w2 = self
8496 .expect_ident_like()
8497 .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8498 if !w2.eq_ignore_ascii_case("read") {
8499 return Err(self.err(alloc::format!(
8500 "expected READ after REPEATABLE, got {w2:?}"
8501 )));
8502 }
8503 IsolationLevel::RepeatableRead
8504 }
8505 "read" => {
8506 let w2 = self
8507 .expect_ident_like()
8508 .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8509 match w2.to_ascii_lowercase().as_str() {
8510 "committed" => IsolationLevel::ReadCommitted,
8511 "uncommitted" => IsolationLevel::ReadUncommitted,
8512 other => {
8513 return Err(self.err(alloc::format!(
8514 "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8515 )));
8516 }
8517 }
8518 }
8519 other => {
8520 return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8521 }
8522 };
8523 have_level = true;
8524 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8525 // v7.39 — READ ONLY | READ WRITE. The comment here used to
8526 // read "parsed, not behaviorally honoured", and it was
8527 // accurate: the clause was thrown away, so `BEGIN READ ONLY`
8528 // opened an ordinary read-write transaction and accepted
8529 // every write in it.
8530 self.advance();
8531 match self.peek().clone() {
8532 Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8533 self.advance();
8534 read_only = Some(true);
8535 }
8536 Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8537 self.advance();
8538 read_only = Some(false);
8539 }
8540 other => {
8541 return Err(self.err(alloc::format!(
8542 "expected ONLY or WRITE after READ, got {other:?}"
8543 )));
8544 }
8545 }
8546 } else if matches!(self.peek(), Token::Not) {
8547 // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8548 self.advance();
8549 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8550 return Err(self.err(alloc::format!(
8551 "expected DEFERRABLE after NOT, got {:?}",
8552 self.peek()
8553 )));
8554 }
8555 self.advance();
8556 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8557 {
8558 self.advance();
8559 } else {
8560 break;
8561 }
8562 // Optional comma between modes.
8563 if matches!(self.peek(), Token::Comma) {
8564 self.advance();
8565 }
8566 }
8567 Ok(crate::ast::TransactionModes {
8568 isolation: have_level.then_some(level),
8569 read_only,
8570 })
8571 }
8572
8573 fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8574 // FOR is a v6.1.2-reserved keyword (Token::For). The
8575 // other two are bare idents — they've never needed lexer
8576 // support and we keep it that way.
8577 if !matches!(self.peek(), Token::For) {
8578 return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8579 }
8580 self.advance();
8581 self.expect_keyword_ident("wal")?;
8582 self.expect_keyword_ident("position")?;
8583 let pos = self.expect_u64_literal()?;
8584 let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8585 {
8586 self.advance();
8587 self.expect_keyword_ident("timeout")?;
8588 Some(self.expect_u64_literal()?)
8589 } else {
8590 None
8591 };
8592 Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8593 }
8594
8595 /// v6.1.7 helper — consume a `Token::Integer` and check it
8596 /// fits `u64`. WAL positions and millisecond timeouts are
8597 /// non-negative.
8598 fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8599 match self.advance() {
8600 Token::Integer(n) if n >= 0 => Ok(n as u64),
8601 Token::Integer(n) => Err(ParseError {
8602 message: format!("expected non-negative integer, got {n}"),
8603 token_pos: self.consumed_pos(),
8604 }),
8605 other => Err(ParseError {
8606 message: format!("expected integer literal, got {other:?}"),
8607 token_pos: self.consumed_pos(),
8608 }),
8609 }
8610 }
8611
8612 /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8613 /// ROLE '<role>' (defaults to readonly). All string slots accept
8614 /// either a quoted ident or a quoted string literal.
8615 /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8616 /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8617 ///
8618 /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8619 /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8620 /// wire role) still parses — it is a different axis from the PG attributes.
8621 /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8622 /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8623 /// or RESET, so the plain attribute forms keep their old path.
8624 fn peeks_db_role_setting(&self) -> bool {
8625 let mut i = self.pos + 1; // past the object's name
8626 let word = |p: usize| -> Option<String> {
8627 match self.tokens.get(p) {
8628 Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8629 Some(Token::In) => Some(String::from("in")),
8630 _ => None,
8631 }
8632 };
8633 if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8634 i += 3; // IN DATABASE <name>
8635 }
8636 matches!(word(i).as_deref(), Some("set" | "reset"))
8637 }
8638
8639 fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8640 use crate::ast::SetDbRoleSettingStatement;
8641 // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8642 // identifier, so the ordinary name reader refuses it. Same trap
8643 // as TABLE / INDEX / FULL / DEFAULT before it.
8644 let name = if matches!(self.peek(), Token::All) {
8645 self.advance();
8646 String::from("all")
8647 } else {
8648 self.expect_ident_or_string()?
8649 };
8650 // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8651 let all = name.eq_ignore_ascii_case("all");
8652 let (mut database, mut role) = if is_database {
8653 (Some(name), None)
8654 } else if all {
8655 (None, None)
8656 } else {
8657 (None, Some(name))
8658 };
8659 if matches!(self.peek(), Token::In) {
8660 self.advance();
8661 self.advance(); // DATABASE
8662 database = Some(self.expect_ident_or_string()?);
8663 }
8664 let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8665 self.advance(); // SET | RESET
8666 if resetting && matches!(self.peek(), Token::All) {
8667 self.advance();
8668 self.consume_until_statement_boundary();
8669 return Ok(Statement::SetDbRoleSetting(Box::new(
8670 SetDbRoleSettingStatement {
8671 database,
8672 role,
8673 param: None,
8674 value: None,
8675 },
8676 )));
8677 }
8678 let param = self.expect_ident_like()?;
8679 let value = if resetting {
8680 None
8681 } else {
8682 // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8683 // KEYWORD, so the ident-only check missed it and consumed
8684 // the word itself as the value — the same trap as ALL, one
8685 // clause over.
8686 if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8687 self.advance();
8688 }
8689 Some(self.take_guc_value())
8690 };
8691 self.consume_until_statement_boundary();
8692 Ok(Statement::SetDbRoleSetting(Box::new(
8693 SetDbRoleSettingStatement {
8694 database,
8695 role,
8696 param: Some(param),
8697 value,
8698 },
8699 )))
8700 }
8701
8702 /// The remainder of a `SET <p> = …` clause as PG renders it back:
8703 /// a quoted literal loses its quotes, a bare word or number does not.
8704 fn take_guc_value(&mut self) -> String {
8705 match self.advance() {
8706 Token::String(s) => s,
8707 Token::Integer(n) => format!("{n}"),
8708 Token::Float(f) => format!("{f}"),
8709 Token::Ident(s) | Token::QuotedIdent(s) => s,
8710 other => format!("{other:?}"),
8711 }
8712 }
8713
8714 fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8715 let name = self.expect_ident_or_string()?;
8716 if self.peek_keyword_ident("with") {
8717 self.advance();
8718 }
8719 let mut password = String::new();
8720 let mut role = String::new();
8721 let mut login: Option<bool> = None;
8722 let mut inherit: Option<bool> = None;
8723 let mut superuser: Option<bool> = None;
8724 // Not a `while let`: the pattern would borrow `self` across the
8725 // body, which calls `self.advance()` / `self.expect_*` (&mut).
8726 #[allow(clippy::while_let_loop)]
8727 loop {
8728 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8729 break;
8730 };
8731 match w.to_ascii_lowercase().as_str() {
8732 "password" => {
8733 self.advance();
8734 password = self.expect_string_literal()?;
8735 }
8736 // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8737 // is the same slot.
8738 "encrypted" => {
8739 self.advance();
8740 self.expect_keyword_ident("password")?;
8741 password = self.expect_string_literal()?;
8742 }
8743 "login" => {
8744 self.advance();
8745 login = Some(true);
8746 }
8747 "nologin" => {
8748 self.advance();
8749 login = Some(false);
8750 }
8751 "inherit" => {
8752 self.advance();
8753 inherit = Some(true);
8754 }
8755 "noinherit" => {
8756 self.advance();
8757 inherit = Some(false);
8758 }
8759 "superuser" => {
8760 self.advance();
8761 superuser = Some(true);
8762 }
8763 "nosuperuser" => {
8764 self.advance();
8765 superuser = Some(false);
8766 }
8767 // SPG's own coarse wire role: `ROLE 'readwrite'`.
8768 "role" => {
8769 self.advance();
8770 role = self.expect_string_literal()?;
8771 }
8772 // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8773 // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8774 // accepted and ignored so a pg_dump role block restores. They
8775 // gate capabilities SPG does not have.
8776 "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8777 | "noreplication" | "bypassrls" | "nobypassrls" => {
8778 self.advance();
8779 }
8780 "connection" => {
8781 self.advance();
8782 self.expect_keyword_ident("limit")?;
8783 self.advance(); // the number
8784 }
8785 "valid" => {
8786 self.advance();
8787 self.expect_keyword_ident("until")?;
8788 self.expect_string_literal()?;
8789 }
8790 _ => break,
8791 }
8792 }
8793 if role.is_empty() {
8794 role = "readonly".to_string();
8795 }
8796 Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8797 name,
8798 password,
8799 role,
8800 login,
8801 inherit,
8802 superuser,
8803 is_user,
8804 }))
8805 }
8806
8807 /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8808 /// consumed the USING / WITH CHECK keyword.
8809 fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8810 if !matches!(self.peek(), Token::LParen) {
8811 return Err(self.err(alloc::format!(
8812 "expected '(' after {clause}, got {:?}",
8813 self.peek()
8814 )));
8815 }
8816 self.advance();
8817 let e = self.parse_expr(0)?;
8818 if !matches!(self.peek(), Token::RParen) {
8819 return Err(self.err(alloc::format!(
8820 "expected ')' to close {clause}, got {:?}",
8821 self.peek()
8822 )));
8823 }
8824 self.advance();
8825 Ok(e)
8826 }
8827
8828 /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8829 fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8830 let mut roles = Vec::new();
8831 loop {
8832 roles.push(self.expect_ident_like()?);
8833 if matches!(self.peek(), Token::Comma) {
8834 self.advance();
8835 } else {
8836 break;
8837 }
8838 }
8839 Ok(roles)
8840 }
8841
8842 /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8843 /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8844 /// `CREATE POLICY`.
8845 fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8846 use crate::ast::PolicyCmd;
8847 let name = self.expect_ident_like()?;
8848 if !matches!(self.peek(), Token::On) {
8849 return Err(self.err(alloc::format!(
8850 "expected ON after CREATE POLICY name, got {:?}",
8851 self.peek()
8852 )));
8853 }
8854 self.advance();
8855 let table = self.expect_ident_like()?;
8856
8857 let mut permissive = true;
8858 if matches!(self.peek(), Token::As) {
8859 self.advance();
8860 let w = self.expect_ident_like()?;
8861 permissive = if w.eq_ignore_ascii_case("permissive") {
8862 true
8863 } else if w.eq_ignore_ascii_case("restrictive") {
8864 false
8865 } else {
8866 return Err(self.err(alloc::format!(
8867 "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8868 )));
8869 };
8870 }
8871
8872 let mut cmd = PolicyCmd::All;
8873 if matches!(self.peek(), Token::For) {
8874 self.advance();
8875 cmd = self.parse_policy_cmd()?;
8876 }
8877
8878 let mut roles = Vec::new();
8879 if matches!(self.peek(), Token::To) {
8880 self.advance();
8881 roles = self.parse_policy_roles()?;
8882 }
8883
8884 let mut using = None;
8885 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8886 {
8887 self.advance();
8888 using = Some(self.parse_paren_expr("USING")?);
8889 }
8890
8891 let mut with_check = None;
8892 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8893 {
8894 self.advance();
8895 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8896 {
8897 return Err(self.err(alloc::format!(
8898 "expected CHECK after WITH, got {:?}",
8899 self.peek()
8900 )));
8901 }
8902 self.advance();
8903 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8904 }
8905
8906 // Clause-per-command matrix (PG wording).
8907 match cmd {
8908 PolicyCmd::Insert => {
8909 if using.is_some() {
8910 return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8911 }
8912 }
8913 PolicyCmd::Select | PolicyCmd::Delete => {
8914 if with_check.is_some() {
8915 return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8916 }
8917 }
8918 PolicyCmd::Update | PolicyCmd::All => {}
8919 }
8920
8921 Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8922 name,
8923 table,
8924 permissive,
8925 cmd,
8926 roles,
8927 using,
8928 with_check,
8929 }))
8930 }
8931
8932 /// v7.39 (RLS) — the command word after `FOR`.
8933 fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8934 use crate::ast::PolicyCmd;
8935 match self.peek().clone() {
8936 Token::All => {
8937 self.advance();
8938 Ok(PolicyCmd::All)
8939 }
8940 Token::Select => {
8941 self.advance();
8942 Ok(PolicyCmd::Select)
8943 }
8944 Token::Insert => {
8945 self.advance();
8946 Ok(PolicyCmd::Insert)
8947 }
8948 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8949 self.advance();
8950 Ok(PolicyCmd::Update)
8951 }
8952 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8953 self.advance();
8954 Ok(PolicyCmd::Delete)
8955 }
8956 other => Err(self.err(alloc::format!(
8957 "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8958 ))),
8959 }
8960 }
8961
8962 /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8963 /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8964 fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8965 let name = self.expect_ident_like()?;
8966 if !matches!(self.peek(), Token::On) {
8967 return Err(self.err(alloc::format!(
8968 "expected ON after ALTER POLICY name, got {:?}",
8969 self.peek()
8970 )));
8971 }
8972 self.advance();
8973 let table = self.expect_ident_like()?;
8974
8975 // RENAME TO new
8976 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8977 {
8978 self.advance();
8979 if !matches!(self.peek(), Token::To) {
8980 return Err(self.err(alloc::format!(
8981 "expected TO after RENAME, got {:?}",
8982 self.peek()
8983 )));
8984 }
8985 self.advance();
8986 let new = self.expect_ident_like()?;
8987 return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8988 name,
8989 table,
8990 rename_to: Some(new),
8991 roles: None,
8992 using: None,
8993 with_check: None,
8994 }));
8995 }
8996
8997 let mut roles = None;
8998 if matches!(self.peek(), Token::To) {
8999 self.advance();
9000 roles = Some(self.parse_policy_roles()?);
9001 }
9002 let mut using = None;
9003 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
9004 {
9005 self.advance();
9006 using = Some(self.parse_paren_expr("USING")?);
9007 }
9008 let mut with_check = None;
9009 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
9010 {
9011 self.advance();
9012 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
9013 {
9014 return Err(self.err(alloc::format!(
9015 "expected CHECK after WITH, got {:?}",
9016 self.peek()
9017 )));
9018 }
9019 self.advance();
9020 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
9021 }
9022 Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
9023 name,
9024 table,
9025 rename_to: None,
9026 roles,
9027 using,
9028 with_check,
9029 }))
9030 }
9031
9032 /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
9033 /// `DROP POLICY`.
9034 fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
9035 let if_exists = self.consume_if_exists();
9036 let name = self.expect_ident_like()?;
9037 if !matches!(self.peek(), Token::On) {
9038 return Err(self.err(alloc::format!(
9039 "expected ON after DROP POLICY name, got {:?}",
9040 self.peek()
9041 )));
9042 }
9043 self.advance();
9044 let table = self.expect_ident_like()?;
9045 Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
9046 name,
9047 table,
9048 if_exists,
9049 }))
9050 }
9051}
9052fn wrap_from_leaves(
9053 e: &mut Expr,
9054 names: &[String],
9055 make: &dyn Fn(Expr) -> Expr,
9056 refs: &dyn Fn(&Expr) -> bool,
9057) {
9058 if let Expr::Column(c) = e {
9059 if c.qualifier
9060 .as_deref()
9061 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
9062 {
9063 let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
9064 *e = make(taken);
9065 }
9066 return;
9067 }
9068 match e {
9069 Expr::Binary { lhs, rhs, .. } => {
9070 wrap_from_leaves(lhs, names, make, refs);
9071 wrap_from_leaves(rhs, names, make, refs);
9072 }
9073 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
9074 wrap_from_leaves(expr, names, make, refs)
9075 }
9076 Expr::FunctionCall { args, .. } => {
9077 for a in args.iter_mut() {
9078 wrap_from_leaves(a, names, make, refs);
9079 }
9080 }
9081 Expr::Case {
9082 operand,
9083 branches,
9084 else_branch,
9085 } => {
9086 if let Some(o) = operand.as_deref_mut() {
9087 wrap_from_leaves(o, names, make, refs);
9088 }
9089 for (w, t) in branches.iter_mut() {
9090 wrap_from_leaves(w, names, make, refs);
9091 wrap_from_leaves(t, names, make, refs);
9092 }
9093 if let Some(el) = else_branch.as_deref_mut() {
9094 wrap_from_leaves(el, names, make, refs);
9095 }
9096 }
9097 // Compound variants the walk doesn't decompose: keep the
9098 // pre-D.30 behavior — wrap the whole sub-expr if it touches
9099 // a source table, so nothing regresses.
9100 other => {
9101 if refs(other) {
9102 let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
9103 *other = make(taken);
9104 }
9105 }
9106 }
9107}
9108
9109/// v7.39 (round 241) — does this expression reference any of the FROM /
9110/// USING table names (shared by the UPDATE…FROM and DELETE…USING
9111/// lowerings)?
9112fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
9113 match e {
9114 Expr::Column(c) => c
9115 .qualifier
9116 .as_deref()
9117 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9118 Expr::Binary { lhs, rhs, .. } => {
9119 expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
9120 }
9121 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
9122 Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
9123 Expr::Case {
9124 operand,
9125 branches,
9126 else_branch,
9127 } => {
9128 operand
9129 .as_deref()
9130 .is_some_and(|o| expr_refs_tables(o, names))
9131 || branches
9132 .iter()
9133 .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
9134 || else_branch
9135 .as_deref()
9136 .is_some_and(|el| expr_refs_tables(el, names))
9137 }
9138 _ => false,
9139 }
9140}
9141
9142impl Parser {
9143 /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
9144 /// Caller already consumed the leading `UPDATE` ident.
9145 /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
9146 /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
9147 /// after the target name has been read. `JOIN` is a reserved token;
9148 /// the qualifiers are bare idents.
9149 fn peek_is_update_join_start(&self) -> bool {
9150 match self.peek() {
9151 // JOIN and its qualifiers are reserved lexer tokens (the grammar
9152 // dedicates arms to `LEFT [OUTER] JOIN` and friends).
9153 Token::Join
9154 | Token::Inner
9155 | Token::Left
9156 | Token::Right
9157 | Token::Cross
9158 | Token::Full => true,
9159 // NATURAL / STRAIGHT_JOIN arrive as bare idents.
9160 Token::Ident(s) | Token::QuotedIdent(s) => {
9161 matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
9162 }
9163 _ => false,
9164 }
9165 }
9166
9167 /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
9168 /// USER-variable assignment. Its own per-session namespace, an arbitrary
9169 /// expression on the right, and `:=` as a second spelling of `=`.
9170 ///
9171 /// Out-of-line (`inline(never)`): the statement-parse frame it is called
9172 /// from sits on the nesting recursion chain (a CTE body, a subquery),
9173 /// and holding this loop's `Vec` + `String` locals there overflowed the
9174 /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
9175 #[inline(never)]
9176 fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
9177 let mut assigns: Vec<(String, Expr)> = Vec::new();
9178 let mut settings: Vec<(String, Expr)> = Vec::new();
9179 loop {
9180 // v7.39 (round 554) — a plain NAME here is a session
9181 // setting, not a user variable. mysqldump writes the two in
9182 // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
9183 // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
9184 // changes it — and this refused the mixture outright, so no
9185 // dump could be restored past its preamble.
9186 if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
9187 self.advance();
9188 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9189 return Err(self.err(alloc::format!(
9190 "expected `=` after {name}, got {:?}",
9191 self.peek()
9192 )));
9193 }
9194 self.advance();
9195 let value = self.parse_expr(0)?;
9196 settings.push((name.to_ascii_lowercase(), value));
9197 if matches!(self.peek(), Token::Comma) {
9198 self.advance();
9199 continue;
9200 }
9201 break;
9202 }
9203 let Token::SessionVar(raw) = self.peek().clone() else {
9204 return Err(self.err(alloc::format!(
9205 "expected a user variable after SET, got {:?}",
9206 self.peek()
9207 )));
9208 };
9209 if raw.starts_with("@@") {
9210 return Err(self.err(alloc::string::String::from(
9211 "cannot mix `@@` settings with `@` user variables in one SET",
9212 )));
9213 }
9214 self.advance();
9215 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9216 return Err(self.err(alloc::format!(
9217 "expected `=` or `:=` after {raw}, got {:?}",
9218 self.peek()
9219 )));
9220 }
9221 self.advance();
9222 let value = self.parse_expr(0)?;
9223 assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
9224 if matches!(self.peek(), Token::Comma) {
9225 self.advance();
9226 continue;
9227 }
9228 break;
9229 }
9230 Ok(Statement::SetUserVars(assigns, settings))
9231 }
9232
9233 fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
9234 // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
9235 // NAMED `only` until now, which failed on `relation "only" does
9236 // not exist`. The lookahead is what keeps a table actually
9237 // called `only` working: the keyword is only a keyword when a
9238 // TABLE NAME follows it — and `SET` arrives as an identifier
9239 // here, so `UPDATE only SET a = 2` would otherwise take `SET`
9240 // for the table and die on the `=`. Measured by the pin.
9241 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9242 if s.eq_ignore_ascii_case("only"))
9243 && matches!(
9244 self.tokens.get(self.pos + 1),
9245 Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
9246 );
9247 if only {
9248 self.advance();
9249 }
9250 let table = self.expect_ident_like()?;
9251 // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
9252 // bare spelling; a bare identifier that is the SET keyword itself
9253 // is the clause, not an alias.
9254 // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
9255 // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
9256 // multi-table form, and swallowing `LEFT` as `a`'s alias made the
9257 // following JOIN a syntax error.
9258 let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
9259 let alias = if matches!(self.peek(), Token::As) {
9260 self.advance();
9261 Some(self.expect_ident_like()?)
9262 } else {
9263 match self.peek() {
9264 Token::Ident(s) | Token::QuotedIdent(s)
9265 if !s.eq_ignore_ascii_case("set") && !starts_join =>
9266 {
9267 let a = s.clone();
9268 self.advance();
9269 Some(a)
9270 }
9271 _ => None,
9272 }
9273 };
9274 // v7.39 (round 420) — MySQL's multi-table UPDATE:
9275 // UPDATE a, b SET a.v = b.v WHERE a.id = b.id
9276 // UPDATE a JOIN b ON a.id = b.id SET a.v = b.v + 1
9277 // UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
9278 // The FIRST table is the mutation target and the rest are sources —
9279 // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
9280 // SPG already lowers onto correlated subqueries. So rewind, let
9281 // `parse_from_clause` read the whole list (it handles aliases, comma
9282 // lists, and every JOIN form), then peel the target off the front.
9283 let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
9284 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9285 {
9286 // NOTE: `advance()` destroys the tokens it returns
9287 // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
9288 // is NOT possible — the tail is read forward, once, through the
9289 // same grammar `parse_from_clause` uses after its primary.
9290 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9291 let mut joins = self.parse_from_joins(&target_qual)?;
9292 if joins.is_empty() {
9293 return Err(self.err(alloc::string::String::from(
9294 "multi-table UPDATE needs at least one source table",
9295 )));
9296 }
9297 let head = joins.remove(0);
9298 // A LEFT join keeps every target row (the unmatched ones see NULL
9299 // on the source side), so it must NOT get the EXISTS row filter
9300 // the inner / comma forms use.
9301 let outer = matches!(head.kind, crate::ast::JoinKind::Left);
9302 let src = FromClause {
9303 primary: head.table,
9304 joins,
9305 };
9306 (Some(src), head.on, outer)
9307 } else {
9308 (None, None, false)
9309 };
9310 self.expect_keyword_ident("set")?;
9311 let mut assignments = Vec::new();
9312 loop {
9313 // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
9314 // …)` — the parenthesized multi-assignment. Expressions
9315 // assign positionally; a subquery RHS clones per column
9316 // keeping only the Nth projection item.
9317 if matches!(self.peek(), Token::LParen) {
9318 self.advance();
9319 let mut cols = alloc::vec![self.expect_ident_like()?];
9320 while matches!(self.peek(), Token::Comma) {
9321 self.advance();
9322 cols.push(self.expect_ident_like()?);
9323 }
9324 if !matches!(self.peek(), Token::RParen) {
9325 return Err(self.err(format!(
9326 "expected ')' after SET column list, got {:?}",
9327 self.peek()
9328 )));
9329 }
9330 self.advance();
9331 if !matches!(self.peek(), Token::Eq) {
9332 return Err(self.err(format!(
9333 "expected `=` after SET column list, got {:?}",
9334 self.peek()
9335 )));
9336 }
9337 self.advance();
9338 if !matches!(self.peek(), Token::LParen) {
9339 return Err(self.err(format!(
9340 "expected '(' after SET (…) =, got {:?}",
9341 self.peek()
9342 )));
9343 }
9344 self.advance();
9345 if matches!(self.peek(), Token::Select) {
9346 let inner = match self.parse_select_stmt()? {
9347 Statement::Select(s) => s,
9348 other => {
9349 return Err(self.err(alloc::format!(
9350 "expected SELECT in SET (…) = (SELECT …), got {other:?}"
9351 )));
9352 }
9353 };
9354 if !matches!(self.peek(), Token::RParen) {
9355 return Err(self.err(format!(
9356 "expected ')' after SET subquery, got {:?}",
9357 self.peek()
9358 )));
9359 }
9360 self.advance();
9361 if inner.items.len() != cols.len() {
9362 return Err(self.err(alloc::format!(
9363 "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9364 cols.len(),
9365 inner.items.len()
9366 )));
9367 }
9368 for (i, col) in cols.into_iter().enumerate() {
9369 let mut sub = inner.clone();
9370 sub.items = alloc::vec![sub.items[i].clone()];
9371 assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9372 }
9373 } else {
9374 let mut exprs = alloc::vec![self.parse_expr(0)?];
9375 while matches!(self.peek(), Token::Comma) {
9376 self.advance();
9377 exprs.push(self.parse_expr(0)?);
9378 }
9379 if !matches!(self.peek(), Token::RParen) {
9380 return Err(self.err(format!(
9381 "expected ')' after SET row values, got {:?}",
9382 self.peek()
9383 )));
9384 }
9385 self.advance();
9386 if exprs.len() != cols.len() {
9387 return Err(self.err(alloc::format!(
9388 "SET (…) = (…) arity mismatch: {} columns, {} values",
9389 cols.len(),
9390 exprs.len()
9391 )));
9392 }
9393 for (col, e) in cols.into_iter().zip(exprs) {
9394 assignments.push((col, e));
9395 }
9396 }
9397 if matches!(self.peek(), Token::Comma) {
9398 self.advance();
9399 continue;
9400 }
9401 break;
9402 }
9403 // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9404 // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9405 // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9406 // `public.` dump qualifiers), so the qualifier has to be read off
9407 // the token stream first — otherwise `SET b.v = 888` would write
9408 // to the TARGET table's `v` while naming a source table, a
9409 // silent-wrong. A qualifier naming a SOURCE table means a
9410 // multi-TARGET update — mutating two tables in one statement —
9411 // which SPG does not model, so it is refused loudly.
9412 let set_qual: Option<String> = if mysql_from.is_some()
9413 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9414 {
9415 match self.peek() {
9416 Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9417 _ => None,
9418 }
9419 } else {
9420 None
9421 };
9422 let col = self.expect_ident_like()?;
9423 if let Some(q) = set_qual {
9424 let names_target = q.eq_ignore_ascii_case(&table)
9425 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9426 if !names_target {
9427 return Err(self.err(alloc::format!(
9428 "multi-table UPDATE can only assign to its first table \
9429 ({table}); `{q}.{col}` targets another table"
9430 )));
9431 }
9432 }
9433 // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9434 // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9435 // `__column_default` marker lowering just below). PG assigns to the
9436 // i-th (1-based) element, NULL-padding when i exceeds the length.
9437 if matches!(self.peek(), Token::LBracket) {
9438 self.advance();
9439 let index = self.parse_expr(0)?;
9440 // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9441 // (and the open `arr[lo:]`), lowered to
9442 // `__array_assign_slice`. Only the single-subscript form
9443 // parsed before, so a slice assignment was a syntax error.
9444 let mut slice_hi: Option<Option<Expr>> = None;
9445 if matches!(self.peek(), Token::Colon) {
9446 self.advance();
9447 slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9448 None
9449 } else {
9450 Some(self.parse_expr(0)?)
9451 });
9452 }
9453 if !matches!(self.peek(), Token::RBracket) {
9454 return Err(self.err(format!(
9455 "expected `]` after array subscript in UPDATE SET, got {:?}",
9456 self.peek()
9457 )));
9458 }
9459 self.advance();
9460 if !matches!(self.peek(), Token::Eq) {
9461 return Err(self.err(format!(
9462 "expected `=` after array subscript in UPDATE SET, got {:?}",
9463 self.peek()
9464 )));
9465 }
9466 self.advance();
9467 let value = self.parse_expr(0)?;
9468 // PG merges several subscript writes to the same column into one
9469 // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9470 // assignment to `col` rather than each overwriting the original.
9471 let existing = assignments.iter().position(|(c, _)| c == &col);
9472 let base = match existing {
9473 Some(i) => assignments[i].1.clone(),
9474 None => Expr::Column(ColumnName {
9475 qualifier: None,
9476 name: col.clone(),
9477 }),
9478 };
9479 let assigned = match slice_hi {
9480 None => Expr::FunctionCall {
9481 name: "__array_assign".to_string(),
9482 args: alloc::vec![base, index, value],
9483 },
9484 Some(hi) => Expr::FunctionCall {
9485 name: "__array_assign_slice".to_string(),
9486 args: alloc::vec![
9487 base,
9488 index,
9489 hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9490 value,
9491 ],
9492 },
9493 };
9494 match existing {
9495 Some(i) => assignments[i].1 = assigned,
9496 None => assignments.push((col, assigned)),
9497 }
9498 if matches!(self.peek(), Token::Comma) {
9499 self.advance();
9500 continue;
9501 }
9502 break;
9503 }
9504 if !matches!(self.peek(), Token::Eq) {
9505 return Err(self.err(format!(
9506 "expected `=` after column name in UPDATE SET, got {:?}",
9507 self.peek()
9508 )));
9509 }
9510 self.advance();
9511 // `SET col = DEFAULT` — the column's declared default;
9512 // rides out as a marker call the update executor
9513 // resolves against the schema.
9514 let value = if matches!(self.peek(), Token::Default) {
9515 self.advance();
9516 Expr::FunctionCall {
9517 name: "__column_default".to_string(),
9518 args: Vec::new(),
9519 }
9520 } else {
9521 self.parse_expr(0)?
9522 };
9523 assignments.push((col, value));
9524 if matches!(self.peek(), Token::Comma) {
9525 self.advance();
9526 continue;
9527 }
9528 break;
9529 }
9530 // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9531 // update. Lowers onto the correlated-subquery machinery:
9532 // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9533 // and each assignment that references a FROM-list table
9534 // wraps into a correlated scalar subquery
9535 // (SELECT expr FROM src WHERE cond). Equivalent for the
9536 // unique-join shape (the overwhelmingly common one); a
9537 // multi-match, which PG resolves by arbitrary pick,
9538 // surfaces as a scalar-subquery cardinality error instead
9539 // of a silent arbitrary result.
9540 // v7.39 (round 420) — the MySQL multi-table form supplies the source
9541 // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9542 // the SAME lowering below. Both spellings together is not legal in
9543 // either dialect.
9544 let from_clause = if let Some(fc) = mysql_from {
9545 if matches!(self.peek(), Token::From) {
9546 return Err(self.err(alloc::string::String::from(
9547 "multi-table UPDATE already names its sources; drop the FROM clause",
9548 )));
9549 }
9550 Some(fc)
9551 } else if matches!(self.peek(), Token::From) {
9552 self.advance();
9553 Some(self.parse_from_clause()?)
9554 } else {
9555 None
9556 };
9557 let where_ = if matches!(self.peek(), Token::Where) {
9558 self.advance();
9559 Some(self.parse_expr(0)?)
9560 } else {
9561 None
9562 };
9563 // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9564 // and the TARGET-row filter are NOT the same predicate once a LEFT
9565 // join is involved:
9566 // * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9567 // one conjunction, and the whole thing filters target rows via
9568 // EXISTS.
9569 // * LEFT join: only the ON predicate belongs inside the source
9570 // subquery. The WHERE still filters TARGET rows (with source
9571 // columns read through the correlated subquery, which yields NULL
9572 // for an unmatched row — exactly LEFT-join semantics).
9573 // Round 420 folded ON into WHERE unconditionally and then dropped the
9574 // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9575 // WHERE a.id > 1` updated EVERY row.
9576 let sub_where = match (mysql_on.clone(), where_.clone()) {
9577 _ if mysql_outer => mysql_on.clone(),
9578 (Some(on), Some(w)) => Some(Expr::Binary {
9579 lhs: Box::new(on),
9580 op: crate::ast::BinOp::And,
9581 rhs: Box::new(w),
9582 }),
9583 (Some(on), None) => Some(on),
9584 (None, w) => w,
9585 };
9586 // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9587 // has no such clause on UPDATE, so this is accepted only under the
9588 // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9589 let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9590 let mut returning = self.parse_optional_returning()?;
9591 // v7.39 (round 533) — kept for the engine, which can resolve the
9592 // UNQUALIFIED leaves this lowering has to leave alone.
9593 let from_sources = from_clause.as_ref().map(|fc| {
9594 alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9595 from: fc.clone(),
9596 sub_where: sub_where.clone(),
9597 })
9598 });
9599 let (assignments, where_) = if let Some(fc) = from_clause {
9600 let names: Vec<String> = core::iter::once(&fc.primary)
9601 .chain(fc.joins.iter().map(|j| &j.table))
9602 .flat_map(|t| {
9603 t.alias
9604 .clone()
9605 .into_iter()
9606 .chain(core::iter::once(t.name.clone()))
9607 })
9608 .collect();
9609 let refs_list = |e: &Expr| -> bool {
9610 fn walk(e: &Expr, names: &[String]) -> bool {
9611 match e {
9612 Expr::Column(c) => c
9613 .qualifier
9614 .as_deref()
9615 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9616 Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9617 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9618 Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9619 Expr::Case {
9620 operand,
9621 branches,
9622 else_branch,
9623 } => {
9624 operand.as_deref().is_some_and(|o| walk(o, names))
9625 || branches
9626 .iter()
9627 .any(|(w, t)| walk(w, names) || walk(t, names))
9628 || else_branch.as_deref().is_some_and(|el| walk(el, names))
9629 }
9630 _ => false,
9631 }
9632 }
9633 walk(e, &names)
9634 };
9635 let sub_select = |items: Vec<SelectItem>| SelectStatement {
9636 locking: None,
9637 ctes: Vec::new(),
9638 distinct: false,
9639 distinct_on: Vec::new(),
9640 items,
9641 from: Some(fc.clone()),
9642 where_: sub_where.clone(),
9643 group_by: None,
9644 group_by_all: false,
9645 having: None,
9646 unions: Vec::new(),
9647 order_by: Vec::new(),
9648 limit: None,
9649 offset: None,
9650 limit_with_ties: false,
9651 window_check_exprs: Vec::new(),
9652 };
9653 // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9654 // assignment RHS with a correlated scalar subquery, instead of
9655 // wrapping the whole RHS. Wrapping the whole expr moved a target-
9656 // column reference (`SET v = v + u.bonus`, where `v` is the target
9657 // table's column) inside a subquery whose FROM only has the source
9658 // table, so the unqualified `v` resolved against the source and
9659 // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9660 // context — where they belong — fixes it; only the source columns
9661 // (`u.bonus`) become subqueries. A whole-expr fallback covers
9662 // compound variants the leaf-walk doesn't decompose.
9663 let make_subq = |inner: Expr| {
9664 Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9665 expr: inner,
9666 alias: None,
9667 }])))
9668 };
9669 let assignments = assignments
9670 .into_iter()
9671 .map(|(col, mut expr)| {
9672 wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9673 (col, expr)
9674 })
9675 .collect();
9676 let exists = Expr::Exists {
9677 subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9678 expr: Expr::Literal(Literal::Integer(1)),
9679 alias: None,
9680 }])),
9681 negated: false,
9682 };
9683 // v7.39 (round 241) — RETURNING may reference the FROM-list
9684 // tables too (`RETURNING emp.id, dept.name`); the same
9685 // leaf-to-correlated-subquery lowering the assignments get.
9686 // Without it the qualifier died at eval with "unknown table
9687 // qualifier". (RETURNING was parsed before this block — the
9688 // lowering is a pure AST transformation.)
9689 if let Some(items) = returning.as_mut() {
9690 for item in items.iter_mut() {
9691 if let SelectItem::Expr { expr, .. } = item {
9692 wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9693 }
9694 }
9695 }
9696 // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9697 // EVERY matching target row: it gets no EXISTS filter, but the
9698 // caller's WHERE still applies, with source columns read through
9699 // the correlated subquery (NULL when unmatched — LEFT-join
9700 // semantics). `sub_where` above already excluded the WHERE from
9701 // the source subquery for this case.
9702 if mysql_outer {
9703 let mut outer = where_;
9704 if let Some(w) = outer.as_mut() {
9705 wrap_from_leaves(w, &names, &make_subq, &refs_list);
9706 }
9707 (assignments, outer)
9708 } else {
9709 (assignments, Some(exists))
9710 }
9711 } else {
9712 (assignments, where_)
9713 };
9714 Ok(Statement::Update(crate::ast::UpdateStatement {
9715 ctes: Vec::new(),
9716 table,
9717 only,
9718 alias,
9719 assignments,
9720 from_sources,
9721 where_,
9722 order_limit: update_order_limit,
9723 returning,
9724 }))
9725 }
9726
9727 /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9728 /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9729 /// clause and its meaning are identical, so both call this rather than
9730 /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9731 /// legal. PG has no such clause on either statement, so it is read only
9732 /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9733 /// errors.
9734 ///
9735 /// `#[inline(never)]`: its locals would otherwise land on the statement-
9736 /// parsing recursion frame, which is what tipped the 512 KiB nesting
9737 /// stack in round 430.
9738 #[inline(never)]
9739 fn parse_mysql_dml_order_limit(
9740 &mut self,
9741 what: &str,
9742 ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9743 if !self.mysql_dialect {
9744 return Ok(None);
9745 }
9746 let order_by = self.parse_order_by_keys()?;
9747 let limit = if matches!(self.peek(), Token::Limit) {
9748 self.advance();
9749 let tok = self.advance();
9750 let Token::Integer(n) = tok else {
9751 return Err(self.err(alloc::format!(
9752 "expected integer after {what} LIMIT, got {tok:?}"
9753 )));
9754 };
9755 // MySQL rejects the `LIMIT offset, count` form here — only a
9756 // single row count is legal on a DML statement.
9757 if matches!(self.peek(), Token::Comma) {
9758 return Err(self.err(alloc::format!(
9759 "{what} LIMIT takes a row count, not an offset"
9760 )));
9761 }
9762 let n = u32::try_from(n)
9763 .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9764 Some(n)
9765 } else {
9766 None
9767 };
9768 if order_by.is_empty() && limit.is_none() {
9769 return Ok(None);
9770 }
9771 Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9772 order_by,
9773 limit,
9774 })))
9775 }
9776
9777 /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9778 /// the leading `DELETE` ident.
9779 fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9780 // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9781 // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9782 // USING a, b WHERE …` — the third MySQL spelling — needs no special
9783 // parse here; it reaches the existing USING path with the target
9784 // repeated in the list, which the source-list peel below handles.)
9785 // More than one name is a multi-TARGET delete, which SPG does not
9786 // model; it is refused rather than half-applied.
9787 let mysql_pre_target: Option<String> =
9788 if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9789 let first = self.expect_ident_like()?;
9790 if matches!(self.peek(), Token::Comma) {
9791 return Err(self.err(alloc::format!(
9792 "multi-table DELETE can only delete from one table; \
9793 `DELETE {first}, …` names several"
9794 )));
9795 }
9796 Some(first)
9797 } else {
9798 None
9799 };
9800 if !matches!(self.peek(), Token::From) {
9801 return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9802 }
9803 self.advance();
9804 // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9805 // lookahead as the UPDATE spelling.
9806 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9807 if s.eq_ignore_ascii_case("only"))
9808 && matches!(
9809 self.tokens.get(self.pos + 1),
9810 Some(Token::Ident(_) | Token::QuotedIdent(_))
9811 );
9812 if only {
9813 self.advance();
9814 }
9815 let table = self.expect_ident_like()?;
9816 // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9817 // spelling must not swallow the clause keywords that can follow
9818 // the target.
9819 let alias = if matches!(self.peek(), Token::As) {
9820 self.advance();
9821 Some(self.expect_ident_like()?)
9822 } else {
9823 match self.peek() {
9824 Token::Ident(s) | Token::QuotedIdent(s)
9825 if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9826 {
9827 let a = s.clone();
9828 self.advance();
9829 Some(a)
9830 }
9831 _ => None,
9832 }
9833 };
9834 // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9835 // through the SAME join grammar the FROM clause uses (see the
9836 // `advance()`-destroys-tokens note on `parse_from_joins`).
9837 let mut mysql_on: Option<Expr> = None;
9838 let mut mysql_outer = false;
9839 let mysql_using = if mysql_pre_target.is_some()
9840 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9841 {
9842 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9843 let mut joins = self.parse_from_joins(&target_qual)?;
9844 if joins.is_empty() {
9845 return Err(self.err(alloc::string::String::from(
9846 "multi-table DELETE needs at least one source table",
9847 )));
9848 }
9849 let head = joins.remove(0);
9850 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9851 mysql_on = head.on;
9852 Some(FromClause {
9853 primary: head.table,
9854 joins,
9855 })
9856 } else {
9857 None
9858 };
9859 // The pre-FROM target must be the table the FROM names (or its
9860 // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9861 // is not the scan target.
9862 if let Some(t) = &mysql_pre_target {
9863 let names_target = t.eq_ignore_ascii_case(&table)
9864 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9865 if !names_target {
9866 return Err(self.err(alloc::format!(
9867 "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9868 )));
9869 }
9870 }
9871 // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9872 // delete. Same lowering as UPDATE … FROM: the WHERE
9873 // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9874 // target row by the correlated machinery.
9875 let using_clause = if let Some(fc) = mysql_using {
9876 Some(fc)
9877 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9878 self.advance();
9879 let mut fc = self.parse_from_clause()?;
9880 // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9881 // repeats the TARGET as the first USING entry (PG's spelling
9882 // lists only the extra sources). Peel it so the source subquery
9883 // does not re-scan — and shadow — the target table.
9884 let primary_is_target =
9885 fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9886 if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9887 let head = fc.joins.remove(0);
9888 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9889 mysql_on = head.on;
9890 fc = FromClause {
9891 primary: head.table,
9892 joins: fc.joins,
9893 };
9894 }
9895 Some(fc)
9896 } else {
9897 None
9898 };
9899 let where_ = if matches!(self.peek(), Token::Where) {
9900 self.advance();
9901 Some(self.parse_expr(0)?)
9902 } else {
9903 None
9904 };
9905 // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9906 // read before RETURNING (MariaDB's own extension trails the LIMIT).
9907 let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9908 let mut returning = self.parse_optional_returning()?;
9909 let where_ = if let Some(fc) = using_clause {
9910 // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9911 // a USING-table reference in RETURNING becomes a correlated
9912 // scalar subquery over the USING list.
9913 let names: Vec<String> = core::iter::once(&fc.primary)
9914 .chain(fc.joins.iter().map(|j| &j.table))
9915 .flat_map(|t| {
9916 t.alias
9917 .clone()
9918 .into_iter()
9919 .chain(core::iter::once(t.name.clone()))
9920 })
9921 .collect();
9922 // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9923 // join filters the SOURCE subquery on the ON predicate alone and
9924 // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9925 // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9926 // rows); every other form folds ON and WHERE into one EXISTS.
9927 let sub_where = match (mysql_on.clone(), where_.clone()) {
9928 _ if mysql_outer => mysql_on.clone(),
9929 (Some(on), Some(w)) => Some(Expr::Binary {
9930 lhs: Box::new(on),
9931 op: crate::ast::BinOp::And,
9932 rhs: Box::new(w),
9933 }),
9934 (Some(on), None) => Some(on),
9935 (None, w) => w,
9936 };
9937 let exists_where = sub_where.clone();
9938 let sub_fc = fc.clone();
9939 let make_subq = move |leaf: Expr| -> Expr {
9940 Expr::ScalarSubquery(Box::new(SelectStatement {
9941 locking: None,
9942 ctes: Vec::new(),
9943 distinct: false,
9944 distinct_on: Vec::new(),
9945 items: alloc::vec![SelectItem::Expr {
9946 expr: leaf,
9947 alias: None,
9948 }],
9949 from: Some(sub_fc.clone()),
9950 where_: sub_where.clone(),
9951 group_by: None,
9952 group_by_all: false,
9953 having: None,
9954 unions: Vec::new(),
9955 order_by: Vec::new(),
9956 limit: None,
9957 offset: None,
9958 limit_with_ties: false,
9959 window_check_exprs: Vec::new(),
9960 }))
9961 };
9962 let refs = |e: &Expr| expr_refs_tables(e, &names);
9963 if let Some(items) = returning.as_mut() {
9964 for item in items.iter_mut() {
9965 if let SelectItem::Expr { expr, .. } = item {
9966 wrap_from_leaves(expr, &names, &make_subq, &refs);
9967 }
9968 }
9969 }
9970 // A LEFT join deletes the target rows the WHERE selects, reading
9971 // source columns through the correlated subquery (NULL when
9972 // unmatched); no EXISTS row filter.
9973 if mysql_outer {
9974 let mut outer = where_;
9975 if let Some(w) = outer.as_mut() {
9976 wrap_from_leaves(w, &names, &make_subq, &refs);
9977 }
9978 outer
9979 } else {
9980 Some(Expr::Exists {
9981 subquery: Box::new(SelectStatement {
9982 locking: None,
9983 ctes: Vec::new(),
9984 distinct: false,
9985 distinct_on: Vec::new(),
9986 items: alloc::vec![SelectItem::Expr {
9987 expr: Expr::Literal(Literal::Integer(1)),
9988 alias: None,
9989 }],
9990 from: Some(fc),
9991 where_: exists_where,
9992 group_by: None,
9993 group_by_all: false,
9994 having: None,
9995 unions: Vec::new(),
9996 order_by: Vec::new(),
9997 limit: None,
9998 offset: None,
9999 limit_with_ties: false,
10000 window_check_exprs: Vec::new(),
10001 }),
10002 negated: false,
10003 })
10004 }
10005 } else {
10006 where_
10007 };
10008 Ok(Statement::Delete(crate::ast::DeleteStatement {
10009 ctes: Vec::new(),
10010 table,
10011 only,
10012 alias,
10013 where_,
10014 order_limit: delete_order_limit,
10015 returning,
10016 }))
10017 }
10018
10019 /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
10020 /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
10021 /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
10022 /// keyword. v7.17 surface:
10023 /// * source: table reference (subquery source is a follow-up)
10024 /// * actions: UPDATE SET / DELETE / DO NOTHING (matched);
10025 /// INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
10026 /// * AND-conditioned WHEN clauses; clauses tried in declaration
10027 /// order
10028 fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
10029 // INTO
10030 let is_into_kw = matches!(self.peek(), Token::Into)
10031 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
10032 if !is_into_kw {
10033 return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
10034 }
10035 self.advance();
10036 let target = self.expect_ident_like()?;
10037 // Optional alias — bare ident before USING.
10038 let target_alias = match self.peek() {
10039 Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
10040 Some(self.expect_ident_like()?)
10041 }
10042 _ => None,
10043 };
10044 // USING
10045 let is_using_kw = matches!(
10046 self.peek(),
10047 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
10048 );
10049 if !is_using_kw {
10050 return Err(self.err(format!(
10051 "expected USING after MERGE INTO target, got {:?}",
10052 self.peek()
10053 )));
10054 }
10055 self.advance();
10056 // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
10057 // <table> [alias]`. PG requires an alias after a subquery source.
10058 let (source, source_select) = if matches!(self.peek(), Token::LParen) {
10059 self.advance(); // (
10060 // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
10061 // constant-SELECT lowering the derived-table parser uses
10062 // (PG deletes through this form; it was a parse error).
10063 let inner = if matches!(self.peek(), Token::Values) {
10064 self.advance(); // VALUES
10065 Statement::Select(self.parse_values_rows_body()?)
10066 } else {
10067 self.parse_select_stmt()?
10068 };
10069 match self.advance() {
10070 Token::RParen => {}
10071 other => {
10072 return Err(self.err(format!(
10073 "expected ')' after MERGE USING subquery, got {other:?}"
10074 )));
10075 }
10076 }
10077 let Statement::Select(sub) = inner else {
10078 return Err(self.err("MERGE USING subquery must be a SELECT".into()));
10079 };
10080 (String::new(), Some(Box::new(sub)))
10081 } else {
10082 (self.expect_ident_like()?, None)
10083 };
10084 let source_alias = match self.peek() {
10085 Token::Ident(s) | Token::QuotedIdent(s)
10086 if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
10087 {
10088 Some(self.expect_ident_like()?)
10089 }
10090 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
10091 self.advance(); // AS
10092 Some(self.expect_ident_like()?)
10093 }
10094 _ => None,
10095 };
10096 // v7.39 (round 768, F31-D5) — optional positional column-alias
10097 // list after the source alias (`s(id, v)`).
10098 let mut source_column_aliases: Vec<String> = Vec::new();
10099 if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
10100 self.advance();
10101 loop {
10102 source_column_aliases.push(self.expect_ident_like()?);
10103 match self.peek() {
10104 Token::Comma => {
10105 self.advance();
10106 }
10107 Token::RParen => {
10108 self.advance();
10109 break;
10110 }
10111 other => {
10112 return Err(self.err(format!(
10113 "expected ',' or ')' in MERGE source column list, got {other:?}"
10114 )));
10115 }
10116 }
10117 }
10118 }
10119 if source_select.is_some() && source_alias.is_none() {
10120 return Err(self.err("MERGE USING (subquery) requires an alias".into()));
10121 }
10122 // ON
10123 if !matches!(self.peek(), Token::On) {
10124 return Err(self.err(format!(
10125 "expected ON after MERGE … USING source, got {:?}",
10126 self.peek()
10127 )));
10128 }
10129 self.advance();
10130 let on = self.parse_expr(0)?;
10131 // One or more WHEN clauses.
10132 let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
10133 loop {
10134 let is_when_kw = matches!(
10135 self.peek(),
10136 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
10137 );
10138 if !is_when_kw {
10139 break;
10140 }
10141 self.advance(); // WHEN
10142 // [NOT] MATCHED
10143 let matched = if matches!(self.peek(), Token::Not) {
10144 self.advance();
10145 crate::ast::MergeMatched::NotMatched
10146 } else {
10147 crate::ast::MergeMatched::Matched
10148 };
10149 let is_matched_kw = matches!(
10150 self.peek(),
10151 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
10152 );
10153 if !is_matched_kw {
10154 return Err(self.err(format!(
10155 "expected MATCHED in WHEN clause, got {:?}",
10156 self.peek()
10157 )));
10158 }
10159 self.advance();
10160 // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
10161 // BY TARGET is the default (a synonym); BY SOURCE flips the clause
10162 // to fire for target rows no source row matches.
10163 let mut matched = matched;
10164 if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
10165 self.advance();
10166 match self.peek() {
10167 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
10168 self.advance();
10169 matched = crate::ast::MergeMatched::NotMatchedBySource;
10170 }
10171 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
10172 self.advance();
10173 }
10174 other => {
10175 return Err(self.err(format!(
10176 "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
10177 )));
10178 }
10179 }
10180 }
10181 // Optional AND <expr>
10182 let condition = if matches!(self.peek(), Token::And) {
10183 self.advance();
10184 Some(self.parse_expr(0)?)
10185 } else {
10186 None
10187 };
10188 // THEN
10189 let is_then_kw = matches!(
10190 self.peek(),
10191 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
10192 );
10193 if !is_then_kw {
10194 return Err(self.err(format!(
10195 "expected THEN in WHEN clause, got {:?}",
10196 self.peek()
10197 )));
10198 }
10199 self.advance();
10200 // Action: INSERT / UPDATE / DELETE / DO NOTHING
10201 let action = match self.peek().clone() {
10202 Token::Insert => {
10203 self.advance();
10204 // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
10205 // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
10206 // VALUES (…)` omits it and fills every column in declaration
10207 // order. PG accepts this; SPG used to require the list.
10208 let mut columns: Vec<String> = Vec::new();
10209 if matches!(self.peek(), Token::LParen) {
10210 self.advance();
10211 loop {
10212 columns.push(self.expect_ident_like()?);
10213 if matches!(self.peek(), Token::Comma) {
10214 self.advance();
10215 continue;
10216 }
10217 break;
10218 }
10219 if !matches!(self.peek(), Token::RParen) {
10220 return Err(self.err(format!(
10221 "expected ')' after INSERT column list, got {:?}",
10222 self.peek()
10223 )));
10224 }
10225 self.advance();
10226 }
10227 // VALUES (...)
10228 if !matches!(self.peek(), Token::Values) {
10229 return Err(self.err(format!(
10230 "expected VALUES in MERGE INSERT, got {:?}",
10231 self.peek()
10232 )));
10233 }
10234 self.advance();
10235 if !matches!(self.peek(), Token::LParen) {
10236 return Err(self.err(format!(
10237 "expected '(' after VALUES in MERGE INSERT, got {:?}",
10238 self.peek()
10239 )));
10240 }
10241 self.advance();
10242 let mut values: Vec<crate::ast::Expr> = Vec::new();
10243 loop {
10244 values.push(self.parse_expr(0)?);
10245 if matches!(self.peek(), Token::Comma) {
10246 self.advance();
10247 continue;
10248 }
10249 break;
10250 }
10251 if !matches!(self.peek(), Token::RParen) {
10252 return Err(self.err(format!(
10253 "expected ')' after MERGE INSERT values, got {:?}",
10254 self.peek()
10255 )));
10256 }
10257 self.advance();
10258 // Empty column list = positional into every column, so the
10259 // count is checked against the table arity at execution.
10260 if !columns.is_empty() && columns.len() != values.len() {
10261 return Err(self.err(format!(
10262 "MERGE INSERT column count ({}) ≠ value count ({})",
10263 columns.len(),
10264 values.len()
10265 )));
10266 }
10267 crate::ast::MergeAction::Insert { columns, values }
10268 }
10269 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
10270 self.advance();
10271 // SET
10272 let is_set_kw = matches!(
10273 self.peek(),
10274 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
10275 );
10276 if !is_set_kw {
10277 return Err(self.err(format!(
10278 "expected SET after UPDATE in MERGE, got {:?}",
10279 self.peek()
10280 )));
10281 }
10282 self.advance();
10283 let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
10284 loop {
10285 let col = self.expect_ident_like()?;
10286 if !matches!(self.peek(), Token::Eq) {
10287 return Err(self.err(format!(
10288 "expected '=' in MERGE UPDATE assignment, got {:?}",
10289 self.peek()
10290 )));
10291 }
10292 self.advance();
10293 let expr = self.parse_expr(0)?;
10294 assignments.push((col, expr));
10295 if matches!(self.peek(), Token::Comma) {
10296 self.advance();
10297 continue;
10298 }
10299 break;
10300 }
10301 crate::ast::MergeAction::Update { assignments }
10302 }
10303 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
10304 self.advance();
10305 crate::ast::MergeAction::Delete
10306 }
10307 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
10308 self.advance();
10309 let is_nothing_kw = matches!(
10310 self.peek(),
10311 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
10312 );
10313 if !is_nothing_kw {
10314 return Err(self.err(format!(
10315 "expected NOTHING after DO in MERGE clause, got {:?}",
10316 self.peek()
10317 )));
10318 }
10319 self.advance();
10320 crate::ast::MergeAction::DoNothing
10321 }
10322 other => {
10323 return Err(self.err(format!(
10324 "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
10325 )));
10326 }
10327 };
10328 // PG's grammar simply has no INSERT production under BY SOURCE
10329 // (a target row already exists there) — same syntax error.
10330 if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
10331 && matches!(action, crate::ast::MergeAction::Insert { .. })
10332 {
10333 return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
10334 }
10335 clauses.push(crate::ast::MergeWhenClause {
10336 matched,
10337 condition,
10338 action,
10339 });
10340 }
10341 if clauses.is_empty() {
10342 return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
10343 }
10344 // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
10345 // unconditional (no `AND`) WHEN of the same match kind: it could
10346 // never fire. Check per match kind in clause order.
10347 let mut seen_unconditional_matched = false;
10348 let mut seen_unconditional_not_matched = false;
10349 let mut seen_unconditional_by_source = false;
10350 for c in &clauses {
10351 let seen = match c.matched {
10352 crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10353 crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10354 crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10355 };
10356 if *seen {
10357 return Err(self.err(String::from(
10358 "unreachable WHEN clause specified after unconditional WHEN clause",
10359 )));
10360 }
10361 if c.condition.is_none() {
10362 *seen = true;
10363 }
10364 }
10365 // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10366 let returning = self.parse_optional_returning()?;
10367 Ok(Statement::Merge(crate::ast::MergeStatement {
10368 // Attached by `parse_with_cte_then_select` when the MERGE
10369 // heads a WITH clause (round 149).
10370 ctes: Vec::new(),
10371 target,
10372 target_alias,
10373 source,
10374 source_alias,
10375 source_select,
10376 source_column_aliases,
10377 on,
10378 clauses,
10379 returning,
10380 }))
10381 }
10382
10383 /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10384 /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10385 /// as SELECT, so `RETURNING *`, `RETURNING col`,
10386 /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10387 fn parse_optional_returning(
10388 &mut self,
10389 ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10390 let is_returning_kw = matches!(
10391 self.peek(),
10392 Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10393 );
10394 if !is_returning_kw {
10395 return Ok(None);
10396 }
10397 self.advance();
10398 let mut items = Vec::new();
10399 loop {
10400 items.push(self.parse_select_item()?);
10401 if matches!(self.peek(), Token::Comma) {
10402 self.advance();
10403 continue;
10404 }
10405 break;
10406 }
10407 Ok(Some(items))
10408 }
10409
10410 /// v6.0.4 — parse the tail of an ALTER statement after the
10411 /// leading `ALTER` keyword has been consumed. Only one form is
10412 /// supported in v6.0.4:
10413 ///
10414 /// ```text
10415 /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10416 /// ```
10417 fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10418 // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10419 // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10420 // exclusion) is accepted by stripping the `ONLY` keyword
10421 // before the table parse.
10422 // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10423 // and the long PG-dump tail are accepted as no-ops.
10424 match self.advance() {
10425 Token::Index => {}
10426 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10427 // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10428 // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10429 Token::Table => {
10430 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10431 self.advance();
10432 }
10433 return self.parse_alter_table_after_keyword();
10434 }
10435 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10436 return self.parse_alter_policy_after_keyword();
10437 }
10438 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10439 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10440 self.advance();
10441 }
10442 return self.parse_alter_table_after_keyword();
10443 }
10444 // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10445 // of the silent-noop tail.
10446 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10447 return self.parse_alter_sequence_after_keyword();
10448 }
10449 // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10450 // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10451 // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10452 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10453 // NB: the match arm consumed `TYPE` via self.advance(); the
10454 // cursor is now at the type name — do NOT advance again.
10455 let type_name = self.expect_ident_like()?;
10456 let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10457 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10458 if is_add_value {
10459 self.advance(); // ADD
10460 self.advance(); // VALUE
10461 // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10462 // IF/EXISTS as identifiers.
10463 let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10464 {
10465 let n1 = self.tokens.get(self.pos + 1);
10466 let n2 = self.tokens.get(self.pos + 2);
10467 if matches!(n1, Some(Token::Not))
10468 && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10469 {
10470 self.advance();
10471 self.advance();
10472 self.advance();
10473 true
10474 } else {
10475 false
10476 }
10477 } else {
10478 false
10479 };
10480 let label = self.expect_string_literal()?;
10481 let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10482 {
10483 let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10484 self.advance();
10485 let anchor = self.expect_string_literal()?;
10486 Some((is_before, anchor))
10487 } else {
10488 None
10489 };
10490 return Ok(Statement::AlterTypeAddValue {
10491 type_name,
10492 label,
10493 if_not_exists,
10494 position,
10495 });
10496 }
10497 // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10498 // Used to fall into the no-op tail below: accepted, silently
10499 // ignored. `RENAME TO <newtype>` keeps falling through.
10500 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10501 && matches!(
10502 self.tokens.get(self.pos + 1),
10503 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10504 )
10505 {
10506 self.advance(); // RENAME
10507 self.advance(); // VALUE
10508 let old = self.expect_string_literal()?;
10509 if matches!(self.peek(), Token::To) {
10510 self.advance();
10511 } else {
10512 self.expect_keyword_ident("to")?;
10513 }
10514 let new = self.expect_string_literal()?;
10515 return Ok(Statement::AlterTypeRenameValue {
10516 type_name,
10517 old,
10518 new,
10519 });
10520 }
10521 // Other ALTER TYPE forms — the ACTION stays a no-op
10522 // (pg_dump tail), but v7.39 (round 708) the NAME is
10523 // validated: `ALTER TYPE nosuch RENAME TO x` reported
10524 // success for a type that does not exist.
10525 self.consume_until_statement_boundary();
10526 return Ok(Statement::ValidateOnly {
10527 kind: crate::ast::ValidateOnlyKind::TypeName,
10528 names: alloc::vec![type_name],
10529 });
10530 }
10531 // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10532 // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10533 // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10534 // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10535 // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10536 // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10537 // pg_dump no-op list below: every form used to report success
10538 // and change nothing, which is worse than refusing outright
10539 // (a migration dropping a constraint kept rejecting data).
10540 // NOTE: the enclosing `match self.advance()` already consumed
10541 // the DOMAIN keyword, so the name is next.
10542 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10543 return self.parse_alter_domain_after_keyword();
10544 }
10545 // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10546 // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10547 // used to fall into the pg_dump no-op tail below, so a DBA
10548 // setting a per-role default was told it worked and nothing
10549 // happened. Intercepted here, BEFORE that tail.
10550 // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10551 // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10552 // interception below exists: swallowed with the no-op tail, an
10553 // unknown parameter name was ACCEPTED where PG18 answers
10554 // `unrecognized configuration parameter`. SPG applies nothing
10555 // either way — there is no postgresql.auto.conf — but it now
10556 // says so about a name it does not know.
10557 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10558 // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10559 // already consumed here. An extra advance eats the SET and
10560 // the parameter name is never seen — which is exactly the
10561 // bug a panic in this branch disproved: the branch WAS on
10562 // the path, the reading of it was wrong.
10563 let mut parameter = None;
10564 // SET <name> … | RESET <name> | RESET ALL
10565 if matches!(self.peek(), Token::Ident(k)
10566 if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10567 {
10568 self.advance();
10569 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10570 && !n.eq_ignore_ascii_case("all")
10571 {
10572 self.advance();
10573 // A dotted GUC (`plpgsql.check_asserts`) is two
10574 // tokens; keep the whole name.
10575 let mut full = n;
10576 while matches!(self.peek(), Token::Dot) {
10577 self.advance();
10578 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10579 full.push('.');
10580 full.push_str(&t);
10581 }
10582 }
10583 parameter = Some(full);
10584 }
10585 }
10586 self.consume_until_statement_boundary();
10587 return Ok(Statement::AlterSystem { parameter });
10588 }
10589 Token::Ident(s) | Token::QuotedIdent(s)
10590 if matches!(
10591 s.to_ascii_lowercase().as_str(),
10592 "role" | "user" | "database"
10593 ) && self.peeks_db_role_setting() =>
10594 {
10595 let is_database = s.eq_ignore_ascii_case("database");
10596 return self.parse_db_role_setting(is_database);
10597 }
10598 // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10599 // (the non-SET forms; SET/RESET took the branch above). The
10600 // attributes still no-op — recorded, and the ignored PASSWORD
10601 // is ledgered as its own follow-up — but the ROLE is validated:
10602 // any name was accepted for a role that does not exist.
10603 Token::Ident(s) | Token::QuotedIdent(s)
10604 if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10605 {
10606 // NB: the enclosing `match self.advance()` already consumed
10607 // ROLE/USER — the round-695 trap, hit again in this round's
10608 // first draft (the name was eaten and WITH parsed as the
10609 // role). The cursor is at the name.
10610 let name = self.expect_ident_or_string()?;
10611 // v7.39 (round 750) — scan the attribute tail for
10612 // PASSWORD. Everything else stays a recorded no-op, but
10613 // a dropped credential rotation is a SECURITY bug:
10614 // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10615 // changed nothing, so the old password kept working.
10616 // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10617 // NULL` clears the credential.
10618 let mut password: Option<Option<String>> = None;
10619 loop {
10620 match self.peek() {
10621 Token::Semicolon | Token::Eof => break,
10622 Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10623 self.advance();
10624 match self.advance() {
10625 Token::String(p) => password = Some(Some(p)),
10626 Token::Null => password = Some(None),
10627 Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10628 password = Some(None);
10629 }
10630 other => {
10631 return Err(self.err(alloc::format!(
10632 "expected password string or NULL after PASSWORD, got {other:?}"
10633 )));
10634 }
10635 }
10636 }
10637 _ => {
10638 self.advance();
10639 }
10640 }
10641 }
10642 if name.eq_ignore_ascii_case("all") {
10643 // `ALTER ROLE ALL …` names every role; nothing to check.
10644 return Ok(Statement::Empty);
10645 }
10646 if let Some(pw) = password {
10647 return Ok(Statement::AlterRolePassword { name, password: pw });
10648 }
10649 return Ok(Statement::ValidateOnly {
10650 kind: crate::ast::ValidateOnlyKind::RoleName,
10651 names: alloc::vec![name],
10652 });
10653 }
10654 // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10655 // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10656 // list far enough to validate the NAME; the actions still no-op.
10657 // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10658 // models none of them and their dumps are rare.)
10659 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10660 let name = self.expect_ident_or_string()?;
10661 self.consume_until_statement_boundary();
10662 return Ok(Statement::ValidateOnly {
10663 kind: crate::ast::ValidateOnlyKind::CollationName,
10664 names: alloc::vec![name],
10665 });
10666 }
10667 Token::Ident(s) | Token::QuotedIdent(s)
10668 if s.eq_ignore_ascii_case("text")
10669 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10670 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10671 {
10672 self.advance(); // SEARCH
10673 self.advance(); // CONFIGURATION
10674 let name = self.expect_ident_like()?;
10675 self.consume_until_statement_boundary();
10676 return Ok(Statement::ValidateOnly {
10677 kind: crate::ast::ValidateOnlyKind::TsConfigName,
10678 names: alloc::vec![name],
10679 });
10680 }
10681 Token::Ident(s) | Token::QuotedIdent(s)
10682 if s.eq_ignore_ascii_case("event")
10683 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10684 {
10685 self.advance(); // TRIGGER
10686 let name = self.expect_ident_like()?;
10687 self.consume_until_statement_boundary();
10688 return Ok(Statement::ValidateOnly {
10689 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10690 names: alloc::vec![name],
10691 });
10692 }
10693 Token::Ident(s) | Token::QuotedIdent(s)
10694 if s.eq_ignore_ascii_case("large")
10695 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10696 {
10697 self.advance(); // OBJECT
10698 let oid = match self.advance() {
10699 Token::Integer(n) => alloc::format!("{n}"),
10700 other => {
10701 return Err(
10702 self.err(alloc::format!("expected large object oid, got {other:?}"))
10703 );
10704 }
10705 };
10706 self.consume_until_statement_boundary();
10707 return Ok(Statement::ValidateOnly {
10708 kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10709 names: alloc::vec![oid],
10710 });
10711 }
10712 // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10713 // argument-list parse as DROP AGGREGATE (round 707); the
10714 // action no-ops, the existence check is real.
10715 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10716 // Same round-695 trap as above: AGGREGATE is already
10717 // consumed; the cursor is at the name.
10718 let name = self.expect_ident_like()?;
10719 let mut names = alloc::vec![name];
10720 if matches!(self.peek(), Token::LParen) {
10721 self.advance();
10722 loop {
10723 match self.peek().clone() {
10724 Token::RParen => {
10725 self.advance();
10726 break;
10727 }
10728 Token::Star => {
10729 self.advance();
10730 names.push(String::from("*"));
10731 }
10732 Token::Comma => {
10733 self.advance();
10734 }
10735 _ => {
10736 let mut t = self.expect_ident_like()?;
10737 while let Token::Ident(nx) = self.peek() {
10738 let nx = nx.clone();
10739 self.advance();
10740 t.push(' ');
10741 t.push_str(&nx);
10742 }
10743 names.push(t);
10744 }
10745 }
10746 }
10747 }
10748 self.consume_until_statement_boundary();
10749 return Ok(Statement::ValidateOnly {
10750 kind: crate::ast::ValidateOnlyKind::AggregateName,
10751 names,
10752 });
10753 }
10754 Token::Ident(s) | Token::QuotedIdent(s)
10755 if matches!(
10756 s.to_ascii_lowercase().as_str(),
10757 "view"
10758 | "function"
10759 | "database"
10760 | "schema"
10761 | "owner"
10762 | "default"
10763 | "extension"
10764 | "materialized"
10765 | "publication"
10766 | "subscription"
10767 // v7.37.17 (17.6 siblings) — additional ALTER
10768 // targets pg_dump / pg_dumpall / operator DB
10769 // migration scripts commonly emit. SPG has
10770 // no matching machinery for any of these; the
10771 // parser accepts + Empty-returns so pg_dump
10772 // tail statements don't stall.
10773 | "tablespace"
10774 | "language"
10775 | "operator"
10776 | "conversion"
10777 | "statistics"
10778 | "server"
10779 | "foreign"
10780 // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10781 // / TEMPLATE (CONFIGURATION intercepted above).
10782 | "text"
10783 ) =>
10784 {
10785 self.consume_until_statement_boundary();
10786 return Ok(Statement::Empty);
10787 }
10788 other => {
10789 return Err(self.err(format!(
10790 "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10791 after ALTER, got {other:?}"
10792 )));
10793 }
10794 }
10795 // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10796 // (mailrs migrate-042 ships these). The presence of an
10797 // IF EXISTS makes the subsequent name lookup tolerate
10798 // a missing index — engine returns CommandOk no-op.
10799 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10800 let next = self.tokens.get(self.pos + 1);
10801 if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10802 self.advance();
10803 self.advance();
10804 true
10805 } else {
10806 false
10807 }
10808 } else {
10809 false
10810 };
10811 let name = self.expect_ident_like()?;
10812 // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10813 // Detect BEFORE the REBUILD path so the existing REBUILD
10814 // arm stays untouched.
10815 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10816 self.advance();
10817 if matches!(self.peek(), Token::To) {
10818 self.advance();
10819 } else {
10820 self.expect_keyword_ident("to")?;
10821 }
10822 let new = self.expect_ident_like()?;
10823 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10824 name,
10825 target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10826 }));
10827 }
10828 // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10829 // A syntax error before; the index is validated, the params no-op.
10830 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10831 || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10832 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10833 {
10834 self.consume_until_statement_boundary();
10835 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10836 name,
10837 target: crate::ast::AlterIndexTarget::StorageParams,
10838 }));
10839 }
10840 // REBUILD
10841 self.expect_keyword_ident("rebuild")?;
10842 // Optional: WITH (encoding = <enc>)
10843 let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10844 self.advance();
10845 if !matches!(self.peek(), Token::LParen) {
10846 return Err(self.err(format!(
10847 "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10848 self.peek()
10849 )));
10850 }
10851 self.advance();
10852 self.expect_keyword_ident("encoding")?;
10853 if !matches!(self.peek(), Token::Eq) {
10854 return Err(self.err(format!(
10855 "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10856 self.peek()
10857 )));
10858 }
10859 self.advance();
10860 let enc_ident = match self.advance() {
10861 Token::Ident(s) | Token::QuotedIdent(s) => s,
10862 other => {
10863 return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10864 }
10865 };
10866 let enc = match enc_ident.to_ascii_lowercase().as_str() {
10867 "f32" => VecEncoding::F32,
10868 "sq8" => VecEncoding::Sq8,
10869 "half" => VecEncoding::F16,
10870 other => {
10871 return Err(self.err(format!(
10872 "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10873 )));
10874 }
10875 };
10876 if !matches!(self.peek(), Token::RParen) {
10877 return Err(self.err(format!(
10878 "expected ')' after encoding value, got {:?}",
10879 self.peek()
10880 )));
10881 }
10882 self.advance();
10883 Some(enc)
10884 } else {
10885 None
10886 };
10887 Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10888 name,
10889 target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10890 }))
10891 }
10892
10893 /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10894 /// only `SET` form currently supported; future v6.7.x can add
10895 /// more SET subjects without changing the dispatch shape.
10896 /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10897 /// subactions. Single-subaction shape stays a 1-element vec.
10898 fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10899 let table_name = self.expect_ident_like()?;
10900 let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10901 loop {
10902 let subaction = self.parse_alter_table_subaction()?;
10903 // ADD COLUMN with inline REFERENCES emits both an
10904 // AddColumn and an AddForeignKey subaction; the
10905 // helper returns 1 or 2 items.
10906 targets.extend(subaction);
10907 if matches!(self.peek(), Token::Comma) {
10908 self.advance();
10909 continue;
10910 }
10911 break;
10912 }
10913 Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10914 name: table_name,
10915 targets,
10916 }))
10917 }
10918
10919 /// Parse one ALTER TABLE subaction. Returns a Vec because
10920 /// inline `REFERENCES` on `ADD COLUMN` produces both an
10921 /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10922 fn parse_alter_table_subaction(
10923 &mut self,
10924 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10925 match self.peek() {
10926 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10927 self.advance();
10928 // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10929 // storage parameters: paren-prefixed; consume.
10930 if matches!(self.peek(), Token::LParen) {
10931 self.consume_until_statement_boundary();
10932 return Ok(Vec::new());
10933 }
10934 let setting = self.expect_ident_like()?;
10935 if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10936 if !matches!(self.peek(), Token::Eq) {
10937 return Err(self.err(alloc::format!(
10938 "expected '=' after hot_tier_bytes, got {:?}",
10939 self.peek()
10940 )));
10941 }
10942 self.advance();
10943 let n = self.expect_u64_literal()?;
10944 return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10945 }
10946 // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10947 // accept-and-no-op for ALTER TABLE SET <subject>
10948 // forms that pg_dump emits but SPG either treats
10949 // as N/A (single-tenant, single-owner, no shared
10950 // tablespaces) or accepts the dump-side declaration
10951 // without runtime effect:
10952 // SET SCHEMA <name> (18.11)
10953 // SET TABLESPACE <name> (18.8)
10954 // SET LOGGED / UNLOGGED (18.7 alt-form)
10955 // SET WITHOUT CLUSTER (18.13)
10956 // SET WITHOUT OIDS (PG legacy)
10957 // SET (option = value, …) (storage parameters)
10958 // SET REPLICA IDENTITY {…} (18.14)
10959 if setting.eq_ignore_ascii_case("schema")
10960 || setting.eq_ignore_ascii_case("tablespace")
10961 || setting.eq_ignore_ascii_case("logged")
10962 || setting.eq_ignore_ascii_case("unlogged")
10963 || setting.eq_ignore_ascii_case("without")
10964 {
10965 self.consume_until_statement_boundary();
10966 return Ok(Vec::new());
10967 }
10968 if setting.eq_ignore_ascii_case("replica") {
10969 // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10970 self.consume_until_statement_boundary();
10971 return Ok(Vec::new());
10972 }
10973 // SET (option=value, …) — storage parameters.
10974 if matches!(self.peek(), Token::LParen) {
10975 self.consume_until_statement_boundary();
10976 return Ok(Vec::new());
10977 }
10978 Err(self.err(alloc::format!(
10979 "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10980 hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10981 WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10982 )))
10983 }
10984 // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10985 // not ignored: round 645 gave SPG the inheritance the
10986 // v7.37.18 no-op said it did not have.
10987 Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10988 self.advance();
10989 let parent = self.expect_ident_like()?;
10990 self.consume_until_statement_boundary();
10991 Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10992 parent,
10993 detach: false
10994 }])
10995 }
10996 // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10997 // LEVEL SECURITY`, which has its own RLS arm below — without
10998 // the guard this swallowed NO FORCE as a no-op.
10999 Token::Ident(s)
11000 if s.eq_ignore_ascii_case("no")
11001 && !matches!(
11002 self.tokens.get(self.pos + 1),
11003 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11004 ) =>
11005 {
11006 self.advance();
11007 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
11008 if k.eq_ignore_ascii_case("inherit"))
11009 {
11010 self.advance();
11011 let parent = self.expect_ident_like()?;
11012 self.consume_until_statement_boundary();
11013 return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
11014 parent,
11015 detach: true
11016 }]);
11017 }
11018 self.consume_until_statement_boundary();
11019 Ok(Vec::new())
11020 }
11021 // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
11022 // single-owner, so there is still nothing to record.
11023 //
11024 // v7.39 (round 652) — but the name now reaches the engine,
11025 // which refuses a role that does not exist as PG does. The
11026 // no-op was swallowing the whole statement, so a dump naming
11027 // a role this server never heard of restored clean and left
11028 // the table owned by whoever ran the restore.
11029 Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
11030 self.advance();
11031 if matches!(self.peek(), Token::To) {
11032 self.advance();
11033 }
11034 let role = self.expect_ident_like()?;
11035 Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
11036 role
11037 }])
11038 }
11039 // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
11040 // PG sets a hint; SPG doesn't have clustered storage, so the
11041 // hint itself stays a no-op.
11042 //
11043 // v7.39 (round 652) — the index name is checked now. PG
11044 // errors on one that does not exist, and swallowing that let
11045 // a typo'd CLUSTER ON pass silently.
11046 Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
11047 self.advance();
11048 // `ON` is a reserved token, not an ident.
11049 if !matches!(self.peek(), Token::On) {
11050 return Err(self.err(alloc::format!(
11051 "expected ON after CLUSTER, got {:?}",
11052 self.peek()
11053 )));
11054 }
11055 self.advance();
11056 let index = self.expect_ident_like()?;
11057 Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
11058 index: Some(index)
11059 }])
11060 }
11061 // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
11062 // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
11063 // what a logical decoder puts in the old-tuple image; SPG's
11064 // replication is SQL-text, so there is nothing to record.
11065 // Accept-and-no-op (it used to be a parse error).
11066 Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
11067 self.advance();
11068 // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
11069 // validates the index; DEFAULT / FULL / NOTHING stay no-op.
11070 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
11071 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
11072 {
11073 self.advance(); // IDENTITY
11074 self.advance(); // USING
11075 if matches!(self.peek(), Token::Index)
11076 || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
11077 {
11078 self.advance();
11079 }
11080 let index = self.expect_ident_like()?;
11081 self.consume_until_statement_boundary();
11082 return Ok(alloc::vec![
11083 crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
11084 ]);
11085 }
11086 self.consume_until_statement_boundary();
11087 Ok(Vec::new())
11088 }
11089 // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
11090 //
11091 // v7.39 (round 652) — it used to consume the statement and
11092 // return nothing, on the stated theory that SPG validated at
11093 // ADD CONSTRAINT time so there was never anything left to
11094 // validate. Measured against PG18, ADD CONSTRAINT did not
11095 // scan the existing rows at all — the comment described a
11096 // property SPG did not have, which is why nobody looked. Both
11097 // halves are real now: ADD scans unless told NOT VALID, and
11098 // this scans what NOT VALID skipped.
11099 Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
11100 self.advance();
11101 self.expect_keyword_ident("constraint")?;
11102 let name = self.expect_ident_like()?;
11103 Ok(alloc::vec![
11104 crate::ast::AlterTableTarget::ValidateConstraint { name }
11105 ])
11106 }
11107 // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
11108 // SET (option = value, …). PG uses it to clear per-table
11109 // storage params like fillfactor or autovacuum_*. SPG
11110 // engine-manages those parameters; accept-and-no-op.
11111 Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
11112 self.advance();
11113 self.consume_until_statement_boundary();
11114 Ok(Vec::new())
11115 }
11116 // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
11117 // type-of binding (PG 9.0+). SPG composite types
11118 // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
11119 // TABLE OF is rare and inverse of CREATE TABLE OF.
11120 // Accept-and-no-op until a customer dump round-trips it.
11121 Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
11122 self.advance();
11123 // v7.39 (round 710) — the type name is validated now.
11124 let type_name = self.expect_ident_like()?;
11125 self.consume_until_statement_boundary();
11126 Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
11127 type_name
11128 }])
11129 }
11130 // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
11131 // (reserved keyword) rather than Token::Ident("not"),
11132 // so it needs its own arm. Accept-and-no-op same as OF.
11133 Token::Not => {
11134 self.advance();
11135 self.consume_until_statement_boundary();
11136 Ok(Vec::new())
11137 }
11138 // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
11139 Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
11140 self.advance();
11141 self.expect_row_level_security()?;
11142 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11143 enabled: None,
11144 force: Some(true),
11145 }])
11146 }
11147 // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
11148 Token::Ident(s)
11149 if s.eq_ignore_ascii_case("no")
11150 && matches!(
11151 self.tokens.get(self.pos + 1),
11152 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11153 ) =>
11154 {
11155 self.advance(); // NO
11156 self.advance(); // FORCE
11157 self.expect_row_level_security()?;
11158 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11159 enabled: None,
11160 force: Some(false),
11161 }])
11162 }
11163 // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
11164 // (sets relrowsecurity). The guard requires the next token to be
11165 // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
11166 Token::Ident(s)
11167 if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
11168 && matches!(
11169 self.tokens.get(self.pos + 1),
11170 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
11171 ) =>
11172 {
11173 let enabled = s.eq_ignore_ascii_case("enable");
11174 self.advance(); // ENABLE/DISABLE
11175 self.expect_row_level_security()?;
11176 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11177 enabled: Some(enabled),
11178 force: None,
11179 }])
11180 }
11181 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11182 self.advance();
11183 // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
11184 // {INDEX|KEY} [name] (cols)`, which every ORM migration
11185 // emits. The same grammar CREATE TABLE already accepts
11186 // inline (`KEY idx (a)`, prefix lengths and all), so it goes
11187 // through the SAME parser — an ALTER-only copy would be a
11188 // second place for the two to drift.
11189 if self.peek_mysql_inline_key_start() {
11190 return Ok(match self.parse_mysql_inline_key()? {
11191 Some(c) => {
11192 alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
11193 }
11194 // FULLTEXT / SPATIAL parse and are accepted as a
11195 // no-op here exactly as they are inline.
11196 None => Vec::new(),
11197 });
11198 }
11199 // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
11200 // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
11201 // PRIMARY KEY this way; mysqldump emits both.
11202 // Peek-only dispatch (no advance) — `advance()`
11203 // destructively replaces consumed tokens with Eof,
11204 // so saved-pos restore would land on Eofs.
11205 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
11206 {
11207 // The next-but-one ident is the constraint
11208 // name; the one after THAT is the kind.
11209 let kind_pos = self.pos + 2;
11210 let kind = self.tokens.get(kind_pos).cloned();
11211 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
11212 {
11213 let fk = self.parse_table_level_fk()?;
11214 return Ok(alloc::vec![
11215 crate::ast::AlterTableTarget::AddForeignKey(fk)
11216 ]);
11217 }
11218 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
11219 {
11220 self.advance(); // CONSTRAINT
11221 // v7.39 (read01 round 48) — keep the name; the engine
11222 // stores it now instead of dropping it on the floor.
11223 let con_name = self.expect_ident_like()?;
11224 self.advance(); // PRIMARY
11225 self.expect_keyword_ident("key")?;
11226 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11227 // v7.39 (round 711) — the ALTER form carries the
11228 // timing too (pg_dump writes it here).
11229 let (deferrable, initially_deferred) =
11230 self.consume_deferrable_clauses_timed()?;
11231 return Ok(alloc::vec![
11232 crate::ast::AlterTableTarget::AddTableConstraint(
11233 crate::ast::TableConstraint::PrimaryKey {
11234 name: Some(con_name),
11235 columns: cols,
11236 deferrable,
11237 initially_deferred,
11238 }
11239 )
11240 ]);
11241 }
11242 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
11243 {
11244 self.advance(); // CONSTRAINT
11245 // v7.39 (read01 round 48) — keep the name.
11246 let con_name = self.expect_ident_like()?;
11247 // v7.22 (mailrs round-13 gap 6) — delegate so
11248 // the optional `NULLS [NOT] DISTINCT` modifier
11249 // parses here too (pg_dump emits the ALTER
11250 // form; semantics enforced by the engine
11251 // since v7.13).
11252 let mut uc = self.parse_table_level_unique()?;
11253 if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
11254 *name = Some(con_name);
11255 }
11256 return Ok(alloc::vec![
11257 crate::ast::AlterTableTarget::AddTableConstraint(uc)
11258 ]);
11259 }
11260 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
11261 {
11262 self.advance(); // CONSTRAINT
11263 // v7.39 (read01 round 48) — keep the name.
11264 let con_name = self.expect_ident_like()?;
11265 self.advance(); // CHECK
11266 if !matches!(self.peek(), Token::LParen) {
11267 return Err(self.err(alloc::format!(
11268 "expected '(' after CHECK, got {:?}", self.peek()
11269 )));
11270 }
11271 self.advance();
11272 let expr = self.parse_expr(0)?;
11273 if matches!(self.peek(), Token::RParen) {
11274 self.advance();
11275 }
11276 let not_valid = self.parse_not_valid_suffix();
11277 return Ok(alloc::vec![
11278 crate::ast::AlterTableTarget::AddTableConstraint(
11279 crate::ast::TableConstraint::Check {
11280 name: Some(con_name),
11281 expr,
11282 not_valid,
11283 }
11284 )
11285 ]);
11286 }
11287 // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
11288 // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
11289 // exclusion constraints via this ALTER form.
11290 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
11291 {
11292 self.advance(); // CONSTRAINT
11293 let con_name = self.expect_ident_like()?;
11294 let mut ex = self.parse_table_level_exclude()?;
11295 if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
11296 *name = Some(con_name);
11297 }
11298 return Ok(alloc::vec![
11299 crate::ast::AlterTableTarget::AddTableConstraint(ex)
11300 ]);
11301 }
11302 // Unknown kind — fall through to FK path which
11303 // produces a descriptive parse error.
11304 }
11305 let is_fk = matches!(
11306 self.peek(),
11307 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
11308 || s.eq_ignore_ascii_case("foreign")
11309 );
11310 if is_fk {
11311 let fk = self.parse_table_level_fk()?;
11312 return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
11313 }
11314 // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
11315 // (no CONSTRAINT prefix) — same dispatch.
11316 match self.peek().clone() {
11317 Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
11318 self.advance();
11319 self.expect_keyword_ident("key")?;
11320 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11321 let (deferrable, initially_deferred) =
11322 self.consume_deferrable_clauses_timed()?;
11323 return Ok(alloc::vec![
11324 crate::ast::AlterTableTarget::AddTableConstraint(
11325 crate::ast::TableConstraint::PrimaryKey {
11326 name: None,
11327 columns: cols,
11328 deferrable,
11329 initially_deferred,
11330 }
11331 )
11332 ]);
11333 }
11334 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
11335 // v7.22 — delegate (NULLS [NOT] DISTINCT).
11336 let uc = self.parse_table_level_unique()?;
11337 return Ok(alloc::vec![
11338 crate::ast::AlterTableTarget::AddTableConstraint(uc)
11339 ]);
11340 }
11341 // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
11342 // prefix). The other three bare forms were here and
11343 // this one was not, so it fell through to the column
11344 // path and came back as "unexpected reserved keyword
11345 // 'check' at start of column definition".
11346 _ if self.peek_table_level_check_start() => {
11347 let chk = self.parse_table_level_check()?;
11348 let not_valid = self.parse_not_valid_suffix();
11349 let crate::ast::TableConstraint::Check { expr, .. } = chk else {
11350 unreachable!("parse_table_level_check returns Check")
11351 };
11352 return Ok(alloc::vec![
11353 crate::ast::AlterTableTarget::AddTableConstraint(
11354 crate::ast::TableConstraint::Check {
11355 name: None,
11356 expr,
11357 not_valid,
11358 }
11359 )
11360 ]);
11361 }
11362 // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11363 Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11364 let ex = self.parse_table_level_exclude()?;
11365 return Ok(alloc::vec![
11366 crate::ast::AlterTableTarget::AddTableConstraint(ex)
11367 ]);
11368 }
11369 _ => {}
11370 }
11371 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11372 self.advance();
11373 }
11374 let mut if_not_exists = false;
11375 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11376 self.advance();
11377 if !matches!(self.peek(), Token::Not) {
11378 return Err(self.err(alloc::format!(
11379 "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11380 self.peek()
11381 )));
11382 }
11383 self.advance();
11384 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11385 return Err(self.err(alloc::format!(
11386 "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11387 self.peek()
11388 )));
11389 }
11390 self.advance();
11391 if_not_exists = true;
11392 }
11393 // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11394 // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11395 // returns ColumnDef + an optional inline FK.
11396 let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11397 let col_name = column.name.clone();
11398 let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11399 column,
11400 if_not_exists,
11401 }];
11402 if let Some(mut fk) = col_level_fk {
11403 if fk.columns.is_empty() {
11404 fk.columns.push(col_name);
11405 }
11406 out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11407 }
11408 Ok(out)
11409 }
11410 Token::Drop => {
11411 self.advance();
11412 // v7.13.3 — dispatch on the next token. mailrs round-7
11413 // S8 closed DROP COLUMN; round-6 S7 closed
11414 // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11415 // RESTRICT modifiers.
11416 // DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11417 // DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11418 let subject = match self.peek() {
11419 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11420 self.advance();
11421 "constraint"
11422 }
11423 Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11424 self.advance();
11425 "column"
11426 }
11427 // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11428 // `INDEX` lexes as the reserved Token::Index, so it is
11429 // unambiguous. `KEY` is a plain ident, and PG allows a
11430 // column literally named "key", so only read it as the
11431 // keyword when a name follows it.
11432 Token::Index => {
11433 self.advance();
11434 "index"
11435 }
11436 Token::Ident(s)
11437 if s.eq_ignore_ascii_case("key")
11438 && matches!(
11439 self.tokens.get(self.pos + 1),
11440 Some(Token::Ident(_) | Token::QuotedIdent(_))
11441 ) =>
11442 {
11443 self.advance();
11444 "index"
11445 }
11446 // PG-canonical bare `DROP <col>` without COLUMN
11447 // keyword is also valid; treat any other ident
11448 // as the column name.
11449 Token::Ident(_) | Token::QuotedIdent(_) => "column",
11450 other => {
11451 return Err(self.err(alloc::format!(
11452 "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11453 )));
11454 }
11455 };
11456 let mut if_exists = false;
11457 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11458 let n1 = self.tokens.get(self.pos + 1);
11459 if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11460 self.advance();
11461 self.advance();
11462 if_exists = true;
11463 }
11464 }
11465 let name = self.expect_ident_like()?;
11466 let mut cascade = false;
11467 if matches!(
11468 self.peek(),
11469 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11470 || s.eq_ignore_ascii_case("restrict")
11471 ) {
11472 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11473 {
11474 cascade = true;
11475 }
11476 self.advance();
11477 }
11478 if subject == "index" {
11479 Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11480 name,
11481 if_exists,
11482 }])
11483 } else if subject == "constraint" {
11484 Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11485 name,
11486 if_exists,
11487 }])
11488 } else {
11489 Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11490 column: name,
11491 if_exists,
11492 cascade,
11493 }])
11494 }
11495 }
11496 Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11497 self.advance();
11498 // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11499 // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11500 // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11501 // immediately; accept-and-no-op.
11502 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11503 self.advance();
11504 self.consume_until_statement_boundary();
11505 return Ok(Vec::new());
11506 }
11507 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11508 self.advance();
11509 }
11510 let col_name = self.expect_ident_like()?;
11511 match self.peek() {
11512 Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11513 self.advance();
11514 }
11515 // v7.14.0 — pg_dump emits BIGSERIAL via
11516 // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11517 // nextval('seq')` (the sequence is created
11518 // separately). SPG's BIGSERIAL already uses
11519 // AUTO_INCREMENT; accept SET DEFAULT / DROP
11520 // DEFAULT / SET NOT NULL / DROP NOT NULL as
11521 // engine no-ops by consuming the tail.
11522 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11523 // v7.22 (round-13 T2) — `SET DEFAULT
11524 // nextval('…')` is how pg_dump spells a
11525 // SERIAL column (plain integer in CREATE
11526 // TABLE + this ALTER). It used to be
11527 // swallowed as a no-op, which silently
11528 // STRIPPED auto-increment from imported
11529 // schemas — the first post-import INSERT
11530 // without an explicit id then violated NOT
11531 // NULL. Lower it to the auto-increment
11532 // marker instead.
11533 let is_default_nextval =
11534 matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11535 && matches!(
11536 self.tokens.get(self.pos + 2),
11537 Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11538 );
11539 if is_default_nextval {
11540 let seq_name = self.scan_sequence_name_until_boundary();
11541 return Ok(alloc::vec![
11542 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11543 column: col_name,
11544 seq_name,
11545 }
11546 ]);
11547 }
11548 // v7.37.18 (18.1 + 18.2) — proper lowering.
11549 self.advance(); // consume "set"
11550 match self.peek().clone() {
11551 Token::Default => {
11552 self.advance();
11553 let default_expr = self.parse_expr(0)?;
11554 return Ok(alloc::vec![
11555 crate::ast::AlterTableTarget::AlterColumnSetDefault {
11556 column: col_name,
11557 default_expr,
11558 }
11559 ]);
11560 }
11561 Token::Not => {
11562 self.advance();
11563 if !matches!(self.peek(), Token::Null) {
11564 return Err(self.err(alloc::format!(
11565 "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11566 self.peek()
11567 )));
11568 }
11569 self.advance();
11570 return Ok(alloc::vec![
11571 crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11572 column: col_name,
11573 }
11574 ]);
11575 }
11576 // `SET EXPRESSION AS (expr)` (PG 17) — change a
11577 // stored generated column's expression and
11578 // recompute existing rows.
11579 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11580 self.advance(); // EXPRESSION
11581 if matches!(self.peek(), Token::As) {
11582 self.advance();
11583 }
11584 let expr = self.parse_expr(0)?;
11585 return Ok(alloc::vec![
11586 crate::ast::AlterTableTarget::AlterColumnSetExpression {
11587 column: col_name,
11588 expr,
11589 }
11590 ]);
11591 }
11592 other => {
11593 // Other SET subjects (STATISTICS,
11594 // STORAGE, COMPRESSION, …) stay no-ops —
11595 // storage hints with no SPG semantics.
11596 let _ = other;
11597 self.consume_until_statement_boundary();
11598 return Ok(Vec::new());
11599 }
11600 }
11601 }
11602 Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11603 self.advance(); // consume "drop"
11604 return self.parse_alter_column_drop_tail(col_name);
11605 }
11606 Token::Drop => {
11607 self.advance(); // consume Drop token
11608 return self.parse_alter_column_drop_tail(col_name);
11609 }
11610 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11611 // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11612 // GENERATED { ALWAYS | BY DEFAULT } AS
11613 // IDENTITY ( … )`: pg_dump's spelling for
11614 // identity columns. Same auto-increment
11615 // lowering as the nextval default; the
11616 // sequence options inside the parens are
11617 // no-ops under SPG's max+1 semantics.
11618 let is_generated = matches!(
11619 self.tokens.get(self.pos + 1),
11620 Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11621 );
11622 if !is_generated {
11623 return Err(self.err(alloc::format!(
11624 "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11625 self.tokens.get(self.pos + 1)
11626 )));
11627 }
11628 let seq_name = self.scan_sequence_name_until_boundary();
11629 return Ok(alloc::vec![
11630 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11631 column: col_name,
11632 seq_name,
11633 }
11634 ]);
11635 }
11636 // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11637 // column: floor the next allocated value at n (bare
11638 // RESTART = restart from the start value, 1).
11639 Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11640 self.advance();
11641 let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11642 {
11643 self.advance();
11644 let neg = if matches!(self.peek(), Token::Minus) {
11645 self.advance();
11646 true
11647 } else {
11648 false
11649 };
11650 match self.advance() {
11651 Token::Integer(v) => Some(if neg { -v } else { v }),
11652 other => {
11653 return Err(self.err(alloc::format!(
11654 "expected integer after RESTART WITH, got {other:?}"
11655 )));
11656 }
11657 }
11658 } else {
11659 None
11660 };
11661 return Ok(alloc::vec![
11662 crate::ast::AlterTableTarget::AlterColumnRestart {
11663 column: col_name,
11664 with,
11665 }
11666 ]);
11667 }
11668 other => {
11669 return Err(self.err(alloc::format!(
11670 "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11671 )));
11672 }
11673 }
11674 // v7.39 (round 713) — the type parser has consumed a
11675 // trailing `COLLATE <name>` since Phase 2.5, and
11676 // `parse_column_type_name` discarded it: `ALTER COLUMN t
11677 // TYPE text COLLATE "C"` parsed clean and changed
11678 // nothing. Keep the clause; the engine re-collates.
11679 let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _, _, _) =
11680 self.parse_type_with_implied_flags()?;
11681 let collation = if coll_explicit {
11682 coll_name.map(|n| (coll, n))
11683 } else {
11684 None
11685 };
11686 let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11687 {
11688 self.advance();
11689 Some(self.parse_expr(0)?)
11690 } else {
11691 None
11692 };
11693 Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11694 column: col_name,
11695 new_type,
11696 using,
11697 collation,
11698 }])
11699 }
11700 // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11701 // PG also supports `RENAME TO new_table` for table-name
11702 // rename; that surface is deferred (pg_dump never emits
11703 // it). If the first post-RENAME ident is `TO`, the user
11704 // is asking for table rename — error with a clear
11705 // message rather than misparsing `TO` as a column name.
11706 Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11707 self.advance();
11708 // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11709 // table-name rename (mailrs round-10 A.5 — used
11710 // by migrate-042's `RENAME TO email_contacts`).
11711 // `TO` lexes as Token::To.
11712 if matches!(self.peek(), Token::To)
11713 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11714 {
11715 self.advance();
11716 let new = self.expect_ident_like()?;
11717 return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11718 new,
11719 }]);
11720 }
11721 // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11722 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11723 self.advance();
11724 let old = self.expect_ident_like()?;
11725 if matches!(self.peek(), Token::To) {
11726 self.advance();
11727 } else {
11728 self.expect_keyword_ident("to")?;
11729 }
11730 let new = self.expect_ident_like()?;
11731 return Ok(alloc::vec![
11732 crate::ast::AlterTableTarget::RenameConstraint { old, new }
11733 ]);
11734 }
11735 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11736 self.advance();
11737 }
11738 let old = self.expect_ident_like()?;
11739 // `TO` is a reserved keyword token; accept both
11740 // Token::To and Token::Ident("to") for consistency.
11741 if matches!(self.peek(), Token::To) {
11742 self.advance();
11743 } else {
11744 self.expect_keyword_ident("to")?;
11745 }
11746 let new = self.expect_ident_like()?;
11747 Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11748 old,
11749 new,
11750 }])
11751 }
11752 // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11753 // { ALL | <name> }`. pg_dump --disable-triggers wraps
11754 // every data block with these. Real disable semantics —
11755 // not no-op — because reload correctness assumes the
11756 // triggers don't fire (rows already carry their
11757 // computed values from prod).
11758 Token::Ident(s)
11759 if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11760 {
11761 let enabled = s.eq_ignore_ascii_case("enable");
11762 self.advance();
11763 // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11764 // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11765 // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11766 // pg_dump output) — anything else falls through to
11767 // the catch-all error below.
11768 // v7.22 (round-13 T3) — mysqldump wraps every data
11769 // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11770 // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11771 // maintains indexes incrementally — engine no-op.
11772 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11773 self.advance();
11774 return Ok(Vec::new());
11775 }
11776 // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11777 // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11778 // to gate triggers on session_replication_role; SPG
11779 // has no replica role, so the prefix is consumed and
11780 // treated identically to the plain ENABLE/DISABLE
11781 // TRIGGER form.
11782 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11783 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11784 {
11785 self.advance();
11786 }
11787 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11788 return Err(self.err(alloc::format!(
11789 "expected TRIGGER after {}, got {:?}",
11790 if enabled { "ENABLE" } else { "DISABLE" },
11791 self.peek()
11792 )));
11793 }
11794 self.advance();
11795 // `ALL` lexes as Token::All (reserved); also
11796 // accept Token::Ident("all") for symmetry.
11797 // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11798 // TRIGGER selectors. USER (= all user triggers) is
11799 // semantically ALL here; REPLICA / ALWAYS gate on
11800 // session_replication_role which SPG doesn't track.
11801 // All map to TriggerSelector::All.
11802 let which = if matches!(self.peek(), Token::All)
11803 || matches!(self.peek(), Token::Ident(s)
11804 if s.eq_ignore_ascii_case("all")
11805 || s.eq_ignore_ascii_case("user")
11806 || s.eq_ignore_ascii_case("replica")
11807 || s.eq_ignore_ascii_case("always"))
11808 {
11809 self.advance();
11810 crate::ast::TriggerSelector::All
11811 } else {
11812 let name = self.expect_ident_like()?;
11813 crate::ast::TriggerSelector::Named(name)
11814 };
11815 Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11816 which,
11817 enabled,
11818 }])
11819 }
11820 // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11821 Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11822 self.advance();
11823 if !matches!(self.peek(), Token::Partition)
11824 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11825 if s.eq_ignore_ascii_case("partition"))
11826 {
11827 return Err(self.err(alloc::format!(
11828 "expected PARTITION after ATTACH, got {:?}",
11829 self.peek()
11830 )));
11831 }
11832 self.advance();
11833 let child = self.expect_ident_like()?;
11834 let bounds = self.parse_partition_bounds_tail()?;
11835 Ok(alloc::vec![
11836 crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11837 ])
11838 }
11839 // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11840 Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11841 self.advance();
11842 if !matches!(self.peek(), Token::Partition)
11843 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11844 if s.eq_ignore_ascii_case("partition"))
11845 {
11846 return Err(self.err(alloc::format!(
11847 "expected PARTITION after DETACH, got {:?}",
11848 self.peek()
11849 )));
11850 }
11851 self.advance();
11852 let child = self.expect_ident_like()?;
11853 let mut concurrently = false;
11854 let mut finalize = false;
11855 loop {
11856 match self.peek().clone() {
11857 Token::Ident(s) | Token::QuotedIdent(s)
11858 if s.eq_ignore_ascii_case("concurrently") =>
11859 {
11860 self.advance();
11861 concurrently = true;
11862 }
11863 Token::Ident(s) | Token::QuotedIdent(s)
11864 if s.eq_ignore_ascii_case("finalize") =>
11865 {
11866 self.advance();
11867 finalize = true;
11868 }
11869 _ => break,
11870 }
11871 }
11872 Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11873 child,
11874 concurrently,
11875 finalize,
11876 }])
11877 }
11878 other => Err(self.err(alloc::format!(
11879 "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11880 ))),
11881 }
11882 }
11883
11884 /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11885 /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11886 /// TABLE … ATTACH PARTITION. Shares the same grammar as
11887 /// `parse_partition_of_tail`'s bounds branch.
11888 /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11889 /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11890 /// lowering each to the respective AlterTableTarget. Any
11891 /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11892 /// no-op via consume_until_statement_boundary.
11893 fn parse_alter_column_drop_tail(
11894 &mut self,
11895 col_name: String,
11896 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11897 match self.peek().clone() {
11898 Token::Default => {
11899 self.advance();
11900 Ok(alloc::vec![
11901 crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11902 ])
11903 }
11904 Token::Not => {
11905 self.advance();
11906 if !matches!(self.peek(), Token::Null) {
11907 return Err(self.err(alloc::format!(
11908 "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11909 self.peek()
11910 )));
11911 }
11912 self.advance();
11913 Ok(alloc::vec![
11914 crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11915 ])
11916 }
11917 // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11918 // generated column into a plain column.
11919 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11920 self.advance();
11921 // v7.39 (round 187, U10) — IF EXISTS was consumed but
11922 // dropped, so the engine still errored on a plain
11923 // column; PG's semantics are NOTICE + skip.
11924 let mut if_exists = false;
11925 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11926 self.advance();
11927 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11928 self.advance();
11929 if_exists = true;
11930 }
11931 }
11932 Ok(alloc::vec![
11933 crate::ast::AlterTableTarget::AlterColumnDropExpression {
11934 column: col_name,
11935 if_exists,
11936 }
11937 ])
11938 }
11939 // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11940 // identity column into a plain column.
11941 Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11942 self.advance();
11943 let mut if_exists = false;
11944 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11945 self.advance();
11946 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11947 self.advance();
11948 if_exists = true;
11949 }
11950 }
11951 Ok(alloc::vec![
11952 crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11953 column: col_name,
11954 if_exists,
11955 }
11956 ])
11957 }
11958 _ => {
11959 self.consume_until_statement_boundary();
11960 Ok(Vec::new())
11961 }
11962 }
11963 }
11964
11965 /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11966 /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11967 /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11968 /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11969 fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11970 let mut opts = crate::ast::CopyOptions::default();
11971 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11972 return Ok(opts);
11973 }
11974 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11975 self.advance();
11976 }
11977 if matches!(self.peek(), Token::LParen) {
11978 self.advance();
11979 loop {
11980 self.parse_one_copy_option(&mut opts)?;
11981 match self.peek() {
11982 Token::Comma => {
11983 self.advance();
11984 }
11985 Token::RParen => {
11986 self.advance();
11987 break;
11988 }
11989 other => {
11990 return Err(self.err(alloc::format!(
11991 "expected ',' or ')' in COPY options, got {other:?}"
11992 )));
11993 }
11994 }
11995 }
11996 } else {
11997 while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11998 self.parse_one_copy_option(&mut opts)?;
11999 }
12000 }
12001 if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
12002 return Err(self.err(alloc::format!(
12003 "unexpected token after COPY options: {:?}",
12004 self.peek()
12005 )));
12006 }
12007 Ok(opts)
12008 }
12009
12010 fn parse_one_copy_option(
12011 &mut self,
12012 opts: &mut crate::ast::CopyOptions,
12013 ) -> Result<(), ParseError> {
12014 use crate::ast::CopyFormat;
12015 // The option keyword. NULL lexes as its own token; the rest are
12016 // bare identifiers.
12017 let kw = match self.advance() {
12018 Token::Null => alloc::string::String::from("NULL"),
12019 Token::Ident(s) => s.to_uppercase(),
12020 other => {
12021 return Err(self.err(alloc::format!(
12022 "expected a COPY option keyword, got {other:?}"
12023 )));
12024 }
12025 };
12026 match kw.as_str() {
12027 "FORMAT" => {
12028 let fmt = self.expect_ident_like()?;
12029 match fmt.to_ascii_uppercase().as_str() {
12030 "CSV" => opts.format = CopyFormat::Csv,
12031 "TEXT" => opts.format = CopyFormat::Text,
12032 other => {
12033 return Err(self.err(alloc::format!(
12034 "COPY format \"{}\" not recognized",
12035 other.to_ascii_lowercase()
12036 )));
12037 }
12038 }
12039 }
12040 // Legacy bare format keywords.
12041 "CSV" => opts.format = CopyFormat::Csv,
12042 "TEXT" => opts.format = CopyFormat::Text,
12043 "HEADER" => {
12044 opts.header = match self.peek() {
12045 Token::True => {
12046 self.advance();
12047 true
12048 }
12049 Token::False => {
12050 self.advance();
12051 false
12052 }
12053 Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
12054 self.advance();
12055 true
12056 }
12057 Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
12058 self.advance();
12059 false
12060 }
12061 // Bare HEADER (no boolean) means HEADER true.
12062 _ => true,
12063 };
12064 }
12065 // r1066 (7.38 S5.1) — pgbench 14+ loads with
12066 // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
12067 // vacuum bookkeeping on a freshly created/truncated
12068 // table; SPG's per-statement visibility makes it a
12069 // faithful no-op, and rejecting it aborted `pgbench -i`
12070 // against the drop-in. Accept ON/OFF/bare, change nothing.
12071 "FREEZE" => match self.peek() {
12072 Token::True | Token::False => {
12073 self.advance();
12074 }
12075 Token::Ident(s)
12076 if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
12077 {
12078 self.advance();
12079 }
12080 _ => {}
12081 },
12082 "DELIMITER" | "QUOTE" | "ESCAPE" => {
12083 let s = match self.advance() {
12084 Token::String(s) => s,
12085 other => {
12086 return Err(self.err(alloc::format!(
12087 "COPY {kw} expects a single-character string, got {other:?}"
12088 )));
12089 }
12090 };
12091 // v7.39 (round 247) — PG's wording (0A000), keyword in
12092 // lowercase: "COPY delimiter must be a single one-byte
12093 // character".
12094 let one_byte_err = || {
12095 self.err(alloc::format!(
12096 "COPY {} must be a single one-byte character",
12097 kw.to_ascii_lowercase()
12098 ))
12099 };
12100 let mut chars = s.chars();
12101 let c = chars.next().ok_or_else(one_byte_err)?;
12102 if chars.next().is_some() || c.len_utf8() != 1 {
12103 return Err(one_byte_err());
12104 }
12105 match kw.as_str() {
12106 "DELIMITER" => opts.delimiter = Some(c),
12107 "QUOTE" => opts.quote = Some(c),
12108 _ => opts.escape = Some(c),
12109 }
12110 }
12111 // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
12112 "FORCE_QUOTE" => {
12113 if matches!(self.peek(), Token::Star) {
12114 self.advance();
12115 opts.force_quote = Some(Vec::new());
12116 } else {
12117 if !matches!(self.peek(), Token::LParen) {
12118 return Err(self.err(alloc::format!(
12119 "expected '(' or '*' after FORCE_QUOTE, got {:?}",
12120 self.peek()
12121 )));
12122 }
12123 self.advance();
12124 let mut cols = Vec::new();
12125 loop {
12126 cols.push(self.expect_ident_like()?);
12127 match self.peek() {
12128 Token::Comma => {
12129 self.advance();
12130 }
12131 Token::RParen => {
12132 self.advance();
12133 break;
12134 }
12135 other => {
12136 return Err(self.err(alloc::format!(
12137 "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
12138 )));
12139 }
12140 }
12141 }
12142 opts.force_quote = Some(cols);
12143 }
12144 }
12145 "NULL" => {
12146 opts.null_str = Some(match self.advance() {
12147 Token::String(s) => s,
12148 other => {
12149 return Err(self.err(alloc::format!(
12150 "COPY NULL expects a quoted string, got {other:?}"
12151 )));
12152 }
12153 });
12154 }
12155 // v7.39 (round 265) — the two CSV FROM-side column lists. Same
12156 // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
12157 // FORCE_NULL too.
12158 "FORCE_NOT_NULL" | "FORCE_NULL" => {
12159 let cols = self.parse_copy_column_list(&kw)?;
12160 if kw == "FORCE_NOT_NULL" {
12161 opts.force_not_null = Some(cols);
12162 } else {
12163 opts.force_null = Some(cols);
12164 }
12165 }
12166 other => {
12167 // PG's wording, lowercased option name.
12168 return Err(self.err(alloc::format!(
12169 "option \"{}\" not recognized",
12170 other.to_ascii_lowercase()
12171 )));
12172 }
12173 }
12174 Ok(())
12175 }
12176
12177 /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
12178 /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
12179 /// is the `*` spelling.
12180 fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
12181 if matches!(self.peek(), Token::Star) {
12182 self.advance();
12183 return Ok(Vec::new());
12184 }
12185 if !matches!(self.peek(), Token::LParen) {
12186 return Err(self.err(alloc::format!(
12187 "expected '(' or '*' after {kw}, got {:?}",
12188 self.peek()
12189 )));
12190 }
12191 self.advance();
12192 let mut cols = Vec::new();
12193 loop {
12194 cols.push(self.expect_ident_like()?);
12195 match self.peek() {
12196 Token::Comma => {
12197 self.advance();
12198 }
12199 Token::RParen => {
12200 self.advance();
12201 break;
12202 }
12203 other => {
12204 return Err(self.err(alloc::format!(
12205 "expected ',' or ')' in {kw} list, got {other:?}"
12206 )));
12207 }
12208 }
12209 }
12210 Ok(cols)
12211 }
12212
12213 fn parse_partition_bounds_tail(
12214 &mut self,
12215 ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
12216 use crate::ast::PartitionOfBoundsAst;
12217 match self.peek() {
12218 Token::Default => {
12219 self.advance();
12220 Ok(PartitionOfBoundsAst::Default)
12221 }
12222 Token::For => {
12223 self.advance();
12224 if !matches!(self.peek(), Token::Values) {
12225 return Err(
12226 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
12227 );
12228 }
12229 self.advance();
12230 let want_with = matches!(
12231 self.peek(),
12232 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
12233 );
12234 if want_with {
12235 self.advance();
12236 if !matches!(self.peek(), Token::LParen) {
12237 return Err(self.err(format!(
12238 "expected '(' after FOR VALUES WITH, got {:?}",
12239 self.peek()
12240 )));
12241 }
12242 self.advance();
12243 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
12244 loop {
12245 let key = self.expect_ident_like()?;
12246 let n = match self.peek().clone() {
12247 Token::Integer(v) if u32::try_from(v).is_ok() => {
12248 self.advance();
12249 v as u32
12250 }
12251 other => {
12252 return Err(self.err(format!(
12253 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
12254 )));
12255 }
12256 };
12257 match key.to_ascii_uppercase().as_str() {
12258 "MODULUS" => modulus = Some(n),
12259 "REMAINDER" => remainder = Some(n),
12260 other => {
12261 return Err(self.err(format!(
12262 "FOR VALUES WITH: unknown key {other:?}; \
12263 expected MODULUS or REMAINDER"
12264 )));
12265 }
12266 }
12267 match self.peek() {
12268 Token::Comma => {
12269 self.advance();
12270 }
12271 Token::RParen => {
12272 self.advance();
12273 break;
12274 }
12275 other => {
12276 return Err(self.err(format!(
12277 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
12278 )));
12279 }
12280 }
12281 }
12282 let modulus = modulus
12283 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
12284 let remainder = remainder.ok_or_else(|| {
12285 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
12286 })?;
12287 if modulus == 0 {
12288 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
12289 }
12290 if remainder >= modulus {
12291 return Err(self.err(format!(
12292 "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
12293 )));
12294 }
12295 return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
12296 }
12297 match self.peek() {
12298 Token::From => {
12299 self.advance();
12300 let lower = Box::new(self.parse_partition_bound_expr()?);
12301 if !matches!(self.peek(), Token::To) {
12302 return Err(self.err(format!(
12303 "expected TO after FROM (...), got {:?}",
12304 self.peek()
12305 )));
12306 }
12307 self.advance();
12308 let upper = Box::new(self.parse_partition_bound_expr()?);
12309 Ok(PartitionOfBoundsAst::Range { lower, upper })
12310 }
12311 Token::In => {
12312 self.advance();
12313 if !matches!(self.peek(), Token::LParen) {
12314 return Err(self.err(format!(
12315 "expected '(' after FOR VALUES IN, got {:?}",
12316 self.peek()
12317 )));
12318 }
12319 self.advance();
12320 let mut values = Vec::new();
12321 loop {
12322 values.push(self.parse_expr(0)?);
12323 match self.peek() {
12324 Token::Comma => {
12325 self.advance();
12326 }
12327 Token::RParen => {
12328 self.advance();
12329 break;
12330 }
12331 other => {
12332 return Err(self.err(format!(
12333 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
12334 )));
12335 }
12336 }
12337 }
12338 if values.is_empty() {
12339 return Err(
12340 self.err("FOR VALUES IN requires at least one literal".to_string())
12341 );
12342 }
12343 Ok(PartitionOfBoundsAst::List { values })
12344 }
12345 other => Err(self.err(format!(
12346 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
12347 ))),
12348 }
12349 }
12350 other => Err(self.err(format!(
12351 "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
12352 ))),
12353 }
12354 }
12355
12356 /// v7.16.2 — peek for `information_schema.<tbl>` /
12357 /// `pg_catalog.<tbl>` triples and, if matched, consume all
12358 /// three tokens + return a synthetic table name the engine's
12359 /// SELECT path recognises as a virtual view. Returns `None`
12360 /// when the head doesn't look like a meta-qualified name.
12361 /// Used by `parse_table_ref` to bypass the
12362 /// `expect_ident_like` schema-strip for these specific PG
12363 /// meta schemas (mailrs round-10 A.3).
12364 fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12365 // Extract the schema name. Must be a plain ident token.
12366 let schema = match self.tokens.get(self.pos) {
12367 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12368 _ => return None,
12369 };
12370 // Dot.
12371 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12372 return None;
12373 }
12374 // The table-side ident may lex as a reserved keyword
12375 // (e.g. `Token::Tables`). Tolerate the common ones via a
12376 // helper that reads the trailing token's underlying name.
12377 let tbl = match self.tokens.get(self.pos + 2)? {
12378 Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12379 Token::Tables => "tables".to_string(),
12380 // Other PG meta table names that may collide with
12381 // reserved keywords land here as needed.
12382 _ => return None,
12383 };
12384 // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12385 // names so the synthetic name doesn't double-prefix
12386 // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12387 let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12388 ("__spg_info_", tbl.to_ascii_lowercase())
12389 } else if schema.eq_ignore_ascii_case("pg_catalog") {
12390 // v7.39 (round 541) — only the catalogs SPG actually
12391 // synthesises are rewritten, which is what the BARE path
12392 // has always checked. Anything else keeps its own name and
12393 // takes the ordinary route: `pg_stat_activity` and friends
12394 // resolve through meta_view_result, and a name that is no
12395 // catalog at all gets PG's "relation does not exist"
12396 // instead of a message about a view SPG cannot materialise.
12397 let lowered = tbl.to_ascii_lowercase();
12398 if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12399 self.advance(); // schema
12400 self.advance(); // dot
12401 self.advance(); // tbl
12402 return Some((lowered.clone(), lowered));
12403 }
12404 let bare = lowered
12405 .strip_prefix("pg_")
12406 .map(alloc::string::String::from)
12407 .unwrap_or(lowered);
12408 ("__spg_pg_", bare)
12409 } else if schema.eq_ignore_ascii_case("mysql") {
12410 // v7.17.0 Phase 3.P0-65 — MySQL system schema
12411 // (`mysql.user`, `mysql.db`). Same synthetic-name
12412 // shape as pg_catalog.
12413 ("__spg_mysql_", tbl.to_ascii_lowercase())
12414 } else {
12415 return None;
12416 };
12417 self.advance(); // schema
12418 self.advance(); // dot
12419 self.advance(); // tbl
12420 Some((
12421 alloc::format!("{prefix}{normalised}"),
12422 tbl.to_ascii_lowercase(),
12423 ))
12424 }
12425
12426 /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12427 /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12428 /// implicit front of every search_path, so a bare reference to a
12429 /// known catalog table always means the catalog table. Only the
12430 /// names the engine actually synthesises are recognised — any
12431 /// other `pg_*` ident stays a user table (mailrs embed round-12).
12432 fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12433 // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12434 // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12435 // `pg_catalog` at the front of every search_path. (pg_stat_activity
12436 // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12437 // through the meta_view_result path instead, and already resolve
12438 // bare — they must NOT be listed here or the __spg_ rewrite would
12439 // mis-target them.)
12440 const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12441 let name = match self.tokens.get(self.pos) {
12442 Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12443 _ => return None,
12444 };
12445 // A following dot means this ident is a schema qualifier,
12446 // not a table name — let the qualified path handle it.
12447 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12448 return None;
12449 }
12450 if !PG_META_TABLES.contains(&name.as_str()) {
12451 return None;
12452 }
12453 self.advance();
12454 let bare = name.strip_prefix("pg_").unwrap_or(&name);
12455 Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12456 }
12457
12458 /// Consume a bare ident if its lowercase matches `kw`, else err.
12459 /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12460 /// Peeks only; the caller advances.
12461 fn peek_keyword_ident(&self, kw: &str) -> bool {
12462 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12463 }
12464
12465 fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12466 match self.advance() {
12467 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12468 other => Err(ParseError {
12469 message: format!("expected {kw:?}, got {other:?}"),
12470 token_pos: self.consumed_pos(),
12471 }),
12472 }
12473 }
12474
12475 /// Accept either a quoted identifier (`"foo"`) or a quoted string
12476 /// literal (`'foo'`) — same shape used by CREATE USER for the
12477 /// username slot.
12478 fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12479 match self.advance() {
12480 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12481 other => Err(ParseError {
12482 message: format!("expected identifier or string, got {other:?}"),
12483 token_pos: self.consumed_pos(),
12484 }),
12485 }
12486 }
12487
12488 fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12489 match self.advance() {
12490 Token::String(s) => Ok(s),
12491 other => Err(ParseError {
12492 message: format!("expected quoted string, got {other:?}"),
12493 token_pos: self.consumed_pos(),
12494 }),
12495 }
12496 }
12497
12498 fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12499 // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12500 // subqueries recurse through here without passing
12501 // parse_expr; share the same nesting budget.
12502 self.enter_nested()?;
12503 let r = self.parse_select_stmt_inner();
12504 self.nest_depth -= 1;
12505 r
12506 }
12507
12508 fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12509 // Caller dispatches on Token::Select; the inner helper handles
12510 // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12511 // get a fresh bare-select parse and may not have their own ORDER
12512 // BY / LIMIT.
12513 let mut head = self.parse_bare_select()?;
12514 let into = self.pending_select_into.take();
12515 self.parse_setop_chain_into(&mut head)?;
12516 self.parse_select_tail_into(&mut head)?;
12517 // v7.38.19 — `SELECT … INTO t` lowers to the SAME node as
12518 // `CREATE TABLE t AS SELECT …`, which is what a comment in
12519 // `ast.rs` has claimed since v7.38 and what only CTAS actually
12520 // did. The tail (ORDER BY / LIMIT) is parsed first so it belongs
12521 // to the body, as it does in PostgreSQL.
12522 if let Some((name, temporary)) = into {
12523 return Ok(Statement::CreateMaterializedView(
12524 crate::ast::CreateMaterializedViewStatement {
12525 temporary,
12526 name,
12527 if_not_exists: false,
12528 columns: Vec::new(),
12529 body: head,
12530 with_data: true,
12531 as_plain_table: true,
12532 },
12533 ));
12534 }
12535 Ok(Statement::Select(head))
12536 }
12537
12538 /// v7.37.17 (17.6 siblings) — the three SQL set operations
12539 /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12540 /// token), and INTERSECT [ALL] (a bare ident — it was never
12541 /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12542 /// tighter than UNION / EXCEPT — the executor folds the chain
12543 /// left-to-right, which is already correct for LEADING
12544 /// intersects; an INTERSECT pair that FOLLOWS a union/except
12545 /// pair nests into that previous peer, so A UNION B INTERSECT C
12546 /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12547 /// groups.
12548 fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12549 // A parenthesized group arrives with its own (already
12550 // regrouped) unions on `head`; only the pairs THIS chain
12551 // appends participate in the precedence regroup below —
12552 // nesting an outer INTERSECT into a group-internal peer
12553 // would dissolve the explicit grouping.
12554 let boundary = head.unions.len();
12555 loop {
12556 let base = match self.peek() {
12557 Token::Union => UnionKind::Distinct,
12558 Token::Except => UnionKind::Except,
12559 Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12560 _ => break,
12561 };
12562 self.advance();
12563 let kind = if matches!(self.peek(), Token::All) {
12564 self.advance();
12565 match base {
12566 UnionKind::Distinct => UnionKind::All,
12567 UnionKind::Except => UnionKind::ExceptAll,
12568 _ => UnionKind::IntersectAll,
12569 }
12570 } else {
12571 base
12572 };
12573 let peer = self.parse_bare_select()?;
12574 head.unions.push((kind, peer));
12575 }
12576 let mut pairs = core::mem::take(&mut head.unions);
12577 let tail = pairs.split_off(boundary);
12578 let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12579 for (kind, peer) in tail {
12580 let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12581 // An intersect nests into the previous element of THIS
12582 // chain only; with no new previous element it stays at
12583 // the outer level (the left fold applies it to the
12584 // whole head, group included).
12585 match (
12586 is_intersect,
12587 regrouped.len() > boundary,
12588 regrouped.last_mut(),
12589 ) {
12590 (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12591 _ => regrouped.push((kind, peer)),
12592 }
12593 }
12594 head.unions = regrouped;
12595 Ok(())
12596 }
12597
12598 /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12599 /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12600 /// the top-level bare VALUES statement reuses it verbatim.
12601 /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12602 /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12603 /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12604 /// where the grouping-set universe is still in scope.
12605 fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12606 if !matches!(self.peek(), Token::Order) {
12607 return Ok(Vec::new());
12608 }
12609 self.advance();
12610 if !self.peek_is_by() {
12611 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12612 }
12613 self.advance();
12614 let mut keys = Vec::new();
12615 loop {
12616 // v7.39 (round 691) — save/restore, the discipline this parser
12617 // already uses around `pending_sample_preds`, so a subquery inside
12618 // a key neither inherits nor leaks the channel.
12619 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12620 let saved_coll = self.order_key_collation.take();
12621 let parsed = self.parse_expr(0);
12622 self.in_order_by_key = saved_flag;
12623 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12624 let expr = parsed?;
12625 let desc = if matches!(self.peek(), Token::Desc) {
12626 self.advance();
12627 true
12628 } else if matches!(self.peek(), Token::Asc) {
12629 self.advance();
12630 false
12631 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12632 // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12633 // one ordering per type, so the btree comparison operators map
12634 // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12635 // would need a custom operator class — honest error.
12636 self.advance();
12637 match self.advance() {
12638 Token::Lt | Token::LtEq => false,
12639 Token::Gt | Token::GtEq => true,
12640 other => {
12641 return Err(self.err(alloc::format!(
12642 "ORDER BY USING supports the btree comparison \
12643 operators (< <= > >=); got {other:?}"
12644 )));
12645 }
12646 }
12647 } else {
12648 false
12649 };
12650 // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12651 let nulls_first = self.parse_optional_nulls_placement()?;
12652 keys.push(OrderBy {
12653 expr,
12654 desc,
12655 nulls_first,
12656 collation,
12657 });
12658 if matches!(self.peek(), Token::Comma) {
12659 self.advance();
12660 } else {
12661 break;
12662 }
12663 }
12664 Ok(keys)
12665 }
12666
12667 fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12668 // v7.39 (round 135) — a grouping-set query may have already parsed +
12669 // rewritten its ORDER BY (to reference synthetic grouping columns); if
12670 // no ORDER BY token is present, keep that pre-set order_by rather than
12671 // clobbering it with an empty list.
12672 let parsed_keys = self.parse_order_by_keys()?;
12673 head.order_by = if parsed_keys.is_empty() {
12674 core::mem::take(&mut head.order_by)
12675 } else {
12676 parsed_keys
12677 };
12678 // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12679 // order. PG's grammar takes a limit clause and an offset clause
12680 // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12681 // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12682 // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12683 // spelling died on `expected end of input, got Limit`.
12684 //
12685 // Each may appear at most once, and LIMIT and FETCH FIRST are
12686 // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12687 // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12688 // A second one is left unconsumed here, which the caller reports
12689 // as trailing input rather than silently taking the last.
12690 let mut saw_limit = false;
12691 let mut saw_offset = false;
12692 loop {
12693 if !saw_limit && matches!(self.peek(), Token::Limit) {
12694 self.advance();
12695 // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12696 // PG synonyms for "no limit". Treat both as None
12697 // (no head.limit set) so the engine's existing
12698 // unlimited-result path takes over. Reject was the
12699 // pre-5.1 behaviour and broke pg_dump-flavoured
12700 // tooling that occasionally emits LIMIT NULL.
12701 if self.consume_limit_unbounded_sentinel() {
12702 head.limit = None;
12703 } else {
12704 let first = self.parse_limit_expr("LIMIT")?;
12705 // MySQL `LIMIT offset, count` — the first number is
12706 // the offset when a comma follows.
12707 if matches!(self.peek(), Token::Comma) {
12708 self.advance();
12709 let count = self.parse_limit_expr("LIMIT")?;
12710 head.offset = Some(first);
12711 saw_offset = true;
12712 head.limit = Some(count);
12713 } else {
12714 head.limit = Some(first);
12715 }
12716 }
12717 saw_limit = true;
12718 continue;
12719 }
12720 if !saw_offset && matches!(self.peek(), Token::Offset) {
12721 self.advance();
12722 // PG also accepts an optional `ROW` / `ROWS` trailer
12723 // after the offset value (`OFFSET 10 ROWS`). The
12724 // FETCH-FIRST branch below relies on the same.
12725 let off = self.parse_limit_expr("OFFSET")?;
12726 self.consume_optional_rows_keyword();
12727 head.offset = Some(off);
12728 saw_offset = true;
12729 continue;
12730 }
12731 // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12732 // the SQL-standard alias for LIMIT. PG accepts both
12733 // spellings interchangeably; pg_dump emits FETCH FIRST in
12734 // newer versions. We map it onto `head.limit` so the
12735 // engine path is unified.
12736 if !saw_limit
12737 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12738 if s.eq_ignore_ascii_case("fetch"))
12739 {
12740 self.advance(); // FETCH
12741 // `FIRST` or `NEXT` (both legal per SQL standard).
12742 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12743 if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12744 {
12745 self.advance();
12746 }
12747 // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12748 // implicit 1 — but we always consume one if present).
12749 let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12750 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12751 {
12752 // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12753 crate::ast::LimitExpr::Literal(1)
12754 } else {
12755 self.parse_limit_expr("FETCH FIRST")?
12756 };
12757 // Eat `ROW` / `ROWS` if not already consumed above.
12758 self.consume_optional_rows_keyword();
12759 // Optional `ONLY` (the spec form) — or the SQL:2008
12760 // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12761 // now honours WITH TIES by extending past the LIMIT
12762 // truncation point through every row that shares the
12763 // last-kept row's ORDER BY key.
12764 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12765 if s.eq_ignore_ascii_case("only"))
12766 {
12767 self.advance();
12768 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12769 if s.eq_ignore_ascii_case("with"))
12770 {
12771 self.advance(); // WITH
12772 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12773 if s.eq_ignore_ascii_case("ties"))
12774 {
12775 self.advance();
12776 head.limit_with_ties = true;
12777 }
12778 }
12779 head.limit = Some(count);
12780 saw_limit = true;
12781 continue;
12782 }
12783 break;
12784 }
12785 // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12786 // FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12787 // [ OF table_name [, …] ]
12788 // [ NOWAIT | SKIP LOCKED ]
12789 // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12790 // FOR SHARE OF t2`). SPG is a single-writer engine — every
12791 // SELECT already returns a consistent snapshot — so these
12792 // are accept-and-discard: the parser absorbs them so
12793 // mailrs / Rails / Django code paths that emit `SELECT
12794 // … FOR UPDATE` for advisory pessimistic locking load
12795 // without a parser error. The on-disk locking model is
12796 // unchanged; callers that rely on FOR UPDATE for read-
12797 // through-write ordering still get the right answer
12798 // because SPG serialises writes anyway.
12799 head.locking = self
12800 .consume_optional_for_lock_clauses()
12801 .map(alloc::boxed::Box::new);
12802 Ok(())
12803 }
12804
12805 /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12806 /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12807 /// LOCKED ]` trailers. Each clause is fully accepted and
12808 /// discarded — SPG's single-writer model already satisfies the
12809 /// callers' implicit ordering requirement. Stops at the first
12810 /// token that isn't `FOR`.
12811 fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12812 // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12813 // not discarded. PG keeps the strongest of several clauses; the
12814 // policy of the last one wins, which is what this loop records.
12815 let mut seen: Option<crate::ast::LockingClause> = None;
12816 while matches!(self.peek(), Token::For) {
12817 // v7.37.14 (A2.5-stub) — record that this query asked
12818 // for a row lock the parser is about to silently
12819 // discard. Operators surface the count via
12820 // `spg_sql::silent_for_update_count()` so they can
12821 // gauge how much of the workload depends on advisory
12822 // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12823 // before v7.37.15's per-row tuple locking lands.
12824 crate::record_silent_for_update_clause();
12825 self.advance(); // FOR
12826 // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12827 // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12828 let mut no_key = false;
12829 let mut key = false;
12830 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12831 if s.eq_ignore_ascii_case("no"))
12832 {
12833 self.advance(); // NO
12834 no_key = true;
12835 // The next ident should be KEY but be generous;
12836 // anything followed by UPDATE/SHARE is accepted.
12837 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12838 if s.eq_ignore_ascii_case("key"))
12839 {
12840 self.advance(); // KEY
12841 }
12842 }
12843 // `KEY` prefix (PG `FOR KEY SHARE`).
12844 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12845 if s.eq_ignore_ascii_case("key"))
12846 {
12847 self.advance(); // KEY
12848 key = true;
12849 }
12850 // Lock-strength keyword: UPDATE / SHARE. Required, but
12851 // we're lenient — an unexpected token here just bails
12852 // (we already consumed FOR; caller's downstream
12853 // dispatch will error if anything actually depends on
12854 // the trailing tokens).
12855 let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12856 if s.eq_ignore_ascii_case("update"));
12857 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12858 if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12859 {
12860 self.advance();
12861 use crate::ast::LockStrength as LS;
12862 let strength = match (is_update, no_key, key) {
12863 (true, true, _) => LS::NoKeyUpdate,
12864 (true, _, _) => LS::Update,
12865 (false, _, true) => LS::KeyShare,
12866 (false, _, _) => LS::Share,
12867 };
12868 seen = Some(crate::ast::LockingClause {
12869 strength,
12870 of_tables: alloc::vec::Vec::new(),
12871 policy: crate::ast::LockWait::Wait,
12872 });
12873 } else {
12874 // FOR by itself (or `FOR KEY` with nothing after) —
12875 // give up on the lock-clause path. We've already
12876 // advanced past FOR; further attempts to parse
12877 // here would clobber state.
12878 return seen;
12879 }
12880 // Optional `OF tbl[, tbl …]`. mailrs emits this when
12881 // joining and locking only a subset of tables.
12882 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12883 if s.eq_ignore_ascii_case("of"))
12884 {
12885 self.advance(); // OF
12886 #[allow(clippy::while_let_loop)]
12887 loop {
12888 match self.peek() {
12889 Token::Ident(_) | Token::QuotedIdent(_) => {
12890 // v7.39 (round 294) — the name is CAPTURED now: PG
12891 // validates it against the FROM clause, and an
12892 // uncaptured list silently means "lock everything".
12893 let mut nm = match self.advance() {
12894 Token::Ident(n) | Token::QuotedIdent(n) => n,
12895 _ => alloc::string::String::new(),
12896 };
12897 // Optional schema-qualified `schema.table`.
12898 if matches!(self.peek(), Token::Dot) {
12899 self.advance();
12900 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12901 {
12902 self.advance();
12903 nm = n;
12904 }
12905 }
12906 if let Some(c) = seen.as_mut() {
12907 c.of_tables.push(nm);
12908 }
12909 }
12910 _ => break,
12911 }
12912 if matches!(self.peek(), Token::Comma) {
12913 self.advance();
12914 } else {
12915 break;
12916 }
12917 }
12918 }
12919 // Optional `NOWAIT` | `SKIP LOCKED`.
12920 match self.peek().clone() {
12921 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12922 self.advance();
12923 if let Some(c) = seen.as_mut() {
12924 c.policy = crate::ast::LockWait::NoWait;
12925 }
12926 }
12927 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12928 self.advance(); // SKIP
12929 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12930 if s.eq_ignore_ascii_case("locked"))
12931 {
12932 self.advance(); // LOCKED
12933 if let Some(c) = seen.as_mut() {
12934 c.policy = crate::ast::LockWait::SkipLocked;
12935 }
12936 }
12937 }
12938 _ => {}
12939 }
12940 // Loop: PG allows multiple FOR clauses chained.
12941 }
12942 seen
12943 }
12944
12945 /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12946 /// Bind value gets resolved during prepared-statement Execute;
12947 /// the Pratt expression parser would over-accept here (e.g.
12948 /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12949 /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12950 /// sentinel tokens (PG synonyms for "no limit"). Returns true
12951 /// when one was consumed; caller skips the regular
12952 /// limit-value parse and leaves `head.limit` at None.
12953 fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12954 if matches!(self.peek(), Token::Null) {
12955 self.advance();
12956 return true;
12957 }
12958 if matches!(self.peek(), Token::All) {
12959 self.advance();
12960 return true;
12961 }
12962 false
12963 }
12964
12965 /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12966 /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12967 /// SQL-standard shape. No-op when missing.
12968 fn consume_optional_rows_keyword(&mut self) {
12969 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12970 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12971 {
12972 self.advance();
12973 }
12974 }
12975
12976 /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12977 ///
12978 /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12979 /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12980 /// constant, which is why that spelling keeps the token path below.
12981 ///
12982 /// Constants are folded here rather than carried into the tree: the
12983 /// 15+ execution paths that read the row count go through
12984 /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12985 /// means "no limit". A clause the engine could not resolve would
12986 /// therefore return the WHOLE table instead of failing. Folding at
12987 /// parse time keeps that impossible; a non-constant clause is still
12988 /// a clean error (recorded residual — closing it wants a resolution
12989 /// pre-pass on the simple-query path, where `substitute_placeholders`
12990 /// does not run).
12991 fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12992 // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12993 // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12994 // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12995 // ONLY` both work (its grammar takes a c_expr). Both measured
12996 // against PG 18.4 in round 305.
12997 if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12998 return self.parse_limit_constant(label);
12999 }
13000 // One pass, no rewind: `advance()` takes each token by
13001 // `mem::replace`, so a consumed token reads back as Eof and this
13002 // parser cannot backtrack. Everything — bare literal included —
13003 // is therefore folded from the parsed expression rather than
13004 // re-read from the token stream.
13005 let start = self.pos;
13006 let e = self.parse_expr(0)?;
13007 if let crate::ast::Expr::Placeholder(n) = e {
13008 return Ok(crate::ast::LimitExpr::Placeholder(n));
13009 }
13010 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
13011 match fold_limit_constant(&e) {
13012 Some(Ok(v)) if v < 0 => Err(ParseError {
13013 message: alloc::format!("{neg_label} must not be negative"),
13014 token_pos: start,
13015 }),
13016 Some(Ok(v)) => u32::try_from(v)
13017 .map(crate::ast::LimitExpr::Literal)
13018 .map_err(|_| ParseError {
13019 message: alloc::format!("{label} value too large: {v}"),
13020 token_pos: start,
13021 }),
13022 Some(Err(message)) => Err(ParseError {
13023 message: message.replace("{L}", neg_label),
13024 token_pos: start,
13025 }),
13026 // v7.39 (round 305, V23) — not foldable at parse time
13027 // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
13028 // expression; the engine evaluates it once before dispatch.
13029 None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
13030 }
13031 }
13032
13033 fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
13034 // v7.39 (round 239) — PG's row-count clause takes a bigint with its
13035 // coercion rules, not just an integer token: a NUMERIC rounds half
13036 // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
13037 // refused with PG's wording ("LIMIT must not be negative", 2201W /
13038 // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
13039 // content, failing as an input-syntax error on the value. General
13040 // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
13041 // they need an Expr-carrying LimitExpr variant.
13042 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
13043 let err_at = |message: alloc::string::String, pos: usize| ParseError {
13044 message,
13045 token_pos: pos,
13046 };
13047 match self.advance() {
13048 Token::Integer(n) if n >= 0 => u32::try_from(n)
13049 .map(crate::ast::LimitExpr::Literal)
13050 .map_err(|_| ParseError {
13051 message: alloc::format!("{label} value too large: {n}"),
13052 token_pos: self.consumed_pos(),
13053 }),
13054 Token::Integer(_) => Err(err_at(
13055 alloc::format!("{neg_label} must not be negative"),
13056 self.pos.saturating_sub(1),
13057 )),
13058 Token::Numeric(t) => {
13059 let pos = self.pos.saturating_sub(1);
13060 let v: f64 = t.parse().map_err(|_| {
13061 err_at(
13062 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13063 pos,
13064 )
13065 })?;
13066 if v < 0.0 {
13067 return Err(err_at(
13068 alloc::format!("{neg_label} must not be negative"),
13069 pos,
13070 ));
13071 }
13072 // Round half away from zero — PG's numeric→bigint cast.
13073 // (no_std: no f64::round; v is non-negative, so truncating
13074 // v + 0.5 is the same thing.)
13075 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
13076 let rounded = (v + 0.5) as u64;
13077 u32::try_from(rounded)
13078 .map(crate::ast::LimitExpr::Literal)
13079 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
13080 }
13081 Token::Minus => {
13082 let pos = self.pos.saturating_sub(1);
13083 match self.peek() {
13084 Token::Integer(_) | Token::Numeric(_) => {
13085 self.advance();
13086 Err(err_at(
13087 alloc::format!("{neg_label} must not be negative"),
13088 pos,
13089 ))
13090 }
13091 other => Err(err_at(
13092 alloc::format!(
13093 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13094 ),
13095 pos,
13096 )),
13097 }
13098 }
13099 Token::String(t) => {
13100 let pos = self.pos.saturating_sub(1);
13101 match t.trim().parse::<i64>() {
13102 Ok(n) if n < 0 => Err(err_at(
13103 alloc::format!("{neg_label} must not be negative"),
13104 pos,
13105 )),
13106 Ok(n) => u32::try_from(n)
13107 .map(crate::ast::LimitExpr::Literal)
13108 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
13109 Err(_) => Err(err_at(
13110 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13111 pos,
13112 )),
13113 }
13114 }
13115 Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
13116 other => Err(ParseError {
13117 message: alloc::format!(
13118 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13119 ),
13120 token_pos: self.consumed_pos(),
13121 }),
13122 }
13123 }
13124
13125 /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
13126 /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
13127 /// `unions` empty and `order_by` / `limit` `None`; the top-level
13128 /// `parse_select_stmt` is responsible for filling those in.
13129 /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
13130 /// call in the expression tree to the per-set integer bitmask
13131 /// (PG semantics: one bit per argument, MSB first; 1 = the key
13132 /// is dropped in this grouping set). Runs during the ROLLUP /
13133 /// CUBE / GROUPING SETS expansion, where the set is known.
13134 /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
13135 /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
13136 fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
13137 if let Expr::FunctionCall { name, .. } = expr
13138 && name.eq_ignore_ascii_case("grouping")
13139 {
13140 if !out.iter().any(|e| e == expr) {
13141 out.push(expr.clone());
13142 }
13143 return;
13144 }
13145 match expr {
13146 Expr::Binary { lhs, rhs, .. } => {
13147 Self::collect_grouping_calls(lhs, out);
13148 Self::collect_grouping_calls(rhs, out);
13149 }
13150 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13151 Self::collect_grouping_calls(expr, out)
13152 }
13153 Expr::FunctionCall { args, .. } => {
13154 for a in args {
13155 Self::collect_grouping_calls(a, out);
13156 }
13157 }
13158 Expr::Case {
13159 operand,
13160 branches,
13161 else_branch,
13162 } => {
13163 if let Some(o) = operand {
13164 Self::collect_grouping_calls(o, out);
13165 }
13166 for (c, v) in branches {
13167 Self::collect_grouping_calls(c, out);
13168 Self::collect_grouping_calls(v, out);
13169 }
13170 if let Some(x) = else_branch {
13171 Self::collect_grouping_calls(x, out);
13172 }
13173 }
13174 _ => {}
13175 }
13176 }
13177
13178 /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
13179 /// `grp_exprs[k]` with a reference to the synthetic ordering column
13180 /// `__grp_ord_k` (injected per grouping-set branch).
13181 fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
13182 if let Expr::FunctionCall { name, .. } = expr
13183 && name.eq_ignore_ascii_case("grouping")
13184 {
13185 if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
13186 *expr = Expr::Column(crate::ast::ColumnName {
13187 qualifier: None,
13188 name: alloc::format!("__grp_ord_{k}"),
13189 });
13190 }
13191 return;
13192 }
13193 match expr {
13194 Expr::Binary { lhs, rhs, .. } => {
13195 Self::rewrite_grouping_to_col(lhs, grp_exprs);
13196 Self::rewrite_grouping_to_col(rhs, grp_exprs);
13197 }
13198 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13199 Self::rewrite_grouping_to_col(expr, grp_exprs)
13200 }
13201 Expr::FunctionCall { args, .. } => {
13202 for a in args {
13203 Self::rewrite_grouping_to_col(a, grp_exprs);
13204 }
13205 }
13206 Expr::Case {
13207 operand,
13208 branches,
13209 else_branch,
13210 } => {
13211 if let Some(o) = operand {
13212 Self::rewrite_grouping_to_col(o, grp_exprs);
13213 }
13214 for (c, v) in branches {
13215 Self::rewrite_grouping_to_col(c, grp_exprs);
13216 Self::rewrite_grouping_to_col(v, grp_exprs);
13217 }
13218 if let Some(x) = else_branch {
13219 Self::rewrite_grouping_to_col(x, grp_exprs);
13220 }
13221 }
13222 _ => {}
13223 }
13224 }
13225
13226 /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
13227 /// as the list of key sets it contributes. A bare expression is one
13228 /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
13229 /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
13230 /// the concatenation of its items' sets, where an item is itself an
13231 /// element, a parenthesized key list, or the empty set `()`. A
13232 /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
13233 /// move together.
13234 fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
13235 let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
13236 // ROLLUP ( … ) / CUBE ( … )
13237 if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
13238 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
13239 {
13240 let is_cube = is_kw(self.peek(), "cube");
13241 self.advance(); // ROLLUP / CUBE
13242 self.advance(); // (
13243 let mut units: Vec<Vec<Expr>> = Vec::new();
13244 loop {
13245 if matches!(self.peek(), Token::LParen) {
13246 // Composite unit: (a, b) rolls up as one.
13247 self.advance();
13248 let mut unit = Vec::new();
13249 if !matches!(self.peek(), Token::RParen) {
13250 loop {
13251 unit.push(self.parse_expr(0)?);
13252 match self.peek() {
13253 Token::Comma => {
13254 self.advance();
13255 }
13256 Token::RParen => break,
13257 other => {
13258 return Err(self.err(format!(
13259 "expected ',' or ')' in grouping unit, got {other:?}"
13260 )));
13261 }
13262 }
13263 }
13264 }
13265 self.advance(); // )
13266 units.push(unit);
13267 } else {
13268 units.push(alloc::vec![self.parse_expr(0)?]);
13269 }
13270 match self.peek() {
13271 Token::Comma => {
13272 self.advance();
13273 }
13274 Token::RParen => break,
13275 other => {
13276 return Err(self.err(format!(
13277 "expected ',' or ')' in grouping list, got {other:?}"
13278 )));
13279 }
13280 }
13281 }
13282 self.advance(); // )
13283 let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
13284 units
13285 .iter()
13286 .zip(unit_sel.iter())
13287 .filter(|(_, keep)| **keep)
13288 .flat_map(|(u, _)| u.iter().cloned())
13289 .collect()
13290 };
13291 let n = units.len();
13292 if is_cube {
13293 let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
13294 .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
13295 .collect();
13296 subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
13297 return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
13298 }
13299 return Ok((0..=n)
13300 .rev()
13301 .map(|keep| {
13302 let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
13303 flatten(&sel)
13304 })
13305 .collect());
13306 }
13307 // GROUPING SETS ( item [, item]* )
13308 if is_kw(self.peek(), "grouping")
13309 && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
13310 {
13311 self.advance(); // GROUPING
13312 self.advance(); // SETS
13313 if !matches!(self.peek(), Token::LParen) {
13314 return Err(self.err(format!(
13315 "expected '(' after GROUPING SETS, got {:?}",
13316 self.peek()
13317 )));
13318 }
13319 self.advance(); // outer (
13320 let mut sets: Vec<Vec<Expr>> = Vec::new();
13321 loop {
13322 if matches!(self.peek(), Token::LParen) {
13323 // A parenthesized key list (or the empty set).
13324 self.advance();
13325 let mut set = Vec::new();
13326 if !matches!(self.peek(), Token::RParen) {
13327 loop {
13328 set.push(self.parse_expr(0)?);
13329 match self.peek() {
13330 Token::Comma => {
13331 self.advance();
13332 }
13333 Token::RParen => break,
13334 other => {
13335 return Err(self.err(format!(
13336 "expected ',' or ')' in grouping set, got {other:?}"
13337 )));
13338 }
13339 }
13340 }
13341 }
13342 self.advance(); // )
13343 sets.push(set);
13344 } else {
13345 // A nested element: ROLLUP/CUBE/GROUPING SETS or a
13346 // bare expression.
13347 sets.extend(self.parse_grouping_element()?);
13348 }
13349 match self.peek() {
13350 Token::Comma => {
13351 self.advance();
13352 }
13353 Token::RParen => break,
13354 other => {
13355 return Err(self.err(format!(
13356 "expected ',' or ')' after a grouping set, got {other:?}"
13357 )));
13358 }
13359 }
13360 }
13361 self.advance(); // outer )
13362 return Ok(sets);
13363 }
13364 Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
13365 }
13366
13367 fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
13368 // v7.38 (read01) — a reference to a key that is dropped in this grouping
13369 // set evaluates to NULL, at any depth. Previously only a *top-level*
13370 // select item equal to a dropped key was nullified, so a key nested in
13371 // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13372 // column and failed to resolve against the set's synthetic schema.
13373 if dropped.iter().any(|d| d == expr) {
13374 *expr = Expr::Literal(Literal::Null);
13375 return;
13376 }
13377 if let Expr::FunctionCall { name, args } = expr
13378 && name.eq_ignore_ascii_case("grouping")
13379 {
13380 let mut mask: i64 = 0;
13381 for a in args.iter() {
13382 mask <<= 1;
13383 if dropped.iter().any(|d| d == a) {
13384 mask |= 1;
13385 }
13386 }
13387 // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13388 // literal: a bare integer in a select item is indistinguishable
13389 // from a positional reference once `ORDER BY 1` substitutes the
13390 // item back in, and the round-232 position check then read the
13391 // mask value as an out-of-range position. The cast changes
13392 // nothing semantically (grouping() is integer).
13393 *expr = Expr::Cast {
13394 expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13395 target: crate::ast::CastTarget::Int,
13396 };
13397 return;
13398 }
13399 // Generic recursion over the common expression shapes the
13400 // SELECT list uses; anything without child expressions is
13401 // left alone.
13402 match expr {
13403 Expr::FunctionCall { args, .. } => {
13404 for a in args {
13405 Self::substitute_grouping_calls(a, dropped);
13406 }
13407 }
13408 Expr::Binary { lhs, rhs, .. } => {
13409 Self::substitute_grouping_calls(lhs, dropped);
13410 Self::substitute_grouping_calls(rhs, dropped);
13411 }
13412 Expr::Unary { expr: inner, .. } => {
13413 Self::substitute_grouping_calls(inner, dropped);
13414 }
13415 Expr::Cast { expr: inner, .. } => {
13416 Self::substitute_grouping_calls(inner, dropped);
13417 }
13418 Expr::Case {
13419 operand,
13420 branches,
13421 else_branch,
13422 } => {
13423 if let Some(op) = operand {
13424 Self::substitute_grouping_calls(op, dropped);
13425 }
13426 for (w, t) in branches {
13427 Self::substitute_grouping_calls(w, dropped);
13428 Self::substitute_grouping_calls(t, dropped);
13429 }
13430 if let Some(e) = else_branch {
13431 Self::substitute_grouping_calls(e, dropped);
13432 }
13433 }
13434 // v7.38 (read01) — recurse into the remaining child-bearing shapes
13435 // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13436 // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13437 // …` is the canonical rollup-total label idiom).
13438 Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13439 Expr::Like { expr, pattern, .. } => {
13440 Self::substitute_grouping_calls(expr, dropped);
13441 Self::substitute_grouping_calls(pattern, dropped);
13442 }
13443 Expr::InList { expr, list, .. } => {
13444 Self::substitute_grouping_calls(expr, dropped);
13445 for item in list {
13446 Self::substitute_grouping_calls(item, dropped);
13447 }
13448 }
13449 Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13450 Expr::Array(items) => {
13451 for item in items {
13452 Self::substitute_grouping_calls(item, dropped);
13453 }
13454 }
13455 Expr::ArraySubscript { target, index } => {
13456 Self::substitute_grouping_calls(target, dropped);
13457 Self::substitute_grouping_calls(index, dropped);
13458 }
13459 Expr::ArraySlice { target, lo, hi } => {
13460 Self::substitute_grouping_calls(target, dropped);
13461 if let Some(lo) = lo {
13462 Self::substitute_grouping_calls(lo, dropped);
13463 }
13464 if let Some(hi) = hi {
13465 Self::substitute_grouping_calls(hi, dropped);
13466 }
13467 }
13468 Expr::AnyAll { expr, array, .. } => {
13469 Self::substitute_grouping_calls(expr, dropped);
13470 Self::substitute_grouping_calls(array, dropped);
13471 }
13472 _ => {}
13473 }
13474 }
13475
13476 fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13477 // v7.37.17 (17.6 siblings) — parenthesized set-operation
13478 // group: `( <select chain> )` usable anywhere a query block
13479 // is (head or peer of an outer chain). The group's own
13480 // unions ride the returned SelectStatement; the executor's
13481 // nested-peer recursion runs them.
13482 if matches!(self.peek(), Token::LParen)
13483 && matches!(
13484 self.tokens.get(self.pos + 1),
13485 Some(Token::Select | Token::LParen | Token::Values)
13486 )
13487 {
13488 self.advance(); // (
13489 self.enter_nested()?;
13490 // v7.37 D.20 — a group whose head is a VALUES list:
13491 // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13492 // otherwise recurse into a nested SELECT/group head.
13493 let mut head = (if matches!(self.peek(), Token::Values) {
13494 self.advance(); // VALUES
13495 self.parse_values_rows_body()
13496 } else {
13497 self.parse_bare_select()
13498 })
13499 .and_then(|mut h| {
13500 self.parse_setop_chain_into(&mut h)?;
13501 Ok(h)
13502 });
13503 self.nest_depth -= 1;
13504 let mut head = match &mut head {
13505 Ok(h) => core::mem::take(h),
13506 Err(_) => return head,
13507 };
13508 // v7.37.17 (17.6 siblings) — group-internal tail:
13509 // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13510 // group head, then wrap the group as a derived table
13511 // (SELECT * FROM (group)) so the outer chain / outer
13512 // tail can't clobber the group's own ordering or limit.
13513 let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13514 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13515 if s.eq_ignore_ascii_case("fetch"));
13516 if has_tail {
13517 self.parse_select_tail_into(&mut head)?;
13518 head = SelectStatement {
13519 locking: None,
13520 ctes: Vec::new(),
13521 distinct: false,
13522 distinct_on: Vec::new(),
13523 items: alloc::vec![SelectItem::Wildcard],
13524 from: Some(FromClause {
13525 primary: TableRef {
13526 name: "subquery".to_string(),
13527 alias: None,
13528 only: false,
13529 as_of_segment: None,
13530 unnest_expr: None,
13531 unnest_column_aliases: Vec::new(),
13532 with_ordinality: false,
13533 generate_series_args: None,
13534 lateral_subquery: Some(Box::new(head)),
13535 jsonb_each_text_arg: None,
13536 table_fn_call: None,
13537 rows_from: None,
13538 json_table: None,
13539 scalar_fn_item: false,
13540 },
13541 joins: Vec::new(),
13542 }),
13543 where_: None,
13544 group_by: None,
13545 group_by_all: false,
13546 having: None,
13547 unions: Vec::new(),
13548 order_by: Vec::new(),
13549 limit: None,
13550 offset: None,
13551 limit_with_ties: false,
13552 window_check_exprs: Vec::new(),
13553 };
13554 }
13555 if !matches!(self.peek(), Token::RParen) {
13556 return Err(self.err(format!(
13557 "expected ')' after parenthesized query group, got {:?}",
13558 self.peek()
13559 )));
13560 }
13561 self.advance();
13562 return Ok(head);
13563 }
13564 // `TABLE name` shorthand as a query block — valid anywhere
13565 // a SELECT head is (set-op peers included).
13566 if matches!(self.peek(), Token::Table)
13567 && matches!(
13568 self.tokens.get(self.pos + 1),
13569 Some(Token::Ident(_) | Token::QuotedIdent(_))
13570 )
13571 {
13572 return self.parse_table_shorthand();
13573 }
13574 if !matches!(self.peek(), Token::Select) {
13575 return Err(self.err(format!(
13576 "expected SELECT to start a query block, got {:?}",
13577 self.peek()
13578 )));
13579 }
13580 self.advance();
13581 let distinct = if matches!(self.peek(), Token::Distinct) {
13582 self.advance();
13583 true
13584 } else {
13585 false
13586 };
13587 // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13588 // keep the first row (per ORDER BY) of each group the
13589 // expressions define. Django's .distinct('field') shape.
13590 let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13591 self.advance(); // ON
13592 if !matches!(self.peek(), Token::LParen) {
13593 return Err(self.err(format!(
13594 "expected '(' after DISTINCT ON, got {:?}",
13595 self.peek()
13596 )));
13597 }
13598 self.advance();
13599 let mut exprs = Vec::new();
13600 loop {
13601 exprs.push(self.parse_expr(0)?);
13602 match self.peek() {
13603 Token::Comma => {
13604 self.advance();
13605 }
13606 Token::RParen => break,
13607 other => {
13608 return Err(self.err(format!(
13609 "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13610 )));
13611 }
13612 }
13613 }
13614 self.advance(); // )
13615 exprs
13616 } else {
13617 Vec::new()
13618 };
13619 let mut items = self.parse_select_list()?;
13620 // v7.38.19 — `SELECT … INTO <table>`, PostgreSQL's other spelling
13621 // of CTAS. It sits exactly here in PG's grammar, right after the
13622 // target list.
13623 //
13624 // A comment in `ast.rs` has said since v7.38 that CTAS and
13625 // `SELECT INTO` lower to the same node. Only CTAS ever did:
13626 // `SELECT i INTO t FROM src` answered `syntax error at or near
13627 // "INTO"`, which the differential found while measuring what
13628 // PostgreSQL tags each of the five materialising forms with. A
13629 // comment describing a capability the code does not have is the
13630 // defect this version has been finding all day, and this is the
13631 // one it found in the parser.
13632 //
13633 // `INTO` is captured rather than consumed here: the name has to
13634 // travel out of a function that returns a `SelectStatement`, and
13635 // the caller lowers the whole thing to the CTAS node.
13636 if matches!(self.peek(), Token::Into) {
13637 self.advance();
13638 // `TEMP` / `TEMPORARY` / `UNLOGGED` / `TABLE` are modifiers on
13639 // the target, not part of its name. SPG has one storage
13640 // class, so `UNLOGGED` is accepted and means nothing, which
13641 // is what it already means on `CREATE TABLE`.
13642 let mut temporary = false;
13643 loop {
13644 match self.peek().clone() {
13645 Token::Ident(w) | Token::QuotedIdent(w)
13646 if w.eq_ignore_ascii_case("temp")
13647 || w.eq_ignore_ascii_case("temporary") =>
13648 {
13649 temporary = true;
13650 self.advance();
13651 }
13652 Token::Ident(w) | Token::QuotedIdent(w)
13653 if w.eq_ignore_ascii_case("unlogged") =>
13654 {
13655 self.advance();
13656 }
13657 Token::Table => {
13658 self.advance();
13659 }
13660 _ => break,
13661 }
13662 }
13663 let name = match self.peek().clone() {
13664 Token::Ident(w) | Token::QuotedIdent(w) => {
13665 self.advance();
13666 w
13667 }
13668 other => {
13669 return Err(self.err(alloc::format!(
13670 "expected a table name after SELECT … INTO, got {other:?}"
13671 )));
13672 }
13673 };
13674 self.pending_select_into = Some((name, temporary));
13675 }
13676 // Scope the TABLESAMPLE lowering channel to this SELECT:
13677 // stash whatever an enclosing select accumulated, collect
13678 // our own FROM's predicates, restore after the combine.
13679 let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13680 let mut from = if matches!(self.peek(), Token::From) {
13681 self.advance();
13682 Some(self.parse_from_clause()?)
13683 } else {
13684 None
13685 };
13686 // v7.37 D.22 — a set-returning function in the projection with no FROM
13687 // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13688 // rows. Move the first SRF projection item to a FROM-position derived
13689 // table and replace it in the projection with a reference to its output
13690 // column; sibling scalar columns repeat per SRF row. PG names the output
13691 // column after the function (or its AS alias). Reuses the FROM-SRF
13692 // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13693 // works via the targetlist-SRF path.
13694 // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13695 // `SELECT * FROM f(args)` — the record's fields become the columns, which
13696 // is exactly what the function's own row shape already is. Anywhere else
13697 // (per outer row, or beside other items) it would need a real record-typed
13698 // projection, so it says so rather than answering something else.
13699 if let [
13700 SelectItem::Expr {
13701 expr: Expr::FunctionCall { name, args },
13702 ..
13703 },
13704 ] = items.as_slice()
13705 && name == "__record_expand"
13706 {
13707 let Some(Expr::FunctionCall {
13708 name: inner_name,
13709 args: inner_args,
13710 }) = args.first()
13711 else {
13712 return Err(self.err(
13713 "(<expr>).* expands a function's record — it needs a function call".into(),
13714 ));
13715 };
13716 if from.is_some() {
13717 return Err(self.err(
13718 "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13719 .into(),
13720 ));
13721 }
13722 let fn_ref = TableRef {
13723 name: inner_name.clone(),
13724 alias: None,
13725 only: false,
13726 as_of_segment: None,
13727 unnest_expr: None,
13728 unnest_column_aliases: Vec::new(),
13729 with_ordinality: false,
13730 generate_series_args: None,
13731 lateral_subquery: None,
13732 jsonb_each_text_arg: None,
13733 table_fn_call: Some(Box::new((
13734 inner_name.to_ascii_lowercase(),
13735 inner_args.clone(),
13736 ))),
13737 rows_from: None,
13738 json_table: None,
13739 scalar_fn_item: false,
13740 };
13741 items = alloc::vec![SelectItem::Wildcard];
13742 from = Some(FromClause {
13743 primary: fn_ref,
13744 joins: Vec::new(),
13745 });
13746 }
13747 // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13748 // FROM, keeps its marker: the ENGINE lowers it, because naming the
13749 // record's fields takes the catalog. It becomes a LATERAL of the same
13750 // function plus one item per declared column — the machinery rounds 65
13751 // and 69 already built.
13752 // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13753 // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13754 // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13755 // express, since the lifted one becomes a scan and the other would
13756 // expand per its rows (a cross product, not a zip). So when the
13757 // projection holds more than one top-level function call, the lift steps
13758 // aside and the engine's target-list expansion takes the whole list.
13759 let fn_call_items = items
13760 .iter()
13761 .filter(|it| {
13762 matches!(
13763 it,
13764 SelectItem::Expr {
13765 expr: Expr::FunctionCall { .. },
13766 ..
13767 }
13768 )
13769 })
13770 .count();
13771 if from.is_none() && fn_call_items <= 1 {
13772 let mut found: Option<(usize, TableRef, String)> = None;
13773 for (i, item) in items.iter().enumerate() {
13774 if let SelectItem::Expr {
13775 expr: Expr::FunctionCall { name, args },
13776 alias,
13777 } = item
13778 {
13779 let lname = name.to_ascii_lowercase();
13780 let colname = alias.clone().unwrap_or_else(|| lname.clone());
13781 let (unnest, gs) = match lname.as_str() {
13782 "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13783 "generate_series" if (2..=3).contains(&args.len()) => {
13784 (None, Some(args.clone()))
13785 }
13786 // v7.38 (read01) — generate_subscripts(arr, dim) in a
13787 // no-FROM projection yields the 1-based subscripts, i.e.
13788 // generate_series(1, array_length(arr, dim)); an invalid
13789 // dimension makes array_length NULL → 0 rows, as in PG.
13790 "generate_subscripts" if args.len() == 2 => (
13791 None,
13792 Some(alloc::vec![
13793 Expr::Literal(Literal::Integer(1)),
13794 Expr::FunctionCall {
13795 name: "array_length".to_string(),
13796 args: args.clone(),
13797 },
13798 ]),
13799 ),
13800 // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13801 // in a no-FROM projection unnest their *_to_array form.
13802 "string_to_table" | "regexp_split_to_table" => {
13803 let array_fn = if lname == "string_to_table" {
13804 "string_to_array"
13805 } else {
13806 "regexp_split_to_array"
13807 };
13808 (
13809 Some(Box::new(Expr::FunctionCall {
13810 name: array_fn.to_string(),
13811 args: args.clone(),
13812 })),
13813 None,
13814 )
13815 }
13816 // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13817 // a no-FROM projection expand per element. The scalar form
13818 // returns the elements as a TEXT array, so unnest over the
13819 // same call materialises one row each (same rewrite the
13820 // FROM-clause form uses).
13821 "jsonb_array_elements"
13822 | "json_array_elements"
13823 | "jsonb_array_elements_text"
13824 | "json_array_elements_text"
13825 if args.len() == 1 =>
13826 {
13827 (
13828 Some(Box::new(Expr::FunctionCall {
13829 name: lname.clone(),
13830 args: args.clone(),
13831 })),
13832 None,
13833 )
13834 }
13835 // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13836 // in a no-FROM projection expands per match (scalar form
13837 // returns the matches as a TEXT array → unnest).
13838 "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13839 Some(Box::new(Expr::FunctionCall {
13840 name: lname.clone(),
13841 args: args.clone(),
13842 })),
13843 None,
13844 ),
13845 _ => continue,
13846 };
13847 found = Some((
13848 i,
13849 TableRef {
13850 name: colname.clone(),
13851 alias: Some(colname.clone()),
13852 only: false,
13853 as_of_segment: None,
13854 unnest_expr: unnest,
13855 unnest_column_aliases: alloc::vec![colname.clone()],
13856 with_ordinality: false,
13857 generate_series_args: gs,
13858 lateral_subquery: None,
13859 jsonb_each_text_arg: None,
13860 table_fn_call: None,
13861 rows_from: None,
13862 json_table: None,
13863 scalar_fn_item: false,
13864 },
13865 colname,
13866 ));
13867 break;
13868 }
13869 }
13870 if let Some((idx, tref, colname)) = found {
13871 from = Some(FromClause {
13872 primary: tref,
13873 joins: Vec::new(),
13874 });
13875 items[idx] = SelectItem::Expr {
13876 expr: Expr::Column(ColumnName {
13877 qualifier: None,
13878 name: colname.clone(),
13879 }),
13880 alias: Some(colname),
13881 };
13882 }
13883 }
13884 let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13885 let where_ = if matches!(self.peek(), Token::Where) {
13886 self.advance();
13887 Some(self.parse_expr(0)?)
13888 } else {
13889 None
13890 };
13891 let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13892 Some(match acc {
13893 Some(w) => Expr::Binary {
13894 lhs: Box::new(pred),
13895 op: crate::ast::BinOp::And,
13896 rhs: Box::new(w),
13897 },
13898 None => pred,
13899 })
13900 });
13901 self.pending_sample_preds = enclosing_sample_preds;
13902 let mut group_by_all = false;
13903 // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13904 // share one expansion: `grouping_sets` lists the key subsets
13905 // (first = primary, assigned to stmt.group_by; the rest
13906 // become UNION ALL peers), `grouping_universe` is the full
13907 // key list used to compute each peer's dropped keys.
13908 let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13909 let mut grouping_universe: Vec<Expr> = Vec::new();
13910 // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13911 // A BOOL, not the key list: this frame is the statement parser's, and
13912 // round 430 measured that a `Vec` local here is enough on its own to
13913 // tip the 512 KiB nesting guard. The keys are recoverable from
13914 // `grouping_universe`, which a rollup fills with exactly them.
13915 let mut mysql_rollup = false;
13916 let group_by = if matches!(self.peek(), Token::Group) {
13917 self.advance();
13918 if !self.peek_is_by() {
13919 return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13920 }
13921 self.advance();
13922 // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13923 // every non-aggregate SELECT-list item later.
13924 if matches!(self.peek(), Token::All) {
13925 self.advance();
13926 group_by_all = true;
13927 None
13928 } else {
13929 // v7.39 (round 242) — PG's general grouping-element grammar:
13930 // GROUP BY [DISTINCT] element [, element]*, where an element
13931 // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13932 // SETS (…) — mixed freely. Each element yields a list of
13933 // key sets; the query's grouping sets are the CARTESIAN
13934 // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13935 // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13936 // content. ROLLUP/CUBE members may be composite
13937 // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13938 // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13939 // parser handled only a lone ROLLUP/CUBE/GS as the whole
13940 // clause.
13941 let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13942 self.advance();
13943 true
13944 } else {
13945 false
13946 };
13947 let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13948 loop {
13949 element_sets.push(self.parse_grouping_element()?);
13950 if matches!(self.peek(), Token::Comma) {
13951 self.advance();
13952 } else {
13953 break;
13954 }
13955 }
13956 let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13957 for el in &element_sets {
13958 let mut next: Vec<Vec<Expr>> = Vec::new();
13959 for base in &total {
13960 for set in el {
13961 let mut merged = base.clone();
13962 for k in set {
13963 if !merged.iter().any(|m| m == k) {
13964 merged.push(k.clone());
13965 }
13966 }
13967 next.push(merged);
13968 }
13969 }
13970 total = next;
13971 }
13972 // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13973 // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13974 // The keys and the aggregates come out identical; the ROW
13975 // ORDER does not, and that is the part a report depends on.
13976 // MySQL interleaves each group's subtotal right after its
13977 // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13978 // where the union-of-grouping-sets expansion emits every
13979 // leaf first and then every subtotal. MariaDB REFUSES an
13980 // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13981 // order itself — measured on MariaDB 11 and MySQL 9.7, which
13982 // agree on the order and disagree only on whether ORDER BY
13983 // is allowed (MySQL allows it; SPG allows it too, since
13984 // refusing would break the clients that can write it).
13985 if self.mysql_dialect
13986 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13987 && matches!(
13988 self.tokens.get(self.pos + 1),
13989 Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13990 )
13991 {
13992 self.advance(); // WITH
13993 self.advance(); // ROLLUP
13994 let keys = total.into_iter().next().unwrap_or_default();
13995 mysql_rollup = true;
13996 // n+1 prefixes, largest first — the same expansion
13997 // `ROLLUP (…)` produces.
13998 total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13999 }
14000 if distinct_sets {
14001 let mut seen: Vec<Vec<String>> = Vec::new();
14002 total.retain(|set| {
14003 let mut key: Vec<String> =
14004 set.iter().map(|e| alloc::format!("{e}")).collect();
14005 key.sort();
14006 if seen.contains(&key) {
14007 false
14008 } else {
14009 seen.push(key);
14010 true
14011 }
14012 });
14013 }
14014 if total.len() > 1 {
14015 let mut universe: Vec<Expr> = Vec::new();
14016 for set in &total {
14017 for k in set {
14018 if !universe.iter().any(|u| u == k) {
14019 universe.push(k.clone());
14020 }
14021 }
14022 }
14023 grouping_universe = universe;
14024 let primary = total[0].clone();
14025 grouping_sets = total;
14026 Some(primary)
14027 } else {
14028 // One set (a plain GROUP BY list, or a single-set
14029 // spelling like GROUPING SETS ((a, b))). An EMPTY
14030 // single set — GROUPING SETS (()) — stays
14031 // `Some(vec![])`: the grand-total group, which must
14032 // run the aggregate path.
14033 Some(total.into_iter().next().unwrap_or_default())
14034 }
14035 }
14036 } else {
14037 None
14038 };
14039 let having = if matches!(self.peek(), Token::Having) {
14040 self.advance();
14041 Some(self.parse_expr(0)?)
14042 } else {
14043 None
14044 };
14045 // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
14046 // OVER w parsed to a marker above; inline each definition
14047 // into the referencing WindowFunction nodes.
14048 let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
14049 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
14050 self.advance();
14051 loop {
14052 let wname = self.expect_ident_like()?;
14053 if !matches!(self.peek(), Token::As) {
14054 return Err(self.err(format!(
14055 "expected AS after WINDOW {wname}, got {:?}",
14056 self.peek()
14057 )));
14058 }
14059 self.advance();
14060 // v7.39 (round 229) — PG rejects a redefinition outright.
14061 if window_defs
14062 .iter()
14063 .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
14064 {
14065 return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
14066 }
14067 let def = self.parse_over_clause()?;
14068 // A definition may itself copy an earlier one
14069 // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
14070 // so resolve it against the defs already in scope. Same
14071 // copy rules as an `OVER (w1 …)` in the select list.
14072 let mut probe = Expr::WindowFunction {
14073 name: String::new(),
14074 args: Vec::new(),
14075 partition_by: def.0,
14076 order_by: def.1,
14077 frame: def.2,
14078 null_treatment: crate::ast::NullTreatment::Respect,
14079 filter: None,
14080 };
14081 Self::substitute_named_windows(&mut probe, &window_defs)
14082 .map_err(|m| self.err(m))?;
14083 let Expr::WindowFunction {
14084 partition_by,
14085 order_by,
14086 frame,
14087 ..
14088 } = probe
14089 else {
14090 unreachable!("probe is a WindowFunction")
14091 };
14092 window_defs.push((wname, (partition_by, order_by, frame)));
14093 if matches!(self.peek(), Token::Comma) {
14094 self.advance();
14095 continue;
14096 }
14097 break;
14098 }
14099 }
14100 // v7.39 (round 705) — which definitions did anything reference?
14101 // The ones nothing did used to be dropped here, unexamined, so
14102 // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
14103 // definition whether referenced or not. Their key expressions ride
14104 // out on the statement for the engine to resolve.
14105 let mut window_refs: Vec<String> = Vec::new();
14106 if !window_defs.is_empty() {
14107 for it in &items {
14108 if let SelectItem::Expr { expr, .. } = it {
14109 Self::collect_named_window_refs(expr, &mut window_refs);
14110 }
14111 }
14112 }
14113 let window_check_exprs: Vec<Expr> = window_defs
14114 .iter()
14115 .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
14116 .flat_map(|(_, (partition, order, _))| {
14117 partition
14118 .iter()
14119 .cloned()
14120 .chain(order.iter().map(|(e, _, _)| e.clone()))
14121 })
14122 .collect();
14123 if !window_defs.is_empty()
14124 || items
14125 .iter()
14126 .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
14127 {
14128 for it in &mut items {
14129 if let SelectItem::Expr { expr, .. } = it {
14130 Self::substitute_named_windows(expr, &window_defs)
14131 .map_err(|m| self.err(m))?;
14132 }
14133 }
14134 }
14135 // `GROUP BY 1` — positional keys substitute with the Nth
14136 // select item's expression (same contract ORDER BY has had
14137 // since v6.x). Out-of-range positions error.
14138 let group_by = match group_by {
14139 Some(mut keys) => {
14140 for k in &mut keys {
14141 if let Expr::Literal(Literal::Integer(n)) = k {
14142 let idx = *n;
14143 if idx < 1 || idx as usize > items.len() {
14144 return Err(self.err(alloc::format!(
14145 "GROUP BY position {idx} is not in select list"
14146 )));
14147 }
14148 match &items[(idx - 1) as usize] {
14149 SelectItem::Expr { expr, .. } => *k = expr.clone(),
14150 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
14151 return Err(self.err(alloc::format!(
14152 "GROUP BY position {idx} references a wildcard item"
14153 )));
14154 }
14155 }
14156 }
14157 }
14158 Some(keys)
14159 }
14160 None => None,
14161 };
14162 let mut stmt = SelectStatement {
14163 locking: None,
14164 ctes: Vec::new(),
14165 distinct,
14166 distinct_on,
14167 items,
14168 from,
14169 where_,
14170 group_by,
14171 group_by_all,
14172 having,
14173 unions: Vec::new(),
14174 order_by: Vec::new(),
14175 limit: None,
14176 offset: None,
14177 limit_with_ties: false,
14178 window_check_exprs,
14179 };
14180 // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
14181 // first set is the primary (already on stmt.group_by); each
14182 // further set becomes a UNION ALL peer with its dropped
14183 // keys (universe minus the set) replaced by NULL literals
14184 // in the peer's items and group_by. PG-legal: non-grouped
14185 // select items must be group keys or aggregates, so a
14186 // dropped key's occurrences in the projection are exactly
14187 // the ones to nullify.
14188 // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
14189 // over a plain GROUP BY (every argument must be a group key; the
14190 // mask is then 0) and rejects anything else with 42803. SPG's
14191 // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
14192 // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
14193 // function `grouping`".
14194 if grouping_sets.len() <= 1 {
14195 let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
14196 let mut calls: Vec<Expr> = Vec::new();
14197 for item in &stmt.items {
14198 if let SelectItem::Expr { expr, .. } = item {
14199 Self::collect_grouping_calls(expr, &mut calls);
14200 }
14201 }
14202 if let Some(h) = &stmt.having {
14203 Self::collect_grouping_calls(h, &mut calls);
14204 }
14205 for call in &calls {
14206 let Expr::FunctionCall { args, .. } = call else {
14207 continue;
14208 };
14209 for a in args {
14210 if !keys.iter().any(|k| k == a) {
14211 return Err(self.err(
14212 "arguments to GROUPING must be grouping expressions of the associated query level"
14213 .to_string(),
14214 ));
14215 }
14216 }
14217 }
14218 if !calls.is_empty() {
14219 for item in &mut stmt.items {
14220 if let SelectItem::Expr { expr, .. } = item {
14221 Self::substitute_grouping_calls(expr, &[]);
14222 }
14223 }
14224 if let Some(h) = &mut stmt.having {
14225 Self::substitute_grouping_calls(h, &[]);
14226 }
14227 }
14228 }
14229 if grouping_sets.len() > 1 {
14230 // The primary set's own dropped keys nullify in the
14231 // HEAD's projection too (GROUPING SETS's first set may
14232 // omit keys other sets use).
14233 let primary = grouping_sets[0].clone();
14234 let head_dropped: Vec<Expr> = grouping_universe
14235 .iter()
14236 .filter(|u| !primary.iter().any(|k| k == *u))
14237 .cloned()
14238 .collect();
14239 for set in grouping_sets.iter().skip(1) {
14240 let mut peer = stmt.clone();
14241 peer.unions = Vec::new();
14242 let dropped: Vec<&Expr> = grouping_universe
14243 .iter()
14244 .filter(|u| !set.iter().any(|k| k == *u))
14245 .collect();
14246 // Empty set = grand-total group: `Some(vec![])` forces
14247 // the aggregate path (one collapsed row) instead of a
14248 // per-row passthrough. See the primary-set note above.
14249 peer.group_by = Some(set.clone());
14250 let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
14251 for item in &mut peer.items {
14252 if let SelectItem::Expr { expr, alias } = item {
14253 if dropped.iter().any(|d| *d == expr) {
14254 // v7.39 — keep the dropped key's name on the
14255 // NULL literal so the UNION output column
14256 // (and any top-level ORDER BY on it) still
14257 // resolves.
14258 if alias.is_none()
14259 && let Expr::Column(c) = &expr
14260 {
14261 *alias = Some(c.name.clone());
14262 }
14263 *expr = Expr::Literal(Literal::Null);
14264 } else {
14265 Self::substitute_grouping_calls(expr, &dropped_owned);
14266 }
14267 }
14268 }
14269 if let Some(h) = &mut peer.having {
14270 Self::substitute_grouping_calls(h, &dropped_owned);
14271 }
14272 stmt.unions.push((UnionKind::All, peer));
14273 }
14274 for item in &mut stmt.items {
14275 if let SelectItem::Expr { expr, alias } = item {
14276 if head_dropped.iter().any(|d| d == expr) {
14277 if alias.is_none()
14278 && let Expr::Column(c) = &expr
14279 {
14280 *alias = Some(c.name.clone());
14281 }
14282 *expr = Expr::Literal(Literal::Null);
14283 } else {
14284 Self::substitute_grouping_calls(expr, &head_dropped);
14285 }
14286 }
14287 }
14288 if let Some(h) = &mut stmt.having {
14289 Self::substitute_grouping_calls(h, &head_dropped);
14290 }
14291 // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
14292 // (while `grouping_universe` / the per-branch sets are in scope). For
14293 // each grouping() call in it, inject a per-branch hidden column
14294 // `__grp_ord_K` carrying that branch's mask into the head + every
14295 // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
14296 // preserves this pre-set order_by; the engine strips `__grp_ord_*`
14297 // from the final output. A standalone grouping-set query has ORDER BY
14298 // (not an explicit set-op) next, so consuming it here is safe.
14299 // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
14300 // rollup carries the hierarchical order: sort by the grouping
14301 // keys with the rolled-up NULLs last, which is exactly the
14302 // interleaving both oracles emit. A client's own ORDER BY wins,
14303 // which is what MySQL does (MariaDB refuses to let one be
14304 // written at all).
14305 // The synthesised keys have to travel the SAME path a written
14306 // ORDER BY does: the block below is what turns a `grouping()`
14307 // call into the per-branch `__grp_ord_K` column the engine can
14308 // actually sort on. Bypassing it left a bare `grouping(text)`
14309 // for the evaluator to reject.
14310 let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
14311 self.parse_order_by_keys()?
14312 } else if mysql_rollup {
14313 Self::mysql_rollup_order(&grouping_universe)
14314 } else {
14315 Vec::new()
14316 };
14317 if !synthesised_or_parsed.is_empty() {
14318 let mut order_keys = synthesised_or_parsed;
14319 let mut grp_exprs: Vec<Expr> = Vec::new();
14320 for ob in &order_keys {
14321 Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
14322 }
14323 for (k, gexpr) in grp_exprs.iter().enumerate() {
14324 let colname = alloc::format!("__grp_ord_{k}");
14325 // Head branch (primary set) uses `head_dropped`.
14326 let mut he = gexpr.clone();
14327 Self::substitute_grouping_calls(&mut he, &head_dropped);
14328 stmt.items.push(SelectItem::Expr {
14329 expr: he,
14330 alias: Some(colname.clone()),
14331 });
14332 // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
14333 for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14334 let set = &grouping_sets[i + 1];
14335 let dropped: Vec<Expr> = grouping_universe
14336 .iter()
14337 .filter(|u| !set.iter().any(|k| k == *u))
14338 .cloned()
14339 .collect();
14340 let mut pe = gexpr.clone();
14341 Self::substitute_grouping_calls(&mut pe, &dropped);
14342 peer.items.push(SelectItem::Expr {
14343 expr: pe,
14344 alias: Some(colname.clone()),
14345 });
14346 }
14347 }
14348 for ob in &mut order_keys {
14349 Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
14350 }
14351 stmt.order_by = order_keys;
14352 }
14353 }
14354 Ok(stmt)
14355 }
14356
14357 /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
14358 /// as ORDER BY keys.
14359 ///
14360 /// Per key: the rollup marker, then the key. Sorting on the key alone
14361 /// is not enough, and a table with a NULL in it says why — MariaDB puts
14362 /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
14363 /// the ROLLUP-introduced NULL last, and both print as NULL.
14364 /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
14365 /// real group including the data-NULL one, 1 only for the row the
14366 /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
14367 /// rolls up to NULL|2, a|1, b|3, NULL|6.
14368 ///
14369 /// `#[inline(never)]`: its locals must not join the statement parser's
14370 /// frame, which round 430 measured sitting against the nesting guard.
14371 #[inline(never)]
14372 fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
14373 let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
14374 for e in keys {
14375 out.push(OrderBy {
14376 expr: Expr::FunctionCall {
14377 name: "grouping".into(),
14378 args: alloc::vec![e.clone()],
14379 },
14380 desc: false,
14381 nulls_first: None,
14382 collation: None,
14383 });
14384 out.push(OrderBy {
14385 expr: e.clone(),
14386 desc: false,
14387 // MySQL orders NULL first on an ascending key.
14388 nulls_first: Some(true),
14389 collation: None,
14390 });
14391 }
14392 out
14393 }
14394
14395 /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
14396 /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
14397 #[inline(never)]
14398 fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
14399 use crate::ast::MaintainKind;
14400 self.skip_paren_option_list();
14401 let kind = match self.peek() {
14402 // `TABLE` and `INDEX` lex as keywords, not identifiers.
14403 Token::Table | Token::Index => {
14404 self.advance();
14405 MaintainKind::ReindexRelation
14406 }
14407 Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
14408 "index" | "table" => {
14409 self.advance();
14410 MaintainKind::ReindexRelation
14411 }
14412 "schema" => {
14413 self.advance();
14414 MaintainKind::ReindexSchema
14415 }
14416 "system" | "database" => {
14417 self.advance();
14418 MaintainKind::Whole
14419 }
14420 // PG requires the object type; anything else is the
14421 // caller's problem, not something to swallow.
14422 _ => MaintainKind::ReindexRelation,
14423 },
14424 _ => MaintainKind::Whole,
14425 };
14426 // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
14427 // allows the plain form, so the modifier is recorded rather than
14428 // skipped. It still has no effect on how the reindex runs.
14429 let mut concurrently = false;
14430 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14431 self.advance();
14432 concurrently = true;
14433 }
14434 let target = self.take_optional_maintain_name();
14435 self.consume_until_statement_boundary();
14436 Ok(Statement::Maintain {
14437 kind,
14438 concurrently,
14439 target,
14440 })
14441 }
14442
14443 /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14444 /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14445 #[inline(never)]
14446 fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14447 use crate::ast::MaintainKind;
14448 self.skip_paren_option_list();
14449 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14450 self.advance();
14451 }
14452 let target = self.take_optional_maintain_name();
14453 self.consume_until_statement_boundary();
14454 Ok(Statement::Maintain {
14455 kind: if target.is_some() {
14456 MaintainKind::ClusterRelation
14457 } else {
14458 MaintainKind::Whole
14459 },
14460 // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14461 // transaction block quite happily (measured).
14462 concurrently: false,
14463 target,
14464 })
14465 }
14466
14467 /// The next token as a relation / schema name, when there is one.
14468 fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14469 match self.peek() {
14470 Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14471 Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14472 _ => None,
14473 },
14474 _ => None,
14475 }
14476 }
14477
14478 /// A parenthesised option list, absorbed.
14479 fn skip_paren_option_list(&mut self) {
14480 if !matches!(self.peek(), Token::LParen) {
14481 return;
14482 }
14483 let mut depth = 0usize;
14484 loop {
14485 match self.advance() {
14486 Token::LParen => depth += 1,
14487 Token::RParen => {
14488 depth -= 1;
14489 if depth == 0 {
14490 return;
14491 }
14492 }
14493 Token::Eof => return,
14494 _ => {}
14495 }
14496 }
14497 }
14498
14499 /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14500 /// column list.
14501 ///
14502 /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14503 /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14504 /// / ALL. The three that describe physical storage have no meaning
14505 /// here, so they parse and change nothing rather than making a
14506 /// dump that mentions them fail to load.
14507 ///
14508 /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14509 /// parse chain the nesting sentinel is tuned against.
14510 #[inline(never)]
14511 fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14512 self.advance(); // LIKE
14513 let source = self.expect_ident_like()?;
14514 let mut options = crate::ast::LikeOptions::default();
14515 loop {
14516 let including = match self.peek() {
14517 Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14518 Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14519 _ => break,
14520 };
14521 self.advance();
14522 // `ALL` lexes as its own keyword, not an identifier.
14523 let opt = if matches!(self.peek(), Token::All) {
14524 self.advance();
14525 alloc::string::String::from("all")
14526 } else {
14527 self.expect_ident_like()?
14528 };
14529 let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14530 o.defaults = on;
14531 o.constraints = on;
14532 o.identity = on;
14533 o.generated = on;
14534 o.indexes = on;
14535 o.comments = on;
14536 };
14537 match opt.to_ascii_lowercase().as_str() {
14538 "all" => set(&mut options, including),
14539 "defaults" => options.defaults = including,
14540 "constraints" => options.constraints = including,
14541 "identity" => options.identity = including,
14542 "generated" => options.generated = including,
14543 "indexes" => options.indexes = including,
14544 "comments" => options.comments = including,
14545 // No storage model to copy into.
14546 "storage" | "statistics" | "compression" => {}
14547 other => {
14548 return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14549 }
14550 }
14551 }
14552 Ok(crate::ast::LikeSpec {
14553 source,
14554 at,
14555 options,
14556 })
14557 }
14558
14559 fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14560 // Caller already consumed CREATE; we're sitting on TABLE.
14561 debug_assert!(matches!(self.peek(), Token::Table));
14562 self.advance();
14563 let if_not_exists = self.consume_if_not_exists();
14564 let name = self.expect_ident_like()?;
14565 // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14566 // child shape has no column list; the child inherits its
14567 // columns from the parent at engine-DDL time. Detect it
14568 // before the `(` requirement below.
14569 if matches!(self.peek(), Token::Partition)
14570 && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14571 {
14572 self.advance(); // PARTITION
14573 self.advance(); // of
14574 let partition_of = self.parse_partition_of_tail()?;
14575 return Ok(Statement::CreateTable(CreateTableStatement {
14576 temporary: false,
14577 name,
14578 engine: None,
14579 columns: Vec::new(),
14580 like_specs: Vec::new(),
14581 inherits: Vec::new(),
14582 if_not_exists,
14583 foreign_keys: Vec::new(),
14584 table_constraints: Vec::new(),
14585 partition_by: None,
14586 partition_of: Some(partition_of),
14587 }));
14588 }
14589 // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14590 // the materialized-view materialisation path (run the SELECT, infer the
14591 // column types, create + populate the table) but marks the node so the
14592 // executor creates a plain table without a mat-view registry entry.
14593 if matches!(self.peek(), Token::As) {
14594 self.advance();
14595 let body_stmt = self.parse_select_stmt()?;
14596 let Statement::Select(body) = body_stmt else {
14597 return Err(self.err(format!(
14598 "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14599 )));
14600 };
14601 let with_data = self.parse_optional_with_data(true)?;
14602 return Ok(Statement::CreateMaterializedView(
14603 crate::ast::CreateMaterializedViewStatement {
14604 temporary: false,
14605 name,
14606 if_not_exists,
14607 columns: Vec::new(),
14608 body,
14609 with_data,
14610 as_plain_table: true,
14611 },
14612 ));
14613 }
14614 if !matches!(self.peek(), Token::LParen) {
14615 return Err(self.err(format!(
14616 "expected '(' after table name, got {:?}",
14617 self.peek()
14618 )));
14619 }
14620 self.advance();
14621 let mut columns = Vec::new();
14622 let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14623 let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14624 let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14625 loop {
14626 // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14627 // column list. It is how a child that adds nothing of its own is
14628 // written, and this loop demanded at least one entry: `syntax
14629 // error at or near ")"`. The child takes the parent's columns,
14630 // which the INHERITS clause already arranges.
14631 if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14632 self.advance();
14633 break;
14634 }
14635 // v7.6.0 / v7.9.18 — distinguish table-level constraint
14636 // clauses from column definitions. Constraints start
14637 // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14638 // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14639 // a column.
14640 if self.peek_table_level_pk_start() {
14641 table_constraints.push(self.parse_table_level_primary_key()?);
14642 } else if matches!(self.peek(), Token::Like) {
14643 // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14644 // <opt> ]*`. The source table's shape lives in the catalog,
14645 // so this records the clause and the engine expands it.
14646 like_specs.push(self.parse_create_table_like(columns.len())?);
14647 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14648 // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14649 table_constraints.push(self.parse_table_level_exclude()?);
14650 } else if self.peek_table_level_unique_start() {
14651 table_constraints.push(self.parse_table_level_unique()?);
14652 } else if self.peek_table_level_check_start() {
14653 // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14654 table_constraints.push(self.parse_table_level_check()?);
14655 } else if self.peek_mysql_inline_key_start() {
14656 // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14657 // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14658 // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14659 // inside the column list. Skip name + paren list;
14660 // for UNIQUE KEY, register as a UC.
14661 if let Some(uc) = self.parse_mysql_inline_key()? {
14662 table_constraints.push(uc);
14663 }
14664 } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14665 // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14666 // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14667 // CHECK is named, and the named-CONSTRAINT arm used
14668 // to accept FOREIGN KEY only. The name is accepted
14669 // and discarded — same handling as every other SPG
14670 // constraint name.
14671 self.advance(); // CONSTRAINT
14672 // v7.39 (read01 round 48) — the name is kept now: the schema
14673 // stores it, so DROP / RENAME CONSTRAINT can find it.
14674 let con_name = self.expect_ident_like()?;
14675 let mut tc = match kind {
14676 NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14677 NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14678 NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14679 NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14680 };
14681 match &mut tc {
14682 crate::ast::TableConstraint::Check { name, .. }
14683 | crate::ast::TableConstraint::Unique { name, .. }
14684 | crate::ast::TableConstraint::PrimaryKey { name, .. }
14685 | crate::ast::TableConstraint::Exclude { name, .. } => {
14686 *name = Some(con_name);
14687 }
14688 _ => {}
14689 }
14690 table_constraints.push(tc);
14691 } else if self.peek_constraint_or_fk_start() {
14692 foreign_keys.push(self.parse_table_level_fk()?);
14693 } else {
14694 let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14695 // v7.13.0 — fold inline UNIQUE / CHECK column
14696 // constraints into table-level entries so the
14697 // engine path stays uniform.
14698 if col.is_unique {
14699 table_constraints.push(crate::ast::TableConstraint::Unique {
14700 name: None,
14701 columns: alloc::vec![col.name.clone()],
14702 nulls_not_distinct: col.unique_nulls_not_distinct,
14703 deferrable: col.constraint_deferrable,
14704 initially_deferred: col.constraint_initially_deferred,
14705 });
14706 }
14707 if let Some(check_expr) = col.check.clone() {
14708 table_constraints.push(crate::ast::TableConstraint::Check {
14709 name: None,
14710 expr: check_expr,
14711 not_valid: false,
14712 });
14713 }
14714 columns.push(col);
14715 if let Some(fk) = col_level_fk {
14716 foreign_keys.push(fk);
14717 }
14718 }
14719 match self.peek() {
14720 Token::Comma => {
14721 self.advance();
14722 }
14723 Token::RParen => {
14724 self.advance();
14725 break;
14726 }
14727 other => {
14728 return Err(
14729 self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14730 );
14731 }
14732 }
14733 }
14734 // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14735 // `CREATE TABLE k (LIKE t)` is a complete definition even though
14736 // nothing is written between the parentheses.
14737 // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14738 // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14739 // empty parentheses were a parse error in their own right — quite apart
14740 // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14741 // SPG does not have (filed separately).
14742 let _ = &like_specs;
14743 // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14744 // It sits between the column list and the MySQL table options,
14745 // and it was a syntax error until this round.
14746 let mut inherits: Vec<String> = Vec::new();
14747 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14748 if k.eq_ignore_ascii_case("inherits"))
14749 {
14750 self.advance();
14751 if !matches!(self.peek(), Token::LParen) {
14752 return Err(self.err(alloc::format!(
14753 "expected ( after INHERITS, got {:?}",
14754 self.peek()
14755 )));
14756 }
14757 self.advance();
14758 loop {
14759 inherits.push(self.expect_ident_like()?);
14760 if matches!(self.peek(), Token::Comma) {
14761 self.advance();
14762 continue;
14763 }
14764 break;
14765 }
14766 if !matches!(self.peek(), Token::RParen) {
14767 return Err(self.err(alloc::format!(
14768 "expected ) closing INHERITS, got {:?}",
14769 self.peek()
14770 )));
14771 }
14772 self.advance();
14773 }
14774 // v7.14.0 — consume MySQL/MariaDB table options after the
14775 // closing `)`. mysqldump emits things like
14776 // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14777 // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14778 // SPG accepts all forms as no-ops (each option is
14779 // `<ident> [=] <ident-or-string>` separated by whitespace).
14780 let engine = self.consume_mysql_table_options();
14781 // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14782 // SPG has no per-table reloptions, so accept and ignore them so a
14783 // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14784 self.consume_with_reloptions();
14785 // v7.37.6-B — declarative-partition-parent suffix
14786 // (`PARTITION BY RANGE (key_col)`) sits after the column
14787 // list + MySQL table-options. v7.37.6-B only accepts RANGE
14788 // and locks the key column at one ident; the engine then
14789 // verifies the column type is TIMESTAMPTZ.
14790 let partition_by = if matches!(self.peek(), Token::Partition) {
14791 self.advance(); // PARTITION
14792 if !self.peek_is_by() {
14793 return Err(self.err(format!(
14794 "expected BY after PARTITION, got {:?}",
14795 self.peek()
14796 )));
14797 }
14798 self.advance();
14799 Some(self.parse_partition_by_tail()?)
14800 } else {
14801 None
14802 };
14803 Ok(Statement::CreateTable(CreateTableStatement {
14804 temporary: false,
14805 name,
14806 engine,
14807 columns,
14808 like_specs,
14809 inherits,
14810 if_not_exists,
14811 foreign_keys,
14812 table_constraints,
14813 partition_by,
14814 partition_of: None,
14815 }))
14816 }
14817
14818 /// v7.37.6-B — case-insensitive ident match helper for the
14819 /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14820 /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14821 /// didn't burn a global keyword slot for each (see the
14822 /// `Token::Partition` doc-comment in `lexer.rs`).
14823 fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14824 matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14825 }
14826
14827 /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14828 /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14829 fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14830 use crate::ast::{PartitionBySpec, PartitionKindAst};
14831 let kind = match self.peek() {
14832 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14833 self.advance();
14834 PartitionKindAst::Range
14835 }
14836 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14837 self.advance();
14838 PartitionKindAst::List
14839 }
14840 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14841 self.advance();
14842 PartitionKindAst::Hash
14843 }
14844 other => {
14845 return Err(self.err(format!(
14846 "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14847 )));
14848 }
14849 };
14850 if !matches!(self.peek(), Token::LParen) {
14851 return Err(self.err(format!(
14852 "expected '(' after PARTITION BY <strategy>, got {:?}",
14853 self.peek()
14854 )));
14855 }
14856 self.advance();
14857 let mut key_columns = Vec::new();
14858 loop {
14859 key_columns.push(self.expect_ident_like()?);
14860 match self.peek() {
14861 Token::Comma => {
14862 self.advance();
14863 }
14864 Token::RParen => {
14865 self.advance();
14866 break;
14867 }
14868 other => {
14869 return Err(self.err(format!(
14870 "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14871 )));
14872 }
14873 }
14874 }
14875 if key_columns.is_empty() {
14876 return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14877 }
14878 Ok(PartitionBySpec { kind, key_columns })
14879 }
14880
14881 /// v7.37.6-B — after `PARTITION OF`, expect
14882 /// <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14883 /// or
14884 /// <parent> DEFAULT
14885 fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14886 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14887 let parent_name = self.expect_ident_like()?;
14888 // v7.37.6-B rejects an explicit column list — the child
14889 // inherits from the parent. mailrs round-7 taught us that
14890 // CREATE TABLE-side schema reconciliation hides drift, so
14891 // we surface this as a parse error rather than silently
14892 // ignoring user columns.
14893 if matches!(self.peek(), Token::LParen) {
14894 return Err(self.err(
14895 "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14896 at v7.37.6-B; the child inherits its columns from the parent"
14897 .to_string(),
14898 ));
14899 }
14900 let bounds = match self.peek() {
14901 Token::Default => {
14902 self.advance();
14903 PartitionOfBoundsAst::Default
14904 }
14905 Token::For => {
14906 self.advance();
14907 if !matches!(self.peek(), Token::Values) {
14908 return Err(
14909 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14910 );
14911 }
14912 self.advance();
14913 // WITH is not a reserved Token in the lexer — it lexes
14914 // as Token::Ident("with"). Disambiguate manually.
14915 let want_with = matches!(
14916 self.peek(),
14917 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14918 );
14919 if want_with {
14920 self.advance();
14921 if !matches!(self.peek(), Token::LParen) {
14922 return Err(self.err(format!(
14923 "expected '(' after FOR VALUES WITH, got {:?}",
14924 self.peek()
14925 )));
14926 }
14927 self.advance();
14928 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14929 loop {
14930 let key = self.expect_ident_like()?;
14931 let n = match self.peek().clone() {
14932 Token::Integer(v) if u32::try_from(v).is_ok() => {
14933 self.advance();
14934 v as u32
14935 }
14936 other => {
14937 return Err(self.err(format!(
14938 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14939 )));
14940 }
14941 };
14942 match key.to_ascii_uppercase().as_str() {
14943 "MODULUS" => modulus = Some(n),
14944 "REMAINDER" => remainder = Some(n),
14945 other => {
14946 return Err(self.err(format!(
14947 "FOR VALUES WITH: unknown key {other:?}; \
14948 expected MODULUS or REMAINDER"
14949 )));
14950 }
14951 }
14952 match self.peek() {
14953 Token::Comma => {
14954 self.advance();
14955 }
14956 Token::RParen => {
14957 self.advance();
14958 break;
14959 }
14960 other => {
14961 return Err(self.err(format!(
14962 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14963 )));
14964 }
14965 }
14966 }
14967 let modulus = modulus
14968 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14969 let remainder = remainder.ok_or_else(|| {
14970 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14971 })?;
14972 if modulus == 0 {
14973 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14974 }
14975 if remainder >= modulus {
14976 return Err(self.err(format!(
14977 "FOR VALUES WITH: REMAINDER ({remainder}) \
14978 must be < MODULUS ({modulus})"
14979 )));
14980 }
14981 PartitionOfBoundsAst::Hash { modulus, remainder }
14982 } else {
14983 match self.peek() {
14984 Token::From => {
14985 self.advance();
14986 let lower = Box::new(self.parse_partition_bound_expr()?);
14987 if !matches!(self.peek(), Token::To) {
14988 return Err(self.err(format!(
14989 "expected TO after FROM (...), got {:?}",
14990 self.peek()
14991 )));
14992 }
14993 self.advance();
14994 let upper = Box::new(self.parse_partition_bound_expr()?);
14995 PartitionOfBoundsAst::Range { lower, upper }
14996 }
14997 // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14998 Token::In => {
14999 self.advance();
15000 if !matches!(self.peek(), Token::LParen) {
15001 return Err(self.err(format!(
15002 "expected '(' after FOR VALUES IN, got {:?}",
15003 self.peek()
15004 )));
15005 }
15006 self.advance();
15007 let mut values = Vec::new();
15008 loop {
15009 values.push(self.parse_expr(0)?);
15010 match self.peek() {
15011 Token::Comma => {
15012 self.advance();
15013 }
15014 Token::RParen => {
15015 self.advance();
15016 break;
15017 }
15018 other => {
15019 return Err(self.err(format!(
15020 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
15021 )));
15022 }
15023 }
15024 }
15025 if values.is_empty() {
15026 return Err(self.err(
15027 "FOR VALUES IN requires at least one literal".to_string(),
15028 ));
15029 }
15030 PartitionOfBoundsAst::List { values }
15031 }
15032 other => {
15033 return Err(self.err(format!(
15034 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
15035 )));
15036 }
15037 }
15038 }
15039 }
15040 other => {
15041 return Err(self.err(format!(
15042 "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
15043 )));
15044 }
15045 };
15046 Ok(PartitionOfSpec {
15047 parent_name,
15048 bounds,
15049 })
15050 }
15051
15052 /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
15053 /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
15054 /// markers (no-arg builtins) so the engine resolves them
15055 /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
15056 fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
15057 if !matches!(self.peek(), Token::LParen) {
15058 return Err(self.err(format!(
15059 "expected '(' before partition bound, got {:?}",
15060 self.peek()
15061 )));
15062 }
15063 self.advance();
15064 let expr = match self.peek() {
15065 Token::Ident(s) | Token::QuotedIdent(s)
15066 if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
15067 {
15068 let name = s.to_ascii_uppercase();
15069 self.advance();
15070 crate::ast::Expr::FunctionCall {
15071 name,
15072 args: Vec::new(),
15073 }
15074 }
15075 _ => self.parse_expr(0)?,
15076 };
15077 if !matches!(self.peek(), Token::RParen) {
15078 return Err(self.err(format!(
15079 "expected ')' after partition bound, got {:?}",
15080 self.peek()
15081 )));
15082 }
15083 self.advance();
15084 Ok(expr)
15085 }
15086
15087 /// v7.14.0 — true when the next tokens look like an inline
15088 /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
15089 /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
15090 /// — each followed by an optional name + `(...)`. Critical:
15091 /// a column NAMED `key` / `index` (PG accepts as ident) must
15092 /// NOT be mistaken for the KEY constraint shape. We disambig
15093 /// by requiring the keyword to be followed by either `(` or
15094 /// `<ident> (`.
15095 fn peek_mysql_inline_key_start(&self) -> bool {
15096 let cur = self.peek();
15097 // Shapes:
15098 // KEY (cols)
15099 // KEY name (cols)
15100 // INDEX (cols)
15101 // INDEX name (cols)
15102 // UNIQUE KEY [name] (cols)
15103 // UNIQUE INDEX [name] (cols)
15104 // FULLTEXT [KEY|INDEX] [name] (cols)
15105 // SPATIAL [KEY|INDEX] [name] (cols)
15106 let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
15107 // tokens at skip = the position AFTER the index-form
15108 // keywords (KEY/INDEX) have been consumed.
15109 match self.tokens.get(skip) {
15110 Some(Token::LParen) => true,
15111 Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
15112 matches!(self.tokens.get(skip + 1), Some(Token::LParen))
15113 }
15114 _ => false,
15115 }
15116 };
15117 // `INDEX` lexes as Token::Index (reserved), not as
15118 // Token::Ident("index"). Both shapes count as a KEY/INDEX
15119 // start; the peek helper below handles either.
15120 let is_key_or_index_tok = |t: &Token| -> bool {
15121 matches!(t, Token::Index)
15122 || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
15123 };
15124 match cur {
15125 Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
15126 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15127 after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
15128 }
15129 Token::Ident(s)
15130 if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
15131 {
15132 let nxt = self.tokens.get(self.pos + 1);
15133 let after_after = if nxt.is_some_and(is_key_or_index_tok) {
15134 self.pos + 2
15135 } else {
15136 self.pos + 1
15137 };
15138 after_keyword_followed_by_paren_or_ident_paren(after_after)
15139 }
15140 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
15141 let nxt = self.tokens.get(self.pos + 1);
15142 if !nxt.is_some_and(is_key_or_index_tok) {
15143 return false;
15144 }
15145 after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
15146 }
15147 _ => false,
15148 }
15149 }
15150
15151 /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
15152 /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
15153 /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
15154 /// returns Some(TableConstraint::Index) so the engine builds
15155 /// a real BTree index on the leading column (mysqldump
15156 /// `KEY idx_posts_author (author_id)` shape).
15157 /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
15158 /// (the storage layer has no matching AM).
15159 fn parse_mysql_inline_key(
15160 &mut self,
15161 ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
15162 // Detect UNIQUE prefix.
15163 let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
15164 {
15165 self.advance();
15166 true
15167 } else {
15168 false
15169 };
15170 // Consume FULLTEXT / SPATIAL prefix and record which one
15171 // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
15172 // dedicated TableConstraint variant so the engine can
15173 // build a tsvector-GIN; SPATIAL still has no matching
15174 // AM, so it falls back to accept-as-no-op.
15175 let mut is_fulltext = false;
15176 let mut is_spatial = false;
15177 if let Token::Ident(s) = self.peek().clone() {
15178 if s.eq_ignore_ascii_case("fulltext") {
15179 self.advance();
15180 is_fulltext = true;
15181 } else if s.eq_ignore_ascii_case("spatial") {
15182 self.advance();
15183 is_spatial = true;
15184 }
15185 }
15186 // KEY / INDEX keyword. `INDEX` lexes as Token::Index
15187 // (reserved); accept either token shape.
15188 match self.peek() {
15189 Token::Index => {
15190 self.advance();
15191 }
15192 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15193 self.advance();
15194 }
15195 other => {
15196 return Err(self.err(alloc::format!(
15197 "expected KEY/INDEX in inline index declaration, got {other:?}"
15198 )));
15199 }
15200 }
15201 // Optional index name (an ident before the `(`).
15202 // v7.15.0 — capture the name when present so the engine
15203 // builds the secondary index under the user's chosen
15204 // name (matches mysqldump's `KEY idx_x (col)` shape).
15205 let mut idx_name: Option<String> = None;
15206 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
15207 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
15208 {
15209 if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
15210 idx_name = Some(s);
15211 }
15212 }
15213 // Optional `USING BTREE` / `USING HASH` (MySQL).
15214 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15215 self.advance();
15216 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15217 self.advance();
15218 }
15219 }
15220 // Required column list `(col [, col]*)`.
15221 if !matches!(self.peek(), Token::LParen) {
15222 return Err(self.err(alloc::format!(
15223 "expected '(' in inline KEY/INDEX, got {:?}",
15224 self.peek()
15225 )));
15226 }
15227 self.advance();
15228 let mut cols: Vec<String> = Vec::new();
15229 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
15230 self.advance();
15231 cols.push(s);
15232 // Skip optional `(length)` per-column prefix.
15233 if matches!(self.peek(), Token::LParen) {
15234 let mut depth = 1usize;
15235 self.advance();
15236 while depth > 0 {
15237 match self.peek() {
15238 Token::LParen => depth += 1,
15239 Token::RParen => depth -= 1,
15240 Token::Eof => break,
15241 _ => {}
15242 }
15243 self.advance();
15244 }
15245 }
15246 // Skip optional ASC / DESC.
15247 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
15248 || matches!(self.peek(), Token::Asc | Token::Desc)
15249 {
15250 self.advance();
15251 }
15252 if matches!(self.peek(), Token::Comma) {
15253 self.advance();
15254 continue;
15255 }
15256 break;
15257 }
15258 if matches!(self.peek(), Token::RParen) {
15259 self.advance();
15260 }
15261 // Trailing options on the inline index — comment / etc.
15262 // Skip until comma or `)`.
15263 while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
15264 self.advance();
15265 }
15266 if cols.is_empty() {
15267 return Ok(None);
15268 }
15269 if is_unique {
15270 // Carry the captured idx_name on UNIQUE too so future
15271 // engine work can name the underlying BTree
15272 // accordingly; today the unique-constraint installer
15273 // synthesises the name itself, but Display round-trip
15274 // benefits from preserving it.
15275 Ok(Some(crate::ast::TableConstraint::Unique {
15276 name: idx_name,
15277 columns: cols,
15278 nulls_not_distinct: false,
15279 // MySQL inline UNIQUE KEY has no deferral vocabulary.
15280 deferrable: false,
15281 initially_deferred: false,
15282 }))
15283 } else if is_fulltext {
15284 // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
15285 // routes through `TableConstraint::FulltextIndex`;
15286 // the engine builds a tsvector-GIN over each named
15287 // column so MATCH AGAINST gets a real inverted
15288 // index instead of a silently-dropped declaration.
15289 Ok(Some(crate::ast::TableConstraint::FulltextIndex {
15290 name: idx_name,
15291 columns: cols,
15292 }))
15293 } else if is_spatial {
15294 // SPG has no native SPATIAL AM. Accept-as-no-op
15295 // (declaration is parsed, but no index is built).
15296 Ok(None)
15297 } else {
15298 // v7.15.0 — plain KEY / INDEX builds a real BTree
15299 // secondary index.
15300 Ok(Some(crate::ast::TableConstraint::Index {
15301 name: idx_name,
15302 columns: cols,
15303 }))
15304 }
15305 }
15306
15307 /// v7.14.0 — consume MySQL/MariaDB table-options tail after
15308 /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
15309 /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
15310 /// (in any order, separated by whitespace).
15311 /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
15312 /// storage-parameter clause on CREATE TABLE. SPG has no per-table
15313 /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
15314 /// bare ident here, and only the parenthesised form is reloptions (so this
15315 /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
15316 fn consume_with_reloptions(&mut self) {
15317 let is_with = matches!(
15318 self.peek(),
15319 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15320 );
15321 if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
15322 return;
15323 }
15324 self.advance(); // WITH
15325 self.advance(); // (
15326 let mut depth = 1u32;
15327 while depth > 0 && !matches!(self.peek(), Token::Eof) {
15328 match self.peek() {
15329 Token::LParen => depth += 1,
15330 Token::RParen => depth -= 1,
15331 _ => {}
15332 }
15333 self.advance();
15334 }
15335 }
15336
15337 /// v7.39 — returns the `ENGINE=` name, which used to be consumed and
15338 /// dropped with everything else here. The rest of the MySQL table
15339 /// options genuinely have no meaning for SPG's storage; the engine
15340 /// name does, because MySQL REFUSES one it does not know and a dump
15341 /// with a typo in it should not quietly become a table.
15342 fn consume_mysql_table_options(&mut self) -> Option<alloc::string::String> {
15343 let mut engine: Option<alloc::string::String> = None;
15344 loop {
15345 // Heuristic: a table option is an ident (or `DEFAULT`
15346 // reserved keyword) followed by `=` and an
15347 // ident / string / integer.
15348 let name_lc = match self.peek().clone() {
15349 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15350 Token::Default => alloc::string::String::from("default"),
15351 _ => break,
15352 };
15353 let known = matches!(
15354 name_lc.as_str(),
15355 "engine"
15356 | "default"
15357 | "charset"
15358 | "collate"
15359 | "auto_increment"
15360 | "row_format"
15361 | "comment"
15362 | "pack_keys"
15363 | "stats_persistent"
15364 | "stats_auto_recalc"
15365 | "stats_sample_pages"
15366 | "key_block_size"
15367 | "tablespace"
15368 | "min_rows"
15369 | "max_rows"
15370 | "checksum"
15371 | "delay_key_write"
15372 | "insert_method"
15373 | "data"
15374 | "index"
15375 | "encryption"
15376 | "compression"
15377 );
15378 if !known {
15379 break;
15380 }
15381 self.advance(); // option name
15382 // `DEFAULT` optional prefix is followed by `CHARSET` /
15383 // `COLLATE`; consume the next ident too.
15384 if name_lc == "default" {
15385 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15386 self.advance();
15387 }
15388 }
15389 if matches!(self.peek(), Token::Eq) {
15390 self.advance();
15391 }
15392 match self.peek().clone() {
15393 Token::Ident(v) | Token::QuotedIdent(v) | Token::String(v) => {
15394 if name_lc == "engine" {
15395 // v7.39.3 — as WRITTEN. MySQL 9.7.2 refuses an
15396 // engine it does not know and names it back
15397 // exactly: `Unknown storage engine 'NoSuchEng'`,
15398 // measured. The lexer folds a bare identifier, so
15399 // the message quoted a name the dump did not
15400 // contain, which is the one thing that message is
15401 // for. Guarded the same way the column spelling
15402 // is: the span runs to the next token, so what
15403 // comes back has to be the same word.
15404 let written = self
15405 .source_span(self.pos, self.pos)
15406 .map(|raw| raw.trim().trim_matches('`').trim_matches('\''))
15407 .filter(|raw| raw.eq_ignore_ascii_case(&v))
15408 .map(alloc::string::String::from);
15409 engine = Some(written.unwrap_or(v));
15410 }
15411 self.advance();
15412 }
15413 Token::Integer(_) => {
15414 self.advance();
15415 }
15416 _ => {}
15417 }
15418 }
15419 engine
15420 }
15421
15422 /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
15423 /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
15424 /// sure (otherwise a column literally named `primary` would
15425 /// be mistaken).
15426 fn peek_table_level_pk_start(&self) -> bool {
15427 let cur = self.peek();
15428 let nxt = self.tokens.get(self.pos + 1);
15429 let nxt2 = self.tokens.get(self.pos + 2);
15430 let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
15431 let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
15432 let is_lparen = matches!(nxt2, Some(Token::LParen));
15433 is_primary && is_key && is_lparen
15434 }
15435
15436 /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
15437 /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
15438 /// (mailrs round-5 G10).
15439 fn peek_table_level_unique_start(&self) -> bool {
15440 let cur = self.peek();
15441 let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
15442 if !is_unique {
15443 return false;
15444 }
15445 let n1 = self.tokens.get(self.pos + 1);
15446 // Plain `UNIQUE (…)`.
15447 if matches!(n1, Some(Token::LParen)) {
15448 return true;
15449 }
15450 // `UNIQUE NULLS [NOT] DISTINCT (…)`.
15451 let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
15452 if !is_nulls {
15453 return false;
15454 }
15455 let n2 = self.tokens.get(self.pos + 2);
15456 let n3 = self.tokens.get(self.pos + 3);
15457 let n4 = self.tokens.get(self.pos + 4);
15458 // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15459 if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15460 return true;
15461 }
15462 // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15463 if matches!(n2, Some(Token::Not))
15464 && matches!(n3, Some(Token::Distinct))
15465 && matches!(n4, Some(Token::LParen))
15466 {
15467 return true;
15468 }
15469 false
15470 }
15471
15472 fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15473 self.advance(); // PRIMARY
15474 self.advance(); // KEY
15475 let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15476 // v7.39 (round 711) — the trailer's values are CARRIED now; round
15477 // 621 consumed and dropped them (the storing half of F08).
15478 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15479 Ok(crate::ast::TableConstraint::PrimaryKey {
15480 name: None,
15481 columns,
15482 deferrable,
15483 initially_deferred,
15484 })
15485 }
15486
15487 fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15488 self.advance(); // UNIQUE
15489 // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15490 // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15491 // is `NULLS DISTINCT` per the SQL standard.
15492 let mut nulls_not_distinct = false;
15493 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15494 let n1 = self.tokens.get(self.pos + 1);
15495 let n2 = self.tokens.get(self.pos + 2);
15496 let is_not = matches!(n1, Some(Token::Not));
15497 let is_distinct = matches!(n2, Some(Token::Distinct));
15498 if is_not && is_distinct {
15499 self.advance(); // NULLS
15500 self.advance(); // NOT
15501 self.advance(); // DISTINCT
15502 nulls_not_distinct = true;
15503 } else if matches!(n1, Some(Token::Distinct)) {
15504 self.advance(); // NULLS
15505 self.advance(); // DISTINCT
15506 }
15507 }
15508 let columns = self.parse_paren_ident_list("UNIQUE")?;
15509 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15510 Ok(crate::ast::TableConstraint::Unique {
15511 name: None,
15512 columns,
15513 nulls_not_distinct,
15514 deferrable,
15515 initially_deferred,
15516 })
15517 }
15518
15519 /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15520 /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15521 /// expression.
15522 /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15523 /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15524 /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15525 /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15526 /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15527 /// commit: `NOT` starts no other suffix here, but reading both
15528 /// tokens before advancing keeps the caller's error message intact
15529 /// if someone writes `NOT NULL` by mistake.
15530 fn parse_not_valid_suffix(&mut self) -> bool {
15531 if !matches!(self.peek(), Token::Not) {
15532 return false;
15533 }
15534 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15535 {
15536 return false;
15537 }
15538 self.advance();
15539 self.advance();
15540 true
15541 }
15542
15543 fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15544 self.advance(); // EXCLUDE
15545 // Optional `USING <method>`.
15546 let mut method = None;
15547 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15548 self.advance();
15549 method = Some(match self.advance() {
15550 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15551 other => {
15552 return Err(self.err(alloc::format!(
15553 "expected index method after USING, got {other:?}"
15554 )));
15555 }
15556 });
15557 }
15558 if !matches!(self.peek(), Token::LParen) {
15559 return Err(self.err(alloc::format!(
15560 "expected '(' after EXCLUDE, got {:?}",
15561 self.peek()
15562 )));
15563 }
15564 self.advance();
15565 let mut elements: Vec<(String, String)> = Vec::new();
15566 loop {
15567 let col = match self.advance() {
15568 Token::Ident(s) | Token::QuotedIdent(s) => s,
15569 other => {
15570 return Err(self.err(alloc::format!(
15571 "expected column name in EXCLUDE, got {other:?}"
15572 )));
15573 }
15574 };
15575 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15576 return Err(self.err(alloc::format!(
15577 "expected WITH after EXCLUDE column, got {:?}",
15578 self.peek()
15579 )));
15580 }
15581 self.advance();
15582 let op = match self.advance() {
15583 Token::InetOverlap => String::from("&&"),
15584 Token::Intersects => String::from("?#"),
15585 Token::IsBelow => String::from("<^"),
15586 Token::IsAbove => String::from(">^"),
15587 Token::PatternLt => String::from("~<~"),
15588 Token::PatternLtEq => String::from("~<=~"),
15589 Token::PatternGt => String::from("~>~"),
15590 Token::PatternGtEq => String::from("~>=~"),
15591 Token::TsMatchOld => String::from("@@@"),
15592 Token::Eq => String::from("="),
15593 Token::JsonContains => String::from("@>"),
15594 Token::JsonContainedBy => String::from("<@"),
15595 Token::OverLeft => String::from("&<"),
15596 Token::OverRight => String::from("&>"),
15597 other => {
15598 return Err(self.err(alloc::format!(
15599 "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15600 )));
15601 }
15602 };
15603 elements.push((col, op));
15604 if matches!(self.peek(), Token::Comma) {
15605 self.advance();
15606 continue;
15607 }
15608 break;
15609 }
15610 if !matches!(self.peek(), Token::RParen) {
15611 return Err(self.err(alloc::format!(
15612 "expected ')' to close EXCLUDE, got {:?}",
15613 self.peek()
15614 )));
15615 }
15616 self.advance();
15617 Ok(crate::ast::TableConstraint::Exclude {
15618 name: None,
15619 method,
15620 elements,
15621 })
15622 }
15623
15624 fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15625 self.advance(); // CHECK
15626 if !matches!(self.peek(), Token::LParen) {
15627 return Err(self.err(alloc::format!(
15628 "expected '(' after CHECK, got {:?}",
15629 self.peek()
15630 )));
15631 }
15632 self.advance();
15633 let expr = self.parse_expr(0)?;
15634 if !matches!(self.peek(), Token::RParen) {
15635 return Err(self.err(alloc::format!(
15636 "expected ')' to close CHECK predicate, got {:?}",
15637 self.peek()
15638 )));
15639 }
15640 self.advance();
15641 // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15642 // are no existing rows for PG to skip, so it rejects the suffix.
15643 Ok(crate::ast::TableConstraint::Check {
15644 name: None,
15645 expr,
15646 not_valid: false,
15647 })
15648 }
15649
15650 /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15651 fn peek_table_level_check_start(&self) -> bool {
15652 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15653 }
15654
15655 /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15656 /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15657 /// on the dedicated FK path (`parse_table_level_fk` consumes its
15658 /// own CONSTRAINT prefix).
15659 fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15660 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15661 return None;
15662 }
15663 // tokens[pos+1] is the constraint name (any ident-like);
15664 // tokens[pos+2] is the kind keyword.
15665 match self.tokens.get(self.pos + 2) {
15666 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15667 Some(NamedTableConstraintKind::Check)
15668 }
15669 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15670 Some(NamedTableConstraintKind::Unique)
15671 }
15672 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15673 Some(NamedTableConstraintKind::PrimaryKey)
15674 }
15675 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15676 Some(NamedTableConstraintKind::Exclude)
15677 }
15678 _ => None,
15679 }
15680 }
15681
15682 fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15683 if !matches!(self.peek(), Token::LParen) {
15684 return Err(self.err(alloc::format!(
15685 "expected '(' after {ctx}, got {:?}",
15686 self.peek()
15687 )));
15688 }
15689 self.advance();
15690 let mut out = Vec::new();
15691 loop {
15692 out.push(self.expect_ident_like()?);
15693 match self.peek() {
15694 Token::Comma => {
15695 self.advance();
15696 }
15697 Token::RParen => {
15698 self.advance();
15699 break;
15700 }
15701 other => {
15702 return Err(self.err(alloc::format!(
15703 "expected ',' or ')' in {ctx} list, got {other:?}"
15704 )));
15705 }
15706 }
15707 }
15708 if out.is_empty() {
15709 return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15710 }
15711 Ok(out)
15712 }
15713
15714 /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15715 /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15716 /// table-level FK; a column def never starts with either keyword
15717 /// (column names are not in this reserved set).
15718 fn peek_constraint_or_fk_start(&self) -> bool {
15719 let is_constraint_kw = matches!(
15720 self.peek(),
15721 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15722 );
15723 let is_foreign_kw = matches!(
15724 self.peek(),
15725 Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15726 );
15727 is_constraint_kw || is_foreign_kw
15728 }
15729
15730 /// v7.6.0 — parse a table-level FK clause:
15731 /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15732 /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15733 fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15734 let mut name: Option<String> = None;
15735 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15736 self.advance();
15737 name = Some(self.expect_ident_like()?);
15738 }
15739 // `FOREIGN`
15740 match self.advance() {
15741 Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15742 other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15743 }
15744 // `KEY`
15745 match self.advance() {
15746 Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15747 other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15748 }
15749 // `(col, col, ...)`
15750 if !matches!(self.peek(), Token::LParen) {
15751 return Err(self.err(format!(
15752 "expected '(' after FOREIGN KEY, got {:?}",
15753 self.peek()
15754 )));
15755 }
15756 self.advance();
15757 let mut columns = Vec::new();
15758 loop {
15759 columns.push(self.expect_ident_like()?);
15760 match self.peek() {
15761 Token::Comma => {
15762 self.advance();
15763 }
15764 Token::RParen => {
15765 self.advance();
15766 break;
15767 }
15768 other => {
15769 return Err(self.err(format!(
15770 "expected ',' or ')' in FK column list, got {other:?}"
15771 )));
15772 }
15773 }
15774 }
15775 if columns.is_empty() {
15776 return Err(self.err("FOREIGN KEY requires at least one column".into()));
15777 }
15778 let (
15779 parent_table,
15780 parent_columns,
15781 on_delete,
15782 on_update,
15783 match_type,
15784 deferrable,
15785 initially_deferred,
15786 ) = self.parse_references_tail(columns.len())?;
15787 Ok(ForeignKeyConstraint {
15788 name,
15789 columns,
15790 parent_table,
15791 parent_columns,
15792 on_delete,
15793 on_update,
15794 match_type,
15795 deferrable,
15796 initially_deferred,
15797 })
15798 }
15799
15800 /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15801 /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15802 /// the local column count, used to default the parent column
15803 /// list when omitted (SQL spec: parent's PK is implied).
15804 fn parse_references_tail(
15805 &mut self,
15806 expected_arity: usize,
15807 ) -> Result<
15808 (
15809 String,
15810 Vec<String>,
15811 FkAction,
15812 FkAction,
15813 crate::ast::MatchType,
15814 // v7.39 (round 288) — deferrable, initially_deferred.
15815 bool,
15816 bool,
15817 ),
15818 ParseError,
15819 > {
15820 match self.advance() {
15821 Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15822 other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15823 }
15824 let parent_table = self.expect_ident_like()?;
15825 let mut parent_columns: Vec<String> = Vec::new();
15826 if matches!(self.peek(), Token::LParen) {
15827 self.advance();
15828 loop {
15829 parent_columns.push(self.expect_ident_like()?);
15830 match self.peek() {
15831 Token::Comma => {
15832 self.advance();
15833 }
15834 Token::RParen => {
15835 self.advance();
15836 break;
15837 }
15838 other => {
15839 return Err(self.err(format!(
15840 "expected ',' or ')' in REFERENCES column list, got {other:?}"
15841 )));
15842 }
15843 }
15844 }
15845 }
15846 if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15847 return Err(self.err(format!(
15848 "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15849 expected_arity,
15850 parent_columns.len()
15851 )));
15852 }
15853 // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15854 // it between the referenced column list and the ON / DEFERRABLE
15855 // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15856 // is skipped when any referencing column is NULL), so SIMPLE —
15857 // the default, and the only spelling pg_dump emits — is accepted
15858 // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15859 // mixed-NULL rule, which is not wired yet; reject them honestly
15860 // rather than silently applying SIMPLE (PG itself errors on
15861 // MATCH PARTIAL as "not yet implemented").
15862 let mut match_type = crate::ast::MatchType::Simple;
15863 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15864 self.advance();
15865 // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15866 // SIMPLE / PARTIAL arrive as bare identifiers.
15867 let kind = match self.advance() {
15868 Token::Full => "FULL".to_string(),
15869 Token::Ident(s) => s.to_uppercase(),
15870 other => {
15871 return Err(self.err(format!(
15872 "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15873 )));
15874 }
15875 };
15876 match kind.as_str() {
15877 "SIMPLE" => {} // Default — match_type stays Simple.
15878 // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15879 // when ALL referencing columns are NULL; a mixed-NULL key errors.
15880 "FULL" => match_type = crate::ast::MatchType::Full,
15881 "PARTIAL" => {
15882 return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15883 }
15884 _ => {
15885 return Err(self.err(format!(
15886 "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15887 )));
15888 }
15889 }
15890 }
15891 // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15892 // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15893 // <action>` / `ON UPDATE <action>` in either order. PG /
15894 // pg_dump emits the timing clause AFTER the ON clauses
15895 // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15896 // but the SQL spec allows either order. We loop over
15897 // every possible trailer and dispatch on the next token,
15898 // stopping when nothing matches. Phase 3.1 changes the
15899 // bare DEFERRABLE form from hard-error to accept-as-
15900 // immediate; SPG is single-writer with no deferred-
15901 // constraint window so the runtime semantics are always
15902 // immediate even when INITIALLY DEFERRED is requested.
15903 // PG's default referential action (no ON DELETE / ON UPDATE
15904 // clause) is NO ACTION, not RESTRICT — the two enforce
15905 // identically in SPG (single-writer, no deferred window; see the
15906 // shared match arm in constraints.rs) but information_schema.
15907 // referential_constraints must report NO ACTION to match PG.
15908 let mut on_delete = FkAction::NoAction;
15909 let mut on_update = FkAction::NoAction;
15910 let mut seen_on_delete = false;
15911 let mut seen_on_update = false;
15912 let mut deferrable = false;
15913 let mut initially_deferred = false;
15914 loop {
15915 // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15916 let before = self.pos;
15917 let (d, idef) = self.consume_deferrable_clauses_timed()?;
15918 if self.pos != before {
15919 deferrable = d;
15920 initially_deferred = idef;
15921 continue;
15922 }
15923 // ON DELETE / ON UPDATE.
15924 if !matches!(self.peek(), Token::On) {
15925 break;
15926 }
15927 self.advance();
15928 let which = self.advance();
15929 let action = self.parse_fk_action()?;
15930 match which {
15931 Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15932 if seen_on_delete {
15933 return Err(self.err("ON DELETE specified twice".into()));
15934 }
15935 seen_on_delete = true;
15936 on_delete = action;
15937 }
15938 Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15939 if seen_on_update {
15940 return Err(self.err("ON UPDATE specified twice".into()));
15941 }
15942 seen_on_update = true;
15943 on_update = action;
15944 }
15945 other => {
15946 return Err(
15947 self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15948 );
15949 }
15950 }
15951 }
15952 Ok((
15953 parent_table,
15954 parent_columns,
15955 on_delete,
15956 on_update,
15957 match_type,
15958 deferrable,
15959 initially_deferred,
15960 ))
15961 }
15962
15963 /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15964 /// NO ACTION`.
15965 fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15966 match self.advance() {
15967 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15968 Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15969 Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15970 Token::Null => Ok(FkAction::SetNull),
15971 Token::Default => Ok(FkAction::SetDefault),
15972 other => Err(self.err(format!(
15973 "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15974 ))),
15975 },
15976 Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15977 Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15978 other => Err(self.err(format!(
15979 "expected ACTION after NO in FK action, got {other:?}"
15980 ))),
15981 },
15982 other => Err(self.err(format!(
15983 "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15984 ))),
15985 }
15986 }
15987
15988 /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15989 /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15990 fn consume_if_not_exists(&mut self) -> bool {
15991 // `IF` arrives as a bare Ident (we don't reserve it because it
15992 // also appears mid-expression in PG, though we don't support
15993 // those forms yet).
15994 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15995 if !looks_like_if {
15996 return false;
15997 }
15998 // Peek one ahead before committing: only consume IF when it's
15999 // actually `IF NOT EXISTS`.
16000 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
16001 return false;
16002 }
16003 if !matches!(
16004 self.tokens.get(self.pos + 2),
16005 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
16006 ) {
16007 return false;
16008 }
16009 self.advance(); // IF
16010 self.advance(); // NOT
16011 self.advance(); // EXISTS
16012 true
16013 }
16014
16015 /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
16016 /// Consumes IF EXISTS as a pair; returns false otherwise
16017 /// without consuming any tokens.
16018 /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
16019 /// ENABLE/DISABLE/FORCE/NO FORCE.
16020 fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
16021 for kw in ["row", "level", "security"] {
16022 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
16023 {
16024 return Err(self.err(alloc::format!(
16025 "expected {} in ROW LEVEL SECURITY, got {:?}",
16026 kw.to_ascii_uppercase(),
16027 self.peek()
16028 )));
16029 }
16030 self.advance();
16031 }
16032 Ok(())
16033 }
16034
16035 fn consume_if_exists(&mut self) -> bool {
16036 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
16037 if !looks_like_if {
16038 return false;
16039 }
16040 if !matches!(
16041 self.tokens.get(self.pos + 1),
16042 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
16043 ) {
16044 return false;
16045 }
16046 self.advance(); // IF
16047 self.advance(); // EXISTS
16048 true
16049 }
16050
16051 /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
16052 /// qualifiers after an index column ref. ASC / DESC are
16053 /// reserved tokens; NULLS / FIRST / LAST are bare idents.
16054 /// We accept and discard them since single-column BTree
16055 /// stores rows in natural key order today.
16056 /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
16057 /// ORDER BY key. Returns None when absent.
16058 fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
16059 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16060 return Ok(None);
16061 }
16062 self.advance();
16063 match self.advance() {
16064 Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
16065 Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
16066 other => Err(self.err(alloc::format!(
16067 "expected FIRST or LAST after NULLS, got {other:?}"
16068 ))),
16069 }
16070 }
16071
16072 /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
16073 /// rather than discarded.
16074 ///
16075 /// SPG's index does not scan in a direction — column ordering is
16076 /// intrinsic to the storage — but `pg_indexes.indexdef` is a
16077 /// reproduction of the DDL, and dropping the clause meant
16078 /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
16079 /// dump lost it, and a schema diff saw drift on every run.
16080 fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
16081 let mut order = crate::ast::IndexColumnOrder::default();
16082 loop {
16083 match self.peek() {
16084 Token::Asc => {
16085 self.advance();
16086 }
16087 Token::Desc => {
16088 order.descending = true;
16089 self.advance();
16090 }
16091 Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
16092 let look = self.tokens.get(self.pos + 1);
16093 if matches!(
16094 look,
16095 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
16096 || k.eq_ignore_ascii_case("last")
16097 ) {
16098 self.advance();
16099 order.nulls_first = Some(matches!(
16100 self.advance(),
16101 Token::Ident(k) if k.eq_ignore_ascii_case("first")
16102 ));
16103 } else {
16104 break;
16105 }
16106 }
16107 _ => break,
16108 }
16109 }
16110 order
16111 }
16112
16113 fn parse_create_index_stmt_after_create(
16114 &mut self,
16115 is_unique: bool,
16116 ) -> Result<Statement, ParseError> {
16117 // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
16118 debug_assert!(matches!(self.peek(), Token::Index));
16119 self.advance();
16120 // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
16121 // SPG's CREATE INDEX is synchronous end-to-end today (real
16122 // CONCURRENTLY variant with restartable scans queues with
16123 // v7.39 indexes epic), so the modifier has no runtime effect
16124 // — same accept-and-no-op shape as v7.37.16.5 DETACH
16125 // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
16126 // VIEW CONCURRENTLY.
16127 let mut concurrently = false;
16128 if matches!(
16129 self.peek(),
16130 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
16131 ) {
16132 self.advance();
16133 concurrently = true;
16134 }
16135 let if_not_exists = self.consume_if_not_exists();
16136 // v7.39 (read01 round 93) — the index name is optional (PG since
16137 // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
16138 // When the token after `[IF NOT EXISTS]` is already `ON`, no name
16139 // was given; leave it empty and the engine derives a PG-style
16140 // `<table>_<cols>_idx` name at CREATE time (with collision counter).
16141 let name = if matches!(self.peek(), Token::On) {
16142 String::new()
16143 } else {
16144 self.expect_ident_like()?
16145 };
16146 if !matches!(self.peek(), Token::On) {
16147 return Err(self.err(format!(
16148 "expected ON after CREATE INDEX <name>, got {:?}",
16149 self.peek()
16150 )));
16151 }
16152 self.advance();
16153 let table = self.expect_ident_like()?;
16154 // Optional `USING <method>` — only recognised method in v2.0 is
16155 // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
16156 // ident `using` (we don't promote it to a reserved keyword
16157 // because it isn't reserved anywhere else in our SQL surface).
16158 let mut method_name: Option<String> = None;
16159 let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
16160 self.advance();
16161 let m = self.expect_ident_like()?;
16162 method_name = Some(m.to_ascii_lowercase());
16163 match m.to_ascii_lowercase().as_str() {
16164 "hnsw" => IndexMethod::Hnsw,
16165 "btree" => IndexMethod::BTree,
16166 "brin" => IndexMethod::Brin,
16167 // v7.12.3 — real GIN inverted index over `tsvector`.
16168 // v7.9.26b's `USING gin` → BTree silent fallback is
16169 // gone; the engine validates that the indexed column
16170 // is `tsvector` at CREATE INDEX time.
16171 "gin" => IndexMethod::Gin,
16172 // v7.9.26b — PG `pg_dump` emits `USING gist` /
16173 // `USING spgist` / `USING hash` for their built-in
16174 // AMs that SPG doesn't have a matching
16175 // implementation for; degrade to BTree on the
16176 // leading column so the schema loads + the index
16177 // catalogue stays consistent. Operator pays the
16178 // planner cost only for the queries that would have
16179 // used the specialised AM.
16180 "gist" | "spgist" | "hash" => IndexMethod::BTree,
16181 // v7.11.3 — pgvector ships both `ivfflat` and
16182 // `hnsw`. Customers shouldn't have to choose
16183 // their on-disk index method based on what SPG
16184 // implements; accept `ivfflat` as a synonym for
16185 // `hnsw` so PG schemas using either method drop
16186 // in. The vector distance op (`<->` / `<#>` /
16187 // `<=>`) at query time still picks the metric.
16188 "ivfflat" => IndexMethod::Hnsw,
16189 other => {
16190 return Err(self.err(alloc::format!(
16191 "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
16192 )));
16193 }
16194 }
16195 } else {
16196 IndexMethod::BTree
16197 };
16198 if !matches!(self.peek(), Token::LParen) {
16199 return Err(self.err(format!(
16200 "expected '(' before indexed column, got {:?}",
16201 self.peek()
16202 )));
16203 }
16204 self.advance();
16205 // v6.8.2 — accept either a bare column ident (legacy) or
16206 // an expression `fn(col, …)` for expression indexes.
16207 // Distinguish by peeking the token *after* the current
16208 // ident: `ident )` is the legacy column-only path;
16209 // anything else triggers the Pratt expression parser.
16210 // (`advance()` uses `mem::replace` to nil out the current
16211 // slot, so we can't save+rewind cleanly — peek-ahead via
16212 // direct index avoids the mutation.)
16213 let mut opclass: Option<String> = None;
16214 let mut key_collation: Option<String> = None;
16215 let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
16216 // Single column with `)` immediately after — fast path.
16217 // v7.9.29 — also: bare column followed by `,` (the
16218 // multi-column form `(a, b, c)`). Without this branch
16219 // the leading ident gets pulled into `parse_expr`
16220 // which then sets `expression = Some(Column(a))` and
16221 // breaks Display round-trip on the multi-column shape.
16222 Token::Ident(s) | Token::QuotedIdent(s)
16223 if matches!(
16224 self.tokens.get(self.pos + 1),
16225 Some(Token::RParen | Token::Comma)
16226 ) =>
16227 {
16228 self.advance();
16229 (s, None)
16230 }
16231 // v7.9.22 — single column followed by a pgvector
16232 // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
16233 // v7.15.0 — capture the opclass instead of discarding
16234 // it so the engine can dispatch (e.g. `gin_trgm_ops`
16235 // → real trigram-shingle GIN over a TEXT column).
16236 // Vector/HNSW opclasses still take their distance
16237 // metric from the query operator (`<->` / `<#>` /
16238 // `<=>`), so for those callers the opclass stays
16239 // informational.
16240 // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
16241 // opclass: `(embedding public.vector_cosine_ops)`. Strip
16242 // the schema and dispatch on the bare opclass, the same
16243 // treatment table/type names get.
16244 Token::Ident(s) | Token::QuotedIdent(s)
16245 if matches!(
16246 self.tokens.get(self.pos + 1),
16247 Some(Token::Ident(_) | Token::QuotedIdent(_))
16248 ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
16249 && matches!(
16250 self.tokens.get(self.pos + 3),
16251 Some(Token::Ident(op) | Token::QuotedIdent(op))
16252 if is_vector_opclass_name(op)
16253 ) =>
16254 {
16255 self.advance(); // column name
16256 self.advance(); // schema qualifier
16257 self.advance(); // dot
16258 let op_tok = self.advance();
16259 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16260 opclass = Some(op.to_ascii_lowercase());
16261 }
16262 (s, None)
16263 }
16264 // r1038 — an operator class is recognised by its POSITION, not
16265 // by a list of names. It used to be `is_vector_opclass_name`,
16266 // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
16267 // sentori's migration wrote — was a syntax error while
16268 // `USING gin (doc)` parsed. Anything sitting between a column
16269 // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
16270 // two bare identifiers in a row are not valid there otherwise.
16271 Token::Ident(s) | Token::QuotedIdent(s)
16272 if matches!(
16273 self.tokens.get(self.pos + 1),
16274 Some(Token::Ident(op) | Token::QuotedIdent(op))
16275 if is_vector_opclass_name(op) || Self::opclass_position_follows(
16276 self.tokens.get(self.pos + 2)
16277 )
16278 ) =>
16279 {
16280 self.advance(); // column name
16281 // Capture the opclass token, lower-cased for
16282 // case-insensitive engine dispatch.
16283 let op_tok = self.advance();
16284 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16285 opclass = Some(op.to_ascii_lowercase());
16286 }
16287 (s, None)
16288 }
16289 Token::Ident(_) | Token::QuotedIdent(_) => {
16290 // v7.39 (round 538) — an explicit COLLATE on the key,
16291 // read by LOOKAHEAD because `parse_expr` absorbs the
16292 // clause as a no-op (SPG orders text by bytes, which is
16293 // the C collation, so it changes nothing to honour). PG
16294 // still PRINTS it: an explicitly written `"C"` and the
16295 // collation a column inherits are different collation
16296 // OBJECTS even where they sort identically, which is why
16297 // `(a COLLATE "C")` shows on a C-collation database too.
16298 if matches!(
16299 self.tokens.get(self.pos + 1),
16300 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
16301 ) {
16302 key_collation = match self.tokens.get(self.pos + 2) {
16303 Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
16304 Some(n.clone())
16305 }
16306 _ => None,
16307 };
16308 }
16309 // v7.39.2 — the clause is read by the LOOKAHEAD above and
16310 // belongs to the KEY, not to the expression. Since
16311 // `COLLATE` became a node, letting `parse_expr` build one
16312 // here put the collation in twice and the key deparsed as
16313 // `(c COLLATE "C" COLLATE "C")`. The ORDER-BY-key channel
16314 // is the same idea and already exists, so this borrows it:
16315 // absorb into the side channel, and the key's own
16316 // lookahead is what carries it.
16317 // v7.39.2 — and the key can only CARRY the byte-order
16318 // spellings. Absorbing into the side channel accepts any
16319 // name, so suppressing the node here without this check
16320 // silently accepted `(name COLLATE "en_US")`, which SPG's
16321 // index cannot honour — a refusal that was doing real
16322 // work, removed by the suppression and put back here.
16323 if let Some(name) = &key_collation {
16324 let lc = name.to_ascii_lowercase();
16325 let byte_order = matches!(
16326 lc.as_str(),
16327 "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
16328 );
16329 let mysql_ok = self.mysql_dialect
16330 && (lc.ends_with("_ci")
16331 || lc.ends_with("_bin")
16332 || lc == "binary"
16333 || matches!(lc.as_str(), "case_insensitive" | "nocase"));
16334 if !byte_order && !mysql_ok {
16335 return Err(self.err(alloc::format!(
16336 "COLLATE {name:?} is not supported in this position: an index \
16337 key carries the byte-order spellings only. Declare it on the \
16338 column (`x text COLLATE {name:?}`) instead"
16339 )));
16340 }
16341 }
16342 let saved_key_ctx = self.in_order_by_key;
16343 self.in_order_by_key = true;
16344 let key_expr = self.parse_expr(0);
16345 self.in_order_by_key = saved_key_ctx;
16346 let key_expr = key_expr?;
16347 let primary = extract_first_column(&key_expr).ok_or_else(|| {
16348 self.err("expression index key must reference at least one column".into())
16349 })?;
16350 (primary, Some(key_expr))
16351 }
16352 // v7.37.43-T4 — parenthesised expression index key
16353 // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
16354 // PG's CREATE INDEX requires the expression to be in
16355 // its own parens to disambiguate function calls from
16356 // column lists, so this `LParen` is the inner open-paren
16357 // of an expression key. parse_expr handles the recursive
16358 // descent and consumes the matching `RParen`.
16359 Token::LParen => {
16360 let key_expr = self.parse_expr(0)?;
16361 let primary = extract_first_column(&key_expr).ok_or_else(|| {
16362 self.err("expression index key must reference at least one column".into())
16363 })?;
16364 (primary, Some(key_expr))
16365 }
16366 other => {
16367 return Err(self.err(format!(
16368 "expected column ident or expression, got {other:?}"
16369 )));
16370 }
16371 };
16372 // v7.9.14 — accept extra comma-separated columns inside
16373 // the index key parens (`CREATE INDEX … (a, b, c)`).
16374 // mailrs F2. Each extra column may carry an optional
16375 // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
16376 // — parsed and discarded; SPG doesn't honour direction
16377 // on a BTree index today (column ordering is intrinsic
16378 // to the storage). v7.10 will widen to genuine composite
16379 // index keys.
16380 let mut extra_columns: Vec<String> = Vec::new();
16381 // The leading column may also have ASC/DESC after it — and that
16382 // one is the column SPG indexes, so its clause is kept.
16383 let key_order = self.consume_optional_index_column_qualifiers();
16384 while matches!(self.peek(), Token::Comma) {
16385 self.advance();
16386 let extra = self.expect_ident_like()?;
16387 let _ = self.consume_optional_index_column_qualifiers();
16388 extra_columns.push(extra);
16389 }
16390 if !matches!(self.peek(), Token::RParen) {
16391 return Err(self.err(format!(
16392 "expected ')' after indexed column / expression, got {:?}",
16393 self.peek()
16394 )));
16395 }
16396 self.advance();
16397 // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
16398 // index-only-scan annotation. Bare ident (not a reserved
16399 // keyword) so we test by case-insensitive string match.
16400 let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
16401 {
16402 self.advance();
16403 if !matches!(self.peek(), Token::LParen) {
16404 return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
16405 }
16406 self.advance();
16407 let mut cols = Vec::new();
16408 loop {
16409 cols.push(self.expect_ident_like()?);
16410 match self.peek() {
16411 Token::Comma => {
16412 self.advance();
16413 }
16414 Token::RParen => {
16415 self.advance();
16416 break;
16417 }
16418 other => {
16419 return Err(self.err(format!(
16420 "expected ',' or ')' in INCLUDE list, got {other:?}"
16421 )));
16422 }
16423 }
16424 }
16425 cols
16426 } else {
16427 Vec::new()
16428 };
16429 // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
16430 // storage parameters. pgvector emits `WITH (lists = N)` for
16431 // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
16432 // SPG's HNSW picks its own parameters today (tunable via
16433 // env vars), so the WITH clause is informational and dropped.
16434 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
16435 self.advance();
16436 if !matches!(self.peek(), Token::LParen) {
16437 return Err(self.err(format!(
16438 "expected '(' after WITH in CREATE INDEX, got {:?}",
16439 self.peek()
16440 )));
16441 }
16442 self.advance();
16443 loop {
16444 if matches!(self.peek(), Token::RParen) {
16445 self.advance();
16446 break;
16447 }
16448 // Drain `key = value` or bare `key` tokens.
16449 let _ = self.advance(); // key
16450 if matches!(self.peek(), Token::Eq) {
16451 self.advance();
16452 let _ = self.advance(); // value (int / string / ident)
16453 }
16454 match self.peek() {
16455 Token::Comma => {
16456 self.advance();
16457 }
16458 Token::RParen => {
16459 self.advance();
16460 break;
16461 }
16462 other => {
16463 return Err(self.err(format!(
16464 "expected ',' or ')' in WITH (…) clause, got {other:?}"
16465 )));
16466 }
16467 }
16468 }
16469 }
16470 // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
16471 // which sits between the key list and the WHERE clause.
16472 let mut nulls_not_distinct = false;
16473 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16474 let n1 = self.tokens.get(self.pos + 1);
16475 let n2 = self.tokens.get(self.pos + 2);
16476 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
16477 self.advance(); // NULLS
16478 self.advance(); // NOT
16479 self.advance(); // DISTINCT
16480 nulls_not_distinct = true;
16481 } else if matches!(n1, Some(Token::Distinct)) {
16482 self.advance(); // NULLS
16483 self.advance(); // DISTINCT
16484 }
16485 }
16486 // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
16487 let partial_predicate = if matches!(self.peek(), Token::Where) {
16488 self.advance();
16489 Some(self.parse_expr(0)?)
16490 } else {
16491 None
16492 };
16493 // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16494 // sense: uniqueness over an ANN structure has no clean
16495 // semantics. Reject early. (BRIN UNIQUE is similarly
16496 // meaningless — block both.)
16497 if is_unique && !matches!(method, IndexMethod::BTree) {
16498 return Err(self.err(alloc::format!(
16499 "UNIQUE is only supported on BTree indexes, got USING {:?}",
16500 method
16501 )));
16502 }
16503 Ok(Statement::CreateIndex(CreateIndexStatement {
16504 concurrently,
16505 name,
16506 key_order,
16507 key_collation,
16508 table,
16509 column,
16510 nulls_not_distinct,
16511 method,
16512 if_not_exists,
16513 included_columns,
16514 partial_predicate,
16515 extra_columns: extra_columns.clone(),
16516 expression,
16517 is_unique,
16518 opclass,
16519 method_name,
16520 }))
16521 }
16522
16523 /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16524 /// column-level `REFERENCES ...` clause. The trailing FK is
16525 /// normalised into table-level shape (single-element columns +
16526 /// parent_columns) so the engine sees one uniform constraint list.
16527 fn parse_column_def_with_fk(
16528 &mut self,
16529 ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16530 let col = self.parse_column_def()?;
16531 // v7.39 (round 308, V29) — an explicitly named inline FK:
16532 // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16533 // loop leaves this spelling intact precisely so the name can be
16534 // kept here; PG reports it in violation messages and matches it
16535 // in `SET CONSTRAINTS`.
16536 let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16537 {
16538 self.advance();
16539 Some(self.expect_ident_like()?)
16540 } else {
16541 None
16542 };
16543 // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16544 let inline_references = matches!(
16545 self.peek(),
16546 Token::Ident(s) if s.eq_ignore_ascii_case("references")
16547 );
16548 if !inline_references {
16549 return Ok((col, None));
16550 }
16551 let (
16552 parent_table,
16553 parent_columns,
16554 on_delete,
16555 on_update,
16556 match_type,
16557 deferrable,
16558 initially_deferred,
16559 ) = self.parse_references_tail(1)?;
16560 let fk = ForeignKeyConstraint {
16561 name: declared_name,
16562 columns: vec![col.name.clone()],
16563 parent_table,
16564 parent_columns,
16565 on_delete,
16566 on_update,
16567 match_type,
16568 deferrable,
16569 initially_deferred,
16570 };
16571 Ok((col, Some(fk)))
16572 }
16573
16574 /// v7.13.0 — parse a column type (consuming the type ident and
16575 /// any trailing parameters / `[]`), without surrounding column
16576 /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16577 /// Returns the resolved `ColumnTypeName` plus implied
16578 /// `(auto_increment, not_null)` flags from PG SERIAL family
16579 /// shorthands — callers that don't expect those (ALTER COLUMN
16580 /// TYPE) can discard them.
16581 fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16582 let (ty, _, _, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16583 Ok(ty)
16584 }
16585
16586 #[allow(clippy::type_complexity)]
16587 fn parse_type_with_implied_flags(
16588 &mut self,
16589 ) -> Result<
16590 (
16591 ColumnTypeName,
16592 bool,
16593 bool,
16594 Option<String>,
16595 Collation,
16596 // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16597 bool,
16598 // v7.39 (round 676) — the collation NAME as written, which the
16599 // `Collation` enum above cannot carry.
16600 Option<String>,
16601 bool,
16602 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16603 // list captured at type-parse time. None for all
16604 // non-ENUM types.
16605 Option<Vec<String>>,
16606 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16607 // list. Distinct from ENUM (subset semantics).
16608 Option<Vec<String>>,
16609 // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16610 // width, lost when the type collapses to SmallInt / Int.
16611 Option<MysqlIntWidth>,
16612 // v7.39 (round 424) — declared fractional-seconds precision of a
16613 // MySQL temporal column (bare spelling = 0). None under PG.
16614 Option<u8>,
16615 // v7.39.2 — written `TIMESTAMP` rather than `DATETIME`. The
16616 // two are different types on MySQL and SPG stores both as
16617 // `Timestamp`, so the spelling has to travel separately or
16618 // a dump silently rewrites the column.
16619 bool,
16620 // v7.39.3 — a MySQL `FLOAT(m,d)` / `DOUBLE(m,d)` pair. Not a
16621 // display hint: it rounds on write.
16622 Option<(u8, u8)>,
16623 ),
16624 ParseError,
16625 > {
16626 let mut ty_ident = match self.advance() {
16627 Token::Ident(s) => s,
16628 // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16629 // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16630 // '<span>'` literal grammar. As a column type it lands
16631 // here directly; downstream resolution still uses the
16632 // canonical lowercase string.
16633 Token::Interval => "interval".to_string(),
16634 other => {
16635 return Err(ParseError {
16636 message: format!("expected column type, got {other:?}"),
16637 token_pos: self.consumed_pos(),
16638 });
16639 }
16640 };
16641 // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16642 // pg_dump qualifies extension types (`public.vector(1024)`).
16643 // SPG is single-namespace; drop the schema and resolve the
16644 // bare type — same treatment table names already get.
16645 while matches!(self.peek(), Token::Dot) {
16646 self.advance();
16647 ty_ident = self.expect_ident_like()?;
16648 }
16649 let mut implied_auto_increment = false;
16650 let mut implied_not_null = false;
16651 let mut user_type_ref: Option<String> = None;
16652 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16653 // value list, captured here and bubbled up through the
16654 // ColumnDef so the engine can attach it to the column
16655 // schema (and validate INSERT cells against it).
16656 let mut inline_enum_variants: Option<Vec<String>> = None;
16657 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16658 let mut inline_set_variants: Option<Vec<String>> = None;
16659 // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16660 // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16661 // collapses to SmallInt / Int. Only under the MySQL dialect.
16662 let mut mysql_int_width: Option<MysqlIntWidth> = None;
16663 // v7.39 (round 424) — the declared fractional-seconds precision of a
16664 // MySQL temporal column. Set by the temporal arms below; stays None
16665 // for PG (whose temporal columns keep full microseconds).
16666 let mut mysql_fsp: Option<u8> = None;
16667 let mut mysql_declared_timestamp = false;
16668 let mut mysql_float_md: Option<(u8, u8)> = None;
16669 let mut ty = match ty_ident.as_str() {
16670 // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16671 "smallserial" | "serial2" => {
16672 implied_auto_increment = true;
16673 implied_not_null = true;
16674 ColumnTypeName::SmallInt
16675 }
16676 "serial" | "serial4" => {
16677 implied_auto_increment = true;
16678 implied_not_null = true;
16679 ColumnTypeName::Int
16680 }
16681 "bigserial" | "serial8" => {
16682 implied_auto_increment = true;
16683 implied_not_null = true;
16684 ColumnTypeName::BigInt
16685 }
16686 // MySQL flavours we accept by aliasing to the closest SPG
16687 // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16688 // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16689 // 24-bit) → INT. UNSIGNED modifiers are consumed below
16690 // without semantic effect.
16691 // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16692 // PG's internal type names; pg_dump and hand-written PG schemas
16693 // use them interchangeably with smallint / int / bigint (the cast
16694 // path already accepted them, only the column grammar didn't).
16695 "smallint" | "int2" => {
16696 // v7.14.0 — MySQL display-width on integers
16697 // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16698 // parenthesised number is purely cosmetic — it
16699 // doesn't change storage. Accept + discard.
16700 self.consume_optional_paren_size();
16701 ColumnTypeName::SmallInt
16702 }
16703 // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16704 // canonical encoding for BOOLEAN. Every MySQL driver
16705 // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16706 // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16707 // 4.3 SPG classified TINYINT(1) as SmallInt, which
16708 // gave the customer i16-shaped values where the app
16709 // expected bool — a Tier-A silent type drift on
16710 // mysqldump restores. Now: `TINYINT(1)` → Bool;
16711 // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16712 // stay SmallInt (the legacy width-agnostic path).
16713 "tinyint" => {
16714 let width = self.peek_optional_paren_size_value();
16715 self.consume_optional_paren_size();
16716 if width == Some(1) {
16717 ColumnTypeName::Bool
16718 } else {
16719 // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16720 // lost width so the write path can enforce -128..127.
16721 if self.mysql_dialect {
16722 mysql_int_width = Some(MysqlIntWidth::Tiny);
16723 }
16724 ColumnTypeName::SmallInt
16725 }
16726 }
16727 "mediumint" => {
16728 self.consume_optional_paren_size();
16729 // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16730 if self.mysql_dialect {
16731 mysql_int_width = Some(MysqlIntWidth::Medium);
16732 }
16733 ColumnTypeName::Int
16734 }
16735 "int" | "integer" | "int4" => {
16736 self.consume_optional_paren_size();
16737 ColumnTypeName::Int
16738 }
16739 "bigint" | "int8" => {
16740 self.consume_optional_paren_size();
16741 ColumnTypeName::BigInt
16742 }
16743 // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16744 // (mailrs round-5 G6). Consume the optional `PRECISION`
16745 // tail when the type keyword was `double` / `DOUBLE`.
16746 //
16747 // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16748 // FLOAT". `FLOAT(p)` picks the width the way PG does:
16749 // p in 1..=24 is real, 25..=53 is double precision, and
16750 // anything else is an error.
16751 "float" | "double" | "real" => {
16752 if ty_ident.eq_ignore_ascii_case("double")
16753 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16754 {
16755 self.advance();
16756 }
16757 if ty_ident.eq_ignore_ascii_case("real") {
16758 // v7.39 (round 274) — the two dialects genuinely
16759 // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16760 // synonym for DOUBLE (8-byte). Round 269 made REAL
16761 // 32-bit globally and thereby narrowed the stored
16762 // precision of every MySQL REAL column.
16763 if self.mysql_dialect {
16764 ColumnTypeName::Float
16765 } else {
16766 ColumnTypeName::Real
16767 }
16768 } else if self.mysql_dialect
16769 && matches!(self.peek(), Token::LParen)
16770 && self.peek_paren_has_comma()
16771 {
16772 // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16773 // display form (`FLOAT(10,2)`), which PG has no
16774 // equivalent of. It was `syntax error at or near ","`,
16775 // so the whole CREATE failed.
16776 //
16777 // v7.39.2 — the guard said `float` while the comment
16778 // said both, so `DOUBLE(10,2)` — which every legacy
16779 // MySQL schema uses for money — still failed the
16780 // whole CREATE with `syntax error at or near "("`.
16781 // Measured on 9.7.2: both forms are accepted, and the
16782 // digits are NOT a display hint, they round on write
16783 // (3.14159265358979 into either stores 3.14). The
16784 // rounding is recorded as a residual; accepting the
16785 // syntax and keeping the width is the half this
16786 // change makes.
16787 // v7.39.3 — keep the pair. The digits are not a
16788 // display hint: MySQL 9.7.2 ROUNDS on write and
16789 // refuses a value wider than `m` (errno 1264), so a
16790 // column declared for money held more precision here
16791 // than its schema said.
16792 let (m, d) = self.parse_optional_numeric_params()?;
16793 mysql_float_md = Some((
16794 u8::try_from(m).unwrap_or(u8::MAX),
16795 u8::try_from(d.max(0)).unwrap_or(u8::MAX),
16796 ));
16797 if ty_ident.eq_ignore_ascii_case("float") {
16798 ColumnTypeName::Real
16799 } else {
16800 ColumnTypeName::Float
16801 }
16802 } else if ty_ident.eq_ignore_ascii_case("float")
16803 && matches!(self.peek(), Token::LParen)
16804 {
16805 // PG words the two bounds differently, and
16806 // parse_paren_size already rejects a zero.
16807 let p = self.parse_paren_size("FLOAT")?;
16808 if p > 53 {
16809 return Err(self.err(String::from(
16810 "precision for type float must be less than 54 bits",
16811 )));
16812 }
16813 if p <= 24 {
16814 ColumnTypeName::Real
16815 } else {
16816 ColumnTypeName::Float
16817 }
16818 } else if ty_ident.eq_ignore_ascii_case("float") && self.mysql_dialect {
16819 // v7.39.2 — MySQL's bare FLOAT is FOUR bytes; PG's is
16820 // eight (it is `float8`'s spelling there). SPG used
16821 // PG's for both, so a MySQL FLOAT column silently
16822 // kept more precision than MySQL does — measured,
16823 // 3.14159265358979 comes back as 3.14159 there and
16824 // came back whole here — and reported itself as
16825 // `double` to every reflection.
16826 //
16827 // This is the mirror of the REAL split above: the
16828 // two dialects disagree about which spelling means
16829 // which width, and one of them was already honoured.
16830 ColumnTypeName::Real
16831 } else {
16832 ColumnTypeName::Float
16833 }
16834 }
16835 // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16836 "float4" => ColumnTypeName::Real,
16837 "float8" => ColumnTypeName::Float,
16838 "text" => ColumnTypeName::Text,
16839 // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16840 // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16841 // real MySQL schema and NONE of them existed: the CREATE
16842 // failed outright with `type "blob" does not exist`, so the
16843 // table was never made. The sizes differ only in MySQL's
16844 // maximum length, which SPG does not cap, so they collapse
16845 // onto TEXT and BYTEA the way the unsized spellings do.
16846 "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16847 "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16848 // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16849 // enforce, consumed so the declaration parses.
16850 "varbinary" | "binary" => {
16851 self.consume_optional_paren_size();
16852 ColumnTypeName::Bytes
16853 }
16854 "name" => ColumnTypeName::Name,
16855 "xid" => ColumnTypeName::Xid,
16856 "oid" => ColumnTypeName::Oid,
16857 "xid8" => ColumnTypeName::Xid8,
16858 "bool" | "boolean" => ColumnTypeName::Bool,
16859 // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16860 // an unbounded `character varying`, which the arm below has always
16861 // read as text. Only the short spelling demanded a length, so
16862 // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16863 // there is — failed on `VARCHAR type requires (N)` while the long
16864 // spelling of the same thing was accepted. The same asymmetry
16865 // round 613 closed on the CAST side, here on the DDL side.
16866 "varchar" => {
16867 if matches!(self.peek(), Token::LParen) {
16868 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16869 } else {
16870 ColumnTypeName::Text
16871 }
16872 }
16873 // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16874 // `character` below (SQL standard).
16875 "char" => {
16876 if matches!(self.peek(), Token::LParen) {
16877 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16878 } else {
16879 ColumnTypeName::Char(1)
16880 }
16881 }
16882 // pg_dump's canonical spellings: `character varying(n)` = varchar,
16883 // `character(n)` = char, bare `character` = char(1). Unbounded
16884 // `character varying` maps to text.
16885 "character" => {
16886 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16887 self.advance();
16888 if matches!(self.peek(), Token::LParen) {
16889 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16890 } else {
16891 ColumnTypeName::Text
16892 }
16893 } else if matches!(self.peek(), Token::LParen) {
16894 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16895 } else {
16896 ColumnTypeName::Char(1)
16897 }
16898 }
16899 "vector" => {
16900 let dim = self.parse_paren_size("VECTOR")?;
16901 let encoding = self.parse_optional_vector_encoding()?;
16902 ColumnTypeName::Vector { dim, encoding }
16903 }
16904 // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16905 // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16906 // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16907 // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16908 // DECIMAL(10,2))` — how nearly every money column is written,
16909 // in either dialect — was a syntax error and the table was
16910 // never created. `FIXED` is MySQL's alias alone, so it is
16911 // taken only in that dialect.
16912 "numeric" | "decimal" | "dec" => {
16913 let (precision, scale) = self.parse_optional_numeric_params()?;
16914 ColumnTypeName::Numeric(precision, scale)
16915 }
16916 "fixed" if self.mysql_dialect => {
16917 let (precision, scale) = self.parse_optional_numeric_params()?;
16918 ColumnTypeName::Numeric(precision, scale)
16919 }
16920 "date" => ColumnTypeName::Date,
16921 // MySQL's `DATETIME` is the same domain as standard
16922 // `TIMESTAMP` — accept both spellings.
16923 "timestamp" | "datetime" => {
16924 // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16925 // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16926 // TIME ZONE` clause, so consume it first.
16927 // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16928 // (it truncates on write and pads on render), so capture it;
16929 // a bare spelling means precision 0 there. PG stores µs always
16930 // and keeps `None`.
16931 let n = self.take_optional_paren_size();
16932 if self.mysql_dialect {
16933 mysql_fsp = Some(n.unwrap_or(0).min(6));
16934 // v7.39.2 — remember WHICH spelling was written.
16935 // MySQL and MariaDB keep `timestamp` and `datetime`
16936 // apart everywhere a client can read the type back,
16937 // and SPG reported `datetime` for both — so a dump
16938 // and reload silently changed the column's declared
16939 // type, and MySQL's TIMESTAMP is not DATETIME (a
16940 // different range, and UTC conversion on the way in
16941 // and out).
16942 mysql_declared_timestamp = ty_ident.eq_ignore_ascii_case("timestamp");
16943 }
16944 // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16945 // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16946 // the full form. SPG canonicalises:
16947 // - WITH TIME ZONE → Timestamptz
16948 // - WITHOUT TIME ZONE → Timestamp
16949 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16950 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16951 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16952 {
16953 self.advance(); // WITH
16954 self.advance(); // TIME
16955 self.advance(); // ZONE
16956 ColumnTypeName::Timestamptz
16957 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16958 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16959 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16960 {
16961 self.advance(); // WITHOUT
16962 self.advance(); // TIME
16963 self.advance(); // ZONE
16964 ColumnTypeName::Timestamp
16965 } else {
16966 // A second `(precision)` cannot legally follow, but the
16967 // old grammar tolerated it; keep that tolerance.
16968 self.consume_optional_paren_size();
16969 ColumnTypeName::Timestamp
16970 }
16971 }
16972 // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16973 // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16974 // only PG-wire OID differs.
16975 "timestamptz" => {
16976 self.consume_optional_paren_size();
16977 ColumnTypeName::Timestamptz
16978 }
16979 // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16980 // validation. We accept the JSONB spelling too because
16981 // most PG clients default to it; SPG doesn't distinguish
16982 // the two (no path-operator perf advantage to model).
16983 "json" => ColumnTypeName::Json,
16984 "jsonb" => ColumnTypeName::Jsonb,
16985 // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16986 // surface here. Same storage shape; mapping happens at
16987 // the engine side via the ColumnTypeName → DataType
16988 // resolver. Literal forms are handled at coerce_value
16989 // time so the lexer stays untouched.
16990 "bytea" | "bytes" => ColumnTypeName::Bytes,
16991 // v7.17.0 Phase 7 — PG network address types
16992 // v7.17.0 had a Text-backed fallback here for
16993 // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16994 // each to a first-class type; the keywords are
16995 // bound below in the ζ-A block.
16996 // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16997 // The actual `to_tsvector` / `@@` / `ts_rank` surface
16998 // arrives in v7.12.1+; the type itself loads here so
16999 // mailrs's `scripts/init-schema.sql` runs unmodified.
17000 "tsvector" => ColumnTypeName::TsVector,
17001 "tsquery" => ColumnTypeName::TsQuery,
17002 // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
17003 // surface for Django / Rails / Hibernate's default
17004 // PK pattern.
17005 "uuid" => ColumnTypeName::Uuid,
17006 // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
17007 // Storage = three-field {months, days, micros}, catalog
17008 // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
17009 // line `INTERVAL` was parser-rejected at CREATE TABLE.
17010 "interval" => {
17011 // pg_dump emits field-qualified forms like `INTERVAL DAY TO
17012 // SECOND` and an optional `(p)` precision. SPG stores the full
17013 // {months,days,micros}; consume + ignore the qualifier/precision.
17014 while matches!(self.peek(), Token::To)
17015 || matches!(self.peek(), Token::Ident(s) if matches!(
17016 s.to_ascii_lowercase().as_str(),
17017 "year" | "month" | "day" | "hour" | "minute" | "second"
17018 ))
17019 {
17020 self.advance();
17021 }
17022 self.consume_optional_paren_size();
17023 ColumnTypeName::Interval
17024 }
17025 // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
17026 // i64 microseconds since 00:00:00. Wire OID 1083.
17027 // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
17028 "time" => {
17029 // v7.39 (round 424) — MySQL TIME carries a semantic
17030 // fractional-seconds precision, bare meaning 0.
17031 let n = self.take_optional_paren_size();
17032 if self.mysql_dialect {
17033 mysql_fsp = Some(n.unwrap_or(0).min(6));
17034 }
17035 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
17036 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17037 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17038 {
17039 self.advance();
17040 self.advance();
17041 self.advance();
17042 ColumnTypeName::TimeTz
17043 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
17044 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17045 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17046 {
17047 self.advance();
17048 self.advance();
17049 self.advance();
17050 ColumnTypeName::Time
17051 } else {
17052 ColumnTypeName::Time
17053 }
17054 }
17055 // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
17056 // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
17057 "year" => ColumnTypeName::Year,
17058 // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
17059 // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
17060 "timetz" => ColumnTypeName::TimeTz,
17061 // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
17062 // Wire OID 790.
17063 "money" => ColumnTypeName::Money,
17064 // v7.17.0 Phase 3.P0-38 — PG range types.
17065 "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
17066 "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
17067 "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
17068 "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
17069 "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
17070 "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
17071 // v7.37.5 δ — PG 14+ multirange keywords.
17072 "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
17073 "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
17074 "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
17075 "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
17076 "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
17077 "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
17078 // v7.37.5 ε — PG geometry scalar keywords.
17079 "point" => ColumnTypeName::Point,
17080 "lseg" => ColumnTypeName::Lseg,
17081 "path" => ColumnTypeName::Path,
17082 "box" => ColumnTypeName::PgBox,
17083 "polygon" => ColumnTypeName::Polygon,
17084 "line" => ColumnTypeName::Line,
17085 "circle" => ColumnTypeName::Circle,
17086 // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
17087 "inet" => ColumnTypeName::Inet,
17088 "cidr" => ColumnTypeName::Cidr,
17089 "macaddr" => ColumnTypeName::Macaddr,
17090 "macaddr8" => ColumnTypeName::Macaddr8,
17091 // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
17092 // width in the value, so the optional `(N)` typmod is accepted and
17093 // ignored (the column stores whatever width it's given).
17094 "bit" => {
17095 let varying = matches!(
17096 self.peek(),
17097 Token::Ident(k) if k.eq_ignore_ascii_case("varying")
17098 );
17099 if varying {
17100 self.advance();
17101 }
17102 // v7.39 (round 281) — the length used to be parsed and
17103 // dropped, so `bit(3)` accepted a five-bit string.
17104 let n = if matches!(self.peek(), Token::LParen) {
17105 self.parse_paren_size("BIT")?
17106 } else {
17107 0
17108 };
17109 if varying {
17110 ColumnTypeName::BitVarying(n)
17111 } else {
17112 ColumnTypeName::Bit(n)
17113 }
17114 }
17115 "varbit" => {
17116 let n = if matches!(self.peek(), Token::LParen) {
17117 self.parse_paren_size("VARBIT")?
17118 } else {
17119 0
17120 };
17121 ColumnTypeName::BitVarying(n)
17122 }
17123 "xml" => ColumnTypeName::Xml,
17124 // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
17125 "hstore" => ColumnTypeName::Hstore,
17126 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
17127 // `ENUM('a','b','c')`. Storage is TEXT; the value
17128 // list lands on `inline_enum_variants` for the
17129 // engine to validate INSERT cells against. Empty
17130 // value list is a parse error (matches MySQL).
17131 "enum" => {
17132 // Expect the opening `(`.
17133 if !matches!(self.peek(), Token::LParen) {
17134 return Err(self.err(alloc::format!(
17135 "expected '(' after ENUM, got {:?}",
17136 self.peek()
17137 )));
17138 }
17139 self.advance();
17140 let mut variants: Vec<String> = Vec::new();
17141 loop {
17142 match self.advance() {
17143 Token::String(s) => variants.push(s),
17144 other => {
17145 return Err(self.err(alloc::format!(
17146 "ENUM(...) expects string literal variants, got {other:?}"
17147 )));
17148 }
17149 }
17150 match self.peek() {
17151 Token::Comma => {
17152 self.advance();
17153 continue;
17154 }
17155 Token::RParen => {
17156 self.advance();
17157 break;
17158 }
17159 other => {
17160 return Err(self.err(alloc::format!(
17161 "expected ',' or ')' in ENUM(...), got {other:?}"
17162 )));
17163 }
17164 }
17165 }
17166 if variants.is_empty() {
17167 return Err(self.err("ENUM(...) must declare at least one variant".into()));
17168 }
17169 inline_enum_variants = Some(variants);
17170 // Storage is plain TEXT; the variant list lives on
17171 // the ColumnSchema side.
17172 ColumnTypeName::Text
17173 }
17174 // v7.17.0 Phase 3.P0-37 — MySQL inline SET
17175 // `SET('a','b','c')`. Same parse shape as ENUM;
17176 // semantics differ (subset rather than pick-one).
17177 "set" => {
17178 if !matches!(self.peek(), Token::LParen) {
17179 return Err(self.err(alloc::format!(
17180 "expected '(' after SET, got {:?}",
17181 self.peek()
17182 )));
17183 }
17184 self.advance();
17185 let mut variants: Vec<String> = Vec::new();
17186 loop {
17187 match self.advance() {
17188 Token::String(s) => variants.push(s),
17189 other => {
17190 return Err(self.err(alloc::format!(
17191 "SET(...) expects string literal variants, got {other:?}"
17192 )));
17193 }
17194 }
17195 match self.peek() {
17196 Token::Comma => {
17197 self.advance();
17198 continue;
17199 }
17200 Token::RParen => {
17201 self.advance();
17202 break;
17203 }
17204 other => {
17205 return Err(self.err(alloc::format!(
17206 "expected ',' or ')' in SET(...), got {other:?}"
17207 )));
17208 }
17209 }
17210 }
17211 if variants.is_empty() {
17212 return Err(self.err("SET(...) must declare at least one variant".into()));
17213 }
17214 inline_set_variants = Some(variants);
17215 ColumnTypeName::Text
17216 }
17217 _other => {
17218 // v7.17.0 Phase 1.4 — unknown ident → defer
17219 // resolution to the engine. Stored as Text in
17220 // ColumnTypeName + the original name carried as
17221 // `user_type_ref` so CREATE TABLE can look up
17222 // user-defined enum / domain types.
17223 user_type_ref = Some(ty_ident.clone());
17224 ColumnTypeName::Text
17225 }
17226 };
17227 // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
17228 // right after the type keyword. Pre-4.4 SPG consumed +
17229 // discarded the keyword, leaving a customer column
17230 // declared `id INT UNSIGNED NOT NULL` silently accepting
17231 // negative values — a Tier-A correctness drift where
17232 // application invariants (auto-increment-IDs never
17233 // negative) silently broke on cutover. Now: capture as
17234 // a column flag, persist on the schema, enforce at
17235 // INSERT / UPDATE time.
17236 let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
17237 {
17238 self.advance();
17239 true
17240 } else {
17241 false
17242 };
17243 // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
17244 // `<type> COLLATE <name>` post-fixes on text columns. SPG
17245 // stores text as UTF-8 always so CHARACTER SET is still a
17246 // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
17247 // name: it gets classified into a `Collation` variant the
17248 // engine consults at WHERE-eval time. PG `default` /
17249 // `pg_catalog.default` / `C` / `POSIX` collations all
17250 // resolve to `Binary` (the prior behaviour); `_ci` /
17251 // `case_insensitive` / `nocase` shift to CaseInsensitive.
17252 // The schema-qualifier form (`pg_catalog.default`) lexes
17253 // as `Ident '.' Ident` — peek for the `.` and consume both
17254 // halves so it's treated as one collation name. PG's
17255 // `IDENT.IDENT` collation form (which can appear here) is
17256 // resolved by Collation::from_collation_name on the bare
17257 // identifier after the dot.
17258 let mut collation = Collation::Binary;
17259 // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
17260 // clause was written. The engine needs this to tell an explicit
17261 // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
17262 // clause at all: both resolve to `Collation::Binary`, but under the
17263 // MySQL dialect the latter takes the folding default collation.
17264 let mut collation_explicit = false;
17265 let mut collation_name: Option<alloc::string::String> = None;
17266 loop {
17267 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
17268 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
17269 {
17270 self.advance(); // CHARACTER
17271 self.advance(); // SET
17272 if matches!(
17273 self.peek(),
17274 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
17275 ) {
17276 self.advance();
17277 }
17278 continue;
17279 }
17280 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
17281 self.advance(); // COLLATE
17282 // Accept Ident / QuotedIdent / String AND the
17283 // keyword-tokenised `Default` (PG `pg_catalog.default`
17284 // and bare `DEFAULT` collation names — `default` is a
17285 // reserved word so the lexer hands back Token::Default
17286 // not Token::Ident).
17287 let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
17288 match this.peek().clone() {
17289 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
17290 this.advance();
17291 Some(s)
17292 }
17293 Token::Default => {
17294 this.advance();
17295 Some(alloc::string::String::from("default"))
17296 }
17297 _ => None,
17298 }
17299 };
17300 let raw = if let Some(head) = read_collation_atom(self) {
17301 // Schema-qualified PG form: `pg_catalog.default`.
17302 if matches!(self.peek(), Token::Dot) {
17303 self.advance();
17304 let tail = read_collation_atom(self).unwrap_or_default();
17305 alloc::format!("{head}.{tail}")
17306 } else {
17307 head
17308 }
17309 } else {
17310 alloc::string::String::new()
17311 };
17312 if !raw.is_empty() {
17313 collation_explicit = true;
17314 // v7.39 (round 676) — keep the name too. The enum below
17315 // folds C / POSIX / en_US / default into one value, and
17316 // `pg_attribute.attcollation` has to tell them apart.
17317 // The schema qualifier goes: PG's `pg_catalog.default`
17318 // and a bare `default` name the same collation.
17319 // v7.39 (round 679) — strip a SCHEMA qualifier, not an
17320 // encoding suffix. Round 676 used `rsplit('.')` for
17321 // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
17322 // PG writes `pg_catalog.default` (qualifier) and
17323 // `en_US.utf8` (locale + encoding) with the same
17324 // separator. Only `pg_catalog.` is a qualifier, and it
17325 // is the only one PG's own dumps emit.
17326 let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
17327 let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
17328 collation_name = Some(alloc::string::String::from(bare));
17329 let parsed = Collation::from_collation_name(&raw);
17330 // Last COLLATE clause wins, but `Binary` from a
17331 // bare keyword like `default` should not
17332 // silently downgrade a stronger one set earlier
17333 // on the same column. v7.17 only ships one
17334 // non-Binary variant so a simple OR is enough.
17335 if parsed != Collation::Binary {
17336 collation = parsed;
17337 }
17338 }
17339 continue;
17340 }
17341 break;
17342 }
17343 // v7.10.10 — postfix `[]` widens the base type to its array
17344 // type. PG accepts `TYPE[]` after any base type and so does
17345 // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
17346 // all through; the old "only TEXT[]" note was stale).
17347 if matches!(self.peek(), Token::LBracket) {
17348 self.advance();
17349 if !matches!(self.peek(), Token::RBracket) {
17350 return Err(self.err(alloc::format!(
17351 "TEXT[] takes no dimension; got {:?}",
17352 self.peek()
17353 )));
17354 }
17355 self.advance();
17356 // v7.11.13 — widened to INT[] and BIGINT[] in addition
17357 // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
17358 // still error here.
17359 ty = match ty {
17360 ColumnTypeName::Text => ColumnTypeName::TextArray,
17361 ColumnTypeName::Int => ColumnTypeName::IntArray,
17362 ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
17363 // v7.37.5 β-P4 — INTERVAL[] via the same postfix
17364 // `[]` grammar. Wire OID 1187.
17365 ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
17366 // v7.37.5 γ — full PG array-of-scalar family.
17367 ColumnTypeName::Bool => ColumnTypeName::BoolArray,
17368 ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
17369 ColumnTypeName::Float => ColumnTypeName::FloatArray,
17370 // NUMERIC(p, s) loses its precision params at the
17371 // array level (matches PG: `NUMERIC[]` is untyped,
17372 // per-element precision flows through values).
17373 ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
17374 ColumnTypeName::Date => ColumnTypeName::DateArray,
17375 ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
17376 ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
17377 ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
17378 ColumnTypeName::Json => ColumnTypeName::JsonArray,
17379 ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
17380 ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
17381 // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
17382 // the array level (matches PG semantics where the
17383 // element precision is per-row, not column-wide).
17384 ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
17385 ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
17386 // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
17387 // follow-up.
17388 ColumnTypeName::Money => ColumnTypeName::MoneyArray,
17389 other => {
17390 return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
17391 }
17392 };
17393 // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
17394 // for INT/TEXT/BIGINT. Anything else is an error.
17395 if matches!(self.peek(), Token::LBracket) {
17396 self.advance();
17397 if !matches!(self.peek(), Token::RBracket) {
17398 return Err(self.err(alloc::format!(
17399 "TYPE[][] second dimension takes no size; got {:?}",
17400 self.peek()
17401 )));
17402 }
17403 self.advance();
17404 ty = match ty {
17405 ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
17406 ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
17407 ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
17408 // v7.39 (read01 round 75) — bool[][].
17409 ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
17410 other => {
17411 return Err(self.err(alloc::format!(
17412 "v7.17 2D arrays support INT[][] / BIGINT[][] / \
17413 TEXT[][] only; got {other:?}"
17414 )));
17415 }
17416 };
17417 }
17418 }
17419 Ok((
17420 ty,
17421 implied_auto_increment,
17422 implied_not_null,
17423 user_type_ref,
17424 collation,
17425 collation_explicit,
17426 collation_name,
17427 is_unsigned,
17428 inline_enum_variants,
17429 inline_set_variants,
17430 mysql_int_width,
17431 mysql_fsp,
17432 mysql_declared_timestamp,
17433 mysql_float_md,
17434 ))
17435 }
17436
17437 fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
17438 // v7.20 — PG reserves the table-constraint keywords, so a
17439 // BARE `UNIQUE` / `PRIMARY` / … in column position is a
17440 // malformed constraint clause (e.g. `UNIQUE a` missing its
17441 // parens), not a column named "unique". Since v7.17's
17442 // unknown-type leniency (`user_type_ref`) such a clause
17443 // would otherwise parse as a column with a user-defined
17444 // type — silently accepting invalid DDL. Quoted
17445 // identifiers ("unique" / `unique`) remain valid names.
17446 if let Token::Ident(s) = self.peek()
17447 && [
17448 "unique",
17449 "primary",
17450 "foreign",
17451 "constraint",
17452 "check",
17453 "references",
17454 "exclude",
17455 ]
17456 .iter()
17457 .any(|kw| s.eq_ignore_ascii_case(kw))
17458 {
17459 return Err(self.err(alloc::format!(
17460 "unexpected reserved keyword '{s}' at start of column definition \
17461 (malformed table constraint?)"
17462 )));
17463 }
17464 let name_tok = self.pos;
17465 let name = self.expect_ident_like()?;
17466 // v7.39.3 — MySQL 9.7.2 reports a column by the SPELLING it was
17467 // declared with: `MyCol` stays `MyCol` in SHOW COLUMNS, in
17468 // information_schema, and in SHOW CREATE (measured). SPG folded
17469 // an unquoted name, so a table restored from a dump reported
17470 // names the application had never written.
17471 //
17472 // The written form comes back from the source span, which only
17473 // the MySQL dialect keeps. The span runs to the START of the
17474 // next token, so a comment or unusual spacing between them
17475 // arrives with it — hence the check that what came back is the
17476 // same identifier. It is not decoration: without it,
17477 // `CREATE TABLE t (MyCol /* c */ INT)` names the column
17478 // `MyCol /* c */`.
17479 let name = self
17480 .source_span(name_tok, name_tok)
17481 .map(|raw| raw.trim().trim_matches('`').trim_matches('"'))
17482 .filter(|raw| raw.eq_ignore_ascii_case(&name))
17483 .map_or(name, alloc::string::String::from);
17484 let (
17485 ty,
17486 implied_auto_increment,
17487 implied_not_null,
17488 user_type_ref,
17489 collation,
17490 collation_explicit,
17491 collation_name,
17492 is_unsigned,
17493 inline_enum_variants,
17494 inline_set_variants,
17495 mysql_int_width,
17496 mysql_fsp,
17497 mysql_declared_timestamp,
17498 mysql_float_md,
17499 ) = self.parse_type_with_implied_flags()?;
17500 // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
17501 // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
17502 // each at most once.
17503 let mut default: Option<Expr> = None;
17504 let mut nullable = !implied_not_null;
17505 let mut nullability_seen = implied_not_null;
17506 let mut auto_increment = implied_auto_increment;
17507 let mut is_primary_key = false;
17508 let mut is_unique = false;
17509 let mut unique_nulls_not_distinct = false;
17510 let mut constraint_deferrable = false;
17511 let mut constraint_initially_deferred = false;
17512 let mut check: Option<Expr> = None;
17513 let mut on_update_runtime: Option<Expr> = None;
17514 let mut generated_stored_expr: Option<Box<Expr>> = None;
17515 let mut identity_always = false;
17516 loop {
17517 // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
17518 // not-null constraints by name and pg_dump emits them
17519 // inline: `id bigint CONSTRAINT contacts_id_not_null1
17520 // NOT NULL`. Accept and discard the name; whatever
17521 // constraint follows is parsed by the arms below.
17522 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
17523 // v7.39 (round 308, V29) — a name on an inline
17524 // REFERENCES belongs to the FOREIGN KEY, and the caller
17525 // (`parse_column_def_with_fk`) is what builds it, so
17526 // leave the whole clause for it. Dropping the name here
17527 // is what made `CONSTRAINT fk_a REFERENCES …` come back
17528 // as the synthesised `c_pid_fkey` — which then could
17529 // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
17530 // `advance()` takes tokens by `mem::replace`, so there
17531 // is no rewinding once consumed.
17532 if matches!(
17533 self.tokens.get(self.pos + 2),
17534 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
17535 ) {
17536 break;
17537 }
17538 self.advance();
17539 let _name = self.expect_ident_like()?;
17540 continue;
17541 }
17542 // v7.39 (round 379) — MySQL's SHORT generated-column form
17543 // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
17544 // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
17545 // below), but hand-written schemas and app migrations use this.
17546 // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
17547 // SPG computes-and-stores either way, like the long form.
17548 if matches!(self.peek(), Token::As) {
17549 self.advance();
17550 if !matches!(self.peek(), Token::LParen) {
17551 return Err(self.err(alloc::format!(
17552 "expected '(' after AS in a generated column, got {:?}",
17553 self.peek()
17554 )));
17555 }
17556 self.advance();
17557 let expr = self.parse_expr(0)?;
17558 if !matches!(self.peek(), Token::RParen) {
17559 return Err(self.err(alloc::format!(
17560 "expected ')' after AS (<expr>), got {:?}",
17561 self.peek()
17562 )));
17563 }
17564 self.advance();
17565 if matches!(self.peek(), Token::Ident(s)
17566 if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
17567 {
17568 self.advance();
17569 }
17570 generated_stored_expr = Some(alloc::boxed::Box::new(expr));
17571 continue;
17572 }
17573 // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
17574 // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
17575 // the modern replacement for SERIAL in hand-written
17576 // schemas). Both flavours map onto the auto-increment
17577 // machinery — SPG's serial semantics ≈ BY DEFAULT;
17578 // ALWAYS's reject-explicit-values nuance is documented
17579 // leniency. Generated EXPRESSION columns
17580 // (`AS (expr) STORED`) are not supported: error loudly
17581 // instead of silently storing NULLs.
17582 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
17583 self.advance();
17584 let mut saw_generated_always = false;
17585 match self.peek().clone() {
17586 Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
17587 self.advance();
17588 saw_generated_always = true;
17589 }
17590 Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
17591 self.advance();
17592 if !matches!(self.peek(), Token::Default) {
17593 return Err(self.err(alloc::format!(
17594 "expected DEFAULT after GENERATED BY, got {:?}",
17595 self.peek()
17596 )));
17597 }
17598 self.advance();
17599 }
17600 other => {
17601 return Err(self.err(alloc::format!(
17602 "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
17603 )));
17604 }
17605 }
17606 if !matches!(self.peek(), Token::As) {
17607 return Err(self.err(alloc::format!(
17608 "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
17609 self.peek()
17610 )));
17611 }
17612 self.advance();
17613 // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
17614 // ( <expr> ) STORED` stored computed-column. The
17615 // expression is captured for the engine to recompute
17616 // on every INSERT / UPDATE. v7.37.7 accepts the
17617 // STORED keyword only; PG also has VIRTUAL, which
17618 // v7.37.7 carves out (sentori only uses STORED).
17619 if matches!(self.peek(), Token::LParen) {
17620 self.advance();
17621 let expr = self.parse_expr(0)?;
17622 if !matches!(self.peek(), Token::RParen) {
17623 return Err(self.err(alloc::format!(
17624 "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
17625 self.peek()
17626 )));
17627 }
17628 self.advance();
17629 let stored = match self.peek() {
17630 Token::Ident(s) | Token::QuotedIdent(s)
17631 if s.eq_ignore_ascii_case("stored") =>
17632 {
17633 self.advance();
17634 true
17635 }
17636 // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
17637 // generated columns. SPG computes them on write and
17638 // persists like STORED; the two are observably
17639 // identical for query results (the value, recompute
17640 // on base-column change, and NOT NULL enforcement all
17641 // match), so a PG 18 schema/dump using VIRTUAL loads
17642 // and behaves correctly. The compute-on-read storage
17643 // saving is an invisible internal difference.
17644 Token::Ident(s) | Token::QuotedIdent(s)
17645 if s.eq_ignore_ascii_case("virtual") =>
17646 {
17647 self.advance();
17648 false
17649 }
17650 other => {
17651 return Err(self.err(alloc::format!(
17652 "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
17653 got {other:?}"
17654 )));
17655 }
17656 };
17657 let _ = stored; // STORED / VIRTUAL both compute-and-store.
17658 generated_stored_expr = Some(Box::new(expr));
17659 continue;
17660 }
17661 self.expect_keyword_ident("identity")?;
17662 // Optional `(START WITH 1 INCREMENT BY 1 …)` —
17663 // consume the balanced parens and discard (SPG's
17664 // auto-increment is max+1-scan based).
17665 if matches!(self.peek(), Token::LParen) {
17666 let mut depth = 0usize;
17667 loop {
17668 match self.advance() {
17669 Token::LParen => depth += 1,
17670 Token::RParen => {
17671 depth -= 1;
17672 if depth == 0 {
17673 break;
17674 }
17675 }
17676 Token::Eof => {
17677 return Err(self.err(
17678 "unterminated sequence-options parens after IDENTITY".into(),
17679 ));
17680 }
17681 _ => {}
17682 }
17683 }
17684 }
17685 auto_increment = true;
17686 // v7.38 (read01) — remember the ALWAYS flavour so the engine
17687 // can reject explicit non-DEFAULT INSERT values (unless
17688 // OVERRIDING SYSTEM VALUE) the way PG does.
17689 identity_always = saw_generated_always;
17690 // PG identity columns are implicitly NOT NULL.
17691 nullable = false;
17692 continue;
17693 }
17694 // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
17695 // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
17696 // is accepted today. The "ON" token is an Ident
17697 // (not reserved) — peek before consuming.
17698 if matches!(self.peek(), Token::On)
17699 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17700 {
17701 self.advance(); // ON
17702 self.advance(); // update
17703 // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17704 let next = self.peek().clone();
17705 match next {
17706 Token::Ident(s) | Token::QuotedIdent(s)
17707 if s.eq_ignore_ascii_case("current_timestamp") =>
17708 {
17709 self.advance();
17710 // Optional `(N)` precision.
17711 if matches!(self.peek(), Token::LParen) {
17712 self.advance();
17713 if !matches!(self.peek(), Token::Integer(_)) {
17714 return Err(self.err(alloc::format!(
17715 "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17716 self.peek()
17717 )));
17718 }
17719 self.advance();
17720 if !matches!(self.peek(), Token::RParen) {
17721 return Err(self.err(alloc::format!(
17722 "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17723 self.peek()
17724 )));
17725 }
17726 self.advance();
17727 }
17728 on_update_runtime = Some(Expr::FunctionCall {
17729 name: "now".into(),
17730 args: Vec::new(),
17731 });
17732 continue;
17733 }
17734 other => {
17735 return Err(self.err(alloc::format!(
17736 "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17737 )));
17738 }
17739 }
17740 }
17741 if matches!(self.peek(), Token::Default) {
17742 if default.is_some() {
17743 return Err(self.err("DEFAULT specified twice".into()));
17744 }
17745 self.advance();
17746 default = Some(self.parse_expr(0)?);
17747 continue;
17748 }
17749 // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17750 // token with NOT NULL and sits EARLIER in the loop than the
17751 // deferrability arm, so without the lookahead it was reported as
17752 // "NOT NULL specified twice" (or "expected NULL after NOT").
17753 if matches!(self.peek(), Token::Not)
17754 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17755 {
17756 // NOT DEFERRABLE — explicit immediate; nothing to carry.
17757 self.consume_optional_deferrable_clauses()?;
17758 continue;
17759 }
17760 if matches!(self.peek(), Token::Not) {
17761 if nullability_seen {
17762 return Err(self.err("NOT NULL specified twice".into()));
17763 }
17764 self.advance();
17765 if !matches!(self.peek(), Token::Null) {
17766 return Err(self.err(format!(
17767 "expected NULL after NOT in column def, got {:?}",
17768 self.peek()
17769 )));
17770 }
17771 self.advance();
17772 nullable = false;
17773 nullability_seen = true;
17774 continue;
17775 }
17776 // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17777 // "this column is nullable" marker (the default in
17778 // standard SQL anyway). mysqldump emits it routinely
17779 // (`col TYPE NULL DEFAULT NULL` for nullable
17780 // timestamps etc). Accept + no-op.
17781 if matches!(self.peek(), Token::Null) {
17782 if nullability_seen && !nullable {
17783 // v7.39 (round 761, F31 tranche 2 #31) — PG's
17784 // sentence, PG18-measured (the table name is the
17785 // caller's; the column half is exact).
17786 return Err(self.err(alloc::format!(
17787 "conflicting NULL/NOT NULL declarations for column \"{name}\""
17788 )));
17789 }
17790 self.advance();
17791 nullable = true;
17792 nullability_seen = true;
17793 continue;
17794 }
17795 // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17796 // arrives as a bare Ident. Match either, case-insensitive.
17797 if let Token::Ident(s) = self.peek()
17798 && (s.eq_ignore_ascii_case("auto_increment")
17799 || s.eq_ignore_ascii_case("autoincrement"))
17800 {
17801 if auto_increment {
17802 return Err(self.err("AUTO_INCREMENT specified twice".into()));
17803 }
17804 self.advance();
17805 auto_increment = true;
17806 continue;
17807 }
17808 // v7.9.13 — inline `PRIMARY KEY` column constraint
17809 // (mailrs F1). Implies `NOT NULL`. The engine creates
17810 // a BTree index for the PK column at CREATE TABLE time
17811 // so FK parent-side index lookups resolve.
17812 // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17813 // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17814 // spelling was a parse error, so a pg_dump carrying one stopped
17815 // mid-restore. The clauses are consumed by the same helper the FK
17816 // path has used since round 288 and recorded nowhere: SPG enforces
17817 // the constraint IMMEDIATELY either way, which fails earlier than
17818 // PG inside a transaction that violates-then-repairs — a refusal,
17819 // not a wrong answer. True deferral is the open remainder of F08.
17820 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17821 || (matches!(self.peek(), Token::Not)
17822 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
17823 {
17824 // v7.39 (round 711) — CARRIED now (the storing half of
17825 // F08); round 621 only consumed.
17826 let (d, idef) = self.consume_deferrable_clauses_timed()?;
17827 constraint_deferrable |= d;
17828 constraint_initially_deferred |= idef;
17829 continue;
17830 }
17831 if let Token::Ident(s) = self.peek()
17832 && s.eq_ignore_ascii_case("primary")
17833 {
17834 if is_primary_key {
17835 return Err(self.err("PRIMARY KEY specified twice".into()));
17836 }
17837 // Peek-ahead for the required `KEY` token.
17838 let next = self.tokens.get(self.pos + 1);
17839 let next_is_key = matches!(
17840 next,
17841 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17842 );
17843 if !next_is_key {
17844 return Err(self.err(format!(
17845 "expected KEY after PRIMARY in column def, got {:?}",
17846 next
17847 )));
17848 }
17849 self.advance(); // PRIMARY
17850 self.advance(); // KEY
17851 is_primary_key = true;
17852 if nullability_seen && nullable {
17853 return Err(self.err(
17854 "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17855 ));
17856 }
17857 nullable = false;
17858 nullability_seen = true;
17859 continue;
17860 }
17861 // v7.13.0 — inline `UNIQUE` column constraint
17862 // (mailrs round-5 G2). Fold into a single-column
17863 // table-level UNIQUE at CREATE TABLE post-process time.
17864 if let Token::Ident(s) = self.peek()
17865 && s.eq_ignore_ascii_case("unique")
17866 {
17867 if is_unique {
17868 return Err(self.err("UNIQUE specified twice".into()));
17869 }
17870 self.advance();
17871 is_unique = true;
17872 // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17873 // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17874 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17875 let n1 = self.tokens.get(self.pos + 1);
17876 let n2 = self.tokens.get(self.pos + 2);
17877 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17878 self.advance(); // NULLS
17879 self.advance(); // NOT
17880 self.advance(); // DISTINCT
17881 unique_nulls_not_distinct = true;
17882 } else if matches!(n1, Some(Token::Distinct)) {
17883 self.advance(); // NULLS
17884 self.advance(); // DISTINCT
17885 }
17886 }
17887 continue;
17888 }
17889 // v7.13.0 — inline `CHECK (<expr>)` column constraint
17890 // (mailrs round-5 G3). PG semantics: column-level
17891 // CHECK is equivalent to a table-level CHECK. Multiple
17892 // inline CHECKs on the same column AND together.
17893 if let Token::Ident(s) = self.peek()
17894 && s.eq_ignore_ascii_case("check")
17895 {
17896 self.advance();
17897 if !matches!(self.peek(), Token::LParen) {
17898 return Err(self.err(alloc::format!(
17899 "expected '(' after CHECK in column def, got {:?}",
17900 self.peek()
17901 )));
17902 }
17903 self.advance();
17904 let pred = self.parse_expr(0)?;
17905 if !matches!(self.peek(), Token::RParen) {
17906 return Err(self.err(alloc::format!(
17907 "expected ')' to close CHECK predicate, got {:?}",
17908 self.peek()
17909 )));
17910 }
17911 self.advance();
17912 check = Some(match check.take() {
17913 Some(prev) => Expr::Binary {
17914 op: BinOp::And,
17915 lhs: Box::new(prev),
17916 rhs: Box::new(pred),
17917 },
17918 None => pred,
17919 });
17920 continue;
17921 }
17922 break;
17923 }
17924 Ok(ColumnDef {
17925 name,
17926 ty,
17927 nullable,
17928 default,
17929 auto_increment,
17930 is_primary_key,
17931 is_unique,
17932 unique_nulls_not_distinct,
17933 constraint_deferrable,
17934 constraint_initially_deferred,
17935 check,
17936 user_type_ref,
17937 on_update_runtime,
17938 collation,
17939 collation_explicit,
17940 collation_name,
17941 is_unsigned,
17942 inline_enum_variants,
17943 inline_set_variants,
17944 generated_stored_expr,
17945 identity_always,
17946 mysql_int_width,
17947 mysql_fsp,
17948 mysql_declared_timestamp,
17949 mysql_float_md,
17950 })
17951 }
17952
17953 /// `NUMERIC` may appear without parameters, with one (precision
17954 /// only, scale=0), or with both. Returns `(precision, scale)` with
17955 /// 0 = unspecified for the bare form.
17956 fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17957 if !matches!(self.peek(), Token::LParen) {
17958 // Bare `NUMERIC` — PG treats this as "unlimited precision";
17959 // we surface it as precision=0 to mean "unconstrained" so
17960 // the engine doesn't need a separate variant.
17961 return Ok((0, 0));
17962 }
17963 self.advance();
17964 // v7.39 (round 272) — PG's declared precision runs to 1000, and
17965 // it words the out-of-range case with the value it saw. SPG
17966 // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17967 // accepts failed to parse at all; values wider than i128 are
17968 // carried by the arbitrary-precision form.
17969 let precision = match self.advance() {
17970 Token::Integer(n) if (1..=1000).contains(&n) => {
17971 u16::try_from(n).expect("range-checked")
17972 }
17973 Token::Integer(n) => {
17974 return Err(ParseError {
17975 message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17976 token_pos: self.consumed_pos(),
17977 });
17978 }
17979 other => {
17980 return Err(ParseError {
17981 message: format!(
17982 "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17983 ),
17984 token_pos: self.consumed_pos(),
17985 });
17986 }
17987 };
17988 // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17989 // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17990 // then overflows). A negative scale rounds to tens / hundreds / …
17991 let scale = if matches!(self.peek(), Token::Comma) {
17992 self.advance();
17993 let neg = if matches!(self.peek(), Token::Minus) {
17994 self.advance();
17995 true
17996 } else {
17997 false
17998 };
17999 match self.advance() {
18000 Token::Integer(n) => {
18001 let signed = if neg { -n } else { n };
18002 if !(-1000..=1000).contains(&signed) {
18003 return Err(ParseError {
18004 message: format!(
18005 "NUMERIC scale {signed} must be between -1000 and 1000"
18006 ),
18007 token_pos: self.consumed_pos(),
18008 });
18009 }
18010 i16::try_from(signed).expect("range-checked")
18011 }
18012 other => {
18013 return Err(ParseError {
18014 message: format!("NUMERIC scale must be an integer, got {other:?}"),
18015 token_pos: self.consumed_pos(),
18016 });
18017 }
18018 }
18019 } else {
18020 0
18021 };
18022 if !matches!(self.peek(), Token::RParen) {
18023 return Err(self.err(format!(
18024 "expected ')' to close NUMERIC params, got {:?}",
18025 self.peek()
18026 )));
18027 }
18028 self.advance();
18029 Ok((precision, scale))
18030 }
18031
18032 /// Parse `(N)` where `N` is a positive integer literal — used by the
18033 /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
18034 /// for the error message.
18035 /// v6.0.1: parse the optional `USING <encoding>` clause that
18036 /// follows `VECTOR(N)` in a column definition. Missing clause
18037 /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
18038 /// ident → `ParseError` listing the encodings recognised today.
18039 fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
18040 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
18041 return Ok(VecEncoding::F32);
18042 }
18043 // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
18044 // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
18045 // consume the token when the very next token is a known
18046 // vector-encoding keyword (SQ8 / HALF). Otherwise leave
18047 // `USING` for the caller — it's the rewrite-expression form.
18048 let n1 = self.tokens.get(self.pos + 1);
18049 let next_is_encoding = matches!(
18050 n1,
18051 Some(Token::Ident(s))
18052 if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
18053 );
18054 if !next_is_encoding {
18055 return Ok(VecEncoding::F32);
18056 }
18057 self.advance();
18058 let enc_ident = match self.advance() {
18059 Token::Ident(s) => s,
18060 other => {
18061 return Err(self.err(format!(
18062 "expected vector encoding after USING, got {other:?}"
18063 )));
18064 }
18065 };
18066 match enc_ident.to_ascii_lowercase().as_str() {
18067 "sq8" => Ok(VecEncoding::Sq8),
18068 // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
18069 // binary16 per-element storage.
18070 "half" => Ok(VecEncoding::F16),
18071 other => Err(self.err(format!(
18072 "unknown vector encoding {other:?}; supported: SQ8, HALF"
18073 ))),
18074 }
18075 }
18076
18077 /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
18078 /// without consuming it. Returns `Some(N)` when the next
18079 /// tokens are `( <int> )`; None otherwise. Used by the
18080 /// TINYINT classifier to decide whether to map to Bool or
18081 /// SmallInt.
18082 fn peek_optional_paren_size_value(&self) -> Option<i64> {
18083 if !matches!(self.peek(), Token::LParen) {
18084 return None;
18085 }
18086 let next = self.tokens.get(self.pos + 1)?;
18087 let n = match next {
18088 Token::Integer(n) => *n,
18089 _ => return None,
18090 };
18091 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18092 return None;
18093 }
18094 Some(n)
18095 }
18096
18097 /// v7.14.0 — consume an optional MySQL display-width
18098 /// parenthesised number after an integer type, returning
18099 /// nothing. `TINYINT(1)` etc.
18100 /// v7.39 (round 360) — does the parenthesised group ahead contain a
18101 /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
18102 fn peek_paren_has_comma(&self) -> bool {
18103 let mut i = self.pos + 1;
18104 let mut depth = 1usize;
18105 while depth > 0 {
18106 match self.tokens.get(i) {
18107 Some(Token::LParen) => depth += 1,
18108 Some(Token::RParen) => depth -= 1,
18109 Some(Token::Comma) if depth == 1 => return true,
18110 None | Some(Token::Eof) => return false,
18111 _ => {}
18112 }
18113 i += 1;
18114 }
18115 false
18116 }
18117
18118 /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
18119 /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
18120 /// fractional-seconds precision that drives write truncation and render
18121 /// padding, where `consume_optional_paren_size` throws it away.
18122 /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
18123 fn take_optional_paren_size(&mut self) -> Option<u8> {
18124 let Some(Token::Integer(n)) = self
18125 .tokens
18126 .get(self.pos + 1)
18127 .filter(|_| matches!(self.peek(), Token::LParen))
18128 .cloned()
18129 else {
18130 self.consume_optional_paren_size();
18131 return None;
18132 };
18133 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18134 self.consume_optional_paren_size();
18135 return None;
18136 }
18137 self.consume_optional_paren_size();
18138 u8::try_from(n).ok()
18139 }
18140
18141 fn consume_optional_paren_size(&mut self) {
18142 if !matches!(self.peek(), Token::LParen) {
18143 return;
18144 }
18145 self.advance();
18146 // Skip until matching RParen (allow nested or any tokens).
18147 let mut depth = 1usize;
18148 while depth > 0 {
18149 match self.peek() {
18150 Token::LParen => depth += 1,
18151 Token::RParen => depth -= 1,
18152 Token::Eof => return,
18153 _ => {}
18154 }
18155 self.advance();
18156 }
18157 }
18158
18159 fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
18160 if !matches!(self.peek(), Token::LParen) {
18161 return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
18162 }
18163 self.advance();
18164 let n = match self.advance() {
18165 Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
18166 message: format!("{label} size too large: {n}"),
18167 token_pos: self.consumed_pos(),
18168 })?,
18169 other => {
18170 return Err(ParseError {
18171 message: format!("expected positive integer {label} size, got {other:?}"),
18172 token_pos: self.consumed_pos(),
18173 });
18174 }
18175 };
18176 if !matches!(self.peek(), Token::RParen) {
18177 return Err(self.err(format!(
18178 "expected ')' after {label} size, got {:?}",
18179 self.peek()
18180 )));
18181 }
18182 self.advance();
18183 Ok(n)
18184 }
18185
18186 /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
18187 /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
18188 /// key, like MySQL) whose action skips conflicting rows.
18189 /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
18190 /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
18191 /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
18192 /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
18193 /// common bulk-upsert spellings —
18194 /// INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
18195 /// REPLACE INTO t SELECT …
18196 /// — were a parse error / a duplicate-key failure respectively.
18197 ///
18198 /// Precedence: an explicitly written clause beats a statement-level flag.
18199 /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
18200 /// implicit `REPLACE` and `IGNORE` lowerings.
18201 fn parse_insert_conflict_clause(
18202 &mut self,
18203 replace: bool,
18204 ignore: bool,
18205 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18206 if let Some(c) = self.parse_optional_on_duplicate_key()? {
18207 return Ok(Some(c));
18208 }
18209 if let Some(c) = self.parse_optional_on_conflict()? {
18210 return Ok(Some(c));
18211 }
18212 if replace {
18213 // REPLACE INTO = delete-then-insert, which PG spells as
18214 // `ON CONFLICT DO UPDATE SET` over every column; the engine
18215 // reads an empty assignment list as "take the incoming row".
18216 return Ok(Some(crate::ast::OnConflictClause {
18217 target_columns: Vec::new(),
18218 index_where: None,
18219 constraint_name: None,
18220 mysql_lowered: true,
18221 action: crate::ast::OnConflictAction::Update {
18222 assignments: Vec::new(),
18223 where_: None,
18224 },
18225 }));
18226 }
18227 if ignore {
18228 return Ok(Some(Self::insert_ignore_clause()));
18229 }
18230 Ok(None)
18231 }
18232
18233 /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
18234 /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
18235 /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
18236 /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
18237 fn parse_optional_on_duplicate_key(
18238 &mut self,
18239 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18240 if !(matches!(self.peek(), Token::On)
18241 && matches!(self.tokens.get(self.pos + 1),
18242 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
18243 {
18244 return Ok(None);
18245 }
18246 self.advance(); // ON
18247 self.advance(); // DUPLICATE
18248 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
18249 return Err(self.err(format!(
18250 "expected KEY after ON DUPLICATE, got {:?}",
18251 self.peek()
18252 )));
18253 }
18254 self.advance();
18255 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
18256 return Err(self.err(format!(
18257 "expected UPDATE after ON DUPLICATE KEY, got {:?}",
18258 self.peek()
18259 )));
18260 }
18261 self.advance();
18262 let mut assignments: Vec<(String, Expr)> = Vec::new();
18263 loop {
18264 let col = self.expect_ident_like()?;
18265 if !matches!(self.peek(), Token::Eq) {
18266 return Err(self.err(format!(
18267 "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
18268 self.peek()
18269 )));
18270 }
18271 self.advance();
18272 let mut expr = self.parse_expr(0)?;
18273 Self::rewrite_mysql_values_refs(&mut expr);
18274 assignments.push((col, expr));
18275 if matches!(self.peek(), Token::Comma) {
18276 self.advance();
18277 continue;
18278 }
18279 break;
18280 }
18281 Ok(Some(crate::ast::OnConflictClause {
18282 target_columns: Vec::new(),
18283 index_where: None,
18284 constraint_name: None,
18285 mysql_lowered: true,
18286 action: crate::ast::OnConflictAction::Update {
18287 assignments,
18288 where_: None,
18289 },
18290 }))
18291 }
18292
18293 fn insert_ignore_clause() -> crate::ast::OnConflictClause {
18294 crate::ast::OnConflictClause {
18295 target_columns: Vec::new(),
18296 index_where: None,
18297 constraint_name: None,
18298 mysql_lowered: true,
18299 action: crate::ast::OnConflictAction::Nothing,
18300 }
18301 }
18302
18303 fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
18304 debug_assert!(
18305 matches!(self.peek(), Token::Insert)
18306 || (replace
18307 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
18308 );
18309 self.advance();
18310 // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
18311 // would raise a duplicate-key error instead of failing the statement,
18312 // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
18313 // plain ident to the lexer; only the MySQL dialect accepts it here.
18314 let ignore = self.mysql_dialect
18315 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
18316 if ignore {
18317 self.advance();
18318 }
18319 if !matches!(self.peek(), Token::Into) {
18320 return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
18321 }
18322 self.advance();
18323 let table = self.expect_ident_like()?;
18324 // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
18325 // grammar requires the AS keyword here (a bare identifier would be
18326 // ambiguous with a column list). The alias is what the ON CONFLICT
18327 // DO UPDATE expressions refer to the target row by.
18328 let alias = if matches!(self.peek(), Token::As) {
18329 self.advance();
18330 Some(self.expect_ident_like()?)
18331 } else {
18332 None
18333 };
18334 // v7.39 (round 428) — MySQL's SET-form INSERT:
18335 // INSERT INTO t SET a = 1, b = 'x'
18336 // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
18337 // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
18338 // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
18339 // measured). So it lowers to the column list + one VALUES row and
18340 // rejoins the ordinary path, which already handles every one of
18341 // those. PG has no such spelling, hence the dialect gate.
18342 if self.mysql_dialect
18343 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
18344 {
18345 self.advance(); // SET
18346 let mut names = Vec::new();
18347 let mut values = Vec::new();
18348 loop {
18349 names.push(self.expect_ident_like()?);
18350 if !matches!(self.peek(), Token::Eq) {
18351 return Err(self.err(alloc::format!(
18352 "expected '=' in INSERT … SET, got {:?}",
18353 self.peek()
18354 )));
18355 }
18356 self.advance();
18357 // `SET a = DEFAULT` rides the same `__column_default` marker
18358 // the VALUES-row and UPDATE-SET paths use; the INSERT
18359 // executor resolves it against the target column.
18360 if matches!(self.peek(), Token::Default) {
18361 self.advance();
18362 values.push(Expr::FunctionCall {
18363 name: "__column_default".to_string(),
18364 args: Vec::new(),
18365 });
18366 } else {
18367 values.push(self.parse_expr(0)?);
18368 }
18369 if matches!(self.peek(), Token::Comma) {
18370 self.advance();
18371 continue;
18372 }
18373 break;
18374 }
18375 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18376 let returning = self.parse_optional_returning()?;
18377 return Ok(Statement::Insert(InsertStatement {
18378 ctes: Vec::new(),
18379 table,
18380 alias,
18381 columns: Some(names),
18382 rows: alloc::vec![values],
18383 select_source: None,
18384 // MySQL's SET form has no `OVERRIDING …` clause (that is
18385 // PG's identity-column spelling).
18386 overriding: Overriding::None,
18387 mysql_ignore: ignore,
18388 on_conflict,
18389 returning,
18390 }));
18391 }
18392 // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
18393 // v7.39 (round 151) — a SELECT or WITH right after the paren is
18394 // a parenthesized query source instead (PG select_with_parens:
18395 // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
18396 // both keywords are reserved in PG, so no column list can start
18397 // with them.
18398 let columns = if matches!(self.peek(), Token::LParen) {
18399 self.advance();
18400 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18401 let select_stmt = if self.peek_is_with_kw() {
18402 self.advance();
18403 self.parse_nested_with_select()?
18404 } else {
18405 match self.parse_select_stmt()? {
18406 Statement::Select(s) => s,
18407 other => {
18408 return Err(self.err(alloc::format!(
18409 "expected SELECT in parenthesized INSERT source, got {other:?}"
18410 )));
18411 }
18412 }
18413 };
18414 if !matches!(self.peek(), Token::RParen) {
18415 return Err(self.err(format!(
18416 "expected ')' after parenthesized INSERT source, got {:?}",
18417 self.peek()
18418 )));
18419 }
18420 self.advance();
18421 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18422 let returning = self.parse_optional_returning()?;
18423 return Ok(Statement::Insert(InsertStatement {
18424 ctes: Vec::new(),
18425 table,
18426 alias: alias.clone(),
18427 columns: None,
18428 rows: Vec::new(),
18429 select_source: Some(Box::new(select_stmt)),
18430 on_conflict,
18431 returning,
18432 overriding: Overriding::None,
18433 mysql_ignore: ignore,
18434 }));
18435 }
18436 let mut names = Vec::new();
18437 loop {
18438 names.push(self.expect_ident_like()?);
18439 match self.peek() {
18440 Token::Comma => {
18441 self.advance();
18442 }
18443 Token::RParen => {
18444 self.advance();
18445 break;
18446 }
18447 other => {
18448 return Err(self.err(format!(
18449 "expected ',' or ')' in INSERT column list, got {other:?}"
18450 )));
18451 }
18452 }
18453 }
18454 Some(names)
18455 } else {
18456 None
18457 };
18458 // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
18459 // OVERRIDING SYSTEM VALUE for its identity columns. The clause
18460 // is captured on the statement so the engine can apply PG's
18461 // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
18462 let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
18463 {
18464 self.advance();
18465 let which = self.expect_ident_like()?;
18466 let ov = if which.eq_ignore_ascii_case("system") {
18467 Overriding::System
18468 } else if which.eq_ignore_ascii_case("user") {
18469 Overriding::User
18470 } else {
18471 return Err(self.err(format!(
18472 "expected SYSTEM or USER after OVERRIDING, got {which:?}"
18473 )));
18474 };
18475 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
18476 return Err(self.err(format!(
18477 "expected VALUE after OVERRIDING {}, got {:?}",
18478 which.to_ascii_uppercase(),
18479 self.peek()
18480 )));
18481 }
18482 self.advance();
18483 ov
18484 } else {
18485 Overriding::None
18486 };
18487 // `INSERT INTO t DEFAULT VALUES` — a single row made
18488 // entirely of column defaults. Lower to the permuted
18489 // column-list path with an empty list: every schema column
18490 // is unmapped, so the engine fills each from its default
18491 // (serials advance, plain defaults evaluate, the rest NULL).
18492 if matches!(self.peek(), Token::Default) {
18493 self.advance();
18494 if !matches!(self.peek(), Token::Values) {
18495 return Err(self.err(format!(
18496 "expected VALUES after DEFAULT in INSERT, got {:?}",
18497 self.peek()
18498 )));
18499 }
18500 self.advance();
18501 if columns.is_some() {
18502 return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
18503 }
18504 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18505 let returning = self.parse_optional_returning()?;
18506 return Ok(Statement::Insert(InsertStatement {
18507 ctes: Vec::new(),
18508 table,
18509 alias: alias.clone(),
18510 columns: Some(Vec::new()),
18511 rows: alloc::vec![Vec::new()],
18512 select_source: None,
18513 on_conflict,
18514 returning,
18515 overriding,
18516 mysql_ignore: ignore,
18517 }));
18518 }
18519 // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
18520 // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
18521 // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
18522 // SELECT …`) heads the SOURCE select, as in PG (the statement's
18523 // own WITH comes before INSERT).
18524 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18525 let select_stmt = if self.peek_is_with_kw() {
18526 self.advance();
18527 self.parse_nested_with_select()?
18528 } else {
18529 match self.parse_select_stmt()? {
18530 Statement::Select(s) => s,
18531 other => {
18532 return Err(self.err(alloc::format!(
18533 "expected SELECT after INSERT INTO ... target, got {other:?}"
18534 )));
18535 }
18536 }
18537 };
18538 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18539 let returning = self.parse_optional_returning()?;
18540 return Ok(Statement::Insert(InsertStatement {
18541 ctes: Vec::new(),
18542 table,
18543 alias: alias.clone(),
18544 columns,
18545 rows: Vec::new(),
18546 select_source: Some(Box::new(select_stmt)),
18547 on_conflict,
18548 returning,
18549 overriding,
18550 mysql_ignore: ignore,
18551 }));
18552 }
18553 if !matches!(self.peek(), Token::Values) {
18554 return Err(self.err(format!(
18555 "expected VALUES or SELECT after table name, got {:?}",
18556 self.peek()
18557 )));
18558 }
18559 self.advance();
18560 if !matches!(self.peek(), Token::LParen) {
18561 return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
18562 }
18563 let mut rows = Vec::new();
18564 loop {
18565 // Each iteration consumes one `(expr, expr, …)` tuple.
18566 if !matches!(self.peek(), Token::LParen) {
18567 return Err(self.err(format!(
18568 "expected '(' for next VALUES tuple, got {:?}",
18569 self.peek()
18570 )));
18571 }
18572 self.advance();
18573 let mut tuple = Vec::new();
18574 loop {
18575 // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
18576 // the column's declared default for that slot. Rides out as the
18577 // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
18578 // path uses; the INSERT executor resolves it per target column.
18579 if matches!(self.peek(), Token::Default) {
18580 self.advance();
18581 tuple.push(Expr::FunctionCall {
18582 name: "__column_default".to_string(),
18583 args: Vec::new(),
18584 });
18585 } else {
18586 tuple.push(self.parse_expr(0)?);
18587 }
18588 match self.peek() {
18589 Token::Comma => {
18590 self.advance();
18591 }
18592 Token::RParen => {
18593 self.advance();
18594 break;
18595 }
18596 other => {
18597 return Err(self.err(format!(
18598 "expected ',' or ')' in VALUES tuple, got {other:?}"
18599 )));
18600 }
18601 }
18602 }
18603 if tuple.is_empty() {
18604 return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
18605 }
18606 rows.push(tuple);
18607 // Continue with comma-separated tuples.
18608 if matches!(self.peek(), Token::Comma) {
18609 self.advance();
18610 } else {
18611 break;
18612 }
18613 }
18614 // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
18615 // to ON CONFLICT DO UPDATE with an empty conflict target
18616 // (the engine picks the table's first unique index, which
18617 // matches MySQL's any-unique-key behaviour for the common
18618 // single-key case). `VALUES(col)` in the assignments is
18619 // MySQL's spelling of EXCLUDED.col.
18620 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18621 let returning = self.parse_optional_returning()?;
18622 Ok(Statement::Insert(InsertStatement {
18623 ctes: Vec::new(),
18624 table,
18625 alias,
18626 columns,
18627 rows,
18628 select_source: None,
18629 on_conflict,
18630 returning,
18631 overriding,
18632 mysql_ignore: ignore,
18633 }))
18634 }
18635
18636 /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
18637 /// the incoming row's value — exactly PG's EXCLUDED.col.
18638 fn rewrite_mysql_values_refs(e: &mut Expr) {
18639 match e {
18640 Expr::FunctionCall { name, args }
18641 if name.eq_ignore_ascii_case("values")
18642 && args.len() == 1
18643 && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
18644 {
18645 let Expr::Column(c) = &args[0] else {
18646 unreachable!("guarded above");
18647 };
18648 *e = Expr::Column(crate::ast::ColumnName {
18649 qualifier: Some("EXCLUDED".to_string()),
18650 name: c.name.clone(),
18651 });
18652 }
18653 Expr::FunctionCall { args, .. } => {
18654 for a in args {
18655 Self::rewrite_mysql_values_refs(a);
18656 }
18657 }
18658 Expr::Binary { lhs, rhs, .. } => {
18659 Self::rewrite_mysql_values_refs(lhs);
18660 Self::rewrite_mysql_values_refs(rhs);
18661 }
18662 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
18663 Self::rewrite_mysql_values_refs(expr);
18664 }
18665 Expr::Case {
18666 operand,
18667 branches,
18668 else_branch,
18669 } => {
18670 if let Some(op) = operand {
18671 Self::rewrite_mysql_values_refs(op);
18672 }
18673 for (w, t) in branches {
18674 Self::rewrite_mysql_values_refs(w);
18675 Self::rewrite_mysql_values_refs(t);
18676 }
18677 if let Some(el) = else_branch {
18678 Self::rewrite_mysql_values_refs(el);
18679 }
18680 }
18681 _ => {}
18682 }
18683 }
18684
18685 /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
18686 /// clause sitting between the INSERT body and the trailing
18687 /// RETURNING. All keywords come in as bare idents; `ON` is
18688 /// a reserved Token though.
18689 fn parse_optional_on_conflict(
18690 &mut self,
18691 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18692 if !matches!(self.peek(), Token::On) {
18693 return Ok(None);
18694 }
18695 // Peek further: we want exactly "ON CONFLICT ...". If the
18696 // next ident isn't "conflict", let some other parser handle.
18697 let next_is_conflict = matches!(
18698 self.tokens.get(self.pos + 1),
18699 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18700 );
18701 if !next_is_conflict {
18702 return Ok(None);
18703 }
18704 self.advance(); // ON
18705 self.advance(); // CONFLICT
18706 // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18707 // the constraint instead of listing columns (the pg_dump
18708 // form); the engine resolves it.
18709 let mut constraint_name: Option<String> = None;
18710 if matches!(self.peek(), Token::On) {
18711 self.advance(); // ON
18712 match self.advance() {
18713 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18714 }
18715 other => {
18716 return Err(self.err(alloc::format!(
18717 "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18718 )));
18719 }
18720 }
18721 constraint_name = Some(self.expect_ident_like()?);
18722 }
18723 // Optional `(col [, col]*)` target list.
18724 let mut target_columns: Vec<String> = Vec::new();
18725 if matches!(self.peek(), Token::LParen) {
18726 self.advance();
18727 loop {
18728 target_columns.push(self.expect_ident_like()?);
18729 match self.peek() {
18730 Token::Comma => {
18731 self.advance();
18732 }
18733 Token::RParen => {
18734 self.advance();
18735 break;
18736 }
18737 other => {
18738 return Err(self.err(alloc::format!(
18739 "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18740 )));
18741 }
18742 }
18743 }
18744 }
18745 // v7.39 (round 240) — optional index predicate after the target
18746 // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18747 // PARTIAL unique index; SPG's arbiters are full indexes, which
18748 // satisfy any predicate, so it is parsed and carried but not
18749 // consulted (recorded residual: partial-unique-index arbiters).
18750 let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18751 self.advance();
18752 Some(self.parse_expr(0)?)
18753 } else {
18754 None
18755 };
18756 // Required `DO`.
18757 match self.advance() {
18758 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18759 other => {
18760 return Err(self.err(alloc::format!(
18761 "expected DO after ON CONFLICT [(…)], got {other:?}"
18762 )));
18763 }
18764 }
18765 // Action: NOTHING | UPDATE SET …
18766 let action = match self.advance() {
18767 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18768 crate::ast::OnConflictAction::Nothing
18769 }
18770 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18771 self.parse_on_conflict_update_action()?
18772 }
18773 other => {
18774 return Err(self.err(alloc::format!(
18775 "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18776 )));
18777 }
18778 };
18779 Ok(Some(crate::ast::OnConflictClause {
18780 target_columns,
18781 index_where,
18782 constraint_name,
18783 mysql_lowered: false,
18784 action,
18785 }))
18786 }
18787
18788 /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18789 /// `SET col = expr [, …] [WHERE cond]`. Caller already
18790 /// consumed `UPDATE`.
18791 fn parse_on_conflict_update_action(
18792 &mut self,
18793 ) -> Result<crate::ast::OnConflictAction, ParseError> {
18794 // `SET`
18795 match self.advance() {
18796 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18797 other => {
18798 return Err(self.err(alloc::format!(
18799 "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18800 )));
18801 }
18802 }
18803 let mut assignments: Vec<(String, Expr)> = Vec::new();
18804 loop {
18805 let col = self.expect_ident_like()?;
18806 if !matches!(self.peek(), Token::Eq) {
18807 return Err(self.err(alloc::format!(
18808 "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18809 self.peek()
18810 )));
18811 }
18812 self.advance();
18813 let value = self.parse_expr(0)?;
18814 assignments.push((col, value));
18815 if matches!(self.peek(), Token::Comma) {
18816 self.advance();
18817 continue;
18818 }
18819 break;
18820 }
18821 let where_ = if matches!(self.peek(), Token::Where) {
18822 self.advance();
18823 Some(self.parse_expr(0)?)
18824 } else {
18825 None
18826 };
18827 Ok(crate::ast::OnConflictAction::Update {
18828 assignments,
18829 where_,
18830 })
18831 }
18832
18833 fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18834 let mut items = Vec::new();
18835 // v7.39 (round 341, V66) — PG's target list may be EMPTY
18836 // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18837 // answers one zero-column row per row of t, and a bare `SELECT`
18838 // answers a single zero-column row. SPG required at least one
18839 // item, so both were syntax errors. Recognised by the token that
18840 // follows — nothing that can start an expression appears here.
18841 if self.select_list_is_empty_here() {
18842 return Ok(items);
18843 }
18844 loop {
18845 items.push(self.parse_select_item()?);
18846 if matches!(self.peek(), Token::Comma) {
18847 self.advance();
18848 } else {
18849 break;
18850 }
18851 }
18852 Ok(items)
18853 }
18854
18855 /// Is the target list empty at this point — i.e. does the next token
18856 /// end the SELECT's item list rather than start an item?
18857 fn select_list_is_empty_here(&self) -> bool {
18858 match self.peek() {
18859 Token::From
18860 | Token::Where
18861 | Token::Group
18862 | Token::Having
18863 | Token::Order
18864 | Token::Limit
18865 | Token::Offset
18866 | Token::Semicolon
18867 | Token::RParen
18868 | Token::Union
18869 | Token::Except
18870 | Token::Eof => true,
18871 // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18872 // with unreserved keywords, so they arrive as plain idents.
18873 Token::Ident(s) => {
18874 s.eq_ignore_ascii_case("fetch")
18875 || s.eq_ignore_ascii_case("window")
18876 || s.eq_ignore_ascii_case("intersect")
18877 }
18878 _ => false,
18879 }
18880 }
18881
18882 fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18883 if matches!(self.peek(), Token::Star) {
18884 self.advance();
18885 return Ok(SelectItem::Wildcard);
18886 }
18887 // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18888 // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18889 // choke on the `*` ("expected identifier, got Star"). The lookahead is
18890 // `<ident> . *` with nothing binding tighter.
18891 if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18892 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18893 && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18894 {
18895 self.advance(); // qualifier
18896 self.advance(); // .
18897 self.advance(); // *
18898 return Ok(SelectItem::QualifiedWildcard(q));
18899 }
18900 }
18901 let start_tok = self.pos;
18902 let expr = self.parse_expr(0)?;
18903 let end_tok = self.consumed_pos();
18904 // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18905 // multi-column function returns into columns. Marked here and lowered in
18906 // `parse_bare_select`, where the FROM clause is in hand.
18907 if matches!(self.peek(), Token::Dot)
18908 && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18909 {
18910 self.advance(); // .
18911 self.advance(); // *
18912 return Ok(SelectItem::Expr {
18913 expr: Expr::FunctionCall {
18914 name: "__record_expand".to_string(),
18915 args: alloc::vec![expr],
18916 },
18917 alias: None,
18918 });
18919 }
18920 // v7.39.2 — MySQL lets a STRING name a projection item, with or
18921 // without `AS`: `SELECT 1 'x'`, `SELECT COUNT(*) 'total'`,
18922 // `SELECT 1 'a b'` (which is why one quotes it). SPG answered
18923 // `syntax error at or near "'x'"` to all of them.
18924 //
18925 // Only here, not in `parse_optional_alias`: that one also names
18926 // TABLES, and MySQL 9.7.2 refuses a string there — `FROM t 'ta'`
18927 // and `FROM t AS 'ta'` are both syntax errors, measured. And only
18928 // after the lexer's own rule has joined adjacent literals, or
18929 // `SELECT 'a' 'b'` would read as a literal aliased `b` where
18930 // MySQL answers the concatenation `ab`.
18931 if self.mysql_dialect {
18932 let at_as = matches!(self.peek(), Token::As)
18933 && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)));
18934 if at_as {
18935 self.advance();
18936 }
18937 if let Token::String(name) = self.peek().clone() {
18938 self.advance();
18939 return Ok(SelectItem::Expr {
18940 expr,
18941 alias: Some(name),
18942 });
18943 }
18944 }
18945 let alias = match self.parse_optional_alias()? {
18946 Some(a) => Some(a),
18947 None => self.mysql_item_label(&expr, start_tok, end_tok),
18948 };
18949 Ok(SelectItem::Expr { expr, alias })
18950 }
18951
18952 /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18953 /// carries no `AS`, filled in here so every downstream path reports it
18954 /// without knowing the rule. `None` leaves the item un-aliased, which is
18955 /// what a PG session always gets.
18956 ///
18957 /// Measured against MariaDB 11, three rules and no more:
18958 ///
18959 /// | item | label | why |
18960 /// |------------------|------------|------------------------------|
18961 /// | `lbl.a` | `a` | a column reports its name |
18962 /// | `'it''s'` | `it's` | a string reports its VALUE |
18963 /// | `a + b` | `a + b` | anything else, source text |
18964 ///
18965 /// The third is why this lives in the parser at all: the label is the
18966 /// text the client WROTE, down to the spacing, so it cannot be printed
18967 /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18968 ///
18969 /// Comments survive, and that is right: through a `mariadb` CLI both
18970 /// servers answer `a + b` for `SELECT a /* c */ + b`, but that is the
18971 /// CLIENT stripping the comment before it sends. Asked over the raw
18972 /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18973 /// produces.
18974 fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18975 if !self.mysql_dialect {
18976 return None;
18977 }
18978 match expr {
18979 // A column already reports its own name downstream; naming it
18980 // again here would only re-state the qualifier the label drops.
18981 Expr::Column(_) => None,
18982 // v7.39.3 — `SELECT 'a' 'b'` is ONE literal whose value is
18983 // `ab`, and MySQL 9.7.2 names the column `a`: the label is
18984 // the first segment as written, not the joined value
18985 // (measured). The lexer logs where it joined them.
18986 Expr::Literal(Literal::String(v)) => Some(
18987 self.merged_first_len(start_tok)
18988 .and_then(|n| v.get(..n))
18989 .map_or_else(|| v.clone(), String::from),
18990 ),
18991 _ => self.source_span(start_tok, end_tok).map(str::to_string),
18992 }
18993 }
18994
18995 /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18996 /// consumed VALUES keyword. Each row lowers to a constant SELECT
18997 /// with PG's default column1..columnN names; subsequent rows
18998 /// chain as UNION ALL peers. Shared by the FROM-position
18999 /// `( VALUES … )` arm and the top-level bare VALUES statement.
19000 fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
19001 let mut row_selects: Vec<SelectStatement> = Vec::new();
19002 loop {
19003 if !matches!(self.peek(), Token::LParen) {
19004 return Err(self.err(alloc::format!(
19005 "expected '(' to start a VALUES row, got {:?}",
19006 self.peek()
19007 )));
19008 }
19009 self.advance(); // (
19010 let mut items: Vec<SelectItem> = Vec::new();
19011 loop {
19012 let expr = self.parse_expr(0)?;
19013 items.push(SelectItem::Expr {
19014 expr,
19015 alias: Some(alloc::format!("column{}", items.len() + 1)),
19016 });
19017 match self.peek() {
19018 Token::Comma => {
19019 self.advance();
19020 }
19021 Token::RParen => break,
19022 other => {
19023 return Err(self.err(alloc::format!(
19024 "expected ',' or ')' in VALUES row, got {other:?}"
19025 )));
19026 }
19027 }
19028 }
19029 self.advance(); // )
19030 row_selects.push(SelectStatement {
19031 locking: None,
19032 ctes: Vec::new(),
19033 distinct: false,
19034 distinct_on: Vec::new(),
19035 items,
19036 from: None,
19037 where_: None,
19038 group_by: None,
19039 group_by_all: false,
19040 having: None,
19041 unions: Vec::new(),
19042 order_by: Vec::new(),
19043 limit: None,
19044 offset: None,
19045 limit_with_ties: false,
19046 window_check_exprs: Vec::new(),
19047 });
19048 if matches!(self.peek(), Token::Comma) {
19049 self.advance();
19050 continue;
19051 }
19052 break;
19053 }
19054 let mut head = row_selects.remove(0);
19055 head.unions = row_selects
19056 .into_iter()
19057 .map(|s| (UnionKind::All, s))
19058 .collect();
19059 Ok(head)
19060 }
19061
19062 fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
19063 // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
19064 // children. It was read as a table NAMED `only`, so the query
19065 // failed on `relation "only" does not exist`.
19066 //
19067 // v7.39 (round 644) — and it is no longer a no-op. Round 621
19068 // absorbed the keyword, reasoning that SPG's children are
19069 // separate relations a plain scan does not descend into, so ONLY
19070 // already described the scan. That stopped being true when a
19071 // partition parent started unioning its children: measured,
19072 // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
19073 // where PG answers 0. The flag is carried now.
19074 let mut only = false;
19075 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
19076 && matches!(
19077 self.tokens.get(self.pos + 1),
19078 Some(Token::Ident(_) | Token::QuotedIdent(_))
19079 )
19080 {
19081 only = true;
19082 self.advance();
19083 }
19084 // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
19085 // for these SRFs the keyword is noise at parse time: the
19086 // join executor already substitutes outer-column references
19087 // into unnest_expr / generate_series_args per outer row
19088 // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
19089 // licences the correlation even without the keyword. Absorb
19090 // it and fall through to the SRF arms below.
19091 // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
19092 // just the four builtin SRFs: a user set-returning function on a JOIN's
19093 // right side is the whole point of LATERAL. The keyword stays noise at
19094 // parse time — the join executor substitutes the outer row into the
19095 // call's arguments per outer row.
19096 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19097 && matches!(
19098 self.tokens.get(self.pos + 1),
19099 // The json_each family has its OWN `LATERAL …` arm below, which
19100 // needs to see the keyword — absorbing it here would send those
19101 // calls down the generic table-function channel instead.
19102 Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
19103 )
19104 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19105 {
19106 self.advance(); // LATERAL
19107 }
19108 // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
19109 // set-returning function whose argument may reference a
19110 // preceding FROM item. We rewrite this to
19111 // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
19112 // AS __srf__) AS <alias>` so the existing LATERAL subquery
19113 // executor handles per-outer-row evaluation and the
19114 // SRF-primary jsonb_each_text path handles the inner
19115 // materialisation. Sentori 0067 backfill is the dogfood
19116 // shape.
19117 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19118 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
19119 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19120 {
19121 self.advance(); // LATERAL
19122 let each_fn = match self.peek() {
19123 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19124 _ => unreachable!(),
19125 };
19126 self.advance(); // jsonb_each[_text] / json_each[_text]
19127 self.advance(); // (
19128 let arg = self.parse_expr(0)?;
19129 if !matches!(self.peek(), Token::RParen) {
19130 return Err(self.err(alloc::format!(
19131 "expected ')' after LATERAL {each_fn}() argument, got {:?}",
19132 self.peek()
19133 )));
19134 }
19135 self.advance();
19136 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19137 let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19138 // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
19139 // FROM jsonb_each_text(<arg>) AS __srf__
19140 // PG's `AS kv(key, value)` column-alias list maps
19141 // positions to names; default to (key, value) when
19142 // omitted (matching the SRF's natural column names).
19143 let srf_alias = "__srf__".to_string();
19144 let key_alias = column_aliases
19145 .first()
19146 .cloned()
19147 .unwrap_or_else(|| "key".to_string());
19148 let value_alias = column_aliases
19149 .get(1)
19150 .cloned()
19151 .unwrap_or_else(|| "value".to_string());
19152 let inner_select = crate::ast::SelectStatement {
19153 locking: None,
19154 ctes: Vec::new(),
19155 distinct: false,
19156 distinct_on: Vec::new(),
19157 items: alloc::vec![
19158 crate::ast::SelectItem::Expr {
19159 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19160 qualifier: Some(srf_alias.clone()),
19161 name: "key".to_string(),
19162 }),
19163 alias: Some(key_alias),
19164 },
19165 crate::ast::SelectItem::Expr {
19166 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19167 qualifier: Some(srf_alias.clone()),
19168 name: "value".to_string(),
19169 }),
19170 alias: Some(value_alias),
19171 },
19172 ],
19173 from: Some(crate::ast::FromClause {
19174 primary: TableRef {
19175 name: srf_alias.clone(),
19176 alias: Some(srf_alias.clone()),
19177 only: false,
19178 as_of_segment: None,
19179 unnest_expr: None,
19180 unnest_column_aliases: Vec::new(),
19181 with_ordinality: false,
19182 generate_series_args: None,
19183 lateral_subquery: None,
19184 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19185 table_fn_call: None,
19186 rows_from: None,
19187 json_table: None,
19188 scalar_fn_item: false,
19189 },
19190 joins: Vec::new(),
19191 }),
19192 where_: None,
19193 group_by: None,
19194 group_by_all: false,
19195 having: None,
19196 unions: Vec::new(),
19197 order_by: Vec::new(),
19198 limit: None,
19199 offset: None,
19200 limit_with_ties: false,
19201 window_check_exprs: Vec::new(),
19202 };
19203 return Ok(TableRef {
19204 name: alias.clone(),
19205 alias: Some(alias),
19206 only: false,
19207 as_of_segment: None,
19208 unnest_expr: None,
19209 unnest_column_aliases: Vec::new(),
19210 with_ordinality: false,
19211 generate_series_args: None,
19212 lateral_subquery: Some(Box::new(inner_select)),
19213 jsonb_each_text_arg: None,
19214 table_fn_call: None,
19215 rows_from: None,
19216 json_table: None,
19217 scalar_fn_item: false,
19218 });
19219 }
19220 // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
19221 // without an explicit `LATERAL` keyword is the same shape
19222 // PG accepts (SRF naturally licences lateral correlation).
19223 // We mirror the LATERAL rewrite when the argument syntactic-
19224 // ally references an outer column (Column { qualifier:
19225 // Some(_), … }). For simplicity we apply the rewrite
19226 // whenever the SRF directly follows JOIN/CROSS JOIN/comma
19227 // in the FROM-list — caller-side join parsing positions
19228 // this peek correctly.
19229 // (Implementation note: detection lives below; the LATERAL
19230 // branch above already covers the explicit form; the bare
19231 // form falls through to the plain SRF arm and the engine
19232 // treats it as a constant-arg SRF if no outer reference is
19233 // present.)
19234 // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
19235 // table. Detect at the head so it claims precedence over
19236 // every other table-ref shape (unnest / generate_series /
19237 // bare ident); the lateral subquery itself follows the
19238 // regular SELECT grammar.
19239 // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
19240 // t(cols)`. Each row lowers to a constant SELECT with PG's
19241 // default column1..columnN names; subsequent rows chain as
19242 // UNION ALL peers. The result rides the derived-table
19243 // lateral_subquery channel — zero executor work.
19244 if matches!(self.peek(), Token::LParen)
19245 && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
19246 {
19247 self.advance(); // (
19248 self.advance(); // VALUES
19249 let head = self.parse_values_rows_body()?;
19250 if !matches!(self.peek(), Token::RParen) {
19251 return Err(self.err(alloc::format!(
19252 "expected ')' after VALUES list, got {:?}",
19253 self.peek()
19254 )));
19255 }
19256 self.advance();
19257 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19258 let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
19259 return Ok(TableRef {
19260 name,
19261 alias: alias_ident,
19262 only: false,
19263 as_of_segment: None,
19264 unnest_expr: None,
19265 unnest_column_aliases: column_aliases,
19266 with_ordinality: false,
19267 generate_series_args: None,
19268 lateral_subquery: Some(Box::new(head)),
19269 jsonb_each_text_arg: None,
19270 table_fn_call: None,
19271 rows_from: None,
19272 json_table: None,
19273 scalar_fn_item: false,
19274 });
19275 }
19276 // v7.37.17 (17.6 siblings) — plain derived table:
19277 // `FROM ( SELECT … ) [AS] alias`. Rides the same
19278 // lateral_subquery channel the explicit LATERAL form uses —
19279 // an uncorrelated inner SELECT executes identically. The
19280 // inner parse carries UNION tails (they live on
19281 // SelectStatement.unions).
19282 // v7.37 D.20 — the derived-table inner may itself be a
19283 // parenthesized set-operation group (`FROM ((SELECT…) UNION
19284 // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
19285 // bare `(SELECT …)`. parse_one_statement already routes a leading
19286 // `(` set-op group (its LParen arm) and a leading WITH
19287 // (parse_with_cte_then_select), so widen the second-token gate to
19288 // Select | LParen | WITH.
19289 // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
19290 // PG's spelling of `SELECT * FROM t` and is accepted wherever a
19291 // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
19292 // has existed since the shorthand landed and `parse_bare_select`
19293 // already routes it ("valid anywhere a SELECT head is"); what was
19294 // missing is this second-token gate, and the CTE body's dispatch
19295 // below. Round 868 found both by putting the shorthand in a
19296 // subquery — the top-level forms had been the only ones tested.
19297 if matches!(self.peek(), Token::LParen)
19298 && (matches!(
19299 self.tokens.get(self.pos + 1),
19300 Some(Token::Select | Token::LParen | Token::Table)
19301 ) || matches!(self.tokens.get(self.pos + 1),
19302 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
19303 {
19304 self.advance(); // (
19305 let inner = match self.parse_one_statement()? {
19306 Statement::Select(s) => s,
19307 other => {
19308 return Err(self.err(alloc::format!(
19309 "expected SELECT inside derived table ( … ), got {other:?}"
19310 )));
19311 }
19312 };
19313 if !matches!(self.peek(), Token::RParen) {
19314 return Err(self.err(alloc::format!(
19315 "expected ')' after derived-table subquery, got {:?}",
19316 self.peek()
19317 )));
19318 }
19319 self.advance();
19320 // `AS t(a, b)` column-alias list rides the
19321 // unnest_column_aliases field (same positional-rename
19322 // contract the unnest SRFs use).
19323 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19324 let name = alias_ident
19325 .clone()
19326 .unwrap_or_else(|| "subquery".to_string());
19327 return Ok(TableRef {
19328 name,
19329 alias: alias_ident,
19330 only: false,
19331 as_of_segment: None,
19332 unnest_expr: None,
19333 unnest_column_aliases: column_aliases,
19334 with_ordinality: false,
19335 generate_series_args: None,
19336 lateral_subquery: Some(Box::new(inner)),
19337 jsonb_each_text_arg: None,
19338 table_fn_call: None,
19339 rows_from: None,
19340 json_table: None,
19341 scalar_fn_item: false,
19342 });
19343 }
19344 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19345 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19346 {
19347 self.advance(); // LATERAL
19348 self.advance(); // (
19349 // Parse the inner SELECT.
19350 let inner = match self.parse_one_statement()? {
19351 Statement::Select(s) => s,
19352 other => {
19353 return Err(self.err(alloc::format!(
19354 "expected SELECT inside LATERAL ( … ), got {other:?}"
19355 )));
19356 }
19357 };
19358 if !matches!(self.peek(), Token::RParen) {
19359 return Err(self.err(alloc::format!(
19360 "expected ')' after LATERAL subquery, got {:?}",
19361 self.peek()
19362 )));
19363 }
19364 self.advance();
19365 // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
19366 // `(VALUES …) t(g)` derived table round-trips through view-body
19367 // Display, which renders on the lateral_subquery channel).
19368 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19369 let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
19370 return Ok(TableRef {
19371 name,
19372 alias: alias_ident,
19373 only: false,
19374 as_of_segment: None,
19375 unnest_expr: None,
19376 unnest_column_aliases: column_aliases,
19377 with_ordinality: false,
19378 generate_series_args: None,
19379 lateral_subquery: Some(Box::new(inner)),
19380 jsonb_each_text_arg: None,
19381 table_fn_call: None,
19382 rows_from: None,
19383 json_table: None,
19384 scalar_fn_item: false,
19385 });
19386 }
19387 // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
19388 // function as a FROM item. Emits one row per (key, value)
19389 // pair in the JSONB object argument as TEXT columns. May
19390 // be wrapped in CROSS JOIN LATERAL when the argument
19391 // references a preceding FROM item (sentori migration
19392 // 0067 backfill shape: `CROSS JOIN LATERAL
19393 // jsonb_each_text(t.json_col) AS kv(key, value)`).
19394 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
19395 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19396 {
19397 let each_fn = match self.peek() {
19398 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19399 _ => unreachable!(),
19400 };
19401 self.advance(); // jsonb_each[_text] / json_each[_text]
19402 self.advance(); // (
19403 let arg = self.parse_expr(0)?;
19404 if !matches!(self.peek(), Token::RParen) {
19405 return Err(self.err(alloc::format!(
19406 "expected ')' after {each_fn}() argument, got {:?}",
19407 self.peek()
19408 )));
19409 }
19410 self.advance();
19411 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19412 let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19413 return Ok(TableRef {
19414 name,
19415 alias: alias_ident,
19416 only: false,
19417 as_of_segment: None,
19418 unnest_expr: None,
19419 // `AS t(k, v)` renames key/value positionally, same as the
19420 // LATERAL-position form already does.
19421 unnest_column_aliases: column_aliases,
19422 with_ordinality: false,
19423 generate_series_args: None,
19424 lateral_subquery: None,
19425 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19426 table_fn_call: None,
19427 rows_from: None,
19428 json_table: None,
19429 scalar_fn_item: false,
19430 });
19431 }
19432 // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
19433 // (+ json_ variants) — record-returning JSON functions with a
19434 // column-definition list. Desugar to a derived table that
19435 // projects each declared column from the JSON via `->>` + a cast,
19436 // over `jsonb_array_elements(J)` for the *set (per-element) form.
19437 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
19438 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19439 {
19440 return self.parse_json_to_record_from();
19441 }
19442 // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
19443 // row is a text[] of capture groups, so it cannot desugar to unnest
19444 // (that would flatten the array). Wrap it as a derived table
19445 // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
19446 // SRF path already emits one text[] row per match. PG names the column
19447 // `regexp_matches`; an `AS a(col)` alias overrides it.
19448 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19449 if s.eq_ignore_ascii_case("regexp_matches"))
19450 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19451 {
19452 self.advance(); // fn name
19453 self.advance(); // (
19454 let mut fn_args: Vec<Expr> = Vec::new();
19455 loop {
19456 fn_args.push(self.parse_expr(0)?);
19457 if matches!(self.peek(), Token::Comma) {
19458 self.advance();
19459 continue;
19460 }
19461 break;
19462 }
19463 if !matches!(self.peek(), Token::RParen) {
19464 return Err(self.err(alloc::format!(
19465 "expected ')' after regexp_matches() arguments, got {:?}",
19466 self.peek()
19467 )));
19468 }
19469 self.advance();
19470 // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
19471 // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
19472 // it, so it died on the `with` token while every other table function
19473 // accepted it.
19474 let with_ordinality = self.absorb_with_ordinality();
19475 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19476 let table_alias = alias_ident
19477 .clone()
19478 .unwrap_or_else(|| "regexp_matches".to_string());
19479 // PG names a single-column function's output column after the ALIAS
19480 // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
19481 // `m` reads as that column and not as a whole-row composite. Naming
19482 // it after the function regardless made `SELECT m[1] FROM … AS m`
19483 // subscript a record.
19484 let col_name = column_aliases
19485 .first()
19486 .cloned()
19487 .or_else(|| alias_ident.clone())
19488 .unwrap_or_else(|| "regexp_matches".to_string());
19489 let inner = crate::ast::SelectStatement {
19490 locking: None,
19491 ctes: Vec::new(),
19492 distinct: false,
19493 distinct_on: Vec::new(),
19494 items: alloc::vec![SelectItem::Expr {
19495 expr: Expr::FunctionCall {
19496 name: "regexp_matches".to_string(),
19497 args: fn_args,
19498 },
19499 alias: Some(col_name),
19500 }],
19501 from: None,
19502 where_: None,
19503 group_by: None,
19504 group_by_all: false,
19505 having: None,
19506 unions: Vec::new(),
19507 order_by: Vec::new(),
19508 limit: None,
19509 offset: None,
19510 limit_with_ties: false,
19511 window_check_exprs: Vec::new(),
19512 };
19513 return Ok(TableRef {
19514 name: table_alias.clone(),
19515 alias: Some(table_alias),
19516 only: false,
19517 as_of_segment: None,
19518 unnest_expr: None,
19519 unnest_column_aliases: column_aliases,
19520 with_ordinality,
19521 generate_series_args: None,
19522 lateral_subquery: Some(Box::new(inner)),
19523 jsonb_each_text_arg: None,
19524 table_fn_call: None,
19525 rows_from: None,
19526 json_table: None,
19527 // regexp_matches returns text[], a base type: `SELECT m FROM
19528 // regexp_matches(…) AS m` is the array, not a composite wrapping it.
19529 scalar_fn_item: true,
19530 });
19531 }
19532 // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
19533 // / json_ variants as a FROM item. Rewritten into
19534 // `unnest(<same fn>(<expr>))`: the scalar form returns the
19535 // elements as a TEXT array, and the existing unnest SRF path
19536 // materialises one row per element. PG's natural column name
19537 // is `value`; an `AS a(col)` column-alias list overrides it.
19538 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19539 if s.eq_ignore_ascii_case("jsonb_array_elements")
19540 || s.eq_ignore_ascii_case("json_array_elements")
19541 || s.eq_ignore_ascii_case("jsonb_array_elements_text")
19542 || s.eq_ignore_ascii_case("json_array_elements_text")
19543 || s.eq_ignore_ascii_case("jsonb_object_keys")
19544 || s.eq_ignore_ascii_case("json_object_keys")
19545 || s.eq_ignore_ascii_case("jsonb_path_query")
19546 || s.eq_ignore_ascii_case("json_path_query")
19547 || s.eq_ignore_ascii_case("generate_subscripts")
19548 || s.eq_ignore_ascii_case("string_to_table")
19549 || s.eq_ignore_ascii_case("regexp_split_to_table"))
19550 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19551 {
19552 let fn_name = match self.peek() {
19553 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19554 _ => unreachable!(),
19555 };
19556 self.advance(); // fn name
19557 self.advance(); // (
19558 let mut fn_args: Vec<Expr> = Vec::new();
19559 loop {
19560 fn_args.push(self.parse_expr(0)?);
19561 if matches!(self.peek(), Token::Comma) {
19562 self.advance();
19563 continue;
19564 }
19565 break;
19566 }
19567 if !matches!(self.peek(), Token::RParen) {
19568 return Err(self.err(alloc::format!(
19569 "expected ')' after {fn_name}() arguments, got {:?}",
19570 self.peek()
19571 )));
19572 }
19573 self.advance();
19574 let with_ordinality = self.absorb_with_ordinality();
19575 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19576 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
19577 // PG's natural column name: the array-elements SRFs
19578 // declare an OUT parameter `value`; jsonb_object_keys
19579 // and generate_subscripts have none, so the column is
19580 // named after the function. A bare table alias on a
19581 // single-column SRF renames the column too (PG: `FROM
19582 // generate_subscripts(a, 1) AS s` projects column s) —
19583 // except for the OUT-parameter SRFs, whose column stays
19584 // `value` under a bare alias.
19585 let natural_col = if fn_name.ends_with("_array_elements")
19586 || fn_name.ends_with("_array_elements_text")
19587 {
19588 "value".to_string()
19589 } else {
19590 alias_ident.clone().unwrap_or_else(|| fn_name.clone())
19591 };
19592 let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
19593 // Keep any further entries — the second names the
19594 // ordinality column under WITH ORDINALITY.
19595 srf_cols.extend(column_aliases.into_iter().skip(1));
19596 // The *_to_table SRFs are row-streams over the existing
19597 // *_to_array scalars — map the call target; the display
19598 // name (alias / column defaults) keeps the SRF spelling.
19599 let call_name = match fn_name.as_str() {
19600 "string_to_table" => "string_to_array".to_string(),
19601 "regexp_split_to_table" => "regexp_split_to_array".to_string(),
19602 _ => fn_name,
19603 };
19604 // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
19605 // preceding FROM item (bare or qualified column) is correlated;
19606 // route it through the per-outer-row lateral channel.
19607 let expr = crate::ast::Expr::FunctionCall {
19608 name: call_name,
19609 args: fn_args,
19610 };
19611 let correlated = Self::expr_has_any_column(&expr);
19612 let tref = TableRef {
19613 name,
19614 alias: alias_ident,
19615 only: false,
19616 as_of_segment: None,
19617 unnest_expr: Some(Box::new(expr)),
19618 unnest_column_aliases: srf_cols,
19619 with_ordinality,
19620 generate_series_args: None,
19621 lateral_subquery: None,
19622 jsonb_each_text_arg: None,
19623 table_fn_call: None,
19624 rows_from: None,
19625 json_table: None,
19626 // Each of these returns a BASE type (jsonb / text / int), so the item's
19627 // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
19628 // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
19629 scalar_fn_item: !with_ordinality,
19630 };
19631 return Ok(if correlated {
19632 Self::wrap_correlated_srf(tref)
19633 } else {
19634 tref
19635 });
19636 }
19637 // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
19638 // explicit parallel-zip syntax. Each entry lowers to its
19639 // array-returning scalar form (unnest(x) → x itself; the
19640 // FROM-SRF rewrite family → their scalar array calls) and
19641 // the list rides the multi-arg unnest zip channel:
19642 // NULL-padded to the longest, WITH ORDINALITY appends the
19643 // counter. generate_series has no scalar array form and
19644 // errors honestly.
19645 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
19646 && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
19647 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19648 {
19649 self.advance(); // ROWS
19650 self.advance(); // FROM
19651 self.advance(); // (
19652 let mut entries: Vec<Expr> = Vec::new();
19653 // v7.39 (read01 round 74) — the generic channel, filled in parallel.
19654 // Used only when some entry has no array form.
19655 let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
19656 loop {
19657 let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
19658 if !matches!(self.peek(), Token::LParen) {
19659 return Err(self.err(alloc::format!(
19660 "expected '(' after {fn_name} in ROWS FROM, got {:?}",
19661 self.peek()
19662 )));
19663 }
19664 self.advance();
19665 let mut fn_args: Vec<Expr> = Vec::new();
19666 if !matches!(self.peek(), Token::RParen) {
19667 loop {
19668 fn_args.push(self.parse_expr(0)?);
19669 if matches!(self.peek(), Token::Comma) {
19670 self.advance();
19671 continue;
19672 }
19673 break;
19674 }
19675 }
19676 if !matches!(self.peek(), Token::RParen) {
19677 return Err(self.err(alloc::format!(
19678 "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
19679 self.peek()
19680 )));
19681 }
19682 self.advance();
19683 let entry = match fn_name.as_str() {
19684 "unnest" => {
19685 if fn_args.len() != 1 {
19686 return Err(
19687 self.err("unnest inside ROWS FROM takes exactly one array".into())
19688 );
19689 }
19690 fn_args.pop().expect("len checked")
19691 }
19692 "jsonb_array_elements"
19693 | "json_array_elements"
19694 | "jsonb_array_elements_text"
19695 | "json_array_elements_text"
19696 | "jsonb_object_keys"
19697 | "json_object_keys"
19698 | "generate_subscripts" => crate::ast::Expr::FunctionCall {
19699 name: fn_name,
19700 args: fn_args,
19701 },
19702 "string_to_table" => crate::ast::Expr::FunctionCall {
19703 name: "string_to_array".to_string(),
19704 args: fn_args,
19705 },
19706 "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
19707 name: "regexp_split_to_array".to_string(),
19708 args: fn_args,
19709 },
19710 // v7.39 (read01 round 74) — an SRF with no array form
19711 // (`generate_series`, a user `RETURNS SETOF` function) has no
19712 // scalar expression to zip, so the WHOLE list switches to the
19713 // rows_from channel, which runs each function and zips the
19714 // rows themselves. The all-array case keeps the old lowering:
19715 // it is well-trodden and this must not disturb it.
19716 _ => {
19717 generic.push((fn_name, fn_args));
19718 if matches!(self.peek(), Token::Comma) {
19719 self.advance();
19720 continue;
19721 }
19722 break;
19723 }
19724 };
19725 generic.push((
19726 // The array-able entries carry their lowered expr along, so a
19727 // MIXED list still works: the engine sees the scalar array
19728 // form and unnests it.
19729 "__array".to_string(),
19730 alloc::vec![entry.clone()],
19731 ));
19732 entries.push(entry);
19733 if matches!(self.peek(), Token::Comma) {
19734 self.advance();
19735 continue;
19736 }
19737 break;
19738 }
19739 if !matches!(self.peek(), Token::RParen) {
19740 return Err(self.err(alloc::format!(
19741 "expected ')' to close ROWS FROM, got {:?}",
19742 self.peek()
19743 )));
19744 }
19745 self.advance();
19746 let with_ordinality = self.absorb_with_ordinality();
19747 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19748 let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19749 // v7.39 (read01 round 74) — some entry had no array form, so the whole
19750 // list rides the generic channel.
19751 if generic.iter().any(|(n, _)| n != "__array") {
19752 let correlated = generic
19753 .iter()
19754 .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19755 let tref = TableRef {
19756 name,
19757 alias: alias_ident,
19758 only: false,
19759 as_of_segment: None,
19760 unnest_expr: None,
19761 unnest_column_aliases,
19762 with_ordinality,
19763 generate_series_args: None,
19764 lateral_subquery: None,
19765 jsonb_each_text_arg: None,
19766 table_fn_call: None,
19767 rows_from: Some(generic),
19768 json_table: None,
19769 scalar_fn_item: false,
19770 };
19771 return Ok(if correlated {
19772 Self::wrap_correlated_srf(tref)
19773 } else {
19774 tref
19775 });
19776 }
19777 let correlated = entries.iter().any(Self::expr_has_any_column);
19778 let expr = if entries.len() == 1 {
19779 entries.pop().expect("len checked")
19780 } else {
19781 crate::ast::Expr::FunctionCall {
19782 name: "__unnest_zip".to_string(),
19783 args: entries,
19784 }
19785 };
19786 let tref = TableRef {
19787 name,
19788 alias: alias_ident,
19789 only: false,
19790 as_of_segment: None,
19791 unnest_expr: Some(Box::new(expr)),
19792 unnest_column_aliases,
19793 with_ordinality,
19794 generate_series_args: None,
19795 lateral_subquery: None,
19796 jsonb_each_text_arg: None,
19797 table_fn_call: None,
19798 rows_from: None,
19799 json_table: None,
19800 scalar_fn_item: false,
19801 };
19802 return Ok(if correlated {
19803 Self::wrap_correlated_srf(tref)
19804 } else {
19805 tref
19806 });
19807 }
19808 // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19809 // source. Detect at the head before the bare-ident fallback;
19810 // unnest is not a reserved token.
19811 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19812 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19813 {
19814 self.advance(); // unnest
19815 self.advance(); // (
19816 let mut srf_args = alloc::vec![self.parse_expr(0)?];
19817 while matches!(self.peek(), Token::Comma) {
19818 self.advance();
19819 srf_args.push(self.parse_expr(0)?);
19820 }
19821 if !matches!(self.peek(), Token::RParen) {
19822 return Err(self.err(alloc::format!(
19823 "expected ')' after unnest() argument, got {:?}",
19824 self.peek()
19825 )));
19826 }
19827 self.advance();
19828 // Multi-arg unnest(a, b, …) zips the arrays in
19829 // parallel, NULL-padding to the longest (PG's ROWS
19830 // FROM shorthand). Lower onto the unnest channel as an
19831 // internal marker call the executors unpack.
19832 let expr = if srf_args.len() == 1 {
19833 srf_args.pop().expect("len checked")
19834 } else {
19835 crate::ast::Expr::FunctionCall {
19836 name: "__unnest_zip".to_string(),
19837 args: srf_args,
19838 }
19839 };
19840 let with_ordinality = self.absorb_with_ordinality();
19841 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19842 let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19843 let correlated = Self::expr_has_any_column(&expr);
19844 let tref = TableRef {
19845 name,
19846 alias: alias_ident,
19847 only: false,
19848 as_of_segment: None,
19849 unnest_expr: Some(Box::new(expr)),
19850 unnest_column_aliases,
19851 with_ordinality,
19852 generate_series_args: None,
19853 lateral_subquery: None,
19854 jsonb_each_text_arg: None,
19855 table_fn_call: None,
19856 rows_from: None,
19857 json_table: None,
19858 scalar_fn_item: false,
19859 };
19860 return Ok(if correlated {
19861 Self::wrap_correlated_srf(tref)
19862 } else {
19863 tref
19864 });
19865 }
19866 // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19867 // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19868 // generic table-fn arg parser can't read), so it is intercepted
19869 // here BEFORE the generic dispatch. The doc expr may reference
19870 // outer columns (implicit LATERAL) — same correlated-wrap rule.
19871 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19872 if s.eq_ignore_ascii_case("json_table"))
19873 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19874 {
19875 let tref = self.parse_json_table_ref()?;
19876 let correlated = tref
19877 .json_table
19878 .as_deref()
19879 .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19880 return Ok(if correlated {
19881 Self::wrap_correlated_srf(tref)
19882 } else {
19883 tref
19884 });
19885 }
19886 // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19887 // functions dispatched by name (`pg_partition_tree('t')`,
19888 // `pg_partition_ancestors('t')`). Same head-detection shape as
19889 // unnest; the engine executor owns the row shape per function.
19890 // v7.39 (read01 round 65) — and a USER function in FROM position
19891 // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19892 // (generate_series / unnest / the json_each family) keep it — their arms
19893 // sit further down, so they are excluded here by name rather than by
19894 // ordering. Anything else that is an ident followed by `(` is a table
19895 // function; the engine executor decides whether it is a builtin, a
19896 // set-returning user function, or an error.
19897 // 7.38.1 S5.1 — pg_dump spells its table functions
19898 // schema-qualified (`pg_catalog.pg_options_to_table(...)`);
19899 // strip the pg_catalog prefix here so the same head-detection
19900 // fires. Only pg_catalog: a user schema's `s.f(x)` keeps its
19901 // meaning.
19902 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
19903 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
19904 && matches!(
19905 self.tokens.get(self.pos + 2),
19906 Some(Token::Ident(_) | Token::QuotedIdent(_))
19907 )
19908 && matches!(self.tokens.get(self.pos + 3), Some(Token::LParen))
19909 {
19910 self.advance(); // pg_catalog
19911 self.advance(); // .
19912 }
19913 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19914 if !s.eq_ignore_ascii_case("generate_series")
19915 && !s.eq_ignore_ascii_case("unnest")
19916 && !is_json_each_name(s))
19917 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19918 {
19919 // Body out-of-line — this parse sits on the FROM/subquery
19920 // recursion chain (debug frame-cliff discipline).
19921 // v7.39 (read01 round 69) — a call whose arguments reference an outer
19922 // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19923 // outer row, so it rides the lateral channel. Same rule the unnest
19924 // arm uses.
19925 let tref = self.parse_table_fn_ref()?;
19926 let correlated = tref
19927 .table_fn_call
19928 .as_deref()
19929 .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19930 return Ok(if correlated {
19931 Self::wrap_correlated_srf(tref)
19932 } else {
19933 tref
19934 });
19935 }
19936 // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19937 // [, step])` set-returning source. Same shape as unnest:
19938 // detect at the head, parse the comma-separated arg list,
19939 // dispatch downstream through the engine's set-returning
19940 // path. Supports integer triplets (mailrs's `WITH row_no AS
19941 // (SELECT * FROM generate_series(1, N))` pattern) and
19942 // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19943 // date-range iteration pattern, which pre-3.10 had no
19944 // direct equivalent in SPG).
19945 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19946 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19947 {
19948 self.advance(); // generate_series
19949 self.advance(); // (
19950 let mut args: Vec<Expr> = Vec::new();
19951 loop {
19952 args.push(self.parse_expr(0)?);
19953 if matches!(self.peek(), Token::Comma) {
19954 self.advance();
19955 continue;
19956 }
19957 break;
19958 }
19959 if !matches!(self.peek(), Token::RParen) {
19960 return Err(self.err(alloc::format!(
19961 "expected ')' after generate_series() arguments, got {:?}",
19962 self.peek()
19963 )));
19964 }
19965 self.advance();
19966 if args.len() < 2 || args.len() > 3 {
19967 return Err(self.err(alloc::format!(
19968 "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19969 args.len()
19970 )));
19971 }
19972 let with_ordinality = self.absorb_with_ordinality();
19973 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19974 let name = alias_ident
19975 .clone()
19976 .unwrap_or_else(|| "generate_series".to_string());
19977 let correlated = args.iter().any(Self::expr_has_any_column);
19978 let tref = TableRef {
19979 name,
19980 alias: alias_ident,
19981 only: false,
19982 as_of_segment: None,
19983 unnest_expr: None,
19984 unnest_column_aliases: column_aliases,
19985 with_ordinality,
19986 generate_series_args: Some(args),
19987 lateral_subquery: None,
19988 jsonb_each_text_arg: None,
19989 table_fn_call: None,
19990 rows_from: None,
19991 json_table: None,
19992 scalar_fn_item: false,
19993 };
19994 return Ok(if correlated {
19995 Self::wrap_correlated_srf(tref)
19996 } else {
19997 tref
19998 });
19999 }
20000 // v7.16.2 — preserve information_schema / pg_catalog
20001 // qualifiers (mailrs round-10 A.3). The generic
20002 // `expect_ident_like` strip silently drops the schema;
20003 // we want the engine to recognise these PG meta tables
20004 // and synthesise rows from the live catalog. Produce a
20005 // synthetic name (`__spg_info_columns` etc.) so the
20006 // engine's SELECT-side router can dispatch without
20007 // clashing with any user-defined `columns` table.
20008 let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
20009 (synth, Some(orig))
20010 } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
20011 (synth, Some(orig))
20012 } else {
20013 (self.expect_ident_like()?, None)
20014 };
20015 // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
20016 // time-travel clause. Parse BEFORE the alias so the
20017 // alias can still ride at the tail (`tbl AS OF SEGMENT
20018 // '5' alias`). `AS` is a reserved keyword token, while
20019 // `OF` and `SEGMENT` are bare idents.
20020 let as_of_segment = if matches!(self.peek(), Token::As)
20021 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
20022 {
20023 self.advance(); // AS
20024 self.advance(); // OF
20025 let kw = match self.peek().clone() {
20026 Token::Ident(s) | Token::QuotedIdent(s) => s,
20027 other => {
20028 return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
20029 }
20030 };
20031 if !kw.eq_ignore_ascii_case("segment") {
20032 return Err(self.err(format!(
20033 "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
20034 )));
20035 }
20036 self.advance();
20037 // Segment id literal — accept either a string or
20038 // integer for operator ergonomics.
20039 let id = match self.advance() {
20040 Token::String(s) => s
20041 .parse::<u32>()
20042 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20043 Token::Integer(n) => u32::try_from(n)
20044 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20045 other => {
20046 return Err(self.err(format!(
20047 "expected segment id literal after AS OF SEGMENT, got {other:?}"
20048 )));
20049 }
20050 };
20051 Some(id)
20052 } else {
20053 None
20054 };
20055 // TABLESAMPLE is not a reserved token — keep the bare-ident
20056 // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
20057 let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
20058 {
20059 None
20060 } else {
20061 self.parse_optional_alias()?
20062 };
20063 // r1052 — a catalog name rewritten to its synthetic form keeps
20064 // the WRITTEN name as the relation's alias, so `pg_cast.oid`
20065 // still binds after `pg_cast` became `__spg_pg_cast`. PG
20066 // semantics: the visible name of `pg_catalog.pg_cast` IS
20067 // `pg_cast`. Without this, every table-name-qualified column
20068 // on a synthesised catalog answered "missing FROM-clause
20069 // entry" — which is the wall pg_dump hit on its first
20070 // pg_proc/pg_cast query.
20071 let alias = match (&alias, &meta_original) {
20072 (None, Some(orig)) if *orig != name => Some(orig.clone()),
20073 _ => alias,
20074 };
20075 // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
20076 // (PG grammar). BERNOULLI lowers to a per-row
20077 // `random() < p/100` conjunct on the enclosing SELECT's
20078 // WHERE — exact row-level Bernoulli semantics. SYSTEM
20079 // shares the lowering: SPG has no page structure to
20080 // sample, and the row-level form returns the same expected
20081 // fraction. REPEATABLE(seed) promises a deterministic
20082 // sample SPG cannot honour yet — honest error rather than
20083 // a silently ignored seed.
20084 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
20085 self.advance();
20086 let method = self.expect_ident_like()?;
20087 if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
20088 return Err(self.err(alloc::format!(
20089 "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
20090 )));
20091 }
20092 if !matches!(self.peek(), Token::LParen) {
20093 return Err(self.err(alloc::format!(
20094 "expected '(' after TABLESAMPLE {}, got {:?}",
20095 method.to_ascii_uppercase(),
20096 self.peek()
20097 )));
20098 }
20099 self.advance();
20100 let percent = self.parse_expr(0)?;
20101 if !matches!(self.peek(), Token::RParen) {
20102 return Err(self.err(alloc::format!(
20103 "expected ')' after TABLESAMPLE percentage, got {:?}",
20104 self.peek()
20105 )));
20106 }
20107 self.advance();
20108 // REPEATABLE(seed) → a deterministic per-row draw seeded by
20109 // `seed`, so the sample is stable across repeats and rescans.
20110 // Non-REPEATABLE keeps the non-deterministic `random()` draw.
20111 let mut sample_seed: Option<Expr> = None;
20112 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
20113 self.advance();
20114 if !matches!(self.peek(), Token::LParen) {
20115 return Err(self.err(alloc::format!(
20116 "expected '(' after REPEATABLE, got {:?}",
20117 self.peek()
20118 )));
20119 }
20120 self.advance();
20121 let seed = self.parse_expr(0)?;
20122 if !matches!(self.peek(), Token::RParen) {
20123 return Err(self.err(alloc::format!(
20124 "expected ')' after REPEATABLE seed, got {:?}",
20125 self.peek()
20126 )));
20127 }
20128 self.advance();
20129 sample_seed = Some(seed);
20130 }
20131 let draw = match sample_seed {
20132 Some(seed) => Expr::FunctionCall {
20133 name: "__tsm_fract".to_string(),
20134 args: alloc::vec![seed],
20135 },
20136 None => Expr::FunctionCall {
20137 name: "random".to_string(),
20138 args: Vec::new(),
20139 },
20140 };
20141 self.pending_sample_preds.push(Expr::Binary {
20142 lhs: Box::new(draw),
20143 op: crate::ast::BinOp::Lt,
20144 rhs: Box::new(Expr::Binary {
20145 lhs: Box::new(percent),
20146 op: crate::ast::BinOp::Div,
20147 rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
20148 }),
20149 });
20150 }
20151 Ok(TableRef {
20152 name,
20153 alias,
20154 only,
20155 as_of_segment,
20156 unnest_expr: None,
20157 unnest_column_aliases: Vec::new(),
20158 with_ordinality: false,
20159 generate_series_args: None,
20160 lateral_subquery: None,
20161 jsonb_each_text_arg: None,
20162 table_fn_call: None,
20163 rows_from: None,
20164 json_table: None,
20165 scalar_fn_item: false,
20166 })
20167 }
20168
20169 /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
20170 /// but also accepts `AS alias(col [, col, …])` — the
20171 /// PG-standard table-function column-list form. The column
20172 /// list is only honoured when paired with `UNNEST(...)` in
20173 /// the parent; other call sites currently discard it.
20174 /// True when the expression tree contains a qualified column
20175 /// reference (`t.col`) — the syntactic marker that an SRF
20176 /// argument correlates with a preceding FROM item.
20177 fn expr_has_qualified_column(e: &Expr) -> bool {
20178 match e {
20179 Expr::Column(c) => c.qualifier.is_some(),
20180 Expr::Binary { lhs, rhs, .. } => {
20181 Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
20182 }
20183 Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
20184 Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
20185 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
20186 Expr::Case {
20187 operand,
20188 branches,
20189 else_branch,
20190 } => {
20191 operand
20192 .as_deref()
20193 .is_some_and(Self::expr_has_qualified_column)
20194 || branches.iter().any(|(w, t)| {
20195 Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
20196 })
20197 || else_branch
20198 .as_deref()
20199 .is_some_and(Self::expr_has_qualified_column)
20200 }
20201 _ => false,
20202 }
20203 }
20204
20205 /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
20206 /// counts a bare (unqualified) column. A set-returning function has no
20207 /// input columns of its own, so ANY column in its arguments is an outer
20208 /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
20209 fn expr_has_any_column(e: &Expr) -> bool {
20210 match e {
20211 Expr::Column(_) => true,
20212 Expr::Binary { lhs, rhs, .. } => {
20213 Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
20214 }
20215 Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
20216 Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
20217 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
20218 // v7.39 (round 759, F31-B8b) — a column INSIDE an array
20219 // constructor or subscript fell to the `_ => false` arm, so
20220 // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
20221 // channel and the eager peer eval answered `column "x" does
20222 // not exist` (the substitution walker already recurses both
20223 // shapes; only this detector was blind to them).
20224 Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
20225 Expr::ArraySubscript { target, index } => {
20226 Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
20227 }
20228 Expr::Case {
20229 operand,
20230 branches,
20231 else_branch,
20232 } => {
20233 operand.as_deref().is_some_and(Self::expr_has_any_column)
20234 || branches
20235 .iter()
20236 .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
20237 || else_branch
20238 .as_deref()
20239 .is_some_and(Self::expr_has_any_column)
20240 }
20241 _ => false,
20242 }
20243 }
20244
20245 /// Wrap a correlated SRF table ref (`unnest(t.col)` /
20246 /// `generate_series(1, t.n)`) into the lateral_subquery
20247 /// channel: `SELECT * FROM <srf>` executes per outer row with
20248 /// outer references substituted (v7.37.43-T4.5 machinery).
20249 /// Uncorrelated SRFs stay on their plain channels.
20250 fn wrap_correlated_srf(srf: TableRef) -> TableRef {
20251 let name = srf.name.clone();
20252 let alias = srf.alias.clone();
20253 let inner = crate::ast::SelectStatement {
20254 locking: None,
20255 ctes: Vec::new(),
20256 distinct: false,
20257 distinct_on: Vec::new(),
20258 items: alloc::vec![crate::ast::SelectItem::Wildcard],
20259 from: Some(crate::ast::FromClause {
20260 primary: srf,
20261 joins: Vec::new(),
20262 }),
20263 where_: None,
20264 group_by: None,
20265 group_by_all: false,
20266 having: None,
20267 unions: Vec::new(),
20268 order_by: Vec::new(),
20269 limit: None,
20270 offset: None,
20271 limit_with_ties: false,
20272 window_check_exprs: Vec::new(),
20273 };
20274 TableRef {
20275 name,
20276 alias,
20277 only: false,
20278 as_of_segment: None,
20279 unnest_expr: None,
20280 unnest_column_aliases: Vec::new(),
20281 with_ordinality: false,
20282 generate_series_args: None,
20283 lateral_subquery: Some(Box::new(inner)),
20284 jsonb_each_text_arg: None,
20285 table_fn_call: None,
20286 rows_from: None,
20287 json_table: None,
20288 scalar_fn_item: false,
20289 }
20290 }
20291
20292 /// True when the expression tree contains an unresolved
20293 /// `OVER w` marker (see parse_over_clause).
20294 fn expr_has_named_window(e: &Expr) -> bool {
20295 match e {
20296 Expr::WindowFunction { partition_by, .. } => matches!(
20297 partition_by.as_slice(),
20298 [Expr::Column(c)] if matches!(
20299 c.qualifier.as_deref(),
20300 Some("__named_window__") | Some("__named_window_ref__")
20301 )
20302 ),
20303 Expr::Binary { lhs, rhs, .. } => {
20304 Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
20305 }
20306 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
20307 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
20308 Expr::Case {
20309 operand,
20310 branches,
20311 else_branch,
20312 } => {
20313 operand.as_deref().is_some_and(Self::expr_has_named_window)
20314 || branches.iter().any(|(w, t)| {
20315 Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
20316 })
20317 || else_branch
20318 .as_deref()
20319 .is_some_and(Self::expr_has_named_window)
20320 }
20321 _ => false,
20322 }
20323 }
20324
20325 /// v7.39 (round 705) — the NAMES the expression references through the
20326 /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
20327 /// definitions nothing referenced. Traversal mirrors
20328 /// `expr_has_named_window` above.
20329 fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
20330 match e {
20331 Expr::WindowFunction { partition_by, .. } => {
20332 if let [Expr::Column(c)] = partition_by.as_slice()
20333 && matches!(
20334 c.qualifier.as_deref(),
20335 Some("__named_window__") | Some("__named_window_ref__")
20336 )
20337 {
20338 into.push(c.name.clone());
20339 }
20340 }
20341 Expr::Binary { lhs, rhs, .. } => {
20342 Self::collect_named_window_refs(lhs, into);
20343 Self::collect_named_window_refs(rhs, into);
20344 }
20345 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20346 Self::collect_named_window_refs(expr, into);
20347 }
20348 Expr::FunctionCall { args, .. } => {
20349 for a in args {
20350 Self::collect_named_window_refs(a, into);
20351 }
20352 }
20353 Expr::Case {
20354 operand,
20355 branches,
20356 else_branch,
20357 } => {
20358 if let Some(o) = operand.as_deref() {
20359 Self::collect_named_window_refs(o, into);
20360 }
20361 for (w, t) in branches {
20362 Self::collect_named_window_refs(w, into);
20363 Self::collect_named_window_refs(t, into);
20364 }
20365 if let Some(eb) = else_branch.as_deref() {
20366 Self::collect_named_window_refs(eb, into);
20367 }
20368 }
20369 _ => {}
20370 }
20371 }
20372
20373 /// Inline named-window definitions into the `OVER w` markers.
20374 /// An unknown name errors (PG: window "w" does not exist).
20375 #[allow(clippy::type_complexity)]
20376 fn substitute_named_windows(
20377 e: &mut Expr,
20378 defs: &[(
20379 String,
20380 (
20381 Vec<Expr>,
20382 Vec<(Expr, bool, Option<bool>)>,
20383 Option<WindowFrame>,
20384 ),
20385 )],
20386 ) -> Result<(), String> {
20387 match e {
20388 Expr::WindowFunction {
20389 partition_by,
20390 order_by,
20391 frame,
20392 ..
20393 } => {
20394 // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
20395 // from the bare `OVER w1` (a plain reference).
20396 let named = match partition_by.as_slice() {
20397 [Expr::Column(c)] => match c.qualifier.as_deref() {
20398 Some("__named_window__") => Some((c.name.clone(), false)),
20399 Some("__named_window_ref__") => Some((c.name.clone(), true)),
20400 _ => None,
20401 },
20402 _ => None,
20403 };
20404 if let Some((wname, is_copy)) = named {
20405 let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
20406 else {
20407 return Err(alloc::format!("window {wname:?} does not exist"));
20408 };
20409 if !is_copy {
20410 *partition_by = def.0.clone();
20411 *order_by = def.1.clone();
20412 *frame = def.2.clone();
20413 return Ok(());
20414 }
20415 // v7.39 (round 229) — PG's copy rules, probed against
20416 // 18.4: a copy inherits the partitioning, may supply an
20417 // ordering only when the base has none, and may not copy
20418 // a base that already carries a frame (its own frame
20419 // would be ambiguous with the inherited one).
20420 if !def.1.is_empty() && !order_by.is_empty() {
20421 return Err(alloc::format!(
20422 "cannot override ORDER BY clause of window \"{wname}\""
20423 ));
20424 }
20425 if def.2.is_some() {
20426 return Err(alloc::format!(
20427 "cannot copy window \"{wname}\" because it has a frame clause"
20428 ));
20429 }
20430 *partition_by = def.0.clone();
20431 if order_by.is_empty() {
20432 *order_by = def.1.clone();
20433 }
20434 }
20435 Ok(())
20436 }
20437 Expr::Binary { lhs, rhs, .. } => {
20438 Self::substitute_named_windows(lhs, defs)?;
20439 Self::substitute_named_windows(rhs, defs)
20440 }
20441 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20442 Self::substitute_named_windows(expr, defs)
20443 }
20444 Expr::FunctionCall { args, .. } => {
20445 for a in args {
20446 Self::substitute_named_windows(a, defs)?;
20447 }
20448 Ok(())
20449 }
20450 Expr::Case {
20451 operand,
20452 branches,
20453 else_branch,
20454 } => {
20455 if let Some(op) = operand {
20456 Self::substitute_named_windows(op, defs)?;
20457 }
20458 for (w, t) in branches {
20459 Self::substitute_named_windows(w, defs)?;
20460 Self::substitute_named_windows(t, defs)?;
20461 }
20462 if let Some(el) = else_branch {
20463 Self::substitute_named_windows(el, defs)?;
20464 }
20465 Ok(())
20466 }
20467 _ => Ok(()),
20468 }
20469 }
20470
20471 /// SQL-standard `TABLE name` shorthand — builds the equivalent
20472 /// `SELECT * FROM name` head. Callers own set-op chain / tail
20473 /// composition.
20474 fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
20475 debug_assert!(matches!(self.peek(), Token::Table));
20476 self.advance(); // TABLE
20477 let tname = self.expect_ident_like()?;
20478 Ok(SelectStatement {
20479 locking: None,
20480 ctes: Vec::new(),
20481 distinct: false,
20482 distinct_on: Vec::new(),
20483 items: alloc::vec![SelectItem::Wildcard],
20484 from: Some(FromClause {
20485 primary: TableRef {
20486 name: tname,
20487 alias: None,
20488 only: false,
20489 as_of_segment: None,
20490 unnest_expr: None,
20491 unnest_column_aliases: Vec::new(),
20492 with_ordinality: false,
20493 generate_series_args: None,
20494 lateral_subquery: None,
20495 jsonb_each_text_arg: None,
20496 table_fn_call: None,
20497 rows_from: None,
20498 json_table: None,
20499 scalar_fn_item: false,
20500 },
20501 joins: Vec::new(),
20502 }),
20503 where_: None,
20504 group_by: None,
20505 group_by_all: false,
20506 having: None,
20507 unions: Vec::new(),
20508 order_by: Vec::new(),
20509 limit: None,
20510 offset: None,
20511 limit_with_ties: false,
20512 window_check_exprs: Vec::new(),
20513 })
20514 }
20515
20516 /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
20517 /// variants) → a derived table that reads each declared column out of
20518 /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
20519 /// `jsonb_array_elements(J)` (one row per element, column `value`);
20520 /// the scalar *record form projects a single row straight off `J`.
20521 /// Rides the existing lateral-subquery channel, so no new executor or
20522 /// AST is needed.
20523 fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
20524 use crate::ast::{
20525 BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
20526 };
20527 let fn_name = match self.peek() {
20528 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20529 _ => unreachable!("caller guarded is_json_to_record_name"),
20530 };
20531 self.advance(); // fn name
20532 self.advance(); // (
20533 let mut arg = self.parse_expr(0)?;
20534 // populate_record(base, json): the base only carries the record
20535 // type here — the JSON argument is the second expression.
20536 let mut base: Option<Expr> = None;
20537 if matches!(self.peek(), Token::Comma) {
20538 self.advance();
20539 base = Some(arg);
20540 arg = self.parse_expr(0)?;
20541 }
20542 if !matches!(self.peek(), Token::RParen) {
20543 return Err(self.err(alloc::format!(
20544 "expected ')' after {fn_name}() argument, got {:?}",
20545 self.peek()
20546 )));
20547 }
20548 self.advance(); // )
20549 let is_set = fn_name.ends_with("recordset");
20550 // `[AS] alias ( col type [, …] )` column-definition list.
20551 if matches!(self.peek(), Token::As) {
20552 self.advance();
20553 }
20554 let alias_opt = match self.peek() {
20555 Token::Ident(s) | Token::QuotedIdent(s) => {
20556 let a = s.clone();
20557 self.advance();
20558 Some(a)
20559 }
20560 _ => None,
20561 };
20562 // v7.39 (read01 round 76) — the populate family's canonical PG
20563 // spelling carries no column list at all: the row shape comes from
20564 // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
20565 // j)`). The parser has no catalog, so hand the two arguments to the
20566 // engine's table-function channel, which does. Only `*_to_record*`
20567 // (whose base is bare `record`) genuinely requires the list.
20568 if !matches!(self.peek(), Token::LParen) {
20569 if let Some(base_expr) = base {
20570 let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
20571 return Ok(TableRef {
20572 name: alias.clone(),
20573 alias: Some(alias),
20574 only: false,
20575 as_of_segment: None,
20576 unnest_expr: None,
20577 unnest_column_aliases: Vec::new(),
20578 with_ordinality: false,
20579 generate_series_args: None,
20580 lateral_subquery: None,
20581 jsonb_each_text_arg: None,
20582 table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
20583 rows_from: None,
20584 json_table: None,
20585 scalar_fn_item: false,
20586 });
20587 }
20588 return Err(self.err(alloc::format!(
20589 "expected '(' to start the {fn_name} column-definition list, got {:?}",
20590 self.peek()
20591 )));
20592 }
20593 let Some(alias) = alias_opt else {
20594 return Err(self.err(alloc::format!(
20595 "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
20596 )));
20597 };
20598 self.advance(); // (
20599 let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
20600 loop {
20601 let col = self.expect_ident_like()?;
20602 let ty = self.parse_cast_target()?;
20603 coldefs.push((col, ty));
20604 if matches!(self.peek(), Token::Comma) {
20605 self.advance();
20606 continue;
20607 }
20608 if matches!(self.peek(), Token::RParen) {
20609 self.advance();
20610 break;
20611 }
20612 return Err(self.err(alloc::format!(
20613 "expected ',' or ')' in {fn_name} column list, got {:?}",
20614 self.peek()
20615 )));
20616 }
20617 if coldefs.is_empty() {
20618 return Err(self.err(alloc::format!(
20619 "{fn_name} column-definition list must declare at least one column"
20620 )));
20621 }
20622 // Per column: (base ->> 'col')::type AS col. The base is the
20623 // per-element `value` column for the *set form, or the argument
20624 // itself for the scalar record form.
20625 let items: Vec<SelectItem> = coldefs
20626 .into_iter()
20627 .map(|(col, ty)| {
20628 let base = if is_set {
20629 Expr::Column(ColumnName {
20630 qualifier: None,
20631 name: "value".to_string(),
20632 })
20633 } else {
20634 arg.clone()
20635 };
20636 SelectItem::Expr {
20637 expr: Expr::Cast {
20638 expr: Box::new(Expr::Binary {
20639 lhs: Box::new(base),
20640 op: BinOp::JsonGetText,
20641 rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
20642 }),
20643 target: ty,
20644 },
20645 alias: Some(col),
20646 }
20647 })
20648 .collect();
20649 let from = if is_set {
20650 let elem_fn = if fn_name.starts_with("jsonb") {
20651 "jsonb_array_elements"
20652 } else {
20653 "json_array_elements"
20654 };
20655 Some(FromClause {
20656 primary: TableRef {
20657 name: "value".to_string(),
20658 alias: None,
20659 only: false,
20660 as_of_segment: None,
20661 unnest_expr: Some(Box::new(Expr::FunctionCall {
20662 name: elem_fn.to_string(),
20663 args: alloc::vec![arg],
20664 })),
20665 unnest_column_aliases: alloc::vec!["value".to_string()],
20666 with_ordinality: false,
20667 generate_series_args: None,
20668 lateral_subquery: None,
20669 jsonb_each_text_arg: None,
20670 table_fn_call: None,
20671 rows_from: None,
20672 json_table: None,
20673 scalar_fn_item: false,
20674 },
20675 joins: Vec::new(),
20676 })
20677 } else {
20678 None
20679 };
20680 let inner = SelectStatement {
20681 locking: None,
20682 ctes: Vec::new(),
20683 distinct: false,
20684 distinct_on: Vec::new(),
20685 items,
20686 from,
20687 where_: None,
20688 group_by: None,
20689 group_by_all: false,
20690 having: None,
20691 unions: Vec::new(),
20692 order_by: Vec::new(),
20693 limit: None,
20694 offset: None,
20695 limit_with_ties: false,
20696 window_check_exprs: Vec::new(),
20697 };
20698 Ok(TableRef {
20699 name: alias.clone(),
20700 alias: Some(alias),
20701 only: false,
20702 as_of_segment: None,
20703 unnest_expr: None,
20704 unnest_column_aliases: Vec::new(),
20705 with_ordinality: false,
20706 generate_series_args: None,
20707 lateral_subquery: Some(Box::new(inner)),
20708 jsonb_each_text_arg: None,
20709 table_fn_call: None,
20710 rows_from: None,
20711 json_table: None,
20712 scalar_fn_item: false,
20713 })
20714 }
20715
20716 /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
20717 /// Returns true when the clause was present. `WITH` alone (a
20718 /// CTE can never start here) is not enough — the ORDINALITY
20719 /// ident must follow, so a stray WITH still errors downstream.
20720 fn absorb_with_ordinality(&mut self) -> bool {
20721 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
20722 && matches!(self.tokens.get(self.pos + 1),
20723 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
20724 {
20725 self.advance();
20726 self.advance();
20727 true
20728 } else {
20729 false
20730 }
20731 }
20732
20733 /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
20734 /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
20735 /// Out-of-line: the caller sits on the FROM recursion chain.
20736 #[inline(never)]
20737 fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
20738 let fn_name = match self.advance() {
20739 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20740 _ => unreachable!("caller peeked an ident"),
20741 };
20742 self.advance(); // (
20743 let mut args: Vec<Expr> = Vec::new();
20744 if !matches!(self.peek(), Token::RParen) {
20745 loop {
20746 args.push(self.parse_expr(0)?);
20747 if matches!(self.peek(), Token::Comma) {
20748 self.advance();
20749 continue;
20750 }
20751 break;
20752 }
20753 }
20754 if !matches!(self.peek(), Token::RParen) {
20755 return Err(self.err(alloc::format!(
20756 "expected ')' after {fn_name}() arguments, got {:?}",
20757 self.peek()
20758 )));
20759 }
20760 self.advance();
20761 // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20762 // counter column rides after the function's own, and the alias list
20763 // names it.
20764 let with_ordinality = self.absorb_with_ordinality();
20765 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20766 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20767 Ok(TableRef {
20768 name,
20769 alias: alias_ident,
20770 only: false,
20771 as_of_segment: None,
20772 unnest_expr: None,
20773 unnest_column_aliases,
20774 with_ordinality,
20775 generate_series_args: None,
20776 lateral_subquery: None,
20777 jsonb_each_text_arg: None,
20778 table_fn_call: Some(Box::new((fn_name, args))),
20779 rows_from: None,
20780 json_table: None,
20781 scalar_fn_item: false,
20782 })
20783 }
20784
20785 /// v7.39 (round 205, JSON_TABLE) — parse
20786 /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20787 /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20788 /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20789 #[inline(never)]
20790 fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20791 self.advance(); // json_table
20792 self.advance(); // (
20793 let doc = Box::new(self.parse_expr(0)?);
20794 self.expect_comma_json_table()?;
20795 let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20796 // Optional `PASSING <expr> AS <name> [, …]`.
20797 let mut passing: Vec<(String, Expr)> = Vec::new();
20798 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20799 self.advance();
20800 loop {
20801 let e = self.parse_expr(0)?;
20802 if !matches!(self.peek(), Token::As) {
20803 return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20804 }
20805 self.advance();
20806 let vname = match self.advance() {
20807 Token::Ident(s) | Token::QuotedIdent(s) => s,
20808 other => {
20809 return Err(self.err(alloc::format!(
20810 "expected PASSING variable name, got {other:?}"
20811 )));
20812 }
20813 };
20814 passing.push((vname, e));
20815 if matches!(self.peek(), Token::Comma) {
20816 self.advance();
20817 continue;
20818 }
20819 break;
20820 }
20821 }
20822 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20823 return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20824 }
20825 self.advance();
20826 let columns = self.parse_json_table_columns()?;
20827 if !matches!(self.peek(), Token::RParen) {
20828 return Err(self.err(alloc::format!(
20829 "expected ')' to close JSON_TABLE, got {:?}",
20830 self.peek()
20831 )));
20832 }
20833 self.advance();
20834 let alias_ident = self.parse_optional_alias()?;
20835 let name = alias_ident
20836 .clone()
20837 .unwrap_or_else(|| String::from("json_table"));
20838 Ok(TableRef {
20839 name,
20840 alias: alias_ident,
20841 only: false,
20842 as_of_segment: None,
20843 unnest_expr: None,
20844 unnest_column_aliases: Vec::new(),
20845 with_ordinality: false,
20846 generate_series_args: None,
20847 lateral_subquery: None,
20848 jsonb_each_text_arg: None,
20849 table_fn_call: None,
20850 rows_from: None,
20851 json_table: Some(Box::new(crate::ast::JsonTable {
20852 doc,
20853 row_path,
20854 columns,
20855 passing,
20856 })),
20857 scalar_fn_item: false,
20858 })
20859 }
20860
20861 fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20862 if !matches!(self.peek(), Token::Comma) {
20863 return Err(self.err(alloc::format!(
20864 "expected ',' after JSON_TABLE document, got {:?}",
20865 self.peek()
20866 )));
20867 }
20868 self.advance();
20869 Ok(())
20870 }
20871
20872 fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20873 match self.advance() {
20874 Token::String(s) => Ok(s),
20875 other => Err(self.err(alloc::format!(
20876 "expected {what} string literal, got {other:?}"
20877 ))),
20878 }
20879 }
20880
20881 /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20882 #[inline(never)]
20883 fn parse_json_table_columns(
20884 &mut self,
20885 ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20886 if !matches!(self.peek(), Token::LParen) {
20887 return Err(self.err("expected '(' after COLUMNS".into()));
20888 }
20889 self.advance();
20890 let mut cols = Vec::new();
20891 loop {
20892 cols.push(self.parse_json_table_one_column()?);
20893 if matches!(self.peek(), Token::Comma) {
20894 self.advance();
20895 continue;
20896 }
20897 break;
20898 }
20899 if !matches!(self.peek(), Token::RParen) {
20900 return Err(self.err(alloc::format!(
20901 "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20902 self.peek()
20903 )));
20904 }
20905 self.advance();
20906 Ok(cols)
20907 }
20908
20909 fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20910 use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20911 // NESTED [PATH] '<p>' COLUMNS (...)
20912 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20913 self.advance();
20914 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20915 self.advance();
20916 }
20917 let path = self.parse_json_string_literal("NESTED PATH")?;
20918 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20919 return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20920 }
20921 self.advance();
20922 let columns = self.parse_json_table_columns()?;
20923 return Ok(JsonTableColumn::Nested { path, columns });
20924 }
20925 // <name> ...
20926 let name = match self.advance() {
20927 Token::Ident(s) | Token::QuotedIdent(s) => s,
20928 other => {
20929 return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20930 }
20931 };
20932 // <name> FOR ORDINALITY
20933 if matches!(self.peek(), Token::For) {
20934 self.advance();
20935 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20936 return Err(self.err("expected ORDINALITY after FOR".into()));
20937 }
20938 self.advance();
20939 return Ok(JsonTableColumn::Ordinality { name });
20940 }
20941 // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20942 let ty = self.parse_column_type_name()?;
20943 let mut format_json = false;
20944 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20945 self.advance();
20946 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20947 return Err(self.err("expected JSON after FORMAT".into()));
20948 }
20949 self.advance();
20950 format_json = true;
20951 }
20952 let mut exists = false;
20953 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20954 self.advance();
20955 exists = true;
20956 }
20957 let mut path = alloc::format!("$.{name}");
20958 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20959 self.advance();
20960 path = self.parse_json_string_literal("column PATH")?;
20961 }
20962 if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20963 // `FORMAT JSON` after PATH (alternate placement).
20964 self.advance();
20965 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20966 self.advance();
20967 }
20968 format_json = true;
20969 }
20970 let mut wrapper = false;
20971 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20972 self.advance();
20973 // optional CONDITIONAL/UNCONDITIONAL
20974 if matches!(self.peek(), Token::Ident(s)
20975 if s.eq_ignore_ascii_case("unconditional")
20976 || s.eq_ignore_ascii_case("conditional"))
20977 {
20978 self.advance();
20979 }
20980 if !matches!(self.peek(), Token::Ident(s)
20981 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20982 {
20983 return Err(self.err("expected WRAPPER after WITH".into()));
20984 }
20985 self.advance();
20986 // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20987 if matches!(self.peek(), Token::Ident(s)
20988 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20989 {
20990 self.advance();
20991 }
20992 wrapper = true;
20993 }
20994 // ON EMPTY / ON ERROR clauses (two, in any order).
20995 let mut on_empty = JsonTableOnBehavior::Null;
20996 let mut on_error = JsonTableOnBehavior::Null;
20997 for _ in 0..2 {
20998 let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20999 {
21000 self.advance();
21001 Some(JsonTableOnBehavior::Error)
21002 } else if matches!(self.peek(), Token::Null) {
21003 self.advance();
21004 Some(JsonTableOnBehavior::Null)
21005 } else if matches!(self.peek(), Token::Default) {
21006 self.advance();
21007 Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
21008 } else {
21009 None
21010 };
21011 let Some(behavior) = behavior else { break };
21012 // `ON {EMPTY|ERROR}`
21013 if !matches!(self.peek(), Token::On) {
21014 return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
21015 }
21016 self.advance();
21017 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
21018 self.advance();
21019 on_empty = behavior;
21020 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
21021 self.advance();
21022 on_error = behavior;
21023 } else {
21024 return Err(self.err("expected EMPTY or ERROR after ON".into()));
21025 }
21026 }
21027 Ok(JsonTableColumn::Regular {
21028 name,
21029 ty,
21030 path,
21031 exists,
21032 format_json,
21033 wrapper,
21034 on_empty,
21035 on_error,
21036 })
21037 }
21038
21039 fn parse_optional_alias_with_columns(
21040 &mut self,
21041 ) -> Result<(Option<String>, Vec<String>), ParseError> {
21042 let alias = self.parse_optional_alias()?;
21043 if alias.is_none() {
21044 return Ok((None, Vec::new()));
21045 }
21046 let mut cols: Vec<String> = Vec::new();
21047 if matches!(self.peek(), Token::LParen) {
21048 self.advance();
21049 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
21050 self.advance();
21051 cols.push(s);
21052 if matches!(self.peek(), Token::Comma) {
21053 self.advance();
21054 continue;
21055 }
21056 break;
21057 }
21058 if matches!(self.peek(), Token::RParen) {
21059 self.advance();
21060 }
21061 }
21062 Ok((alias, cols))
21063 }
21064
21065 /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
21066 /// whose keyword token was already consumed and whose `(` is the
21067 /// current token. Factored out of `parse_atom` (and marked
21068 /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
21069 /// recursive `parse_atom` frame — inlining them there enlarges the
21070 /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
21071 /// against, risking an overflow before the budget triggers.
21072 #[inline(never)]
21073 fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21074 self.advance(); // (
21075 let mut args = Vec::new();
21076 if !matches!(self.peek(), Token::RParen) {
21077 loop {
21078 args.push(self.parse_expr(0)?);
21079 match self.peek() {
21080 Token::Comma => {
21081 self.advance();
21082 }
21083 Token::RParen => break,
21084 other => {
21085 return Err(self.err(alloc::format!(
21086 "expected ',' or ')' in {name}() args, got {other:?}"
21087 )));
21088 }
21089 }
21090 }
21091 }
21092 self.advance(); // )
21093 Ok(Expr::FunctionCall {
21094 name: name.into(),
21095 args,
21096 })
21097 }
21098
21099 /// FROM-clause: a primary table reference plus zero-or-more joined
21100 /// peers expressed via either `, <table>` (cross-product, no ON) or
21101 /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
21102 /// v1.10 keeps the join list flat (left-associative nested-loop
21103 /// semantics).
21104 fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
21105 let primary = self.parse_table_ref()?;
21106 let primary_qual = primary
21107 .alias
21108 .clone()
21109 .unwrap_or_else(|| primary.name.clone());
21110 let joins = self.parse_from_joins(&primary_qual)?;
21111 Ok(FromClause { primary, joins })
21112 }
21113
21114 /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
21115 /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
21116 /// SAME grammar after its target table has already been consumed.
21117 /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
21118 /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
21119 /// be parsed forward, once.)
21120 /// `left_primary_qual` is the qualifier (alias, else name) of whatever
21121 /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
21122 /// target in the MySQL multi-table form. It only feeds the `USING (…)`
21123 /// desugaring, which needs a name for the left side of each equality.
21124 fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
21125 let mut joins = Vec::new();
21126 loop {
21127 // `, <table>` — cross-product with no ON.
21128 if matches!(self.peek(), Token::Comma) {
21129 self.advance();
21130 let table = self.parse_table_ref()?;
21131 joins.push(FromJoin {
21132 kind: JoinKind::Cross,
21133 table,
21134 on: None,
21135 using_cols: None,
21136 natural: false,
21137 });
21138 continue;
21139 }
21140 // v7.37.16 — optional leading `NATURAL` before the join
21141 // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
21142 // not a lexer keyword (it arrives as a bare Ident), so match
21143 // it case-insensitively here. When present, no ON/USING
21144 // clause is allowed — the common columns are resolved at
21145 // execution time.
21146 let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
21147 if natural {
21148 self.advance();
21149 }
21150 // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
21151 // CROSS JOIN, and bare JOIN (defaults to INNER).
21152 let kind =
21153 match self.peek() {
21154 Token::Inner => {
21155 self.advance();
21156 if !matches!(self.peek(), Token::Join) {
21157 return Err(self
21158 .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
21159 }
21160 self.advance();
21161 JoinKind::Inner
21162 }
21163 Token::Left => {
21164 self.advance();
21165 if matches!(self.peek(), Token::Outer) {
21166 self.advance();
21167 }
21168 if !matches!(self.peek(), Token::Join) {
21169 return Err(self.err(format!(
21170 "expected JOIN after LEFT [OUTER], got {:?}",
21171 self.peek()
21172 )));
21173 }
21174 self.advance();
21175 JoinKind::Left
21176 }
21177 Token::Cross => {
21178 self.advance();
21179 if !matches!(self.peek(), Token::Join) {
21180 return Err(self
21181 .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
21182 }
21183 self.advance();
21184 JoinKind::Cross
21185 }
21186 // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
21187 Token::Right => {
21188 self.advance();
21189 if matches!(self.peek(), Token::Outer) {
21190 self.advance();
21191 }
21192 if !matches!(self.peek(), Token::Join) {
21193 return Err(self.err(format!(
21194 "expected JOIN after RIGHT [OUTER], got {:?}",
21195 self.peek()
21196 )));
21197 }
21198 self.advance();
21199 JoinKind::Right
21200 }
21201 // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
21202 Token::Full => {
21203 self.advance();
21204 if matches!(self.peek(), Token::Outer) {
21205 self.advance();
21206 }
21207 if !matches!(self.peek(), Token::Join) {
21208 return Err(self.err(format!(
21209 "expected JOIN after FULL [OUTER], got {:?}",
21210 self.peek()
21211 )));
21212 }
21213 self.advance();
21214 JoinKind::FullOuter
21215 }
21216 Token::Join => {
21217 self.advance();
21218 JoinKind::Inner
21219 }
21220 _ => break,
21221 };
21222 let table = self.parse_table_ref()?;
21223 // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
21224 // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
21225 // where prev_table is the most-recent left-side table
21226 // (the previous join's table if any, else the FROM primary).
21227 // PG semantics around column merging are richer (USING'd
21228 // cols become deduplicated single output columns); for
21229 // sugar purposes the predicate-only form covers the
21230 // baseline corpus shape and chained `… JOIN x USING (k)
21231 // JOIN y USING (k)` calls.
21232 // v7.37.16 — NATURAL joins carry no ON/USING clause; the
21233 // common columns resolve at execution time.
21234 if natural {
21235 joins.push(FromJoin {
21236 kind,
21237 table,
21238 on: None,
21239 using_cols: None,
21240 natural: true,
21241 });
21242 continue;
21243 }
21244 let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
21245 // v7.37.16 — capture the USING column list (in addition to
21246 // the ON desugar below) so the executor can perform PG's
21247 // column-merge on the output side.
21248 let mut using_cols: Option<Vec<String>> = None;
21249 let on = if matches!(self.peek(), Token::On) {
21250 self.advance();
21251 Some(self.parse_expr(0)?)
21252 } else if using_match {
21253 self.advance();
21254 if !matches!(self.peek(), Token::LParen) {
21255 return Err(
21256 self.err(format!("expected '(' after USING, got {:?}", self.peek()))
21257 );
21258 }
21259 self.advance();
21260 let mut cols: Vec<String> = Vec::new();
21261 loop {
21262 match self.peek().clone() {
21263 Token::Ident(s) | Token::QuotedIdent(s) => {
21264 self.advance();
21265 cols.push(s);
21266 }
21267 other => {
21268 return Err(self.err(format!(
21269 "expected column name inside USING (…), got {other:?}"
21270 )));
21271 }
21272 }
21273 match self.peek() {
21274 Token::Comma => {
21275 self.advance();
21276 continue;
21277 }
21278 Token::RParen => {
21279 self.advance();
21280 break;
21281 }
21282 other => {
21283 return Err(self.err(format!(
21284 "expected ',' or ')' inside USING (…), got {other:?}"
21285 )));
21286 }
21287 }
21288 }
21289 if cols.is_empty() {
21290 return Err(self.err("USING (…) requires at least one column".to_string()));
21291 }
21292 using_cols = Some(cols.clone());
21293 // Pick the left-side alias: prev join's table if any,
21294 // else FROM primary. Use alias when present, else
21295 // table name (PG-equivalent qualifier).
21296 let left_qual: String = joins
21297 .last()
21298 .map(|j| {
21299 j.table
21300 .alias
21301 .clone()
21302 .unwrap_or_else(|| j.table.name.clone())
21303 })
21304 .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
21305 let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
21306 let mut iter = cols.into_iter().map(|c| Expr::Binary {
21307 lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21308 qualifier: Some(left_qual.clone()),
21309 name: c.clone(),
21310 })),
21311 op: crate::ast::BinOp::Eq,
21312 rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21313 qualifier: Some(right_qual.clone()),
21314 name: c,
21315 })),
21316 });
21317 let first = iter.next().expect("at least one col");
21318 Some(iter.fold(first, |acc, pred| Expr::Binary {
21319 lhs: alloc::boxed::Box::new(acc),
21320 op: crate::ast::BinOp::And,
21321 rhs: alloc::boxed::Box::new(pred),
21322 }))
21323 } else if kind == JoinKind::Cross {
21324 None
21325 } else {
21326 return Err(self.err(format!(
21327 "expected ON or USING after {:?} JOIN, got {:?}",
21328 kind,
21329 self.peek()
21330 )));
21331 };
21332 joins.push(FromJoin {
21333 kind,
21334 table,
21335 on,
21336 using_cols,
21337 natural: false,
21338 });
21339 }
21340 Ok(joins)
21341 }
21342
21343 /// Optional alias after an expression or table:
21344 /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
21345 /// accepted (PG-style implicit alias). Returns `None` if the next token
21346 /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
21347 fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
21348 if matches!(self.peek(), Token::As) {
21349 self.advance();
21350 // v7.39 (round 340, V56) — after AS the next token MUST be an
21351 // identifier. This used to return None and "let the caller
21352 // surface the error on the next expectation", but when AS is
21353 // the LAST token there is no next expectation: `SELECT 1 AS`
21354 // parsed clean and silently dropped the alias. PG rejects it.
21355 if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
21356 return self.expect_ident_like().map(Some);
21357 }
21358 return Err(self.err(alloc::format!(
21359 "expected an alias after AS, got {:?}",
21360 self.peek()
21361 )));
21362 }
21363 // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
21364 // grammar reserves a long list of follow-keywords from the
21365 // alias slot. SPG's bareword approximation: skip a small
21366 // set of idents that would otherwise be swallowed as the
21367 // table alias and break trailing clauses like CREATE
21368 // MATERIALIZED VIEW … WITH [NO] DATA or future ON
21369 // CONFLICT WHERE shapes.
21370 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
21371 if is_alias_stopword(s) {
21372 return Ok(None);
21373 }
21374 return Ok(self.expect_ident_like().ok());
21375 }
21376 Ok(None)
21377 }
21378
21379 /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
21380 fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21381 // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
21382 // error beats a stack overflow (an overflow aborts the
21383 // embedding host process).
21384 self.enter_nested()?;
21385 let r = self.parse_expr_inner(min_prec);
21386 self.nest_depth -= 1;
21387 r
21388 }
21389
21390 /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
21391 /// When the upcoming tokens form one, return the underlying
21392 /// operator token and the position just past the closing paren
21393 /// so the binary loop can dispatch on the plain operator.
21394 fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
21395 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
21396 return None;
21397 }
21398 if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
21399 return None;
21400 }
21401 let mut i = self.pos + 2;
21402 // Optional schema qualifier (pg_catalog.<op> etc.).
21403 if matches!(self.tokens.get(i), Some(Token::Ident(_)))
21404 && matches!(self.tokens.get(i + 1), Some(Token::Dot))
21405 {
21406 i += 2;
21407 }
21408 let op_tok = self.tokens.get(i)?.clone();
21409 if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
21410 return None;
21411 }
21412 Some((i + 2, op_tok))
21413 }
21414
21415 /// PG operator symbols that lower onto function calls in
21416 /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
21417 /// family → regexp_like, comparison rung), `^@` (starts_with,
21418 /// comparison rung), `^` (power, tighter than `*`), `#`
21419 /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
21420 /// subset of the OR bits so the subtraction never borrows).
21421 fn try_symbol_operator(
21422 &mut self,
21423 lhs: &Expr,
21424 min_prec: u8,
21425 ) -> Result<Option<Expr>, ParseError> {
21426 enum Sym {
21427 Regex { ci: bool, negated: bool },
21428 Like { ci: bool, negated: bool },
21429 StartsWith,
21430 Power,
21431 Xor,
21432 RangeAdjacent,
21433 }
21434 // v7.39 (IS-precedence knife) — the low-precedence postfix
21435 // predicates ride this existing leaf call (zero new frame slots
21436 // on the nesting chain).
21437 if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
21438 return Ok(Some(e));
21439 }
21440 let (sym, prec): (Sym, u8) = match self.peek() {
21441 Token::Tilde => (
21442 Sym::Regex {
21443 ci: false,
21444 negated: false,
21445 },
21446 5,
21447 ),
21448 Token::TildeStar => (
21449 Sym::Regex {
21450 ci: true,
21451 negated: false,
21452 },
21453 5,
21454 ),
21455 Token::NotTilde => (
21456 Sym::Regex {
21457 ci: false,
21458 negated: true,
21459 },
21460 5,
21461 ),
21462 Token::NotTildeStar => (
21463 Sym::Regex {
21464 ci: true,
21465 negated: true,
21466 },
21467 5,
21468 ),
21469 // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
21470 Token::DoubleTilde => (
21471 Sym::Like {
21472 ci: false,
21473 negated: false,
21474 },
21475 5,
21476 ),
21477 Token::DoubleTildeStar => (
21478 Sym::Like {
21479 ci: true,
21480 negated: false,
21481 },
21482 5,
21483 ),
21484 Token::NotDoubleTilde => (
21485 Sym::Like {
21486 ci: false,
21487 negated: true,
21488 },
21489 5,
21490 ),
21491 Token::NotDoubleTildeStar => (
21492 Sym::Like {
21493 ci: true,
21494 negated: true,
21495 },
21496 5,
21497 ),
21498 Token::CaretAt => (Sym::StartsWith, 5),
21499 // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
21500 // tighter than `* / & |`, which the prec-9 rung preserves —
21501 // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
21502 Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
21503 Token::Caret => (Sym::Power, 9),
21504 // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
21505 // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
21506 Token::Hash => (Sym::Xor, 6),
21507 Token::Adjacent => (Sym::RangeAdjacent, 5),
21508 _ => return Ok(None),
21509 };
21510 if prec < min_prec {
21511 return Ok(None);
21512 }
21513 self.advance();
21514 let rhs = self.parse_expr(prec + 1)?;
21515 let out = match sym {
21516 Sym::Regex { ci, negated } => {
21517 let mut args = alloc::vec![lhs.clone(), rhs];
21518 if ci {
21519 args.push(Expr::Literal(Literal::String(String::from("i"))));
21520 }
21521 maybe_not(
21522 Expr::FunctionCall {
21523 name: String::from("regexp_like"),
21524 args,
21525 },
21526 negated,
21527 )
21528 }
21529 Sym::Like { ci, negated } => Expr::Like {
21530 expr: alloc::boxed::Box::new(lhs.clone()),
21531 pattern: alloc::boxed::Box::new(rhs),
21532 negated,
21533 case_insensitive: ci,
21534 },
21535 Sym::StartsWith => Expr::FunctionCall {
21536 name: String::from("starts_with"),
21537 args: alloc::vec![lhs.clone(), rhs],
21538 },
21539 Sym::Power => Expr::FunctionCall {
21540 name: String::from("power"),
21541 args: alloc::vec![lhs.clone(), rhs],
21542 },
21543 // `#` bitwise XOR — a real operator now (was desugared to
21544 // `(a|b)-(a&b)`, algebraically identical for integers but
21545 // undefined for bit strings; the direct op handles both).
21546 Sym::Xor => Expr::Binary {
21547 lhs: Box::new(lhs.clone()),
21548 op: BinOp::BitXor,
21549 rhs: Box::new(rhs),
21550 },
21551 // range `-|-` "is adjacent to" — lowered to a catalog function.
21552 Sym::RangeAdjacent => Expr::FunctionCall {
21553 name: String::from("range_adjacent"),
21554 args: alloc::vec![lhs.clone(), rhs],
21555 },
21556 };
21557 Ok(Some(out))
21558 }
21559
21560 /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
21561 /// predicates, moved out of the tight postfix-cast loop: PG binds
21562 /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
21563 /// looser than EVERY binary operator (only NOT/AND/OR are looser),
21564 /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
21565 /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
21566 /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
21567 /// when nothing at this position belongs to the family. Out-of-line
21568 /// (`inline(never)`): the caller sits on the per-nesting-level frame
21569 /// chain that MAX_NEST_DEPTH is tuned against.
21570 #[inline(never)]
21571 fn parse_postfix_predicate(
21572 &mut self,
21573 lhs: &Expr,
21574 min_prec: u8,
21575 ) -> Result<Option<Expr>, ParseError> {
21576 // Reached through try_symbol_operator (an existing leaf call of
21577 // the binary loop) so NO new stack slots land on the per-nesting
21578 // frame chain; the lhs clones only when a predicate actually
21579 // consumes it.
21580 match self.peek() {
21581 // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
21582 // comparison family rung 5 (each +1 from the pre-XOR ladder).
21583 Token::Is if min_prec <= 4 => {}
21584 Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
21585 Token::Not
21586 if min_prec <= 5
21587 && matches!(
21588 self.tokens.get(self.pos + 1),
21589 Some(Token::Between | Token::In | Token::Like)
21590 ) => {}
21591 Token::Not | Token::Ident(_)
21592 if min_prec <= 5
21593 && (matches!(self.peek(), Token::Ident(s)
21594 if s.eq_ignore_ascii_case("ilike")
21595 || (self.mysql_dialect
21596 && (s.eq_ignore_ascii_case("regexp")
21597 || s.eq_ignore_ascii_case("rlike")))
21598 || (s.eq_ignore_ascii_case("similar")
21599 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
21600 || (matches!(self.peek(), Token::Not)
21601 && matches!(self.tokens.get(self.pos + 1),
21602 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21603 || (self.mysql_dialect
21604 && (s.eq_ignore_ascii_case("regexp")
21605 || s.eq_ignore_ascii_case("rlike")))
21606 || s.eq_ignore_ascii_case("similar")))) => {}
21607 _ => return Ok(None),
21608 }
21609 let mut expr = lhs.clone();
21610 // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
21611 // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
21612 if min_prec <= 4 {
21613 if matches!(self.peek(), Token::Is) {
21614 self.advance();
21615 let negated = if matches!(self.peek(), Token::Not) {
21616 self.advance();
21617 true
21618 } else {
21619 false
21620 };
21621 // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
21622 // mailrs pg_dump.
21623 if matches!(self.peek(), Token::Distinct) {
21624 self.advance();
21625 if !matches!(self.peek(), Token::From) {
21626 return Err(self.err(format!(
21627 "expected FROM after IS{} DISTINCT, got {:?}",
21628 if negated { " NOT" } else { "" },
21629 self.peek()
21630 )));
21631 }
21632 self.advance();
21633 // Right-hand side: parse at the same precedence
21634 // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
21635 // groups as `x IS DISTINCT FROM (a + b)`.
21636 let rhs = self.parse_expr(5)?;
21637 let op = if negated {
21638 BinOp::IsNotDistinctFrom
21639 } else {
21640 BinOp::IsDistinctFrom
21641 };
21642 expr = Expr::Binary {
21643 op,
21644 lhs: Box::new(expr),
21645 rhs: Box::new(rhs),
21646 };
21647 {
21648 return Ok(Some(expr));
21649 }
21650 }
21651 // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
21652 // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
21653 // Lowers onto pg_is_json(x, kind); NOT wraps the
21654 // call in a logical negation.
21655 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21656 if s.eq_ignore_ascii_case("json"))
21657 {
21658 self.advance(); // JSON
21659 let kind = match self.peek() {
21660 Token::Ident(s) | Token::QuotedIdent(s)
21661 if matches!(
21662 s.to_ascii_lowercase().as_str(),
21663 "value" | "object" | "array" | "scalar"
21664 ) =>
21665 {
21666 let k = s.to_ascii_lowercase();
21667 self.advance();
21668 k
21669 }
21670 _ => "value".to_string(),
21671 };
21672 let call = Expr::FunctionCall {
21673 name: "pg_is_json".to_string(),
21674 args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
21675 };
21676 expr = if negated {
21677 Expr::Unary {
21678 op: UnOp::Not,
21679 expr: Box::new(call),
21680 }
21681 } else {
21682 call
21683 };
21684 {
21685 return Ok(Some(expr));
21686 }
21687 }
21688 // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
21689 // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
21690 // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
21691 {
21692 let form_kw = match self.peek() {
21693 Token::Ident(s) | Token::QuotedIdent(s)
21694 if matches!(
21695 s.to_ascii_uppercase().as_str(),
21696 "NFC" | "NFD" | "NFKC" | "NFKD"
21697 ) && matches!(
21698 self.tokens.get(self.pos + 1),
21699 Some(Token::Ident(n) | Token::QuotedIdent(n))
21700 if n.eq_ignore_ascii_case("normalized")
21701 ) =>
21702 {
21703 Some(s.to_ascii_uppercase())
21704 }
21705 _ => None,
21706 };
21707 let bare_normalized = form_kw.is_none()
21708 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21709 if s.eq_ignore_ascii_case("normalized"));
21710 if form_kw.is_some() || bare_normalized {
21711 if form_kw.is_some() {
21712 self.advance(); // form keyword
21713 }
21714 self.advance(); // NORMALIZED
21715 let mut args = alloc::vec![expr];
21716 if let Some(f) = form_kw {
21717 args.push(Expr::Literal(Literal::String(f)));
21718 }
21719 let call = Expr::FunctionCall {
21720 name: "is_normalized".to_string(),
21721 args,
21722 };
21723 expr = if negated {
21724 Expr::Unary {
21725 op: UnOp::Not,
21726 expr: Box::new(call),
21727 }
21728 } else {
21729 call
21730 };
21731 {
21732 return Ok(Some(expr));
21733 }
21734 }
21735 }
21736 // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
21737 // three-valued boolean tests. IS TRUE/FALSE never
21738 // return NULL, so they lower to CASE forms whose
21739 // ELSE catches the NULL branch; IS UNKNOWN on a
21740 // boolean is exactly IS NULL.
21741 if matches!(self.peek(), Token::True | Token::False)
21742 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
21743 {
21744 let tok = self.advance();
21745 let test = match tok {
21746 Token::True => Some(true),
21747 Token::False => Some(false),
21748 _ => None, // UNKNOWN
21749 };
21750 // v7.39 (round 328, V45) — kept as what the user
21751 // wrote. These used to be lowered here into `CASE` /
21752 // `IS NULL`; the semantics were right but the AST no
21753 // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
21754 // was echoed back as
21755 // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
21756 expr = Expr::BoolTest {
21757 expr: Box::new(expr),
21758 value: test,
21759 negated,
21760 };
21761 {
21762 return Ok(Some(expr));
21763 }
21764 }
21765 if !matches!(self.peek(), Token::Null) {
21766 return Err(self.err(format!(
21767 "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21768 if negated { " NOT" } else { "" },
21769 self.peek()
21770 )));
21771 }
21772 self.advance();
21773 expr = Expr::IsNull {
21774 expr: Box::new(expr),
21775 negated,
21776 };
21777 {
21778 return Ok(Some(expr));
21779 }
21780 }
21781 }
21782 // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21783 if min_prec <= 5 {
21784 // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21785 // Look one token ahead so a stray `NOT` not followed by any of
21786 // these flows through to the early return below untouched.
21787 let negated = if matches!(self.peek(), Token::Not) {
21788 let next = self.tokens.get(self.pos + 1);
21789 matches!(next, Some(Token::Between | Token::In | Token::Like))
21790 || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21791 || (self.mysql_dialect
21792 && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21793 || s.eq_ignore_ascii_case("similar"))
21794 } else {
21795 false
21796 };
21797 if negated {
21798 self.advance();
21799 }
21800 if matches!(self.peek(), Token::Between) {
21801 expr = self.parse_between_tail(expr, negated)?;
21802 {
21803 return Ok(Some(expr));
21804 }
21805 }
21806 if matches!(self.peek(), Token::In) {
21807 if self.suppress_in_tail && !negated {
21808 // POSITION(sub IN str) — IN belongs to the
21809 // enclosing function syntax; stop here.
21810 {
21811 return Ok(None);
21812 }
21813 }
21814 expr = self.parse_in_tail(expr, negated)?;
21815 {
21816 return Ok(Some(expr));
21817 }
21818 }
21819 // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21820 // lowers onto the internal __similar_to(expr, pat[, esc]) call
21821 // (the SQL→regex transform runs inside, in the backtracking-
21822 // friendly shape SPG's matcher needs).
21823 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21824 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21825 {
21826 self.advance(); // SIMILAR
21827 self.advance(); // TO
21828 let pattern = self.parse_expr(6)?;
21829 let mut args = alloc::vec![expr, pattern];
21830 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21831 self.advance();
21832 args.push(self.parse_expr(6)?);
21833 }
21834 let call = Expr::FunctionCall {
21835 name: "__similar_to".to_string(),
21836 args,
21837 };
21838 expr = maybe_not(call, negated);
21839 {
21840 return Ok(Some(expr));
21841 }
21842 }
21843 if matches!(self.peek(), Token::Like) {
21844 self.advance();
21845 // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21846 if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21847 expr = q;
21848 {
21849 return Ok(Some(expr));
21850 }
21851 }
21852 // Pattern at the same precedence as other comparison RHSes —
21853 // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21854 let mut pattern = self.parse_expr(6)?;
21855 // `ESCAPE 'c'` — rewrite a literal pattern to the
21856 // default backslash escape at parse time. Custom
21857 // escapes on non-literal patterns would need
21858 // matcher support; error honestly.
21859 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21860 self.advance();
21861 let esc = self.parse_expr(6)?;
21862 pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21863 }
21864 expr = Expr::Like {
21865 expr: Box::new(expr),
21866 pattern: Box::new(pattern),
21867 negated,
21868 case_insensitive: false,
21869 };
21870 {
21871 return Ok(Some(expr));
21872 }
21873 }
21874 // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21875 // keyword reaches us as a plain identifier.
21876 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21877 self.advance();
21878 if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21879 expr = q;
21880 {
21881 return Ok(Some(expr));
21882 }
21883 }
21884 let pattern = self.parse_expr(6)?;
21885 expr = Expr::Like {
21886 expr: Box::new(expr),
21887 pattern: Box::new(pattern),
21888 negated,
21889 case_insensitive: true,
21890 };
21891 {
21892 return Ok(Some(expr));
21893 }
21894 }
21895 // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21896 // operator (RLIKE is the alias). It is a keyword, not `~`, and
21897 // matches case-insensitively under the default collation, so it
21898 // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21899 // `~*` operator uses, wrapped in NOT when negated.
21900 if self.mysql_dialect
21901 && matches!(self.peek(), Token::Ident(s)
21902 if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21903 {
21904 self.advance();
21905 let pattern = self.parse_expr(6)?;
21906 let call = Expr::FunctionCall {
21907 name: String::from("regexp_like"),
21908 args: alloc::vec![
21909 expr,
21910 pattern,
21911 Expr::Literal(Literal::String(String::from("i"))),
21912 ],
21913 };
21914 return Ok(Some(maybe_not(call, negated)));
21915 }
21916 }
21917 let _ = expr;
21918 Ok(None)
21919 }
21920
21921 fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21922 let mut lhs = self.parse_unary()?;
21923 let mut chain_len = 0usize;
21924 loop {
21925 // OPERATOR([schema.]op) reduces to its underlying
21926 // operator token before the normal dispatch.
21927 let explicit = self.peek_explicit_operator();
21928 let dispatch = match &explicit {
21929 Some((_, tok)) => self.binop_here(tok),
21930 None => self.binop_here(self.peek()),
21931 };
21932 let Some((op, prec)) = dispatch else {
21933 // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21934 // of the symbol family. `binop_here` answers None for them
21935 // because they lower onto function calls rather than a
21936 // BinOp, and the fallback below reads `self.peek()` — the
21937 // word OPERATOR, not the operator. `pg_dump` writes every
21938 // catalog predicate this way, so its first query failed
21939 // and no dump ran:
21940 //
21941 // AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21942 //
21943 // Collapsing the wrapper to the operator it names puts the
21944 // token where the fallback already looks.
21945 if let Some((next, op_tok)) = explicit {
21946 self.tokens.splice(self.pos..next, [op_tok]);
21947 }
21948 if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21949 lhs = e;
21950 chain_len += 1;
21951 if chain_len > MAX_BINARY_CHAIN {
21952 return Err(self.err(alloc::format!(
21953 "more than {MAX_BINARY_CHAIN} chained binary operators"
21954 )));
21955 }
21956 continue;
21957 }
21958 break;
21959 };
21960 if prec < min_prec {
21961 break;
21962 }
21963 // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21964 // iteratively but evaluates and drops recursively;
21965 // depth beyond the budget overflows worker stacks.
21966 chain_len += 1;
21967 if chain_len > MAX_BINARY_CHAIN {
21968 return Err(self.err(alloc::format!(
21969 "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21970 )));
21971 }
21972 match explicit {
21973 Some((end_pos, _)) => self.pos = end_pos,
21974 None => {
21975 self.advance();
21976 }
21977 }
21978 // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21979 // ANY is a bare ident; ALL is a reserved Token. Both
21980 // require an immediate `(` to disambiguate from
21981 // identifier columns named `any` / `all`.
21982 let any_kind = match self.peek() {
21983 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21984 Some(false)
21985 }
21986 Token::Ident(s) | Token::QuotedIdent(s)
21987 if (s.eq_ignore_ascii_case("any")
21988 || s.eq_ignore_ascii_case("some")
21989 || s.eq_ignore_ascii_case("all"))
21990 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21991 {
21992 Some(!s.eq_ignore_ascii_case("all"))
21993 }
21994 _ => None,
21995 };
21996 if let Some(is_any) = any_kind {
21997 lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21998 continue;
21999 }
22000 let rhs = self.parse_expr(prec + 1)?;
22001 lhs = Expr::Binary {
22002 lhs: Box::new(lhs),
22003 op,
22004 rhs: Box::new(rhs),
22005 };
22006 }
22007 Ok(lhs)
22008 }
22009
22010 /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
22011 /// and the array form.
22012 ///
22013 /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
22014 /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
22015 /// this block's `Expr` temporaries and four `format!` sites slots in
22016 /// that frame on every level of `((((1))))`, which never reaches it.
22017 #[inline(never)]
22018 fn parse_any_all_rhs(
22019 &mut self,
22020 lhs: Expr,
22021 op: BinOp,
22022 is_any: bool,
22023 ) -> Result<Expr, ParseError> {
22024 self.advance(); // ident
22025 self.advance(); // (
22026 // `x op ANY (SELECT …)` — the quantified-subquery
22027 // form. `= ANY` is exactly IN; the other operators
22028 // lower onto EXISTS over the subquery as a derived
22029 // table, comparing against its single projection
22030 // aliased __v (x's columns resolve correlated).
22031 // ALL is the negated-EXISTS complement; a NULL
22032 // element makes PG return NULL where this lowering
22033 // returns true — the NOT NULL column case (the
22034 // practical one) is exact.
22035 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
22036 // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
22037 // legal PG too (round-151 sibling). Out-of-line
22038 // (#[inline(never)] helper) — this sits on
22039 // parse_expr's recursive frame and the two-armed
22040 // SELECT temporary blew the nesting-budget stack.
22041 let mut sub = self.parse_any_all_select_body()?;
22042 if !matches!(self.peek(), Token::RParen) {
22043 return Err(self.err(alloc::format!(
22044 "expected ')' after ANY/ALL subquery, got {:?}",
22045 self.peek()
22046 )));
22047 }
22048 self.advance();
22049 if sub.items.len() != 1 {
22050 return Err(self.err(alloc::format!(
22051 "ANY/ALL subquery must return one column, got {}",
22052 sub.items.len()
22053 )));
22054 }
22055 if is_any && matches!(op, BinOp::Eq) {
22056 return Ok(Expr::InSubquery {
22057 expr: Box::new(lhs),
22058 subquery: Box::new(sub),
22059 negated: false,
22060 });
22061 }
22062 // The engine's subquery resolvers materialise
22063 // the single-column result into an ARRAY the
22064 // existing AnyAll three-valued eval consumes.
22065 return Ok(Expr::AnyAll {
22066 expr: Box::new(lhs),
22067 op,
22068 array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
22069 is_any,
22070 });
22071 }
22072 let arr = self.parse_expr(0)?;
22073 if !matches!(self.peek(), Token::RParen) {
22074 return Err(self.err(alloc::format!(
22075 "expected ')' after ANY/ALL argument, got {:?}",
22076 self.peek()
22077 )));
22078 }
22079 self.advance();
22080 Ok(Expr::AnyAll {
22081 expr: Box::new(lhs),
22082 op,
22083 array: Box::new(arr),
22084 is_any,
22085 })
22086 }
22087
22088 /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
22089 /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
22090 #[inline(never)]
22091 fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
22092 self.advance();
22093 let e = self.parse_expr(9)?;
22094 Ok(build_center_call(e))
22095 }
22096
22097 /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
22098 /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
22099 /// unary minus.
22100 ///
22101 /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
22102 /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
22103 /// the Expr-sized local stays out of that frame.
22104 #[inline(never)]
22105 fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
22106 self.advance();
22107 let e = self.parse_expr(9)?;
22108 Ok(Expr::FunctionCall {
22109 name: alloc::string::String::from(name),
22110 args: alloc::vec![e],
22111 })
22112 }
22113
22114 /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
22115 /// (horizontal). Out-of-line from `parse_unary` (frame budget).
22116 #[inline(never)]
22117 fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
22118 self.advance();
22119 let e = self.parse_expr(9)?;
22120 Ok(Expr::FunctionCall {
22121 name: alloc::string::String::from(if vertical {
22122 "isvertical"
22123 } else {
22124 "ishorizontal"
22125 }),
22126 args: alloc::vec![e],
22127 })
22128 }
22129
22130 /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
22131 /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
22132 /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
22133 #[inline(never)]
22134 fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
22135 self.advance();
22136 let e = self.parse_expr(9)?;
22137 Ok(Expr::Cast {
22138 expr: Box::new(e),
22139 target: CastTarget::Named("binary".to_string()),
22140 })
22141 }
22142
22143 /// The prefix operators that share one shape: take the token, parse
22144 /// an operand at `prec`, wrap it.
22145 ///
22146 /// `#[inline(never)]`, and one function instead of five arms, for the
22147 /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
22148 /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
22149 /// debug build gives EVERY arm's locals a slot in the frame, whichever
22150 /// arm runs. `((((1))))` reaches none of these arms and was carrying
22151 /// five `Expr`-sized locals per level for them anyway.
22152 #[inline(never)]
22153 fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
22154 self.advance();
22155 let e = self.parse_expr(prec)?;
22156 Ok(Expr::Unary {
22157 op,
22158 expr: Box::new(e),
22159 })
22160 }
22161
22162 /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
22163 /// and separate from it because of the literal folding below and the
22164 /// `format!` temporaries that folding needs.
22165 #[inline(never)]
22166 fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
22167 self.advance();
22168 // v7.39 (round 549) — fold the sign into an integer literal that
22169 // only fits once it is negative.
22170 //
22171 // `9223372036854775808` is one past i64::MAX, so the lexer hands
22172 // it over as a NUMERIC and `-` on a numeric stays numeric. PG
22173 // folds the sign first, so `-9223372036854775808` is a bigint
22174 // there — and `-9223372036854775808 - 1` raises "bigint out of
22175 // range" where SPG quietly answered -9223372036854775809, a value
22176 // no bigint can hold. The arithmetic itself was already checked;
22177 // only the literal's type was wrong.
22178 if let Token::Numeric(lit) = self.peek()
22179 && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
22180 {
22181 self.advance();
22182 return Ok(Expr::Literal(Literal::Integer(folded)));
22183 }
22184 // Unary minus binds tighter than `*`/`/` (now at prec 7 after
22185 // `<->` slotted into 5 and arithmetic shifted up).
22186 let e = self.parse_expr(9)?;
22187 Ok(Expr::Unary {
22188 op: UnOp::Neg,
22189 expr: Box::new(e),
22190 })
22191 }
22192
22193 /// tsquery `!!` prefix negation, lowered to the catalog function.
22194 /// Binds like unary minus. Out-of-line for the frame reason on
22195 /// `parse_unary_op`.
22196 #[inline(never)]
22197 fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
22198 self.advance();
22199 let e = self.parse_expr(9)?;
22200 Ok(Expr::FunctionCall {
22201 name: String::from("tsquery_not"),
22202 args: alloc::vec![e],
22203 })
22204 }
22205
22206 fn parse_unary(&mut self) -> Result<Expr, ParseError> {
22207 match self.peek() {
22208 // NOT binds tighter than AND / XOR / OR but looser than
22209 // comparisons — its operand takes everything ≥ the comparison
22210 // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
22211 // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
22212 // was rung 3, behaviour-identical when 3 was unused; AND now
22213 // occupies 3, so this must be 4 to keep NOT tighter than AND.)
22214 Token::Not => self.parse_unary_op(UnOp::Not, 4),
22215 // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
22216 // The body is out-of-line: `parse_unary` is one of the three
22217 // frames the parser's MAX_NEST_DEPTH is tuned against, and an
22218 // inline arm here overflowed the native stack in
22219 // `nesting_budget_errors_cleanly` — the guard test caught it,
22220 // exactly as the eval-side cliff did in rounds 346 and 351.
22221 Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
22222 self.parse_binary_prefix()
22223 }
22224 // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
22225 // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
22226 // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
22227 Token::Bang => self.parse_unary_op(UnOp::Not, 9),
22228 Token::Minus => self.parse_prefix_minus(),
22229 // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
22230 // worked only because the lexer reads it as one signed literal;
22231 // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
22232 // PG18 and MariaDB take all of them. Binds like unary minus.
22233 Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
22234 // Bitwise NOT binds like unary minus.
22235 Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
22236 // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
22237 // "center of" operator; desugars to center(x). The whole arm
22238 // is out-of-line: parse_unary sits on the per-nesting-level
22239 // frame chain that MAX_NEST_DEPTH is tuned against, so no
22240 // Expr-sized local may live in this frame.
22241 Token::TsMatch => self.parse_prefix_center(),
22242 // v7.39 (round 508) — the prefix operators that are named
22243 // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
22244 // is length. Out-of-line for the same nesting-frame reason as
22245 // parse_prefix_center — parse_unary sits on the recursive cycle
22246 // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
22247 // live in this frame.
22248 Token::At => self.parse_prefix_call("abs"),
22249 Token::Hash => self.parse_prefix_call("npoints"),
22250 Token::AtMinusAt => self.parse_prefix_call("length"),
22251 // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
22252 // "is horizontal" (lseg / line); desugars to the existing
22253 // isvertical()/ishorizontal() functions. Out-of-line for the
22254 // same nesting-frame reason as parse_prefix_center.
22255 Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
22256 Token::GeomHoriz => self.parse_prefix_geom_axis(false),
22257 Token::DoubleBang => self.parse_prefix_tsquery_not(),
22258 _ => self.parse_atom(),
22259 }
22260 }
22261
22262 /// Parse a parenthesised scalar subquery body after the caller has consumed
22263 /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
22264 /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
22265 /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
22266 /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
22267 /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
22268 /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
22269 /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
22270 /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
22271 /// which sits on the recursive nesting-budget cycle (a few extra bytes there
22272 /// tips the deep-nesting test into a stack overflow).
22273 #[inline(never)]
22274 fn array_subquery_ahead(&self) -> bool {
22275 if !matches!(self.peek(), Token::LParen) {
22276 return false;
22277 }
22278 matches!(
22279 self.tokens.get(self.pos + 1),
22280 Some(Token::Select | Token::Values)
22281 ) || matches!(
22282 self.tokens.get(self.pos + 1),
22283 Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
22284 )
22285 }
22286
22287 /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
22288 /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
22289 /// locals stay off parse_atom's recursive frame (round 105).
22290 #[inline(never)]
22291 fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
22292 self.advance(); // consume `[`
22293 let mut items: Vec<Expr> = Vec::new();
22294 if !matches!(self.peek(), Token::RBracket) {
22295 loop {
22296 // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
22297 // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
22298 if matches!(self.peek(), Token::LBracket) {
22299 items.push(self.parse_array_bracket_body()?);
22300 } else {
22301 items.push(self.parse_expr(0)?);
22302 }
22303 match self.peek() {
22304 Token::Comma => {
22305 self.advance();
22306 }
22307 Token::RBracket => break,
22308 other => {
22309 return Err(self.err(alloc::format!(
22310 "expected ',' or ']' in ARRAY literal, got {other:?}"
22311 )));
22312 }
22313 }
22314 }
22315 }
22316 self.advance(); // consume `]`
22317 Ok(Expr::Array(items))
22318 }
22319
22320 /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
22321 /// is already consumed; the current token is `(`. Desugars to a scalar
22322 /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
22323 /// the subquery's single-column rows in order — reusing the existing
22324 /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
22325 /// keeps the large `Statement` local off parse_atom's recursive frame.
22326 #[inline(never)]
22327 fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
22328 self.advance(); // consume `(`
22329 let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
22330 if w.eq_ignore_ascii_case("with"));
22331 let sub = if is_with {
22332 self.advance(); // WITH
22333 self.parse_with_cte_then_select()?
22334 } else {
22335 self.parse_select_stmt()?
22336 };
22337 if !matches!(self.peek(), Token::RParen) {
22338 return Err(self.err(alloc::format!(
22339 "expected ')' to close ARRAY(subquery), got {:?}",
22340 self.peek()
22341 )));
22342 }
22343 self.advance(); // consume `)`
22344 // Reuse the parser to build the array_agg wrapper from the subquery's
22345 // canonical text — avoids hand-constructing the derived-table AST.
22346 let wrapper = alloc::format!(
22347 "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
22348 );
22349 let stmt = parse_statement(&wrapper)
22350 .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
22351 let Statement::Select(sel) = stmt else {
22352 return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
22353 };
22354 Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
22355 }
22356
22357 #[inline(never)]
22358 fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
22359 let inner = if is_with {
22360 self.advance(); // WITH
22361 self.parse_with_cte_then_select()?
22362 } else {
22363 self.parse_select_stmt()?
22364 };
22365 match self.advance() {
22366 Token::RParen => {
22367 let Statement::Select(s) = inner else {
22368 return Err(ParseError {
22369 message: "scalar subquery body must be a SELECT".into(),
22370 token_pos: self.consumed_pos(),
22371 });
22372 };
22373 Ok(Expr::ScalarSubquery(Box::new(s)))
22374 }
22375 other => Err(ParseError {
22376 message: format!("expected ')' after scalar subquery, got {other:?}"),
22377 token_pos: self.consumed_pos(),
22378 }),
22379 }
22380 }
22381
22382 /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
22383 /// literals. The lexer splits them into an ident + string; recombine
22384 /// here. Out-of-line and returning `Option` so `parse_atom` — the
22385 /// recursive frame the 768 KiB stack budget is tuned against — pays no
22386 /// frame for the `body` / `bits` strings and their char loops (the
22387 /// round-367 frame cliff, M20).
22388 #[inline(never)]
22389 fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
22390 let is_hex = match self.peek() {
22391 Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
22392 Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
22393 _ => return None,
22394 };
22395 if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
22396 return None;
22397 }
22398 // v7.39.3 — where the LITERAL starts, because the errors below
22399 // are about the literal and both engines point at it. `err`
22400 // reports the CURRENT token, which by then is the one after the
22401 // string: `SELECT x'123'` pointed at Eof, so the MySQL wire's
22402 // `near '…'` snippet — which runs from the reported position to
22403 // the end — came out empty where MySQL 9.7.2 says `near
22404 // 'x'123''`.
22405 let lit_pos = self.pos;
22406 self.advance();
22407 let Token::String(body) = self.advance() else {
22408 unreachable!("guarded above");
22409 };
22410 // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
22411 // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
22412 // (hex pairs, even count required — MariaDB errors on an odd
22413 // count); `b'1010'` packs its bits big-endian, left-padded to a
22414 // byte. Lower both onto the bytea cast.
22415 if self.mysql_dialect {
22416 if is_hex {
22417 if body.len() % 2 == 1 {
22418 return Some(Err(self.err_at(
22419 lit_pos,
22420 alloc::format!("invalid hex string literal X'{body}': odd digit count"),
22421 )));
22422 }
22423 for c in body.chars() {
22424 if !c.is_ascii_hexdigit() {
22425 return Some(Err(self.err_at(
22426 lit_pos,
22427 alloc::format!("invalid hexadecimal digit {c:?} in X'…'"),
22428 )));
22429 }
22430 }
22431 return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
22432 }
22433 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22434 return Some(Err(self.err_at(
22435 lit_pos,
22436 alloc::format!("invalid binary digit {bad:?} in b'…'"),
22437 )));
22438 }
22439 return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
22440 }
22441 let bits = if is_hex {
22442 let mut out = String::with_capacity(body.len() * 4);
22443 for c in body.chars() {
22444 let Some(d) = c.to_digit(16) else {
22445 // v7.39.3 — PostgreSQL 18.6's own sentence, and its
22446 // own quoting: `"g" is not a valid hexadecimal
22447 // digit` (measured, with the caret on the literal).
22448 return Some(Err(self.err_at(
22449 lit_pos,
22450 alloc::format!("\"{c}\" is not a valid hexadecimal digit"),
22451 )));
22452 };
22453 out.push_str(&alloc::format!("{d:04b}"));
22454 }
22455 out
22456 } else {
22457 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22458 return Some(Err(self.err_at(
22459 lit_pos,
22460 alloc::format!("\"{bad}\" is not a valid binary digit"),
22461 )));
22462 }
22463 body
22464 };
22465 // Route through the postfix-cast loop so a chained cast like
22466 // `B'1010'::int` attaches onto the implicit `::bit` cast instead
22467 // of erroring at the `::`.
22468 // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
22469 // literal keeps its exact length, while an explicit `::bit` cast is
22470 // bit(1) with pad/truncate semantics (PG).
22471 Some(self.finish_postfix_casts(Expr::Cast {
22472 expr: Box::new(Expr::Literal(Literal::String(bits))),
22473 target: CastTarget::Named("__bit_literal".to_string()),
22474 }))
22475 }
22476
22477 fn parse_atom(&mut self) -> Result<Expr, ParseError> {
22478 if let Some(res) = self.try_parse_bit_string_literal() {
22479 return res;
22480 }
22481 let tok_pos = self.pos;
22482 match self.advance() {
22483 Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
22484 Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
22485 // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
22486 // carrying the source mantissa + scale so no precision is lost. A
22487 // literal too wide for i128 falls back to double precision.
22488 // Out-of-line (#[inline(never)]) — this arm sits on the
22489 // parse_expr recursion chain; its expansion locals must not
22490 // widen the recursive frame (debug frame-cliff discipline).
22491 Token::Numeric(s) => match numeric_token_to_literal(s) {
22492 Ok(lit) => Ok(Expr::Literal(lit)),
22493 Err(msg) => Err(self.err(msg)),
22494 },
22495 Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
22496 // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
22497 // (the lexer only emits this token in the MySQL dialect). Lower
22498 // onto the existing bytea cast; out-of-line to keep this arm off
22499 // the parse recursion frame.
22500 Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
22501 Token::True => Ok(Expr::Literal(Literal::Bool(true))),
22502 Token::False => Ok(Expr::Literal(Literal::Bool(false))),
22503 Token::Null => Ok(Expr::Literal(Literal::Null)),
22504 // v6.1.1 — `$N` placeholder. The actual Value lookup
22505 // happens in the engine eval path against the prepared-
22506 // statement bind buffer.
22507 Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
22508 Token::LParen => {
22509 // v4.10: `(SELECT ...)` in expression position is a
22510 // scalar subquery; otherwise it's a parenthesised
22511 // expression. Peek for SELECT keyword to dispatch.
22512 // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
22513 // lexes as Ident("with") (not a reserved token). The subquery body
22514 // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
22515 // so its large `Statement` local stays out of parse_atom's stack
22516 // frame — parse_atom is on the recursive `((…))` cycle and the
22517 // nesting budget is tuned to its frame size).
22518 let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22519 if s.eq_ignore_ascii_case("with"));
22520 if matches!(self.peek(), Token::Select) || is_with {
22521 self.parse_paren_scalar_subquery(is_with)
22522 } else {
22523 let e = self.parse_expr(0)?;
22524 // `(a, b, …)` — a row constructor. Valid only
22525 // in front of a comparison operator or [NOT]
22526 // IN; both expand at parse time (lexicographic
22527 // comparison / OR'd row equalities).
22528 if matches!(self.peek(), Token::Comma) {
22529 let mut row = alloc::vec![e];
22530 while matches!(self.peek(), Token::Comma) {
22531 self.advance();
22532 row.push(self.parse_expr(0)?);
22533 }
22534 if !matches!(self.peek(), Token::RParen) {
22535 return Err(self.err(alloc::format!(
22536 "expected ')' after row constructor, got {:?}",
22537 self.peek()
22538 )));
22539 }
22540 self.advance();
22541 // A bare `(a, b, …)` row constructor can carry postfix
22542 // (`::text`, `.field`) just like `ROW(a, b, …)`; the
22543 // early return here skips parse_atom's tail postfix
22544 // pass, so fold casts in explicitly. For the
22545 // comparison / predicate forms nothing postfix follows,
22546 // so this is a no-op.
22547 return self
22548 .parse_row_comparison_tail(row)
22549 .and_then(|e| self.finish_postfix_casts(e));
22550 }
22551 match self.advance() {
22552 Token::RParen => Ok(e),
22553 other => Err(ParseError {
22554 message: format!("expected ')', got {other:?}"),
22555 token_pos: self.consumed_pos(),
22556 }),
22557 }
22558 }
22559 }
22560 Token::LBracket => self.parse_vector_literal_body(),
22561 Token::Extract => self.parse_extract_atom(),
22562 Token::Interval => self.parse_interval_atom(),
22563 // `LEFT` / `RIGHT` are reserved-keyword tokens because the
22564 // grammar dedicates arms for `LEFT [OUTER] JOIN` /
22565 // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
22566 // expression position calling the PG `left(string, n)` /
22567 // `right(string, n)` function; rebuild the AST as a regular
22568 // function call so the engine's apply_function dispatch picks
22569 // it up. Delegated to a #[inline(never)] helper so its locals
22570 // don't bloat this recursive `parse_atom` frame (the nesting
22571 // budget in `enter_nested` is tuned to parse_atom's size).
22572 Token::Left if matches!(self.peek(), Token::LParen) => {
22573 self.parse_lr_string_function_call("left")
22574 }
22575 Token::Right if matches!(self.peek(), Token::LParen) => {
22576 self.parse_lr_string_function_call("right")
22577 }
22578 // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
22579 // token; we match on the bare ident. NOT is a token
22580 // (consumed in the comparison rung), but `EXISTS (...)`
22581 // at the top of an expression starts here.
22582 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
22583 self.parse_exists_atom(false)
22584 }
22585 // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
22586 // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
22587 // CASE is a bare ident; we dispatch on lowercase match.
22588 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
22589 self.parse_case_atom()
22590 }
22591 // v7.37.17 (17.6 siblings) — PG typed datetime literals:
22592 // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
22593 // '…'`. Lower onto the ::cast node so the existing
22594 // runtime text→date/timestamp paths do the parsing. The
22595 // string must follow immediately, else the ident stays a
22596 // plain column reference.
22597 Token::Ident(s)
22598 if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
22599 && matches!(self.peek(), Token::String(_)) =>
22600 {
22601 let target =
22602 typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
22603 let Token::String(lit) = self.advance() else {
22604 unreachable!("peek guaranteed a string token");
22605 };
22606 Ok(Expr::Cast {
22607 expr: Box::new(Expr::Literal(Literal::String(lit))),
22608 target,
22609 })
22610 }
22611 // v7.39 (round 221) — the SQL-standard long spellings:
22612 // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
22613 // TIME ZONE '…'`. Consume the modifier and lower to the same
22614 // typed-literal cast (`timetz` / `timestamptz` for WITH).
22615 Token::Ident(s)
22616 if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
22617 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
22618 || w.eq_ignore_ascii_case("without"))
22619 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
22620 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
22621 && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
22622 {
22623 let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
22624 self.advance(); // WITH / WITHOUT
22625 self.advance(); // TIME
22626 self.advance(); // ZONE
22627 let Token::String(lit) = self.advance() else {
22628 unreachable!("guard checked a string token");
22629 };
22630 let base = s.to_ascii_lowercase();
22631 let target = match (base.as_str(), with_tz) {
22632 ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
22633 ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
22634 (_, true) => CastTarget::Timestamptz,
22635 (_, false) => CastTarget::Timestamp,
22636 };
22637 Ok(Expr::Cast {
22638 expr: Box::new(Expr::Literal(Literal::String(lit))),
22639 target,
22640 })
22641 }
22642 // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
22643 // gathers the subquery's single-column rows (in its row order)
22644 // into an array. Desugared to `array_agg` over the subquery as a
22645 // derived table; out-of-line to keep parse_atom's frame small (it
22646 // sits on the recursive nesting-budget cycle).
22647 Token::Ident(s) | Token::QuotedIdent(s)
22648 if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
22649 {
22650 self.parse_array_subquery()
22651 }
22652 // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
22653 // is not a reserved token; we match by case-insensitive
22654 // ident. The opening `[` must follow immediately. v7.39 (read01
22655 // round 105) — the body moved out-of-line so its `Vec`/loop locals
22656 // leave parse_atom's frame (which sits on the nesting-budget cycle).
22657 Token::Ident(s) | Token::QuotedIdent(s)
22658 if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
22659 {
22660 self.parse_array_literal_body()
22661 }
22662 // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
22663 // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
22664 // We special-case before the generic ident dispatch so
22665 // the AGAINST clause never reaches the function-call
22666 // loop (which would mis-read `(cols) AGAINST` as a
22667 // call with no trailing modifier). The shape is
22668 // rewritten to a Boolean OR over per-column
22669 // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
22670 // term)` so the existing FTS evaluator handles
22671 // semantics — the fulltext-GIN built at CREATE TABLE
22672 // time is currently a "real index that survives dump
22673 // round-trip"; the planner hook that actually uses
22674 // it for posting-list intersection lands in a later
22675 // sub-phase (Phase 2.2b) without touching this surface.
22676 Token::Ident(s) | Token::QuotedIdent(s)
22677 if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
22678 {
22679 self.parse_match_against_atom()
22680 }
22681 Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
22682 // v7.37.43-T4 — PG-unreserved keywords are legal column /
22683 // alias names in expression context too. `release` appears
22684 // in sentori `0003_partition_events.sql` as both a column
22685 // reference (SELECT … release …) and an INSERT column list
22686 // entry. Mirrors `expect_ident_like`'s expansion of the
22687 // identifier set.
22688 other if unreserved_keyword_text(&other).is_some() => {
22689 let s = unreserved_keyword_text(&other).unwrap();
22690 self.finish_ident_atom(s)
22691 }
22692 // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
22693 // only inside `SET` before, so `SELECT @@autocommit` — which
22694 // every MySQL connector asks at handshake — was a parse error.
22695 // MariaDB accepts the bare, `@@session.` and `@@global.`
22696 // spellings alike and answers from the session's own value.
22697 Token::SessionVar(v) => {
22698 // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
22699 // has nothing to do with a `@@` engine setting: its own
22700 // per-session namespace, and an unset one reads NULL instead
22701 // of raising. Stripping every `@` (as this did) made `@x` and
22702 // `@@x` the same node, so `SELECT @x` answered "Unknown
22703 // system variable".
22704 Ok(variable_ref_atom(&v))
22705 }
22706 other => Err(ParseError {
22707 message: format!("unexpected token {other:?} in expression"),
22708 token_pos: tok_pos,
22709 }),
22710 }
22711 // After parsing the atom, fold any postfix `::vector` casts.
22712 .and_then(|atom| self.finish_postfix_casts(atom))
22713 }
22714
22715 /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
22716 /// Both bind tighter than any binary op.
22717 /// Shared cast-target parser for postfix `::TYPE` and the
22718 /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
22719 /// If the next tokens are `( N )`, consume them and return the canonical
22720 /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
22721 /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
22722 fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
22723 if !matches!(self.peek(), Token::LParen) {
22724 return None;
22725 }
22726 self.advance(); // (
22727 let n = match self.advance() {
22728 Token::Integer(n) => n,
22729 _ => return Some(base.to_string()), // malformed → drop precision
22730 };
22731 if matches!(self.peek(), Token::RParen) {
22732 self.advance();
22733 }
22734 Some(alloc::format!("{base}({n})"))
22735 }
22736
22737 fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
22738 // r1052 — `::pg_catalog.regproc` and friends: pg_dump
22739 // schema-qualifies every cast target, and `pg_catalog.X` names
22740 // exactly the builtin type X. Consume the qualifier and let
22741 // the ordinary target parse decide.
22742 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
22743 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
22744 {
22745 self.advance();
22746 self.advance();
22747 }
22748 let target = match self.advance() {
22749 Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
22750 "int" | "integer" | "int4" => {
22751 if matches!(self.peek(), Token::LBracket)
22752 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22753 {
22754 self.advance();
22755 self.advance();
22756 CastTarget::IntArray
22757 } else {
22758 CastTarget::Int
22759 }
22760 }
22761 "bigint" | "int8" => {
22762 if matches!(self.peek(), Token::LBracket)
22763 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22764 {
22765 self.advance();
22766 self.advance();
22767 CastTarget::BigIntArray
22768 } else {
22769 CastTarget::BigInt
22770 }
22771 }
22772 "float" | "double" => CastTarget::Float,
22773 "text" => {
22774 // v7.10.11 — `::TEXT[]` widens to TextArray.
22775 if matches!(self.peek(), Token::LBracket)
22776 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22777 {
22778 self.advance();
22779 self.advance();
22780 CastTarget::TextArray
22781 } else {
22782 CastTarget::Text
22783 }
22784 }
22785 "bool" | "boolean" => CastTarget::Bool,
22786 "vector" => CastTarget::Vector,
22787 "date" => CastTarget::Date,
22788 // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22789 // seconds precision through the Named path (the engine rounds
22790 // the sub-second field); bare `::timestamp` keeps the fast arm.
22791 "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22792 Some(named) => CastTarget::Named(named),
22793 None => CastTarget::Timestamp,
22794 },
22795 "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22796 Some(named) => CastTarget::Named(named),
22797 None => CastTarget::Timestamptz,
22798 },
22799 "interval" => CastTarget::Interval,
22800 "json" => CastTarget::Json,
22801 "jsonb" => CastTarget::Jsonb,
22802 // v7.39 (round 694) — these have dedicated CastTarget
22803 // variants, so they never reached the postfix `[]` handling
22804 // further down and `::regtype[]` was a SYNTAX error at the
22805 // `]`. PG has an array type for every scalar; take the
22806 // suffix here and hand the canonical `<ty>_array` name to
22807 // the engine, the same shape every other array cast uses.
22808 "regtype" if self.peek_postfix_array_brackets() => {
22809 self.advance();
22810 self.advance();
22811 CastTarget::Named(alloc::string::String::from("regtype_array"))
22812 }
22813 "regclass" if self.peek_postfix_array_brackets() => {
22814 self.advance();
22815 self.advance();
22816 CastTarget::Named(alloc::string::String::from("regclass_array"))
22817 }
22818 "regtype" => CastTarget::RegType,
22819 "regclass" => CastTarget::RegClass,
22820 // v7.12.0 — `::tsvector` / `::tsquery`.
22821 // Engine decodes the LHS text via the PG
22822 // external form parser.
22823 // v7.39 (round 352, M8) — MySQL's own cast targets.
22824 // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22825 // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22826 // such type, so they are taken only in that dialect and
22827 // fall through to the "type does not exist" arm otherwise.
22828 "signed" | "unsigned" if self.mysql_dialect => {
22829 if matches!(self.peek(), Token::Ident(k)
22830 if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22831 {
22832 self.advance();
22833 }
22834 CastTarget::Named(s.to_ascii_lowercase())
22835 }
22836 // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22837 // in MySQL: MariaDB answers '123' where the SQL-standard
22838 // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22839 // Truncating a number to its first digit is a wrong answer
22840 // with no error, so the MySQL session gets MySQL's reading.
22841 "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22842 CastTarget::Text
22843 }
22844 "tsvector" => CastTarget::TsVector,
22845 "tsquery" => CastTarget::TsQuery,
22846 // v7.17.0 — `::uuid`. Engine decodes the LHS
22847 // text via `spg_storage::parse_uuid_str`.
22848 "uuid" => CastTarget::Uuid,
22849 // v7.18 — `::bytea`. Engine decodes the LHS
22850 // text via the PG hex form (`'\xdeadbeef'`)
22851 // or escape form (`'\\x05\\x00'`). Closes
22852 // mailrs D-pre #3 reverse-acceptance gap.
22853 "bytea" => CastTarget::Bytea,
22854 // v7.37.5 ship triage — generic typed-cast escape.
22855 // Anything the long-tail PG type ident table knows
22856 // about(network/bit/geometry/multirange/etc.)flows
22857 // through `CastTarget::Named(canonical)`; the engine
22858 // resolves via `column_type_to_data_type` and dispatches
22859 // through the typed `coerce_value` path. Truly
22860 // unrecognised idents still hit the error arm below
22861 // because the engine rejects them.
22862 other => {
22863 // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22864 // `::varchar(255)`, etc. Capture into the canonical
22865 // `name(p,s)` form so `type_name_to_data_type` can
22866 // reconstruct the `DataType::Numeric { precision,
22867 // scale }` (and similar param-carrying types).
22868 let mut name = other.to_string();
22869 // v7.39 (round 281) — `::bit varying(3)` is two
22870 // words; fold the tail in so the typmod reaches the
22871 // type resolver instead of tripping the parser.
22872 if name.eq_ignore_ascii_case("bit")
22873 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22874 {
22875 self.advance();
22876 name = alloc::string::String::from("varbit");
22877 }
22878 // v7.39 (round 613) — `::character varying` is the same
22879 // two-word shape and had no fold, so the `varying` was
22880 // left behind and the cast became a bare `character`,
22881 // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22882 // `a` where PG answers `ab`. Silently, and for a spelling
22883 // pg_dump writes.
22884 if name.eq_ignore_ascii_case("character")
22885 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22886 {
22887 self.advance();
22888 name = alloc::string::String::from("varchar");
22889 }
22890 if matches!(self.peek(), Token::LParen) {
22891 let mut buf = alloc::string::String::from("(");
22892 let mut depth = 0usize;
22893 loop {
22894 match self.advance() {
22895 Token::LParen => {
22896 depth += 1;
22897 if depth > 1 {
22898 buf.push('(');
22899 }
22900 }
22901 Token::RParen => {
22902 depth -= 1;
22903 if depth == 0 {
22904 buf.push(')');
22905 break;
22906 }
22907 buf.push(')');
22908 }
22909 Token::Comma => buf.push(','),
22910 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22911 // v7.39 (round 273) — a minus used to fall
22912 // into the catch-all below and vanish, so
22913 // `::numeric(10,-2)` reached the engine as
22914 // the text `numeric(10,2)` and silently
22915 // rounded to two DECIMALS instead of to
22916 // hundreds. A dropped token is not a
22917 // no-op when it carries a sign.
22918 Token::Minus => buf.push('-'),
22919 Token::Eof => break,
22920 _ => {}
22921 }
22922 }
22923 name.push_str(&buf);
22924 }
22925 // Optional postfix `[]` widens to the array form —
22926 // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22927 // The engine's `type_name_to_data_type` recognises
22928 // the canonical `<ty>_array` form.
22929 if matches!(self.peek(), Token::LBracket)
22930 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22931 {
22932 self.advance();
22933 self.advance();
22934 name.push_str("_array");
22935 }
22936 CastTarget::Named(name)
22937 }
22938 },
22939 Token::Interval => CastTarget::Interval,
22940 // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22941 // "char" (oid 18, SPG Char1 — distinct from bare `char`
22942 // = char(1)); other quoted names resolve like idents.
22943 Token::QuotedIdent(q) => {
22944 if q.eq_ignore_ascii_case("char") {
22945 CastTarget::Named("char1".into())
22946 } else {
22947 CastTarget::Named(q.to_ascii_lowercase())
22948 }
22949 }
22950 other => {
22951 return Err(ParseError {
22952 message: format!("expected type ident after `::`, got {other:?}"),
22953 token_pos: self.consumed_pos(),
22954 });
22955 }
22956 };
22957 // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22958 // target to its array sibling. Closed-enum arms (Bool /
22959 // SmallInt / Numeric / Float / Date / …) didn't carry the
22960 // explicit widening that Text / Int / BigInt did, so
22961 // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22962 // error. The widening here mirrors the per-arm Text /
22963 // Int / BigInt logic above + folds the new ζ-A first-class
22964 // types through `CastTarget::Named("<ty>_array")`.
22965 if matches!(self.peek(), Token::LBracket)
22966 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22967 {
22968 let widened = match &target {
22969 CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22970 CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22971 // v7.39 (round 326, V43) — the two temporal types stay
22972 // distinct. Both used to widen to `timestamptz_array`, so
22973 // `::timestamp[]` named the wrong target in its own error
22974 // message and lost the zone-less identity on the way.
22975 CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22976 CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22977 CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22978 CastTarget::Json | CastTarget::Jsonb => {
22979 Some(CastTarget::Named("jsonb_array".to_string()))
22980 }
22981 CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22982 CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22983 CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22984 CastTarget::Named(name) => {
22985 let mut a = name.clone();
22986 a.push_str("_array");
22987 Some(CastTarget::Named(a))
22988 }
22989 // Int / BigInt / Text / Vector / TsVector / TsQuery /
22990 // RegType / RegClass / TextArray / IntArray /
22991 // BigIntArray already finalised — leave as is.
22992 _ => None,
22993 };
22994 if let Some(w) = widened {
22995 self.advance();
22996 self.advance();
22997 return Ok(w);
22998 }
22999 }
23000 Ok(target)
23001 }
23002
23003 fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
23004 loop {
23005 // v7.38 (read01, T9) — composite field access `(expr).field`.
23006 // A bare `a.b` is consumed as a qualified column inside the ident
23007 // atom, so a Dot only survives to this postfix position when the
23008 // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
23009 // `.*` whole-row expansion is not handled here (projection-level).
23010 if matches!(self.peek(), Token::Dot)
23011 && matches!(
23012 self.tokens.get(self.pos + 1),
23013 Some(Token::Ident(_) | Token::QuotedIdent(_))
23014 )
23015 {
23016 self.advance(); // .
23017 let field = match self.advance() {
23018 Token::Ident(s) | Token::QuotedIdent(s) => s,
23019 other => {
23020 return Err(
23021 self.err(format!("expected a field name after '.', got {other:?}"))
23022 );
23023 }
23024 };
23025 expr = Expr::FieldAccess {
23026 base: Box::new(expr),
23027 field,
23028 };
23029 continue;
23030 }
23031 if matches!(self.peek(), Token::DoubleColon) {
23032 self.advance();
23033 // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
23034 // target set to include INTERVAL (reserved Token),
23035 // TIMESTAMPTZ, and PG catalog regtype / regclass.
23036 // mailrs follow-up H3a + H3b.
23037 let target = self.parse_cast_target()?;
23038 expr = Expr::Cast {
23039 expr: Box::new(expr),
23040 target,
23041 };
23042 continue;
23043 }
23044 // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
23045 // returns NULL for out-of-range. Multiple subscripts
23046 // chain: `a[i][j]` parses left-to-right.
23047 if matches!(self.peek(), Token::LBracket) {
23048 self.advance();
23049 // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
23050 // bare index stays a subscript.
23051 let lo = if matches!(self.peek(), Token::Colon) {
23052 None
23053 } else {
23054 Some(self.parse_expr(0)?)
23055 };
23056 if matches!(self.peek(), Token::Colon) {
23057 self.advance();
23058 let hi = if matches!(self.peek(), Token::RBracket) {
23059 None
23060 } else {
23061 Some(Box::new(self.parse_expr(0)?))
23062 };
23063 if !matches!(self.peek(), Token::RBracket) {
23064 return Err(self.err(alloc::format!(
23065 "expected ']' after array slice, got {:?}",
23066 self.peek()
23067 )));
23068 }
23069 self.advance();
23070 expr = Expr::ArraySlice {
23071 target: Box::new(expr),
23072 lo: lo.map(Box::new),
23073 hi,
23074 };
23075 continue;
23076 }
23077 let index = lo.expect("non-colon branch parsed an index");
23078 if !matches!(self.peek(), Token::RBracket) {
23079 return Err(self.err(alloc::format!(
23080 "expected ']' after array index, got {:?}",
23081 self.peek()
23082 )));
23083 }
23084 self.advance();
23085 expr = Expr::ArraySubscript {
23086 target: Box::new(expr),
23087 index: Box::new(index),
23088 };
23089 continue;
23090 }
23091 // `expr AT TIME ZONE zone` — lowers to PG's own function
23092 // form timezone(zone, expr); the scalar implements the
23093 // offset shift (named zones error there — no tzdata).
23094 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
23095 && matches!(self.tokens.get(self.pos + 1),
23096 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
23097 && matches!(self.tokens.get(self.pos + 2),
23098 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
23099 {
23100 self.advance(); // AT
23101 self.advance(); // TIME
23102 self.advance(); // ZONE
23103 // Zone at comparison precedence so AND/OR stay out.
23104 let zone = self.parse_expr(6)?;
23105 expr = Expr::FunctionCall {
23106 name: "timezone".to_string(),
23107 args: alloc::vec![zone, expr],
23108 };
23109 continue;
23110 }
23111 // `expr COLLATE "name"` — SPG's single text ordering IS
23112 // byte order, i.e. the C collation. The byte-order
23113 // spellings absorb as no-ops; a locale collation would
23114 // silently sort differently from PG, so it errors
23115 // honestly instead.
23116 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
23117 self.advance();
23118 let mut cname = match self.advance() {
23119 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23120 other => {
23121 return Err(self.err(alloc::format!(
23122 "expected collation name after COLLATE, got {other:?}"
23123 )));
23124 }
23125 };
23126 // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
23127 // is how `pg_dump` writes the default one:
23128 // `… COLLATE pg_catalog.default`. Reading a single token
23129 // left the SCHEMA as the name, so the clause was refused
23130 // as an unsupported locale collation and no dump ran.
23131 if matches!(self.peek(), Token::Dot) {
23132 // v7.39.2 — the qualifier is DROPPED (SPG is single
23133 // schema) but it is checked first. PostgreSQL 18.6
23134 // answers `schema "nosuch_schema" does not exist` for
23135 // one it has never heard of, and dropping it unread
23136 // meant `COLLATE nosuch_schema."C"` succeeded here —
23137 // a name that names nothing, accepted.
23138 let schema = cname.to_ascii_lowercase();
23139 if !matches!(
23140 schema.as_str(),
23141 "pg_catalog" | "public" | "information_schema"
23142 ) {
23143 return Err(self.err(alloc::format!("schema \"{cname}\" does not exist")));
23144 }
23145 self.advance();
23146 cname = match self.advance() {
23147 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23148 // `default` lexes as a KEYWORD, and it is the name
23149 // pg_dump writes — the same trap round 535 hit with
23150 // TABLE / INDEX / FULL.
23151 Token::Default => alloc::string::String::from("default"),
23152 other => {
23153 return Err(self.err(alloc::format!(
23154 "expected collation name after COLLATE, got {other:?}"
23155 )));
23156 }
23157 };
23158 }
23159 let lc = cname.to_ascii_lowercase();
23160 // v7.39 (round 371, M4 P4b) — a per-expression MySQL
23161 // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
23162 // family / `binary`) forces byte-wise, which is exactly
23163 // what `BINARY expr` does — lower onto that so every fold
23164 // site (comparison, LIKE, ORDER BY) suppresses via
23165 // `is_binary_coerced`. A `_ci` family override folds, and
23166 // under the MySQL dialect the default already folds, so it
23167 // absorbs as a no-op; likewise the C / byte-order spellings.
23168 // v7.39.2 — against MySQL's own list, not against the
23169 // shape of the name. `nosuch_bin` took this shortcut and
23170 // became a BINARY cast; `nosuch_ci` took the one below
23171 // and was absorbed as a no-op. Either way the client
23172 // named a collation that does not exist and was told
23173 // nothing. An unknown name now falls through to the
23174 // node, and the engine refuses it.
23175 let real = crate::charset::is_mysql_collation(&lc);
23176 if self.mysql_dialect && real && (lc.ends_with("_bin") || lc == "binary") {
23177 expr = Expr::Cast {
23178 expr: alloc::boxed::Box::new(expr),
23179 target: CastTarget::Named("binary".to_string()),
23180 };
23181 continue;
23182 }
23183 let mysql_ci = self.mysql_dialect
23184 && ((real && lc.ends_with("_ci"))
23185 || matches!(lc.as_str(), "case_insensitive" | "nocase"));
23186 // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
23187 // goes to the lowering channel, the byte-order spellings
23188 // included. Round 691 recorded only the names the old
23189 // allow-list rejected, which left `ORDER BY a COLLATE "C"`
23190 // absorbed as a no-op — and once a column could declare a
23191 // collation, absorbing the clause meant the COLUMN's
23192 // collation won where the query had asked for bytes.
23193 if self.in_order_by_key && !mysql_ci {
23194 self.order_key_collation = Some(cname);
23195 continue;
23196 }
23197 // v7.39.2 — the clause becomes a NODE rather than being
23198 // refused or absorbed.
23199 //
23200 // What stood here refused the locale names and SILENTLY
23201 // DROPPED the byte-order ones, so `'a' COLLATE "C" < 'B'`
23202 // answered `t` where PostgreSQL 18.6 answers `f`: the one
23203 // family it let through is the one where dropping it
23204 // changes the answer. Absorbing is only correct when the
23205 // collation asked for is the one the comparison would use
23206 // anyway, and that depends on the DATABASE — which the
23207 // parser cannot see. So it rides along and the engine,
23208 // which can, decides.
23209 //
23210 // `collate_derive` already modelled `Explicit(name)` and
23211 // had no way to be handed one.
23212 // v7.39.2 — a MySQL spelling does not exist on the
23213 // PostgreSQL wire, and THIS is where the wire is known.
23214 //
23215 // The check lived in the evaluator first and asked
23216 // `ctx.mysql_dialect`, which the INSERT path builds as a
23217 // hard-coded `false` — so `INSERT … VALUES (_utf8mb4'x')`
23218 // in a MySQL session was refused for a collation that
23219 // does not exist on a wire it was not on. Making that
23220 // context truthful would change INSERT-time evaluation
23221 // in other ways as a side effect; the parser already
23222 // gates the introducer on the same flag and is the
23223 // honest place to ask.
23224 if !self.mysql_dialect
23225 && (lc.ends_with("_ci")
23226 || lc.ends_with("_cs")
23227 || lc.ends_with("_bin")
23228 || lc == "binary"
23229 || matches!(lc.as_str(), "case_insensitive" | "nocase"))
23230 {
23231 return Err(self.err(alloc::format!(
23232 "collation \"{cname}\" for encoding \"UTF8\" does not exist"
23233 )));
23234 }
23235 // v7.39.3 — the node is built for EVERY name, `_ci`
23236 // included.
23237 //
23238 // A MySQL `_ci` spelling used to be absorbed here on the
23239 // reasoning that a MySQL session folds anyway, so the
23240 // clause asked for what it would have got. That stopped
23241 // being true when the fold learned to read the session's
23242 // collation NAME: under `SET NAMES utf8mb4 COLLATE
23243 // utf8mb4_bin`, `'AB' COLLATE utf8mb4_general_ci = 'ab'`
23244 // is 1 on MySQL 9.7.2 and was 0 here, because the clause
23245 // that would have made it 1 had been dropped in the
23246 // parser. Absorbing is only ever correct when the
23247 // collation asked for is the one the comparison would use
23248 // anyway, and the parser cannot know that — the same
23249 // reasoning already written above for the byte-order
23250 // spellings, applied to the family it had exempted.
23251 expr = Expr::Collate {
23252 expr: alloc::boxed::Box::new(expr),
23253 collation: cname,
23254 };
23255 continue;
23256 }
23257 return Ok(expr);
23258 }
23259 }
23260
23261 /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
23262 /// the first token that is not one. Schema qualifiers collapse to the
23263 /// last part, which is what every other name path here does (SPG is
23264 /// single-schema).
23265 fn take_comma_separated_names(&mut self) -> Vec<String> {
23266 let mut out = Vec::new();
23267 while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
23268 self.advance();
23269 let mut last = n;
23270 while matches!(self.peek(), Token::Dot) {
23271 self.advance();
23272 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
23273 last = t;
23274 }
23275 }
23276 out.push(last);
23277 if matches!(self.peek(), Token::Comma) {
23278 self.advance();
23279 } else {
23280 break;
23281 }
23282 }
23283 out
23284 }
23285
23286 /// v7.39 (round 694) — is the next token pair a postfix `[]`?
23287 ///
23288 /// The general cast-target path tests this inline; the types with their
23289 /// own `CastTarget` variant need it as a guard on their match arm,
23290 /// which is what this exists for.
23291 fn peek_postfix_array_brackets(&self) -> bool {
23292 matches!(self.peek(), Token::LBracket)
23293 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23294 }
23295
23296 /// Parse the operator tail after a `(a, b, …)` row constructor
23297 /// and expand at parse time. `=` is the conjunction of element
23298 /// equalities; `<>` its negation; the order operators expand
23299 /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
23300 /// equalities. Anything else (a bare row value, a subquery
23301 /// RHS) errors honestly — SPG has no composite runtime value.
23302 fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
23303 fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
23304 let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
23305 lhs: Box::new(l.clone()),
23306 op: BinOp::Eq,
23307 rhs: Box::new(r.clone()),
23308 });
23309 let first = it.next().expect("row has at least two elements");
23310 it.fold(first, |acc, e| Expr::Binary {
23311 lhs: Box::new(acc),
23312 op: BinOp::And,
23313 rhs: Box::new(e),
23314 })
23315 }
23316 // Lexicographic (a,b) OP (c,d):
23317 // a STRICT c OR (a = c AND (b OP d)) — recursing right.
23318 fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
23319 if lhs.len() == 1 {
23320 return Expr::Binary {
23321 lhs: Box::new(lhs[0].clone()),
23322 op: last,
23323 rhs: Box::new(rhs[0].clone()),
23324 };
23325 }
23326 let head_strict = Expr::Binary {
23327 lhs: Box::new(lhs[0].clone()),
23328 op: strict,
23329 rhs: Box::new(rhs[0].clone()),
23330 };
23331 let head_eq = Expr::Binary {
23332 lhs: Box::new(lhs[0].clone()),
23333 op: BinOp::Eq,
23334 rhs: Box::new(rhs[0].clone()),
23335 };
23336 Expr::Binary {
23337 lhs: Box::new(head_strict),
23338 op: BinOp::Or,
23339 rhs: Box::new(Expr::Binary {
23340 lhs: Box::new(head_eq),
23341 op: BinOp::And,
23342 rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
23343 }),
23344 }
23345 }
23346 let negated_in = if matches!(self.peek(), Token::Not)
23347 && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
23348 {
23349 self.advance();
23350 true
23351 } else {
23352 false
23353 };
23354 if matches!(self.peek(), Token::In) {
23355 self.advance();
23356 if !matches!(self.peek(), Token::LParen) {
23357 return Err(self.err(alloc::format!(
23358 "expected '(' after row IN, got {:?}",
23359 self.peek()
23360 )));
23361 }
23362 self.advance();
23363 // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
23364 // not a list of literal rows. Row-vs-list decomposes to
23365 // OR-of-AND above, but the subquery's rows are only known at
23366 // runtime, so keep it as a RowInSubquery node.
23367 if matches!(self.peek(), Token::Select) {
23368 let inner = self.parse_select_stmt()?;
23369 if !matches!(self.peek(), Token::RParen) {
23370 return Err(self.err(alloc::format!(
23371 "expected ')' after row IN-subquery, got {:?}",
23372 self.peek()
23373 )));
23374 }
23375 self.advance();
23376 let Statement::Select(s) = inner else {
23377 unreachable!("parse_select_stmt always returns Statement::Select")
23378 };
23379 return Ok(Expr::RowInSubquery {
23380 row,
23381 subquery: Box::new(s),
23382 negated: negated_in,
23383 });
23384 }
23385 let mut alternatives: Vec<Expr> = Vec::new();
23386 loop {
23387 // Optional ROW keyword before the paren row.
23388 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23389 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23390 {
23391 self.advance();
23392 }
23393 if !matches!(self.peek(), Token::LParen) {
23394 return Err(self.err(alloc::format!(
23395 "expected '(' to open a row inside IN, got {:?}",
23396 self.peek()
23397 )));
23398 }
23399 self.advance();
23400 let mut rhs = alloc::vec![self.parse_expr(0)?];
23401 while matches!(self.peek(), Token::Comma) {
23402 self.advance();
23403 rhs.push(self.parse_expr(0)?);
23404 }
23405 if !matches!(self.peek(), Token::RParen) {
23406 return Err(self.err(alloc::format!(
23407 "expected ')' after row inside IN, got {:?}",
23408 self.peek()
23409 )));
23410 }
23411 self.advance();
23412 if rhs.len() != row.len() {
23413 return Err(self.err(alloc::format!(
23414 "row IN arity mismatch: left has {}, right has {}",
23415 row.len(),
23416 rhs.len()
23417 )));
23418 }
23419 alternatives.push(row_eq(&row, &rhs));
23420 if matches!(self.peek(), Token::Comma) {
23421 self.advance();
23422 continue;
23423 }
23424 break;
23425 }
23426 if !matches!(self.peek(), Token::RParen) {
23427 return Err(self.err(alloc::format!(
23428 "expected ')' to close row IN list, got {:?}",
23429 self.peek()
23430 )));
23431 }
23432 self.advance();
23433 let mut it = alternatives.into_iter();
23434 let first = it.next().expect("IN list has at least one row");
23435 let combined = it.fold(first, |acc, e| Expr::Binary {
23436 lhs: Box::new(acc),
23437 op: BinOp::Or,
23438 rhs: Box::new(e),
23439 });
23440 return Ok(maybe_not(combined, negated_in));
23441 }
23442 // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
23443 // two periods share at least one time point. Each pair is
23444 // normalised with least/greatest (PG accepts the endpoints
23445 // in either order), then lowered to the standard
23446 // `start1 < end2 AND start2 < end1` form.
23447 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
23448 if row.len() != 2 {
23449 return Err(self.err(alloc::format!(
23450 "OVERLAPS needs (start, end) pairs; left side has {} elements",
23451 row.len()
23452 )));
23453 }
23454 self.advance();
23455 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23456 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23457 {
23458 self.advance();
23459 }
23460 if !matches!(self.peek(), Token::LParen) {
23461 return Err(self.err(alloc::format!(
23462 "expected '(' after OVERLAPS, got {:?}",
23463 self.peek()
23464 )));
23465 }
23466 self.advance();
23467 let r0 = self.parse_expr(0)?;
23468 if !matches!(self.peek(), Token::Comma) {
23469 return Err(self.err(alloc::format!(
23470 "OVERLAPS needs (start, end) on the right, got {:?}",
23471 self.peek()
23472 )));
23473 }
23474 self.advance();
23475 let r1 = self.parse_expr(0)?;
23476 if !matches!(self.peek(), Token::RParen) {
23477 return Err(self.err(alloc::format!(
23478 "expected ')' after OVERLAPS pair, got {:?}",
23479 self.peek()
23480 )));
23481 }
23482 self.advance();
23483 let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
23484 name: String::from(name),
23485 args: alloc::vec![a.clone(), b.clone()],
23486 };
23487 let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
23488 lhs: Box::new(lhs),
23489 op: BinOp::Lt,
23490 rhs: Box::new(rhs),
23491 };
23492 return Ok(Expr::Binary {
23493 lhs: Box::new(lt(
23494 pair_fn("least", &row[0], &row[1]),
23495 pair_fn("greatest", &r0, &r1),
23496 )),
23497 op: BinOp::And,
23498 rhs: Box::new(lt(
23499 pair_fn("least", &r0, &r1),
23500 pair_fn("greatest", &row[0], &row[1]),
23501 )),
23502 });
23503 }
23504 // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
23505 // PG, `IS NULL` is true only when EVERY field is NULL, and
23506 // `IS NOT NULL` is true only when every field is non-NULL — the
23507 // latter is NOT the negation of the former (a mixed row is
23508 // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
23509 // which reproduces exactly that all-fields semantics.
23510 if matches!(self.peek(), Token::Is) {
23511 self.advance();
23512 let negated = if matches!(self.peek(), Token::Not) {
23513 self.advance();
23514 true
23515 } else {
23516 false
23517 };
23518 if !matches!(self.peek(), Token::Null) {
23519 return Err(self.err(alloc::format!(
23520 "expected NULL after row IS [NOT], got {:?}",
23521 self.peek()
23522 )));
23523 }
23524 self.advance();
23525 let mut it = row.iter().map(|e| Expr::IsNull {
23526 expr: Box::new(e.clone()),
23527 negated,
23528 });
23529 let first = it.next().expect("row has at least two elements");
23530 return Ok(it.fold(first, |acc, e| Expr::Binary {
23531 lhs: Box::new(acc),
23532 op: BinOp::And,
23533 rhs: Box::new(e),
23534 }));
23535 }
23536 let op = match self.peek() {
23537 Token::Eq => BinOp::Eq,
23538 Token::NotEq => BinOp::NotEq,
23539 Token::Lt => BinOp::Lt,
23540 Token::LtEq => BinOp::LtEq,
23541 Token::Gt => BinOp::Gt,
23542 Token::GtEq => BinOp::GtEq,
23543 // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
23544 // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
23545 // constructor value, identical to the `ROW(a, b, …)` keyword form:
23546 // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
23547 // (`::text`, `.field`) applies at the caller just as it does for the
23548 // ROW(...) node. All the comparison / predicate forms returned above.
23549 _ => {
23550 return Ok(Expr::FunctionCall {
23551 name: String::from("row"),
23552 args: row,
23553 });
23554 }
23555 };
23556 self.advance();
23557 // Optional ROW keyword before the paren row.
23558 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23559 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23560 {
23561 self.advance();
23562 }
23563 if !matches!(self.peek(), Token::LParen) {
23564 return Err(self.err(alloc::format!(
23565 "expected '(' to open the right-hand row, got {:?}",
23566 self.peek()
23567 )));
23568 }
23569 self.advance();
23570 // `(a, b) <op> (SELECT x, y)` — compare against a single-row
23571 // subquery. Kept as a node (the subquery's row is a runtime value);
23572 // the literal-RHS form below still decomposes at parse time.
23573 if matches!(self.peek(), Token::Select) {
23574 let inner = self.parse_select_stmt()?;
23575 if !matches!(self.peek(), Token::RParen) {
23576 return Err(self.err(alloc::format!(
23577 "expected ')' after row comparison subquery, got {:?}",
23578 self.peek()
23579 )));
23580 }
23581 self.advance();
23582 let Statement::Select(s) = inner else {
23583 unreachable!("parse_select_stmt always returns Statement::Select")
23584 };
23585 return Ok(Expr::RowCmpSubquery {
23586 row,
23587 op,
23588 subquery: Box::new(s),
23589 });
23590 }
23591 let mut rhs = alloc::vec![self.parse_expr(0)?];
23592 while matches!(self.peek(), Token::Comma) {
23593 self.advance();
23594 rhs.push(self.parse_expr(0)?);
23595 }
23596 if !matches!(self.peek(), Token::RParen) {
23597 return Err(self.err(alloc::format!(
23598 "expected ')' after right-hand row, got {:?}",
23599 self.peek()
23600 )));
23601 }
23602 self.advance();
23603 if rhs.len() != row.len() {
23604 // v7.39 (round 239) — PG's wording (42601).
23605 return Err(self.err("unequal number of entries in row expressions".to_string()));
23606 }
23607 Ok(match op {
23608 BinOp::Eq => row_eq(&row, &rhs),
23609 BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
23610 BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
23611 BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
23612 BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
23613 BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
23614 _ => unreachable!("op restricted above"),
23615 })
23616 }
23617
23618 /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
23619 /// escape character becomes the matcher's default backslash:
23620 /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
23621 /// → the char itself, and any pre-existing backslash escapes
23622 /// itself so it stays literal. Both operands must be string
23623 /// literals — a runtime pattern would need matcher support.
23624 fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
23625 let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
23626 (&pattern, &esc)
23627 else {
23628 return Err(
23629 "LIKE ... ESCAPE requires string-literal pattern and escape \
23630 (runtime escape characters are not supported yet)"
23631 .into(),
23632 );
23633 };
23634 // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
23635 // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
23636 // multi-character escape is an error.
23637 let esc_ch: Option<char> = {
23638 let mut ch_iter = e.chars();
23639 match (ch_iter.next(), ch_iter.next()) {
23640 (Some(c), None) => Some(c),
23641 (None, _) => None,
23642 (Some(_), Some(_)) => {
23643 return Err(alloc::format!(
23644 "ESCAPE must be a single character, got {e:?}"
23645 ));
23646 }
23647 }
23648 };
23649 let mut out = String::with_capacity(p.len() + 4);
23650 let mut chars = p.chars();
23651 while let Some(c) = chars.next() {
23652 if Some(c) == esc_ch {
23653 match chars.next() {
23654 // Escaped wildcard / escaped escape → keep the
23655 // next char literal via backslash.
23656 Some(next) => {
23657 out.push('\\');
23658 out.push(next);
23659 }
23660 None => {
23661 return Err("LIKE pattern ends with the escape character".into());
23662 }
23663 }
23664 } else if c == '\\' && esc_ch != Some('\\') {
23665 // A raw backslash is literal under a custom (or absent) escape
23666 // — escape it for the backslash-based matcher.
23667 out.push('\\');
23668 out.push('\\');
23669 } else {
23670 out.push(c);
23671 }
23672 }
23673 Ok(Expr::Literal(Literal::String(out)))
23674 }
23675
23676 /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
23677 /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
23678 /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
23679 /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
23680 /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
23681 /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
23682 /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
23683 /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
23684 /// array expression errors honestly rather than silently mismatching.
23685 fn try_like_any_all(
23686 &mut self,
23687 base: &Expr,
23688 negated: bool,
23689 case_insensitive: bool,
23690 ) -> Result<Option<Expr>, ParseError> {
23691 let is_any = match self.peek() {
23692 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
23693 Token::Ident(s)
23694 if s.eq_ignore_ascii_case("any")
23695 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
23696 {
23697 true
23698 }
23699 _ => return Ok(None),
23700 };
23701 self.advance(); // ANY / ALL
23702 self.advance(); // '('
23703 let arr = self.parse_expr(0)?;
23704 if !matches!(self.peek(), Token::RParen) {
23705 return Err(self.err(format!(
23706 "expected ')' after LIKE {} argument, got {:?}",
23707 if is_any { "ANY" } else { "ALL" },
23708 self.peek()
23709 )));
23710 }
23711 self.advance(); // ')'
23712 let Expr::Array(items) = arr else {
23713 return Err(self.err(
23714 "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
23715 ));
23716 };
23717 let mut clauses = items.into_iter().map(|p| Expr::Like {
23718 expr: Box::new(base.clone()),
23719 pattern: Box::new(p),
23720 negated,
23721 case_insensitive,
23722 });
23723 let Some(first) = clauses.next() else {
23724 // ANY(empty) = FALSE, ALL(empty) = TRUE.
23725 return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
23726 };
23727 let op = if is_any { BinOp::Or } else { BinOp::And };
23728 let combined = clauses.fold(first, |acc, c| Expr::Binary {
23729 lhs: Box::new(acc),
23730 op,
23731 rhs: Box::new(c),
23732 });
23733 Ok(Some(combined))
23734 }
23735
23736 /// `x BETWEEN low AND high` → `(x >= low) AND (x <= high)`, wrapped in
23737 /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
23738 /// `AND` is not swallowed.
23739 fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23740 self.advance(); // BETWEEN
23741 // SYMMETRIC — the bounds may arrive in either order; both
23742 // orientations OR together. ASYMMETRIC is the default and
23743 // absorbs as noise.
23744 let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
23745 {
23746 self.advance();
23747 true
23748 } else {
23749 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
23750 self.advance();
23751 }
23752 false
23753 };
23754 let low = self.parse_expr(6)?;
23755 if !matches!(self.peek(), Token::And) {
23756 return Err(self.err(format!(
23757 "expected AND after BETWEEN low bound, got {:?}",
23758 self.peek()
23759 )));
23760 }
23761 self.advance();
23762 let high = self.parse_expr(6)?;
23763 let target = Box::new(expr);
23764 let range = |lo: Expr, hi: Expr| Expr::Binary {
23765 lhs: Box::new(Expr::Binary {
23766 lhs: target.clone(),
23767 op: BinOp::GtEq,
23768 rhs: Box::new(lo),
23769 }),
23770 op: BinOp::And,
23771 rhs: Box::new(Expr::Binary {
23772 lhs: target.clone(),
23773 op: BinOp::LtEq,
23774 rhs: Box::new(hi),
23775 }),
23776 };
23777 let combined = if symmetric {
23778 Expr::Binary {
23779 lhs: Box::new(range(low.clone(), high.clone())),
23780 op: BinOp::Or,
23781 rhs: Box::new(range(high, low)),
23782 }
23783 } else {
23784 range(low, high)
23785 };
23786 Ok(maybe_not(combined, negated))
23787 }
23788
23789 /// `x IN (a, b, c)` → chained OR of equalities. Empty list collapses
23790 /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
23791 /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
23792 /// Caller already consumed the leading `WITH` ident.
23793 /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
23794 /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
23795 /// self-reference that appears more than once in a single term.
23796 fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
23797 use crate::ast::{CteBody, SelectStatement};
23798 if !cte.recursive {
23799 return Ok(());
23800 }
23801 let CteBody::Select(body) = &cte.body else {
23802 return Ok(());
23803 };
23804 // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
23805 // check the anchor and every peer term.
23806 let has_order = |s: &SelectStatement| !s.order_by.is_empty();
23807 let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
23808 if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
23809 return Err(self.err(String::from(
23810 "ORDER BY in a recursive query is not implemented",
23811 )));
23812 }
23813 if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
23814 return Err(self.err(String::from(
23815 "LIMIT in a recursive query is not implemented",
23816 )));
23817 }
23818 let self_refs = |s: &SelectStatement| -> usize {
23819 let Some(from) = &s.from else {
23820 return 0;
23821 };
23822 let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
23823 for j in &from.joins {
23824 if j.table.name.eq_ignore_ascii_case(&cte.name) {
23825 n += 1;
23826 }
23827 }
23828 n
23829 };
23830 if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
23831 return Err(self.err(alloc::format!(
23832 "recursive reference to query \"{}\" must not appear more than once",
23833 cte.name
23834 )));
23835 }
23836 // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
23837 // apply only when the body actually references itself (a non-self-
23838 // referencing CTE under WITH RECURSIVE may use any set-op shape).
23839 let anchor_refs = self_refs(body);
23840 let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
23841 if anchor_refs > 0 || union_refs {
23842 // Shape: the top level must be UNION [ALL] arms only. A self-ref
23843 // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
23844 // "does not have the form" error — SPG used to compute a value.
23845 if body.unions.is_empty()
23846 || body.unions.iter().any(|(k, _)| {
23847 !matches!(
23848 k,
23849 crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
23850 )
23851 })
23852 {
23853 return Err(self.err(alloc::format!(
23854 "recursive query \"{}\" does not have the form non-recursive-term \
23855 UNION [ALL] recursive-term",
23856 cte.name
23857 )));
23858 }
23859 if anchor_refs > 0 {
23860 return Err(self.err(alloc::format!(
23861 "recursive reference to query \"{}\" must not appear within its non-recursive term",
23862 cte.name
23863 )));
23864 }
23865 }
23866 let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23867 for (_, u) in &body.unions {
23868 if self_refs(u) == 0 {
23869 continue;
23870 }
23871 // The self-reference must not sit on the nullable side of an outer
23872 // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23873 if let Some(from) = &u.from {
23874 for (i, j) in from.joins.iter().enumerate() {
23875 let left_has_self = is_self(&from.primary)
23876 || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23877 let violated = match j.kind {
23878 crate::ast::JoinKind::Left => is_self(&j.table),
23879 crate::ast::JoinKind::Right => left_has_self,
23880 crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23881 _ => false,
23882 };
23883 if violated {
23884 return Err(self.err(alloc::format!(
23885 "recursive reference to query \"{}\" must not appear within an outer join",
23886 cte.name
23887 )));
23888 }
23889 }
23890 }
23891 // No aggregates at the top level of the recursive term (SPG used
23892 // to run them and surface a misleading downstream error).
23893 let mut items_and_having: Vec<&Expr> = Vec::new();
23894 for it in &u.items {
23895 if let crate::ast::SelectItem::Expr { expr, .. } = it {
23896 items_and_having.push(expr);
23897 }
23898 }
23899 if let Some(h) = &u.having {
23900 items_and_having.push(h);
23901 }
23902 for e in items_and_having {
23903 if expr_has_toplevel_aggregate(e) {
23904 return Err(self.err(String::from(
23905 "aggregate functions are not allowed in a recursive query's recursive term",
23906 )));
23907 }
23908 }
23909 }
23910 // A self-reference inside a sublink expression (EXISTS / IN / scalar
23911 // subquery) anywhere in the body is rejected; a plain FROM derived
23912 // table is legal in PG and untouched here.
23913 let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23914 all_terms.extend(body.unions.iter().map(|(_, u)| u));
23915 for term in all_terms {
23916 if select_has_self_ref_in_sublink(term, &cte.name) {
23917 return Err(self.err(alloc::format!(
23918 "recursive reference to query \"{}\" must not appear within a subquery",
23919 cte.name
23920 )));
23921 }
23922 }
23923 Ok(())
23924 }
23925
23926 /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23927 /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23928 /// right after parse so the engine sees a plain recursive CTE with the
23929 /// tracking columns already projected. DEPTH FIRST and CYCLE are
23930 /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23931 /// text-rendered rows can't provide, and errors honestly.
23932 fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23933 use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23934 if cte.search.is_none() && cte.cycle.is_none() {
23935 return Ok(());
23936 }
23937 let cte_name = cte.name.clone();
23938 let col_names = cte.column_overrides.clone();
23939 if col_names.is_empty() {
23940 return Err(
23941 self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23942 );
23943 }
23944 let search = cte.search.take();
23945 let cycle = cte.cycle.take();
23946 let mut extra_cols: Vec<String> = Vec::new();
23947 let col_ref = |name: &str| {
23948 Expr::Column(ColumnName {
23949 qualifier: Some(cte_name.clone()),
23950 name: name.to_string(),
23951 })
23952 };
23953 // Position of a SEARCH/CYCLE column within the CTE's column list.
23954 let pos_of = |name: &str| -> Result<usize, ParseError> {
23955 col_names
23956 .iter()
23957 .position(|c| c.eq_ignore_ascii_case(name))
23958 .ok_or_else(|| {
23959 self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23960 })
23961 };
23962 let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23963 let mut args = Vec::with_capacity(positions.len());
23964 for &p in positions {
23965 match items.get(p) {
23966 Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23967 _ => {
23968 return Err(self.err(
23969 "SEARCH/CYCLE column maps to a non-expression select item".into(),
23970 ));
23971 }
23972 }
23973 }
23974 Ok(Expr::FunctionCall {
23975 name: "row".into(),
23976 args,
23977 })
23978 };
23979 let CteBody::Select(body) = &mut cte.body else {
23980 return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23981 };
23982 if body.unions.is_empty() {
23983 return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23984 }
23985 let rec = body.unions.len() - 1; // recursive term = last UNION peer
23986
23987 if let Some(srch) = search {
23988 // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23989 // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23990 // no typed `record[]`, but element-wise array ORDER BY is correct
23991 // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23992 // exactly onto a typed array: DEPTH is the root→node path
23993 // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23994 // orders numerically (multi-digit keys included), matching PG.
23995 //
23996 // A multi-column BY would need a record[] to keep the per-node key
23997 // tuple orderable, which SPG can't express — error honestly there
23998 // rather than mis-order.
23999 if srch.by_columns.len() != 1 {
24000 return Err(self.err(
24001 "SEARCH … BY with multiple columns needs typed record[] ordering \
24002 SPG doesn't have yet; a single BY column is supported"
24003 .into(),
24004 ));
24005 }
24006 let key_pos = pos_of(&srch.by_columns[0])?;
24007 let base_key = match body.items.get(key_pos) {
24008 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
24009 _ => {
24010 return Err(
24011 self.err("SEARCH BY column maps to a non-expression select item".into())
24012 );
24013 }
24014 };
24015 let rec_key = match body.unions[rec].1.items.get(key_pos) {
24016 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
24017 _ => {
24018 return Err(
24019 self.err("SEARCH BY column maps to a non-expression select item".into())
24020 );
24021 }
24022 };
24023 if srch.depth_first {
24024 // base: ARRAY[key]; rec: array_append(cte.set, key).
24025 body.items.push(SelectItem::Expr {
24026 expr: Expr::Array(alloc::vec![base_key]),
24027 alias: Some(srch.set_column.clone()),
24028 });
24029 body.unions[rec].1.items.push(SelectItem::Expr {
24030 expr: Expr::FunctionCall {
24031 name: "array_append".into(),
24032 args: alloc::vec![col_ref(&srch.set_column), rec_key],
24033 },
24034 alias: Some(srch.set_column.clone()),
24035 });
24036 } else {
24037 // BREADTH: [depth, key]; depth starts at 0 and increments. The
24038 // leading depth element dominates the element-wise comparison,
24039 // so shallower rows sort first, then by key — PG's (depth, key).
24040 body.items.push(SelectItem::Expr {
24041 expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
24042 alias: Some(srch.set_column.clone()),
24043 });
24044 // rec depth = cte.set[1] + 1.
24045 let parent_depth = Expr::ArraySubscript {
24046 target: Box::new(col_ref(&srch.set_column)),
24047 index: Box::new(Expr::Literal(Literal::Integer(1))),
24048 };
24049 body.unions[rec].1.items.push(SelectItem::Expr {
24050 expr: Expr::Array(alloc::vec![
24051 Expr::Binary {
24052 lhs: Box::new(parent_depth),
24053 op: BinOp::Add,
24054 rhs: Box::new(Expr::Literal(Literal::Integer(1))),
24055 },
24056 rec_key,
24057 ]),
24058 alias: Some(srch.set_column.clone()),
24059 });
24060 }
24061 extra_cols.push(srch.set_column);
24062 }
24063
24064 if let Some(cyc) = cycle {
24065 let positions: Vec<usize> = cyc
24066 .columns
24067 .iter()
24068 .map(|c| pos_of(c))
24069 .collect::<Result<_, _>>()?;
24070 // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
24071 // cast it to text for the cycle path: membership only needs equality,
24072 // and the record text form gives SPG a TextArray path (SPG has no
24073 // typed record[] array). Cycle detection is unaffected.
24074 let base_row = Expr::Cast {
24075 expr: Box::new(row_of(&body.items, &positions)?),
24076 target: CastTarget::Text,
24077 };
24078 let rec_row = Expr::Cast {
24079 expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
24080 target: CastTarget::Text,
24081 };
24082 let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
24083 let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
24084 // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
24085 body.items.push(SelectItem::Expr {
24086 expr: Expr::Literal(dflt.clone()),
24087 alias: Some(cyc.mark_column.clone()),
24088 });
24089 body.items.push(SelectItem::Expr {
24090 expr: Expr::Array(alloc::vec![base_row]),
24091 alias: Some(cyc.path_column.clone()),
24092 });
24093 // rec mark: ROW(cols) already in the path → cycle.
24094 let hit = Expr::AnyAll {
24095 expr: Box::new(rec_row.clone()),
24096 op: BinOp::Eq,
24097 array: Box::new(col_ref(&cyc.path_column)),
24098 is_any: true,
24099 };
24100 let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
24101 Expr::Case {
24102 operand: None,
24103 branches: alloc::vec![(hit, Expr::Literal(mark))],
24104 else_branch: Some(Box::new(Expr::Literal(dflt))),
24105 }
24106 } else {
24107 hit
24108 };
24109 body.unions[rec].1.items.push(SelectItem::Expr {
24110 expr: mark_expr,
24111 alias: Some(cyc.mark_column.clone()),
24112 });
24113 // rec path: array_append(cte.path, ROW(cols)).
24114 body.unions[rec].1.items.push(SelectItem::Expr {
24115 expr: Expr::FunctionCall {
24116 name: "array_append".into(),
24117 args: alloc::vec![col_ref(&cyc.path_column), rec_row],
24118 },
24119 alias: Some(cyc.path_column.clone()),
24120 });
24121 // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
24122 let stop = Expr::Unary {
24123 op: UnOp::Not,
24124 expr: Box::new(col_ref(&cyc.mark_column)),
24125 };
24126 let w = &mut body.unions[rec].1.where_;
24127 *w = Some(match w.take() {
24128 Some(prev) => Expr::Binary {
24129 lhs: Box::new(prev),
24130 op: BinOp::And,
24131 rhs: Box::new(stop),
24132 },
24133 None => stop,
24134 });
24135 extra_cols.push(cyc.mark_column);
24136 extra_cols.push(cyc.path_column);
24137 }
24138 cte.column_overrides.extend(extra_cols);
24139 Ok(())
24140 }
24141
24142 /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
24143 /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
24144 fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
24145 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
24146 return Ok(None);
24147 }
24148 self.advance(); // SEARCH
24149 let depth_first = match self.peek() {
24150 Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
24151 Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
24152 other => {
24153 return Err(self.err(format!(
24154 "expected DEPTH or BREADTH after SEARCH, got {other:?}"
24155 )));
24156 }
24157 };
24158 self.advance();
24159 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
24160 return Err(self.err(format!(
24161 "expected FIRST after SEARCH mode, got {:?}",
24162 self.peek()
24163 )));
24164 }
24165 self.advance();
24166 if !self.peek_is_by() {
24167 return Err(self.err(format!(
24168 "expected BY after SEARCH … FIRST, got {:?}",
24169 self.peek()
24170 )));
24171 }
24172 self.advance();
24173 let mut by_columns = alloc::vec![self.expect_ident_like()?];
24174 while matches!(self.peek(), Token::Comma) {
24175 self.advance();
24176 by_columns.push(self.expect_ident_like()?);
24177 }
24178 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24179 return Err(self.err(format!(
24180 "expected SET in SEARCH clause, got {:?}",
24181 self.peek()
24182 )));
24183 }
24184 self.advance();
24185 let set_column = self.expect_ident_like()?;
24186 Ok(Some(crate::ast::SearchClause {
24187 depth_first,
24188 by_columns,
24189 set_column,
24190 }))
24191 }
24192
24193 /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
24194 /// USING pathcol`. Returns None when the next token isn't CYCLE.
24195 fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
24196 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
24197 return Ok(None);
24198 }
24199 self.advance(); // CYCLE
24200 let mut columns = alloc::vec![self.expect_ident_like()?];
24201 while matches!(self.peek(), Token::Comma) {
24202 self.advance();
24203 columns.push(self.expect_ident_like()?);
24204 }
24205 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24206 return Err(self.err(format!(
24207 "expected SET in CYCLE clause, got {:?}",
24208 self.peek()
24209 )));
24210 }
24211 self.advance();
24212 let mark_column = self.expect_ident_like()?;
24213 let mut mark_value = None;
24214 let mut default_value = None;
24215 if matches!(self.peek(), Token::To) {
24216 self.advance();
24217 mark_value = Some(self.parse_cycle_literal()?);
24218 if !matches!(self.peek(), Token::Default) {
24219 return Err(self.err(format!(
24220 "expected DEFAULT after CYCLE … TO, got {:?}",
24221 self.peek()
24222 )));
24223 }
24224 self.advance();
24225 default_value = Some(self.parse_cycle_literal()?);
24226 }
24227 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
24228 return Err(self.err(format!(
24229 "expected USING in CYCLE clause, got {:?}",
24230 self.peek()
24231 )));
24232 }
24233 self.advance();
24234 let path_column = self.expect_ident_like()?;
24235 Ok(Some(crate::ast::CycleClause {
24236 columns,
24237 mark_column,
24238 mark_value,
24239 default_value,
24240 path_column,
24241 }))
24242 }
24243
24244 /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
24245 /// literal (string / bool / number) in PG.
24246 fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
24247 match self.parse_expr(0)? {
24248 Expr::Literal(l) => Ok(l),
24249 other => Err(self.err(format!(
24250 "CYCLE mark/default value must be a literal, got {other:?}"
24251 ))),
24252 }
24253 }
24254
24255 fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
24256 // v4.22: WITH RECURSIVE — optional keyword right after WITH.
24257 // Comes through as an identifier; consume it if present and
24258 // mark every CTE in the clause as recursive (PG semantics —
24259 // the flag is per-WITH, not per-CTE).
24260 let mut recursive = false;
24261 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
24262 && s.eq_ignore_ascii_case("recursive")
24263 {
24264 self.advance();
24265 recursive = true;
24266 }
24267 let mut ctes = Vec::new();
24268 loop {
24269 let name = self.expect_ident_like()?;
24270 // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
24271 // PG uses these to rename the body's output columns; we
24272 // do the same below by overriding `columns[i].name`.
24273 let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
24274 self.advance();
24275 let mut names = Vec::new();
24276 loop {
24277 names.push(self.expect_ident_like()?);
24278 if matches!(self.peek(), Token::Comma) {
24279 self.advance();
24280 continue;
24281 }
24282 break;
24283 }
24284 if !matches!(self.peek(), Token::RParen) {
24285 return Err(self.err(format!(
24286 "expected ')' to close CTE column list, got {:?}",
24287 self.peek()
24288 )));
24289 }
24290 self.advance();
24291 names
24292 } else {
24293 Vec::new()
24294 };
24295 // AS is a reserved Token::As (used by SELECT-item / FROM
24296 // aliasing) — handle it specially rather than as a bare
24297 // ident.
24298 if !matches!(self.peek(), Token::As) {
24299 return Err(self.err(format!(
24300 "expected AS after CTE name {name:?}, got {:?}",
24301 self.peek()
24302 )));
24303 }
24304 self.advance();
24305 // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
24306 // MATERIALIZED` optimizer hints. SPG materialises every
24307 // CTE, so both spellings are accepted and absorbed.
24308 if matches!(self.peek(), Token::Not) {
24309 self.advance(); // NOT
24310 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24311 if s.eq_ignore_ascii_case("materialized"))
24312 {
24313 self.advance();
24314 } else {
24315 return Err(self.err(format!(
24316 "expected MATERIALIZED after AS NOT, got {:?}",
24317 self.peek()
24318 )));
24319 }
24320 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24321 if s.eq_ignore_ascii_case("materialized"))
24322 {
24323 self.advance();
24324 }
24325 if !matches!(self.peek(), Token::LParen) {
24326 return Err(self.err(format!(
24327 "expected '(' after AS in WITH clause, got {:?}",
24328 self.peek()
24329 )));
24330 }
24331 self.advance();
24332 // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
24333 // RETURNING) as the CTE body in addition to SELECT.
24334 // PG writable CTE semantics. UPDATE / DELETE come in as
24335 // bare Idents (lexer keeps SELECT / INSERT as reserved
24336 // tokens but treats the rest of DML as case-insensitive
24337 // idents).
24338 let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24339 let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24340 let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24341 let body = match self.peek() {
24342 Token::Select => {
24343 let inner = self.parse_select_stmt()?;
24344 let Statement::Select(s) = inner else {
24345 unreachable!("parse_select_stmt returns Select");
24346 };
24347 crate::ast::CteBody::Select(s)
24348 }
24349 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24350 // `SELECT * FROM t` this way and accepts it wherever a
24351 // SELECT goes, so the CTE body dispatch needs its own
24352 // arm: this match is keyed on the FIRST token, and
24353 // `Token::Table` fell through to a tail that then
24354 // rejected what it got. `parse_table_shorthand` has
24355 // returned a desugared SelectStatement since the
24356 // shorthand landed — only the routing was missing.
24357 // Round 868 found this by putting the shorthand in a
24358 // subquery; every earlier check used a top-level form.
24359 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24360 // `SELECT * FROM t` this way and accepts it wherever a
24361 // SELECT goes, so the CTE body dispatch needs its own
24362 // arm: this match is keyed on the FIRST token, and
24363 // `Token::Table` fell through to a tail that rejected
24364 // what it got. `parse_table_shorthand` has returned a
24365 // desugared SelectStatement since the shorthand landed —
24366 // only the routing was missing, here and in the derived
24367 // table's second-token gate. Round 868 found both by
24368 // putting the shorthand in a subquery; every earlier
24369 // check had used a top-level form.
24370 Token::Table
24371 if matches!(
24372 self.tokens.get(self.pos + 1),
24373 Some(Token::Ident(_) | Token::QuotedIdent(_))
24374 ) =>
24375 {
24376 let mut head = self.parse_table_shorthand()?;
24377 self.parse_setop_chain_into(&mut head)?;
24378 self.parse_select_tail_into(&mut head)?;
24379 crate::ast::CteBody::Select(head)
24380 }
24381 // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
24382 // WITH t(a) AS (VALUES (1), (2)) … lowers through
24383 // the shared rows helper onto a Select body.
24384 Token::Values => {
24385 self.advance(); // VALUES
24386 let mut head = self.parse_values_rows_body()?;
24387 // A VALUES seed can head a set-operation chain —
24388 // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
24389 // SELECT n+1 FROM t …). Attach any trailing
24390 // UNION / INTERSECT / EXCEPT peers so the
24391 // recursive-CTE body parses like the SELECT seed.
24392 self.parse_setop_chain_into(&mut head)?;
24393 crate::ast::CteBody::Select(head)
24394 }
24395 Token::Insert => {
24396 let inner = self.parse_one_statement()?;
24397 let Statement::Insert(s) = inner else {
24398 unreachable!("Token::Insert routes to Insert");
24399 };
24400 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24401 }
24402 _ if is_update_kw => {
24403 let inner = self.parse_one_statement()?;
24404 let Statement::Update(s) = inner else {
24405 return Err(
24406 self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
24407 );
24408 };
24409 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24410 }
24411 _ if is_delete_kw => {
24412 let inner = self.parse_one_statement()?;
24413 let Statement::Delete(s) = inner else {
24414 return Err(
24415 self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
24416 );
24417 };
24418 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24419 }
24420 // v7.39 (round 149) — PG 17 allows MERGE as a
24421 // data-modifying CTE body.
24422 _ if is_merge_kw => {
24423 let inner = self.parse_one_statement()?;
24424 let Statement::Merge(s) = inner else {
24425 return Err(
24426 self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
24427 );
24428 };
24429 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24430 }
24431 // v7.39 (round 151) — a CTE body may itself be
24432 // WITH-headed (PG grammar: PreparableStmt carries its
24433 // own with_clause). The nested statement keeps its own
24434 // ctes; the modifying-CTE-at-top-level rule is enforced
24435 // at execution.
24436 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
24437 self.advance(); // WITH
24438 match self.parse_with_cte_then_select()? {
24439 Statement::Select(s) => crate::ast::CteBody::Select(s),
24440 Statement::Insert(s) => {
24441 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24442 }
24443 Statement::Update(s) => {
24444 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24445 }
24446 Statement::Delete(s) => {
24447 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24448 }
24449 Statement::Merge(s) => {
24450 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24451 }
24452
24453 other => {
24454 return Err(self.err(format!(
24455 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24456 )));
24457 }
24458 }
24459 }
24460 other => {
24461 return Err(self.err(format!(
24462 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24463 )));
24464 }
24465 };
24466 if !matches!(self.peek(), Token::RParen) {
24467 return Err(self.err(format!(
24468 "expected ')' after CTE body, got {:?}",
24469 self.peek()
24470 )));
24471 }
24472 self.advance();
24473 // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
24474 // CTE, desugared into extra body columns by the engine.
24475 let search = self.parse_cte_search_clause()?;
24476 let cycle = self.parse_cte_cycle_clause()?;
24477 let mut cte = crate::ast::Cte {
24478 name,
24479 body,
24480 recursive,
24481 column_overrides,
24482 search,
24483 cycle,
24484 };
24485 self.validate_recursive_cte(&cte)?;
24486 self.desugar_cte_search_cycle(&mut cte)?;
24487 ctes.push(cte);
24488 if matches!(self.peek(), Token::Comma) {
24489 self.advance();
24490 continue;
24491 }
24492 break;
24493 }
24494 // v7.37.43-T4.4 — the outer body may be SELECT (classical),
24495 // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
24496 // the parsed CTEs to whichever statement the body produces.
24497 let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24498 let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24499 let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24500 match self.peek() {
24501 Token::Select => {
24502 let body_stmt = self.parse_select_stmt()?;
24503 let Statement::Select(mut body) = body_stmt else {
24504 unreachable!()
24505 };
24506 body.ctes = ctes;
24507 Ok(Statement::Select(body))
24508 }
24509 Token::Insert => {
24510 let body_stmt = self.parse_one_statement()?;
24511 let Statement::Insert(mut body) = body_stmt else {
24512 unreachable!()
24513 };
24514 body.ctes = ctes;
24515 Ok(Statement::Insert(body))
24516 }
24517 _ if outer_is_update => {
24518 let body_stmt = self.parse_one_statement()?;
24519 let Statement::Update(mut body) = body_stmt else {
24520 return Err(self.err(format!("expected UPDATE after WITH clause")));
24521 };
24522 body.ctes = ctes;
24523 Ok(Statement::Update(body))
24524 }
24525 _ if outer_is_delete => {
24526 let body_stmt = self.parse_one_statement()?;
24527 let Statement::Delete(mut body) = body_stmt else {
24528 return Err(self.err(format!("expected DELETE after WITH clause")));
24529 };
24530 body.ctes = ctes;
24531 Ok(Statement::Delete(body))
24532 }
24533 // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
24534 // WITH RECURSIVE is rejected with PG's exact message
24535 // (parse analysis, transformWithClause).
24536 _ if outer_is_merge => {
24537 if recursive {
24538 return Err(self.err(String::from(
24539 "WITH RECURSIVE is not supported for MERGE statement",
24540 )));
24541 }
24542 let body_stmt = self.parse_one_statement()?;
24543 let Statement::Merge(mut body) = body_stmt else {
24544 return Err(self.err(format!("expected MERGE after WITH clause")));
24545 };
24546 body.ctes = ctes;
24547 Ok(Statement::Merge(body))
24548 }
24549 other => Err(self.err(format!(
24550 "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
24551 ))),
24552 }
24553 }
24554
24555 /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
24556 /// already consumed the leading `EXISTS` ident via
24557 /// `self.advance()`.
24558 /// v7.13.0 — parse the rest of a `CASE … END` expression after
24559 /// the leading `CASE` ident has been consumed (mailrs round-5
24560 /// G9). Supports both the searched form
24561 /// (`CASE WHEN cond THEN val …`) and the simple form
24562 /// (`CASE operand WHEN val THEN val …`).
24563 fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
24564 // Disambiguate searched vs simple form: if the next token
24565 // is `WHEN`, we're in the searched form. Otherwise the
24566 // intervening expression is the operand.
24567 let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
24568 None
24569 } else {
24570 Some(Box::new(self.parse_expr(0)?))
24571 };
24572 let mut branches: Vec<(Expr, Expr)> = Vec::new();
24573 loop {
24574 match self.peek() {
24575 Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
24576 self.advance();
24577 let cond = self.parse_expr(0)?;
24578 match self.peek() {
24579 Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
24580 self.advance();
24581 }
24582 other => {
24583 return Err(self.err(alloc::format!(
24584 "expected THEN after CASE WHEN <expr>, got {other:?}"
24585 )));
24586 }
24587 }
24588 let value = self.parse_expr(0)?;
24589 branches.push((cond, value));
24590 }
24591 _ => break,
24592 }
24593 }
24594 if branches.is_empty() {
24595 return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
24596 }
24597 let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
24598 {
24599 self.advance();
24600 Some(Box::new(self.parse_expr(0)?))
24601 } else {
24602 None
24603 };
24604 match self.peek() {
24605 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
24606 self.advance();
24607 }
24608 other => {
24609 return Err(self.err(alloc::format!(
24610 "expected END to close CASE expression, got {other:?}"
24611 )));
24612 }
24613 }
24614 Ok(Expr::Case {
24615 operand,
24616 branches,
24617 else_branch,
24618 })
24619 }
24620
24621 /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
24622 /// query-source position (EXISTS / IN / INSERT source / CTE body /
24623 /// view body). Caller consumed the WITH keyword. Only a SELECT
24624 /// outer is grammatical here; the data-modifying-CTE-at-top-level
24625 /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
24626 /// maps correctly.
24627 fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24628 let inner = self.parse_with_cte_then_select()?;
24629 match inner {
24630 Statement::Select(s) => Ok(s),
24631 other => Err(self.err(format!(
24632 "expected SELECT after WITH in a subquery, got {other:?}"
24633 ))),
24634 }
24635 }
24636
24637 /// True when the next token is the (unquoted) WITH keyword. WITH is
24638 /// reserved in PG, so a bare `with` can never be a column reference
24639 /// in these positions; a quoted `"with"` stays an identifier.
24640 fn peek_is_with_kw(&self) -> bool {
24641 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
24642 }
24643
24644 /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
24645 /// `#[inline(never)]` keeps the large SelectStatement temporaries
24646 /// off parse_expr's recursive frame (the nesting-budget stack
24647 /// cliff — see the round-153 gate regression).
24648 #[inline(never)]
24649 fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24650 if self.peek_is_with_kw() {
24651 self.advance();
24652 self.parse_nested_with_select()
24653 } else {
24654 match self.parse_select_stmt()? {
24655 Statement::Select(s) => Ok(s),
24656 other => Err(self.err(alloc::format!(
24657 "expected SELECT inside ANY/ALL, got {other:?}"
24658 ))),
24659 }
24660 }
24661 }
24662
24663 fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
24664 if !matches!(self.peek(), Token::LParen) {
24665 return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
24666 }
24667 self.advance();
24668 // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
24669 let s = if self.peek_is_with_kw() {
24670 self.advance();
24671 self.parse_nested_with_select()?
24672 } else {
24673 let inner = self.parse_select_stmt()?;
24674 let Statement::Select(s) = inner else {
24675 unreachable!("parse_select_stmt returns Select")
24676 };
24677 s
24678 };
24679 if !matches!(self.peek(), Token::RParen) {
24680 return Err(self.err(format!(
24681 "expected ')' after EXISTS-subquery, got {:?}",
24682 self.peek()
24683 )));
24684 }
24685 self.advance();
24686 Ok(Expr::Exists {
24687 subquery: Box::new(s),
24688 negated,
24689 })
24690 }
24691
24692 fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
24693 self.advance(); // IN
24694 if !matches!(self.peek(), Token::LParen) {
24695 return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
24696 }
24697 self.advance();
24698 // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
24699 // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
24700 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
24701 let s = if self.peek_is_with_kw() {
24702 self.advance();
24703 self.parse_nested_with_select()?
24704 } else {
24705 let inner = self.parse_select_stmt()?;
24706 let Statement::Select(s) = inner else {
24707 unreachable!("parse_select_stmt always returns Statement::Select")
24708 };
24709 s
24710 };
24711 if !matches!(self.peek(), Token::RParen) {
24712 return Err(self.err(format!(
24713 "expected ')' after IN-subquery, got {:?}",
24714 self.peek()
24715 )));
24716 }
24717 self.advance();
24718 return Ok(Expr::InSubquery {
24719 expr: Box::new(expr),
24720 subquery: Box::new(s),
24721 negated,
24722 });
24723 }
24724 let mut elements = Vec::new();
24725 if !matches!(self.peek(), Token::RParen) {
24726 loop {
24727 elements.push(self.parse_expr(0)?);
24728 match self.peek() {
24729 Token::Comma => {
24730 self.advance();
24731 }
24732 Token::RParen => break,
24733 other => {
24734 return Err(
24735 self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
24736 );
24737 }
24738 }
24739 }
24740 }
24741 self.advance(); // ')'
24742 // v7.30.2 (mailrs round-25) — flat InList node instead of a
24743 // left-deep OR-Eq chain: chain depth scaled with the element
24744 // count and overflowed the stack (eval + drop are recursive).
24745 if elements.is_empty() {
24746 return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
24747 }
24748 Ok(Expr::InList {
24749 expr: Box::new(expr),
24750 list: elements,
24751 negated,
24752 })
24753 }
24754
24755 /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
24756 /// already consumed by the caller. Elements must be numeric literals
24757 /// (with optional unary `-`); any compound expression is rejected at
24758 /// parse time so the runtime never needs to evaluate inside a vector.
24759 /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
24760 /// has already consumed the `EXTRACT` token before calling us —
24761 /// we pick up at the opening `(`.
24762 /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
24763 /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
24764 /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
24765 /// per-column OR-fold of
24766 /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
24767 /// term)` so the existing FTS evaluator handles semantics.
24768 ///
24769 /// The mode modifier is accepted-and-ignored at v7.17 — all
24770 /// modes map to the same `plainto_tsquery` rewrite. Boolean-
24771 /// mode operators (`+foo -bar`) would need their own parser
24772 /// (Phase 2.2c); customers who hit them today already get a
24773 /// correct lexeme-match against the bare term, only without
24774 /// the +/- precedence the customer asked for.
24775 fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
24776 // Already at `MATCH`-consumed position; the dispatcher
24777 // confirmed the next token is `(`.
24778 if !matches!(self.peek(), Token::LParen) {
24779 return Err(self.err(alloc::format!(
24780 "expected '(' after MATCH, got {:?}",
24781 self.peek()
24782 )));
24783 }
24784 self.advance();
24785 let mut cols: Vec<Expr> = Vec::new();
24786 loop {
24787 cols.push(self.parse_expr(0)?);
24788 match self.peek() {
24789 Token::Comma => {
24790 self.advance();
24791 }
24792 Token::RParen => break,
24793 other => {
24794 return Err(self.err(alloc::format!(
24795 "expected ',' or ')' in MATCH column list, got {other:?}"
24796 )));
24797 }
24798 }
24799 }
24800 self.advance(); // ')'
24801 // Expect AGAINST.
24802 match self.peek() {
24803 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
24804 self.advance();
24805 }
24806 other => {
24807 return Err(self.err(alloc::format!(
24808 "expected AGAINST after MATCH column list, got {other:?}"
24809 )));
24810 }
24811 }
24812 if !matches!(self.peek(), Token::LParen) {
24813 return Err(self.err(alloc::format!(
24814 "expected '(' after AGAINST, got {:?}",
24815 self.peek()
24816 )));
24817 }
24818 self.advance();
24819 // Read AGAINST's argument as a single primary token —
24820 // string literal, placeholder, or column-ref ident. We
24821 // can't call `parse_expr` / `parse_unary` here because
24822 // the postfix chain inside `parse_atom` would greedily
24823 // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
24824 // and fail at "expected '(' after IN". Customers always
24825 // write a literal or bound parameter in AGAINST, so this
24826 // restriction is non-blocking; the error path explains
24827 // the limit if a more complex expression shows up.
24828 let term = match self.advance() {
24829 Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
24830 Token::Placeholder(n) => Expr::Placeholder(n),
24831 Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
24832 qualifier: None,
24833 name: s,
24834 }),
24835 other => {
24836 return Err(self.err(alloc::format!(
24837 "MATCH ... AGAINST(<term>) expects a string literal, \
24838 bound parameter, or column ref, got {other:?}"
24839 )));
24840 }
24841 };
24842 // Optional mode tail — accept-and-ignore at v7.17:
24843 // IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
24844 // IN BOOLEAN MODE
24845 // WITH QUERY EXPANSION
24846 loop {
24847 match self.peek() {
24848 // IN lexes as a reserved Token::In, not an ident,
24849 // so it gets its own arm.
24850 Token::In => {
24851 self.advance();
24852 }
24853 Token::Ident(s) | Token::QuotedIdent(s)
24854 if s.eq_ignore_ascii_case("natural")
24855 || s.eq_ignore_ascii_case("language")
24856 || s.eq_ignore_ascii_case("boolean")
24857 || s.eq_ignore_ascii_case("mode")
24858 || s.eq_ignore_ascii_case("with")
24859 || s.eq_ignore_ascii_case("query")
24860 || s.eq_ignore_ascii_case("expansion") =>
24861 {
24862 self.advance();
24863 }
24864 _ => break,
24865 }
24866 }
24867 if !matches!(self.peek(), Token::RParen) {
24868 return Err(self.err(alloc::format!(
24869 "expected ')' to close AGAINST, got {:?}",
24870 self.peek()
24871 )));
24872 }
24873 self.advance();
24874 // Build per-column `to_tsvector('simple', col) @@
24875 // plainto_tsquery('simple', term)` and OR-fold.
24876 let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24877 let plainto = Expr::FunctionCall {
24878 name: String::from("plainto_tsquery"),
24879 args: alloc::vec![simple_lit(), term.clone()],
24880 };
24881 let mut folded: Option<Expr> = None;
24882 for col in cols {
24883 let to_tsv = Expr::FunctionCall {
24884 name: String::from("to_tsvector"),
24885 args: alloc::vec![simple_lit(), col],
24886 };
24887 let leaf = Expr::Binary {
24888 lhs: Box::new(to_tsv),
24889 op: crate::ast::BinOp::TsMatch,
24890 rhs: Box::new(plainto.clone()),
24891 };
24892 folded = Some(match folded {
24893 None => leaf,
24894 Some(prev) => Expr::Binary {
24895 lhs: Box::new(prev),
24896 op: crate::ast::BinOp::Or,
24897 rhs: Box::new(leaf),
24898 },
24899 });
24900 }
24901 match folded {
24902 Some(e) => Ok(e),
24903 None => Err(self.err(String::from(
24904 "MATCH(...) AGAINST(...) requires at least one column",
24905 ))),
24906 }
24907 }
24908
24909 fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24910 if !matches!(self.peek(), Token::LParen) {
24911 return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24912 }
24913 self.advance();
24914 let field_name = self.expect_ident_like()?;
24915 let field = match field_name.to_ascii_lowercase().as_str() {
24916 // PG accepts the plural spellings (years/months/…/millenniums) as
24917 // aliases for the singular fields — its datetime unit table has both.
24918 // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24919 "year" | "years" => ExtractField::Year,
24920 "month" | "months" => ExtractField::Month,
24921 "day" | "days" => ExtractField::Day,
24922 "hour" | "hours" => ExtractField::Hour,
24923 "minute" | "minutes" => ExtractField::Minute,
24924 "second" | "seconds" => ExtractField::Second,
24925 "microsecond" | "microseconds" => ExtractField::Microsecond,
24926 "epoch" => ExtractField::Epoch,
24927 "dow" => ExtractField::Dow,
24928 "isodow" => ExtractField::Isodow,
24929 "doy" => ExtractField::Doy,
24930 "week" | "weeks" => ExtractField::Week,
24931 "isoyear" => ExtractField::Isoyear,
24932 "quarter" => ExtractField::Quarter,
24933 "decade" | "decades" => ExtractField::Decade,
24934 "century" | "centuries" => ExtractField::Century,
24935 "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24936 "julian" => ExtractField::Julian,
24937 "millisecond" | "milliseconds" => ExtractField::Millisecond,
24938 "timezone" => ExtractField::Timezone,
24939 "timezone_hour" => ExtractField::TimezoneHour,
24940 "timezone_minute" => ExtractField::TimezoneMinute,
24941 // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24942 // reports an unknown one with the source type (22023); carry the
24943 // raw name so eval can word it.
24944 other => ExtractField::Other(alloc::string::String::from(other)),
24945 };
24946 if !matches!(self.peek(), Token::From) {
24947 return Err(self.err(format!(
24948 "expected FROM after EXTRACT field, got {:?}",
24949 self.peek()
24950 )));
24951 }
24952 self.advance();
24953 let source = self.parse_expr(0)?;
24954 if !matches!(self.peek(), Token::RParen) {
24955 return Err(self.err(format!(
24956 "expected ')' to close EXTRACT, got {:?}",
24957 self.peek()
24958 )));
24959 }
24960 self.advance();
24961 Ok(Expr::Extract {
24962 field,
24963 source: Box::new(source),
24964 })
24965 }
24966
24967 /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24968 /// is already consumed; we expect a single string literal next and
24969 /// resolve it into `Literal::Interval` at parse time so the engine
24970 /// never has to re-tokenise inside the string.
24971 /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24972 /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24973 /// is the SQL-standard form and is left to the path below.
24974 fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24975 // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24976 let (offset, sign) = match self.peek() {
24977 Token::Minus => (1, "-"),
24978 _ => (0, ""),
24979 };
24980 let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24981 return None;
24982 };
24983 self.tokens
24984 .get(self.pos + offset + 1)
24985 .filter(|t| mysql_interval_unit(t).is_some())?;
24986 Some((alloc::format!("{sign}{n}"), offset + 1))
24987 }
24988
24989 /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24990 /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24991 /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24992 ///
24993 /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24994 /// this by parsing the group and then restoring `self.pos` — which could
24995 /// never have worked, because `advance()` DESTROYS the token it returns
24996 /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24997 /// inert only because both branches errored back then.
24998 fn interval_paren_is_quantity(&self) -> bool {
24999 let mut depth = 0usize;
25000 let mut saw_top_level_comma = false;
25001 let mut i = self.pos;
25002 while let Some(tok) = self.tokens.get(i) {
25003 match tok {
25004 Token::LParen => depth += 1,
25005 Token::RParen => {
25006 depth = depth.saturating_sub(1);
25007 if depth == 0 {
25008 return !saw_top_level_comma
25009 && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
25010 .is_some();
25011 }
25012 }
25013 // A comma directly inside the outermost parens means the
25014 // argument list of the INTERVAL() function.
25015 Token::Comma if depth == 1 => saw_top_level_comma = true,
25016 Token::Eof => return false,
25017 _ => {}
25018 }
25019 i += 1;
25020 }
25021 false
25022 }
25023
25024 fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
25025 // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
25026 // (the index of the last Ni ≤ N), distinct from the interval literal.
25027 // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
25028 // is decided by a non-destructive lookahead (round 422) before either
25029 // branch consumes anything. MySQL only.
25030 if self.mysql_dialect
25031 && matches!(self.peek(), Token::LParen)
25032 && !self.interval_paren_is_quantity()
25033 {
25034 self.advance(); // (
25035 let mut args = Vec::new();
25036 if !matches!(self.peek(), Token::RParen) {
25037 loop {
25038 args.push(self.parse_expr(0)?);
25039 if matches!(self.peek(), Token::Comma) {
25040 self.advance();
25041 continue;
25042 }
25043 break;
25044 }
25045 }
25046 if !matches!(self.peek(), Token::RParen) {
25047 return Err(self.err(alloc::format!(
25048 "expected ')' after INTERVAL() arguments, got {:?}",
25049 self.peek()
25050 )));
25051 }
25052 self.advance(); // )
25053 return Ok(Expr::FunctionCall {
25054 name: alloc::string::String::from("interval"),
25055 args,
25056 });
25057 }
25058 // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
25059 // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
25060 // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
25061 // writes every date arithmetic there is, and it did not parse at
25062 // all. PG rejects the unquoted form outright (`syntax error at or
25063 // near "1"`, measured), so it is taken only in the MySQL dialect —
25064 // PG's own `INTERVAL '1' DAY` is untouched below.
25065 if self.mysql_dialect
25066 && let Some((text, consume)) = self.peek_unquoted_interval_count()
25067 {
25068 for _ in 0..consume {
25069 self.advance(); // the optional `-` and the number
25070 }
25071 let Some(unit) = mysql_interval_unit(self.peek()) else {
25072 return Err(self.err(alloc::format!(
25073 "expected an interval unit after INTERVAL {text}, got {:?}",
25074 self.peek()
25075 )));
25076 };
25077 self.advance(); // the unit
25078 let (months, days, micros) = scale_mysql_interval(&text, unit)
25079 .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
25080 return Ok(Expr::Literal(Literal::Interval {
25081 months,
25082 days,
25083 micros,
25084 // The canonical rendering, so Display round-trips into a
25085 // form both dialects read back.
25086 text: alloc::format!("{text} {unit}"),
25087 }));
25088 }
25089 // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
25090 // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
25091 // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
25092 // Those cannot fold into a compile-time `Literal::Interval`, so they
25093 // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
25094 // builtin, which builds the value at run time (and yields NULL for a
25095 // NULL quantity, as MariaDB does). The literal path above still folds
25096 // the constant case — it is cheaper and round-trips through Display.
25097 //
25098 // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
25099 // MySQL's quoted spelling) keep the qualifier path below.
25100 if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
25101 let qty = self.parse_expr(0)?;
25102 let Some(unit) = mysql_interval_unit(self.peek()) else {
25103 return Err(self.err(alloc::format!(
25104 "expected an interval unit after INTERVAL <expr>, got {:?}",
25105 self.peek()
25106 )));
25107 };
25108 self.advance(); // the unit
25109 return Ok(make_interval_call(qty, unit));
25110 }
25111 let tok = self.advance();
25112 let Token::String(text) = tok else {
25113 return Err(self.err(format!(
25114 "expected string literal after INTERVAL, got {tok:?}"
25115 )));
25116 };
25117 // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
25118 // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
25119 // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
25120 // bare number means and the leading/trailing precision.
25121 let field1 = interval_field_of(self.peek());
25122 let qualifier = if let Some(f1) = field1 {
25123 self.advance();
25124 let f2 = if matches!(self.peek(), Token::To) {
25125 self.advance();
25126 let Some(f) = interval_field_of(self.peek()) else {
25127 return Err(self.err(format!(
25128 "expected an interval field after TO, got {:?}",
25129 self.peek()
25130 )));
25131 };
25132 self.advance();
25133 Some(f)
25134 } else {
25135 None
25136 };
25137 Some((f1, f2))
25138 } else {
25139 None
25140 };
25141 let (months, days, micros) = match qualifier {
25142 Some(q) => interpret_qualified_interval(&text, q),
25143 None => parse_interval_text(&text),
25144 }
25145 .ok_or_else(|| ParseError {
25146 message: format!(
25147 "cannot parse INTERVAL {text:?}; \
25148 expected `<n> <unit> [<n> <unit> ...]` with units \
25149 microsecond[s], millisecond[s], second[s], minute[s], \
25150 hour[s], day[s], week[s], month[s], year[s]"
25151 ),
25152 token_pos: self.consumed_pos(),
25153 })?;
25154 Ok(Expr::Literal(Literal::Interval {
25155 months,
25156 days,
25157 micros,
25158 text,
25159 }))
25160 }
25161
25162 /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
25163 /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
25164 /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
25165 /// than a pgvector literal.
25166 fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
25167 self.advance(); // consume `[`
25168 let mut items: Vec<Expr> = Vec::new();
25169 if !matches!(self.peek(), Token::RBracket) {
25170 loop {
25171 if matches!(self.peek(), Token::LBracket) {
25172 items.push(self.parse_array_bracket_body()?);
25173 } else {
25174 items.push(self.parse_expr(0)?);
25175 }
25176 match self.peek() {
25177 Token::Comma => {
25178 self.advance();
25179 }
25180 Token::RBracket => break,
25181 other => {
25182 return Err(self.err(alloc::format!(
25183 "expected ',' or ']' in array literal, got {other:?}"
25184 )));
25185 }
25186 }
25187 }
25188 }
25189 self.advance(); // consume `]`
25190 Ok(Expr::Array(items))
25191 }
25192
25193 fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
25194 let mut elems = Vec::new();
25195 if matches!(self.peek(), Token::RBracket) {
25196 self.advance();
25197 return Ok(Expr::Literal(Literal::Vector(elems)));
25198 }
25199 loop {
25200 let e = self.parse_expr(0)?;
25201 let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
25202 message: format!("vector element must be a numeric literal, got {e:?}"),
25203 token_pos: self.pos,
25204 })?;
25205 elems.push(x);
25206 match self.peek() {
25207 Token::Comma => {
25208 self.advance();
25209 }
25210 Token::RBracket => {
25211 self.advance();
25212 break;
25213 }
25214 other => {
25215 return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
25216 }
25217 }
25218 }
25219 Ok(Expr::Literal(Literal::Vector(elems)))
25220 }
25221
25222 /// Atom that started with an identifier: could be `t.col`, `col`, or
25223 /// `func(arg, ...)`. Detect each shape by looking at the next token.
25224 /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
25225 /// [, ...])`. Caller has already consumed `OVER`. Either clause
25226 /// is optional; an empty `()` is also legal (PG semantics).
25227 /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
25228 /// modifier between `name(args)` and `OVER (...)`. Default is
25229 /// `Respect`. Unrecognised idents leave the stream unchanged.
25230 fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
25231 let Token::Ident(s) = self.peek().clone() else {
25232 return NullTreatment::Respect;
25233 };
25234 let is_ignore = s.eq_ignore_ascii_case("ignore");
25235 let is_respect = s.eq_ignore_ascii_case("respect");
25236 if !is_ignore && !is_respect {
25237 return NullTreatment::Respect;
25238 }
25239 // Lookahead for NULLS — only consume both tokens together.
25240 // pos+1 must hold a "nulls" ident.
25241 if self.pos + 1 < self.tokens.len()
25242 && let Token::Ident(s2) = &self.tokens[self.pos + 1]
25243 && s2.eq_ignore_ascii_case("nulls")
25244 {
25245 self.advance();
25246 self.advance();
25247 return if is_ignore {
25248 NullTreatment::Ignore
25249 } else {
25250 NullTreatment::Respect
25251 };
25252 }
25253 NullTreatment::Respect
25254 }
25255
25256 /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
25257 /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
25258 /// (same shape as the `OVER` tail). Consumes the whole clause and
25259 /// returns the predicate; returns `None` when no `FILTER` follows.
25260 fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
25261 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25262 return Ok(None);
25263 };
25264 if !s.eq_ignore_ascii_case("filter") {
25265 return Ok(None);
25266 }
25267 self.advance(); // FILTER
25268 if !matches!(self.peek(), Token::LParen) {
25269 return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
25270 }
25271 self.advance(); // (
25272 if !matches!(self.peek(), Token::Where) {
25273 return Err(self.err(format!(
25274 "expected WHERE inside FILTER (...), got {:?}",
25275 self.peek()
25276 )));
25277 }
25278 self.advance(); // WHERE
25279 let cond = self.parse_expr(0)?;
25280 if !matches!(self.peek(), Token::RParen) {
25281 return Err(self.err(format!(
25282 "expected ')' to close FILTER (WHERE ...), got {:?}",
25283 self.peek()
25284 )));
25285 }
25286 self.advance(); // )
25287 Ok(Some(Box::new(cond)))
25288 }
25289
25290 /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
25291 /// the separator as the aggregate's second argument, which is the
25292 /// shape `string_agg` already takes. Returns whether one was there.
25293 fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
25294 if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
25295 return Ok(false);
25296 }
25297 self.advance();
25298 let Token::String(sep) = self.peek().clone() else {
25299 return Err(self.err(alloc::format!(
25300 "expected a string literal after SEPARATOR, got {:?}",
25301 self.peek()
25302 )));
25303 };
25304 self.advance();
25305 args.push(Expr::Literal(Literal::String(sep)));
25306 Ok(true)
25307 }
25308
25309 /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
25310 /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
25311 /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
25312 /// keys, or an empty vec when no `WITHIN GROUP` follows.
25313 fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
25314 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25315 return Ok(Vec::new());
25316 };
25317 if !s.eq_ignore_ascii_case("within") {
25318 return Ok(Vec::new());
25319 }
25320 self.advance(); // WITHIN
25321 if !matches!(self.peek(), Token::Group) {
25322 return Err(self.err(format!(
25323 "expected GROUP after WITHIN, got {:?}",
25324 self.peek()
25325 )));
25326 }
25327 self.advance(); // GROUP
25328 if !matches!(self.peek(), Token::LParen) {
25329 return Err(self.err(format!(
25330 "expected '(' after WITHIN GROUP, got {:?}",
25331 self.peek()
25332 )));
25333 }
25334 self.advance(); // (
25335 if !matches!(self.peek(), Token::Order) {
25336 return Err(self.err(format!(
25337 "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
25338 self.peek()
25339 )));
25340 }
25341 self.advance(); // ORDER
25342 if !self.peek_is_by() {
25343 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25344 }
25345 self.advance(); // BY
25346 let mut keys: Vec<OrderBy> = Vec::new();
25347 loop {
25348 // v7.39 (round 691) — save/restore, the discipline this parser
25349 // already uses around `pending_sample_preds`, so a subquery inside
25350 // a key neither inherits nor leaks the channel.
25351 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25352 let saved_coll = self.order_key_collation.take();
25353 let parsed = self.parse_expr(0);
25354 self.in_order_by_key = saved_flag;
25355 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
25356 let expr = parsed?;
25357 let desc = if matches!(self.peek(), Token::Desc) {
25358 self.advance();
25359 true
25360 } else if matches!(self.peek(), Token::Asc) {
25361 self.advance();
25362 false
25363 } else {
25364 false
25365 };
25366 let nulls_first = self.parse_optional_nulls_placement()?;
25367 keys.push(OrderBy {
25368 expr,
25369 desc,
25370 nulls_first,
25371 collation,
25372 });
25373 if matches!(self.peek(), Token::Comma) {
25374 self.advance();
25375 } else {
25376 break;
25377 }
25378 }
25379 if !matches!(self.peek(), Token::RParen) {
25380 return Err(self.err(format!(
25381 "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
25382 self.peek()
25383 )));
25384 }
25385 self.advance(); // )
25386 Ok(keys)
25387 }
25388
25389 /// No frame clause is supported.
25390 #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
25391 fn parse_over_clause(
25392 &mut self,
25393 ) -> Result<
25394 (
25395 Vec<Expr>,
25396 Vec<(Expr, bool, Option<bool>)>,
25397 Option<WindowFrame>,
25398 ),
25399 ParseError,
25400 > {
25401 // `OVER w` — a named-window reference. The WINDOW clause
25402 // parses after the select list, so the name rides out as a
25403 // marker in partition_by; parse_bare_select substitutes the
25404 // definition once the clause is known.
25405 if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
25406 let name = w.clone();
25407 self.advance();
25408 return Ok((
25409 alloc::vec![Expr::Column(crate::ast::ColumnName {
25410 qualifier: Some("__named_window__".to_string()),
25411 name,
25412 })],
25413 Vec::new(),
25414 None,
25415 ));
25416 }
25417 if !matches!(self.peek(), Token::LParen) {
25418 return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
25419 }
25420 self.advance();
25421 let mut partition_by = Vec::new();
25422 let mut order_by = Vec::new();
25423 // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
25424 // window, refined in place. PG's rules (probed against 18.4) differ
25425 // from the bare `OVER w1` form, so the reference rides out under its
25426 // own marker and `substitute_named_windows` applies them. The base
25427 // name is any leading identifier that isn't a window-spec keyword.
25428 let base_window = match self.peek() {
25429 Token::Ident(s) | Token::QuotedIdent(s)
25430 if !s.eq_ignore_ascii_case("partition")
25431 && !s.eq_ignore_ascii_case("rows")
25432 && !s.eq_ignore_ascii_case("range")
25433 && !s.eq_ignore_ascii_case("groups") =>
25434 {
25435 let n = s.clone();
25436 self.advance();
25437 Some(n)
25438 }
25439 _ => None,
25440 };
25441 // PARTITION BY ?
25442 // v7.37.6-B promoted PARTITION to a reserved keyword
25443 // (Token::Partition); pre-7.37.6-B catalogs lexed it as
25444 // `Token::Ident("partition")`. Accept both so older sources
25445 // and the new lexer surface land on the same path.
25446 let is_partition_kw = match self.peek() {
25447 Token::Partition => true,
25448 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
25449 _ => false,
25450 };
25451 if is_partition_kw {
25452 self.advance();
25453 if !self.peek_is_by() {
25454 return Err(self.err(format!(
25455 "expected BY after PARTITION, got {:?}",
25456 self.peek()
25457 )));
25458 }
25459 self.advance();
25460 loop {
25461 partition_by.push(self.parse_expr(0)?);
25462 if matches!(self.peek(), Token::Comma) {
25463 self.advance();
25464 continue;
25465 }
25466 break;
25467 }
25468 }
25469 // ORDER BY ?
25470 if matches!(self.peek(), Token::Order) {
25471 self.advance();
25472 if !self.peek_is_by() {
25473 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25474 }
25475 self.advance();
25476 loop {
25477 let e = self.parse_expr(0)?;
25478 let desc = if matches!(self.peek(), Token::Desc) {
25479 self.advance();
25480 true
25481 } else if matches!(self.peek(), Token::Asc) {
25482 self.advance();
25483 false
25484 } else {
25485 false
25486 };
25487 // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
25488 let nulls_first = self.parse_optional_nulls_placement()?;
25489 order_by.push((e, desc, nulls_first));
25490 if matches!(self.peek(), Token::Comma) {
25491 self.advance();
25492 continue;
25493 }
25494 break;
25495 }
25496 }
25497 // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
25498 // Both keywords come through the lexer as identifiers; match
25499 // case-insensitively.
25500 let mut frame: Option<WindowFrame> = None;
25501 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
25502 let kind = if s.eq_ignore_ascii_case("rows") {
25503 Some(FrameKind::Rows)
25504 } else if s.eq_ignore_ascii_case("range") {
25505 Some(FrameKind::Range)
25506 } else if s.eq_ignore_ascii_case("groups") {
25507 // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
25508 Some(FrameKind::Groups)
25509 } else {
25510 None
25511 };
25512 if let Some(kind) = kind {
25513 self.advance();
25514 frame = Some(self.parse_frame_tail(kind)?);
25515 }
25516 }
25517 if !matches!(self.peek(), Token::RParen) {
25518 return Err(self.err(format!(
25519 "expected ')' to close OVER clause, got {:?}",
25520 self.peek()
25521 )));
25522 }
25523 self.advance();
25524 if let Some(base) = base_window {
25525 // A copy may refine but never override the base's partitioning
25526 // (PG rejects it outright, before looking the name up).
25527 if !partition_by.is_empty() {
25528 return Err(self.err(alloc::format!(
25529 "cannot override PARTITION BY clause of window \"{base}\""
25530 )));
25531 }
25532 partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
25533 qualifier: Some("__named_window_ref__".to_string()),
25534 name: base,
25535 })];
25536 }
25537 Ok((partition_by, order_by, frame))
25538 }
25539
25540 /// v4.20: parse the tail of an explicit frame, given the `ROWS`
25541 /// or `RANGE` keyword was just consumed. Accepts both
25542 /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
25543 /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
25544 /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
25545 fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
25546 let (start, end) = if matches!(self.peek(), Token::Between) {
25547 self.advance();
25548 let start = self.parse_frame_bound()?;
25549 if !matches!(self.peek(), Token::And) {
25550 return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
25551 }
25552 self.advance();
25553 let end = self.parse_frame_bound()?;
25554 (start, Some(end))
25555 } else {
25556 (self.parse_frame_bound()?, None)
25557 };
25558 let exclude = self.parse_frame_exclusion()?;
25559 Ok(WindowFrame {
25560 kind,
25561 start,
25562 end,
25563 exclude,
25564 })
25565 }
25566
25567 /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
25568 /// after a frame spec. NO OTHERS is the default no-op.
25569 fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
25570 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
25571 return Ok(FrameExclusion::NoOthers);
25572 }
25573 self.advance(); // EXCLUDE
25574 match self.peek() {
25575 Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
25576 self.advance();
25577 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
25578 return Err(self.err(format!(
25579 "expected ROW after EXCLUDE CURRENT, got {:?}",
25580 self.peek()
25581 )));
25582 }
25583 self.advance();
25584 Ok(FrameExclusion::CurrentRow)
25585 }
25586 // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
25587 // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
25588 // Without this arm it fell to the catch-all, whose message
25589 // self-contradictingly listed GROUP as expected.
25590 Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
25591 self.advance();
25592 Ok(FrameExclusion::Group)
25593 }
25594 Token::Group => {
25595 self.advance();
25596 Ok(FrameExclusion::Group)
25597 }
25598 Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
25599 self.advance();
25600 Ok(FrameExclusion::Ties)
25601 }
25602 Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
25603 self.advance();
25604 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
25605 return Err(self.err(format!(
25606 "expected OTHERS after EXCLUDE NO, got {:?}",
25607 self.peek()
25608 )));
25609 }
25610 self.advance();
25611 Ok(FrameExclusion::NoOthers)
25612 }
25613 other => Err(self.err(format!(
25614 "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
25615 ))),
25616 }
25617 }
25618
25619 /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
25620 /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
25621 /// `UNBOUNDED FOLLOWING`.
25622 fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
25623 // Interval-typed offset for a value-based RANGE frame over a
25624 // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
25625 // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
25626 // PRECEDING`.
25627 if let Some((months, days, micros)) = self.try_take_interval_offset()? {
25628 let dir = self.expect_ident_like()?;
25629 return if dir.eq_ignore_ascii_case("preceding") {
25630 Ok(FrameBound::IntervalPreceding {
25631 months,
25632 days,
25633 micros,
25634 })
25635 } else if dir.eq_ignore_ascii_case("following") {
25636 Ok(FrameBound::IntervalFollowing {
25637 months,
25638 days,
25639 micros,
25640 })
25641 } else {
25642 Err(self.err(format!(
25643 "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
25644 )))
25645 };
25646 }
25647 // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
25648 if let Token::Integer(n) = *self.peek() {
25649 self.advance();
25650 let n: u64 = u64::try_from(n).map_err(|_| {
25651 self.err(format!(
25652 "invalid frame offset {n} — expected non-negative integer"
25653 ))
25654 })?;
25655 let dir = self.expect_ident_like()?;
25656 return if dir.eq_ignore_ascii_case("preceding") {
25657 Ok(FrameBound::OffsetPreceding(n))
25658 } else if dir.eq_ignore_ascii_case("following") {
25659 Ok(FrameBound::OffsetFollowing(n))
25660 } else {
25661 Err(self.err(format!(
25662 "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
25663 )))
25664 };
25665 }
25666 let first = self.expect_ident_like()?;
25667 if first.eq_ignore_ascii_case("unbounded") {
25668 let dir = self.expect_ident_like()?;
25669 return if dir.eq_ignore_ascii_case("preceding") {
25670 Ok(FrameBound::UnboundedPreceding)
25671 } else if dir.eq_ignore_ascii_case("following") {
25672 Ok(FrameBound::UnboundedFollowing)
25673 } else {
25674 Err(self.err(format!(
25675 "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
25676 )))
25677 };
25678 }
25679 if first.eq_ignore_ascii_case("current") {
25680 let row = self.expect_ident_like()?;
25681 if !row.eq_ignore_ascii_case("row") {
25682 return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
25683 }
25684 return Ok(FrameBound::CurrentRow);
25685 }
25686 Err(self.err(format!(
25687 "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
25688 )))
25689 }
25690
25691 /// Detect and consume a leading interval offset in a frame bound —
25692 /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
25693 /// `(months, days, micros)`. Leaves the cursor on the trailing
25694 /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
25695 /// when the next tokens are not an interval offset.
25696 fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
25697 // Shape A — `INTERVAL '1 day'`.
25698 if matches!(self.peek(), Token::Interval) {
25699 self.advance(); // INTERVAL
25700 let atom = self.parse_interval_atom()?;
25701 if let Expr::Literal(Literal::Interval {
25702 months,
25703 days,
25704 micros,
25705 ..
25706 }) = atom
25707 {
25708 return Ok(Some((months, days, micros)));
25709 }
25710 return Err(self.err("expected an interval literal in frame offset".to_string()));
25711 }
25712 // Shape B — `'1 day'::interval`. Look ahead for the exact
25713 // string / `::` / interval-target triple before committing.
25714 if let Token::String(text) = self.peek() {
25715 let target_is_interval = match self.tokens.get(self.pos + 2) {
25716 Some(Token::Interval) => true,
25717 Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
25718 _ => false,
25719 };
25720 let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
25721 && target_is_interval;
25722 if is_cast {
25723 let text = text.clone();
25724 self.advance(); // string
25725 self.advance(); // ::
25726 self.advance(); // interval
25727 let parts = parse_interval_text(&text).ok_or_else(|| {
25728 self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
25729 })?;
25730 return Ok(Some(parts));
25731 }
25732 }
25733 Ok(None)
25734 }
25735
25736 fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
25737 // v7.39.2 — MySQL's charset INTRODUCER: `_utf8mb4'x'`, `N'y'`,
25738 // `_binary'z'`. All three were `ERROR 1064 syntax error` here
25739 // and all three answer the literal on MySQL 9.7.2.
25740 //
25741 // It is not only syntax, which is why it waited for
25742 // `Expr::Collate`: measured, `_binary'A' = 'a'` is 0 on MySQL
25743 // because `_binary` makes the comparison byte-wise, while
25744 // `_utf8mb4'A' = _utf8mb4'a'` is 1. Accepting the syntax and
25745 // dropping the charset would have turned a hard error into a
25746 // silently wrong comparison — worse than the error it replaced.
25747 //
25748 // An UNKNOWN charset is NOT an introducer: MySQL answers
25749 // `Unknown column '_nosuch'`, because it parses as a column
25750 // reference followed by a string. So the table decides, and it
25751 // is the same table `SET NAMES` reads.
25752 //
25753 // A space is allowed between the two (`_utf8mb4 'x'`), which
25754 // falls out of asking the token stream rather than the bytes.
25755 if self.mysql_dialect
25756 && let Token::String(_) = self.peek()
25757 {
25758 let lower = first.to_ascii_lowercase();
25759 let charset = if lower == "n" {
25760 // `N'…'` is the national character set, which MySQL
25761 // documents as utf8 — utf8mb3 in 9.7.2's spelling.
25762 //
25763 // utf8mb3 and utf8mb4 both fold case in their default
25764 // collations, so nothing SPG can be asked distinguishes
25765 // the two here: an ablation swapping this to utf8mb4
25766 // reddens no pin. Recorded rather than implied — the
25767 // spelling follows MySQL's documentation, not a
25768 // measurement.
25769 Some("utf8mb3")
25770 } else {
25771 // No filter here: the lookup below IS the test for
25772 // "is this a charset". An ablation that removed a filter
25773 // in this spot reddened nothing, which is how the two
25774 // were found to be one check written twice.
25775 lower.strip_prefix('_')
25776 };
25777 if let Some(cs) = charset
25778 && let Some(collation) = crate::charset::charset_default_collation(cs)
25779 {
25780 let Token::String(body) = self.advance() else {
25781 unreachable!("peeked a string");
25782 };
25783 return Ok(Expr::Collate {
25784 expr: Box::new(Expr::Literal(Literal::String(body))),
25785 collation: String::from(collation),
25786 });
25787 }
25788 }
25789 if matches!(self.peek(), Token::Dot) {
25790 self.advance();
25791 let name = self.expect_ident_like()?;
25792 // v7.14.0 — schema-qualified function call
25793 // `<schema>.<fn>(args)`. PG dumps emit
25794 // `pg_catalog.set_config(...)` in the preamble. SPG
25795 // is single-namespace: drop the schema prefix and
25796 // route the dispatch on the bare function name.
25797 if matches!(self.peek(), Token::LParen) {
25798 return self.finish_ident_atom(name);
25799 }
25800 return Ok(Expr::Column(ColumnName {
25801 qualifier: Some(first),
25802 name,
25803 }));
25804 }
25805 if matches!(self.peek(), Token::LParen) {
25806 self.advance();
25807 // `COUNT(*)` — special-cased here because `*` isn't a normal
25808 // expression token. Lower-case match on `first` since the lexer
25809 // folds identifiers.
25810 if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
25811 self.advance();
25812 if !matches!(self.peek(), Token::RParen) {
25813 return Err(self.err(format!(
25814 "expected ')' after COUNT(*), got {:?}",
25815 self.peek()
25816 )));
25817 }
25818 self.advance();
25819 // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
25820 let filter = self.parse_filter_clause()?;
25821 // v4.12: COUNT(*) OVER (...) — same window tail.
25822 let null_treatment = self.parse_null_treatment_modifier();
25823 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25824 && s.eq_ignore_ascii_case("over")
25825 {
25826 self.advance();
25827 let (partition_by, order_by, frame) = self.parse_over_clause()?;
25828 return Ok(Expr::WindowFunction {
25829 name: "count_star".into(),
25830 args: Vec::new(),
25831 partition_by,
25832 order_by,
25833 frame,
25834 null_treatment,
25835 filter,
25836 });
25837 }
25838 if let Some(filter) = filter {
25839 return Ok(Expr::AggregateOrdered {
25840 call: Box::new(Expr::FunctionCall {
25841 name: "count_star".into(),
25842 args: Vec::new(),
25843 }),
25844 order_by: Vec::new(),
25845 distinct: false,
25846 filter: Some(filter),
25847 });
25848 }
25849 return Ok(Expr::FunctionCall {
25850 name: "count_star".into(),
25851 args: Vec::new(),
25852 });
25853 }
25854 // Function call. PG-style: zero-or-more comma-separated args.
25855 let mut args = Vec::new();
25856 // v7.38 (read01, T14) — named-argument notation `argname => value`.
25857 // Names are collected in lock-step with `args` and resolved to
25858 // positional order after the loop (the AST stays positional).
25859 let mut arg_names: Vec<Option<String>> = Vec::new();
25860 let mut agg_order_by: Vec<OrderBy> = Vec::new();
25861 // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
25862 // seen, so the value arguments before it can be folded.
25863 let mut saw_separator = false;
25864 // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
25865 // v7.32 (round-29) — accept the dual `ALL` quantifier too
25866 // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
25867 let agg_distinct = if matches!(self.peek(), Token::Distinct) {
25868 self.advance();
25869 true
25870 } else if matches!(self.peek(), Token::All) {
25871 self.advance();
25872 false
25873 } else {
25874 false
25875 };
25876 // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
25877 // TIMESTAMPDIFF take a bare unit keyword as the first
25878 // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
25879 // bare type keyword (DATE / TIME / DATETIME); lower them
25880 // onto string literals so the evaluator sees plain text.
25881 if ((first.eq_ignore_ascii_case("timestampadd")
25882 || first.eq_ignore_ascii_case("timestampdiff"))
25883 && matches!(self.peek(), Token::Ident(u) if matches!(
25884 u.to_ascii_lowercase().as_str(),
25885 "microsecond" | "second" | "minute" | "hour" | "day"
25886 | "week" | "month" | "quarter" | "year"
25887 )))
25888 || (first.eq_ignore_ascii_case("get_format")
25889 && matches!(self.peek(), Token::Ident(u) if matches!(
25890 u.to_ascii_lowercase().as_str(),
25891 "date" | "time" | "datetime" | "timestamp"
25892 )))
25893 {
25894 if let Token::Ident(u) = self.peek() {
25895 args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
25896 }
25897 self.advance();
25898 if matches!(self.peek(), Token::Comma) {
25899 self.advance();
25900 }
25901 }
25902 // `ROW(a, b, …)` keyword constructor. Followed by a
25903 // comparison operator or [NOT] IN it joins the paren
25904 // row-constructor machinery (fieldwise parse-time
25905 // expansion); bare, it stays a `row` call the evaluator
25906 // renders as PG record text.
25907 if first.eq_ignore_ascii_case("row") {
25908 let mut row_items = Vec::new();
25909 if !matches!(self.peek(), Token::RParen) {
25910 loop {
25911 row_items.push(self.parse_expr(0)?);
25912 match self.peek() {
25913 Token::Comma => {
25914 self.advance();
25915 }
25916 Token::RParen => break,
25917 other => {
25918 return Err(self.err(format!(
25919 "expected ',' or ')' in ROW(...), got {other:?}"
25920 )));
25921 }
25922 }
25923 }
25924 }
25925 self.advance(); // ')'
25926 let comparison_follows = matches!(
25927 self.peek(),
25928 Token::Eq
25929 | Token::NotEq
25930 | Token::Lt
25931 | Token::LtEq
25932 | Token::Gt
25933 | Token::GtEq
25934 | Token::In
25935 ) || (matches!(self.peek(), Token::Not)
25936 && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25937 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25938 if comparison_follows && !row_items.is_empty() {
25939 return self.parse_row_comparison_tail(row_items);
25940 }
25941 return Ok(Expr::FunctionCall {
25942 name: String::from("row"),
25943 args: row_items,
25944 });
25945 }
25946 // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25947 // the parse-mode keyword introduces the source text. SPG
25948 // carries XML as text, so both modes lower to __xmlparse(expr)
25949 // which validates well-formedness and returns Value::Xml.
25950 if first.eq_ignore_ascii_case("xmlparse")
25951 && matches!(self.peek(), Token::Ident(kw)
25952 if kw.eq_ignore_ascii_case("document")
25953 || kw.eq_ignore_ascii_case("content"))
25954 {
25955 let mode = match self.advance() {
25956 Token::Ident(kw) => kw.to_ascii_lowercase(),
25957 _ => unreachable!("peeked an ident"),
25958 };
25959 let src = self.parse_expr(0)?;
25960 if !matches!(self.peek(), Token::RParen) {
25961 return Err(self.err(format!(
25962 "expected ')' to close XMLPARSE, got {:?}",
25963 self.peek()
25964 )));
25965 }
25966 self.advance();
25967 return Ok(Expr::FunctionCall {
25968 name: String::from("__xmlparse"),
25969 args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25970 });
25971 }
25972 // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25973 // keyword introduces the element name (a bare or quoted
25974 // identifier), then optional content expressions. Lower to a
25975 // plain `xmlelement(name_text, content …)` call.
25976 if first.eq_ignore_ascii_case("xmlelement")
25977 && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25978 {
25979 self.advance(); // consume NAME
25980 let elem_name = match self.peek().clone() {
25981 Token::Ident(n) | Token::QuotedIdent(n) => {
25982 self.advance();
25983 n
25984 }
25985 other => {
25986 return Err(self.err(format!(
25987 "expected element name after XMLELEMENT NAME, got {other:?}"
25988 )));
25989 }
25990 };
25991 let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25992 while matches!(self.peek(), Token::Comma) {
25993 self.advance();
25994 args.push(self.parse_expr(0)?);
25995 }
25996 if !matches!(self.peek(), Token::RParen) {
25997 return Err(self.err(format!(
25998 "expected ')' to close XMLELEMENT, got {:?}",
25999 self.peek()
26000 )));
26001 }
26002 self.advance();
26003 return Ok(Expr::FunctionCall {
26004 name: String::from("xmlelement"),
26005 args,
26006 });
26007 }
26008 // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
26009 // becomes a `<name>value</name>` element; a bare column infers its
26010 // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
26011 // v7.39.2 — MySQL's two CONVERT forms, neither of which parsed.
26012 // `CONVERT(expr USING cs)` was a syntax error at USING, and
26013 // `CONVERT(expr, CHAR)` was read as PostgreSQL's three-argument
26014 // `convert(bytea, src, dest)` and answered `column "char" does
26015 // not exist`. Both are casts in MySQL: measured on 9.7.2,
26016 // `CONVERT(0x41 USING utf8mb4)` and `CONVERT(0x41, CHAR)` are
26017 // both 'A', and `CONVERT(123, CHAR)` is '123'.
26018 //
26019 // The charset is checked against the same table the introducers
26020 // use, so an unknown one is refused rather than quietly ignored.
26021 if self.mysql_dialect
26022 && first.eq_ignore_ascii_case("convert")
26023 && !matches!(self.peek(), Token::RParen)
26024 {
26025 let save = self.pos;
26026 let inner = self.parse_expr(0)?;
26027 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
26028 self.advance();
26029 let cs = match self.peek().clone() {
26030 Token::Ident(n) | Token::QuotedIdent(n) => {
26031 self.advance();
26032 n
26033 }
26034 other => {
26035 return Err(self.err(alloc::format!(
26036 "expected a charset after USING, got {other:?}"
26037 )));
26038 }
26039 };
26040 let lc = cs.to_ascii_lowercase();
26041 if lc != "binary" && crate::charset::charset_default_collation(&lc).is_none() {
26042 return Err(self.err(alloc::format!("unknown character set: '{cs}'")));
26043 }
26044 if !matches!(self.peek(), Token::RParen) {
26045 return Err(self.err(alloc::format!(
26046 "expected ')' after CONVERT … USING, got {:?}",
26047 self.peek()
26048 )));
26049 }
26050 self.advance();
26051 let target = if lc == "binary" {
26052 CastTarget::Named("binary".to_string())
26053 } else {
26054 CastTarget::Text
26055 };
26056 return self.finish_postfix_casts(Expr::Cast {
26057 expr: alloc::boxed::Box::new(inner),
26058 target,
26059 });
26060 }
26061 if matches!(self.peek(), Token::Comma) {
26062 self.advance();
26063 // A type name here is MySQL's cast form; anything else
26064 // (three string arguments) is PostgreSQL's `convert`,
26065 // which keeps its own path.
26066 if let Ok(target) = self.parse_cast_target()
26067 && matches!(self.peek(), Token::RParen)
26068 {
26069 self.advance();
26070 return self.finish_postfix_casts(Expr::Cast {
26071 expr: alloc::boxed::Box::new(inner),
26072 target,
26073 });
26074 }
26075 }
26076 self.pos = save;
26077 }
26078 if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
26079 let mut args: Vec<Expr> = Vec::new();
26080 loop {
26081 let val = self.parse_expr(0)?;
26082 let name = if matches!(self.peek(), Token::As) {
26083 self.advance();
26084 match self.peek().clone() {
26085 Token::Ident(n) | Token::QuotedIdent(n) => {
26086 self.advance();
26087 n
26088 }
26089 other => {
26090 return Err(self.err(format!(
26091 "expected name after AS in XMLFOREST, got {other:?}"
26092 )));
26093 }
26094 }
26095 } else if let Expr::Column(c) = &val {
26096 c.name.clone()
26097 } else {
26098 return Err(
26099 self.err("XMLFOREST element without a column name needs AS".into())
26100 );
26101 };
26102 args.push(Expr::Literal(Literal::String(name)));
26103 args.push(val);
26104 if matches!(self.peek(), Token::Comma) {
26105 self.advance();
26106 } else {
26107 break;
26108 }
26109 }
26110 if !matches!(self.peek(), Token::RParen) {
26111 return Err(self.err(format!(
26112 "expected ')' to close XMLFOREST, got {:?}",
26113 self.peek()
26114 )));
26115 }
26116 self.advance();
26117 return Ok(Expr::FunctionCall {
26118 name: String::from("xmlforest"),
26119 args,
26120 });
26121 }
26122 // SQL-standard `POSITION(sub IN str)` — lowers onto
26123 // strpos(str, sub). IN is the argument separator here,
26124 // so the needle parses with the IN-tail suppressed.
26125 if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
26126 let saved = self.suppress_in_tail;
26127 self.suppress_in_tail = true;
26128 let needle = self.parse_expr(0);
26129 self.suppress_in_tail = saved;
26130 let needle = needle?;
26131 if matches!(self.peek(), Token::In) {
26132 self.advance();
26133 let haystack = self.parse_expr(0)?;
26134 if !matches!(self.peek(), Token::RParen) {
26135 return Err(self.err(format!(
26136 "expected ')' to close POSITION, got {:?}",
26137 self.peek()
26138 )));
26139 }
26140 self.advance();
26141 return Ok(Expr::FunctionCall {
26142 name: String::from("strpos"),
26143 args: alloc::vec![haystack, needle],
26144 });
26145 }
26146 // position(sub, str) comma form (incl. bytea) —
26147 // hand the parsed first arg to the generic list.
26148 args.push(needle);
26149 if matches!(self.peek(), Token::Comma) {
26150 self.advance();
26151 }
26152 }
26153 // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
26154 // FROM str)` — lowers onto btrim / ltrim / rtrim. The
26155 // plain comma forms TRIM(str) / TRIM(str, chars) keep
26156 // riding the generic argument list below.
26157 if first.eq_ignore_ascii_case("trim") {
26158 let mode = match self.peek() {
26159 Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
26160 self.advance();
26161 Some("btrim")
26162 }
26163 Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
26164 self.advance();
26165 Some("ltrim")
26166 }
26167 Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
26168 self.advance();
26169 Some("rtrim")
26170 }
26171 _ => None,
26172 };
26173 if mode.is_some() || matches!(self.peek(), Token::From) {
26174 // TRIM([mode] FROM str) — no strip-chars.
26175 let chars = if matches!(self.peek(), Token::From) {
26176 None
26177 } else {
26178 Some(self.parse_expr(0)?)
26179 };
26180 if !matches!(self.peek(), Token::From) {
26181 return Err(self.err(format!(
26182 "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
26183 self.peek()
26184 )));
26185 }
26186 self.advance();
26187 let target = self.parse_expr(0)?;
26188 if !matches!(self.peek(), Token::RParen) {
26189 return Err(
26190 self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
26191 );
26192 }
26193 self.advance();
26194 let mut trim_args = alloc::vec![target];
26195 if let Some(c) = chars {
26196 trim_args.push(c);
26197 }
26198 return Ok(Expr::FunctionCall {
26199 name: String::from(mode.unwrap_or("btrim")),
26200 args: trim_args,
26201 });
26202 }
26203 }
26204 if !matches!(self.peek(), Token::RParen) {
26205 loop {
26206 // v7.38 (read01, T14) — `argname => value` names this arg.
26207 // v7.39 (read01 round 77) — `argname := value` is the same
26208 // thing, and it is the spelling PG's own docs lead with. It
26209 // was simply never lexed here, so every `f(x := 1)` died in
26210 // the parser regardless of what `f` was.
26211 let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
26212 (
26213 Token::Ident(n) | Token::QuotedIdent(n),
26214 Some(Token::FatArrow | Token::ColonEq),
26215 ) => {
26216 let name = n.clone();
26217 self.advance(); // name
26218 self.advance(); // => / :=
26219 Some(name)
26220 }
26221 _ => None,
26222 };
26223 // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
26224 // array's elements into a variadic call's trailing args
26225 // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
26226 // reserved, so it arrives as a bare ident before the arg.
26227 let is_variadic = this_name.is_none()
26228 && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
26229 if is_variadic {
26230 self.advance();
26231 }
26232 let arg = self.parse_expr(0)?;
26233 args.push(match &this_name {
26234 // The callee's parameter names decide the slot, and a
26235 // user function's live in the catalog. Carry the name
26236 // to eval rather than guessing here.
26237 Some(n) => Expr::NamedArg {
26238 name: n.clone(),
26239 expr: Box::new(arg),
26240 },
26241 None if is_variadic => Expr::Variadic(Box::new(arg)),
26242 None => arg,
26243 });
26244 arg_names.push(this_name);
26245 // v7.25 (round-17) — standard `CAST(expr AS type)`.
26246 // The `::` cast already worked; this lowers the
26247 // function form onto the same Expr::Cast node.
26248 if first.eq_ignore_ascii_case("cast")
26249 && args.len() == 1
26250 && matches!(self.peek(), Token::As)
26251 {
26252 self.advance();
26253 let target = self.parse_cast_target()?;
26254 if !matches!(self.peek(), Token::RParen) {
26255 return Err(self.err(format!(
26256 "expected ')' to close CAST, got {:?}",
26257 self.peek()
26258 )));
26259 }
26260 self.advance();
26261 return Ok(Expr::Cast {
26262 expr: Box::new(args.pop().expect("one arg")),
26263 target,
26264 });
26265 }
26266 // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
26267 // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
26268 // keywords; SPG's lexer makes them plain idents (so they'd be
26269 // read as column refs). Lower the keyword to the string form
26270 // the evaluator already accepts.
26271 if first.eq_ignore_ascii_case("normalize")
26272 && args.len() == 1
26273 && matches!(self.peek(), Token::Comma)
26274 {
26275 let form = match self.tokens.get(self.pos + 1) {
26276 Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
26277 let up = f.to_ascii_uppercase();
26278 matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
26279 }
26280 _ => None,
26281 };
26282 if let Some(up) = form {
26283 self.advance(); // comma
26284 self.advance(); // form keyword
26285 args.push(Expr::Literal(Literal::String(up)));
26286 }
26287 }
26288 // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
26289 // form. Desugars to the comma-list shape evaluator already
26290 // handles. Triggered after the first arg when the function
26291 // name is substring / substr and the next token is FROM
26292 // (a reserved keyword in PG; SPG also reserves it).
26293 // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
26294 // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
26295 // internal __substring_similar(str, pat, esc) call.
26296 if (first.eq_ignore_ascii_case("substring")
26297 || first.eq_ignore_ascii_case("substr"))
26298 && args.len() == 1
26299 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
26300 {
26301 self.advance(); // SIMILAR
26302 let pattern = self.parse_expr(0)?;
26303 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
26304 {
26305 return Err(self.err(format!(
26306 "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
26307 self.peek()
26308 )));
26309 }
26310 self.advance(); // ESCAPE
26311 let esc = self.parse_expr(0)?;
26312 if !matches!(self.peek(), Token::RParen) {
26313 return Err(self.err(format!(
26314 "expected ')' to close substring(... SIMILAR ...), got {:?}",
26315 self.peek()
26316 )));
26317 }
26318 self.advance();
26319 args.push(pattern);
26320 args.push(esc);
26321 return Ok(Expr::FunctionCall {
26322 name: "__substring_similar".to_string(),
26323 args,
26324 });
26325 }
26326 if (first.eq_ignore_ascii_case("substring")
26327 || first.eq_ignore_ascii_case("substr"))
26328 && args.len() == 1
26329 && matches!(self.peek(), Token::From | Token::For)
26330 {
26331 // `substring(str FROM pos [FOR len])`, or the FOR-only
26332 // `substring(str FOR len)` which PG treats as FROM 1.
26333 if matches!(self.peek(), Token::From) {
26334 self.advance();
26335 let start = self.parse_expr(0)?;
26336 args.push(start);
26337 } else {
26338 args.push(Expr::Literal(Literal::Integer(1)));
26339 }
26340 if matches!(self.peek(), Token::For) {
26341 self.advance();
26342 let length = self.parse_expr(0)?;
26343 args.push(length);
26344 }
26345 if !matches!(self.peek(), Token::RParen) {
26346 return Err(self.err(format!(
26347 "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
26348 self.peek()
26349 )));
26350 }
26351 self.advance();
26352 return Ok(Expr::FunctionCall {
26353 name: first.to_ascii_lowercase(),
26354 args,
26355 });
26356 }
26357 // PG `overlay(str PLACING repl FROM n [FOR len])`
26358 // syntactic form. Desugars to the `overlay(str,
26359 // repl, n[, len])` comma-list shape the evaluator
26360 // already implements. `PLACING` is not a reserved
26361 // token in SPG, so it arrives as a bare Ident.
26362 if first.eq_ignore_ascii_case("overlay")
26363 && args.len() == 1
26364 && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
26365 {
26366 self.advance(); // consume PLACING
26367 args.push(self.parse_expr(0)?); // replacement
26368 if !matches!(self.peek(), Token::From) {
26369 return Err(self.err(format!(
26370 "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
26371 self.peek()
26372 )));
26373 }
26374 self.advance();
26375 args.push(self.parse_expr(0)?); // start position
26376 if matches!(self.peek(), Token::For) {
26377 self.advance();
26378 args.push(self.parse_expr(0)?); // length
26379 }
26380 if !matches!(self.peek(), Token::RParen) {
26381 return Err(self.err(format!(
26382 "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
26383 self.peek()
26384 )));
26385 }
26386 self.advance();
26387 return Ok(Expr::FunctionCall {
26388 name: String::from("overlay"),
26389 args,
26390 });
26391 }
26392 // `TRIM(chars FROM str)` — the keyword-less
26393 // spelling lands here after the chars parse
26394 // (the keyword forms return earlier).
26395 if first.eq_ignore_ascii_case("trim")
26396 && args.len() == 1
26397 && matches!(self.peek(), Token::From)
26398 {
26399 self.advance();
26400 let target = self.parse_expr(0)?;
26401 if !matches!(self.peek(), Token::RParen) {
26402 return Err(self.err(format!(
26403 "expected ')' to close TRIM(chars FROM str), got {:?}",
26404 self.peek()
26405 )));
26406 }
26407 self.advance();
26408 let chars = args.pop().expect("one arg");
26409 return Ok(Expr::FunctionCall {
26410 name: String::from("btrim"),
26411 args: alloc::vec![target, chars],
26412 });
26413 }
26414 // v7.24 (round-16 A) — aggregate-internal
26415 // ordering: `array_agg(x ORDER BY y DESC NULLS
26416 // LAST)`. Keys close the argument list.
26417 if matches!(self.peek(), Token::Order) {
26418 self.advance();
26419 if !self.peek_is_by() {
26420 return Err(self.err(format!(
26421 "expected BY after ORDER in aggregate args, got {:?}",
26422 self.peek()
26423 )));
26424 }
26425 self.advance();
26426 loop {
26427 // v7.39 (round 691) — save/restore, the discipline this parser
26428 // already uses around `pending_sample_preds`, so a subquery inside
26429 // a key neither inherits nor leaks the channel.
26430 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
26431 let saved_coll = self.order_key_collation.take();
26432 let parsed = self.parse_expr(0);
26433 self.in_order_by_key = saved_flag;
26434 let collation =
26435 core::mem::replace(&mut self.order_key_collation, saved_coll);
26436 let expr = parsed?;
26437 let desc = if matches!(self.peek(), Token::Desc) {
26438 self.advance();
26439 true
26440 } else if matches!(self.peek(), Token::Asc) {
26441 self.advance();
26442 false
26443 } else {
26444 false
26445 };
26446 let nulls_first = self.parse_optional_nulls_placement()?;
26447 agg_order_by.push(OrderBy {
26448 expr,
26449 desc,
26450 nulls_first,
26451 collation,
26452 });
26453 if matches!(self.peek(), Token::Comma) {
26454 self.advance();
26455 } else {
26456 break;
26457 }
26458 }
26459 // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
26460 // follow the ORDER BY inside GROUP_CONCAT.
26461 if self.consume_group_concat_separator(&mut args)? {
26462 saw_separator = true;
26463 }
26464 if !matches!(self.peek(), Token::RParen) {
26465 return Err(self.err(format!(
26466 "expected ')' after aggregate ORDER BY, got {:?}",
26467 self.peek()
26468 )));
26469 }
26470 break;
26471 }
26472 // v7.39 (round 354, M12) — …or directly after the
26473 // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
26474 // own spelling of what PG passes as string_agg's second
26475 // argument; it was a parse error, so every MySQL query
26476 // that names its own separator failed outright.
26477 if self.consume_group_concat_separator(&mut args)? {
26478 saw_separator = true;
26479 break;
26480 }
26481 match self.peek() {
26482 Token::Comma => {
26483 self.advance();
26484 }
26485 Token::RParen => break,
26486 other => {
26487 return Err(self.err(format!(
26488 "expected ',' or ')' in function args, got {other:?}"
26489 )));
26490 }
26491 }
26492 }
26493 }
26494 // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
26495 // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
26496 // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
26497 // meaning a separator — that is what the explicit SEPARATOR
26498 // tail is for. Fold them into one `concat(...)` so the
26499 // aggregate keeps its single value argument.
26500 if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
26501 let values = args.len() - usize::from(saw_separator);
26502 if values > 1 {
26503 let sep_arg = if saw_separator { args.pop() } else { None };
26504 let folded = Expr::FunctionCall {
26505 name: "concat".to_string(),
26506 args: core::mem::take(&mut args),
26507 };
26508 args.push(folded);
26509 if let Some(sep) = sep_arg {
26510 args.push(sep);
26511 }
26512 }
26513 }
26514 self.advance(); // consume ')'
26515 // v7.39 (read01 round 77) — named arguments are NOT reordered here
26516 // any more. The parser has no catalog, so it could only ever resolve
26517 // the handful of `make_*` builtins whose parameter names were baked
26518 // into a table right here — every user function got
26519 // "does not support named arguments", though the catalog has been
26520 // storing its parameter names all along. Reordering happens in eval,
26521 // in one place, for builtins and user functions alike.
26522 // v7.32 (round-29) — ordered-set aggregate tail
26523 // `name(direct_args) WITHIN GROUP (ORDER BY …)`
26524 // (percentile_cont / percentile_disc / mode). The sort spec
26525 // lands in the same `order_by` slot a decorated aggregate
26526 // uses; the executor dispatches on the function name. WITHIN
26527 // GROUP and an intra-argument ORDER BY are mutually
26528 // exclusive (PG rejects both).
26529 let within_group_order = self.parse_within_group_clause()?;
26530 if !within_group_order.is_empty() && !agg_order_by.is_empty() {
26531 return Err(self.err(
26532 "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
26533 .into(),
26534 ));
26535 }
26536 let within_group_seen = !within_group_order.is_empty();
26537 let agg_order_by = if within_group_order.is_empty() {
26538 agg_order_by
26539 } else {
26540 within_group_order
26541 };
26542 // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
26543 let filter = self.parse_filter_clause()?;
26544 // v4.12: window-function tail — `name(args) OVER (...)`.
26545 // Promotes the just-parsed FunctionCall into a
26546 // WindowFunction node carrying partition + order.
26547 // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
26548 // / `RESPECT NULLS OVER (...)` between the closing paren
26549 // and `OVER`.
26550 let null_treatment = self.parse_null_treatment_modifier();
26551 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
26552 && s.eq_ignore_ascii_case("over")
26553 {
26554 self.advance();
26555 // v7.39 (round 230) — PG implements neither modifier for a
26556 // windowed call and says so (0A000). Both used to be parsed
26557 // and then silently dropped here, so `count(DISTINCT v)
26558 // OVER (…)` quietly answered the non-distinct count.
26559 if agg_distinct {
26560 return Err(
26561 self.err("DISTINCT is not implemented for window functions".to_string())
26562 );
26563 }
26564 if !agg_order_by.is_empty() {
26565 // PG separates the two shapes that land here: a
26566 // WITHIN GROUP call is an ordered-set aggregate and gets
26567 // its own message naming the aggregate; a plain
26568 // `agg(x ORDER BY y)` gets the generic one.
26569 let msg = if within_group_seen {
26570 alloc::format!("OVER is not supported for ordered-set aggregate {first}")
26571 } else {
26572 "aggregate ORDER BY is not implemented for window functions".to_string()
26573 };
26574 return Err(self.err(msg));
26575 }
26576 let (partition_by, order_by, frame) = self.parse_over_clause()?;
26577 return Ok(Expr::WindowFunction {
26578 name: first,
26579 args,
26580 partition_by,
26581 order_by,
26582 frame,
26583 null_treatment,
26584 filter,
26585 });
26586 }
26587 if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
26588 return Ok(Expr::AggregateOrdered {
26589 call: Box::new(Expr::FunctionCall { name: first, args }),
26590 order_by: agg_order_by,
26591 distinct: agg_distinct,
26592 filter,
26593 });
26594 }
26595 // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
26596 // over TIMESTAMPTZ and has no timestamp overload, so a
26597 // timestamp argument is coerced on the way in and the answer
26598 // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
26599 // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
26600 // zone`. SPG answered `timestamp without time zone`, dropping
26601 // the offset from every rendering.
26602 //
26603 // Writing the coercion PG performs makes the existing
26604 // argument-driven typing (the one `date_trunc` uses) reach the
26605 // right answer, rather than teaching the type layer a second
26606 // rule. MySQL's DATE_ADD is a different function that returns
26607 // DATE or DATETIME, so this is PG-dialect only.
26608 //
26609 // Out-of-line because this sits on the RECURSIVE descent
26610 // frame: an inline block with locals here costs every nesting
26611 // level, and the suite's deep-nesting sentinel overflowed the
26612 // 512 KiB parser stack the moment one was added (round 430's
26613 // lesson, in the same shape).
26614 if !self.mysql_dialect {
26615 lift_date_add_arg_to_timestamptz(&first, &mut args);
26616 }
26617 return Ok(Expr::FunctionCall { name: first, args });
26618 }
26619 // v7.9.20 — SQL-standard parenless keyword expressions
26620 // (PG treats these as functions called without parens).
26621 // Resolve to a synthetic FunctionCall so the engine's
26622 // eval path reuses the existing function-call routing.
26623 // mailrs G3.
26624 let lc = first.to_ascii_lowercase();
26625 if matches!(
26626 lc.as_str(),
26627 "current_date"
26628 | "current_time"
26629 | "current_timestamp"
26630 | "localtimestamp"
26631 | "localtime"
26632 // v7.37.17 (17.6 siblings) — session-identity SQL-
26633 // standard parenless keywords. current_user /
26634 // session_user / user were already caught by the
26635 // pgwire canned-response shortcut but bare-select
26636 // in the embedded engine went through Expr::Column
26637 // and errored. Adding them here so the parser
26638 // resolves to a synthetic FunctionCall that reuses
26639 // the existing eval/functions.rs dispatch.
26640 | "current_user"
26641 | "session_user"
26642 | "current_role"
26643 | "current_catalog"
26644 | "current_schema"
26645 | "current_database"
26646 // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
26647 | "system_user"
26648 ) {
26649 return Ok(Expr::FunctionCall {
26650 name: lc,
26651 args: Vec::new(),
26652 });
26653 }
26654 Ok(Expr::Column(ColumnName {
26655 qualifier: None,
26656 name: first,
26657 }))
26658 }
26659}
26660
26661/// v7.39 (round 522) — write the coercion PG's `date_add` /
26662/// `date_subtract` signature performs.
26663///
26664/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
26665/// timestamp argument is cast on the way in and the answer is
26666/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
26667/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
26668/// `timestamp without time zone`, dropping the offset from every
26669/// rendering of the result.
26670///
26671/// Writing the cast the signature implies lets the existing
26672/// argument-driven typing (the one `date_trunc` uses) reach the right
26673/// answer instead of teaching the type layer a second rule. MySQL's
26674/// DATE_ADD is a different function returning DATE or DATETIME, so the
26675/// caller applies this in PG dialect only.
26676///
26677/// A free function, and not a block at the call site, because the caller
26678/// is on the recursive-descent frame chain.
26679#[inline(never)]
26680fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
26681 if args.len() != 2
26682 || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
26683 {
26684 return;
26685 }
26686 let base = args.remove(0);
26687 args.insert(
26688 0,
26689 Expr::Cast {
26690 expr: Box::new(base),
26691 target: CastTarget::Timestamptz,
26692 },
26693 );
26694}
26695
26696/// v6.8.2 — walk an expression tree and return the first column
26697/// reference's bare name. Used by `parse_create_index_stmt_after_create`
26698/// to derive `CreateIndexStatement.column` from an expression
26699/// key (so downstream planner code resolving a primary column
26700/// position keeps working with expression indexes). Returns
26701/// `None` when the expression has no column ref at all — caller
26702/// surfaces that as a parse error.
26703fn extract_first_column(expr: &Expr) -> Option<String> {
26704 match expr {
26705 Expr::Column(cn) => Some(cn.name.clone()),
26706 Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
26707 Expr::Binary { lhs, rhs, .. } => {
26708 extract_first_column(lhs).or_else(|| extract_first_column(rhs))
26709 }
26710 Expr::Unary { expr: e, .. } => extract_first_column(e),
26711 // v7.39 (read01 round 93) — a cast wraps its operand: a common
26712 // expression-index key is `lower(col::text)`, where the column
26713 // sits under the `::text` cast inside the function arg. Without
26714 // descending here the key was rejected as "references no column".
26715 Expr::Cast { expr: e, .. } => extract_first_column(e),
26716 // v7.39.2 — and a COLLATE wraps its operand the same way.
26717 // `CREATE INDEX rc ON t (c COLLATE "C" DESC)` stopped naming a
26718 // column the moment the clause became a node instead of being
26719 // absorbed, and the key was rejected as referencing none. This
26720 // is the shape the wildcard below silently produces, which is
26721 // why it is spelled out.
26722 Expr::Collate { expr: e, .. } => extract_first_column(e),
26723 _ => None,
26724 }
26725}
26726
26727fn maybe_not(expr: Expr, negated: bool) -> Expr {
26728 if negated {
26729 Expr::Unary {
26730 op: UnOp::Not,
26731 expr: Box::new(expr),
26732 }
26733 } else {
26734 expr
26735 }
26736}
26737
26738/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
26739/// things in the two dialects, and SPG read all three PG's way:
26740///
26741/// | token | PG (and SPG) | MySQL, measured |
26742/// |---|---|---|
26743/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
26744/// | `&&` | inet / array overlap | **AND** |
26745/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
26746///
26747/// `1 || 0` answering the string '10' on a MySQL session is a wrong
26748/// answer with no error, which is why they are routed here rather than
26749/// left to the shared table.
26750impl Parser {
26751 fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
26752 if self.mysql_dialect {
26753 // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
26754 // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
26755 // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
26756 if let Token::Ident(w) = tok
26757 && w.eq_ignore_ascii_case("div")
26758 {
26759 return Some((BinOp::IntDiv, 8));
26760 }
26761 // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
26762 // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
26763 // the lexer; the `MOD(x, y)` function form is unaffected (MOD
26764 // there sits in operand position, not infix).
26765 if let Token::Ident(w) = tok
26766 && w.eq_ignore_ascii_case("mod")
26767 {
26768 return Some((BinOp::Mod, 8));
26769 }
26770 // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
26771 // plain ident to the lexer. Its precedence sits between OR (1)
26772 // and AND (3) — hence rung 2, the slot freed by moving AND up.
26773 if let Token::Ident(w) = tok
26774 && w.eq_ignore_ascii_case("xor")
26775 {
26776 return Some((BinOp::LogicalXor, 2));
26777 }
26778 match tok {
26779 Token::Concat => return Some((BinOp::Or, 1)),
26780 // MySQL's `&&` is logical AND, sharing AND's rung (3).
26781 Token::InetOverlap => return Some((BinOp::And, 3)),
26782 // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
26783 Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
26784 _ => {}
26785 }
26786 }
26787 binop_from(tok)
26788 }
26789}
26790
26791// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
26792// (which sits strictly between OR and AND), every level from AND upward was
26793// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
26794// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
26795// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
26796// the *relative* order of every PG operator is unchanged by the shift.
26797fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
26798 let pair = match tok {
26799 Token::Or => (BinOp::Or, 1),
26800 Token::And => (BinOp::And, 3),
26801 Token::Eq => (BinOp::Eq, 5),
26802 Token::NotEq => (BinOp::NotEq, 5),
26803 Token::Lt => (BinOp::Lt, 5),
26804 Token::LtEq => (BinOp::LtEq, 5),
26805 Token::Gt => (BinOp::Gt, 5),
26806 Token::GtEq => (BinOp::GtEq, 5),
26807 // pgvector distance ops all sit on the same rung — tighter than
26808 // comparisons (5) so `col <-> v < threshold` parses correctly.
26809 Token::L2Distance => (BinOp::L2Distance, 6),
26810 // v7.39 (read01 geo_ops.c) — geometric predicates ride the
26811 // comparison rung.
26812 Token::GeomParallel => (BinOp::GeomParallel, 5),
26813 // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
26814 // comparison rung.
26815 Token::OverLeft => (BinOp::OverLeft, 5),
26816 Token::OverRight => (BinOp::OverRight, 5),
26817 Token::GeomPerp => (BinOp::GeomPerp, 5),
26818 Token::GeomSameAs => (BinOp::GeomSameAs, 5),
26819 Token::ClosestPoint => (BinOp::ClosestPoint, 6),
26820 Token::GeomHoriz => (BinOp::GeomHoriz, 5),
26821 Token::InnerProduct => (BinOp::InnerProduct, 6),
26822 Token::CosineDistance => (BinOp::CosineDistance, 6),
26823 Token::Plus => (BinOp::Add, 7),
26824 Token::Minus => (BinOp::Sub, 7),
26825 // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
26826 // binds every "other" operator (`||`, `|`, `&`, `#`, the
26827 // pgvector distances above) BETWEEN additive (7) and the
26828 // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
26829 // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
26830 // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
26831 // ("matches PG conceptually" — the round-753 audit measured it
26832 // false; the old rung errored on `'a' || 1 + 1` with
26833 // `text + integer`). Same-level chains left-fold, as PG does.
26834 Token::Concat => (BinOp::Concat, 6),
26835 Token::Pipe => (BinOp::BitOr, 6),
26836 Token::Amp => (BinOp::BitAnd, 6),
26837 Token::Star => (BinOp::Mul, 8),
26838 Token::Slash => (BinOp::Div, 8),
26839 Token::Percent => (BinOp::Mod, 8),
26840 // v4.14: JSON path ops bind tighter than comparisons (5)
26841 // and additive (7) so `doc->'k' = 'v'` parses correctly.
26842 // Same rung as the multiplicative ops.
26843 Token::JsonGet => (BinOp::JsonGet, 8),
26844 Token::JsonGetText => (BinOp::JsonGetText, 8),
26845 Token::JsonGetPath => (BinOp::JsonGetPath, 8),
26846 Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
26847 Token::JsonContains => (BinOp::JsonContains, 8),
26848 Token::JsonPathExists => (BinOp::JsonPathExists, 8),
26849 Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
26850 Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
26851 Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
26852 Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
26853 Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
26854 // v7.12.2 — `@@` binds at the comparison rung (looser than
26855 // arithmetic, tighter than AND / OR). PG places `@@` at
26856 // the same precedence as `=` / `<`, so we follow.
26857 Token::TsMatch => (BinOp::TsMatch, 5),
26858 // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
26859 // PG places these at the comparison rung (same level as `=`),
26860 // so we follow.
26861 Token::InetContainedBy => (BinOp::InetContainedBy, 5),
26862 Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
26863 Token::InetContains => (BinOp::InetContains, 5),
26864 Token::InetContainsEq => (BinOp::InetContainsEq, 5),
26865 Token::InetOverlap => (BinOp::InetOverlap, 5),
26866 // v7.39 (round 508) — the geometric and pattern-order predicates
26867 // ride the comparison rung, as every other predicate does.
26868 Token::Intersects => (BinOp::Intersects, 5),
26869 Token::IsBelow => (BinOp::IsBelow, 5),
26870 Token::IsAbove => (BinOp::IsAbove, 5),
26871 Token::PatternLt => (BinOp::PatternLt, 5),
26872 Token::PatternLtEq => (BinOp::PatternLtEq, 5),
26873 Token::PatternGt => (BinOp::PatternGt, 5),
26874 Token::PatternGtEq => (BinOp::PatternGtEq, 5),
26875 // `@@@` is the old spelling of `@@` and means exactly it.
26876 Token::TsMatchOld => (BinOp::TsMatch, 5),
26877 _ => return None,
26878 };
26879 Some(pair)
26880}
26881
26882#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26883// `as f32` here is intentional: vector elements widen / narrow into f32 on
26884// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
26885// past ~15 decimal digits — both are acceptable for a fixed-precision
26886// pgvector column.
26887/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
26888/// implicit table alias and break trailing clauses. WITH lands
26889/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
26890/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
26891/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
26892/// / VALUES / FOR / LATERAL — all of which would otherwise be
26893/// silently swallowed by `parse_optional_alias`.
26894fn is_alias_stopword(s: &str) -> bool {
26895 matches!(
26896 s.to_ascii_lowercase().as_str(),
26897 "with"
26898 | "on"
26899 | "where"
26900 | "having"
26901 | "group"
26902 | "order"
26903 | "limit"
26904 | "offset"
26905 | "union"
26906 | "except"
26907 | "intersect"
26908 | "returning"
26909 | "set"
26910 | "values"
26911 | "for"
26912 | "window"
26913 | "tablesample"
26914 | "lateral"
26915 | "left"
26916 | "right"
26917 | "inner"
26918 | "outer"
26919 | "full"
26920 | "cross"
26921 | "join"
26922 | "natural"
26923 | "using"
26924 | "fetch"
26925 )
26926}
26927
26928fn extract_numeric_literal(e: &Expr) -> Option<f32> {
26929 match e {
26930 Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
26931 Expr::Literal(Literal::Float(x)) => Some(*x as f32),
26932 // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
26933 // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
26934 // so scale the divisor by hand instead of `f32::powi`.)
26935 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26936 let mut div = 1.0f32;
26937 for _ in 0..*scale {
26938 div *= 10.0;
26939 }
26940 Some(*unscaled as f32 / div)
26941 }
26942 Expr::Unary {
26943 op: UnOp::Neg,
26944 expr,
26945 } => extract_numeric_literal(expr).map(|x| -x),
26946 _ => None,
26947 }
26948}
26949
26950/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
26951/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
26952/// negative. Returns `None` if any pair fails to parse or no pair is found.
26953///
26954/// Recognised units (case-insensitive, optional trailing `s`):
26955/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
26956/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
26957/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
26958/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
26959/// (PG-canonical: DST and month-boundary semantics depend on this).
26960/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
26961/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
26962/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
26963#[allow(clippy::cast_possible_truncation)]
26964fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
26965 let mut months: i64 = 0;
26966 let mut days: i64 = 0;
26967 let mut micros: i64 = 0;
26968 let mut in_time = false;
26969 let mut num = alloc::string::String::new();
26970 for ch in rest.chars() {
26971 if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
26972 num.push(ch);
26973 continue;
26974 }
26975 if ch == 'T' || ch == 't' {
26976 if !num.is_empty() {
26977 return None;
26978 }
26979 in_time = true;
26980 continue;
26981 }
26982 let n: f64 = num.parse().ok()?;
26983 num.clear();
26984 match (ch, in_time) {
26985 ('Y' | 'y', false) => months += (n * 12.0) as i64,
26986 ('M', false) => months += n as i64,
26987 ('W' | 'w', false) => days += (n * 7.0) as i64,
26988 ('D' | 'd', false) => days += n as i64,
26989 ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26990 ('M', true) => micros += (n * 60_000_000.0) as i64,
26991 ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26992 _ => return None,
26993 }
26994 }
26995 if !num.is_empty() {
26996 return None;
26997 }
26998 Some((
26999 i32::try_from(months).ok()?,
27000 i32::try_from(days).ok()?,
27001 micros,
27002 ))
27003}
27004
27005/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
27006/// leading `-` negates the whole value). Rejects date-like strings.
27007fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
27008 let (neg, body) = match s.strip_prefix('-') {
27009 Some(b) => (true, b),
27010 None => (false, s),
27011 };
27012 let (y, m) = body.split_once('-')?;
27013 let years: i32 = y.parse().ok()?;
27014 let mons: i32 = m.parse().ok()?;
27015 if years < 0 || mons < 0 {
27016 return None;
27017 }
27018 let total = years.checked_mul(12)?.checked_add(mons)?;
27019 Some((if neg { -total } else { total }, 0, 0))
27020}
27021
27022/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
27023/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
27024fn parse_interval_clock(tok: &str) -> Option<i64> {
27025 let (neg, body) = match tok.strip_prefix('-') {
27026 Some(r) => (true, r),
27027 None => (false, tok.strip_prefix('+').unwrap_or(tok)),
27028 };
27029 let mut it = body.split(':');
27030 let h: i64 = it.next()?.parse().ok()?;
27031 let m: i64 = it.next()?.parse().ok()?;
27032 let s_tok = it.next().unwrap_or("0");
27033 if it.next().is_some() {
27034 return None;
27035 }
27036 let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
27037 let sec: i64 = sec.parse().ok()?;
27038 let mut f = alloc::string::String::from(frac);
27039 while f.len() < 6 {
27040 f.push('0');
27041 }
27042 f.truncate(6);
27043 let fus: i64 = f.parse().ok()?;
27044 sec.checked_mul(1_000_000)?.checked_add(fus)?
27045 } else {
27046 s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
27047 };
27048 let total = h
27049 .checked_mul(3_600_000_000)?
27050 .checked_add(m.checked_mul(60_000_000)?)?
27051 .checked_add(sec_us)?;
27052 Some(if neg { -total } else { total })
27053}
27054
27055/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
27056/// every spelling PG accepts (measured against live PG18.4, not guessed):
27057/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
27058/// Before this, the unit table matched long names only, with an ad-hoc
27059/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
27060/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
27061/// INTERVAL", and it had also grown arms for the debris that stripping leaves
27062/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
27063/// fractional) both read from this one table now.
27064fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
27065 let u = raw.to_ascii_lowercase();
27066 Some(match u.as_str() {
27067 "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
27068 "microsecond"
27069 }
27070 "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
27071 "millisecond"
27072 }
27073 "second" | "seconds" | "sec" | "secs" | "s" => "second",
27074 "minute" | "minutes" | "min" | "mins" | "m" => "minute",
27075 "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
27076 "day" | "days" | "d" => "day",
27077 "week" | "weeks" | "w" => "week",
27078 "month" | "months" | "mon" | "mons" => "month",
27079 "year" | "years" | "yr" | "yrs" | "y" => "year",
27080 "decade" | "decades" | "dec" | "decs" => "decade",
27081 "century" | "centuries" | "cent" | "c" => "century",
27082 "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
27083 _ => return None,
27084 })
27085}
27086
27087/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
27088/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
27089#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27090pub(crate) enum IntervalField {
27091 Year,
27092 Month,
27093 Day,
27094 Hour,
27095 Minute,
27096 Second,
27097}
27098
27099/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
27100/// spellings aren't standard for the qualifier position, so only the singular
27101/// forms are accepted.
27102/// v7.39 (round 350, M7) — MySQL's interval units, measured against
27103/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
27104/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
27105/// take a `'1 2'` style literal — are not read here; they stay a parse
27106/// error rather than being silently misread.)
27107/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
27108///
27109/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
27110/// to do with a `@@` engine setting, and an unset one reads NULL rather
27111/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
27112/// were the same node and `SELECT @x` answered "Unknown system variable".)
27113/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
27114/// not see a session override — measured, after `SET autocommit=0`,
27115/// `@@global.autocommit` is still 1.
27116///
27117/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
27118/// the parser's nesting budget is tuned against, and building these
27119/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
27120/// wall `parse_left_right_atom` and friends were factored out for).
27121#[inline(never)]
27122fn variable_ref_atom(raw: &str) -> Expr {
27123 let user_var = !raw.starts_with("@@");
27124 let bare = raw.trim_start_matches('@').to_ascii_lowercase();
27125 Expr::FunctionCall {
27126 name: String::from(if user_var {
27127 "__spg_user_var"
27128 } else {
27129 "__spg_session_var"
27130 }),
27131 args: alloc::vec![Expr::Literal(Literal::String(bare))],
27132 }
27133}
27134
27135fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
27136 let Token::Ident(s) = tok else { return None };
27137 Some(match () {
27138 () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
27139 () if s.eq_ignore_ascii_case("second") => "second",
27140 () if s.eq_ignore_ascii_case("minute") => "minute",
27141 () if s.eq_ignore_ascii_case("hour") => "hour",
27142 () if s.eq_ignore_ascii_case("day") => "day",
27143 () if s.eq_ignore_ascii_case("week") => "week",
27144 () if s.eq_ignore_ascii_case("month") => "month",
27145 () if s.eq_ignore_ascii_case("quarter") => "quarter",
27146 () if s.eq_ignore_ascii_case("year") => "year",
27147 () => return None,
27148 })
27149}
27150
27151/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
27152/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
27153/// which constructs the value at run time. Only the slot the unit names
27154/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
27155/// slot the builtin has (months and fractional seconds respectively).
27156fn make_interval_call(qty: Expr, unit: &str) -> Expr {
27157 let zero = || Expr::Literal(Literal::Integer(0));
27158 let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
27159 lhs: alloc::boxed::Box::new(qty.clone()),
27160 op,
27161 rhs: alloc::boxed::Box::new(by),
27162 };
27163 // (years, months, weeks, days, hours, mins, secs)
27164 let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
27165 match unit {
27166 "year" => args[0] = qty,
27167 "quarter" => {
27168 args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
27169 }
27170 "month" => args[1] = qty,
27171 "week" => args[2] = qty,
27172 "day" => args[3] = qty,
27173 "hour" => args[4] = qty,
27174 "minute" => args[5] = qty,
27175 "second" => args[6] = qty,
27176 // The builtin's seconds slot takes a fraction, so microseconds ride
27177 // it scaled down; the divisor is a NUMERIC literal so the division
27178 // stays exact rather than going through a float.
27179 "microsecond" => {
27180 args[6] = scaled(
27181 crate::ast::BinOp::Div,
27182 Expr::Literal(Literal::Numeric {
27183 unscaled: 1_000_000,
27184 scale: 0,
27185 }),
27186 );
27187 }
27188 _ => args[3] = qty,
27189 }
27190 Expr::FunctionCall {
27191 name: alloc::string::String::from("make_interval"),
27192 args,
27193 }
27194}
27195
27196/// `(count, unit)` → `(months, days, micros)`.
27197fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
27198 let n: i64 = count.trim().parse().ok()?;
27199 Some(match unit {
27200 "microsecond" => (0, 0, n),
27201 "second" => (0, 0, n.checked_mul(1_000_000)?),
27202 "minute" => (0, 0, n.checked_mul(60_000_000)?),
27203 "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
27204 "day" => (0, i32::try_from(n).ok()?, 0),
27205 "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
27206 "month" => (i32::try_from(n).ok()?, 0, 0),
27207 "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
27208 "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
27209 _ => return None,
27210 })
27211}
27212
27213fn interval_field_of(tok: &Token) -> Option<IntervalField> {
27214 let Token::Ident(s) = tok else { return None };
27215 Some(match () {
27216 () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
27217 () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
27218 () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
27219 () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
27220 () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
27221 () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
27222 () => return None,
27223 })
27224}
27225
27226/// v7.39 (read01 round 102) — interpret an interval literal under a field
27227/// qualifier. Returns `(months, days, micros)`.
27228///
27229/// * A single field applied to a bare number sets which unit the number means,
27230/// truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
27231/// SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
27232/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
27233/// * Every other range, and any literal a single field can't read as a plain
27234/// number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
27235/// interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
27236/// like PG, and the qualifier there only bounds precision.
27237fn interpret_qualified_interval(
27238 text: &str,
27239 (f1, f2): (IntervalField, Option<IntervalField>),
27240) -> Option<(i32, i32, i64)> {
27241 if let Some(f2) = f2 {
27242 if f1 == IntervalField::Year && f2 == IntervalField::Month {
27243 if let Some(m) = parse_year_month_literal(text) {
27244 return Some((m, 0, 0));
27245 }
27246 }
27247 return parse_interval_text(text);
27248 }
27249 // Single field: reinterpret a bare number; otherwise the default parse.
27250 let trimmed = text.trim();
27251 if let Ok(val) = trimmed.parse::<f64>() {
27252 // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
27253 #[allow(clippy::cast_possible_truncation)]
27254 let whole = val as i64;
27255 #[allow(clippy::cast_possible_truncation)]
27256 let secs_micros = {
27257 let m = val * 1_000_000.0;
27258 if m >= 0.0 {
27259 (m + 0.5) as i64
27260 } else {
27261 (m - 0.5) as i64
27262 }
27263 };
27264 return Some(match f1 {
27265 IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
27266 IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
27267 IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
27268 IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
27269 IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
27270 IntervalField::Second => (0, 0, secs_micros),
27271 });
27272 }
27273 parse_interval_text(text)
27274}
27275
27276/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
27277fn parse_year_month_literal(text: &str) -> Option<i32> {
27278 let t = text.trim();
27279 let (neg, body) = match t.strip_prefix('-') {
27280 Some(r) => (true, r),
27281 None => (false, t.strip_prefix('+').unwrap_or(t)),
27282 };
27283 let mut it = body.split('-');
27284 let years: i32 = it.next()?.trim().parse().ok()?;
27285 let months: i32 = match it.next() {
27286 Some(m) => m.trim().parse().ok()?,
27287 None => 0,
27288 };
27289 if it.next().is_some() {
27290 return None;
27291 }
27292 let total = years.checked_mul(12)?.checked_add(months)?;
27293 Some(if neg { -total } else { total })
27294}
27295
27296pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
27297 // v7.38.19 — the two infinities, answered as the three extreme
27298 // fields PostgreSQL itself puts on the wire for them:
27299 //
27300 // COPY (SELECT 'infinity'::interval) TO STDOUT (FORMAT binary)
27301 // … 7fffffffffffffff 7fffffff 7fffffff
27302 //
27303 // So no caller has to know the spelling — every one of them already
27304 // reads the three numbers, and `IntervalKind::from_fields` names
27305 // what they mean.
27306 //
27307 // `inf` is NOT one of them, measured: `'inf'::interval` is *invalid
27308 // input syntax* on PostgreSQL 18.4 while `'inf'::float8` is
27309 // infinity. Interval takes the full word, in any case.
27310 {
27311 let word = s.trim();
27312 let word = word.strip_prefix('@').map_or(word, str::trim);
27313 let (neg, body) = match word.strip_prefix('-') {
27314 Some(rest) => (true, rest.trim_start()),
27315 None => (false, word.strip_prefix('+').map_or(word, str::trim_start)),
27316 };
27317 if body.eq_ignore_ascii_case("infinity") {
27318 return Some(if neg {
27319 (i32::MIN, i32::MIN, i64::MIN)
27320 } else {
27321 (i32::MAX, i32::MAX, i64::MAX)
27322 });
27323 }
27324 }
27325 // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
27326 // `@` is decorative; a trailing `ago` negates the whole interval.
27327 let mut trimmed = s.trim();
27328 trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
27329 let mut negate = false;
27330 if let Some(rest) = trimmed
27331 .strip_suffix("ago")
27332 .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
27333 {
27334 negate = true;
27335 trimmed = rest.trim();
27336 }
27337 let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
27338 let (mo, d, us) = v?;
27339 if negate {
27340 Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
27341 } else {
27342 Some((mo, d, us))
27343 }
27344 };
27345 let s = trimmed;
27346 // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
27347 // are single tokens, not the `<n> <unit>` pair form handled below.
27348 if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
27349 return finish(parse_iso8601_interval(rest));
27350 }
27351 if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
27352 if let Some(iv) = parse_year_month_interval(trimmed) {
27353 return finish(Some(iv));
27354 }
27355 }
27356 // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
27357 // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
27358 // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
27359 if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
27360 if let Ok(n) = trimmed.parse::<i64>() {
27361 return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
27362 }
27363 if let Ok(f) = trimmed.parse::<f64>() {
27364 if f.is_finite() {
27365 #[allow(clippy::cast_possible_truncation)]
27366 return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
27367 }
27368 }
27369 }
27370 // v7.39 (round 243) — PG accepts the number and unit run together
27371 // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
27372 // the `<n> <unit>` pair loop below sees them as two.
27373 let raw_parts: Vec<&str> = s.split_whitespace().collect();
27374 let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
27375 for p in raw_parts {
27376 let boundary = p
27377 .char_indices()
27378 .find(|(i, c)| {
27379 *i > 0
27380 && c.is_ascii_alphabetic()
27381 && p[..*i]
27382 .chars()
27383 .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
27384 && p[..*i].chars().any(|d| d.is_ascii_digit())
27385 })
27386 .map(|(i, _)| i);
27387 match boundary {
27388 Some(i) => {
27389 parts.push(&p[..i]);
27390 parts.push(&p[i..]);
27391 }
27392 None => parts.push(p),
27393 }
27394 }
27395 // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
27396 // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
27397 // remains is the `<n> <unit>` pair form handled below.
27398 let mut clock_us: i64 = 0;
27399 let mut had_clock = false;
27400 if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
27401 clock_us = parse_interval_clock(parts[pos])?;
27402 parts.remove(pos);
27403 had_clock = true;
27404 }
27405 // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
27406 // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
27407 let mut lone_days: i32 = 0;
27408 if had_clock && parts.len() == 1 {
27409 if let Ok(n) = parts[0].parse::<i64>() {
27410 lone_days = i32::try_from(n).ok()?;
27411 parts.clear();
27412 }
27413 }
27414 if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
27415 return None;
27416 }
27417 let mut months: i32 = 0;
27418 let mut days: i32 = lone_days;
27419 let mut micros: i64 = clock_us;
27420 let mut i = 0;
27421 while i < parts.len() {
27422 let unit_stripped = canonical_interval_unit(parts[i + 1])?;
27423 if let Ok(n) = parts[i].parse::<i64>() {
27424 match unit_stripped {
27425 "microsecond" => micros = micros.checked_add(n)?,
27426 "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
27427 "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
27428 "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
27429 "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
27430 "day" => {
27431 let n32 = i32::try_from(n).ok()?;
27432 days = days.checked_add(n32)?;
27433 }
27434 "week" => {
27435 let n32 = i32::try_from(n).ok()?;
27436 days = days.checked_add(n32.checked_mul(7)?)?;
27437 }
27438 "month" => {
27439 let n32 = i32::try_from(n).ok()?;
27440 months = months.checked_add(n32)?;
27441 }
27442 "year" => {
27443 let n32 = i32::try_from(n).ok()?;
27444 months = months.checked_add(n32.checked_mul(12)?)?;
27445 }
27446 // v7.39 (read01 timestamp.c) — the larger calendar units.
27447 "decade" => {
27448 let n32 = i32::try_from(n).ok()?;
27449 months = months.checked_add(n32.checked_mul(120)?)?;
27450 }
27451 "century" => {
27452 let n32 = i32::try_from(n).ok()?;
27453 months = months.checked_add(n32.checked_mul(1200)?)?;
27454 }
27455 "millennium" => {
27456 let n32 = i32::try_from(n).ok()?;
27457 months = months.checked_add(n32.checked_mul(12000)?)?;
27458 }
27459 _ => return None,
27460 }
27461 } else if let Ok(f) = parts[i].parse::<f64>() {
27462 // Fractional units cascade down to the next-finer field the way
27463 // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
27464 // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
27465 // no_std: f64 has no trunc/fract/round methods, so do them with
27466 // casts (toward-zero) + explicit round-half-away-from-zero.
27467 #[allow(clippy::cast_possible_truncation)]
27468 fn round_i64(x: f64) -> i64 {
27469 if x >= 0.0 {
27470 (x + 0.5) as i64
27471 } else {
27472 (x - 0.5) as i64
27473 }
27474 }
27475 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27476 fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
27477 const DAY_US: f64 = 86_400_000_000.0;
27478 let whole = d as i64; // truncates toward zero
27479 let frac = d - whole as f64;
27480 *days = days.checked_add(i32::try_from(whole).ok()?)?;
27481 *micros = micros.checked_add(round_i64(frac * DAY_US))?;
27482 Some(())
27483 }
27484 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27485 match unit_stripped {
27486 "microsecond" => micros = micros.checked_add(round_i64(f))?,
27487 "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
27488 "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
27489 "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
27490 "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
27491 "day" => add_days_frac(&mut days, &mut micros, f)?,
27492 "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
27493 "month" => {
27494 let whole = f as i64;
27495 months = months.checked_add(i32::try_from(whole).ok()?)?;
27496 add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
27497 }
27498 "year" => {
27499 let m = f * 12.0;
27500 let whole = m as i64;
27501 months = months.checked_add(i32::try_from(whole).ok()?)?;
27502 add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
27503 }
27504 _ => return None,
27505 }
27506 } else {
27507 return None;
27508 }
27509 i += 2;
27510 }
27511 finish(Some((months, days, micros)))
27512}
27513
27514/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
27515/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
27516/// `interval` is intentionally absent (handled by its own parser arm).
27517/// Returns `None` for names that aren't sensible as a bare typed literal, so
27518/// the caller falls back to treating the ident as a column reference.
27519fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
27520 Some(match ident {
27521 "date" => CastTarget::Date,
27522 "timestamp" | "datetime" => CastTarget::Timestamp,
27523 "timestamptz" => CastTarget::Timestamptz,
27524 "bool" | "boolean" => CastTarget::Bool,
27525 "int" | "integer" | "int4" => CastTarget::Int,
27526 "bigint" | "int8" => CastTarget::BigInt,
27527 "float8" | "double precision" => CastTarget::Float,
27528 "uuid" => CastTarget::Uuid,
27529 "bytea" => CastTarget::Bytea,
27530 "json" => CastTarget::Json,
27531 "jsonb" => CastTarget::Jsonb,
27532 // Types without a dedicated CastTarget variant flow through the
27533 // generic Named path (engine resolves via column_type_to_data_type).
27534 "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
27535 | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
27536 | "money" | "bit" | "varbit"
27537 // Geometric types accept the `TYPE 'literal'` prefix spelling too.
27538 | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
27539 // Range / multirange types likewise.
27540 | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
27541 | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
27542 | "datemultirange" | "tsmultirange" | "tstzmultirange"
27543 // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
27544 | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
27545 CastTarget::Named(alloc::string::String::from(ident))
27546 }
27547 _ => return None,
27548 })
27549}
27550
27551/// v7.12.4 — map a bare type-name identifier (the form that
27552/// appears in a function arg list or RETURNS clause) to a
27553/// [`ColumnTypeName`]. Returns `None` for unknown / extension
27554/// types so the caller can preserve them as
27555/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
27556///
27557/// Subset of the full column-type grammar — we deliberately
27558/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
27559/// here because function-arg types in v7.12.4 are mostly the
27560/// bare form (`text`, `int`, `bytea`, …).
27561/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
27562/// than being `name TYPE`?
27563///
27564/// The multi-word spellings SQL allows for a bare argument type, each
27565/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
27566///
27567/// NOTE this list also exists in `spg-storage`, which computes the
27568/// signature key from the rendered argument text and has to reach the
27569/// same verdict. The two crates are siblings — neither depends on the
27570/// other — and each already carries its own table of type spellings
27571/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
27572/// there), so this follows the structure rather than inventing new
27573/// duplication. Recorded as V49.
27574pub fn is_multiword_type_phrase(phrase: &str) -> bool {
27575 let t = phrase.trim().to_ascii_lowercase();
27576 let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
27577 matches!(
27578 base,
27579 "double precision"
27580 | "character varying"
27581 | "bit varying"
27582 | "timestamp with time zone"
27583 | "timestamp without time zone"
27584 | "time with time zone"
27585 | "time without time zone"
27586 | "national character"
27587 | "national character varying"
27588 )
27589}
27590
27591fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
27592 Some(match ident.to_ascii_lowercase().as_str() {
27593 "smallint" | "tinyint" => ColumnTypeName::SmallInt,
27594 "int" | "integer" | "mediumint" => ColumnTypeName::Int,
27595 "bigint" => ColumnTypeName::BigInt,
27596 "float" | "double" => ColumnTypeName::Float,
27597 // v7.39 (round 269) — real is 32-bit.
27598 "real" | "float4" => ColumnTypeName::Real,
27599 "text" => ColumnTypeName::Text,
27600 "bool" | "boolean" => ColumnTypeName::Bool,
27601 "date" => ColumnTypeName::Date,
27602 "timestamp" | "datetime" => ColumnTypeName::Timestamp,
27603 "timestamptz" => ColumnTypeName::Timestamptz,
27604 "json" => ColumnTypeName::Json,
27605 "jsonb" => ColumnTypeName::Jsonb,
27606 "bytea" | "bytes" => ColumnTypeName::Bytes,
27607 "tsvector" => ColumnTypeName::TsVector,
27608 "tsquery" => ColumnTypeName::TsQuery,
27609 "uuid" => ColumnTypeName::Uuid,
27610 "interval" => ColumnTypeName::Interval,
27611 "time" => ColumnTypeName::Time,
27612 "year" => ColumnTypeName::Year,
27613 "timetz" => ColumnTypeName::TimeTz,
27614 "money" => ColumnTypeName::Money,
27615 _ => return None,
27616 })
27617}
27618
27619/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
27620/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
27621///
27622/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
27623/// / embedded SQL land in v7.12.5+):
27624///
27625/// ```text
27626/// body := [ws] block [ws]
27627/// block := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
27628/// stmt := assign | return
27629/// assign := assign_target := expr
27630/// assign_target := ( NEW | OLD ) . ident | ident
27631/// return := RETURN ( NEW | OLD | NULL | expr )
27632/// ```
27633///
27634/// `expr` is parsed by recursing into the regular `Parser` — so a
27635/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
27636/// NEW.subject || ' ' || NEW.sender)` body shape works without
27637/// the body parser knowing what `to_tsvector` is.
27638///
27639/// Errors here cause the caller to fall back to
27640/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
27641/// successful, but the executor will refuse to invoke the
27642/// function with an "unparseable body" error.
27643/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
27644/// from the crate root as `spg_sql::parse_function_body`.
27645pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
27646 parse_plpgsql_body(body)
27647}
27648
27649fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
27650 // Use the regular lexer on the body text. The trailing
27651 // `END;` may or may not have a semicolon; the lexer treats
27652 // both forms identically.
27653 let tokens = lexer::tokenize(body).map_err(|e| ParseError {
27654 message: alloc::format!("plpgsql body lex error: {e}"),
27655 token_pos: 0,
27656 })?;
27657 let mut parser = Parser::new(tokens);
27658 parser.parse_plpgsql_block()
27659}
27660
27661/// v7.39 (GUC) — the textual body of a SET value, for list joining.
27662fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
27663 match v {
27664 crate::ast::SetValue::String(s)
27665 | crate::ast::SetValue::Ident(s)
27666 | crate::ast::SetValue::Number(s) => s.clone(),
27667 crate::ast::SetValue::Default => "DEFAULT".into(),
27668 }
27669}
27670
27671/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
27672/// contains an aggregate call at ITS OWN query level (recursion stops at
27673/// sublink boundaries — a sublink's aggregates belong to the sublink).
27674/// Backs the "aggregate functions are not allowed in a recursive query's
27675/// recursive term" well-formedness check.
27676fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
27677 const AGG_NAMES: &[&str] = &[
27678 "count",
27679 "sum",
27680 "min",
27681 "max",
27682 "avg",
27683 "string_agg",
27684 "array_agg",
27685 "bool_and",
27686 "bool_or",
27687 "every",
27688 "any_value",
27689 "json_agg",
27690 "jsonb_agg",
27691 "json_object_agg",
27692 "jsonb_object_agg",
27693 "bit_and",
27694 "bit_or",
27695 "bit_xor",
27696 "var_pop",
27697 "var_samp",
27698 "variance",
27699 "stddev",
27700 "stddev_pop",
27701 "stddev_samp",
27702 "range_agg",
27703 "range_intersect_agg",
27704 "percentile_cont",
27705 "percentile_disc",
27706 "mode",
27707 "corr",
27708 "covar_pop",
27709 "covar_samp",
27710 ];
27711 match e {
27712 Expr::AggregateOrdered { .. } => true,
27713 Expr::FunctionCall { name, args } => {
27714 AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
27715 || args.iter().any(expr_has_toplevel_aggregate)
27716 }
27717 Expr::NamedArg { expr, .. }
27718 | Expr::Variadic(expr)
27719 | Expr::Unary { expr, .. }
27720 | Expr::Cast { expr, .. }
27721 | Expr::IsNull { expr, .. }
27722 | Expr::FieldAccess { base: expr, .. }
27723 | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
27724 Expr::Binary { lhs, rhs, .. } => {
27725 expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
27726 }
27727 Expr::Like { expr, pattern, .. } => {
27728 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
27729 }
27730 Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
27731 Expr::InList { expr, list, .. } => {
27732 expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
27733 }
27734 Expr::ArraySubscript { target, index } => {
27735 expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
27736 }
27737 Expr::ArraySlice { target, lo, hi } => {
27738 expr_has_toplevel_aggregate(target)
27739 || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
27740 || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
27741 }
27742 Expr::AnyAll { expr, array, .. } => {
27743 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
27744 }
27745 Expr::Case {
27746 operand,
27747 branches,
27748 else_branch,
27749 } => {
27750 operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
27751 || branches
27752 .iter()
27753 .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
27754 || else_branch
27755 .as_deref()
27756 .is_some_and(expr_has_toplevel_aggregate)
27757 }
27758 // The outer-level operands of a sublink can aggregate; the sublink's
27759 // own body cannot leak its aggregates up here.
27760 Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
27761 Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
27762 row.iter().any(expr_has_toplevel_aggregate)
27763 }
27764 _ => false,
27765 }
27766}
27767
27768/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
27769/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
27770/// named table anywhere in its subtree. A plain FROM derived table is NOT a
27771/// sublink and is legal in a recursive term, so it is not walked here.
27772fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
27773 let mut exprs: Vec<&Expr> = Vec::new();
27774 for it in &s.items {
27775 if let crate::ast::SelectItem::Expr { expr, .. } = it {
27776 exprs.push(expr);
27777 }
27778 }
27779 if let Some(w) = &s.where_ {
27780 exprs.push(w);
27781 }
27782 if let Some(h) = &s.having {
27783 exprs.push(h);
27784 }
27785 if let Some(g) = &s.group_by {
27786 exprs.extend(g.iter());
27787 }
27788 if let Some(from) = &s.from {
27789 for j in &from.joins {
27790 if let Some(on) = &j.on {
27791 exprs.push(on);
27792 }
27793 }
27794 }
27795 exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
27796}
27797
27798/// Does this expression contain a sublink whose subquery mentions `name`?
27799fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
27800 match e {
27801 Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
27802 Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
27803 Expr::InSubquery { expr, subquery, .. } => {
27804 expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
27805 }
27806 Expr::RowInSubquery { row, subquery, .. } => {
27807 row.iter().any(|x| expr_sublink_mentions(x, name))
27808 || select_mentions_table(subquery, name)
27809 }
27810 Expr::RowCmpSubquery { row, subquery, .. } => {
27811 row.iter().any(|x| expr_sublink_mentions(x, name))
27812 || select_mentions_table(subquery, name)
27813 }
27814 Expr::NamedArg { expr, .. }
27815 | Expr::Variadic(expr)
27816 | Expr::Unary { expr, .. }
27817 | Expr::Cast { expr, .. }
27818 | Expr::IsNull { expr, .. }
27819 | Expr::FieldAccess { base: expr, .. }
27820 | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
27821 Expr::Binary { lhs, rhs, .. } => {
27822 expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
27823 }
27824 Expr::Like { expr, pattern, .. } => {
27825 expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
27826 }
27827 Expr::FunctionCall { args, .. } | Expr::Array(args) => {
27828 args.iter().any(|x| expr_sublink_mentions(x, name))
27829 }
27830 Expr::InList { expr, list, .. } => {
27831 expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
27832 }
27833 Expr::ArraySubscript { target, index } => {
27834 expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
27835 }
27836 Expr::ArraySlice { target, lo, hi } => {
27837 expr_sublink_mentions(target, name)
27838 || lo
27839 .as_deref()
27840 .is_some_and(|x| expr_sublink_mentions(x, name))
27841 || hi
27842 .as_deref()
27843 .is_some_and(|x| expr_sublink_mentions(x, name))
27844 }
27845 Expr::AnyAll { expr, array, .. } => {
27846 expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
27847 }
27848 Expr::Case {
27849 operand,
27850 branches,
27851 else_branch,
27852 } => {
27853 operand
27854 .as_deref()
27855 .is_some_and(|x| expr_sublink_mentions(x, name))
27856 || branches
27857 .iter()
27858 .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
27859 || else_branch
27860 .as_deref()
27861 .is_some_and(|x| expr_sublink_mentions(x, name))
27862 }
27863 _ => false,
27864 }
27865}
27866
27867/// Does this SELECT (in full — FROM tables, derived tables, its own
27868/// sublinks, and union arms) mention the named table?
27869fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
27870 if let Some(from) = &s.from {
27871 if from.primary.name.eq_ignore_ascii_case(name) {
27872 return true;
27873 }
27874 if let Some(sub) = &from.primary.lateral_subquery
27875 && select_mentions_table(sub, name)
27876 {
27877 return true;
27878 }
27879 for j in &from.joins {
27880 if j.table.name.eq_ignore_ascii_case(name) {
27881 return true;
27882 }
27883 if let Some(sub) = &j.table.lateral_subquery
27884 && select_mentions_table(sub, name)
27885 {
27886 return true;
27887 }
27888 }
27889 }
27890 if select_has_self_ref_in_sublink(s, name) {
27891 return true;
27892 }
27893 s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
27894}
27895
27896/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
27897/// row count, the way PG evaluates one before applying it.
27898///
27899/// `None` = not a constant (a column, a subquery, a function call).
27900/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
27901/// message stands in for LIMIT / OFFSET, which the caller substitutes.
27902/// All wordings were read off live PG 18.4.
27903fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
27904 use crate::ast::{BinOp, Expr, Literal, UnOp};
27905 match e {
27906 Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
27907 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27908 Some(Ok(round_scaled_half_away(*unscaled, *scale)))
27909 }
27910 // PG coerces a string by its CONTENT, and fails on the value.
27911 Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
27912 |_| {
27913 Err(alloc::format!(
27914 "invalid input syntax for type bigint: \"{t}\""
27915 ))
27916 },
27917 |n| Ok(i128::from(n)),
27918 )),
27919 Expr::Literal(Literal::Bool(_)) => Some(Err(
27920 "argument of {L} must be type bigint, not type boolean".into(),
27921 )),
27922 Expr::Unary {
27923 op: UnOp::Neg,
27924 expr,
27925 } => match fold_limit_constant(expr)? {
27926 Ok(v) => Some(Ok(-v)),
27927 e @ Err(_) => Some(e),
27928 },
27929 Expr::Binary { lhs, op, rhs } => {
27930 let a = match fold_limit_constant(lhs)? {
27931 Ok(v) => v,
27932 e @ Err(_) => return Some(e),
27933 };
27934 let b = match fold_limit_constant(rhs)? {
27935 Ok(v) => v,
27936 e @ Err(_) => return Some(e),
27937 };
27938 let out = match op {
27939 BinOp::Add => a.checked_add(b),
27940 BinOp::Sub => a.checked_sub(b),
27941 BinOp::Mul => a.checked_mul(b),
27942 BinOp::Div if b != 0 => a.checked_div(b),
27943 BinOp::Div => return Some(Err("division by zero".into())),
27944 BinOp::Mod if b != 0 => a.checked_rem(b),
27945 BinOp::Mod => return Some(Err("division by zero".into())),
27946 _ => return None,
27947 };
27948 // PG evaluates the arithmetic in the operand's own type, so an
27949 // int-by-int product that leaves int range fails there — before
27950 // the row count is ever looked at.
27951 match out {
27952 Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
27953 Some(Err("integer out of range".into()))
27954 }
27955 Some(v) => Some(Ok(v)),
27956 None => Some(Err("integer out of range".into())),
27957 }
27958 }
27959 _ => None,
27960 }
27961}
27962
27963/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
27964/// cast, which is what makes `LIMIT 2.5` keep three rows.
27965fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
27966 if scale == 0 {
27967 return unscaled;
27968 }
27969 let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
27970 return 0;
27971 };
27972 let neg = unscaled < 0;
27973 let mag = unscaled.unsigned_abs() as i128;
27974 let rounded = (mag + div / 2) / div;
27975 if neg { -rounded } else { rounded }
27976}
27977
27978#[cfg(test)]
27979mod tests {
27980 use super::*;
27981 use alloc::string::ToString;
27982
27983 fn parse(s: &str) -> Statement {
27984 parse_statement(s).expect("parse ok")
27985 }
27986
27987 // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
27988 // `tables`, `partition`, etc. are unreserved keywords per PG's
27989 // `pg_get_keywords()` and MUST be usable as column / table /
27990 // alias names. Pre-T4 every drop-in user whose schema had one
27991 // of these as a column name (sentori events.release, mailrs
27992 // messages.index in some forks) blew the parser up at CREATE
27993 // TABLE time with "expected identifier, got Release". The
27994 // generalisation lives in `unreserved_keyword_text` + the
27995 // `expect_ident_like` and `parse_atom` arms that consult it.
27996 #[test]
27997 fn release_usable_as_column_name_in_create_table() {
27998 let stmt =
27999 parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
28000 if let Statement::CreateTable(t) = stmt {
28001 let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
28002 assert_eq!(names, alloc::vec!["id", "release", "payload"]);
28003 } else {
28004 panic!("expected CreateTable");
28005 }
28006 }
28007
28008 #[test]
28009 fn release_usable_as_column_ref_in_select_projection() {
28010 // The sentori `0003_partition_events.sql` INSERT-SELECT
28011 // walk references `release` in both column lists; the
28012 // projection-side use exercises `parse_atom`'s relaxed
28013 // identifier set.
28014 parse("SELECT id, release, payload FROM events WHERE id = 1");
28015 }
28016
28017 #[test]
28018 fn release_usable_as_column_ref_in_insert_column_list() {
28019 // INSERT INTO t (id, release, payload) VALUES (…)
28020 parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
28021 }
28022
28023 #[test]
28024 fn alter_column_drop_not_null_uses_keyword_drop_token() {
28025 // Sentori `0013_audit_tombstone.sql` issues
28026 // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
28027 // emits Token::Drop (not Ident("drop")); the parser must
28028 // accept both in the ALTER COLUMN sub-dispatch.
28029 parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
28030 }
28031
28032 #[test]
28033 fn create_index_accepts_parenthesised_expression_key() {
28034 // sentori `0040_events_bundle_idx.sql` shape — JSONB
28035 // expression index. Pre-T4 the parser bailed at the
28036 // inner `(` with "expected column ident or expression,
28037 // got LParen". The Token::LParen arm in CREATE INDEX
28038 // routes through the expression parser instead.
28039 parse(
28040 "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
28041 ON events ((payload->'bundle'->>'id'))",
28042 );
28043 }
28044
28045 // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
28046 // surface as parse errors, never stack overflows (embed hosts
28047 // abort on overflow).
28048 /// The nesting budget is a COUNT; what it has to fit inside is a
28049 /// number of BYTES, and only one of those two is stable across
28050 /// compiler versions. Round 847 measured 30,336 bytes per level
28051 /// after a toolchain move, which puts 64 levels at 1.94 MB and
28052 /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
28053 /// aborted instead of erroring, which is precisely the outcome it
28054 /// exists to rule out.
28055 ///
28056 /// So the budget is metered rather than assumed. The ceiling leaves
28057 /// the depth SPG advertises fitting in a default 2 MiB thread with
28058 /// room to spare, in the debug build, where frames are widest.
28059 #[test]
28060 fn nesting_frame_cost_stays_under_ceiling() {
28061 // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
28062 // thread keeps a margin for whatever called the parser.
28063 const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
28064
28065 frame_meter::reset();
28066 let depth = frame_meter::SAMPLE_HI + 8;
28067 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28068 parse(&sql);
28069
28070 let per_level = frame_meter::bytes_per_level();
28071 {
28072 extern crate std;
28073 std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
28074 }
28075 assert!(
28076 per_level <= CEILING,
28077 "{per_level} bytes per nesting level exceeds {CEILING}; \
28078 {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
28079 in parse_expr_inner / parse_unary rather than lowering the \
28080 depth or widening the stack.",
28081 per_level * MAX_NEST_DEPTH
28082 );
28083 }
28084
28085 #[test]
28086 fn nesting_budget_errors_cleanly() {
28087 let depth = MAX_NEST_DEPTH + 50;
28088 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28089 let err = parse_statement(&sql).expect_err("must reject");
28090 assert!(err.message.contains("nests deeper"), "{err:?}");
28091 // Within budget still parses.
28092 let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
28093 parse(&sql);
28094 }
28095
28096 #[test]
28097 fn binary_chain_budget_errors_cleanly() {
28098 let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
28099 let err = parse_statement(&sql).expect_err("must reject");
28100 assert!(err.message.contains("chained binary"), "{err:?}");
28101 // Within budget still parses (chain depth ≤ budget is safe
28102 // for recursive eval/drop on 2 MiB stacks).
28103 let sql = format!("SELECT 1{}", " + 1".repeat(200));
28104 parse(&sql);
28105 }
28106
28107 #[test]
28108 fn in_list_unaffected_by_chain_budget() {
28109 // Flat InList: 20k elements parse fine and stay flat.
28110 let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
28111 let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
28112 let Statement::Select(s) = parse(&sql) else {
28113 panic!("expected select")
28114 };
28115 let Some(Expr::InList { list, negated, .. }) = s.where_ else {
28116 panic!("expected flat InList, got {:?}", s.where_)
28117 };
28118 assert_eq!(list.len(), 20_000);
28119 assert!(!negated);
28120 }
28121
28122 fn lit_int(n: i64) -> Expr {
28123 Expr::Literal(Literal::Integer(n))
28124 }
28125
28126 fn col(name: &str) -> Expr {
28127 Expr::Column(ColumnName {
28128 qualifier: None,
28129 name: name.into(),
28130 })
28131 }
28132
28133 #[test]
28134 fn select_single_integer() {
28135 let s = parse("SELECT 1");
28136 let Statement::Select(s) = s else {
28137 panic!("expected SELECT")
28138 };
28139 assert_eq!(s.items.len(), 1);
28140 assert!(s.from.is_none());
28141 assert!(s.where_.is_none());
28142 }
28143
28144 #[test]
28145 fn select_multiple_literal_kinds() {
28146 let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
28147 let Statement::Select(s) = s else {
28148 panic!("expected SELECT")
28149 };
28150 assert_eq!(s.items.len(), 5);
28151 }
28152
28153 #[test]
28154 fn select_wildcard_from_table() {
28155 let s = parse("SELECT * FROM users");
28156 let Statement::Select(s) = s else {
28157 panic!("expected SELECT")
28158 };
28159 assert!(matches!(s.items[..], [SelectItem::Wildcard]));
28160 assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
28161 }
28162
28163 #[test]
28164 fn select_with_table_alias() {
28165 let s = parse("SELECT * FROM users AS u");
28166 let Statement::Select(s) = s else {
28167 panic!("expected SELECT")
28168 };
28169 let t = &s.from.as_ref().unwrap().primary;
28170 assert_eq!(t.name, "users");
28171 assert_eq!(t.alias.as_deref(), Some("u"));
28172 }
28173
28174 #[test]
28175 fn select_with_where_eq() {
28176 let s = parse("SELECT a FROM t WHERE a = 1");
28177 let Statement::Select(s) = s else {
28178 panic!("expected SELECT")
28179 };
28180 let w = s.where_.unwrap();
28181 assert_eq!(
28182 w,
28183 Expr::Binary {
28184 lhs: Box::new(col("a")),
28185 op: BinOp::Eq,
28186 rhs: Box::new(lit_int(1)),
28187 }
28188 );
28189 }
28190
28191 #[test]
28192 fn arithmetic_precedence() {
28193 let s = parse("SELECT 1 + 2 * 3");
28194 let Statement::Select(s) = s else {
28195 panic!("expected SELECT")
28196 };
28197 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28198 panic!("wildcard?")
28199 };
28200 assert_eq!(
28201 expr,
28202 &Expr::Binary {
28203 lhs: Box::new(lit_int(1)),
28204 op: BinOp::Add,
28205 rhs: Box::new(Expr::Binary {
28206 lhs: Box::new(lit_int(2)),
28207 op: BinOp::Mul,
28208 rhs: Box::new(lit_int(3)),
28209 }),
28210 }
28211 );
28212 }
28213
28214 #[test]
28215 fn parentheses_override_precedence() {
28216 let s = parse("SELECT (1 + 2) * 3");
28217 let Statement::Select(s) = s else {
28218 panic!("expected SELECT")
28219 };
28220 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28221 panic!()
28222 };
28223 assert_eq!(
28224 expr,
28225 &Expr::Binary {
28226 lhs: Box::new(Expr::Binary {
28227 lhs: Box::new(lit_int(1)),
28228 op: BinOp::Add,
28229 rhs: Box::new(lit_int(2)),
28230 }),
28231 op: BinOp::Mul,
28232 rhs: Box::new(lit_int(3)),
28233 }
28234 );
28235 }
28236
28237 #[test]
28238 fn not_binds_below_comparison() {
28239 // `NOT a = 1` should parse as `NOT (a = 1)`.
28240 let s = parse("SELECT NOT a = 1 FROM t");
28241 let Statement::Select(s) = s else {
28242 panic!("expected SELECT")
28243 };
28244 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28245 panic!()
28246 };
28247 assert_eq!(
28248 expr,
28249 &Expr::Unary {
28250 op: UnOp::Not,
28251 expr: Box::new(Expr::Binary {
28252 lhs: Box::new(col("a")),
28253 op: BinOp::Eq,
28254 rhs: Box::new(lit_int(1)),
28255 }),
28256 }
28257 );
28258 }
28259
28260 #[test]
28261 fn unary_minus_binds_above_multiplication() {
28262 // `-a * 2` should be `(-a) * 2`.
28263 let s = parse("SELECT -a * 2 FROM t");
28264 let Statement::Select(s) = s else {
28265 panic!("expected SELECT")
28266 };
28267 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28268 panic!()
28269 };
28270 assert_eq!(
28271 expr,
28272 &Expr::Binary {
28273 lhs: Box::new(Expr::Unary {
28274 op: UnOp::Neg,
28275 expr: Box::new(col("a")),
28276 }),
28277 op: BinOp::Mul,
28278 rhs: Box::new(lit_int(2)),
28279 }
28280 );
28281 }
28282
28283 #[test]
28284 fn qualified_column() {
28285 let s = parse("SELECT t.col FROM t");
28286 let Statement::Select(s) = s else {
28287 panic!("expected SELECT")
28288 };
28289 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28290 panic!()
28291 };
28292 assert_eq!(
28293 expr,
28294 &Expr::Column(ColumnName {
28295 qualifier: Some("t".into()),
28296 name: "col".into()
28297 })
28298 );
28299 }
28300
28301 #[test]
28302 fn select_item_alias_with_as() {
28303 let s = parse("SELECT a AS y FROM t");
28304 let Statement::Select(s) = s else {
28305 panic!("expected SELECT")
28306 };
28307 let SelectItem::Expr { alias, .. } = &s.items[0] else {
28308 panic!()
28309 };
28310 assert_eq!(alias.as_deref(), Some("y"));
28311 }
28312
28313 #[test]
28314 fn trailing_semicolon_accepted() {
28315 let s = parse("SELECT 1;");
28316 let Statement::Select(s) = s else {
28317 panic!("expected SELECT")
28318 };
28319 assert_eq!(s.items.len(), 1);
28320 }
28321
28322 #[test]
28323 fn boolean_chain_with_and_or_not() {
28324 // (NOT a) OR (b AND (NOT c))
28325 let s = parse("SELECT NOT a OR b AND NOT c FROM t");
28326 let Statement::Select(s) = s else {
28327 panic!("expected SELECT")
28328 };
28329 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28330 panic!()
28331 };
28332 let expected = Expr::Binary {
28333 lhs: Box::new(Expr::Unary {
28334 op: UnOp::Not,
28335 expr: Box::new(col("a")),
28336 }),
28337 op: BinOp::Or,
28338 rhs: Box::new(Expr::Binary {
28339 lhs: Box::new(col("b")),
28340 op: BinOp::And,
28341 rhs: Box::new(Expr::Unary {
28342 op: UnOp::Not,
28343 expr: Box::new(col("c")),
28344 }),
28345 }),
28346 };
28347 assert_eq!(expr, &expected);
28348 }
28349
28350 #[test]
28351 fn empty_input_errors() {
28352 // v7.14.0 — pg_dump preambles emit several comment-only
28353 // / blank-line statements that collapse to Statement::
28354 // Empty rather than a parse error. The old "SELECT in
28355 // message" assertion is stale; verify the new contract:
28356 // empty / whitespace / comment-only input parses to
28357 // Statement::Empty.
28358 assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
28359 assert!(matches!(
28360 parse_statement(" \n\t ").unwrap(),
28361 Statement::Empty
28362 ));
28363 // Sanity: malformed-but-non-empty still errors.
28364 assert!(parse_statement("SELECT FROM WHERE").is_err());
28365 }
28366
28367 #[test]
28368 fn unmatched_paren_errors() {
28369 assert!(parse_statement("SELECT (1 + 2").is_err());
28370 }
28371
28372 #[test]
28373 fn display_round_trip_simple_select() {
28374 let original = parse("SELECT a + 1 FROM t WHERE a > 0");
28375 let text = original.to_string();
28376 let again = parse_statement(&text).expect("re-parse");
28377 assert_eq!(original, again);
28378 }
28379
28380 // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
28381
28382 #[test]
28383 fn create_table_single_column() {
28384 let s = parse("CREATE TABLE foo (a INT)");
28385 let Statement::CreateTable(c) = s else {
28386 panic!("expected CreateTable")
28387 };
28388 assert_eq!(c.name, "foo");
28389 assert_eq!(c.columns.len(), 1);
28390 assert_eq!(c.columns[0].name, "a");
28391 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28392 assert!(c.columns[0].nullable);
28393 }
28394
28395 #[test]
28396 fn create_table_multi_column_with_not_null_mix() {
28397 let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
28398 let Statement::CreateTable(c) = s else {
28399 panic!()
28400 };
28401 assert_eq!(c.columns.len(), 4);
28402 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28403 assert!(!c.columns[0].nullable);
28404 assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
28405 assert!(c.columns[1].nullable);
28406 assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
28407 assert!(!c.columns[2].nullable);
28408 assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
28409 }
28410
28411 #[test]
28412 fn create_table_bigint_supported() {
28413 let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
28414 let Statement::CreateTable(c) = s else {
28415 panic!()
28416 };
28417 assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
28418 }
28419
28420 #[test]
28421 fn create_table_vector_default_is_f32() {
28422 let s = parse("CREATE TABLE t (v VECTOR(128))");
28423 let Statement::CreateTable(c) = s else {
28424 panic!()
28425 };
28426 assert_eq!(
28427 c.columns[0].ty,
28428 ColumnTypeName::Vector {
28429 dim: 128,
28430 encoding: VecEncoding::F32,
28431 },
28432 );
28433 }
28434
28435 #[test]
28436 fn create_table_vector_using_sq8() {
28437 // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
28438 // Case-insensitive on both `USING` and the encoding name.
28439 for sql in [
28440 "CREATE TABLE t (v VECTOR(128) USING SQ8)",
28441 "CREATE TABLE t (v VECTOR(128) using sq8)",
28442 ] {
28443 let s = parse(sql);
28444 let Statement::CreateTable(c) = s else {
28445 panic!()
28446 };
28447 assert_eq!(
28448 c.columns[0].ty,
28449 ColumnTypeName::Vector {
28450 dim: 128,
28451 encoding: VecEncoding::Sq8,
28452 },
28453 "{sql}",
28454 );
28455 }
28456 }
28457
28458 #[test]
28459 fn create_table_vector_using_unknown_errors() {
28460 // v7.16.1 — the inline `USING <encoding>` shape on
28461 // CREATE TABLE column defs was withdrawn before
28462 // v7.14.0 in favour of `CREATE INDEX … USING hnsw
28463 // (col vector_<metric>_ops)`; the parser now rejects
28464 // USING at column-list position with a clearer
28465 // "expected ',' or ')'" message. Test asserts the
28466 // current rejection, not the old "unknown vector
28467 // encoding" string.
28468 let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
28469 assert!(
28470 err.message.contains("USING")
28471 || err.message.contains("using")
28472 || err.message.contains("')'")
28473 || err.message.contains("','"),
28474 "expected USING/column-list rejection, got: {}",
28475 err.message
28476 );
28477 }
28478
28479 #[test]
28480 fn vector_using_sq8_display_roundtrips() {
28481 // The Display impl must produce text that re-parses to the
28482 // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
28483 let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
28484 let Statement::CreateTable(c) = s else {
28485 panic!()
28486 };
28487 assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
28488 }
28489
28490 #[test]
28491 fn parser_recognises_placeholders() {
28492 use crate::ast::{Expr, SelectItem, Statement};
28493 // $N in expression position parses as Expr::Placeholder(N).
28494 let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
28495 let Statement::Select(sel) = s else { panic!() };
28496 assert!(matches!(
28497 sel.items[0],
28498 SelectItem::Expr {
28499 expr: Expr::Placeholder(1),
28500 alias: None
28501 }
28502 ));
28503 // $2 + 1
28504 let SelectItem::Expr {
28505 expr: Expr::Binary { lhs, rhs, .. },
28506 ..
28507 } = &sel.items[1]
28508 else {
28509 panic!()
28510 };
28511 assert!(matches!(**lhs, Expr::Placeholder(2)));
28512 assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
28513 // WHERE x = $3
28514 let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
28515 panic!()
28516 };
28517 assert!(matches!(**rhs, Expr::Placeholder(3)));
28518 }
28519
28520 #[test]
28521 fn parser_rejects_dollar_zero() {
28522 // $0 is not valid in PG; the lexer rejects it.
28523 assert!(parse_statement("SELECT $0").is_err());
28524 }
28525
28526 #[test]
28527 fn placeholder_display_roundtrips() {
28528 // The Display impl must produce text that re-lexes to the
28529 // same Placeholder token.
28530 let s = parse("SELECT $42 FROM t");
28531 let printed = s.to_string();
28532 assert!(printed.contains("$42"));
28533 let again = parse(&printed);
28534 assert_eq!(s, again);
28535 }
28536
28537 #[test]
28538 fn alter_index_rebuild_bare() {
28539 use crate::ast::{AlterIndexTarget, Statement};
28540 let s = parse("ALTER INDEX my_idx REBUILD");
28541 let Statement::AlterIndex(a) = s else {
28542 panic!("expected AlterIndex, got {s:?}")
28543 };
28544 assert_eq!(a.name, "my_idx");
28545 assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
28546 }
28547
28548 #[test]
28549 fn alter_index_rebuild_with_encoding() {
28550 use crate::ast::{AlterIndexTarget, Statement};
28551 for (sql, want) in [
28552 (
28553 "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
28554 VecEncoding::F32,
28555 ),
28556 (
28557 "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
28558 VecEncoding::Sq8,
28559 ),
28560 (
28561 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28562 VecEncoding::F16,
28563 ),
28564 ] {
28565 let s = parse(sql);
28566 let Statement::AlterIndex(a) = s else {
28567 panic!("{sql}: expected AlterIndex")
28568 };
28569 assert_eq!(a.name, "my_idx");
28570 assert_eq!(
28571 a.target,
28572 AlterIndexTarget::Rebuild {
28573 encoding: Some(want)
28574 },
28575 "{sql}"
28576 );
28577 }
28578 }
28579
28580 #[test]
28581 fn alter_index_rebuild_unknown_encoding_errors() {
28582 let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
28583 assert!(
28584 err.message.contains("unknown vector encoding"),
28585 "got: {}",
28586 err.message
28587 );
28588 }
28589
28590 #[test]
28591 fn alter_index_rebuild_display_roundtrips() {
28592 for (input, want) in [
28593 ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
28594 (
28595 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28596 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28597 ),
28598 (
28599 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28600 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28601 ),
28602 ] {
28603 let s = parse(input);
28604 assert_eq!(s.to_string(), want);
28605 }
28606 }
28607
28608 #[test]
28609 fn create_table_unknown_type_defers_to_engine() {
28610 // v4.9 picked XML as a parse-time "unsupported column
28611 // type" probe. v7.17.0 Phase 1.4 changed the contract:
28612 // an unknown type ident parses as Text + `user_type_ref`
28613 // so CREATE TABLE can resolve user-defined enum / domain
28614 // types — rejection of truly-unknown types moved to the
28615 // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
28616 // to a first-class built-in, so this probe switched to a
28617 // synthetic name nothing in the lexer will ever recognise.
28618 let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
28619 let Statement::CreateTable(t) = stmt else {
28620 panic!("expected CreateTable");
28621 };
28622 assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
28623 }
28624
28625 #[test]
28626 fn create_table_missing_table_keyword_errors() {
28627 assert!(parse_statement("CREATE x (a INT)").is_err());
28628 }
28629
28630 // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
28631 // `PARTITION OF parent <bounds>` child parse + Display round-trip.
28632
28633 #[test]
28634 fn parse_create_table_partition_by_range() {
28635 use crate::ast::{PartitionBySpec, PartitionKindAst};
28636 let stmt = parse_statement(
28637 "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
28638 payload JSONB) PARTITION BY RANGE (ts)",
28639 )
28640 .unwrap();
28641 let Statement::CreateTable(t) = stmt else {
28642 panic!("expected CreateTable");
28643 };
28644 assert!(t.partition_of.is_none(), "parent has no partition_of");
28645 assert_eq!(t.columns.len(), 3);
28646 let by = t.partition_by.as_ref().expect("expected PARTITION BY");
28647 assert_eq!(
28648 by,
28649 &PartitionBySpec {
28650 kind: PartitionKindAst::Range,
28651 key_columns: alloc::vec!["ts".to_string()],
28652 }
28653 );
28654 // Display round-trip preserves the suffix. `quote_ident`
28655 // only adds double quotes when the ident needs escaping, so
28656 // a plain `ts` survives bare here.
28657 assert!(
28658 t.to_string().contains("PARTITION BY RANGE (ts)"),
28659 "Display lost PARTITION BY suffix: {t}"
28660 );
28661 }
28662
28663 #[test]
28664 fn parse_create_table_partition_of_range() {
28665 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
28666 let stmt = parse_statement(
28667 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
28668 FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
28669 )
28670 .unwrap();
28671 let Statement::CreateTable(t) = stmt else {
28672 panic!("expected CreateTable");
28673 };
28674 assert!(t.columns.is_empty(), "child inherits columns from parent");
28675 assert!(t.partition_by.is_none());
28676 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
28677 assert_eq!(of.parent_name, "events_partitioned");
28678 let PartitionOfSpec { bounds, .. } = of.clone();
28679 match bounds {
28680 PartitionOfBoundsAst::Range { lower, upper } => {
28681 assert!(lower.to_string().contains("2026-06-01"));
28682 assert!(upper.to_string().contains("2026-07-01"));
28683 }
28684 other => panic!("expected Range, got {other:?}"),
28685 }
28686 // Display round-trip emits the FOR VALUES tail. `quote_ident`
28687 // skips quotes when not required, so the parent name appears
28688 // bare here.
28689 let s = t.to_string();
28690 assert!(
28691 s.contains("PARTITION OF events_partitioned"),
28692 "Display lost PARTITION OF: {s}"
28693 );
28694 assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
28695 assert!(s.contains(") TO ("), "Display lost TO: {s}");
28696 }
28697
28698 #[test]
28699 fn parse_create_table_partition_of_default() {
28700 use crate::ast::PartitionOfBoundsAst;
28701 let stmt =
28702 parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
28703 .unwrap();
28704 let Statement::CreateTable(t) = stmt else {
28705 panic!("expected CreateTable");
28706 };
28707 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
28708 assert_eq!(of.parent_name, "events_partitioned");
28709 assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
28710 assert!(
28711 t.to_string()
28712 .contains("PARTITION OF events_partitioned DEFAULT"),
28713 "Display lost DEFAULT: {t}"
28714 );
28715 }
28716
28717 #[test]
28718 fn parse_create_table_partition_by_list() {
28719 // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
28720 // child with `FOR VALUES IN (lit, lit, …)`.
28721 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28722 let parent =
28723 parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
28724 .unwrap();
28725 let Statement::CreateTable(t) = parent else {
28726 panic!("expected CreateTable");
28727 };
28728 let Some(PartitionBySpec {
28729 kind,
28730 ref key_columns,
28731 }) = t.partition_by
28732 else {
28733 panic!("expected PARTITION BY");
28734 };
28735 assert_eq!(kind, PartitionKindAst::List);
28736 assert_eq!(*key_columns, vec!["region".to_string()]);
28737 assert!(t.to_string().contains("PARTITION BY LIST (region)"));
28738
28739 let child = parse_statement(
28740 "CREATE TABLE events_apac PARTITION OF events_listed \
28741 FOR VALUES IN ('jp', 'kr', 'tw')",
28742 )
28743 .unwrap();
28744 let Statement::CreateTable(c) = child else {
28745 panic!("expected CreateTable");
28746 };
28747 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28748 let PartitionOfBoundsAst::List { values } = &of.bounds else {
28749 panic!("expected List bounds, got {:?}", of.bounds);
28750 };
28751 assert_eq!(values.len(), 3);
28752 let disp = c.to_string();
28753 assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
28754 }
28755
28756 #[test]
28757 fn parse_create_table_partition_by_hash() {
28758 // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
28759 // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
28760 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28761 let parent =
28762 parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
28763 let Statement::CreateTable(t) = parent else {
28764 panic!("expected CreateTable");
28765 };
28766 let Some(PartitionBySpec {
28767 kind,
28768 ref key_columns,
28769 }) = t.partition_by
28770 else {
28771 panic!("expected PARTITION BY");
28772 };
28773 assert_eq!(kind, PartitionKindAst::Hash);
28774 assert_eq!(*key_columns, vec!["id".to_string()]);
28775 assert!(t.to_string().contains("PARTITION BY HASH (id)"));
28776
28777 let child = parse_statement(
28778 "CREATE TABLE orders_h_0 PARTITION OF orders_h \
28779 FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
28780 )
28781 .unwrap();
28782 let Statement::CreateTable(c) = child else {
28783 panic!("expected CreateTable");
28784 };
28785 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28786 let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
28787 panic!("expected Hash bounds");
28788 };
28789 assert_eq!(modulus, 4);
28790 assert_eq!(remainder, 0);
28791 let disp = c.to_string();
28792 assert!(
28793 disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
28794 "Display lost HASH bounds: {disp}"
28795 );
28796
28797 // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
28798 let bad = parse_statement(
28799 "CREATE TABLE orders_h_bad PARTITION OF orders_h \
28800 FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
28801 );
28802 let msg = format!("{}", bad.unwrap_err());
28803 assert!(
28804 msg.contains("REMAINDER") && msg.contains("MODULUS"),
28805 "expected REMAINDER/MODULUS validation error: {msg}"
28806 );
28807 }
28808
28809 #[test]
28810 fn parse_create_table_partition_of_rejects_columns() {
28811 // v7.37.6-B contract: PARTITION OF children inherit columns
28812 // from the parent; an explicit list MUST surface as a parse
28813 // error rather than getting silently ignored.
28814 let err = parse_statement(
28815 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
28816 FOR VALUES FROM ('a') TO ('b')",
28817 );
28818 assert!(err.is_err(), "expected parse error for explicit columns");
28819 let msg = format!("{}", err.unwrap_err());
28820 assert!(
28821 msg.contains("PARTITION OF") && msg.contains("column"),
28822 "error should mention PARTITION OF + columns: {msg}"
28823 );
28824 }
28825
28826 #[test]
28827 fn insert_single_value() {
28828 let s = parse("INSERT INTO foo VALUES (42)");
28829 let Statement::Insert(i) = s else {
28830 panic!("expected Insert")
28831 };
28832 assert_eq!(i.table, "foo");
28833 assert_eq!(i.rows.len(), 1);
28834 assert_eq!(i.rows[0].len(), 1);
28835 assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
28836 }
28837
28838 #[test]
28839 fn insert_multi_value_with_mixed_literals() {
28840 let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
28841 let Statement::Insert(i) = s else { panic!() };
28842 assert_eq!(i.rows.len(), 1);
28843 assert_eq!(i.rows[0].len(), 5);
28844 }
28845
28846 #[test]
28847 fn insert_missing_into_errors() {
28848 assert!(parse_statement("INSERT foo VALUES (1)").is_err());
28849 }
28850
28851 #[test]
28852 fn create_table_round_trip() {
28853 let original =
28854 parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
28855 let text = original.to_string();
28856 let again = parse_statement(&text).expect("re-parse");
28857 assert_eq!(original, again);
28858 }
28859
28860 #[test]
28861 fn insert_round_trip_with_negation_and_string() {
28862 let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
28863 let text = original.to_string();
28864 let again = parse_statement(&text).expect("re-parse");
28865 assert_eq!(original, again);
28866 }
28867
28868 #[test]
28869 fn unknown_keyword_at_statement_start_errors() {
28870 // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
28871 // the top-level dispatch still has no branch to take.
28872 let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
28873 assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
28874 }
28875
28876 // --- v0.8 CREATE INDEX --------------------------------------------------
28877
28878 #[test]
28879 fn create_index_basic() {
28880 let s = parse("CREATE INDEX idx_id ON users (id)");
28881 let Statement::CreateIndex(c) = s else {
28882 panic!("expected CreateIndex")
28883 };
28884 assert_eq!(c.name, "idx_id");
28885 assert_eq!(c.table, "users");
28886 assert_eq!(c.column, "id");
28887 }
28888
28889 #[test]
28890 fn create_index_missing_on_errors() {
28891 assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
28892 }
28893
28894 #[test]
28895 fn create_index_missing_paren_errors() {
28896 assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
28897 }
28898
28899 #[test]
28900 fn create_index_round_trip() {
28901 let original = parse("CREATE INDEX by_name ON users (name)");
28902 let again = parse_statement(&original.to_string()).unwrap();
28903 assert_eq!(original, again);
28904 }
28905
28906 // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
28907
28908 #[test]
28909 fn create_unique_index_basic() {
28910 let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
28911 let Statement::CreateIndex(c) = s else {
28912 panic!("expected CreateIndex");
28913 };
28914 assert!(c.is_unique);
28915 assert_eq!(c.column, "a");
28916 assert!(c.partial_predicate.is_none());
28917 }
28918
28919 #[test]
28920 fn create_unique_index_partial() {
28921 // mailrs's email_templates "one default per user" shape.
28922 let s = parse(
28923 "CREATE UNIQUE INDEX idx_email_templates_user_default \
28924 ON email_templates (user_address) WHERE is_default = true",
28925 );
28926 let Statement::CreateIndex(c) = s else {
28927 panic!("expected CreateIndex");
28928 };
28929 assert!(c.is_unique);
28930 assert_eq!(c.table, "email_templates");
28931 assert_eq!(c.column, "user_address");
28932 assert!(c.partial_predicate.is_some());
28933 }
28934
28935 #[test]
28936 fn create_unique_index_composite_with_predicate() {
28937 // mailrs's calendar_events instance: composite columns.
28938 let s = parse(
28939 "CREATE UNIQUE INDEX uq_calendar_events_instance \
28940 ON calendar_events (calendar_id, uid, recurrence_id) \
28941 WHERE recurrence_id IS NOT NULL",
28942 );
28943 let Statement::CreateIndex(c) = s else {
28944 panic!("expected CreateIndex");
28945 };
28946 assert!(c.is_unique);
28947 assert_eq!(c.column, "calendar_id");
28948 assert_eq!(
28949 c.extra_columns,
28950 vec!["uid".to_string(), "recurrence_id".to_string()]
28951 );
28952 assert!(c.partial_predicate.is_some());
28953 }
28954
28955 #[test]
28956 fn create_unique_index_using_btree_ok() {
28957 let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
28958 assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
28959 }
28960
28961 #[test]
28962 fn create_unique_index_using_hnsw_rejected() {
28963 let err =
28964 parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
28965 assert!(err.message.contains("UNIQUE"), "{}", err.message);
28966 }
28967
28968 #[test]
28969 fn create_unique_index_round_trip() {
28970 let original = parse(
28971 "CREATE UNIQUE INDEX uq_calendar_events_master \
28972 ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
28973 );
28974 let again = parse_statement(&original.to_string()).unwrap();
28975 assert_eq!(original, again);
28976 }
28977
28978 #[test]
28979 fn create_unique_without_index_errors() {
28980 let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
28981 // v7.39 (round 340, V56) — PG 18.4, verbatim.
28982 assert_eq!(err.message, "syntax error at or near \"TABLE\"");
28983 }
28984
28985 // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
28986
28987 #[test]
28988 fn create_table_bytea_column() {
28989 let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
28990 let Statement::CreateTable(c) = s else {
28991 panic!("expected CreateTable");
28992 };
28993 assert_eq!(c.columns.len(), 2);
28994 assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
28995 assert!(!c.columns[1].nullable);
28996 }
28997
28998 #[test]
28999 fn create_table_bytes_alias_column() {
29000 let s = parse("CREATE TABLE t (blob BYTES)");
29001 let Statement::CreateTable(c) = s else {
29002 panic!("expected CreateTable");
29003 };
29004 assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
29005 }
29006
29007 #[test]
29008 fn bytea_round_trip_display() {
29009 let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
29010 let again = parse_statement(&original.to_string()).unwrap();
29011 assert_eq!(original, again);
29012 }
29013
29014 // --- v0.9 transactions -------------------------------------------------
29015
29016 #[test]
29017 fn begin_commit_rollback_parse_as_unit_variants() {
29018 let plain = crate::ast::TransactionModes::default();
29019 assert_eq!(parse("BEGIN"), Statement::Begin(plain));
29020 assert_eq!(parse("COMMIT"), Statement::Commit);
29021 // r1066 — PG synonyms pgbench's tpcb script relies on.
29022 assert_eq!(parse("END"), Statement::Commit);
29023 assert_eq!(parse("END TRANSACTION"), Statement::Commit);
29024 assert_eq!(parse("COMMIT WORK"), Statement::Commit);
29025 assert_eq!(parse("ROLLBACK"), Statement::Rollback);
29026 // Trailing semicolons accepted too.
29027 assert_eq!(parse("BEGIN;"), Statement::Begin(plain));
29028 // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
29029 // statement (with or without the WORK/TRANSACTION noise word).
29030 assert_eq!(
29031 parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
29032 Statement::Begin(crate::ast::TransactionModes {
29033 isolation: Some(IsolationLevel::RepeatableRead),
29034 read_only: None,
29035 })
29036 );
29037 assert_eq!(
29038 parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
29039 Statement::Begin(crate::ast::TransactionModes {
29040 isolation: Some(IsolationLevel::Serializable),
29041 read_only: None,
29042 })
29043 );
29044 // v7.39 — this line used to read
29045 //
29046 // // A non-isolation mode keeps the session default (None).
29047 // assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
29048 //
29049 // which pinned the defect rather than catching it: the READ ONLY
29050 // was thrown away, so the statement opened an ordinary read-write
29051 // transaction and every write inside it was accepted. The
29052 // isolation level is still absent here, because this statement
29053 // does not name one — that part was right.
29054 assert_eq!(
29055 parse("BEGIN READ ONLY"),
29056 Statement::Begin(crate::ast::TransactionModes {
29057 isolation: None,
29058 read_only: Some(true),
29059 })
29060 );
29061 assert_eq!(
29062 parse("START TRANSACTION READ WRITE"),
29063 Statement::Begin(crate::ast::TransactionModes {
29064 isolation: None,
29065 read_only: Some(false),
29066 })
29067 );
29068 assert_eq!(
29069 parse("BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY"),
29070 Statement::Begin(crate::ast::TransactionModes {
29071 isolation: Some(IsolationLevel::Serializable),
29072 read_only: Some(true),
29073 })
29074 );
29075 }
29076
29077 // --- v1.2: pgvector distance ops + ::vector cast --------------------
29078
29079 #[test]
29080 fn inner_product_binop_parses() {
29081 let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
29082 let Statement::Select(s) = s else { panic!() };
29083 let SelectItem::Expr { expr, .. } = &s.items[0] else {
29084 panic!()
29085 };
29086 assert!(matches!(
29087 expr,
29088 Expr::Binary {
29089 op: BinOp::InnerProduct,
29090 ..
29091 }
29092 ));
29093 }
29094
29095 #[test]
29096 fn cosine_distance_binop_parses() {
29097 let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
29098 let Statement::Select(s) = s else { panic!() };
29099 let SelectItem::Expr { expr, .. } = &s.items[0] else {
29100 panic!()
29101 };
29102 assert!(matches!(
29103 expr,
29104 Expr::Binary {
29105 op: BinOp::CosineDistance,
29106 ..
29107 }
29108 ));
29109 }
29110
29111 #[test]
29112 fn vector_cast_postfix_wraps_string_literal() {
29113 let s = parse("SELECT '[1,2,3]'::vector FROM t");
29114 let Statement::Select(s) = s else { panic!() };
29115 let SelectItem::Expr { expr, .. } = &s.items[0] else {
29116 panic!()
29117 };
29118 assert!(matches!(
29119 expr,
29120 Expr::Cast {
29121 target: CastTarget::Vector,
29122 ..
29123 }
29124 ));
29125 }
29126
29127 #[test]
29128 fn unsupported_cast_target_errors() {
29129 // v7.37.5 ship triage promoted the parser to accept every
29130 // ident as a `CastTarget::Named(canonical)`; the engine
29131 // surfaces the "unsupported cast target" error at eval
29132 // time when `type_name_to_data_type` can't resolve it.
29133 // Parser-side error now requires a NON-ident after `::`
29134 // (e.g. a punctuation token).
29135 let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
29136 assert_eq!(err.message, "syntax error at or near \",\"");
29137 }
29138
29139 #[test]
29140 fn tx_statements_round_trip() {
29141 for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
29142 let original = parse(q);
29143 let again = parse_statement(&original.to_string()).unwrap();
29144 assert_eq!(original, again);
29145 }
29146 }
29147
29148 #[test]
29149 fn interval_text_parsing_units() {
29150 // v7.37.5 β — three-field shape `(months, days, micros)` so
29151 // `'1 day'` and `'24 hours'` no longer collide (PG parity).
29152 // Single unit.
29153 assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
29154 assert_eq!(
29155 parse_interval_text("24 hours"),
29156 Some((0, 0, 86_400_000_000))
29157 );
29158 assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
29159 assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
29160 assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
29161 assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
29162 // Compound spans accumulate per-dimension.
29163 assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
29164 assert_eq!(
29165 parse_interval_text("1 day 2 hours"),
29166 Some((0, 1, 7_200_000_000))
29167 );
29168 // Negative numbers carry through per-dimension.
29169 assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
29170 // Bad shapes return None.
29171 assert_eq!(parse_interval_text(""), None);
29172 assert_eq!(parse_interval_text("garbage"), None);
29173 assert_eq!(parse_interval_text("1 fortnight"), None);
29174 // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
29175 // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
29176 assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
29177 assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
29178 assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
29179 }
29180
29181 #[test]
29182 fn interval_literal_roundtrips_via_display() {
29183 let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
29184 let s = parsed.to_string();
29185 // Display preserves the original text verbatim.
29186 assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
29187 // And re-parsing yields a structurally equal statement.
29188 let again = parse_statement(&s).unwrap();
29189 assert_eq!(parsed, again);
29190 }
29191
29192 // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
29193
29194 #[test]
29195 fn parser_recognises_create_publication_bare() {
29196 let s = parse("CREATE PUBLICATION pub_a");
29197 let Statement::CreatePublication(p) = s else {
29198 panic!("expected CreatePublication, got {s:?}")
29199 };
29200 assert_eq!(p.name, "pub_a");
29201 assert_eq!(p.scope, PublicationScope::AllTables);
29202 }
29203
29204 #[test]
29205 fn parser_recognises_create_publication_for_all_tables() {
29206 let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
29207 let Statement::CreatePublication(p) = s else {
29208 panic!("expected CreatePublication, got {s:?}")
29209 };
29210 assert_eq!(p.name, "pub_a");
29211 assert_eq!(p.scope, PublicationScope::AllTables);
29212 }
29213
29214 #[test]
29215 fn parser_recognises_drop_publication() {
29216 let s = parse("DROP PUBLICATION pub_a");
29217 let Statement::DropPublication { name, .. } = s else {
29218 panic!("expected DropPublication, got {s:?}")
29219 };
29220 assert_eq!(name, "pub_a");
29221 }
29222
29223 #[test]
29224 fn parser_recognises_for_table_list() {
29225 let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
29226 let Statement::CreatePublication(p) = s else {
29227 panic!("expected CreatePublication, got {s:?}")
29228 };
29229 assert_eq!(p.name, "pub_a");
29230 let PublicationScope::ForTables(ts) = p.scope else {
29231 panic!("expected ForTables scope")
29232 };
29233 assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
29234 }
29235
29236 #[test]
29237 fn parser_rejects_bare_for_tables_and_takes_in_schema() {
29238 // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
29239 // is rejected (`invalid publication object list`; the old
29240 // test pinned an unverifiable "PG 19 accepts both" claim);
29241 // TABLES pairs with IN SCHEMA.
29242 let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
29243 .expect_err("bare FOR TABLES must reject");
29244 assert!(
29245 alloc::format!("{err}").contains("invalid publication object list"),
29246 "got: {err}"
29247 );
29248 let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
29249 let Statement::CreatePublication(p) = s else {
29250 panic!("expected CreatePublication, got {s:?}")
29251 };
29252 let PublicationScope::TablesInSchema(schema) = p.scope else {
29253 panic!("expected TablesInSchema")
29254 };
29255 assert_eq!(schema, "public");
29256 }
29257
29258 #[test]
29259 fn parser_recognises_for_all_tables_except_list() {
29260 let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
29261 let Statement::CreatePublication(p) = s else {
29262 panic!()
29263 };
29264 let PublicationScope::AllTablesExcept(ts) = p.scope else {
29265 panic!("expected AllTablesExcept")
29266 };
29267 assert_eq!(ts, alloc::vec!["t1", "t2"]);
29268 }
29269
29270 #[test]
29271 fn parser_rejects_for_table_with_empty_list() {
29272 // `FOR TABLE` with nothing after is a parse error.
29273 let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
29274 .expect_err("must error on empty list");
29275 // No specific message asserted — the call falls through to
29276 // expect_ident_like which yields "expected identifier, got …".
29277 assert!(!err.message.is_empty());
29278 }
29279
29280 #[test]
29281 fn parser_recognises_show_publications() {
29282 // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
29283 // bare ident in this position, NOT a reserved keyword.
29284 let s = parse("SHOW PUBLICATIONS");
29285 assert!(matches!(s, Statement::ShowPublications));
29286 }
29287
29288 // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
29289
29290 #[test]
29291 fn parser_recognises_create_subscription_single_publication() {
29292 let s = parse(
29293 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
29294 );
29295 let Statement::CreateSubscription(c) = s else {
29296 panic!("expected CreateSubscription, got {s:?}")
29297 };
29298 assert_eq!(c.name, "sub_a");
29299 assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
29300 assert_eq!(c.publications, alloc::vec!["pub_a"]);
29301 }
29302
29303 #[test]
29304 fn parser_recognises_create_subscription_multi_publication() {
29305 let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
29306 let Statement::CreateSubscription(c) = s else {
29307 panic!()
29308 };
29309 assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
29310 }
29311
29312 #[test]
29313 fn parser_rejects_create_subscription_missing_connection() {
29314 let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
29315 .expect_err("must error on missing CONNECTION");
29316 assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
29317 }
29318
29319 #[test]
29320 fn parser_rejects_create_subscription_missing_publication() {
29321 let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
29322 .expect_err("must error on missing PUBLICATION");
29323 assert_eq!(err.message, "syntax error at end of input");
29324 }
29325
29326 #[test]
29327 fn parser_recognises_drop_subscription() {
29328 let s = parse("DROP SUBSCRIPTION sub_a");
29329 let Statement::DropSubscription { name, .. } = s else {
29330 panic!("expected DropSubscription, got {s:?}")
29331 };
29332 assert_eq!(name, "sub_a");
29333 }
29334
29335 #[test]
29336 fn parser_recognises_show_subscriptions() {
29337 let s = parse("SHOW SUBSCRIPTIONS");
29338 assert!(matches!(s, Statement::ShowSubscriptions));
29339 }
29340
29341 #[test]
29342 fn parser_recognises_wait_for_wal_position_no_timeout() {
29343 let s = parse("WAIT FOR WAL POSITION 12345");
29344 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29345 panic!("expected WaitForWalPosition, got {s:?}")
29346 };
29347 assert_eq!(pos, 12345);
29348 assert!(timeout_ms.is_none());
29349 }
29350
29351 #[test]
29352 fn parser_recognises_wait_for_wal_position_with_timeout() {
29353 let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
29354 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29355 panic!()
29356 };
29357 assert_eq!(pos, 67890);
29358 assert_eq!(timeout_ms, Some(5000));
29359 }
29360
29361 #[test]
29362 fn parser_rejects_wait_with_negative_position() {
29363 // The lexer treats `-` as a token; `expect_u64_literal`
29364 // only sees the Integer that follows, so the negative
29365 // arrives as a unary-minus expression at higher levels.
29366 // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
29367 // parse error one way or another.
29368 let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
29369 assert!(!err.message.is_empty());
29370 }
29371
29372 #[test]
29373 fn parser_recognises_bare_analyze() {
29374 let s = parse("ANALYZE");
29375 assert!(matches!(s, Statement::Analyze(None)));
29376 }
29377
29378 #[test]
29379 fn parser_recognises_analyze_with_table() {
29380 let s = parse("ANALYZE users");
29381 let Statement::Analyze(Some(name)) = s else {
29382 panic!("expected Analyze, got {s:?}")
29383 };
29384 assert_eq!(name, "users");
29385 }
29386
29387 #[test]
29388 fn parser_recognises_analyze_with_quoted_table() {
29389 let s = parse("ANALYZE \"Mixed Case\"");
29390 let Statement::Analyze(Some(name)) = s else {
29391 panic!()
29392 };
29393 assert_eq!(name, "Mixed Case");
29394 }
29395
29396 #[test]
29397 fn parser_rejects_analyze_with_garbage_token() {
29398 let err = parse_statement("ANALYZE 42").expect_err("must error");
29399 assert!(!err.message.is_empty());
29400 }
29401
29402 #[test]
29403 fn analyze_display_roundtrips() {
29404 for sql in ["ANALYZE", "ANALYZE users"] {
29405 let s = parse(sql);
29406 let printed = s.to_string();
29407 let again = parse_statement(&printed)
29408 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29409 assert_eq!(s, again);
29410 }
29411 }
29412
29413 #[test]
29414 fn wait_for_display_roundtrips() {
29415 for sql in [
29416 "WAIT FOR WAL POSITION 12345",
29417 "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
29418 ] {
29419 let s = parse(sql);
29420 let printed = s.to_string();
29421 let again = parse_statement(&printed)
29422 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29423 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29424 }
29425 }
29426
29427 #[test]
29428 fn subscription_ddl_display_roundtrips() {
29429 for sql in [
29430 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
29431 "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
29432 "DROP SUBSCRIPTION sub_a",
29433 "SHOW SUBSCRIPTIONS",
29434 ] {
29435 let s = parse(sql);
29436 let printed = s.to_string();
29437 let again = parse_statement(&printed)
29438 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29439 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29440 }
29441 }
29442
29443 #[test]
29444 fn parser_drop_dispatches_user_vs_publication() {
29445 // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
29446 // tokenises DROP. Both targets must still parse.
29447 let s = parse("DROP USER 'alice'");
29448 let Statement::DropUser { name, .. } = s else {
29449 panic!("expected DropUser, got {s:?}")
29450 };
29451 assert_eq!(name, "alice");
29452 // And DROP PUBLICATION lands the new variant.
29453 let s = parse("DROP PUBLICATION p1");
29454 assert!(matches!(s, Statement::DropPublication { .. }));
29455 }
29456
29457 #[test]
29458 fn publication_ddl_display_roundtrips() {
29459 // Every CREATE PUBLICATION variant must Display → parse →
29460 // same AST. v6.1.3 covers all three scope shapes.
29461 for sql in [
29462 "CREATE PUBLICATION pub_a",
29463 "CREATE PUBLICATION pub_a FOR ALL TABLES",
29464 "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
29465 "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
29466 "DROP PUBLICATION pub_a",
29467 "SHOW PUBLICATIONS",
29468 ] {
29469 let s = parse(sql);
29470 let printed = s.to_string();
29471 let again = parse_statement(&printed)
29472 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29473 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29474 }
29475 }
29476
29477 // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
29478
29479 #[test]
29480 fn create_function_returns_trigger_plpgsql_minimal() {
29481 let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
29482 let s = parse(sql);
29483 let Statement::CreateFunction(f) = s else {
29484 panic!("expected CreateFunction");
29485 };
29486 assert_eq!(f.name, "noop");
29487 assert!(!f.or_replace);
29488 assert!(f.args.is_empty());
29489 assert!(matches!(f.returns, FunctionReturn::Trigger));
29490 assert_eq!(f.language, "plpgsql");
29491 let FunctionBody::PlPgSql(block) = f.body else {
29492 panic!("expected PlPgSql body");
29493 };
29494 assert_eq!(block.statements.len(), 1);
29495 assert!(matches!(
29496 block.statements[0],
29497 PlPgSqlStmt::Return(ReturnTarget::New)
29498 ));
29499 }
29500
29501 #[test]
29502 fn create_function_or_replace_with_assignment() {
29503 // mailrs-shape trigger function: NEW.col := to_tsvector(...);
29504 // RETURN NEW.
29505 let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
29506BEGIN
29507 NEW.search_vector := to_tsvector('english', NEW.subject);
29508 RETURN NEW;
29509END;
29510$$";
29511 let s = parse(sql);
29512 let Statement::CreateFunction(f) = s else {
29513 panic!("expected CreateFunction");
29514 };
29515 assert!(f.or_replace);
29516 let FunctionBody::PlPgSql(block) = &f.body else {
29517 panic!("expected PlPgSql body");
29518 };
29519 assert_eq!(block.statements.len(), 2);
29520 // First statement: NEW.search_vector := to_tsvector(...)
29521 let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
29522 panic!("expected Assign as first stmt");
29523 };
29524 match target {
29525 AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
29526 other => panic!("expected NEW.col, got {other:?}"),
29527 }
29528 // Second statement: RETURN NEW
29529 assert!(matches!(
29530 block.statements[1],
29531 PlPgSqlStmt::Return(ReturnTarget::New)
29532 ));
29533 }
29534
29535 #[test]
29536 fn create_trigger_after_insert_or_update() {
29537 let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
29538 let s = parse(sql);
29539 let Statement::CreateTrigger(t) = s else {
29540 panic!("expected CreateTrigger");
29541 };
29542 assert_eq!(t.name, "tg");
29543 assert_eq!(t.table, "messages");
29544 assert_eq!(t.timing, TriggerTiming::After);
29545 assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
29546 assert_eq!(t.for_each, TriggerForEach::Row);
29547 assert_eq!(t.function, "update_sv");
29548 }
29549
29550 #[test]
29551 fn create_trigger_before_delete_execute_procedure_alias() {
29552 // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
29553 let sql =
29554 "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
29555 let s = parse(sql);
29556 let Statement::CreateTrigger(t) = s else {
29557 panic!("expected CreateTrigger");
29558 };
29559 assert_eq!(t.timing, TriggerTiming::Before);
29560 assert_eq!(t.events, vec![TriggerEvent::Delete]);
29561 }
29562
29563 #[test]
29564 fn drop_trigger_if_exists_round_trips() {
29565 // No parser support for DROP TRIGGER yet — added in v7.12.5
29566 // alongside the broader DROP …{IF EXISTS} cleanup. The
29567 // AST + Display impls are in place so we round-trip via
29568 // construction:
29569 let s = Statement::DropTrigger {
29570 name: "tg".into(),
29571 table: "messages".into(),
29572 if_exists: true,
29573 };
29574 assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
29575 }
29576
29577 #[test]
29578 fn trigger_ddl_display_roundtrips_through_parser() {
29579 // CREATE TRIGGER + its referenced CREATE FUNCTION must
29580 // Display → parse → same AST (modulo PL/pgSQL body
29581 // formatting which is parser-canonicalised).
29582 for sql in [
29583 "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
29584 "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
29585 ] {
29586 let s = parse(sql);
29587 let printed = s.to_string();
29588 let again = parse_statement(&printed)
29589 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29590 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29591 }
29592 }
29593}