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 | "use"
73 // PG psql backslash meta-commands that newer
74 // pg_dump versions emit unescaped (\restrict /
75 // \unrestrict). Real psql intercepts these; SPG's
76 // PG-wire sees them as raw text.
77 | "\\restrict"
78 | "\\unrestrict"
79 // v7.17.0 Phase 4.1 — MySQL `DELIMITER //` and
80 // `DELIMITER ;` directives. Technically client-side
81 // (the `mysql` CLI uses them to set the statement
82 // terminator), not SQL — but mysqldump and stored-
83 // procedure scripts emit them inline. SPG's parser
84 // sees one statement at a time and doesn't care
85 // about the terminator, so consume DELIMITER lines
86 // as Empty.
87 | "delimiter"
88 // v7.37.17 (17.6 siblings) — additional PG maintenance /
89 // session-state statements pg_dump + application startup
90 // scripts emit. SPG has no matching session-state to
91 // discard (no prepared-plan cache surface, no temp
92 // sequences), no matching security-label / storage-
93 // option to apply, no separate CREATE/DROP CAST that
94 // affects execution.
95 // v7.37.17 (17.6 siblings) — PG role-cleanup statements
96 // pg_dump / pg_dumpall emit around DROP ROLE:
97 // REASSIGN OWNED BY <role> [, ...] TO <newrole>
98 // DROP OWNED BY <role> [, ...] [CASCADE | RESTRICT]
99 // Both operate on the role's owned objects; SPG has no
100 // role-owner model, so accept-and-no-op.
101 // v7.37.17 (17.6 sibling) — LOAD '<library>'. pg_dump
102 // + extension scripts use LOAD to preload shared
103 // libraries. SPG doesn't have a shared-library extension
104 // point today (extensions ship as first-class crates
105 // linked at build time); accept as a no-op.
106 | "load"
107 )
108}
109
110/// v7.37.43-T4 — PG-unreserved keywords that are legal identifiers
111/// per `pg_get_keywords()`. SPG tokenizes these as named variants
112/// so the parser can dispatch on them in their owning contexts
113/// (`RELEASE SAVEPOINT`, `SHOW name`, `BEGIN`/`COMMIT`/`ROLLBACK`,
114/// `CREATE INDEX`, etc.), but they MUST stay usable as table /
115/// column / alias names — that's the PG contract for unreserved
116/// keywords (see PG docs Appendix C.1).
117///
118/// Before this generalisation, sentori migration 0001_init.sql
119/// `release TEXT NOT NULL` blew up the parser with "expected
120/// identifier, got Release", and the same gap stalked every
121/// SPG drop-in user whose schema had a column / alias named
122/// `release` / `index` / `tables` / `show` / `savepoint` /
123/// `begin` / `commit` / `rollback` / `drop` / `insert` / `values`
124/// / `limit` / `partition`. PG accepts all of them as identifiers
125/// when unquoted, so SPG must too.
126///
127/// Returns the canonical lowercase identifier text when the token
128/// belongs to PG's unreserved class, `None` otherwise. Used by
129/// `expect_ident_like` (column / table / alias names) so the
130/// generalisation applies everywhere an identifier may appear,
131/// not just in the contexts these tokens were introduced for.
132fn unreserved_keyword_text(tok: &Token) -> Option<String> {
133 let s = match tok {
134 // PG keyword class: unreserved or col_name.
135 //
136 Token::Release => "release",
137 Token::Savepoint => "savepoint",
138 Token::Show => "show",
139 Token::Index => "index",
140 Token::Begin => "begin",
141 Token::Commit => "commit",
142 Token::Rollback => "rollback",
143 Token::Drop => "drop",
144 Token::Insert => "insert",
145 Token::Values => "values",
146 Token::Limit => "limit",
147 Token::Partition => "partition",
148 Token::Tables => "tables",
149 Token::Connection => "connection",
150 Token::Publication => "publication",
151 Token::Subscription => "subscription",
152 Token::Interval => "interval",
153 // `extract` is non-reserved in PG too (it's a function the
154 // parser dispatches via context — outside that context it's
155 // a plain identifier).
156 Token::Extract => "extract",
157 Token::Offset => "offset",
158 // `to` is reserved in PG (used in many "AS … TO …" forms), so
159 // it is NOT relaxed here. Same for `from`, `where`, `as`,
160 // `select`, `not`, `and`, `or`, `null`, `true`, `false`,
161 // `create`, `table`, `into`, `on`, `order`, `by`, `having`,
162 // `group`, `distinct`, `union`, `all`, `join`, `inner`,
163 // `left`, `cross`, `outer`, `default`, `is`, `between`,
164 // `in`, `like`, `for`, `except`, `desc`, `asc`, `partition`
165 // (partial — keep partition as unreserved per modern PG).
166 _ => return None,
167 };
168 Some(s.to_string())
169}
170
171/// v7.9.22 — recognise pgvector / SPG vector-index opclass names
172/// in CREATE INDEX. SPG's HNSW already routes by query operator;
173/// the opclass is accepted for `pg_dump` compatibility (mailrs
174/// migration follow-up G5).
175/// v7.13.0 — extended to recognise PG built-in / pg_trgm opclasses
176/// (mailrs round-5 G5). These are tokens-only acceptance — SPG
177/// doesn't change index behaviour based on them.
178/// v7.37.17 (17.6 siblings) — the four PG `each` SRFs share one
179/// FROM-clause pipeline; the stored name tells the executor whether
180/// the value column keeps JSON rendering (`jsonb_each` / `json_each`)
181/// or unwraps to text (`*_each_text`).
182fn is_json_each_name(s: &str) -> bool {
183 s.eq_ignore_ascii_case("jsonb_each_text")
184 || s.eq_ignore_ascii_case("jsonb_each")
185 || s.eq_ignore_ascii_case("json_each_text")
186 || s.eq_ignore_ascii_case("json_each")
187}
188
189/// v7.38 (read01, T14) — resolve named function arguments (`argname => value`)
190/// to positional order for the `make_*` family (the AST stays positional).
191/// Positional args fill slots left-to-right; a named arg goes to its registered
192/// slot; unfilled slots default to integer 0 (PG's optional make_interval
193/// fields — the make_date/time arity is still checked at eval time).
194fn reorder_named_args(
195 fname: &str,
196 args: Vec<Expr>,
197 names: &[Option<String>],
198) -> Result<Vec<Expr>, String> {
199 let params: &[&str] = match fname.to_ascii_lowercase().as_str() {
200 "make_date" => &["year", "month", "day"],
201 "make_time" => &["hour", "min", "sec"],
202 "make_timestamp" | "make_timestamptz" => &["year", "month", "mday", "hour", "min", "sec"],
203 "make_interval" => &["years", "months", "weeks", "days", "hours", "mins", "secs"],
204 other => {
205 return Err(alloc::format!(
206 "function {other}(...) does not support named arguments"
207 ));
208 }
209 };
210 let mut slots: Vec<Option<Expr>> = (0..params.len()).map(|_| None).collect();
211 let mut next_positional = 0usize;
212 for (arg, name) in args.into_iter().zip(names.iter()) {
213 let idx = match name {
214 Some(n) => params
215 .iter()
216 .position(|p| p.eq_ignore_ascii_case(n))
217 .ok_or_else(|| alloc::format!("{fname}(...) has no argument named \"{n}\""))?,
218 None => {
219 let i = next_positional;
220 next_positional += 1;
221 i
222 }
223 };
224 if idx >= slots.len() {
225 return Err(alloc::format!("too many arguments for {fname}(...)"));
226 }
227 if slots[idx].is_some() {
228 return Err(alloc::format!(
229 "argument \"{}\" specified more than once",
230 params[idx]
231 ));
232 }
233 slots[idx] = Some(arg);
234 }
235 Ok(slots
236 .into_iter()
237 .map(|s| s.unwrap_or(Expr::Literal(Literal::Integer(0))))
238 .collect())
239}
240
241/// v7.38 (read01) — parse a lexer `Token::Numeric` source string (digits with
242/// an optional single `.`, no sign, no exponent) into `(unscaled, scale)` for
243/// `Literal::Numeric`. Returns `None` if the mantissa overflows i128.
244/// v7.39 (read01 numeric.c) — the result of expanding an `1.5e3`-style
245/// scientific literal into PG's plain NUMERIC decimal form.
246#[derive(Debug)]
247pub enum SciExpanded {
248 /// Plain decimal string ("1.5e3" → "1500", "1e-5" → "0.00001").
249 Expanded(String),
250 /// Exponent pushes the value outside PG's numeric format
251 /// (more than 131072 integer digits or 16383 fractional digits).
252 Overflow,
253 /// Not a `[±]digits[.digits]e[±]digits` literal at all.
254 NotScientific,
255}
256
257/// Expand scientific notation into a plain decimal string by moving the
258/// decimal point — no float round-trip, so the value stays exact. PG treats
259/// such literals as NUMERIC; the digit-count caps mirror PG's numeric format
260/// limits ("value overflows numeric format").
261pub fn expand_scientific_literal(s: &str) -> SciExpanded {
262 let s = s.trim();
263 let Some(epos) = s.find(['e', 'E']) else {
264 return SciExpanded::NotScientific;
265 };
266 let (mant, exp_str) = (&s[..epos], &s[epos + 1..]);
267 let Ok(exp) = exp_str.parse::<i64>() else {
268 return SciExpanded::NotScientific;
269 };
270 let (neg, mant) = match mant.strip_prefix('-') {
271 Some(r) => (true, r),
272 None => (false, mant.strip_prefix('+').unwrap_or(mant)),
273 };
274 let (int_part, frac_part) = match mant.split_once('.') {
275 Some((i, f)) => (i, f),
276 None => (mant, ""),
277 };
278 if (int_part.is_empty() && frac_part.is_empty())
279 || !int_part.bytes().all(|b| b.is_ascii_digit())
280 || !frac_part.bytes().all(|b| b.is_ascii_digit())
281 {
282 return SciExpanded::NotScientific;
283 }
284 let mut digits = String::with_capacity(int_part.len() + frac_part.len());
285 digits.push_str(int_part);
286 digits.push_str(frac_part);
287 // Decimal point position within `digits` after applying the exponent.
288 let new_point = int_part.len() as i64 + exp;
289 // PG's numeric format: up to 131072 digits before the point, 16383 after.
290 if new_point > 131_072 {
291 return SciExpanded::Overflow;
292 }
293 if (digits.len() as i64 - new_point) > 16_383 {
294 return SciExpanded::Overflow;
295 }
296 let sign = if neg { "-" } else { "" };
297 let plain = if new_point <= 0 {
298 let mut out = String::with_capacity(digits.len() + 2 + (-new_point) as usize);
299 out.push_str("0.");
300 for _ in 0..(-new_point) {
301 out.push('0');
302 }
303 out.push_str(&digits);
304 out
305 } else if (new_point as usize) >= digits.len() {
306 let mut out = digits;
307 for _ in 0..(new_point as usize - out.len()) {
308 out.push('0');
309 }
310 out
311 } else {
312 let mut out = String::with_capacity(digits.len() + 1);
313 out.push_str(&digits[..new_point as usize]);
314 out.push('.');
315 out.push_str(&digits[new_point as usize..]);
316 out
317 };
318 SciExpanded::Expanded(alloc::format!("{sign}{plain}"))
319}
320
321/// v7.39 (round 367, M20) — lower a MySQL hexadecimal binary-string
322/// literal (`0x…` / `X'…'`) onto the existing bytea cast. The hex digits
323/// are left-padded to an even count (`0x123` → byte string `01 23`, per
324/// MariaDB) and handed to the PG bytea input format (`\x…`).
325#[inline(never)]
326fn hex_literal_to_bytea_expr(hex: &str) -> Expr {
327 let padded = if hex.len() % 2 == 1 {
328 alloc::format!("0{hex}")
329 } else {
330 hex.to_string()
331 };
332 Expr::Cast {
333 expr: alloc::boxed::Box::new(Expr::Literal(Literal::String(alloc::format!(
334 "\\x{padded}"
335 )))),
336 target: CastTarget::Named("bytea".to_string()),
337 }
338}
339
340/// v7.39 (round 367, M20) — lower a MySQL bit-value literal (`b'1010'`)
341/// onto the bytea cast. The bits are read big-endian and left-padded to a
342/// whole number of bytes (`b'1010'` → one byte `0x0A`, per MariaDB).
343#[inline(never)]
344fn bits_literal_to_bytea_expr(bits: &str) -> Expr {
345 let pad = (8 - bits.len() % 8) % 8;
346 let mut hex = String::with_capacity((bits.len() + pad).div_ceil(4));
347 let padded: String = core::iter::repeat_n('0', pad).chain(bits.chars()).collect();
348 for nibble in padded.as_bytes().chunks(4) {
349 let mut v = 0u8;
350 for &b in nibble {
351 v = (v << 1) | (b - b'0');
352 }
353 hex.push(char::from_digit(u32::from(v), 16).unwrap_or('0'));
354 }
355 hex_literal_to_bytea_expr(&hex)
356}
357
358/// Resolve a lexer `Token::Numeric` into its literal. PG semantics: a dotted
359/// or over-i64 literal is exact NUMERIC; scientific notation is NUMERIC too
360/// (expanded to the plain decimal form); only a fractional depth beyond SPG's
361/// scale width (u8) falls back to double precision (recorded delta).
362/// Kept out of the parse_expr recursion frame — see the call site.
363#[inline(never)]
364fn numeric_token_to_literal(s: String) -> Result<Literal, String> {
365 match parse_decimal_literal(&s) {
366 Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
367 // v7.38 (read01, T3.C3) — a plain decimal too wide for i128 keeps
368 // its exact value as a NumericBig.
369 None if !s.contains(['e', 'E']) => Ok(Literal::NumericBig(s)),
370 // v7.39 (read01 numeric.c) — expand the exponent form.
371 None => match expand_scientific_literal(&s) {
372 SciExpanded::Expanded(plain) => match parse_decimal_literal(&plain) {
373 Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
374 None if plain
375 .split_once('.')
376 .is_none_or(|(_, f)| u8::try_from(f.len()).is_ok()) =>
377 {
378 Ok(Literal::NumericBig(plain))
379 }
380 None => s
381 .parse::<f64>()
382 .map(Literal::Float)
383 .map_err(|_| format!("invalid numeric literal {s:?}")),
384 },
385 SciExpanded::Overflow => Err("value overflows numeric format".to_string()),
386 SciExpanded::NotScientific => s
387 .parse::<f64>()
388 .map(Literal::Float)
389 .map_err(|_| format!("invalid numeric literal {s:?}")),
390 },
391 }
392}
393
394fn parse_decimal_literal(s: &str) -> Option<(i128, u16)> {
395 let (int_part, frac_part) = match s.split_once('.') {
396 Some((i, f)) => (i, f),
397 None => (s, ""),
398 };
399 // v7.39 (round 271) — was u8::MAX. A literal with 256 decimal
400 // places fell out of the numeric path here, which is why
401 // `pg_typeof(1e-256)` answered double precision and a plain
402 // 256-place decimal aborted the query in the big-decimal converter.
403 if frac_part.len() > u16::MAX as usize {
404 return None;
405 }
406 let mut digits = String::with_capacity(int_part.len() + frac_part.len());
407 digits.push_str(int_part);
408 digits.push_str(frac_part);
409 let mantissa: i128 = digits.parse().ok()?;
410 #[allow(clippy::cast_possible_truncation)]
411 Some((mantissa, frac_part.len() as u16))
412}
413
414/// `jsonb_to_record` / `jsonb_to_recordset` (+ `json_` variants) — the
415/// record-returning JSON functions that take a `AS alias(col type, …)`
416/// column-definition list in FROM position.
417fn is_json_to_record_name(s: &str) -> bool {
418 s.eq_ignore_ascii_case("jsonb_to_recordset")
419 || s.eq_ignore_ascii_case("jsonb_to_record")
420 // v7.39 (read01 jsonfuncs.c) — the populate family with an AS
421 // column-definition list desugars identically (the record base
422 // argument only carries the type; a non-NULL base's field
423 // defaults are a recorded delta).
424 || s.eq_ignore_ascii_case("json_populate_record")
425 || s.eq_ignore_ascii_case("jsonb_populate_record")
426 || s.eq_ignore_ascii_case("json_populate_recordset")
427 || s.eq_ignore_ascii_case("jsonb_populate_recordset")
428 || s.eq_ignore_ascii_case("json_to_recordset")
429 || s.eq_ignore_ascii_case("json_to_record")
430}
431
432fn is_vector_opclass_name(name: &str) -> bool {
433 let lc = name.to_ascii_lowercase();
434 matches!(
435 lc.as_str(),
436 "vector_cosine_ops"
437 | "vector_l2_ops"
438 | "vector_ip_ops"
439 | "halfvec_cosine_ops"
440 | "halfvec_l2_ops"
441 | "halfvec_ip_ops"
442 | "sq8_cosine_ops"
443 | "sq8_l2_ops"
444 | "sq8_ip_ops"
445 // pg_trgm — trigram operator class. SPG's GIN index
446 // already uses tsvector tokens; trigram-style LIKE
447 // pattern matching still routes through a sequential
448 // scan, but the opclass name is accepted so PG schemas
449 // load.
450 | "gin_trgm_ops"
451 | "gist_trgm_ops"
452 // PG built-in btree opclasses occasionally appear in
453 // pg_dump output for column types with multiple
454 // sort orders (text_pattern_ops, varchar_pattern_ops,
455 // bpchar_pattern_ops).
456 | "text_pattern_ops"
457 | "varchar_pattern_ops"
458 | "bpchar_pattern_ops"
459 | "int4_ops"
460 | "int8_ops"
461 | "text_ops"
462 )
463}
464
465#[derive(Debug, Clone, PartialEq, Eq)]
466pub struct ParseError {
467 pub message: String,
468 /// Index into the token stream where parsing tripped. Not a byte offset.
469 /// v7.39 (read01 round 95) — the byte/char position is NOT stored here: a
470 /// field would grow every `Result<_, ParseError>` slot on the deeply
471 /// recursive parse stack and tip the nesting-budget frame cliff. PG's
472 /// 1-based char position is recovered on the cold error path by
473 /// [`syntax_error_position`], which re-tokenizes to map this token index.
474 pub token_pos: usize,
475}
476
477impl fmt::Display for ParseError {
478 /// v7.39 (round 322/V24) — the message ALONE. It used to be prefixed
479 /// with `parse error at token #N: `, which PG has no equivalent of:
480 /// the message bodies are already PG's verbatim (`LIMIT must not be
481 /// negative`, `invalid input syntax for type bigint: "abc"`), and the
482 /// prefix was SPG's internal token index leaking into every one of
483 /// them. `token_pos` stays a field — the wire recovers PG's 1-based
484 /// character position from it for the ErrorResponse `P`.
485 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
486 f.write_str(&self.message)
487 }
488}
489
490impl From<LexError> for ParseError {
491 fn from(e: LexError) -> Self {
492 Self {
493 message: format!("lex: {e}"),
494 token_pos: 0,
495 }
496 }
497}
498
499/// v7.9.30 — parse a single expression (no trailing junk). Used by
500/// the engine to re-hydrate stored partial-index / unique-index
501/// predicates from their canonical Display form. The same Pratt
502/// parser the statement path uses; this entry point just skips the
503/// statement dispatch.
504pub fn parse_expression(input: &str) -> Result<Expr, ParseError> {
505 let (tokens, offsets) =
506 lexer::tokenize_with_offsets(input, false).map_err(|e| shape_lex_error(&e, input))?;
507 let mut p = Parser::new(tokens);
508 let expr = p
509 .parse_expr(0)
510 .and_then(|e| p.expect_eof().map(|()| e))
511 .map_err(|e| shape_syntax_error(e, input, &offsets))?;
512 Ok(expr)
513}
514
515/// Parse exactly one statement, swallow an optional trailing `;`, and require
516/// the token stream to end there. PG string semantics.
517pub fn parse_statement(input: &str) -> Result<Statement, ParseError> {
518 parse_statement_with(input, false)
519}
520
521/// v7.22 (round-13 T3) — dialect-aware entry: `backslash_escapes`
522/// selects MySQL-style string lexing (see `lexer::tokenize_with`).
523/// The engine threads its session flag through here.
524pub fn parse_statement_with(input: &str, backslash_escapes: bool) -> Result<Statement, ParseError> {
525 let (tokens, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes)
526 .map_err(|e| shape_lex_error(&e, input))?;
527 // The same session flag names the dialect for both the lexer and
528 // the type mapping.
529 let mut p = Parser::new_with_dialect(tokens, backslash_escapes).with_source(input, &offsets);
530 let stmt = (|| {
531 let stmt = p.parse_one_statement()?;
532 if matches!(p.peek(), Token::Semicolon) {
533 p.advance();
534 }
535 p.expect_eof()?;
536 Ok(stmt)
537 })()
538 .map_err(|e: ParseError| shape_syntax_error(e, input, &offsets))?;
539 Ok(stmt)
540}
541
542/// v7.39 (round 340, V56) — PG has exactly two syntax-error wordings:
543/// `syntax error at or near "<token>"` and `syntax error at end of input`
544/// (measured on 18.4 across a dozen shapes). SPG wrote its own per-site
545/// prose — `expected identifier, got Eof`, `unexpected token From in
546/// expression`, `expected end of input, got Ident("with")` — which named
547/// internal token types and, in the Debug forms, leaked the parser's own
548/// enum into a message clients read.
549///
550/// Applied once on the way out, so every construction site is covered and
551/// the token named is the one the error itself points at. Messages whose
552/// bodies are already PG's verbatim (`LIMIT must not be negative`,
553/// `invalid input syntax for type bigint: "abc"`) are left alone — those
554/// are PG's own errors, not its syntax error.
555fn shape_syntax_error(e: ParseError, input: &str, offsets: &[usize]) -> ParseError {
556 if !(e.message.starts_with("expected ") || e.message.starts_with("unexpected token ")) {
557 return e;
558 }
559 let message = match offending_lexeme(input, offsets, e.token_pos) {
560 Some(tok) => alloc::format!("syntax error at or near \"{tok}\""),
561 None => "syntax error at end of input".into(),
562 };
563 ParseError {
564 message,
565 token_pos: e.token_pos,
566 }
567}
568
569/// v7.39 (round 340, V56) — a lexer-level failure the way PG words it.
570/// Measured on 18.4: `unterminated quoted string at or near "'abc"`,
571/// `unterminated quoted identifier at or near ""abc"`, `unterminated /*
572/// comment at or near "/* x"` — the quoted part runs from the opening
573/// delimiter to the end of the input. SPG reported its own internal
574/// shape instead (`unterminated string literal at byte 7`), which named
575/// a byte offset no client can use.
576fn shape_lex_error(e: &lexer::LexError, input: &str) -> ParseError {
577 use lexer::LexErrorKind as K;
578 let from_here = input.get(e.pos..).map(str::trim_end).unwrap_or("");
579 let message = match &e.kind {
580 K::UnterminatedString => {
581 alloc::format!("unterminated quoted string at or near \"{from_here}\"")
582 }
583 K::UnterminatedQuotedIdent => {
584 alloc::format!("unterminated quoted identifier at or near \"{from_here}\"")
585 }
586 K::UnterminatedBlockComment => {
587 alloc::format!("unterminated /* comment at or near \"{from_here}\"")
588 }
589 // PG has no "unknown character" error of its own — the character
590 // is skipped and the parser reports the next token. SPG stops at
591 // the character itself and names it, which is the same shape.
592 K::UnknownChar(c) => alloc::format!("syntax error at or near \"{c}\""),
593 // The number-literal kinds already carry PG's `at or near` form.
594 other => alloc::format!(
595 "{}",
596 lexer::LexError {
597 kind: other.clone(),
598 pos: e.pos,
599 }
600 ),
601 };
602 ParseError {
603 message,
604 token_pos: 0,
605 }
606}
607
608/// The offending token exactly as it appears in the input, or `None` at
609/// end of input. PG echoes the source spelling — a lower-case `frm`
610/// reports as `frm`, not as a canonicalised keyword.
611fn offending_lexeme<'a>(input: &'a str, offsets: &[usize], token_pos: usize) -> Option<&'a str> {
612 let start = *offsets.get(token_pos)?;
613 if start >= input.len() {
614 return None;
615 }
616 let end = offsets
617 .get(token_pos + 1)
618 .copied()
619 .unwrap_or(input.len())
620 .min(input.len());
621 let seg = input.get(start..end)?.trim();
622 if seg.is_empty() {
623 return None;
624 }
625 // A quoted literal / identifier keeps its inner spaces; anything else
626 // ends at the first whitespace (the segment runs to the NEXT token's
627 // start, which may swallow a comment).
628 if seg.starts_with('\'') || seg.starts_with('"') || seg.starts_with('`') {
629 Some(seg)
630 } else {
631 seg.split_whitespace().next()
632 }
633}
634
635/// v7.39 (read01 round 95) — recover PG's 1-based CHARACTER error position for
636/// a [`ParseError::token_pos`]. Kept off the `ParseError` struct (and so off
637/// every recursive `Result` slot) to protect the nesting-budget frame cliff:
638/// this re-tokenizes `input` on the cold error path to map the failing token
639/// index to its start byte, then to a character offset. `backslash_escapes`
640/// must match the parse that produced `token_pos` (it barely shifts offsets,
641/// but stay consistent). Returns `None` when the index has no offset or the
642/// byte isn't a char boundary. The wire attaches it as the ErrorResponse `P`.
643#[must_use]
644pub fn syntax_error_position(
645 input: &str,
646 backslash_escapes: bool,
647 token_pos: usize,
648) -> Option<usize> {
649 let (_, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes).ok()?;
650 let byte_off = *offsets.get(token_pos)?;
651 if byte_off > input.len() || !input.is_char_boundary(byte_off) {
652 return None;
653 }
654 Some(input[..byte_off].chars().count() + 1)
655}
656
657struct Parser {
658 tokens: Vec<Token>,
659 pos: usize,
660 /// v7.39 (round 274) — the session's dialect, carried by the same
661 /// signal that drives string-literal escaping: `SET sql_mode` (only
662 /// MySQL clients and mysqldump preambles emit it) turns it on,
663 /// `SET standard_conforming_strings` (every pg_dump preamble) turns
664 /// it off. Needed here because the two dialects disagree about what
665 /// `REAL` means — see the type mapping below.
666 mysql_dialect: bool,
667 /// v7.30.2 (mailrs round-25 ask 2) — live nesting depth of the
668 /// mutually recursive expr/select parsers. Bounded so a deeply
669 /// nested input returns a parse error instead of overflowing
670 /// the stack (embed hosts die on overflow — it is an abort,
671 /// not a catchable error).
672 nest_depth: usize,
673 /// TABLESAMPLE lowering channel: the table-ref parser pushes a
674 /// `random() < p/100` predicate here; the enclosing SELECT
675 /// drains the list after its WHERE parses and ANDs the
676 /// predicates in. parse_bare_select save/restores around its
677 /// FROM+WHERE so nested selects only drain their own.
678 pending_sample_preds: Vec<Expr>,
679 /// v7.39 (round 691) — collation lowering channel, the same shape as
680 /// `pending_sample_preds` above. `expr COLLATE "name"` is ORDERING
681 /// information, and `ast::OrderBy` is where this parser keeps ordering
682 /// information (`desc`, `nulls_first`); the alternative — a new `Expr`
683 /// variant — puts a new arm on `eval_expr`, which this repo has
684 /// measured to overflow the debug stack. So while an ORDER BY KEY is
685 /// being parsed the postfix loop drops the name here instead of
686 /// refusing it, and the key's parser takes it.
687 ///
688 /// Only inside an ORDER BY key: everywhere else an unperformable
689 /// collation still errors, because accepting one at a COMPARISON and
690 /// ignoring it is the defect F36 exists to close.
691 in_order_by_key: bool,
692 order_key_collation: Option<String>,
693 /// POSITION(sub IN str) — while parsing the needle, the IN
694 /// keyword is the argument separator, not a membership test.
695 /// The postfix loop leaves IN unconsumed when this is set.
696 suppress_in_tail: bool,
697 /// Index of the token the last `advance()` returned — see
698 /// [`Parser::consumed_pos`].
699 last_consumed: usize,
700 /// v7.39 (round 506) — the statement's own text and the byte each token
701 /// starts at, so a MySQL projection item can report the SOURCE TEXT
702 /// MariaDB reports: `SELECT a + b` names its column `a + b`,
703 /// spacing and all. Only filled for a MySQL session — a PG one names
704 /// columns from the parsed shape and pays nothing for this.
705 src: Option<(String, Vec<usize>)>,
706}
707
708/// Max expr/select parser nesting (parens, subqueries, CASE, …).
709/// Real SQL nests a few dozen levels at the extreme. Each nesting level
710/// costs a parse_expr→parse_unary→parse_atom frame chain, so the budget
711/// exists to turn a deep statement into a catchable parse ERROR: a stack
712/// overflow is an abort, and in the server it does not fail one query, it
713/// takes the process down and every other connection with it.
714///
715/// v7.39 (round 507) — measured, because the figure here used to be a
716/// guess ("over 10 KiB in debug … comfortably inside a 2 MiB worker stack
717/// in BOTH debug and release"), and the debug half of that is wrong by
718/// more than an order of magnitude:
719///
720/// * RELEASE, on a 2 MiB worker stack: every recursive shape reaches
721/// this budget and errors. Verified against a live server for nested
722/// derived tables, parens, calls, CASE, IN-subqueries, scalar
723/// subqueries, NOT and unary minus — the server stayed up through all
724/// of them. This is the contract that matters, and it holds.
725/// * DEBUG: nested derived tables cost roughly 235 KiB of stack PER
726/// LEVEL, so parsing alone aborts around 35 levels on an 8 MiB stack
727/// and executing aborts around 8 inside a test thread. The budget is
728/// simply unreachable there, which is why a deep-nesting test has to
729/// ask for a large stack of its own — see `nesting_budget_errors_at`
730/// in the parser tests.
731/// v7.39 (round 541) — the pg_catalog relations SPG synthesises, in
732/// one place.
733///
734/// There were two copies of this fact: a curated list, used for BARE
735/// names, and — in `try_peek_meta_qualified` — no list at all, which
736/// rewrote `pg_catalog.<anything>` to `__spg_pg_<anything>` and left
737/// the engine to complain about a view it could not materialise. So
738/// writing the schema qualifier CHANGED THE ANSWER: `pg_stat_activity`
739/// had rows, `pg_catalog.pg_stat_activity` was an error.
740///
741/// PG puts `pg_catalog` at the implicit front of every search_path, so
742/// the two spellings name the same relation and must resolve the same
743/// way. Names NOT here (`pg_stat_activity`, `pg_locks`,
744/// `pg_stat_statements`, `pg_statio_user_tables`) route through the
745/// meta_view_result path under their own names and must not be
746/// rewritten; a name that is neither reaches the ordinary resolver,
747/// which reports that the relation does not exist — PG's answer.
748const SYNTHESISED_PG_CATALOGS: &[&str] = &[
749 "pg_am",
750 "pg_attrdef",
751 "pg_attribute",
752 "pg_cast",
753 "pg_db_role_setting",
754 "pg_conversion",
755 "pg_default_acl",
756 "pg_shadow",
757 "pg_sequences",
758 "pg_range",
759 "pg_partitioned_table",
760 "pg_language",
761 "pg_group",
762 "pg_authid",
763 "pg_class",
764 "pg_collation",
765 "pg_constraint",
766 "pg_database",
767 "pg_depend",
768 // v7.39 (read01 round 50) — COMMENT ON store, PG's pg_description.
769 "pg_description",
770 "pg_enum",
771 "pg_extension",
772 // v7.39 (round 541) — pg_dump reads it for every relation of kind
773 // 'f'. SPG has no foreign tables, so it is empty, which is also
774 // what PG reports on a database that has none.
775 "pg_foreign_table",
776 // v7.39 (round 541) — the empty-by-truth family; see
777 // EMPTY_PG_CATALOGS in spg-engine::system_catalog.
778 "pg_event_trigger",
779 "pg_file_settings",
780 "pg_foreign_data_wrapper",
781 "pg_foreign_server",
782 "pg_hba_file_rules",
783 "pg_ident_file_mappings",
784 "pg_init_privs",
785 "pg_parameter_acl",
786 "pg_prepared_xacts",
787 "pg_publication_namespace",
788 "pg_publication_rel",
789 "pg_publication_tables",
790 "pg_replication_origin",
791 "pg_replication_origin_status",
792 "pg_seclabel",
793 "pg_seclabels",
794 "pg_shdepend",
795 "pg_shdescription",
796 "pg_shmem_allocations",
797 "pg_shmem_allocations_numa",
798 "pg_shseclabel",
799 "pg_statistic_ext_data",
800 "pg_stats_ext",
801 "pg_stats_ext_exprs",
802 "pg_subscription_rel",
803 "pg_transform",
804 "pg_user_mapping",
805 "pg_user_mappings",
806 "pg_index",
807 "pg_indexes",
808 "pg_inherits",
809 // v7.39 (round 650) — the text-search catalogs SPG can fill
810 // honestly. `pg_ts_config_map` is deliberately NOT here: it maps
811 // token types to dictionaries and SPG has no token-type model,
812 // the same gap that leaves `ts_token_type` / `ts_debug` unbuilt.
813 "pg_ts_config",
814 "pg_ts_config_map",
815 "pg_ts_dict",
816 "pg_ts_parser",
817 "pg_ts_template",
818 "pg_matviews",
819 "pg_namespace",
820 // v7.39 (round 621)
821 "pg_operator",
822 "pg_policies",
823 "pg_policy",
824 "pg_proc",
825 "pg_publication",
826 "pg_replication_slots",
827 "pg_roles",
828 // v7.39 (round 143) — the rewrite-rule listing view.
829 // v7.39 (round 312) — and the rule catalogue itself, which
830 // `pg_get_ruledef(oid)` resolves against.
831 "pg_rewrite",
832 "pg_rules",
833 "pg_sequence",
834 "pg_settings",
835 "pg_stat_archiver",
836 "pg_stat_bgwriter",
837 "pg_stat_checkpointer",
838 "pg_stat_database",
839 "pg_stat_io",
840 "pg_stat_progress_analyze",
841 "pg_auth_members",
842 "pg_stat_progress_create_index",
843 "pg_stat_progress_vacuum",
844 "pg_stat_replication",
845 "pg_stat_slru",
846 "pg_stat_subscription_stats",
847 "pg_stat_user_functions",
848 "pg_stat_user_indexes",
849 "pg_stat_user_tables",
850 "pg_stat_wal",
851 "pg_prepared_statements",
852 "pg_largeobject",
853 "pg_largeobject_metadata",
854 "pg_statistic",
855 "pg_statistic_ext",
856 "pg_subscription",
857 "pg_tables",
858 "pg_tablespace",
859 // v7.39 (round 502) — the timezone catalogues. SPG resolved
860 // named zones correctly but could not list them, so a client
861 // populating a timezone picker got "relation does not exist".
862 "pg_timezone_abbrevs",
863 "pg_timezone_names",
864 "pg_trigger",
865 "pg_type",
866 "pg_user",
867 "pg_views",
868];
869
870const MAX_NEST_DEPTH: usize = 64;
871
872/// Stack accounting for the nesting budget, test-only.
873///
874/// `MAX_NEST_DEPTH` is a fixed count calibrated against a frame size
875/// that MOVES: a compiler upgrade grew the parser's debug frames and
876/// silently ate the margin until `nesting_budget_errors_cleanly` went
877/// from erroring cleanly to aborting on a stack overflow. A count
878/// cannot notice that on its own, so the budget is measured here and
879/// held to a ceiling.
880///
881/// The reading has to come from a helper whose OWN frame is the same at
882/// every call: debug slot placement does not follow source order, so a
883/// local's address inside the function under test is not that
884/// function's frame boundary. Two earlier probes were wrong that way —
885/// one read `&self.nest_depth`, which is the `Parser`'s address and
886/// never moves at all.
887#[cfg(test)]
888mod frame_meter {
889 extern crate std;
890 use std::cell::Cell;
891
892 // Per-THREAD, not global. `cargo test` runs tests in parallel and
893 // plenty of them parse nested expressions, so shared statics get
894 // stack addresses from several threads at once and the subtraction
895 // below turns into noise — it read 229,772 bytes per level that way,
896 // while passing when the test was run on its own.
897 std::thread_local! {
898 static AT_LO: Cell<usize> = const { Cell::new(0) };
899 static AT_HI: Cell<usize> = const { Cell::new(0) };
900 }
901
902 pub(super) const SAMPLE_LO: usize = 4;
903 pub(super) const SAMPLE_HI: usize = 24;
904
905 #[inline(never)]
906 pub(super) fn record(depth: usize) {
907 let anchor = 0u8;
908 let at = core::ptr::from_ref(&anchor) as usize;
909 if depth == SAMPLE_LO {
910 AT_LO.with(|c| c.set(at));
911 } else if depth == SAMPLE_HI {
912 AT_HI.with(|c| c.set(at));
913 }
914 }
915
916 /// Bytes of stack one nesting level costs, averaged over the span.
917 pub(super) fn bytes_per_level() -> usize {
918 let lo = AT_LO.with(Cell::get);
919 let hi = AT_HI.with(Cell::get);
920 assert!(lo > 0 && hi > 0, "meter never sampled: lo={lo} hi={hi}");
921 assert!(lo > hi, "stack grew upwards? lo={lo} hi={hi}");
922 (lo - hi) / (SAMPLE_HI - SAMPLE_LO)
923 }
924
925 pub(super) fn reset() {
926 AT_LO.with(|c| c.set(0));
927 AT_HI.with(|c| c.set(0));
928 }
929}
930
931/// v7.39 (read01 geo_ops.c) — prefix `@@` desugar target, out-of-line so
932/// the constructor's temporaries stay off `parse_unary`'s recursion frame.
933#[inline(never)]
934fn build_center_call(e: Expr) -> Expr {
935 Expr::FunctionCall {
936 name: alloc::string::String::from("center"),
937 args: alloc::vec![e],
938 }
939}
940
941/// Max consecutive binary operators at ONE precedence level
942/// (`a OR b OR c …`, `1+1+1…`). The chain builds iteratively at
943/// parse time but evaluates and drops recursively — depth beyond
944/// this overflows 2 MiB worker stacks (debug eval frames run
945/// multiple KiB). `IN (…)` lists are flat and unaffected.
946const MAX_BINARY_CHAIN: usize = 256;
947
948/// v7.22 (round-13 gap 5) — the kind keyword after `CONSTRAINT
949/// <name>` in a CREATE TABLE column list. FOREIGN KEY is not here:
950/// it keeps its dedicated path (`parse_table_level_fk`).
951enum NamedTableConstraintKind {
952 Check,
953 Unique,
954 PrimaryKey,
955 Exclude,
956}
957
958impl Parser {
959 fn new(tokens: Vec<Token>) -> Self {
960 Self::new_with_dialect(tokens, false)
961 }
962
963 fn new_with_dialect(tokens: Vec<Token>, mysql_dialect: bool) -> Self {
964 Self {
965 tokens,
966 mysql_dialect,
967 in_order_by_key: false,
968 order_key_collation: None,
969 pos: 0,
970 nest_depth: 0,
971 pending_sample_preds: Vec::new(),
972 suppress_in_tail: false,
973 last_consumed: 0,
974 src: None,
975 }
976 }
977
978 /// Hand the parser the text it is parsing, for [`Parser::source_span`].
979 fn with_source(mut self, input: &str, offsets: &[usize]) -> Self {
980 if self.mysql_dialect {
981 self.src = Some((input.to_string(), offsets.to_vec()));
982 }
983 self
984 }
985
986 /// The source text spanning tokens `start ..= end`, trimmed.
987 ///
988 /// The offsets are token STARTS, so the span runs to the start of the
989 /// token after `end` and gives back the whitespace between them —
990 /// trimming is what makes `a + b FROM t` end at `b`.
991 fn source_span(&self, start: usize, end: usize) -> Option<&str> {
992 let (text, offsets) = self.src.as_ref()?;
993 let from = *offsets.get(start)?;
994 let to = *offsets.get(end + 1)?;
995 text.get(from..to).map(str::trim_end)
996 }
997
998 /// v7.30.2 (mailrs round-25 ask 2) — bump the expr/select
999 /// nesting depth, erroring out cleanly past the budget.
1000 fn enter_nested(&mut self) -> Result<(), ParseError> {
1001 self.nest_depth += 1;
1002 #[cfg(test)]
1003 frame_meter::record(self.nest_depth);
1004 if self.nest_depth > MAX_NEST_DEPTH {
1005 self.nest_depth -= 1;
1006 return Err(self.err(alloc::format!(
1007 "statement nests deeper than {MAX_NEST_DEPTH} levels"
1008 )));
1009 }
1010 Ok(())
1011 }
1012
1013 fn peek(&self) -> &Token {
1014 // tokens always ends with Eof; pos is clamped in advance().
1015 &self.tokens[self.pos]
1016 }
1017
1018 fn advance(&mut self) -> Token {
1019 let t = mem::replace(&mut self.tokens[self.pos], Token::Eof);
1020 self.last_consumed = self.pos;
1021 if self.pos + 1 < self.tokens.len() {
1022 self.pos += 1;
1023 }
1024 t
1025 }
1026
1027 /// v7.39 (round 340, V56) — the index of the token `advance()` just
1028 /// returned. It was computed as `pos - 1`, which is wrong at both
1029 /// ends: `advance()` parks on the final Eof rather than running off
1030 /// the end (so `SELECT * FROM` named `FROM` where PG says `at end of
1031 /// input`), and after backtracking `pos` is no longer one past the
1032 /// token that failed. Recorded by `advance()` itself instead.
1033 const fn consumed_pos(&self) -> usize {
1034 self.last_consumed
1035 }
1036
1037 fn err(&self, message: String) -> ParseError {
1038 ParseError {
1039 message,
1040 token_pos: self.pos,
1041 }
1042 }
1043
1044 fn expect_eof(&self) -> Result<(), ParseError> {
1045 if matches!(self.peek(), Token::Eof) {
1046 Ok(())
1047 } else {
1048 Err(self.err(format!("expected end of input, got {:?}", self.peek())))
1049 }
1050 }
1051
1052 /// v7.14.0 — swallow every token up to (but not including) the
1053 /// next semicolon / EOF. Used by the dump-noise dispatcher
1054 /// to consume `COMMENT ON …`, `GRANT …`, `LOCK TABLES …`,
1055 /// etc. without modeling each grammar.
1056 /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
1057 /// Kinds SPG stores by name: TABLE / COLUMN / INDEX / VIEW / SEQUENCE /
1058 /// SCHEMA / TYPE / DATABASE / FUNCTION. Anything else (and the multi-word
1059 /// `CONSTRAINT c ON t` / `MATERIALIZED VIEW` forms) keeps the old
1060 /// swallow-as-no-op behaviour so a pg_dump tail still loads.
1061 fn parse_comment_on(&mut self) -> Result<Statement, ParseError> {
1062 let start = self.pos;
1063 self.advance(); // COMMENT
1064 if !matches!(self.peek(), Token::On) {
1065 self.pos = start;
1066 self.consume_until_statement_boundary();
1067 return Ok(Statement::Empty);
1068 }
1069 self.advance(); // ON
1070 let kind = match self.peek() {
1071 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
1072 Token::Table => "table".into(),
1073 _ => {
1074 self.consume_until_statement_boundary();
1075 return Ok(Statement::Empty);
1076 }
1077 };
1078 if !matches!(
1079 kind.as_str(),
1080 "table"
1081 | "column"
1082 | "index"
1083 | "view"
1084 | "sequence"
1085 | "schema"
1086 | "type"
1087 | "database"
1088 | "function"
1089 ) {
1090 self.consume_until_statement_boundary();
1091 return Ok(Statement::Empty);
1092 }
1093 self.advance(); // the kind keyword
1094 // The object name. ⚠️ `expect_ident_like` strips a leading
1095 // `<schema>.` qualifier and returns only the trailing ident (SPG is
1096 // single-schema), which would turn `COMMENT ON COLUMN t.c` into just
1097 // `c`. Read the dotted parts from raw tokens instead, then let a
1098 // 3-part `schema.t.c` drop its leading schema like everywhere else.
1099 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
1100 loop {
1101 match self.advance() {
1102 Token::Ident(s) | Token::QuotedIdent(s) => parts.push(s),
1103 other if unreserved_keyword_text(&other).is_some() => {
1104 parts.push(unreserved_keyword_text(&other).unwrap());
1105 }
1106 other => {
1107 return Err(ParseError {
1108 message: alloc::format!("expected identifier, got {other:?}"),
1109 token_pos: self.consumed_pos(),
1110 });
1111 }
1112 }
1113 if matches!(self.peek(), Token::Dot) {
1114 self.advance();
1115 } else {
1116 break;
1117 }
1118 }
1119 // COLUMN wants `table.column`; every other kind wants a bare name.
1120 let want = if kind == "column" { 2 } else { 1 };
1121 while parts.len() > want {
1122 parts.remove(0);
1123 }
1124 let name = parts.join(".");
1125 // v7.39 (round 710) — `COMMENT ON FUNCTION f(int, text) IS …`.
1126 // pg_dump writes the SIGNATURE, and the paren list was a syntax
1127 // error here — a dump carrying one function comment failed to
1128 // restore. The list is consumed (the comment store keys by name;
1129 // overload-precise comments are the function-predicate follow-up).
1130 if matches!(self.peek(), Token::LParen)
1131 && matches!(
1132 kind.as_str(),
1133 "function" | "procedure" | "aggregate" | "routine"
1134 )
1135 {
1136 let mut depth = 0usize;
1137 loop {
1138 match self.advance() {
1139 Token::LParen => depth += 1,
1140 Token::RParen => {
1141 depth -= 1;
1142 if depth == 0 {
1143 break;
1144 }
1145 }
1146 Token::Eof => {
1147 return Err(self.err(alloc::string::String::from(
1148 "unterminated argument list in COMMENT ON",
1149 )));
1150 }
1151 _ => {}
1152 }
1153 }
1154 }
1155 // `IS`
1156 if !matches!(self.peek(), Token::Is) {
1157 self.expect_keyword_ident("is")?;
1158 } else {
1159 self.advance();
1160 }
1161 let comment = match self.peek() {
1162 Token::Null => {
1163 self.advance();
1164 None
1165 }
1166 _ => Some(self.expect_string_literal()?),
1167 };
1168 Ok(Statement::CommentOn {
1169 kind,
1170 name,
1171 comment,
1172 })
1173 }
1174
1175 /// v7.39 (read01 round 57) — `GRANT <privs> ON <obj> TO <roles> [WITH GRANT
1176 /// OPTION]` / `REVOKE [GRANT OPTION FOR] <privs> ON <obj> FROM <roles>
1177 /// [CASCADE|RESTRICT]`.
1178 ///
1179 /// TABLE privileges are the real ones (stored, enforced, introspectable).
1180 /// Every other object class — SCHEMA / SEQUENCE / DATABASE / FUNCTION / …,
1181 /// and the no-ON `GRANT role TO role` membership form — parses into
1182 /// `GrantObject::Other` and no-ops in the engine, so a pg_dump that grants
1183 /// on them still restores.
1184 fn parse_grant_or_revoke(&mut self, grant: bool) -> Result<Statement, ParseError> {
1185 self.advance(); // GRANT / REVOKE
1186 // REVOKE's optional `GRANT OPTION FOR` prefix.
1187 let mut grant_option = false;
1188 if !grant
1189 && self.peek_keyword_ident("grant")
1190 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("option"))
1191 {
1192 self.advance(); // GRANT
1193 self.advance(); // OPTION
1194 self.expect_keyword_ident("for")?;
1195 grant_option = true;
1196 }
1197 // The privilege list: `ALL [PRIVILEGES] [(cols)]`, or comma-separated
1198 // words each with an optional COLUMN list.
1199 let mut privileges: Vec<GrantPriv> = Vec::new();
1200 if matches!(self.peek(), Token::All) {
1201 self.advance();
1202 if self.peek_keyword_ident("privileges") {
1203 self.advance();
1204 }
1205 // `GRANT ALL (col) ON t TO r` — every column privilege, on that
1206 // column only.
1207 if matches!(self.peek(), Token::LParen) {
1208 let columns = self.parse_grant_column_list()?;
1209 privileges.push(GrantPriv {
1210 word: "ALL".into(),
1211 columns,
1212 });
1213 }
1214 } else {
1215 loop {
1216 // SELECT and INSERT lex as reserved tokens, so they never
1217 // reach `expect_ident_like` as plain idents; the rest
1218 // (UPDATE / DELETE / TRUNCATE / REFERENCES / TRIGGER /
1219 // MAINTAIN) are ordinary identifiers.
1220 let w = match self.peek() {
1221 Token::Select => {
1222 self.advance();
1223 "SELECT".to_string()
1224 }
1225 Token::Insert => {
1226 self.advance();
1227 "INSERT".to_string()
1228 }
1229 // v7.39 (read01 round 60) — CREATE is a privilege word on a
1230 // schema / database, and it lexes as a reserved token.
1231 Token::Create => {
1232 self.advance();
1233 "CREATE".to_string()
1234 }
1235 // NOT upper-cased: in the no-ON shape (`GRANT devs TO
1236 // alice`) these "privilege words" are ROLE NAMES, and a
1237 // role name is case-sensitive. `priv_from_word` folds case
1238 // itself when they really are privileges.
1239 _ => self.expect_ident_like()?,
1240 };
1241 // v7.39 (read01 round 59) — the optional per-privilege COLUMN
1242 // list: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
1243 let columns = if matches!(self.peek(), Token::LParen) {
1244 self.parse_grant_column_list()?
1245 } else {
1246 Vec::new()
1247 };
1248 privileges.push(GrantPriv { word: w, columns });
1249 if matches!(self.peek(), Token::Comma) {
1250 self.advance();
1251 } else {
1252 break;
1253 }
1254 }
1255 }
1256 // v7.39 (read01 round 58) — no ON clause at all = `GRANT devs TO alice`:
1257 // role MEMBERSHIP. The words parsed as "privileges" are the role names.
1258 if !matches!(self.peek(), Token::On) {
1259 let roles: Vec<String> = core::mem::take(&mut privileges)
1260 .into_iter()
1261 .map(|p| p.word)
1262 .collect();
1263 let grantees = self.parse_grantee_list(grant)?;
1264 // `WITH ADMIN OPTION` / `GRANTED BY x` — accepted, ignored (SPG has
1265 // no admin-option layer: a member cannot re-grant).
1266 self.consume_until_statement_boundary();
1267 return Ok(finish_grant(
1268 grant,
1269 GrantStatement {
1270 privileges: Vec::new(),
1271 object: GrantObject::Roles(roles),
1272 grantees,
1273 grant_option,
1274 },
1275 ));
1276 }
1277 self.advance(); // ON
1278 // An optional object-class keyword. `TABLE` (or no keyword at all) is
1279 // the enforced case; anything else parses and no-ops.
1280 let mut class = "TABLE";
1281 match self.peek() {
1282 Token::Table => {
1283 self.advance();
1284 }
1285 Token::All => {
1286 // v7.39 (read01 round 61) — `ALL TABLES IN SCHEMA x` expands to
1287 // every table at GRANT time, like PG. `ALL SEQUENCES/FUNCTIONS
1288 // IN SCHEMA` stay no-ops and keep their own object class.
1289 self.advance(); // ALL
1290 let kind = match self.peek() {
1291 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
1292 // TABLES has its own token (SHOW TABLES owns it).
1293 Token::Tables | Token::Table => "tables".to_string(),
1294 _ => String::new(),
1295 };
1296 if !kind.is_empty() {
1297 self.advance();
1298 }
1299 // `IN SCHEMA <name>`
1300 if matches!(self.peek(), Token::In) {
1301 self.advance();
1302 if self.peek_keyword_ident("schema") {
1303 self.advance();
1304 let _schema = self.expect_ident_like()?;
1305 }
1306 }
1307 if kind != "tables" {
1308 self.consume_until_statement_boundary();
1309 return Ok(finish_grant(
1310 grant,
1311 GrantStatement {
1312 privileges,
1313 object: GrantObject::Other("ALL … IN SCHEMA".into()),
1314 grantees: Vec::new(),
1315 grant_option,
1316 },
1317 ));
1318 }
1319 let grantees = self.parse_grantee_list(grant)?;
1320 self.consume_until_statement_boundary();
1321 return Ok(finish_grant(
1322 grant,
1323 GrantStatement {
1324 privileges,
1325 object: GrantObject::AllTablesInSchema,
1326 grantees,
1327 grant_option,
1328 },
1329 ));
1330 }
1331 Token::Ident(w) | Token::QuotedIdent(w) => {
1332 let lc = w.to_ascii_lowercase();
1333 // v7.39 (read01 round 60) — SEQUENCE / SCHEMA / DATABASE are
1334 // real objects with real ACLs now.
1335 if matches!(lc.as_str(), "sequence" | "schema" | "database") {
1336 self.advance();
1337 let mut names: Vec<String> = Vec::new();
1338 loop {
1339 let mut parts: Vec<String> = Vec::new();
1340 loop {
1341 parts.push(self.expect_ident_like()?);
1342 if matches!(self.peek(), Token::Dot) {
1343 self.advance();
1344 } else {
1345 break;
1346 }
1347 }
1348 names.push(parts.pop().expect("at least one part"));
1349 if matches!(self.peek(), Token::Comma) {
1350 self.advance();
1351 } else {
1352 break;
1353 }
1354 }
1355 let grantees = self.parse_grantee_list(grant)?;
1356 let mut grant_option = grant_option;
1357 if grant && self.peek_keyword_ident("with") {
1358 self.advance();
1359 self.expect_keyword_ident("grant")?;
1360 self.expect_keyword_ident("option")?;
1361 grant_option = true;
1362 }
1363 self.consume_until_statement_boundary();
1364 let object = match lc.as_str() {
1365 "sequence" => GrantObject::Sequences(names),
1366 "schema" => GrantObject::Schemas(names),
1367 _ => GrantObject::Databases(names),
1368 };
1369 return Ok(finish_grant(
1370 grant,
1371 GrantStatement {
1372 privileges,
1373 object,
1374 grantees,
1375 grant_option,
1376 },
1377 ));
1378 }
1379 // v7.39 (read01 round 61) — `ON FUNCTION f(int)` is real. SPG
1380 // keys functions by NAME, so the argument list parses and is
1381 // dropped (an overload set shares one ACL — recorded residual).
1382 if matches!(lc.as_str(), "function" | "procedure" | "routine") {
1383 self.advance();
1384 let mut names: Vec<(String, Option<Vec<String>>)> = Vec::new();
1385 loop {
1386 let mut parts: Vec<String> = Vec::new();
1387 loop {
1388 parts.push(self.expect_ident_like()?);
1389 if matches!(self.peek(), Token::Dot) {
1390 self.advance();
1391 } else {
1392 break;
1393 }
1394 }
1395 let fname = parts.pop().expect("at least one part");
1396 // v7.39 (read01 round 62) — the signature picks the
1397 // overload, so it is captured.
1398 let sig = if matches!(self.peek(), Token::LParen) {
1399 Some(self.parse_function_signature_types()?)
1400 } else {
1401 None
1402 };
1403 names.push((fname, sig));
1404 if matches!(self.peek(), Token::Comma) {
1405 self.advance();
1406 } else {
1407 break;
1408 }
1409 }
1410 let grantees = self.parse_grantee_list(grant)?;
1411 self.consume_until_statement_boundary();
1412 return Ok(finish_grant(
1413 grant,
1414 GrantStatement {
1415 privileges,
1416 object: GrantObject::Functions(names),
1417 grantees,
1418 grant_option,
1419 },
1420 ));
1421 }
1422 if matches!(
1423 lc.as_str(),
1424 "type"
1425 | "domain"
1426 | "language"
1427 | "tablespace"
1428 | "large"
1429 | "foreign"
1430 | "parameter"
1431 ) {
1432 self.consume_until_statement_boundary();
1433 return Ok(finish_grant(
1434 grant,
1435 GrantStatement {
1436 privileges,
1437 object: GrantObject::Other(lc.to_ascii_uppercase()),
1438 grantees: Vec::new(),
1439 grant_option,
1440 },
1441 ));
1442 }
1443 class = "TABLE";
1444 }
1445 _ => {}
1446 }
1447 let _ = class;
1448 // The table list. Schema-qualified names drop their qualifier (SPG is
1449 // single-schema) — but read the dotted parts from raw tokens, since
1450 // `expect_ident_like` would silently swallow the leading part.
1451 let mut tables: Vec<String> = Vec::new();
1452 loop {
1453 let mut parts: Vec<String> = Vec::new();
1454 loop {
1455 parts.push(self.expect_ident_like()?);
1456 if matches!(self.peek(), Token::Dot) {
1457 self.advance();
1458 } else {
1459 break;
1460 }
1461 }
1462 tables.push(parts.pop().expect("at least one part"));
1463 if matches!(self.peek(), Token::Comma) {
1464 self.advance();
1465 } else {
1466 break;
1467 }
1468 }
1469 let grantees = self.parse_grantee_list(grant)?;
1470 if grant && self.peek_keyword_ident("with") {
1471 self.advance();
1472 self.expect_keyword_ident("grant")?;
1473 self.expect_keyword_ident("option")?;
1474 grant_option = true;
1475 }
1476 // REVOKE's trailing CASCADE / RESTRICT — SPG has no dependent grants
1477 // to cascade to (no re-granting), so both are accepted and ignored.
1478 if !grant && (self.peek_keyword_ident("cascade") || self.peek_keyword_ident("restrict")) {
1479 self.advance();
1480 }
1481 Ok(finish_grant(
1482 grant,
1483 GrantStatement {
1484 privileges,
1485 object: GrantObject::Tables(tables),
1486 grantees,
1487 grant_option,
1488 },
1489 ))
1490 }
1491
1492 /// v7.39 (read01 round 62) — the argument TYPES in a function signature:
1493 /// `(int, text)` or `(x int, y text)` (PG accepts either). Returns the type
1494 /// words; the caller normalises them into a signature key.
1495 fn parse_function_signature_types(&mut self) -> Result<Vec<String>, ParseError> {
1496 self.advance(); // (
1497 let mut types: Vec<String> = Vec::new();
1498 if matches!(self.peek(), Token::RParen) {
1499 self.advance();
1500 return Ok(types);
1501 }
1502 loop {
1503 // Collect the words of one argument up to a comma / close paren.
1504 let mut words: Vec<String> = Vec::new();
1505 loop {
1506 match self.peek() {
1507 Token::Comma | Token::RParen | Token::Eof => break,
1508 _ => {}
1509 }
1510 let tok = self.advance();
1511 match tok {
1512 Token::Ident(w) | Token::QuotedIdent(w) => words.push(w),
1513 other => {
1514 if let Some(w) = unreserved_keyword_text(&other) {
1515 words.push(w);
1516 }
1517 }
1518 }
1519 }
1520 // `name TYPE` or a bare `TYPE`. Several of PG's type names are
1521 // themselves several words (`double precision`, `character
1522 // varying`, `timestamp with time zone`), so "two words means the
1523 // first is a parameter name" reads the type off `f(double
1524 // precision)` as `precision`. v7.39 (round 282): recognise the
1525 // multi-word spellings first — a leading word that STARTS one of
1526 // them is part of the type, not a name.
1527 let joined = words.join(" ");
1528 let ty = if words.len() >= 2 && is_multiword_type_phrase(&joined) {
1529 joined
1530 } else if words.len() >= 2 {
1531 words[1..].join(" ")
1532 } else {
1533 words.first().cloned().unwrap_or_default()
1534 };
1535 types.push(ty);
1536 if matches!(self.peek(), Token::Comma) {
1537 self.advance();
1538 } else {
1539 break;
1540 }
1541 }
1542 if matches!(self.peek(), Token::RParen) {
1543 self.advance();
1544 }
1545 Ok(types)
1546 }
1547
1548 /// v7.39 (read01 round 59) — `( col, col, … )` after a privilege word.
1549 fn parse_grant_column_list(&mut self) -> Result<Vec<String>, ParseError> {
1550 self.advance(); // (
1551 let mut cols = Vec::new();
1552 loop {
1553 cols.push(self.expect_ident_like()?);
1554 if matches!(self.peek(), Token::Comma) {
1555 self.advance();
1556 } else {
1557 break;
1558 }
1559 }
1560 if !matches!(self.peek(), Token::RParen) {
1561 return Err(self.err(alloc::format!(
1562 "expected ')' to close the column list, got {:?}",
1563 self.peek()
1564 )));
1565 }
1566 self.advance(); // )
1567 Ok(cols)
1568 }
1569
1570 /// `TO <roles>` (grant) / `FROM <roles>` (revoke). An empty-string entry is
1571 /// PUBLIC.
1572 fn parse_grantee_list(&mut self, grant: bool) -> Result<Vec<String>, ParseError> {
1573 if grant {
1574 if matches!(self.peek(), Token::To) {
1575 self.advance();
1576 } else {
1577 self.expect_keyword_ident("to")?;
1578 }
1579 } else if matches!(self.peek(), Token::From) {
1580 self.advance();
1581 } else {
1582 self.expect_keyword_ident("from")?;
1583 }
1584 let mut grantees: Vec<String> = Vec::new();
1585 loop {
1586 // `GROUP name` is the legacy spelling of a plain role name.
1587 if self.peek_keyword_ident("group") {
1588 self.advance();
1589 }
1590 if self.peek_keyword_ident("public") {
1591 self.advance();
1592 grantees.push(String::new()); // PUBLIC
1593 } else {
1594 grantees.push(self.expect_ident_like()?);
1595 }
1596 if matches!(self.peek(), Token::Comma) {
1597 self.advance();
1598 } else {
1599 break;
1600 }
1601 }
1602 Ok(grantees)
1603 }
1604
1605 /// v7.39 (round 277) — `PREPARE <name> [(type, …)] AS <statement>`.
1606 /// The body keeps its `$N` placeholders; substitution happens at
1607 /// EXECUTE. The declared types are recorded for
1608 /// `pg_prepared_statements.parameter_types` but are not enforced —
1609 /// PG infers when the list is omitted, and SPG resolves the values
1610 /// at substitution time either way.
1611 fn parse_prepare(&mut self) -> Result<Statement, ParseError> {
1612 let start = self.pos;
1613 self.advance(); // PREPARE
1614 // v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'` is 2PC, a
1615 // different statement that happens to share the keyword. PG
1616 // ships with `max_prepared_transactions = 0` and reports it
1617 // this way; SPG has no prepared-transaction registry, so the
1618 // same wording is the accurate answer rather than a dodge.
1619 // Round 277 turned this from a silent no-op into a confusing
1620 // "expected AS in PREPARE" parse error.
1621 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
1622 self.advance();
1623 let gid = match self.advance() {
1624 Token::String(g) => g,
1625 other => {
1626 return Err(self.err(alloc::format!(
1627 "expected a transaction identifier after PREPARE TRANSACTION, got {other:?}"
1628 )));
1629 }
1630 };
1631 return Ok(Statement::PrepareTransaction(gid));
1632 }
1633 let name = self.expect_ident_like()?;
1634 let mut param_types = Vec::new();
1635 if matches!(self.peek(), Token::LParen) {
1636 self.advance();
1637 loop {
1638 let mut ty = self.expect_ident_like()?;
1639 // A parameterised type name (`numeric(10,2)`,
1640 // `varchar(20)`) keeps its argument list in the text.
1641 if matches!(self.peek(), Token::LParen) {
1642 let mut depth = 0usize;
1643 let mut buf = String::from("(");
1644 loop {
1645 match self.advance() {
1646 Token::LParen => {
1647 depth += 1;
1648 if depth > 1 {
1649 buf.push('(');
1650 }
1651 }
1652 Token::RParen => {
1653 depth -= 1;
1654 buf.push(')');
1655 if depth == 0 {
1656 break;
1657 }
1658 }
1659 Token::Comma => buf.push(','),
1660 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
1661 Token::Eof => break,
1662 _ => {}
1663 }
1664 }
1665 ty.push_str(&buf);
1666 }
1667 param_types.push(ty);
1668 match self.peek() {
1669 Token::Comma => {
1670 self.advance();
1671 }
1672 Token::RParen => {
1673 self.advance();
1674 break;
1675 }
1676 other => {
1677 return Err(self.err(alloc::format!(
1678 "expected ',' or ')' in PREPARE parameter list, got {other:?}"
1679 )));
1680 }
1681 }
1682 }
1683 }
1684 if !matches!(self.peek(), Token::As) {
1685 return Err(self.err(alloc::format!(
1686 "expected AS in PREPARE, got {:?}",
1687 self.peek()
1688 )));
1689 }
1690 self.advance();
1691 let body = self.parse_one_statement()?;
1692 // The Parser holds tokens, not the source text, so the
1693 // statement PG reports in `pg_prepared_statements.statement`
1694 // is rebuilt from the AST rather than sliced from the input.
1695 let _ = start;
1696 let mut source = alloc::format!("PREPARE {}", crate::ast::quote_ident(&name));
1697 if !param_types.is_empty() {
1698 source.push_str(" (");
1699 source.push_str(¶m_types.join(", "));
1700 source.push(')');
1701 }
1702 source.push_str(" AS ");
1703 source.push_str(&alloc::format!("{body}"));
1704 Ok(Statement::Prepare {
1705 name,
1706 param_types,
1707 body: alloc::boxed::Box::new(body),
1708 source,
1709 })
1710 }
1711
1712 /// v7.39 (round 277) — `EXECUTE <name> [(<expr>, …)]`.
1713 fn parse_execute(&mut self) -> Result<Statement, ParseError> {
1714 self.advance(); // EXECUTE
1715 let name = self.expect_ident_like()?;
1716 let mut args = Vec::new();
1717 if matches!(self.peek(), Token::LParen) {
1718 self.advance();
1719 if matches!(self.peek(), Token::RParen) {
1720 self.advance();
1721 } else {
1722 loop {
1723 args.push(self.parse_expr(0)?);
1724 match self.advance() {
1725 Token::Comma => {}
1726 Token::RParen => break,
1727 other => {
1728 return Err(self.err(alloc::format!(
1729 "expected ',' or ')' in EXECUTE arguments, got {other:?}"
1730 )));
1731 }
1732 }
1733 }
1734 }
1735 }
1736 Ok(Statement::Execute { name, args })
1737 }
1738
1739 /// v7.39 (round 277) — `DEALLOCATE {[PREPARE] <name> | ALL}`.
1740 /// v7.39 (round 278) — `CALL <proc>([args])`. There is no
1741 /// procedure catalog yet, so this reports PG's not-found error
1742 /// (with its HINT) rather than pretending the call ran.
1743 /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
1744 /// Bare `DISCARD` is a syntax error in PG; so it is here.
1745 fn parse_discard(&mut self) -> Result<Statement, ParseError> {
1746 self.advance(); // DISCARD
1747 let target = match self.advance() {
1748 Token::All => DiscardTarget::All,
1749 Token::Ident(w) | Token::QuotedIdent(w) => match w.to_ascii_lowercase().as_str() {
1750 "all" => DiscardTarget::All,
1751 "plans" => DiscardTarget::Plans,
1752 "sequences" => DiscardTarget::Sequences,
1753 "temp" | "temporary" => DiscardTarget::Temp,
1754 other => {
1755 return Err(self.err(format!(
1756 "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1757 )));
1758 }
1759 },
1760 other => {
1761 return Err(self.err(format!(
1762 "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1763 )));
1764 }
1765 };
1766 Ok(Statement::Discard(target))
1767 }
1768
1769 /// v7.39 (round 318, V51) — MySQL `KILL [HARD|SOFT] [CONNECTION|QUERY]
1770 /// <expr>`. MariaDB accepts an expression for the id (its own docs use
1771 /// `KILL connection_id()`), and the HARD / SOFT prefixes only pick how
1772 /// aggressively the server interrupts, which SPG does not distinguish.
1773 /// Bare `KILL <id>` means CONNECTION.
1774 fn parse_kill(&mut self) -> Result<Statement, ParseError> {
1775 self.advance(); // KILL
1776 let mut query_only = false;
1777 loop {
1778 // CONNECTION is a reserved keyword token (it also opens
1779 // `CREATE SUBSCRIPTION … CONNECTION '…'`), so it arrives as
1780 // `Token::Connection` rather than a bare ident.
1781 if matches!(self.peek(), Token::Connection) {
1782 self.advance();
1783 break;
1784 }
1785 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
1786 break;
1787 };
1788 match w.to_ascii_lowercase().as_str() {
1789 "hard" | "soft" => {
1790 self.advance();
1791 }
1792 "query" => {
1793 self.advance();
1794 query_only = true;
1795 break;
1796 }
1797 _ => break,
1798 }
1799 }
1800 let id = self.parse_expr(0)?;
1801 Ok(Statement::Kill {
1802 query_only,
1803 id: Box::new(id),
1804 })
1805 }
1806
1807 fn parse_call(&mut self) -> Result<Statement, ParseError> {
1808 self.advance(); // CALL
1809 let name = self.expect_ident_like()?;
1810 self.consume_until_statement_boundary();
1811 Ok(Statement::Call(name))
1812 }
1813
1814 fn parse_deallocate(&mut self) -> Result<Statement, ParseError> {
1815 self.advance(); // DEALLOCATE
1816 // PG accepts an optional noise `PREPARE` keyword here.
1817 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("prepare")) {
1818 self.advance();
1819 }
1820 if matches!(self.peek(), Token::All) {
1821 self.advance();
1822 return Ok(Statement::Deallocate(None));
1823 }
1824 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("all")) {
1825 self.advance();
1826 return Ok(Statement::Deallocate(None));
1827 }
1828 let name = self.expect_ident_like()?;
1829 Ok(Statement::Deallocate(Some(name)))
1830 }
1831
1832 fn consume_until_statement_boundary(&mut self) {
1833 loop {
1834 match self.peek() {
1835 Token::Semicolon | Token::Eof => return,
1836 _ => self.advance(),
1837 };
1838 }
1839 }
1840
1841 /// v7.22 (round-13 T2) — consume to the statement boundary like
1842 /// `consume_until_statement_boundary`, but pick out the sequence
1843 /// name on the way: either `SEQUENCE NAME <ident>` (identity
1844 /// columns) or the first string literal (`nextval('<seq>')`).
1845 /// Schema qualifiers and `::regclass` casts are stripped.
1846 fn scan_sequence_name_until_boundary(&mut self) -> Option<String> {
1847 let mut seq: Option<String> = None;
1848 let mut after_sequence_kw = false;
1849 let mut after_name_kw = false;
1850 loop {
1851 match self.peek().clone() {
1852 Token::Semicolon | Token::Eof => break,
1853 Token::Ident(s) | Token::QuotedIdent(s) => {
1854 if after_name_kw && seq.is_none() {
1855 self.advance();
1856 let mut name = s;
1857 // `SEQUENCE NAME public.groups_id_seq` — keep
1858 // the bare name, drop qualifiers.
1859 while matches!(self.peek(), Token::Dot) {
1860 self.advance();
1861 if let Token::Ident(n) | Token::QuotedIdent(n) = self.advance() {
1862 name = n;
1863 }
1864 }
1865 seq = Some(name);
1866 after_name_kw = false;
1867 continue;
1868 }
1869 if after_sequence_kw && s.eq_ignore_ascii_case("name") {
1870 after_name_kw = true;
1871 after_sequence_kw = false;
1872 } else {
1873 after_sequence_kw = s.eq_ignore_ascii_case("sequence");
1874 }
1875 self.advance();
1876 }
1877 Token::String(s) => {
1878 if seq.is_none() {
1879 // `nextval('public.groups_id_seq'::regclass)`
1880 let bare = s
1881 .rsplit_once('.')
1882 .map_or_else(|| s.clone(), |(_, b)| b.to_string());
1883 seq = Some(bare);
1884 }
1885 self.advance();
1886 }
1887 _ => {
1888 after_sequence_kw = false;
1889 after_name_kw = false;
1890 self.advance();
1891 }
1892 }
1893 }
1894 seq
1895 }
1896
1897 /// v7.39 (round 621) — is the next token the keyword `BY`?
1898 ///
1899 /// `pg_get_keywords()` classes `by` as `U` (unreserved), so it is a legal
1900 /// column, table and alias name — and SPG lexed it into a dedicated
1901 /// `Token::By`, which made it unusable as a name ANYWHERE. Of the seven
1902 /// two-letter keywords the lexer knew, this was the only one PG leaves
1903 /// unreserved (`as`, `in`, `on`, `or`, `to` are reserved and `is` is `T`).
1904 ///
1905 /// The token is gone; the three clauses that own the word — GROUP BY,
1906 /// ORDER BY, PARTITION BY — and the handful of other places that expect it
1907 /// ask this instead. Adding it to the unreserved-identifier table was not
1908 /// enough on its own: identifier positions that match the token shape
1909 /// directly (an index's column list, a table alias) never consult that
1910 /// table, so `CREATE INDEX … ON t(by)` and `FROM t AS by` still failed.
1911 /// Not lexing it as a keyword closes the whole class rather than the two
1912 /// positions that happened to be noticed.
1913 fn peek_is_by(&self) -> bool {
1914 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by"))
1915 }
1916
1917 /// v7.39 (round 621) — the optional `CASCADE` / `RESTRICT` trailer a DROP
1918 /// takes. Accepted and dropped: SPG tracks no dependents to cascade to,
1919 /// which is what `DROP TABLE` and `DROP INDEX` have done since v7.14.
1920 fn consume_drop_behaviour(&mut self) {
1921 if matches!(
1922 self.peek(),
1923 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") || s.eq_ignore_ascii_case("restrict")
1924 ) {
1925 self.advance();
1926 }
1927 }
1928
1929 fn expect_ident_like(&mut self) -> Result<String, ParseError> {
1930 let first = match self.advance() {
1931 Token::Ident(s) | Token::QuotedIdent(s) => s,
1932 // v7.37.43-T4 — PG-unreserved keywords are legal identifiers
1933 // per PG's `pg_get_keywords()` classification. SPG tokenizes
1934 // these as named variants for parsing leverage in the
1935 // contexts that own them (`RELEASE SAVEPOINT`, `SHOW name`,
1936 // `BEGIN`, etc.), but they MUST still be usable as table /
1937 // column / alias names in DDL+DML. Sentori migrations like
1938 // 0001_init.sql ship `release TEXT NOT NULL` in the events
1939 // table — the `events.release` column carries the release
1940 // identifier string. Pre-T4 this triggered "expected
1941 // identifier, got Release" and blocked every drop-in user
1942 // whose schema had a column / alias with one of these names.
1943 other if unreserved_keyword_text(&other).is_some() => {
1944 unreserved_keyword_text(&other).unwrap()
1945 }
1946 other => {
1947 return Err(ParseError {
1948 message: format!("expected identifier, got {other:?}"),
1949 token_pos: self.consumed_pos(),
1950 });
1951 }
1952 };
1953 // v7.14.0 — strip optional `<schema>.` prefix. PG dumps
1954 // qualify every name with `public.` (and pg_catalog.* for
1955 // functions); SPG is single-schema so we discard the
1956 // prefix and return only the trailing ident. Same shape
1957 // also handles MySQL `db.tbl` cross-database refs (SPG
1958 // ignores the db part).
1959 if matches!(self.peek(), Token::Dot) {
1960 self.advance();
1961 match self.advance() {
1962 Token::Ident(s) | Token::QuotedIdent(s) => return Ok(s),
1963 other if unreserved_keyword_text(&other).is_some() => {
1964 return Ok(unreserved_keyword_text(&other).unwrap());
1965 }
1966 other => {
1967 return Err(ParseError {
1968 message: format!("expected identifier after '{first}.', got {other:?}"),
1969 token_pos: self.consumed_pos(),
1970 });
1971 }
1972 }
1973 }
1974 Ok(first)
1975 }
1976
1977 #[allow(clippy::too_many_lines)]
1978 fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
1979 // v7.14.0 — empty / comment-only / semicolon-only input
1980 // (after the lexer strips line + block + MySQL
1981 // conditional comments) lands as Statement::Empty.
1982 // pg_dump and mysqldump emit several wrappers that
1983 // collapse to nothing after stripping (`/*!40101 SET …
1984 // */;`, blank lines between statements); the engine
1985 // returns CommandOk no-op so the dump loads cleanly.
1986 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
1987 return Ok(Statement::Empty);
1988 }
1989 // v7.14.0 — pg_dump / mysqldump "noise" statements:
1990 // catalog / metadata DDL that has no behavioural effect
1991 // on SPG's single-schema, single-database, single-user
1992 // model. Consume the whole statement up to the next
1993 // semicolon / EOF and return Empty. This is broader than
1994 // the per-keyword DROP / SET / COMMENT arms but lets the
1995 // long tail of `LOCK TABLES`, `UNLOCK TABLES`, `GRANT`,
1996 // `REVOKE`, `ALTER OWNER TO`, `\restrict`, `\unrestrict`,
1997 // `BEGIN; COMMIT;` wrappers, etc. all pass through.
1998 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
1999 let lc = s.to_ascii_lowercase();
2000 // v7.39 (read01 round 50) — COMMENT ON is a real statement now.
2001 if lc == "comment" {
2002 return self.parse_comment_on();
2003 }
2004 // v7.39 (read01 round 57) — so is GRANT / REVOKE.
2005 if lc == "grant" || lc == "revoke" {
2006 return self.parse_grant_or_revoke(lc == "grant");
2007 }
2008 // v7.39 (round 277) — the SQL-level prepared-statement
2009 // surface is REAL now. It used to be accepted and dropped
2010 // on the theory that "real execution still happens via the
2011 // extended-query flow" — true only for a driver that uses
2012 // that flow; a plain SQL PREPARE / EXECUTE returned no
2013 // rows at all.
2014 if lc == "prepare" {
2015 return self.parse_prepare();
2016 }
2017 if lc == "execute" {
2018 return self.parse_execute();
2019 }
2020 if lc == "deallocate" {
2021 return self.parse_deallocate();
2022 }
2023 // v7.39 (round 278) — `CALL <proc>(<args>)` used to be
2024 // accepted and dropped, so an application's stored-procedure
2025 // invocation reported success and did nothing. SPG has no
2026 // procedure catalog, so every CALL names a procedure that
2027 // does not exist — which is exactly what PG says.
2028 if lc == "call" {
2029 return self.parse_call();
2030 }
2031 // v7.39 (round 318, V51) — MySQL `KILL`. Not dump noise: it
2032 // names one connection and acts on it.
2033 if lc == "kill" {
2034 return self.parse_kill();
2035 }
2036 if lc == "discard" {
2037 return self.parse_discard();
2038 }
2039 // v7.39 (round 696) — REASSIGN OWNED BY <role> [, …] TO <role>.
2040 // Still performs nothing; the roles are carried out so a name
2041 // that does not exist is refused, as PG18 refuses it.
2042 if lc == "reassign" {
2043 self.advance();
2044 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("owned")) {
2045 self.advance();
2046 }
2047 if self.peek_is_by() {
2048 self.advance();
2049 }
2050 // Only the roles BEFORE the TO are the ones that must
2051 // exist — `TO` names the new owner, which PG checks as
2052 // well, so both lists are collected.
2053 let mut names = self.take_comma_separated_names();
2054 if matches!(self.peek(), Token::To) {
2055 self.advance();
2056 names.extend(self.take_comma_separated_names());
2057 }
2058 self.consume_until_statement_boundary();
2059 return Ok(Statement::ValidateOnly {
2060 kind: crate::ast::ValidateOnlyKind::RoleName,
2061 names,
2062 });
2063 }
2064 // v7.39 (round 696) — SECURITY LABEL. PG18 refuses it
2065 // unconditionally with `no security label providers have been
2066 // loaded`, whatever object it names, because none is loaded.
2067 // SPG has none either; accepting it told the caller a label had
2068 // been applied when nothing anywhere records one.
2069 if lc == "security" {
2070 self.consume_until_statement_boundary();
2071 return Ok(Statement::ValidateOnly {
2072 kind: crate::ast::ValidateOnlyKind::SecurityLabel,
2073 names: Vec::new(),
2074 });
2075 }
2076 if is_dump_noise_statement(&lc) {
2077 self.consume_until_statement_boundary();
2078 return Ok(Statement::Empty);
2079 }
2080 }
2081 match self.peek() {
2082 Token::Select => self.parse_select_stmt(),
2083 // v7.37.17 (17.6 siblings) — a statement opening with a
2084 // parenthesized query group: `(SELECT … UNION …)
2085 // INTERSECT …`. parse_bare_select's group arm consumes
2086 // the parens; the select parser handles the outer chain
2087 // and tail.
2088 Token::LParen
2089 if matches!(
2090 self.tokens.get(self.pos + 1),
2091 Some(Token::Select | Token::LParen | Token::Values)
2092 ) =>
2093 {
2094 self.parse_select_stmt()
2095 }
2096 // v7.37.17 (17.6 siblings) — top-level bare VALUES
2097 // statement (`VALUES (1), (2) [ORDER BY …] [LIMIT …]`).
2098 // Lowers to the same UNION ALL chain the FROM-position
2099 // form uses, then reuses the shared SELECT tail.
2100 Token::Values => {
2101 self.advance(); // VALUES
2102 let mut head = self.parse_values_rows_body()?;
2103 self.parse_select_tail_into(&mut head)?;
2104 Ok(Statement::Select(head))
2105 }
2106 // SQL-standard `TABLE name` shorthand for
2107 // `SELECT * FROM name` — pg_dump never emits it, but
2108 // psql users and PG docs use it constantly. Set-op
2109 // chains and the ORDER BY/LIMIT tail compose like any
2110 // SELECT head.
2111 Token::Table
2112 if matches!(
2113 self.tokens.get(self.pos + 1),
2114 Some(Token::Ident(_) | Token::QuotedIdent(_))
2115 ) =>
2116 {
2117 let mut head = self.parse_table_shorthand()?;
2118 self.parse_setop_chain_into(&mut head)?;
2119 self.parse_select_tail_into(&mut head)?;
2120 Ok(Statement::Select(head))
2121 }
2122 // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2123 // body is a dollar-quoted plpgsql block (lexer already
2124 // collapsed `$$…$$` into a single Token::String).
2125 // v7.16.2 — mailrs round-10 A.2: parse the body as a
2126 // real PlPgSqlBlock so the engine can EXECUTE it at
2127 // top level instead of silently swallowing. Pre-
2128 // v7.16.2 the parser threw the body away and the
2129 // engine returned CommandOk for the entire DO; that
2130 // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2131 // $$` into a SEV-1 silent no-op (the IF + the rename
2132 // were both invisible — mailrs's migrate-042 didn't
2133 // actually run). Now the body parses + executes;
2134 // EmbeddedSql inside the block runs immediately
2135 // against the engine (not deferred — we're at top
2136 // level, not inside a trigger row-write loop).
2137 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2138 self.advance();
2139 let body_text = match self.advance() {
2140 Token::String(s) => s,
2141 other => {
2142 return Err(self.err(alloc::format!(
2143 "expected dollar-quoted body after DO, got {other:?}"
2144 )));
2145 }
2146 };
2147 // Optional `LANGUAGE <name>` trailer (idents only).
2148 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2149 self.advance();
2150 let _ = self.expect_ident_like()?;
2151 }
2152 // Parse the body — same shape CREATE FUNCTION
2153 // uses for trigger function bodies. If the body
2154 // doesn't parse cleanly we surface the error
2155 // (better than silent no-op).
2156 let block = parse_plpgsql_body(&body_text)?;
2157 Ok(Statement::DoBlock(block))
2158 }
2159 // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2160 // WITH isn't a reserved token in our lexer — comes through
2161 // as `Token::Ident("with")` (case-insensitive).
2162 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2163 self.advance();
2164 self.parse_with_cte_then_select()
2165 }
2166 // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2167 // an identifier — not a reserved keyword.
2168 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2169 self.advance();
2170 let mut analyze = false;
2171 let mut suggest = false;
2172 let mut costs_off = false;
2173 let mut buffers = false;
2174 let mut timing_off = false;
2175 let mut settings = false;
2176 let mut wal = false;
2177 let mut summary_off = false;
2178 let mut format = crate::ast::ExplainFormat::Text;
2179 // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2180 // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2181 // options are comma-separated. Booleans default to ON
2182 // when the value token is omitted (matches PG).
2183 if matches!(self.peek(), Token::LParen) {
2184 self.advance();
2185 loop {
2186 let opt = match self.peek().clone() {
2187 Token::Ident(s) | Token::QuotedIdent(s) => s,
2188 other => {
2189 return Err(self.err(format!(
2190 "expected option keyword inside EXPLAIN (…), got {other:?}"
2191 )));
2192 }
2193 };
2194 self.advance();
2195 if opt.eq_ignore_ascii_case("suggest") {
2196 suggest = true;
2197 // SUGGEST takes no explicit value today.
2198 } else if opt.eq_ignore_ascii_case("costs") {
2199 // PG syntax: `COSTS [ON | OFF]`. Default
2200 // when value omitted is ON, so plain
2201 // `COSTS` is a no-op. `COSTS OFF` flips.
2202 // `ON` lexes to `Token::On` (reserved
2203 // keyword in JOIN ... ON contexts); accept
2204 // it alongside the bare Ident form so the
2205 // grammar matches PG verbatim.
2206 let value = match self.peek().clone() {
2207 Token::On => {
2208 self.advance();
2209 true
2210 }
2211 Token::Ident(v) | Token::QuotedIdent(v)
2212 if v.eq_ignore_ascii_case("off") =>
2213 {
2214 self.advance();
2215 false
2216 }
2217 Token::Ident(v) | Token::QuotedIdent(v)
2218 if v.eq_ignore_ascii_case("true") =>
2219 {
2220 self.advance();
2221 true
2222 }
2223 _ => true,
2224 };
2225 costs_off = !value;
2226 } else if opt.eq_ignore_ascii_case("analyze")
2227 || opt.eq_ignore_ascii_case("analyse")
2228 {
2229 // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2230 // Same default-ON rule as ANALYZE keyword form.
2231 let value = match self.peek().clone() {
2232 Token::On => {
2233 self.advance();
2234 true
2235 }
2236 Token::Ident(v) | Token::QuotedIdent(v)
2237 if v.eq_ignore_ascii_case("off") =>
2238 {
2239 self.advance();
2240 false
2241 }
2242 Token::Ident(v) | Token::QuotedIdent(v)
2243 if v.eq_ignore_ascii_case("true") =>
2244 {
2245 self.advance();
2246 true
2247 }
2248 _ => true,
2249 };
2250 analyze = value;
2251 } else if opt.eq_ignore_ascii_case("buffers") {
2252 // v7.37.22 — `BUFFERS [ON|OFF]`.
2253 let value = match self.peek().clone() {
2254 Token::On => {
2255 self.advance();
2256 true
2257 }
2258 Token::Ident(v) | Token::QuotedIdent(v)
2259 if v.eq_ignore_ascii_case("off") =>
2260 {
2261 self.advance();
2262 false
2263 }
2264 Token::Ident(v) | Token::QuotedIdent(v)
2265 if v.eq_ignore_ascii_case("true") =>
2266 {
2267 self.advance();
2268 true
2269 }
2270 _ => true,
2271 };
2272 buffers = value;
2273 } else if opt.eq_ignore_ascii_case("timing") {
2274 // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2275 // the measured wall-clock annotation.
2276 let value = match self.peek().clone() {
2277 Token::On => {
2278 self.advance();
2279 true
2280 }
2281 Token::Ident(v) | Token::QuotedIdent(v)
2282 if v.eq_ignore_ascii_case("off") =>
2283 {
2284 self.advance();
2285 false
2286 }
2287 Token::Ident(v) | Token::QuotedIdent(v)
2288 if v.eq_ignore_ascii_case("true") =>
2289 {
2290 self.advance();
2291 true
2292 }
2293 _ => true,
2294 };
2295 timing_off = !value;
2296 } else if opt.eq_ignore_ascii_case("settings") {
2297 settings = true;
2298 } else if opt.eq_ignore_ascii_case("wal") {
2299 wal = true;
2300 } else if opt.eq_ignore_ascii_case("summary") {
2301 // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2302 // gates the trailing Planning/Execution Time
2303 // lines now (was accept-and-no-op).
2304 let value = match self.peek().clone() {
2305 Token::On => {
2306 self.advance();
2307 true
2308 }
2309 Token::Ident(v) | Token::QuotedIdent(v)
2310 if v.eq_ignore_ascii_case("off") =>
2311 {
2312 self.advance();
2313 false
2314 }
2315 Token::Ident(v) | Token::QuotedIdent(v)
2316 if v.eq_ignore_ascii_case("true") =>
2317 {
2318 self.advance();
2319 true
2320 }
2321 _ => true,
2322 };
2323 summary_off = !value;
2324 } else if opt.eq_ignore_ascii_case("verbose")
2325 || opt.eq_ignore_ascii_case("format")
2326 {
2327 // v7.37.22 — accept-but-no-op the remaining
2328 // PG options so EXPLAIN-using clients
2329 // (pgAdmin / DataGrip) don't see syntax
2330 // errors. FORMAT takes a value (text /
2331 // json / yaml / xml); skip the next token
2332 // if it's an ident.
2333 if opt.eq_ignore_ascii_case("format") {
2334 if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2335 {
2336 self.advance();
2337 format = match v.to_ascii_lowercase().as_str() {
2338 "text" => crate::ast::ExplainFormat::Text,
2339 "json" => crate::ast::ExplainFormat::Json,
2340 "xml" => crate::ast::ExplainFormat::Xml,
2341 "yaml" => crate::ast::ExplainFormat::Yaml,
2342 other => {
2343 return Err(self.err(format!(
2344 "EXPLAIN (FORMAT …): unknown format {other:?}; \
2345 supports text, json, xml, yaml"
2346 )));
2347 }
2348 };
2349 }
2350 } else {
2351 // VERBOSE / SUMMARY take optional ON/OFF;
2352 // consume if present.
2353 if matches!(self.peek(), Token::On) {
2354 self.advance();
2355 } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2356 self.peek().clone()
2357 && (v.eq_ignore_ascii_case("off")
2358 || v.eq_ignore_ascii_case("true"))
2359 {
2360 self.advance();
2361 let _ = v;
2362 }
2363 }
2364 } else {
2365 return Err(self.err(format!(
2366 "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2367 )));
2368 }
2369 if matches!(self.peek(), Token::Comma) {
2370 self.advance();
2371 continue;
2372 }
2373 break;
2374 }
2375 if !matches!(self.peek(), Token::RParen) {
2376 return Err(self.err(format!(
2377 "expected ')' after EXPLAIN options, got {:?}",
2378 self.peek()
2379 )));
2380 }
2381 self.advance();
2382 } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2383 && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2384 {
2385 self.advance();
2386 analyze = true;
2387 }
2388 // v7.39 (round 224) — the body may open with WITH (CTEs);
2389 // route through the same CTE-then-SELECT path the top-level
2390 // WITH statement uses. v7.39 (round 225) — DML bodies parse
2391 // too (PG explains INSERT / UPDATE / DELETE).
2392 let inner = match self.peek().clone() {
2393 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2394 self.advance();
2395 self.parse_with_cte_then_select()?
2396 }
2397 Token::Insert => self.parse_insert_stmt(false)?,
2398 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2399 self.advance();
2400 self.parse_update_after_keyword()?
2401 }
2402 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2403 self.advance();
2404 self.parse_delete_after_keyword()?
2405 }
2406 _ => self.parse_select_stmt()?,
2407 };
2408 if !matches!(
2409 inner,
2410 Statement::Select(_)
2411 | Statement::Insert(_)
2412 | Statement::Update(_)
2413 | Statement::Delete(_)
2414 ) {
2415 return Err(self.err(format!(
2416 "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2417 )));
2418 }
2419 Ok(Statement::Explain(crate::ast::ExplainStatement {
2420 analyze,
2421 inner: Box::new(inner),
2422 suggest,
2423 costs_off,
2424 buffers,
2425 timing_off,
2426 settings,
2427 wal,
2428 summary_off,
2429 format,
2430 }))
2431 }
2432 Token::Create => self.parse_create_stmt(),
2433 Token::Insert => self.parse_insert_stmt(false),
2434 // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2435 // spelling; route to the same handler. DESC is the
2436 // reserved ORDER BY token, so it gets its own arm.
2437 Token::Ident(s)
2438 if s.eq_ignore_ascii_case("describe")
2439 && matches!(
2440 self.tokens.get(self.pos + 1),
2441 Some(Token::Ident(_) | Token::QuotedIdent(_))
2442 ) =>
2443 {
2444 self.advance();
2445 let table = self.expect_ident_like()?;
2446 Ok(Statement::ShowColumns(table))
2447 }
2448 Token::Desc
2449 if matches!(
2450 self.tokens.get(self.pos + 1),
2451 Some(Token::Ident(_) | Token::QuotedIdent(_))
2452 ) =>
2453 {
2454 self.advance();
2455 let table = self.expect_ident_like()?;
2456 Ok(Statement::ShowColumns(table))
2457 }
2458 // `COPY table [(cols)] TO STDOUT` — the export half of
2459 // pg_dump's COPY pair (the FROM stdin half rides the
2460 // embed import path). Options need a format design and
2461 // error honestly.
2462 Token::Ident(s)
2463 if s.eq_ignore_ascii_case("copy")
2464 && matches!(
2465 self.tokens.get(self.pos + 1),
2466 Some(Token::Ident(_) | Token::QuotedIdent(_))
2467 ) =>
2468 {
2469 self.advance(); // COPY
2470 let table = self.expect_ident_like()?;
2471 let columns = if matches!(self.peek(), Token::LParen) {
2472 self.advance();
2473 let mut cols = alloc::vec![self.expect_ident_like()?];
2474 while matches!(self.peek(), Token::Comma) {
2475 self.advance();
2476 cols.push(self.expect_ident_like()?);
2477 }
2478 if !matches!(self.peek(), Token::RParen) {
2479 return Err(self.err(format!(
2480 "expected ')' after COPY column list, got {:?}",
2481 self.peek()
2482 )));
2483 }
2484 self.advance();
2485 Some(cols)
2486 } else {
2487 None
2488 };
2489 // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2490 // endpoint. (FROM STDIN still rides the wire/import path —
2491 // its data arrives out of band.)
2492 if matches!(self.peek(), Token::From)
2493 && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2494 {
2495 self.advance(); // FROM
2496 let Token::String(path) = self.advance() else {
2497 unreachable!()
2498 };
2499 let options = self.parse_copy_to_options()?;
2500 return Ok(Statement::CopyFromFile {
2501 table,
2502 columns,
2503 path,
2504 options,
2505 });
2506 }
2507 if !matches!(self.peek(), Token::To) {
2508 return Err(self.err(format!(
2509 "COPY: only TO STDOUT is supported here (FROM stdin \
2510 rides the import path); got {:?}",
2511 self.peek()
2512 )));
2513 }
2514 self.advance();
2515 if matches!(self.peek(), Token::String(_)) {
2516 let Token::String(path) = self.advance() else { unreachable!() };
2517 let options = self.parse_copy_to_options()?;
2518 return Ok(Statement::CopyToFile {
2519 table,
2520 columns,
2521 query: None,
2522 path,
2523 options,
2524 });
2525 }
2526 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2527 return Err(self.err(format!(
2528 "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2529 self.peek()
2530 )));
2531 }
2532 self.advance();
2533 let options = self.parse_copy_to_options()?;
2534 Ok(Statement::CopyTo {
2535 table,
2536 columns,
2537 query: None,
2538 options,
2539 })
2540 }
2541 // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2542 // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2543 // result set is streamed in COPY format (PG's query form).
2544 Token::Ident(s)
2545 if s.eq_ignore_ascii_case("copy")
2546 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2547 {
2548 self.advance(); // COPY
2549 self.advance(); // (
2550 let query = self.parse_select_stmt()?;
2551 if !matches!(self.peek(), Token::RParen) {
2552 return Err(self.err(format!(
2553 "expected ')' after COPY query, got {:?}",
2554 self.peek()
2555 )));
2556 }
2557 self.advance(); // )
2558 if !matches!(self.peek(), Token::To) {
2559 return Err(self.err(format!(
2560 "COPY (query): only TO STDOUT is supported, got {:?}",
2561 self.peek()
2562 )));
2563 }
2564 self.advance();
2565 if matches!(self.peek(), Token::String(_)) {
2566 let Token::String(path) = self.advance() else { unreachable!() };
2567 let options = self.parse_copy_to_options()?;
2568 return Ok(Statement::CopyToFile {
2569 table: String::new(),
2570 columns: None,
2571 query: Some(alloc::boxed::Box::new(query)),
2572 path,
2573 options,
2574 });
2575 }
2576 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2577 return Err(self.err(format!(
2578 "COPY (query): TO supports STDOUT only, got {:?}",
2579 self.peek()
2580 )));
2581 }
2582 self.advance();
2583 let options = self.parse_copy_to_options()?;
2584 Ok(Statement::CopyTo {
2585 table: String::new(),
2586 columns: None,
2587 query: Some(alloc::boxed::Box::new(query)),
2588 options,
2589 })
2590 }
2591 // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2592 // Shares the INSERT body; the replace flag lowers it
2593 // onto ON CONFLICT DO UPDATE with an empty assignment
2594 // list (engine: replace the whole row).
2595 Token::Ident(s)
2596 if s.eq_ignore_ascii_case("replace")
2597 && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2598 {
2599 self.parse_insert_stmt(true)
2600 }
2601 Token::Begin => {
2602 self.advance();
2603 // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2604 // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2605 // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2606 // is consumed first, then the trailing modes — including the
2607 // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2608 // WORK/TRANSACTION). The explicit level, when present, rides the
2609 // statement so `exec_begin` applies it for this transaction.
2610 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2611 {
2612 self.advance();
2613 }
2614 let iso = self.parse_isolation_level_clauses()?;
2615 Ok(Statement::Begin(iso))
2616 }
2617 // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2618 // for BEGIN. START is contextual in PG too; pattern-match
2619 // on the ident here. Iso clauses are parse-and-ignored,
2620 // same as BEGIN above.
2621 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2622 self.advance();
2623 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2624 {
2625 return Err(self.err(alloc::format!(
2626 "expected TRANSACTION after START, got {:?}",
2627 self.peek()
2628 )));
2629 }
2630 self.advance();
2631 let iso = self.parse_isolation_level_clauses()?;
2632 Ok(Statement::Begin(iso))
2633 }
2634 Token::Commit => {
2635 self.advance();
2636 Ok(Statement::Commit)
2637 }
2638 Token::Rollback => {
2639 self.advance();
2640 // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2641 // savepoint without ending the transaction. Bare
2642 // `ROLLBACK` drops the whole TX.
2643 if matches!(self.peek(), Token::To) {
2644 self.advance();
2645 if matches!(self.peek(), Token::Savepoint) {
2646 self.advance();
2647 }
2648 let name = self.expect_ident_like()?;
2649 Ok(Statement::RollbackToSavepoint(name))
2650 } else {
2651 Ok(Statement::Rollback)
2652 }
2653 }
2654 Token::Savepoint => {
2655 self.advance();
2656 let name = self.expect_ident_like()?;
2657 Ok(Statement::Savepoint(name))
2658 }
2659 Token::Release => {
2660 self.advance();
2661 // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2662 // is optional in standard SQL.
2663 if matches!(self.peek(), Token::Savepoint) {
2664 self.advance();
2665 }
2666 let name = self.expect_ident_like()?;
2667 Ok(Statement::ReleaseSavepoint(name))
2668 }
2669 Token::Show => {
2670 self.advance();
2671 // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2672 // v6.1.2 promoted TABLES to a reserved keyword (for
2673 // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2674 // arrives as `Token::Tables` rather than a bare ident.
2675 // USERS / COLUMNS remain bare idents.
2676 let target = match self.advance() {
2677 Token::Tables => "tables".to_string(),
2678 // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2679 // keyword token; recognise it as the SHOW CREATE
2680 // dispatch keyword too.
2681 Token::Create => "create".to_string(),
2682 // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2683 // keyword too; let SHOW INDEX FROM parse.
2684 Token::Index => "index".to_string(),
2685 // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2686 // reserved (used in aggregate function calls);
2687 // recognise it here so the parser dispatches
2688 // to ShowParameter("all") — the engine returns
2689 // the curated parameter inventory.
2690 Token::All => "all".to_string(),
2691 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2692 other => {
2693 return Err(self.err(format!(
2694 "expected SHOW target, got {other:?}"
2695 )));
2696 }
2697 };
2698 match target.as_str() {
2699 "tables" => Ok(Statement::ShowTables),
2700 "users" => Ok(Statement::ShowUsers),
2701 // v7.38 轴 4 — `SHOW transaction_isolation`
2702 // returns the currently-selected isolation level.
2703 "transaction_isolation" => Ok(Statement::ShowParameter(
2704 "transaction_isolation".to_string(),
2705 )),
2706 // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2707 // TABLE <t>` returns a 2-column row: (Table,
2708 // Create Table). mysqldump emits this for every
2709 // table at scrape time; without it the dump
2710 // round-trip stalls.
2711 // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2712 // FROM <t>` (also spelled `SHOW INDEX` and
2713 // `SHOW KEYS`). admin / mysqldump probes use
2714 // it to list per-table indexes.
2715 "indexes" | "index" | "keys" => {
2716 if !matches!(self.peek(), Token::From) {
2717 return Err(self.err(format!(
2718 "expected FROM after SHOW INDEXES, got {:?}",
2719 self.peek()
2720 )));
2721 }
2722 self.advance();
2723 let table = self.expect_ident_like()?;
2724 Ok(Statement::ShowIndexes(table))
2725 }
2726 // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2727 // `SHOW VARIABLES`. Both return a 2-column row
2728 // set listing server-side state; clients probe
2729 // them at connect time.
2730 "status" => Ok(Statement::ShowStatus),
2731 "variables" => Ok(Statement::ShowVariables),
2732 // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2733 "processlist" => Ok(Statement::ShowProcesslist),
2734 "create" => {
2735 // SHOW CREATE TABLE / VIEW / DATABASE — only
2736 // TABLE is supported in v7.17.
2737 let kind = match self.advance() {
2738 Token::Ident(s) | Token::QuotedIdent(s) => s,
2739 Token::Table => "table".to_string(),
2740 other => {
2741 return Err(self.err(format!(
2742 "expected TABLE after SHOW CREATE, got {other:?}"
2743 )));
2744 }
2745 };
2746 if !kind.eq_ignore_ascii_case("table") {
2747 return Err(self.err(format!(
2748 "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2749 )));
2750 }
2751 let name = self.expect_ident_like()?;
2752 Ok(Statement::ShowCreateTable(name))
2753 }
2754 // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2755 // (and `SHOW SCHEMAS` alias). The mysql client uses
2756 // it to populate the database selector at connect
2757 // time; without it `mysql -p` errors before the
2758 // first user query.
2759 "databases" | "schemas" => Ok(Statement::ShowDatabases),
2760 // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2761 // keyword on its own; it lands here as a bare
2762 // ident. Returning all publications + their
2763 // scope summary.
2764 "publications" => Ok(Statement::ShowPublications),
2765 // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2766 "subscriptions" => Ok(Statement::ShowSubscriptions),
2767 "columns" => {
2768 if !matches!(self.peek(), Token::From) {
2769 return Err(self.err(format!(
2770 "expected FROM after SHOW COLUMNS, got {:?}",
2771 self.peek()
2772 )));
2773 }
2774 self.advance();
2775 let table = self.expect_ident_like()?;
2776 Ok(Statement::ShowColumns(table))
2777 }
2778 // v7.38 轴 4 surface — `SHOW <param>` for any
2779 // remaining session / preset parameter name
2780 // (server_version, search_path, client_encoding,
2781 // …). The engine's ShowParameter handler does the
2782 // dispatch; unrecognised names error there with
2783 // a pointer to pg_settings, not at parse time —
2784 // so a driver that issues `SHOW spam_setting`
2785 // gets a clear runtime error instead of a
2786 // confusing "unknown SHOW target".
2787 other => {
2788 // v7.38 (read01 P3.20) — a custom namespaced GUC
2789 // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
2790 // consume the dotted tail so it round-trips with
2791 // `SET app.foo` / `current_setting('app.foo')`.
2792 let mut full = other.to_string();
2793 while matches!(self.peek(), Token::Dot) {
2794 self.advance();
2795 let seg = self.expect_ident_like()?;
2796 full.push('.');
2797 full.push_str(&seg.to_ascii_lowercase());
2798 }
2799 Ok(Statement::ShowParameter(full))
2800 }
2801 }
2802 }
2803 // v6.1.2: `DROP` is now a reserved keyword (it dispatches
2804 // to DROP USER and DROP PUBLICATION today; DROP TABLE /
2805 // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
2806 // arrived as a bare ident; tokenising it dedicatedly
2807 // keeps the dispatch tree small.
2808 Token::Drop => {
2809 self.advance();
2810 match self.peek() {
2811 // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
2812 // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
2813 // around DROP ROLE cleanup. SPG has no role-owner
2814 // model, so consume to boundary as a no-op.
2815 Token::Ident(s) | Token::QuotedIdent(s)
2816 if s.eq_ignore_ascii_case("owned") =>
2817 {
2818 // v7.39 (round 696) — still a no-op (SPG has no
2819 // role-owner model), but the ROLE is carried out so
2820 // the engine can refuse one that does not exist,
2821 // which is what PG18 does.
2822 self.advance();
2823 if self.peek_is_by() {
2824 self.advance();
2825 }
2826 let names = self.take_comma_separated_names();
2827 self.consume_until_statement_boundary();
2828 Ok(Statement::ValidateOnly {
2829 kind: crate::ast::ValidateOnlyKind::RoleName,
2830 names,
2831 })
2832 }
2833 // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
2834 // It drops only a TEMPORARY table, and name resolution
2835 // already prefers the session's own, so the keyword is
2836 // consumed and the ordinary DROP TABLE path runs.
2837 Token::Ident(s) | Token::QuotedIdent(s)
2838 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
2839 {
2840 self.advance();
2841 if !matches!(self.peek(), Token::Table) {
2842 return Err(self.err(alloc::format!(
2843 "expected TABLE after DROP TEMPORARY, got {:?}",
2844 self.peek()
2845 )));
2846 }
2847 self.parse_drop_table_after_keyword()
2848 }
2849 Token::Publication => {
2850 self.advance();
2851 // v7.39 (round 754, F31-B4) — the round-753
2852 // audit probe tripped over the missing
2853 // `IF EXISTS` here (syntax error).
2854 let if_exists = self.consume_if_exists();
2855 let name = self.expect_ident_or_string()?;
2856 Ok(Statement::DropPublication { name, if_exists })
2857 }
2858 Token::Subscription => {
2859 self.advance();
2860 let if_exists = self.consume_if_exists();
2861 let name = self.expect_ident_or_string()?;
2862 Ok(Statement::DropSubscription { name, if_exists })
2863 }
2864 Token::Ident(s) | Token::QuotedIdent(s)
2865 if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
2866 {
2867 self.advance();
2868 // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
2869 // login user IS a role in PG, and SPG's store holds
2870 // both. `IF EXISTS` is accepted on either spelling.
2871 let if_exists = self.consume_if_exists();
2872 let name = self.expect_ident_or_string()?;
2873 Ok(Statement::DropUser { name, if_exists })
2874 }
2875 // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
2876 // CREATE DATABASE has parsed since v7.14 and this did
2877 // not, so `DROP DATABASE IF EXISTS x` — what every
2878 // teardown script and pg_dumpall preamble opens with —
2879 // came back as a syntax error, which IF EXISTS cannot
2880 // soften. The name is carried so the engine can answer
2881 // the way PG does; PG never lets this succeed on a
2882 // single-database server, since the name is either
2883 // unknown ("database … does not exist", or a notice
2884 // under IF EXISTS) or the one you are connected to
2885 // ("cannot drop the currently open database").
2886 Token::Ident(s) | Token::QuotedIdent(s)
2887 if s.eq_ignore_ascii_case("database") =>
2888 {
2889 self.advance();
2890 let if_exists = self.consume_if_exists();
2891 let name = self.expect_ident_or_string()?;
2892 self.consume_until_statement_boundary();
2893 Ok(Statement::DropDatabase { name, if_exists })
2894 }
2895 // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
2896 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
2897 self.advance();
2898 let if_exists = self.consume_if_exists();
2899 let name = self.expect_ident_like()?;
2900 // ON <table>
2901 if !matches!(self.peek(), Token::On) {
2902 return Err(self.err(alloc::format!(
2903 "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
2904 self.peek()
2905 )));
2906 }
2907 self.advance();
2908 let table = self.expect_ident_like()?;
2909 Ok(Statement::DropTrigger {
2910 name,
2911 table,
2912 if_exists,
2913 })
2914 }
2915 // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
2916 // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
2917 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
2918 self.advance();
2919 let if_exists = self.consume_if_exists();
2920 let name = self.expect_ident_like()?;
2921 if !matches!(self.peek(), Token::On) {
2922 return Err(self.err(alloc::format!(
2923 "expected ON <table> after DROP RULE {name:?}, got {:?}",
2924 self.peek()
2925 )));
2926 }
2927 self.advance();
2928 let table = self.expect_ident_like()?;
2929 // Optional CASCADE / RESTRICT — accepted, no effect.
2930 self.consume_until_statement_boundary();
2931 Ok(Statement::DropRule {
2932 name,
2933 table,
2934 if_exists,
2935 })
2936 }
2937 // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
2938 // v7.12.4 ignores any optional arg-list (signature-
2939 // based overload disambiguation lands in v7.12.5+).
2940 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
2941 self.advance();
2942 let if_exists = self.consume_if_exists();
2943 let name = self.expect_ident_like()?;
2944 // v7.39 (read01 round 62) — the argument list identifies
2945 // WHICH overload to drop, so it is captured, not
2946 // discarded. `DROP FUNCTION f` (no list) is legal when
2947 // the name is unambiguous; the engine enforces that.
2948 let args = if matches!(self.peek(), Token::LParen) {
2949 Some(self.parse_function_signature_types()?)
2950 } else {
2951 None
2952 };
2953 // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
2954 // trailer, which `DROP TABLE` and `DROP INDEX` have
2955 // accepted since v7.14 and this one refused outright.
2956 // pg_dump writes it, so refusing was a parse error in
2957 // the middle of a restore. SPG drops the function
2958 // either way — it tracks no dependents to cascade to —
2959 // which is the same reading the other two give it.
2960 self.consume_drop_behaviour();
2961 Ok(Statement::DropFunction {
2962 name,
2963 args,
2964 if_exists,
2965 })
2966 }
2967 // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
2968 // [CASCADE|RESTRICT]. pg_dump and mysqldump both
2969 // emit DROP TABLE IF EXISTS at the head of every
2970 // CREATE TABLE block so re-importing a dump
2971 // overwrites prior state. SPG accepts and removes
2972 // matching tables; CASCADE/RESTRICT trailers
2973 // accepted silently.
2974 Token::Table => self.parse_drop_table_after_keyword(),
2975 // v7.14.0 — DROP INDEX [IF EXISTS] name
2976 // [CASCADE|RESTRICT]. PG / mysqldump emit this
2977 // for partial-index renames and pgvector
2978 // migrations. SPG removes the matching index;
2979 // IF EXISTS makes the drop idempotent.
2980 Token::Index => {
2981 self.advance();
2982 let if_exists = self.consume_if_exists();
2983 let name = self.expect_ident_like()?;
2984 if matches!(
2985 self.peek(),
2986 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
2987 || s.eq_ignore_ascii_case("restrict")
2988 ) {
2989 self.advance();
2990 }
2991 Ok(Statement::DropIndex { name, if_exists })
2992 }
2993 // v7.14.0 — DROP SCHEMA [IF EXISTS] name
2994 // [CASCADE|RESTRICT]. SPG is single-database;
2995 // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
2996 // name [, name…] [CASCADE | RESTRICT]. Real
2997 // unregister (was silent no-op pre-v7.17).
2998 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
2999 self.advance();
3000 let if_exists = self.consume_if_exists();
3001 let mut names = vec![self.expect_ident_like()?];
3002 while matches!(self.peek(), Token::Comma) {
3003 self.advance();
3004 names.push(self.expect_ident_like()?);
3005 }
3006 if matches!(
3007 self.peek(),
3008 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3009 || s.eq_ignore_ascii_case("restrict")
3010 ) {
3011 self.advance();
3012 }
3013 Ok(Statement::DropSchema { names, if_exists })
3014 }
3015 // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3016 // name [, name…] [CASCADE|RESTRICT].
3017 Token::Ident(s) | Token::QuotedIdent(s)
3018 if s.eq_ignore_ascii_case("type") =>
3019 {
3020 self.advance();
3021 let if_exists = self.consume_if_exists();
3022 let mut names = vec![self.expect_ident_like()?];
3023 while matches!(self.peek(), Token::Comma) {
3024 self.advance();
3025 names.push(self.expect_ident_like()?);
3026 }
3027 if matches!(
3028 self.peek(),
3029 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3030 || s.eq_ignore_ascii_case("restrict")
3031 ) {
3032 self.advance();
3033 }
3034 Ok(Statement::DropType { names, if_exists })
3035 }
3036 // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3037 // name [, name…] [CASCADE|RESTRICT].
3038 Token::Ident(s) | Token::QuotedIdent(s)
3039 if s.eq_ignore_ascii_case("domain") =>
3040 {
3041 self.advance();
3042 let if_exists = self.consume_if_exists();
3043 let mut names = vec![self.expect_ident_like()?];
3044 while matches!(self.peek(), Token::Comma) {
3045 self.advance();
3046 names.push(self.expect_ident_like()?);
3047 }
3048 if matches!(
3049 self.peek(),
3050 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3051 || s.eq_ignore_ascii_case("restrict")
3052 ) {
3053 self.advance();
3054 }
3055 Ok(Statement::DropDomain { names, if_exists })
3056 }
3057 // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3058 // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3059 Token::Ident(s) | Token::QuotedIdent(s)
3060 if s.eq_ignore_ascii_case("materialized") =>
3061 {
3062 self.advance();
3063 let nxt = self.peek().clone();
3064 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3065 {
3066 return Err(self.err(alloc::format!(
3067 "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3068 )));
3069 }
3070 self.advance();
3071 let if_exists = self.consume_if_exists();
3072 let mut names = vec![self.expect_ident_like()?];
3073 while matches!(self.peek(), Token::Comma) {
3074 self.advance();
3075 names.push(self.expect_ident_like()?);
3076 }
3077 if matches!(
3078 self.peek(),
3079 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3080 || s.eq_ignore_ascii_case("restrict")
3081 ) {
3082 self.advance();
3083 }
3084 Ok(Statement::DropMaterializedView { names, if_exists })
3085 }
3086 // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3087 // name [, name…] [CASCADE|RESTRICT].
3088 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3089 self.advance();
3090 let if_exists = self.consume_if_exists();
3091 let mut names = vec![self.expect_ident_like()?];
3092 while matches!(self.peek(), Token::Comma) {
3093 self.advance();
3094 names.push(self.expect_ident_like()?);
3095 }
3096 if matches!(
3097 self.peek(),
3098 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3099 || s.eq_ignore_ascii_case("restrict")
3100 ) {
3101 self.advance();
3102 }
3103 Ok(Statement::DropView { names, if_exists })
3104 }
3105 // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3106 // [CASCADE|RESTRICT]. Real removal from catalog
3107 // (was a silent no-op pre-v7.17).
3108 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3109 self.advance();
3110 let if_exists = self.consume_if_exists();
3111 let mut names = vec![self.expect_ident_like()?];
3112 while matches!(self.peek(), Token::Comma) {
3113 self.advance();
3114 names.push(self.expect_ident_like()?);
3115 }
3116 if matches!(
3117 self.peek(),
3118 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3119 || s.eq_ignore_ascii_case("restrict")
3120 ) {
3121 self.advance();
3122 }
3123 Ok(Statement::DropSequence { names, if_exists })
3124 }
3125 // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3126 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3127 self.advance();
3128 self.parse_drop_policy_after_keyword()
3129 }
3130 // v7.37.17 (17.6 siblings) — DROP <target> for
3131 // targets SPG doesn't natively track. pg_dump
3132 // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3133 // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3134 // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3135 // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3136 // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3137 // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3138 // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3139 // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3140 // etc. — accept + Empty-return so pg_dump tails
3141 // load through. Materialized-view drop dispatches
3142 // to the existing DropTable path when the token
3143 // is Materialized-View-shaped (elsewhere in
3144 // this parser).
3145 Token::Ident(s) | Token::QuotedIdent(s)
3146 if s.eq_ignore_ascii_case("text")
3147 // The DROP dispatch matches on PEEK — `text` is
3148 // not yet consumed, so SEARCH/CONFIGURATION sit
3149 // at pos+1/pos+2 (the round-695 trap's mirror).
3150 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3151 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3152 {
3153 // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3154 // validates the name; DICTIONARY / PARSER / TEMPLATE
3155 // stay in the noise arm below.
3156 self.advance(); // TEXT
3157 self.advance(); // SEARCH
3158 self.advance(); // CONFIGURATION
3159 let if_exists = self.consume_if_exists();
3160 let names = self.take_comma_separated_names();
3161 self.consume_until_statement_boundary();
3162 if if_exists {
3163 return Ok(Statement::Empty);
3164 }
3165 Ok(Statement::ValidateOnly {
3166 kind: crate::ast::ValidateOnlyKind::TsConfigName,
3167 names,
3168 })
3169 }
3170 Token::Ident(s) | Token::QuotedIdent(s)
3171 if matches!(
3172 s.to_ascii_lowercase().as_str(),
3173 "type"
3174 | "domain"
3175 | "operator"
3176 | "cast"
3177 // `text` = TEXT SEARCH DICTIONARY / PARSER /
3178 // TEMPLATE (CONFIGURATION intercepted above).
3179 | "text"
3180 | "materialized"
3181 | "large"
3182 | "role"
3183 | "access"
3184 | "procedure"
3185 | "routine"
3186 ) =>
3187 {
3188 self.consume_until_statement_boundary();
3189 Ok(Statement::Empty)
3190 }
3191 // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3192 // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3193 // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3194 // foreign-data warning family (round 706) so a
3195 // CREATE→DROP sequence in a dump stays consistent.
3196 Token::Ident(s) | Token::QuotedIdent(s)
3197 if s.eq_ignore_ascii_case("server")
3198 || s.eq_ignore_ascii_case("foreign") =>
3199 {
3200 self.advance();
3201 self.consume_until_statement_boundary();
3202 Ok(Statement::ValidateOnly {
3203 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3204 names: Vec::new(),
3205 })
3206 }
3207 Token::Ident(s) | Token::QuotedIdent(s)
3208 if s.eq_ignore_ascii_case("collation")
3209 || s.eq_ignore_ascii_case("tablespace") =>
3210 {
3211 let kind = if s.eq_ignore_ascii_case("collation") {
3212 crate::ast::ValidateOnlyKind::CollationName
3213 } else {
3214 crate::ast::ValidateOnlyKind::TablespaceName
3215 };
3216 self.advance();
3217 let if_exists = self.consume_if_exists();
3218 let names = self.take_comma_separated_names();
3219 self.consume_until_statement_boundary();
3220 if if_exists {
3221 return Ok(Statement::Empty);
3222 }
3223 Ok(Statement::ValidateOnly { kind, names })
3224 }
3225 Token::Ident(s) | Token::QuotedIdent(s)
3226 if s.eq_ignore_ascii_case("event") =>
3227 {
3228 self.advance();
3229 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3230 {
3231 self.advance();
3232 }
3233 let if_exists = self.consume_if_exists();
3234 let names = self.take_comma_separated_names();
3235 self.consume_until_statement_boundary();
3236 if if_exists {
3237 return Ok(Statement::Empty);
3238 }
3239 Ok(Statement::ValidateOnly {
3240 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3241 names,
3242 })
3243 }
3244 // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3245 // leave the noise list; see the ValidateOnly kinds.
3246 Token::Ident(s) | Token::QuotedIdent(s)
3247 if s.eq_ignore_ascii_case("conversion")
3248 || s.eq_ignore_ascii_case("language")
3249 // `DROP PROCEDURAL LANGUAGE` puts the modifier
3250 // FIRST — the first draft looked for it after.
3251 || s.eq_ignore_ascii_case("procedural") =>
3252 {
3253 let kind = if s.eq_ignore_ascii_case("conversion") {
3254 crate::ast::ValidateOnlyKind::ConversionName
3255 } else {
3256 crate::ast::ValidateOnlyKind::LanguageName
3257 };
3258 self.advance();
3259 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3260 {
3261 self.advance();
3262 }
3263 let if_exists = self.consume_if_exists();
3264 let names = self.take_comma_separated_names();
3265 self.consume_until_statement_boundary();
3266 if if_exists {
3267 return Ok(Statement::Empty);
3268 }
3269 Ok(Statement::ValidateOnly { kind, names })
3270 }
3271 // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3272 // name(argtypes)[, …]`. Parsed for real so the engine
3273 // can answer as PG does; see Statement::DropAggregate.
3274 Token::Ident(s) | Token::QuotedIdent(s)
3275 if s.eq_ignore_ascii_case("aggregate") =>
3276 {
3277 self.advance();
3278 let if_exists = self.consume_if_exists();
3279 let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3280 loop {
3281 let name = self.expect_ident_like()?;
3282 if !matches!(self.peek(), Token::LParen) {
3283 return Err(self.err(alloc::format!(
3284 "expected argument list after DROP AGGREGATE {name}"
3285 )));
3286 }
3287 self.advance();
3288 let mut args: Vec<String> = Vec::new();
3289 let mut star = false;
3290 loop {
3291 match self.peek().clone() {
3292 Token::RParen => {
3293 self.advance();
3294 break;
3295 }
3296 Token::Star => {
3297 self.advance();
3298 star = true;
3299 }
3300 Token::Comma => {
3301 self.advance();
3302 }
3303 _ => {
3304 // A type name may be multi-token
3305 // (`double precision`); glue idents
3306 // until , or ).
3307 let mut t = self.expect_ident_like()?;
3308 while let Token::Ident(nx) = self.peek() {
3309 let nx = nx.clone();
3310 self.advance();
3311 t.push(' ');
3312 t.push_str(&nx);
3313 }
3314 args.push(t);
3315 }
3316 }
3317 }
3318 items.push((name, if star { None } else { Some(args) }));
3319 if matches!(self.peek(), Token::Comma) {
3320 self.advance();
3321 } else {
3322 break;
3323 }
3324 }
3325 self.consume_until_statement_boundary();
3326 Ok(Statement::DropAggregate { if_exists, items })
3327 }
3328 // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3329 // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3330 // installed; `IF EXISTS` is the spelling that says do
3331 // not, and it keeps the no-op.
3332 Token::Ident(s) | Token::QuotedIdent(s)
3333 if s.eq_ignore_ascii_case("extension") =>
3334 {
3335 self.advance();
3336 let if_exists = self.consume_if_exists();
3337 let names = self.take_comma_separated_names();
3338 self.consume_until_statement_boundary();
3339 if if_exists {
3340 return Ok(Statement::Empty);
3341 }
3342 Ok(Statement::ValidateOnly {
3343 kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3344 names,
3345 })
3346 }
3347 Token::Ident(s) | Token::QuotedIdent(s)
3348 if s.eq_ignore_ascii_case("statistics") =>
3349 {
3350 self.parse_drop_statistics_after_drop()
3351 }
3352 other => Err(self.err(format!(
3353 "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3354 SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3355 ))),
3356 }
3357 }
3358 // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3359 // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3360 // and accepted before the view name. SPG materialised
3361 // views re-evaluate on read (always-fresh semantics), so
3362 // the CONCURRENTLY-vs-serial distinction has no runtime
3363 // effect — the refresh body does not block readers either
3364 // way. Same accept-and-no-op pattern as DETACH PARTITION
3365 // CONCURRENTLY (16.5).
3366 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3367 self.advance();
3368 let nxt = self.peek().clone();
3369 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3370 {
3371 return Err(self.err(alloc::format!(
3372 "expected MATERIALIZED after REFRESH, got {nxt:?}"
3373 )));
3374 }
3375 self.advance();
3376 let nxt2 = self.peek().clone();
3377 if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3378 {
3379 return Err(self.err(alloc::format!(
3380 "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3381 )));
3382 }
3383 self.advance();
3384 // Optional CONCURRENTLY noise word — consumed without
3385 // changing semantics.
3386 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3387 {
3388 self.advance();
3389 }
3390 let name = self.expect_ident_like()?;
3391 let with_data = self.parse_optional_with_data(true)?;
3392 Ok(Statement::RefreshMaterializedView { name, with_data })
3393 }
3394 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3395 self.advance();
3396 self.parse_update_after_keyword()
3397 }
3398 // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3399 // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3400 // [CASCADE | RESTRICT]. Clears every row from each named
3401 // table. Parses at the top level; the engine dispatcher
3402 // walks Statement::Truncate.
3403 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3404 self.advance();
3405 // Optional TABLE noise word — PG accepts both the reserved
3406 // token and the bare identifier spelling.
3407 if matches!(self.peek(), Token::Table)
3408 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3409 {
3410 self.advance();
3411 }
3412 // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3413 // not absorbed. The lookahead keeps a table genuinely
3414 // called `only` working: the keyword is a keyword only
3415 // when a name follows it.
3416 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3417 if s.eq_ignore_ascii_case("only"))
3418 && matches!(
3419 self.tokens.get(self.pos + 1),
3420 Some(Token::Ident(_) | Token::QuotedIdent(_))
3421 );
3422 if only {
3423 self.advance();
3424 }
3425 // Table names (comma-separated).
3426 let mut tables = Vec::new();
3427 loop {
3428 tables.push(self.expect_ident_like()?);
3429 if matches!(self.peek(), Token::Comma) {
3430 self.advance();
3431 continue;
3432 }
3433 break;
3434 }
3435 // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3436 let mut restart_identity = false;
3437 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3438 {
3439 self.advance();
3440 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3441 {
3442 self.advance();
3443 restart_identity = true;
3444 }
3445 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3446 {
3447 self.advance();
3448 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3449 {
3450 self.advance();
3451 }
3452 }
3453 // Optional CASCADE / RESTRICT.
3454 let mut cascade = false;
3455 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3456 {
3457 self.advance();
3458 cascade = true;
3459 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3460 {
3461 self.advance();
3462 }
3463 Ok(Statement::Truncate {
3464 tables,
3465 restart_identity,
3466 cascade,
3467 only,
3468 })
3469 }
3470 // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3471 // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3472 // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3473 // rows change so the index tree is always up-to-date;
3474 // REINDEX is a strict no-op. Accept the whole statement
3475 // shape to boundary for pg_dump round-trip compatibility.
3476 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3477 // v7.39 (round 535) — the target is CARRIED now. SPG has no
3478 // index bloat to rebuild, so the work stays a no-op, but PG
3479 // validates what it was pointed at and this swallowed the
3480 // name at parse time — `REINDEX TABLE typo` reported
3481 // success. Measured on PG18: INDEX / TABLE name a relation,
3482 // SCHEMA a schema, SYSTEM nothing.
3483 self.advance();
3484 self.parse_reindex_tail()
3485 }
3486 // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3487 // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3488 // SPG has no MVCC bloat today (Phase D visibility map
3489 // queues with v7.38); the freezer collapses hot-tier
3490 // rows into cold segments automatically. VACUUM is a
3491 // no-op — pg_dump maintenance scripts and Discourse's
3492 // periodic-maintenance path both emit it.
3493 // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3494 // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3495 // actual bloat, so the pre-MVCC accept-and-ignore posture
3496 // became a silent no-op on a customer's manual reclaim.
3497 // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3498 // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3499 // ANALYZE is captured, the optional table name is captured.
3500 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3501 self.advance();
3502 // Parenthesised option list: absorb it.
3503 if matches!(self.peek(), Token::LParen) {
3504 let mut depth = 0usize;
3505 loop {
3506 match self.advance() {
3507 Token::LParen => depth += 1,
3508 Token::RParen => {
3509 depth -= 1;
3510 if depth == 0 {
3511 break;
3512 }
3513 }
3514 Token::Eof => break,
3515 _ => {}
3516 }
3517 }
3518 }
3519 let mut analyze = false;
3520 let mut table: Option<String> = None;
3521 loop {
3522 match self.peek() {
3523 // v7.39 (round 535) — `FULL` lexes as a keyword, not
3524 // an identifier, so the loop below broke out on it and
3525 // dropped the table name: `VACUUM FULL nosuch` was
3526 // accepted where `VACUUM nosuch` was refused.
3527 Token::Full => {
3528 self.advance();
3529 }
3530 Token::Ident(w) | Token::QuotedIdent(w) => {
3531 let wl = w.to_ascii_lowercase();
3532 match wl.as_str() {
3533 "full" | "freeze" | "verbose" => {
3534 self.advance();
3535 }
3536 "analyze" | "analyse" => {
3537 analyze = true;
3538 self.advance();
3539 }
3540 _ => {
3541 table = Some(self.expect_ident_like()?);
3542 break;
3543 }
3544 }
3545 }
3546 _ => break,
3547 }
3548 }
3549 // Optional trailing column list / anything else to the
3550 // statement boundary (PG accepts per-column ANALYZE).
3551 self.consume_until_statement_boundary();
3552 Ok(Statement::Vacuum { table, analyze })
3553 }
3554 // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3555 // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3556 // <index>. PG stores rows in physical order matching
3557 // an index; SPG's hot-tier is append-only + cold-tier
3558 // is segment-frozen, so clustering has no persistent
3559 // effect. Accept-and-no-op for pg_dump compat.
3560 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3561 // v7.39 (round 535) — same as REINDEX above: the relation is
3562 // carried so the engine can refuse one that does not exist.
3563 // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3564 self.advance();
3565 self.parse_cluster_tail()
3566 }
3567 // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3568 // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3569 // optional string payload; UNLISTEN takes a channel or `*`.
3570 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3571 self.advance();
3572 let ch = match self.advance() {
3573 Token::Ident(c) | Token::QuotedIdent(c) => c,
3574 other => {
3575 return Err(self.err(format!(
3576 "expected channel name after LISTEN, got {other:?}"
3577 )));
3578 }
3579 };
3580 Ok(Statement::Listen(ch))
3581 }
3582 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3583 self.advance();
3584 let channel = match self.advance() {
3585 Token::Ident(c) | Token::QuotedIdent(c) => c,
3586 other => {
3587 return Err(self.err(format!(
3588 "expected channel name after NOTIFY, got {other:?}"
3589 )));
3590 }
3591 };
3592 let payload = if matches!(self.peek(), Token::Comma) {
3593 self.advance();
3594 match self.advance() {
3595 Token::String(p) => Some(p),
3596 other => {
3597 return Err(self.err(format!(
3598 "expected string payload after NOTIFY <channel>, got {other:?}"
3599 )));
3600 }
3601 }
3602 } else {
3603 None
3604 };
3605 Ok(Statement::Notify { channel, payload })
3606 }
3607 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3608 self.advance();
3609 match self.advance() {
3610 Token::Star => Ok(Statement::Unlisten(None)),
3611 Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3612 other => Err(self.err(format!(
3613 "expected channel name or * after UNLISTEN, got {other:?}"
3614 ))),
3615 }
3616 }
3617 // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3618 // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3619 // process-wide write lock today; explicit LOCK has no
3620 // effect. Accept-and-no-op for pg_dump / migration
3621 // compat.
3622 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3623 self.advance();
3624 // v7.39 (round 696) — the LOCK still has no effect (SPG's
3625 // engine holds a process-wide write lock), but the TABLE
3626 // NAME is now carried out so the engine can refuse one that
3627 // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3628 // READ|WRITE` is a different statement with the same first
3629 // word; it keeps the old no-op, because a MySQL dump's
3630 // bracket names tables it is about to create.
3631 let mysql_tables = matches!(self.peek(), Token::Ident(k)
3632 if k.eq_ignore_ascii_case("tables"));
3633 if mysql_tables {
3634 self.consume_until_statement_boundary();
3635 return Ok(Statement::Empty);
3636 }
3637 if matches!(self.peek(), Token::Table) {
3638 self.advance();
3639 }
3640 let names = self.take_comma_separated_names();
3641 self.consume_until_statement_boundary();
3642 Ok(Statement::ValidateOnly {
3643 kind: crate::ast::ValidateOnlyKind::LockTable,
3644 names,
3645 })
3646 }
3647 // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3648 // durability marker + snapshot in PG. SPG has WAL
3649 // checkpointing on a byte / time schedule (v7.37.10
3650 // 60s / 4 MiB defaults). The bare statement parses to
3651 // `Statement::Empty` here (the no_std engine owns no
3652 // WAL / snapshot); v7.37 Epic Du wires the HOST
3653 // (embedded `Database::execute_buffered`, via
3654 // `sql_is_checkpoint`) to force an immediate synchronous
3655 // checkpoint through `Database::checkpoint` — a real
3656 // durability barrier, matching PG.
3657 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3658 self.advance();
3659 self.consume_until_statement_boundary();
3660 Ok(Statement::Empty)
3661 }
3662 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3663 self.advance();
3664 self.parse_delete_after_keyword()
3665 }
3666 // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3667 // ALTER is not a reserved keyword in the lexer — handled
3668 // as a bare ident here.
3669 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3670 self.advance();
3671 self.parse_alter_after_keyword()
3672 }
3673 // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3674 // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3675 // additions needed.
3676 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3677 self.advance();
3678 self.parse_wait_after_keyword()
3679 }
3680 // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3681 // Bare ANALYZE → analyse every user table; ANALYZE
3682 // <name> → re-stats one. The argument is an optional
3683 // ident (or quoted ident); anything else is a parse
3684 // error.
3685 // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3686 // `WHERE` filter (carved out per V6_7_DESIGN.md
3687 // STABILITY). Lex order: identifier "compact" → "cold"
3688 // → "segments". Anything else after `COMPACT` is a
3689 // parse error.
3690 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3691 self.advance();
3692 let next = self.peek().clone();
3693 let cold = match next {
3694 Token::Ident(s) | Token::QuotedIdent(s) => s,
3695 _ => {
3696 return Err(
3697 self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3698 );
3699 }
3700 };
3701 if !cold.eq_ignore_ascii_case("cold") {
3702 return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3703 }
3704 self.advance();
3705 let next = self.peek().clone();
3706 let segments = match next {
3707 Token::Ident(s) | Token::QuotedIdent(s) => s,
3708 _ => {
3709 return Err(self.err(format!(
3710 "expected SEGMENTS after COMPACT COLD, got {:?}",
3711 self.peek()
3712 )));
3713 }
3714 };
3715 if !segments.eq_ignore_ascii_case("segments") {
3716 return Err(self.err(format!(
3717 "expected SEGMENTS after COMPACT COLD, got {segments:?}"
3718 )));
3719 }
3720 self.advance();
3721 Ok(Statement::CompactColdSegments)
3722 }
3723 // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
3724 // Parsed as a case-insensitive identifier since MERGE
3725 // isn't a reserved lexer keyword (collides with the
3726 // mysqldump `ALGORITHM = MERGE` view clause if it
3727 // were); the inner parser drives the rest of the
3728 // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
3729 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
3730 self.advance();
3731 self.parse_merge_after_keyword()
3732 }
3733 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
3734 self.advance();
3735 let target = match self.peek() {
3736 Token::Eof | Token::Semicolon => None,
3737 Token::Ident(_) | Token::QuotedIdent(_) => {
3738 Some(self.expect_ident_like()?)
3739 }
3740 other => {
3741 return Err(self.err(format!(
3742 "expected table name or end of statement after ANALYZE, got {other:?}"
3743 )));
3744 }
3745 };
3746 // v7.39 (round 776, F31 J7) — the per-column form
3747 // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
3748 // here while the VACUUM arm already consumed it; SPG
3749 // analyzes whole tables, so the list parses and is
3750 // accepted like the VACUUM path's.
3751 if target.is_some() && matches!(self.peek(), Token::LParen) {
3752 self.advance();
3753 loop {
3754 let _ = self.expect_ident_like()?;
3755 match self.peek() {
3756 Token::Comma => {
3757 self.advance();
3758 }
3759 Token::RParen => {
3760 self.advance();
3761 break;
3762 }
3763 other => {
3764 return Err(self.err(format!(
3765 "expected ',' or ')' in ANALYZE column list, got {other:?}"
3766 )));
3767 }
3768 }
3769 }
3770 }
3771 Ok(Statement::Analyze(target))
3772 }
3773 // v7.12.1 — `SET <name> [TO|=] <value>`. The
3774 // `default_text_search_config` parameter is consumed
3775 // by the FTS function dispatcher; other parameter
3776 // names are recorded but treated as a no-op so PG
3777 // dump output loads.
3778 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
3779 self.advance();
3780 // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
3781 // adds `SET GLOBAL` too (and the alias `SET @@global.name =
3782 // …` which the SessionVar path handles). `LOCAL` is the only
3783 // one that changes semantics — it scopes the change to the
3784 // current transaction — so capture it; SESSION / GLOBAL are
3785 // accepted and treated as the default session scope.
3786 let mut set_local = false;
3787 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
3788 let q = s.to_ascii_lowercase();
3789 if q == "local" || q == "session" || q == "global" {
3790 set_local = q == "local";
3791 self.advance();
3792 }
3793 }
3794 // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
3795 // <collation>]` — change the connection client
3796 // charset. SPG stores UTF-8 always and orders
3797 // bytewise; accept as a no-op.
3798 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
3799 {
3800 self.advance();
3801 // Charset ident-or-string.
3802 if matches!(
3803 self.peek(),
3804 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3805 ) {
3806 self.advance();
3807 }
3808 // Optional `COLLATE <name>`.
3809 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
3810 {
3811 self.advance();
3812 if matches!(
3813 self.peek(),
3814 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3815 ) {
3816 self.advance();
3817 }
3818 }
3819 return Ok(Statement::Empty);
3820 }
3821 // v7.37.17 (17.6 sibling) — PG `SET ROLE
3822 // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
3823 // uses this to switch to the object owner before
3824 // recreating tables. SPG has no role system so this
3825 // is a no-op.
3826 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
3827 {
3828 self.advance(); // ROLE
3829 // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
3830 // reset to the login identity; a name / string sets the
3831 // effective role that drives current_user + RLS.
3832 let role = match self.peek().clone() {
3833 Token::Default => {
3834 self.advance();
3835 None
3836 }
3837 Token::Ident(s) | Token::QuotedIdent(s)
3838 if s.eq_ignore_ascii_case("none") =>
3839 {
3840 self.advance();
3841 None
3842 }
3843 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
3844 self.advance();
3845 Some(s)
3846 }
3847 _ => None,
3848 };
3849 return Ok(Statement::SetRole(role));
3850 }
3851 // v7.37.17 (17.6 sibling) — PG `SET SESSION
3852 // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
3853 // ISO SQL surface). pg_dump prepends this to fix
3854 // the isolation level for the restore session. SPG
3855 // defaults to READ COMMITTED and doesn't yet honor
3856 // session-set isolation across statements — accept
3857 // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
3858 // per-tx form is handled elsewhere.
3859 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
3860 {
3861 self.advance(); // CHARACTERISTICS
3862 self.consume_until_statement_boundary();
3863 return Ok(Statement::Empty);
3864 }
3865 // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
3866 // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
3867 // pg_dump emits this to control the deferrability of
3868 // FK / UNIQUE constraints across a bulk restore. SPG
3869 // has no deferrable-constraint machinery today; the
3870 // FK checker is strict-immediate. Accept-and-no-op
3871 // for pg_dump round-trip compatibility.
3872 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
3873 {
3874 self.advance(); // CONSTRAINTS
3875 // v7.39 (round 288) — no longer a no-op: the trailing
3876 // DEFERRED / IMMEDIATE sets the transaction's timing.
3877 // v7.39 (round 308, V29) — and the names are kept.
3878 // They used to be skipped over on the way to the
3879 // DEFERRED keyword, so a named form silently behaved
3880 // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
3881 // every deferrable constraint in the transaction.
3882 let mut names: alloc::vec::Vec<alloc::string::String> =
3883 alloc::vec::Vec::new();
3884 if matches!(self.peek(), Token::All) {
3885 self.advance();
3886 } else {
3887 loop {
3888 let mut n = self.expect_ident_like()?;
3889 // A schema-qualified name (`public.fk_a`)
3890 // identifies the same constraint; PG resolves
3891 // it by the trailing segment.
3892 while matches!(self.peek(), Token::Dot) {
3893 self.advance();
3894 n = self.expect_ident_like()?;
3895 }
3896 names.push(n);
3897 if matches!(self.peek(), Token::Comma) {
3898 self.advance();
3899 } else {
3900 break;
3901 }
3902 }
3903 }
3904 let deferred = match self.peek() {
3905 Token::Ident(s) | Token::QuotedIdent(s)
3906 if s.eq_ignore_ascii_case("deferred") =>
3907 {
3908 true
3909 }
3910 Token::Ident(s) | Token::QuotedIdent(s)
3911 if s.eq_ignore_ascii_case("immediate") =>
3912 {
3913 false
3914 }
3915 other => {
3916 return Err(self.err(alloc::format!(
3917 "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
3918 )));
3919 }
3920 };
3921 self.advance();
3922 return Ok(Statement::SetConstraints { names, deferred });
3923 }
3924 // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
3925 // { DEFAULT | '<role>' | <ident> }` (mailrs
3926 // round-10 A.1). pg_dump preamble emits the
3927 // `DEFAULT` form to reset session authorization.
3928 //
3929 // v7.39 (round 697) — this said "SPG has no role system so
3930 // this is a strict no-op". SPG has had one since round 58;
3931 // the comment outlived it, and with it the reason a name
3932 // that is not a role was accepted here. It still switches
3933 // no authorization — what it does now is refuse a role
3934 // that does not exist, as PG18 does. PG also accepts `RESET SESSION
3935 // AUTHORIZATION` (handled by the RESET parser
3936 // elsewhere). Reference:
3937 // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
3938 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
3939 {
3940 self.advance(); // AUTHORIZATION
3941 match self.peek().clone() {
3942 Token::Default => {
3943 self.advance();
3944 }
3945 Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
3946 self.advance();
3947 return Ok(Statement::ValidateOnly {
3948 kind: crate::ast::ValidateOnlyKind::RoleName,
3949 names: alloc::vec![r],
3950 });
3951 }
3952 other => {
3953 return Err(self.err(alloc::format!(
3954 "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
3955 )));
3956 }
3957 }
3958 return Ok(Statement::Empty);
3959 }
3960 // v7.38 轴 4 — `SET [SESSION] TRANSACTION
3961 // ISOLATION LEVEL { READ COMMITTED | READ
3962 // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
3963 // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
3964 // PG-standard surface. v7.37.8 accepts the syntax
3965 // and tracks the selected level on
3966 // `Engine::current_isolation_level()`; the actual
3967 // MVCC / SSI semantics implementation lands in
3968 // the 轴 4 isolation framework (separate train).
3969 // PG itself maps READ UNCOMMITTED to READ COMMITTED
3970 // internally; SPG behaves the same (effectively
3971 // READ COMMITTED at every level today).
3972 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
3973 {
3974 self.advance(); // TRANSACTION
3975 let level = self.parse_isolation_level_clauses()?.unwrap_or_default();
3976 return Ok(Statement::SetTransaction { isolation: level });
3977 }
3978 // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
3979 // alias — same accept-as-no-op as SET NAMES.
3980 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
3981 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
3982 {
3983 self.advance(); // CHARACTER
3984 self.advance(); // SET
3985 if matches!(
3986 self.peek(),
3987 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3988 ) {
3989 self.advance();
3990 }
3991 return Ok(Statement::Empty);
3992 }
3993 // v7.39 (GUC) — PG spells the timezone GUC as two
3994 // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
3995 // where <value> is a string/ident or the LOCAL /
3996 // DEFAULT keyword (both mean "back to the default").
3997 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
3998 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
3999 {
4000 self.advance(); // TIME
4001 self.advance(); // ZONE
4002 let value = match self.peek().clone() {
4003 Token::Ident(s)
4004 if s.eq_ignore_ascii_case("local")
4005 || s.eq_ignore_ascii_case("default") =>
4006 {
4007 self.advance();
4008 crate::ast::SetValue::Default
4009 }
4010 Token::Default => {
4011 self.advance();
4012 crate::ast::SetValue::Default
4013 }
4014 _ => self.parse_set_value()?,
4015 };
4016 return Ok(Statement::SetParameter {
4017 name: "timezone".into(),
4018 value,
4019 local: set_local,
4020 });
4021 }
4022 // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4023 // MySQL USER-variable assignment: its own per-session
4024 // namespace, an arbitrary expression on the right, and `:=`
4025 // as a second spelling of `=`. It used to fall into the
4026 // session-PARAMETER list below, whose values are literals and
4027 // whose store nothing reads back under a `@` name — so the
4028 // assignment reported success and vanished.
4029 //
4030 // A `@@`-prefixed LHS is a real engine setting and keeps the
4031 // old path.
4032 if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4033 return self.parse_set_user_vars();
4034 }
4035 // v7.14.0 — multi-assignment form
4036 // `SET a = 1, b = 2, …`. Single-assignment is the
4037 // 1-element case. Each LHS may be a regular ident
4038 // or a SessionVar (`@VAR` / `@@VAR`).
4039 let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4040 loop {
4041 let lhs = match self.peek().clone() {
4042 Token::SessionVar(s) => {
4043 self.advance();
4044 s
4045 }
4046 Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4047 other => {
4048 return Err(self.err(format!(
4049 "expected parameter name after SET, got {other:?}"
4050 )));
4051 }
4052 };
4053 // Accept either `=` or the bare `TO` keyword.
4054 match self.peek() {
4055 Token::Eq => {
4056 self.advance();
4057 }
4058 Token::To => {
4059 self.advance();
4060 }
4061 other => {
4062 return Err(self.err(format!(
4063 "expected `=` or TO after SET {lhs}, got {other:?}"
4064 )));
4065 }
4066 }
4067 let mut value = self.parse_set_value()?;
4068 // v7.39 (GUC) — disambiguate the comma: `, name =` /
4069 // `, name TO` continues a MySQL-style multi-assign,
4070 // anything else is a PG list VALUE
4071 // (`SET search_path = myschema, public`) folded into
4072 // one comma-joined string.
4073 while matches!(self.peek(), Token::Comma) {
4074 let is_assign = matches!(
4075 self.tokens.get(self.pos + 1),
4076 Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4077 ) && matches!(
4078 self.tokens.get(self.pos + 2),
4079 Some(Token::Eq | Token::To)
4080 );
4081 if is_assign {
4082 break;
4083 }
4084 self.advance(); // comma
4085 let next = self.parse_set_value()?;
4086 let joined = alloc::format!(
4087 "{}, {}",
4088 set_value_text(&value),
4089 set_value_text(&next)
4090 );
4091 value = crate::ast::SetValue::String(joined);
4092 }
4093 pairs.push((lhs, value));
4094 if matches!(self.peek(), Token::Comma) {
4095 self.advance();
4096 continue;
4097 }
4098 break;
4099 }
4100 if pairs.len() == 1 {
4101 let (name, value) = pairs.into_iter().next().unwrap();
4102 Ok(Statement::SetParameter {
4103 name,
4104 value,
4105 local: set_local,
4106 })
4107 } else {
4108 Ok(Statement::SetParameterList(pairs))
4109 }
4110 }
4111 // v7.12.1 — `RESET <name>` / `RESET ALL`.
4112 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4113 self.advance();
4114 match self.peek().clone() {
4115 Token::All => {
4116 self.advance();
4117 Ok(Statement::ResetParameter(None))
4118 }
4119 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4120 self.advance();
4121 Ok(Statement::ResetParameter(None))
4122 }
4123 // v7.39 (RLS) — `RESET ROLE` clears the session role.
4124 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4125 self.advance();
4126 Ok(Statement::SetRole(None))
4127 }
4128 _ => {
4129 let name = self.parse_set_param_name()?;
4130 Ok(Statement::ResetParameter(Some(name)))
4131 }
4132 }
4133 }
4134 // v7.39 (round 218) — server-side cursors.
4135 Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4136 Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4137 Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4138 Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4139 self.advance();
4140 match self.peek().clone() {
4141 Token::All => {
4142 self.advance();
4143 Ok(Statement::CloseCursor { name: None })
4144 }
4145 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4146 self.advance();
4147 Ok(Statement::CloseCursor { name: None })
4148 }
4149 Token::Ident(n) | Token::QuotedIdent(n) => {
4150 self.advance();
4151 Ok(Statement::CloseCursor { name: Some(n) })
4152 }
4153 other => Err(self.err(format!(
4154 "expected cursor name or ALL after CLOSE, got {other:?}"
4155 ))),
4156 }
4157 }
4158 other => Err(self.err(format!(
4159 "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4160 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4161 ))),
4162 }
4163 }
4164
4165 /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4166 /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4167 /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4168 /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4169 fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4170 self.advance(); // DECLARE
4171 let name = match self.advance() {
4172 Token::Ident(n) | Token::QuotedIdent(n) => n,
4173 other => {
4174 return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4175 }
4176 };
4177 let mut scroll: Option<bool> = None;
4178 loop {
4179 match self.peek() {
4180 Token::Ident(s)
4181 if s.eq_ignore_ascii_case("binary")
4182 || s.eq_ignore_ascii_case("insensitive")
4183 || s.eq_ignore_ascii_case("asensitive") =>
4184 {
4185 self.advance();
4186 }
4187 Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4188 self.advance();
4189 scroll = Some(true);
4190 }
4191 Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4192 {
4193 self.advance(); // NO
4194 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4195 return Err(self.err(format!(
4196 "expected SCROLL after NO in DECLARE, got {:?}",
4197 self.peek()
4198 )));
4199 }
4200 self.advance();
4201 scroll = Some(false);
4202 }
4203 _ => break,
4204 }
4205 }
4206 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4207 return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4208 }
4209 self.advance();
4210 let mut hold = false;
4211 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4212 self.advance();
4213 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4214 return Err(self.err(format!(
4215 "expected HOLD after WITH in DECLARE, got {:?}",
4216 self.peek()
4217 )));
4218 }
4219 self.advance();
4220 hold = true;
4221 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4222 self.advance();
4223 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4224 return Err(self.err(format!(
4225 "expected HOLD after WITHOUT in DECLARE, got {:?}",
4226 self.peek()
4227 )));
4228 }
4229 self.advance();
4230 }
4231 if !matches!(self.peek(), Token::For) {
4232 return Err(self.err(format!(
4233 "expected FOR before the cursor query, got {:?}",
4234 self.peek()
4235 )));
4236 }
4237 self.advance();
4238 let query = self.parse_one_statement()?;
4239 Ok(Statement::DeclareCursor {
4240 name,
4241 scroll,
4242 hold,
4243 query: alloc::boxed::Box::new(query),
4244 })
4245 }
4246
4247 /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4248 /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4249 /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4250 fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4251 use crate::ast::CursorDirection as D;
4252 self.advance(); // FETCH / MOVE
4253 let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4254 let neg = if matches!(this.peek(), Token::Minus) {
4255 this.advance();
4256 true
4257 } else {
4258 false
4259 };
4260 match this.advance() {
4261 Token::Integer(v) => Ok(if neg { -v } else { v }),
4262 other => Err(this.err(format!("expected count, got {other:?}"))),
4263 }
4264 };
4265 let direction = match self.peek().clone() {
4266 Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4267 self.advance();
4268 D::Next
4269 }
4270 Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4271 self.advance();
4272 D::Prior
4273 }
4274 Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4275 self.advance();
4276 D::First
4277 }
4278 Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4279 self.advance();
4280 D::Last
4281 }
4282 Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4283 self.advance();
4284 D::Absolute(signed_count(self)?)
4285 }
4286 Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4287 self.advance();
4288 D::Relative(signed_count(self)?)
4289 }
4290 Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4291 self.advance();
4292 match self.peek().clone() {
4293 Token::All => {
4294 self.advance();
4295 D::All
4296 }
4297 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4298 self.advance();
4299 D::All
4300 }
4301 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4302 _ => D::Next, // bare FORWARD = FORWARD 1
4303 }
4304 }
4305 Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4306 self.advance();
4307 match self.peek().clone() {
4308 Token::All => {
4309 self.advance();
4310 D::BackwardAll
4311 }
4312 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4313 self.advance();
4314 D::BackwardAll
4315 }
4316 Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4317 _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4318 }
4319 }
4320 Token::All => {
4321 self.advance();
4322 D::All
4323 }
4324 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4325 self.advance();
4326 D::All
4327 }
4328 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4329 // Bare `FETCH <name>` — direction defaults to NEXT.
4330 _ => D::Next,
4331 };
4332 // Optional FROM / IN.
4333 if matches!(self.peek(), Token::From)
4334 || matches!(self.peek(), Token::In)
4335 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4336 {
4337 self.advance();
4338 }
4339 let name = match self.advance() {
4340 Token::Ident(n) | Token::QuotedIdent(n) => n,
4341 other => {
4342 return Err(self.err(format!("expected cursor name, got {other:?}")));
4343 }
4344 };
4345 Ok(if is_move {
4346 Statement::MoveCursor { name, direction }
4347 } else {
4348 Statement::FetchCursor { name, direction }
4349 })
4350 }
4351
4352 /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4353 /// [(kind, …)] ON <col>, … FROM <table>`.
4354 fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4355 self.advance(); // STATISTICS
4356 // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4357 let mut if_not_exists = false;
4358 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4359 && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4360 {
4361 self.advance();
4362 self.advance();
4363 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4364 self.advance();
4365 if_not_exists = true;
4366 }
4367 }
4368 let name = self.expect_ident_like()?;
4369 let mut kinds = Vec::new();
4370 if matches!(self.peek(), Token::LParen) {
4371 self.advance();
4372 loop {
4373 let k = self.expect_ident_like()?;
4374 // PG stores the single letters; accept the spelled-out
4375 // names the SQL uses and record what PG records.
4376 kinds.push(match k.to_ascii_lowercase().as_str() {
4377 "ndistinct" => String::from("d"),
4378 "dependencies" => String::from("f"),
4379 "mcv" => String::from("m"),
4380 other => {
4381 return Err(
4382 self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4383 );
4384 }
4385 });
4386 match self.advance() {
4387 Token::Comma => {}
4388 Token::RParen => break,
4389 other => {
4390 return Err(self.err(alloc::format!(
4391 "expected ',' or ')' in statistics kind list, got {other:?}"
4392 )));
4393 }
4394 }
4395 }
4396 }
4397 if !matches!(self.peek(), Token::On) {
4398 return Err(self.err(alloc::format!(
4399 "expected ON in CREATE STATISTICS, got {:?}",
4400 self.peek()
4401 )));
4402 }
4403 self.advance();
4404 let mut columns = Vec::new();
4405 loop {
4406 columns.push(self.expect_ident_like()?);
4407 if matches!(self.peek(), Token::Comma) {
4408 self.advance();
4409 } else {
4410 break;
4411 }
4412 }
4413 if !matches!(self.peek(), Token::From) {
4414 return Err(self.err(alloc::format!(
4415 "expected FROM in CREATE STATISTICS, got {:?}",
4416 self.peek()
4417 )));
4418 }
4419 self.advance();
4420 let table = self.expect_ident_like()?;
4421 Ok(Statement::CreateStatistics {
4422 name,
4423 if_not_exists,
4424 kinds,
4425 columns,
4426 table,
4427 })
4428 }
4429
4430 /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4431 /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4432 /// entered with the `TABLE` keyword still unconsumed. Extracted so
4433 /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4434 /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4435 /// forward call.
4436 fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4437 self.advance(); // TABLE
4438 let if_exists = self.consume_if_exists();
4439 let mut names: Vec<String> = Vec::new();
4440 loop {
4441 names.push(self.expect_ident_like()?);
4442 if matches!(self.peek(), Token::Comma) {
4443 self.advance();
4444 continue;
4445 }
4446 break;
4447 }
4448 if matches!(
4449 self.peek(),
4450 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4451 || s.eq_ignore_ascii_case("restrict")
4452 ) {
4453 self.advance();
4454 }
4455 Ok(Statement::DropTable { names, if_exists })
4456 }
4457
4458 fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4459 self.advance(); // STATISTICS
4460 let mut if_exists = false;
4461 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4462 && matches!(self.tokens.get(self.pos + 1),
4463 Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4464 {
4465 self.advance();
4466 self.advance();
4467 if_exists = true;
4468 }
4469 let name = self.expect_ident_like()?;
4470 Ok(Statement::DropStatistics { name, if_exists })
4471 }
4472
4473 fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4474 debug_assert!(matches!(self.peek(), Token::Create));
4475 self.advance();
4476 match self.peek() {
4477 Token::Table => self.parse_create_table_stmt_after_create(),
4478 Token::Index => self.parse_create_index_stmt_after_create(false),
4479 // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4480 // object now. It used to be consumed by the CREATE-noise
4481 // arm, so a pg_dump that declares extended statistics
4482 // restored silently without them and reflection showed
4483 // nothing.
4484 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4485 self.parse_create_statistics_after_create()
4486 }
4487 // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4488 // The `UNIQUE` modifier turns a partial index into a
4489 // partial-uniqueness invariant (only rows matching the
4490 // WHERE predicate are checked for duplicates). mailrs
4491 // K1 (3 hits: email_templates default, calendar_events
4492 // master, calendar_events instance).
4493 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4494 self.advance();
4495 if !matches!(self.peek(), Token::Index) {
4496 return Err(self.err(alloc::format!(
4497 "expected INDEX after CREATE UNIQUE, got {:?}",
4498 self.peek()
4499 )));
4500 }
4501 self.parse_create_index_stmt_after_create(true)
4502 }
4503 Token::Publication => {
4504 self.advance();
4505 self.parse_create_publication_after_keyword()
4506 }
4507 Token::Subscription => {
4508 self.advance();
4509 self.parse_create_subscription_after_keyword()
4510 }
4511 // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4512 // USER isn't a reserved keyword — we look for the bare
4513 // identifier so the lexer doesn't have to grow a token.
4514 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4515 self.advance();
4516 self.parse_create_user_after_keyword(true)
4517 }
4518 // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4519 // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4520 // the default of the LOGIN attribute.
4521 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4522 self.advance();
4523 self.parse_create_user_after_keyword(false)
4524 }
4525 // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4526 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4527 self.advance();
4528 self.parse_create_policy_after_keyword()
4529 }
4530 // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4531 // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4532 // no-op. mailrs follow-up F3.
4533 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4534 self.advance();
4535 self.parse_create_extension_after_keyword()
4536 }
4537 // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4538 // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4539 // optional; absorb it here and forward to the
4540 // per-kind parsers with the flag. OR is a reserved
4541 // keyword token.
4542 Token::Or => {
4543 self.advance();
4544 let next = self.peek();
4545 let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4546 return Err(self.err(alloc::format!(
4547 "expected REPLACE after CREATE OR, got {next:?}"
4548 )));
4549 };
4550 if !s2.eq_ignore_ascii_case("replace") {
4551 return Err(self.err(alloc::format!(
4552 "expected REPLACE after CREATE OR, got {s2:?}"
4553 )));
4554 }
4555 self.advance();
4556 self.parse_create_function_or_trigger_after_or_replace(true)
4557 }
4558 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4559 self.advance();
4560 self.parse_create_function_after_keyword(false)
4561 }
4562 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4563 self.advance();
4564 self.parse_create_trigger_after_keyword(false)
4565 }
4566 // v7.39 (round 139) — CREATE RULE …
4567 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4568 self.advance();
4569 self.parse_create_rule_after_keyword(false)
4570 }
4571 // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4572 // trigger is a row-level AFTER trigger that additionally carries
4573 // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4574 // path already tolerates and skips those clauses, so consuming the
4575 // CONSTRAINT keyword and reusing it makes the statement parse and the
4576 // trigger fire. (The deferral timing itself is not yet honoured —
4577 // SPG fires it as a plain AFTER trigger, which is correct behaviour
4578 // for every non-deferred use.)
4579 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4580 self.advance();
4581 if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4582 if t.eq_ignore_ascii_case("trigger"))
4583 {
4584 return Err(self.err(alloc::format!(
4585 "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4586 self.peek()
4587 )));
4588 }
4589 self.advance();
4590 self.parse_create_trigger_after_keyword(false)
4591 }
4592 // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4593 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4594 self.advance();
4595 self.parse_create_sequence_after_keyword(false)
4596 }
4597 // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4598 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4599 self.advance();
4600 self.parse_create_view_after_keyword(false, false, false)
4601 }
4602 // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4603 // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4604 // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4605 // appear (in any order) between `CREATE` and `VIEW` in
4606 // every mysqldump-emitted view. Pre-2.6 the parser
4607 // rejected the prefix and the customer's whole view
4608 // backup failed on the first view. The hints are pure
4609 // planner / permission metadata; SPG's view-rewrite
4610 // path is semantically equivalent for all three
4611 // algorithms in v7.17 (TEMPTABLE differs only in
4612 // perf for huge views — out of v7.17 scope), and
4613 // DEFINER / SQL SECURITY are pure single-user
4614 // permissioning that SPG ignores by design.
4615 Token::Ident(s) | Token::QuotedIdent(s)
4616 if s.eq_ignore_ascii_case("algorithm")
4617 || s.eq_ignore_ascii_case("definer")
4618 || s.eq_ignore_ascii_case("sql") =>
4619 {
4620 self.consume_mysql_view_prefix()?;
4621 // After absorbing ALGORITHM / DEFINER / SQL SECURITY
4622 // (in any order, in any combination), the next
4623 // keyword must be VIEW. mysqldump never emits these
4624 // prefixes on non-view statements.
4625 let next = self.peek().clone();
4626 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
4627 if s2.eq_ignore_ascii_case("view"))
4628 {
4629 self.advance();
4630 self.parse_create_view_after_keyword(false, false, false)
4631 } else {
4632 Err(self.err(alloc::format!(
4633 "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
4634 )))
4635 }
4636 }
4637 // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
4638 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
4639 self.advance();
4640 self.parse_create_type_after_keyword()
4641 }
4642 // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
4643 // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
4644 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
4645 self.advance();
4646 self.parse_create_domain_after_keyword()
4647 }
4648 // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
4649 // name [AUTHORIZATION user]. Real catalog registry
4650 // (was silent-no-op'd pre-v7.17).
4651 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
4652 self.advance();
4653 let if_not_exists = self.parse_if_not_exists();
4654 let name = self.expect_ident_like()?;
4655 // Optional `AUTHORIZATION <user>` trailer — accepted,
4656 // ignored (single-user catalog).
4657 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4658 if s.eq_ignore_ascii_case("authorization"))
4659 {
4660 self.advance();
4661 let _ = self.expect_ident_like()?;
4662 }
4663 Ok(Statement::CreateSchema { name, if_not_exists })
4664 }
4665 // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
4666 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
4667 self.advance();
4668 let next = self.peek().clone();
4669 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4670 {
4671 self.advance();
4672 self.parse_create_materialized_view_after_keyword()
4673 } else {
4674 Err(self.err(alloc::format!(
4675 "expected VIEW after CREATE MATERIALIZED, got {next:?}"
4676 )))
4677 }
4678 }
4679 // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
4680 // no-op below), an UNLOGGED table is a real, fully-usable table in
4681 // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
4682 // durability optimisation is a follow-up), so a dump / app that
4683 // declares UNLOGGED tables works instead of failing to parse.
4684 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
4685 self.advance(); // UNLOGGED
4686 if matches!(self.peek(), Token::Table) {
4687 self.parse_create_table_stmt_after_create()
4688 } else {
4689 Err(self.err(format!(
4690 "expected TABLE after CREATE UNLOGGED, got {:?}",
4691 self.peek()
4692 )))
4693 }
4694 }
4695 Token::Ident(s) | Token::QuotedIdent(s)
4696 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
4697 {
4698 self.advance();
4699 // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
4700 let next = self.peek().clone();
4701 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
4702 {
4703 self.advance();
4704 self.parse_create_sequence_after_keyword(true)
4705 } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4706 {
4707 self.advance();
4708 self.parse_create_view_after_keyword(false, false, true)
4709 } else {
4710 // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
4711 // consumed and answered OK while creating nothing, so
4712 // every statement that touched the table afterwards failed
4713 // with "table not found" — the DDL itself lied. It is a
4714 // real CREATE TABLE now, marked temporary so the executor
4715 // puts it in the session's own namespace. An optional
4716 // TABLE keyword may or may not be present (`CREATE TEMP t`
4717 // is not legal, but the keyword is consumed by the
4718 // CREATE TABLE parser itself).
4719 let stmt = self.parse_create_table_stmt_after_create()?;
4720 match stmt {
4721 Statement::CreateTable(mut c) => {
4722 c.temporary = true;
4723 Ok(Statement::CreateTable(c))
4724 }
4725 // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
4726 // CTAS node, which needs the same session namespace.
4727 Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
4728 m.temporary = true;
4729 Ok(Statement::CreateMaterializedView(m))
4730 }
4731 other => Ok(other),
4732 }
4733 }
4734 }
4735 // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
4736 // BEGIN <body> END`. The body may reference `@var`
4737 // session variables, SET statements, internal `;`
4738 // terminators, etc. SPG has no procedure runtime, so
4739 // consume the whole `CREATE PROCEDURE … END` block as
4740 // a no-op so mysqldump scripts that include stored
4741 // routines load through. The matching-END consumer
4742 // tracks BEGIN/END nesting depth to handle nested
4743 // BEGIN blocks correctly.
4744 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
4745 self.consume_mysql_routine_body();
4746 Ok(Statement::Empty)
4747 }
4748 // v7.14.0 — pg_dump / mysqldump emit
4749 // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
4750 // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
4751 // SPG is single-schema / single-database; these have
4752 // no behavioural effect, so consume + return Empty.
4753 // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
4754 // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
4755 // moved up to real parser branches. DATABASE / ROLE /
4756 // POLICY / OPERATOR stay no-op forever
4757 // (single-database, hardcoded roles).
4758 Token::Ident(s) | Token::QuotedIdent(s)
4759 if matches!(
4760 s.to_ascii_lowercase().as_str(),
4761 "database"
4762 | "role"
4763 | "operator"
4764 | "cast"
4765 | "aggregate"
4766 | "language"
4767 | "collation"
4768 | "conversion"
4769 // v7.17.0 Phase 8 (audit N6) — rarely-
4770 // emitted pg_dump shapes that should
4771 // load through without a parser error.
4772 // SPG has no planner statistics catalog,
4773 // no event-trigger hooks, no foreign-
4774 // data-wrapper infrastructure; consume
4775 // + return Empty.
4776 | "statistics"
4777 | "event"
4778 // v7.37.17 (17.6 siblings) — additional CREATE
4779 // targets pg_dump / operator install scripts
4780 // may emit that SPG has no matching machinery
4781 // for. Consume + Empty-return.
4782 | "text"
4783 | "tablespace"
4784 | "access"
4785 | "large"
4786 ) =>
4787 {
4788 // DATABASE is the one member of this list PG refuses
4789 // inside a transaction block; the rest (ROLE, CAST,
4790 // TABLESPACE, …) it runs there quite happily, so only
4791 // this one is named. Still a no-op otherwise — SPG is
4792 // single-database.
4793 let is_database = s.eq_ignore_ascii_case("database");
4794 self.consume_until_statement_boundary();
4795 if is_database {
4796 return Ok(Statement::NoOpPreventedInTransaction {
4797 what: String::from("CREATE DATABASE"),
4798 });
4799 }
4800 Ok(Statement::Empty)
4801 }
4802 // v7.39 (round 706) — the foreign-data family leaves the silent
4803 // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
4804 // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
4805 // FDW machinery), but the ENGINE now warns, so a restore log
4806 // says what will not function instead of reporting success.
4807 Token::Ident(s) | Token::QuotedIdent(s)
4808 if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
4809 {
4810 self.consume_until_statement_boundary();
4811 Ok(Statement::ValidateOnly {
4812 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
4813 names: Vec::new(),
4814 })
4815 }
4816 other => Err(self.err(format!(
4817 "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
4818 ))),
4819 }
4820 }
4821
4822 /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
4823 /// keyword decides whether we parse a function or trigger
4824 /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
4825 /// PROCEDURE) — those land in later releases.
4826 fn parse_create_function_or_trigger_after_or_replace(
4827 &mut self,
4828 or_replace: bool,
4829 ) -> Result<Statement, ParseError> {
4830 let tok = self.peek();
4831 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
4832 return Err(self.err(alloc::format!(
4833 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
4834 )));
4835 };
4836 if s.eq_ignore_ascii_case("function") {
4837 self.advance();
4838 self.parse_create_function_after_keyword(or_replace)
4839 } else if s.eq_ignore_ascii_case("trigger") {
4840 self.advance();
4841 self.parse_create_trigger_after_keyword(or_replace)
4842 } else if s.eq_ignore_ascii_case("rule") {
4843 // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
4844 self.advance();
4845 self.parse_create_rule_after_keyword(or_replace)
4846 } else if s.eq_ignore_ascii_case("view") {
4847 // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
4848 self.advance();
4849 self.parse_create_view_after_keyword(or_replace, false, false)
4850 } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
4851 // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
4852 self.advance();
4853 let nxt = self.peek().clone();
4854 if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
4855 {
4856 self.advance();
4857 self.parse_create_view_after_keyword(or_replace, false, true)
4858 } else {
4859 Err(self.err(alloc::format!(
4860 "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
4861 )))
4862 }
4863 } else {
4864 Err(self.err(alloc::format!(
4865 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
4866 )))
4867 }
4868 }
4869
4870 /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
4871 /// SPG doesn't have a registry; pgvector / similar are
4872 /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
4873 /// the syntax lets dual-target schemas keep the line.
4874 fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
4875 // Optional `IF NOT EXISTS`.
4876 self.consume_if_not_exists();
4877 let name = self.expect_ident_like()?;
4878 // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
4879 // CASCADE / FROM '<v>' clauses; we don't model them.
4880 loop {
4881 match self.peek() {
4882 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
4883 self.advance();
4884 continue;
4885 }
4886 Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
4887 self.advance();
4888 let _ = self.expect_ident_like()?;
4889 continue;
4890 }
4891 Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
4892 self.advance();
4893 // String or ident literal.
4894 let _ = self.advance();
4895 continue;
4896 }
4897 Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
4898 self.advance();
4899 let _ = self.advance();
4900 continue;
4901 }
4902 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
4903 self.advance();
4904 continue;
4905 }
4906 _ => break,
4907 }
4908 }
4909 // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
4910 // nosuch` reported success and `pg_extension` then did not list it,
4911 // which is the accept-and-do-nothing shape F31 exists to find.
4912 Ok(Statement::ValidateOnly {
4913 kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
4914 names: alloc::vec![name],
4915 })
4916 }
4917
4918 /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
4919 /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
4920 /// already been consumed by the caller. Grammar accepted:
4921 ///
4922 /// name `(` arg-list `)`
4923 /// `RETURNS` return-type
4924 /// [ `LANGUAGE` ident ]
4925 /// `AS` $$ body $$
4926 /// [ `LANGUAGE` ident ]
4927 ///
4928 /// Either `LANGUAGE` position is allowed; PG accepts both.
4929 fn parse_create_function_after_keyword(
4930 &mut self,
4931 or_replace: bool,
4932 ) -> Result<Statement, ParseError> {
4933 let name = self.expect_ident_like()?;
4934 // Argument list. v7.12.4 commonly sees the empty `()`
4935 // (trigger functions); typed args parse and round-trip
4936 // but the executor only invokes nullary functions.
4937 if !matches!(self.peek(), Token::LParen) {
4938 return Err(self.err(alloc::format!(
4939 "expected '(' after function name {name:?}, got {:?}",
4940 self.peek()
4941 )));
4942 }
4943 self.advance();
4944 let args = self.parse_function_arg_list()?;
4945 // RETURNS clause.
4946 let tok = self.peek();
4947 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
4948 return Err(self.err(alloc::format!(
4949 "expected RETURNS after function arg list, got {tok:?}"
4950 )));
4951 };
4952 if !s.eq_ignore_ascii_case("returns") {
4953 return Err(self.err(alloc::format!(
4954 "expected RETURNS after function arg list, got {s:?}"
4955 )));
4956 }
4957 self.advance();
4958 let returns = self.parse_function_return()?;
4959 // Optional LANGUAGE clause (PG also accepts after AS — we'll
4960 // re-check after the body too).
4961 let mut language: Option<String> = self.parse_optional_language()?;
4962 // v7.39 (round 322, V46) — attribute clauses. PG allows them on
4963 // either side of the body and in any order, interleaved with
4964 // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
4965 // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
4966 // PG's own pg_dump output did not restore.
4967 let mut attrs = FunctionAttrs::default();
4968 loop {
4969 let before = self.pos;
4970 self.parse_function_attrs_into(&mut attrs)?;
4971 if language.is_none() {
4972 language = self.parse_optional_language()?;
4973 }
4974 if self.pos == before {
4975 break;
4976 }
4977 }
4978 // `AS` followed by a $$-quoted body (lexer already
4979 // collapses both `$$…$$` and `$tag$…$tag$` to a single
4980 // Token::String). AS is a reserved keyword (Token::As).
4981 if !matches!(self.peek(), Token::As) {
4982 return Err(self.err(alloc::format!(
4983 "expected AS before function body, got {:?}",
4984 self.peek()
4985 )));
4986 }
4987 self.advance();
4988 let body_text = match self.peek() {
4989 Token::String(s) => {
4990 let body = s.clone();
4991 self.advance();
4992 body
4993 }
4994 other => {
4995 return Err(self.err(alloc::format!(
4996 "expected $$-quoted function body after AS, got {other:?}"
4997 )));
4998 }
4999 };
5000 // Trailing clauses — PG's other accepted position for both the
5001 // LANGUAGE and the attributes.
5002 loop {
5003 let before = self.pos;
5004 self.parse_function_attrs_into(&mut attrs)?;
5005 if language.is_none() {
5006 language = self.parse_optional_language()?;
5007 }
5008 if self.pos == before {
5009 break;
5010 }
5011 }
5012 let language = language.unwrap_or_else(|| String::from("sql"));
5013 // PL/pgSQL bodies get structure-parsed. Other languages
5014 // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5015 // recognise) round-trip as Raw text — the executor errors
5016 // when invoked with a clear unsupported message.
5017 let body = if language.eq_ignore_ascii_case("plpgsql") {
5018 match parse_plpgsql_body(&body_text) {
5019 Ok(block) => FunctionBody::PlPgSql(block),
5020 // Best-effort: if the body parser doesn't yet
5021 // support a construct used inside, fall back to
5022 // raw — keeps `CREATE FUNCTION` itself working
5023 // (catalogue accepts), executor errors on
5024 // invocation only.
5025 Err(_) => FunctionBody::Raw(body_text),
5026 }
5027 } else {
5028 FunctionBody::Raw(body_text)
5029 };
5030 Ok(Statement::CreateFunction(CreateFunctionStatement {
5031 name,
5032 or_replace,
5033 args,
5034 returns,
5035 language,
5036 body,
5037 attrs,
5038 }))
5039 }
5040
5041 /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5042 /// attribute clauses into `attrs`, stopping at the first token that
5043 /// is not one. Measured against PG 18.4, which accepts them in any
5044 /// order and on either side of the body.
5045 fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5046 loop {
5047 let word = match self.peek() {
5048 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5049 // NOT LEAKPROOF — NOT is a reserved keyword token.
5050 Token::Not
5051 if matches!(
5052 self.tokens.get(self.pos + 1),
5053 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5054 ) =>
5055 {
5056 self.advance();
5057 self.advance();
5058 attrs.leakproof = false;
5059 continue;
5060 }
5061 _ => return Ok(()),
5062 };
5063 match word.as_str() {
5064 "immutable" => {
5065 self.advance();
5066 attrs.volatility = FunctionVolatility::Immutable;
5067 }
5068 "stable" => {
5069 self.advance();
5070 attrs.volatility = FunctionVolatility::Stable;
5071 }
5072 "volatile" => {
5073 self.advance();
5074 attrs.volatility = FunctionVolatility::Volatile;
5075 }
5076 "strict" => {
5077 self.advance();
5078 attrs.strict = true;
5079 }
5080 "leakproof" => {
5081 self.advance();
5082 attrs.leakproof = true;
5083 }
5084 // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5085 // spelled-out forms of STRICT and its opposite.
5086 "returns" | "called" => {
5087 let strict = word == "returns";
5088 let mut probe = self.pos + 1;
5089 if strict {
5090 // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5091 // is not ours.
5092 match self.tokens.get(probe) {
5093 Some(Token::Null) => probe += 1,
5094 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5095 _ => return Ok(()),
5096 }
5097 }
5098 let ok = matches!(self.tokens.get(probe), Some(Token::On))
5099 || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5100 if !ok {
5101 return Ok(());
5102 }
5103 probe += 1;
5104 match self.tokens.get(probe) {
5105 Some(Token::Null) => probe += 1,
5106 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5107 _ => return Ok(()),
5108 }
5109 match self.tokens.get(probe) {
5110 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5111 _ => return Ok(()),
5112 }
5113 self.pos = probe;
5114 attrs.strict = strict;
5115 }
5116 "security" | "external" => {
5117 // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5118 let mut probe = self.pos + 1;
5119 if word == "external" {
5120 match self.tokens.get(probe) {
5121 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5122 probe += 1;
5123 }
5124 _ => return Ok(()),
5125 }
5126 }
5127 let definer = match self.tokens.get(probe) {
5128 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5129 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5130 _ => return Ok(()),
5131 };
5132 self.pos = probe + 1;
5133 attrs.security_definer = definer;
5134 }
5135 "parallel" => {
5136 let level = match self.tokens.get(self.pos + 1) {
5137 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5138 FunctionParallel::Safe
5139 }
5140 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5141 FunctionParallel::Restricted
5142 }
5143 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5144 FunctionParallel::Unsafe
5145 }
5146 _ => return Ok(()),
5147 };
5148 self.pos += 2;
5149 attrs.parallel = level;
5150 }
5151 "cost" | "rows" => {
5152 let Some(n) = self.peek_number_at(self.pos + 1) else {
5153 return Ok(());
5154 };
5155 self.pos += 2;
5156 if word == "cost" {
5157 attrs.cost = Some(n);
5158 } else {
5159 attrs.rows = Some(n);
5160 }
5161 }
5162 _ => return Ok(()),
5163 }
5164 }
5165 }
5166
5167 /// The numeric literal at `idx`, if there is one.
5168 fn peek_number_at(&self, idx: usize) -> Option<f64> {
5169 match self.tokens.get(idx)? {
5170 Token::Integer(n) => Some(*n as f64),
5171 Token::Float(f) => Some(*f),
5172 Token::Numeric(t) => t.parse::<f64>().ok(),
5173 _ => None,
5174 }
5175 }
5176
5177 /// Closing `)`-terminated argument list. v7.12.4 commonly
5178 /// sees the empty `()`; typed args round-trip but the
5179 /// executor (yet) doesn't invoke them.
5180 /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5181 /// it away, which is what PG does with one on a function parameter.
5182 fn skip_type_modifier(&mut self) {
5183 if !matches!(self.peek(), Token::LParen) {
5184 return;
5185 }
5186 // Only a numeric modifier — anything else is not one, and eating
5187 // it would swallow real grammar.
5188 let mut i = self.pos + 1;
5189 let mut seen_number = false;
5190 loop {
5191 match self.tokens.get(i) {
5192 Some(Token::Integer(_)) => seen_number = true,
5193 Some(Token::Comma) => {}
5194 Some(Token::RParen) => break,
5195 _ => return,
5196 }
5197 i += 1;
5198 }
5199 if !seen_number {
5200 return;
5201 }
5202 while self.pos <= i {
5203 self.advance();
5204 }
5205 }
5206
5207 fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5208 let mut args: Vec<FunctionArg> = Vec::new();
5209 if matches!(self.peek(), Token::RParen) {
5210 self.advance();
5211 return Ok(args);
5212 }
5213 loop {
5214 // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5215 // a reserved token; OUT / INOUT are bare idents.
5216 let mode = if matches!(self.peek(), Token::In) {
5217 self.advance();
5218 FunctionArgMode::In
5219 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5220 {
5221 self.advance();
5222 FunctionArgMode::Out
5223 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5224 {
5225 self.advance();
5226 FunctionArgMode::InOut
5227 } else {
5228 FunctionArgMode::In
5229 };
5230 // Optional name. The next token is either a name
5231 // (followed by a type ident) or the type itself.
5232 // Disambiguate by peeking ahead: if the token after
5233 // the next ident is also an ident, we treat the
5234 // first as the name.
5235 // v7.39 (round 315, V19) — take EVERY ident-like word up to
5236 // the comma or paren, then decide. Reading at most two of
5237 // them could not spell `x double precision` at all, and
5238 // silently mis-read the bare `double precision` as a
5239 // parameter named "double" — which is what made the same
5240 // signature key two different ways.
5241 let (name, ty_token) = {
5242 let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5243 while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5244 words.push(self.expect_ident_like()?);
5245 }
5246 // v7.39 (round 344) — a length / precision modifier on the
5247 // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5248 // accepts it and DROPS it — `pg_get_function_arguments`
5249 // reports plain `character varying` / `numeric`, measured on
5250 // 18.4 — but SPG raised `syntax error at or near "("`,
5251 // because the modifier's parens were never consumed.
5252 self.skip_type_modifier();
5253 let whole = words.join(" ");
5254 if words.len() >= 2 && !is_multiword_type_phrase(&whole) {
5255 (Some(words[0].clone()), words[1..].join(" "))
5256 } else {
5257 (None, whole)
5258 }
5259 };
5260 // Type — try to map to ColumnTypeName, else Raw.
5261 let ty = match map_type_ident_to_column_type_name(&ty_token) {
5262 Some(t) => FunctionArgType::Typed(t),
5263 None => FunctionArgType::Raw(ty_token),
5264 };
5265 args.push(FunctionArg { mode, name, ty });
5266 match self.peek() {
5267 Token::Comma => {
5268 self.advance();
5269 continue;
5270 }
5271 Token::RParen => {
5272 self.advance();
5273 return Ok(args);
5274 }
5275 other => {
5276 return Err(self.err(alloc::format!(
5277 "expected , or ) in function arg list, got {other:?}"
5278 )));
5279 }
5280 }
5281 }
5282 }
5283
5284 fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5285 // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5286 // function whose row shape is named inline.
5287 if matches!(self.peek(), Token::Table)
5288 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5289 {
5290 self.advance(); // TABLE
5291 self.advance(); // (
5292 let mut cols: Vec<String> = Vec::new();
5293 loop {
5294 let cname = self.expect_ident_like()?;
5295 let mut ty: Vec<String> = Vec::new();
5296 loop {
5297 match self.peek() {
5298 Token::Comma | Token::RParen | Token::Eof => break,
5299 _ => {}
5300 }
5301 match self.advance() {
5302 Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5303 other => {
5304 if let Some(w) = unreserved_keyword_text(&other) {
5305 ty.push(w);
5306 }
5307 }
5308 }
5309 }
5310 cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5311 if matches!(self.peek(), Token::Comma) {
5312 self.advance();
5313 } else {
5314 break;
5315 }
5316 }
5317 if matches!(self.peek(), Token::RParen) {
5318 self.advance();
5319 }
5320 return Ok(FunctionReturn::Other(alloc::format!(
5321 "TABLE({})",
5322 cols.join(", ")
5323 )));
5324 }
5325 let ident = self.expect_ident_like()?;
5326 // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5327 if ident.eq_ignore_ascii_case("setof") {
5328 let inner = self.expect_ident_like()?;
5329 return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5330 }
5331 if ident.eq_ignore_ascii_case("trigger") {
5332 return Ok(FunctionReturn::Trigger);
5333 }
5334 if ident.eq_ignore_ascii_case("void") {
5335 return Ok(FunctionReturn::Void);
5336 }
5337 match map_type_ident_to_column_type_name(&ident) {
5338 Some(t) => Ok(FunctionReturn::Type(t)),
5339 None => Ok(FunctionReturn::Other(ident)),
5340 }
5341 }
5342
5343 fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5344 match self.peek() {
5345 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5346 self.advance();
5347 let lang = self.expect_ident_like()?;
5348 Ok(Some(lang.to_ascii_lowercase()))
5349 }
5350 _ => Ok(None),
5351 }
5352 }
5353
5354 /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5355 /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5356 /// (expr)]*`. The `DOMAIN` keyword has already been
5357 /// consumed. PG allows the trailing constraints in any
5358 /// order; we approximate with a small loop.
5359 fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5360 let name = self.expect_ident_like()?;
5361 // Optional `AS`.
5362 if matches!(self.peek(), Token::As) {
5363 self.advance();
5364 }
5365 // v7.39 (round 259) — keep the raw type NAME when the base is not
5366 // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5367 // parent domain.
5368 let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _) =
5369 self.parse_type_with_implied_flags()?;
5370 let mut default: Option<Expr> = None;
5371 let mut not_null = false;
5372 let mut checks: Vec<Expr> = Vec::new();
5373 loop {
5374 match self.peek() {
5375 Token::Default => {
5376 if default.is_some() {
5377 return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5378 }
5379 self.advance();
5380 default = Some(self.parse_expr(0)?);
5381 }
5382 Token::Not => {
5383 self.advance();
5384 if !matches!(self.peek(), Token::Null) {
5385 return Err(self.err(alloc::format!(
5386 "expected NULL after NOT in DOMAIN, got {:?}",
5387 self.peek()
5388 )));
5389 }
5390 self.advance();
5391 not_null = true;
5392 }
5393 Token::Null => {
5394 self.advance();
5395 // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5396 // is the default-nullable marker (PG accepts it),
5397 // but AFTER a NOT NULL it is a conflict PG refuses
5398 // (`conflicting NULL/NOT NULL constraints`,
5399 // PG18-measured); the old arm no-opped both ways.
5400 if not_null {
5401 return Err(self.err(alloc::string::String::from(
5402 "conflicting NULL/NOT NULL constraints",
5403 )));
5404 }
5405 }
5406 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5407 self.advance();
5408 if !matches!(self.peek(), Token::LParen) {
5409 return Err(self.err(alloc::format!(
5410 "expected '(' after CHECK in DOMAIN, got {:?}",
5411 self.peek()
5412 )));
5413 }
5414 self.advance();
5415 let expr = self.parse_expr(0)?;
5416 if !matches!(self.peek(), Token::RParen) {
5417 return Err(self.err(alloc::format!(
5418 "expected ')' after CHECK expr, got {:?}",
5419 self.peek()
5420 )));
5421 }
5422 self.advance();
5423 checks.push(expr);
5424 }
5425 // CONSTRAINT <name> CHECK (…) — PG accepts a name
5426 // prefix on the constraint; we drop the name and
5427 // recurse into the constraint parsing.
5428 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5429 self.advance();
5430 let _ = self.expect_ident_like()?;
5431 }
5432 _ => break,
5433 }
5434 }
5435 Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5436 name,
5437 base_type,
5438 base_domain: base_user_ref,
5439 default,
5440 not_null,
5441 checks,
5442 }))
5443 }
5444
5445 /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5446 /// ('a', 'b', …)`. The `TYPE` keyword has already been
5447 /// consumed.
5448 fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5449 let name = self.expect_ident_like()?;
5450 // Required `AS`.
5451 if !matches!(self.peek(), Token::As) {
5452 return Err(self.err(alloc::format!(
5453 "expected AS after CREATE TYPE {name:?}, got {:?}",
5454 self.peek()
5455 )));
5456 }
5457 self.advance();
5458 // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5459 // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5460 // on the next token: `(` = composite, ident `ENUM` = enum.
5461 if matches!(self.peek(), Token::LParen) {
5462 self.advance();
5463 let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5464 let mut field_user_types: Vec<Option<String>> = Vec::new();
5465 // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5466 // is legal PG (an attribute-less composite; measured — the old
5467 // e2e note claimed PG requires at least one attribute).
5468 if matches!(self.peek(), Token::RParen) {
5469 self.advance();
5470 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5471 name,
5472 kind: crate::ast::TypeKind::Composite {
5473 fields,
5474 field_user_types,
5475 },
5476 }));
5477 }
5478 loop {
5479 let field_name = self.expect_ident_like()?;
5480 // v7.39 (round 264) — keep the raw type name when it is not
5481 // a builtin: that is how a NESTED composite field records
5482 // which composite it holds.
5483 let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _) =
5484 self.parse_type_with_implied_flags()?;
5485 fields.push((field_name, field_type));
5486 field_user_types.push(field_user_ref);
5487 if matches!(self.peek(), Token::Comma) {
5488 self.advance();
5489 continue;
5490 }
5491 if matches!(self.peek(), Token::RParen) {
5492 self.advance();
5493 break;
5494 }
5495 return Err(self.err(alloc::format!(
5496 "expected , or ) in composite field list, got {:?}",
5497 self.peek()
5498 )));
5499 }
5500 if fields.is_empty() {
5501 return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5502 }
5503 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5504 name,
5505 kind: crate::ast::TypeKind::Composite {
5506 fields,
5507 field_user_types,
5508 },
5509 }));
5510 }
5511 // Required `ENUM` ident.
5512 let kind_ident = match self.peek().clone() {
5513 Token::Ident(s) | Token::QuotedIdent(s) => s,
5514 other => {
5515 return Err(self.err(alloc::format!(
5516 "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5517 )));
5518 }
5519 };
5520 if !kind_ident.eq_ignore_ascii_case("enum") {
5521 return Err(self.err(alloc::format!(
5522 "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5523 )));
5524 }
5525 self.advance();
5526 if !matches!(self.peek(), Token::LParen) {
5527 return Err(self.err(alloc::format!(
5528 "expected '(' after ENUM, got {:?}",
5529 self.peek()
5530 )));
5531 }
5532 self.advance();
5533 let mut labels: Vec<String> = Vec::new();
5534 loop {
5535 match self.peek().clone() {
5536 Token::String(s) => {
5537 self.advance();
5538 labels.push(s);
5539 }
5540 other => {
5541 return Err(
5542 self.err(alloc::format!("expected enum label string, got {other:?}"))
5543 );
5544 }
5545 }
5546 if matches!(self.peek(), Token::Comma) {
5547 self.advance();
5548 continue;
5549 }
5550 if matches!(self.peek(), Token::RParen) {
5551 self.advance();
5552 break;
5553 }
5554 return Err(self.err(alloc::format!(
5555 "expected , or ) in ENUM label list, got {:?}",
5556 self.peek()
5557 )));
5558 }
5559 if labels.is_empty() {
5560 return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5561 }
5562 Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5563 name,
5564 kind: crate::ast::TypeKind::Enum { labels },
5565 }))
5566 }
5567
5568 /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
5569 /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
5570 /// The `CREATE MATERIALIZED VIEW` keywords have already been
5571 /// consumed.
5572 fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
5573 let if_not_exists = self.parse_if_not_exists();
5574 let name = self.expect_ident_like()?;
5575 let mut columns: Vec<String> = Vec::new();
5576 if matches!(self.peek(), Token::LParen) {
5577 self.advance();
5578 loop {
5579 let c = self.expect_ident_like()?;
5580 columns.push(c);
5581 if matches!(self.peek(), Token::Comma) {
5582 self.advance();
5583 continue;
5584 }
5585 if matches!(self.peek(), Token::RParen) {
5586 self.advance();
5587 break;
5588 }
5589 return Err(self.err(alloc::format!(
5590 "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
5591 self.peek()
5592 )));
5593 }
5594 }
5595 if !matches!(self.peek(), Token::As) {
5596 return Err(self.err(alloc::format!(
5597 "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
5598 self.peek()
5599 )));
5600 }
5601 self.advance();
5602 // v7.39 (round 151) — a WITH-headed body is legal (read-only
5603 // CTEs only; the engine rejects data-modifying ones with PG's
5604 // message). A trailing `WITH [NO] DATA` can't START the body,
5605 // so WITH here heads the query.
5606 let body = if self.peek_is_with_kw() {
5607 self.advance();
5608 self.parse_nested_with_select()?
5609 } else {
5610 let body_stmt = self.parse_select_stmt()?;
5611 let Statement::Select(body) = body_stmt else {
5612 return Err(self.err(alloc::format!(
5613 "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
5614 )));
5615 };
5616 body
5617 };
5618 // Optional trailing `WITH [NO] DATA`.
5619 let with_data = self.parse_optional_with_data(true)?;
5620 Ok(Statement::CreateMaterializedView(
5621 crate::ast::CreateMaterializedViewStatement {
5622 temporary: false,
5623 name,
5624 if_not_exists,
5625 columns,
5626 body,
5627 with_data,
5628 as_plain_table: false,
5629 },
5630 ))
5631 }
5632
5633 /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
5634 /// `default_when_absent` is what to return if the tail is
5635 /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
5636 /// WITH DATA).
5637 fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
5638 let save = self.pos;
5639 // `WITH` is an Ident (not reserved in the lexer).
5640 let is_with = match self.peek() {
5641 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
5642 _ => false,
5643 };
5644 if !is_with {
5645 return Ok(default_when_absent);
5646 }
5647 self.advance();
5648 // Optional `NO`.
5649 let mut with_data = true;
5650 let is_no = match self.peek() {
5651 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
5652 _ => false,
5653 };
5654 if is_no {
5655 self.advance();
5656 with_data = false;
5657 }
5658 // Required `DATA` ident.
5659 let is_data = match self.peek() {
5660 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
5661 _ => false,
5662 };
5663 if is_data {
5664 self.advance();
5665 Ok(with_data)
5666 } else {
5667 // Caller's WITH wasn't WITH-DATA — rewind so the outer
5668 // parser can interpret it.
5669 self.pos = save;
5670 Ok(default_when_absent)
5671 }
5672 }
5673
5674 /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
5675 /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
5676 /// All keyword prefixes have already been consumed; the flags
5677 /// say which were present.
5678 fn parse_create_view_after_keyword(
5679 &mut self,
5680 or_replace: bool,
5681 _materialized_unused: bool,
5682 temporary: bool,
5683 ) -> Result<Statement, ParseError> {
5684 let if_not_exists = self.parse_if_not_exists();
5685 let name = self.expect_ident_like()?;
5686 // Optional `(col, col, …)` rename list.
5687 let mut columns: Vec<String> = Vec::new();
5688 if matches!(self.peek(), Token::LParen) {
5689 self.advance();
5690 loop {
5691 let c = self.expect_ident_like()?;
5692 columns.push(c);
5693 if matches!(self.peek(), Token::Comma) {
5694 self.advance();
5695 continue;
5696 }
5697 if matches!(self.peek(), Token::RParen) {
5698 self.advance();
5699 break;
5700 }
5701 return Err(self.err(alloc::format!(
5702 "expected , or ) in VIEW column list, got {:?}",
5703 self.peek()
5704 )));
5705 }
5706 }
5707 // Required `AS`.
5708 if !matches!(self.peek(), Token::As) {
5709 return Err(self.err(alloc::format!(
5710 "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
5711 self.peek()
5712 )));
5713 }
5714 self.advance();
5715 // Body: a regular SELECT statement. v7.39 (round 151) — a
5716 // WITH-headed body is legal too (read-only CTEs only; the
5717 // engine rejects data-modifying ones with PG's message).
5718 // Disambiguation vs `WITH CHECK OPTION`: a body can't START
5719 // with the check-option clause, so WITH here heads the query.
5720 let body = if self.peek_is_with_kw() {
5721 self.advance();
5722 self.parse_nested_with_select()?
5723 } else {
5724 let body_stmt = self.parse_select_stmt()?;
5725 let Statement::Select(body) = body_stmt else {
5726 return Err(self.err(alloc::format!(
5727 "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
5728 )));
5729 };
5730 body
5731 };
5732 // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
5733 // The SELECT parser stops before a trailing WITH, so it lands here.
5734 let check_option = if matches!(self.peek(),
5735 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
5736 {
5737 self.advance(); // WITH
5738 let opt = match self.peek() {
5739 Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
5740 self.advance();
5741 crate::ast::ViewCheckOption::Local
5742 }
5743 Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
5744 self.advance();
5745 crate::ast::ViewCheckOption::Cascaded
5746 }
5747 // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
5748 _ => crate::ast::ViewCheckOption::Cascaded,
5749 };
5750 if !matches!(self.peek(),
5751 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
5752 {
5753 return Err(self.err(alloc::format!(
5754 "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
5755 self.peek()
5756 )));
5757 }
5758 self.advance(); // CHECK
5759 if !matches!(self.peek(),
5760 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
5761 {
5762 return Err(self.err(alloc::format!(
5763 "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
5764 self.peek()
5765 )));
5766 }
5767 self.advance(); // OPTION
5768 Some(opt)
5769 } else {
5770 None
5771 };
5772 Ok(Statement::CreateView(crate::ast::CreateViewStatement {
5773 name,
5774 or_replace,
5775 if_not_exists,
5776 temporary,
5777 columns,
5778 body,
5779 check_option,
5780 }))
5781 }
5782
5783 /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
5784 /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
5785 /// consumed; `temporary` carries whether TEMPORARY was seen.
5786 fn parse_create_sequence_after_keyword(
5787 &mut self,
5788 temporary: bool,
5789 ) -> Result<Statement, ParseError> {
5790 let if_not_exists = self.parse_if_not_exists();
5791 let name = self.expect_ident_like()?;
5792 // Optional `AS data_type`.
5793 let data_type = if matches!(self.peek(), Token::As) {
5794 self.advance();
5795 Some(self.parse_sequence_data_type()?)
5796 } else {
5797 None
5798 };
5799 let options = self.parse_sequence_options(/* allow_restart = */ false)?;
5800 Ok(Statement::CreateSequence(
5801 crate::ast::CreateSequenceStatement {
5802 name,
5803 if_not_exists,
5804 temporary,
5805 data_type,
5806 options,
5807 },
5808 ))
5809 }
5810
5811 /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
5812 /// already been consumed; this is reached after `SEQUENCE`.
5813 /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
5814 fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5815 use crate::ast::AlterDomainAction as A;
5816 let name = self.expect_ident_like()?;
5817 // DROP / SET / ADD lex as reserved keyword tokens, not idents.
5818 let kw = match self.peek() {
5819 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
5820 Token::Drop => alloc::string::String::from("drop"),
5821 Token::Default => alloc::string::String::from("default"),
5822 other => {
5823 return Err(self.err(alloc::format!(
5824 "expected an ALTER DOMAIN action, got {other:?}"
5825 )));
5826 }
5827 };
5828 let action = match kw.as_str() {
5829 "add" => {
5830 self.advance();
5831 let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
5832 {
5833 self.advance();
5834 Some(self.expect_ident_like()?)
5835 } else {
5836 None
5837 };
5838 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
5839 return Err(self.err(alloc::format!(
5840 "ALTER DOMAIN ADD supports CHECK only, got {:?}",
5841 self.peek()
5842 )));
5843 }
5844 self.advance();
5845 if !matches!(self.peek(), Token::LParen) {
5846 return Err(self.err("expected '(' after CHECK".into()));
5847 }
5848 self.advance();
5849 let check = self.parse_expr(0)?;
5850 if !matches!(self.peek(), Token::RParen) {
5851 return Err(self.err("expected ')' after CHECK expression".into()));
5852 }
5853 self.advance();
5854 A::AddConstraint { name: cname, check }
5855 }
5856 "drop" => {
5857 self.advance();
5858 match self.peek() {
5859 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
5860 self.advance();
5861 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
5862 {
5863 self.advance();
5864 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
5865 {
5866 return Err(self.err("expected EXISTS after IF".into()));
5867 }
5868 self.advance();
5869 true
5870 } else {
5871 false
5872 };
5873 let cn = self.expect_ident_like()?;
5874 A::DropConstraint {
5875 name: cn,
5876 if_exists,
5877 }
5878 }
5879 Token::Default => {
5880 self.advance();
5881 A::DropDefault
5882 }
5883 Token::Not => {
5884 self.advance();
5885 if !matches!(self.peek(), Token::Null) {
5886 return Err(self.err("expected NULL after NOT".into()));
5887 }
5888 self.advance();
5889 A::DropNotNull
5890 }
5891 other => {
5892 return Err(self.err(alloc::format!(
5893 "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
5894 )));
5895 }
5896 }
5897 }
5898 "set" => {
5899 self.advance();
5900 match self.peek() {
5901 Token::Default => {
5902 self.advance();
5903 A::SetDefault(self.parse_expr(0)?)
5904 }
5905 Token::Not => {
5906 self.advance();
5907 if !matches!(self.peek(), Token::Null) {
5908 return Err(self.err("expected NULL after NOT".into()));
5909 }
5910 self.advance();
5911 A::SetNotNull
5912 }
5913 other => {
5914 return Err(self.err(alloc::format!(
5915 "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
5916 )));
5917 }
5918 }
5919 }
5920 "rename" => {
5921 self.advance();
5922 if !matches!(self.peek(), Token::To) {
5923 return Err(self.err("expected TO after RENAME".into()));
5924 }
5925 self.advance();
5926 A::RenameTo(self.expect_ident_like()?)
5927 }
5928 other => {
5929 return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
5930 }
5931 };
5932 Ok(Statement::AlterDomain { name, action })
5933 }
5934
5935 fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
5936 let if_exists = self.parse_if_exists();
5937 let name = self.expect_ident_like()?;
5938 // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
5939 // the option list (PG allows only one or the other).
5940 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
5941 self.advance();
5942 if matches!(self.peek(), Token::To) {
5943 self.advance();
5944 } else {
5945 self.expect_keyword_ident("to")?;
5946 }
5947 let new = self.expect_ident_like()?;
5948 return Ok(Statement::AlterSequence(
5949 crate::ast::AlterSequenceStatement {
5950 name,
5951 if_exists,
5952 options: crate::ast::SequenceOptions::default(),
5953 rename_to: Some(new),
5954 },
5955 ));
5956 }
5957 let options = self.parse_sequence_options(/* allow_restart = */ true)?;
5958 Ok(Statement::AlterSequence(
5959 crate::ast::AlterSequenceStatement {
5960 name,
5961 if_exists,
5962 options,
5963 rename_to: None,
5964 },
5965 ))
5966 }
5967
5968 fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
5969 let kw = self.expect_ident_like()?;
5970 match kw.to_ascii_lowercase().as_str() {
5971 "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
5972 "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
5973 "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
5974 other => Err(self.err(alloc::format!(
5975 "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
5976 ))),
5977 }
5978 }
5979
5980 fn parse_sequence_options(
5981 &mut self,
5982 allow_restart: bool,
5983 ) -> Result<crate::ast::SequenceOptions, ParseError> {
5984 use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
5985 let mut opts = SequenceOptions::default();
5986 #[allow(clippy::while_let_loop)]
5987 loop {
5988 // Match an ident; stop at any non-ident token (sentinel,
5989 // semicolon, end of statement).
5990 let kw_lc = match self.peek() {
5991 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
5992 _ => break,
5993 };
5994 match kw_lc.as_str() {
5995 "increment" => {
5996 self.advance();
5997 // Optional BY.
5998 if self.peek_is_by() {
5999 self.advance();
6000 }
6001 opts.increment = Some(self.expect_signed_int()?);
6002 }
6003 "minvalue" => {
6004 self.advance();
6005 opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6006 }
6007 "maxvalue" => {
6008 self.advance();
6009 opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6010 }
6011 "no" => {
6012 self.advance();
6013 let what = self.expect_ident_like()?;
6014 match what.to_ascii_lowercase().as_str() {
6015 "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6016 "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6017 "cycle" => opts.cycle = Some(false),
6018 other => {
6019 return Err(self.err(alloc::format!(
6020 "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6021 )));
6022 }
6023 }
6024 }
6025 "start" => {
6026 self.advance();
6027 // Optional WITH.
6028 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6029 if s.eq_ignore_ascii_case("with"))
6030 {
6031 self.advance();
6032 }
6033 opts.start = Some(self.expect_signed_int()?);
6034 }
6035 "restart" if allow_restart => {
6036 self.advance();
6037 // Optional WITH n; bare RESTART means restart at START.
6038 let mut with_val: Option<i64> = None;
6039 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6040 if s.eq_ignore_ascii_case("with"))
6041 {
6042 self.advance();
6043 with_val = Some(self.expect_signed_int()?);
6044 } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6045 with_val = Some(self.expect_signed_int()?);
6046 }
6047 opts.restart = Some(with_val);
6048 }
6049 "cache" => {
6050 self.advance();
6051 opts.cache = Some(self.expect_signed_int()?);
6052 }
6053 "cycle" => {
6054 self.advance();
6055 opts.cycle = Some(true);
6056 }
6057 "owned" => {
6058 self.advance();
6059 match self.peek() {
6060 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6061 self.advance();
6062 }
6063 other => {
6064 return Err(
6065 self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6066 );
6067 }
6068 }
6069 // OWNED BY {NONE | tab.col}. Read just one ident
6070 // (NOT expect_ident_like which would auto-strip
6071 // a schema prefix and consume the `.col` we need).
6072 let first = match self.advance() {
6073 Token::Ident(s) | Token::QuotedIdent(s) => s,
6074 other => {
6075 return Err(self.err(alloc::format!(
6076 "expected identifier or NONE after OWNED BY, got {other:?}"
6077 )));
6078 }
6079 };
6080 if first.eq_ignore_ascii_case("none") {
6081 opts.owned_by = Some(SequenceOwnedBy::None);
6082 } else if matches!(self.peek(), Token::Dot) {
6083 self.advance();
6084 let second = match self.advance() {
6085 Token::Ident(s) | Token::QuotedIdent(s) => s,
6086 other => {
6087 return Err(self.err(alloc::format!(
6088 "expected column name after OWNED BY {first}., got {other:?}"
6089 )));
6090 }
6091 };
6092 // v7.17 dump-compat fix — pg_dump emits
6093 // OWNED BY clauses as
6094 // `schema.table.column` (three segments).
6095 // If a third `.<ident>` follows, treat the
6096 // first ident as schema (drop it; SPG is
6097 // single-schema) and the middle / last
6098 // pair as table.column. Otherwise it's
6099 // the two-segment form table.column.
6100 if matches!(self.peek(), Token::Dot) {
6101 self.advance();
6102 let third = match self.advance() {
6103 Token::Ident(s) | Token::QuotedIdent(s) => s,
6104 other => {
6105 return Err(self.err(alloc::format!(
6106 "expected column name after OWNED BY {first}.{second}., got {other:?}"
6107 )));
6108 }
6109 };
6110 let _ = first; // schema prefix discarded
6111 opts.owned_by = Some(SequenceOwnedBy::Column {
6112 table: second,
6113 column: third,
6114 });
6115 } else {
6116 opts.owned_by = Some(SequenceOwnedBy::Column {
6117 table: first,
6118 column: second,
6119 });
6120 }
6121 } else {
6122 return Err(self.err(alloc::format!(
6123 "expected table.column or NONE after OWNED BY, got {first:?}"
6124 )));
6125 }
6126 }
6127 _ => break,
6128 }
6129 }
6130 Ok(opts)
6131 }
6132
6133 fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6134 let neg = if matches!(self.peek(), Token::Minus) {
6135 self.advance();
6136 true
6137 } else {
6138 false
6139 };
6140 match self.peek() {
6141 Token::Integer(n) => {
6142 let v = *n;
6143 self.advance();
6144 Ok(if neg { -v } else { v })
6145 }
6146 other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6147 }
6148 }
6149
6150 /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6151 /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6152 /// clause is fully accepted and discarded — SPG always runs
6153 /// constraint checks immediately (single-writer model). The
6154 /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6155 /// in either order (per the SQL spec they're independent),
6156 /// though pg_dump always emits them in the canonical
6157 /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6158 /// Stops at the first token that isn't part of the clause.
6159 fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6160 self.consume_deferrable_clauses_timed().map(|_| ())
6161 }
6162
6163 /// v7.39 (round 288) — the same scan, but reporting what it saw:
6164 /// `(deferrable, initially_deferred)`. The clauses were parsed and
6165 /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6166 /// NOT DEFERRABLE and a circular-FK migration could not load.
6167 fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6168 let mut deferrable = false;
6169 let mut initially_deferred = false;
6170 loop {
6171 // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6172 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6173 self.advance();
6174 deferrable = true;
6175 if self.consume_optional_initially_clause()? {
6176 initially_deferred = true;
6177 }
6178 continue;
6179 }
6180 // `NOT DEFERRABLE` — already worked pre-3.1.
6181 if matches!(self.peek(), Token::Not) {
6182 let look = self.tokens.get(self.pos + 1);
6183 if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6184 self.advance(); // NOT
6185 self.advance(); // DEFERRABLE
6186 deferrable = false;
6187 initially_deferred = false;
6188 let _ = self.consume_optional_initially_clause()?;
6189 continue;
6190 }
6191 break;
6192 }
6193 // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6194 // accepts this without a leading [NOT] DEFERRABLE
6195 // (the timing keyword alone). pg_dump occasionally
6196 // emits it on FK constraints that inherit timing.
6197 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6198 if self.consume_optional_initially_clause()? {
6199 initially_deferred = true;
6200 // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6201 deferrable = true;
6202 }
6203 continue;
6204 }
6205 break;
6206 }
6207 Ok((deferrable, initially_deferred))
6208 }
6209
6210 /// Helper for [`consume_optional_deferrable_clauses`]. When the
6211 /// next token is `INITIALLY`, consume it plus the required
6212 /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6213 /// Returns true when the timing seen was `DEFERRED`.
6214 fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6215 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6216 return Ok(false);
6217 }
6218 self.advance(); // INITIALLY
6219 match self.advance() {
6220 Token::Ident(s)
6221 if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6222 {
6223 Ok(s.eq_ignore_ascii_case("deferred"))
6224 }
6225 other => Err(self.err(alloc::format!(
6226 "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6227 ))),
6228 }
6229 }
6230
6231 /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6232 /// in its entirety so the parser returns Empty without
6233 /// touching the runtime. The CREATE+PROCEDURE keywords are
6234 /// already consumed; this swallows everything from the
6235 /// procedure name through the matching `END`, including
6236 /// nested `BEGIN`/`END` blocks, internal `;` terminators
6237 /// (DELIMITER `//` makes the script splitter forward the
6238 /// whole block as one statement), `@var` session-variable
6239 /// references, and the trailing terminator.
6240 ///
6241 /// Tracks nesting depth so:
6242 /// BEGIN
6243 /// IF cond THEN
6244 /// BEGIN ... END;
6245 /// END IF;
6246 /// END
6247 /// terminates at the outer END.
6248 fn consume_mysql_routine_body(&mut self) {
6249 // Outer skeleton: name, (...), optional clauses, BEGIN
6250 // <body> END [;]. Scan for the first BEGIN — anything
6251 // before it is signature decoration we don't care about.
6252 // Once inside BEGIN, count up on BEGIN, down on END.
6253 let mut depth: i32 = 0;
6254 let mut started = false;
6255 loop {
6256 match self.peek().clone() {
6257 Token::Begin => {
6258 self.advance();
6259 depth += 1;
6260 started = true;
6261 }
6262 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6263 self.advance();
6264 if started {
6265 depth -= 1;
6266 if depth <= 0 {
6267 // Optional trailing ident (`END IF`,
6268 // `END LOOP`, `END WHILE`, `END CASE`,
6269 // `END label_name`) — eat the next
6270 // ident if present so we don't
6271 // mistake `END IF;` for the outer
6272 // close.
6273 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6274 // If the next token is one of the
6275 // PL/SQL block-closer keywords,
6276 // the END belongs to an inner
6277 // block; bump depth back up.
6278 let is_inner_close = matches!(
6279 self.peek(),
6280 Token::Ident(s) | Token::QuotedIdent(s)
6281 if matches!(
6282 s.to_ascii_lowercase().as_str(),
6283 "if" | "loop" | "while" | "case" | "repeat"
6284 )
6285 );
6286 if is_inner_close {
6287 self.advance();
6288 depth += 1;
6289 continue;
6290 }
6291 }
6292 // Eat optional trailing `;`.
6293 if matches!(self.peek(), Token::Semicolon) {
6294 self.advance();
6295 }
6296 return;
6297 }
6298 }
6299 }
6300 Token::Eof => return,
6301 _ => {
6302 self.advance();
6303 }
6304 }
6305 }
6306 }
6307
6308 /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6309 /// that appear between `CREATE` and `VIEW` in mysqldump output:
6310 ///
6311 /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6312 /// * `DEFINER = <user>` (user may be a quoted string, a bare
6313 /// ident, or `ident @ ident-or-quoted-string` host form)
6314 /// * `SQL SECURITY {DEFINER|INVOKER}`
6315 ///
6316 /// Each clause may appear at most once but in any order.
6317 /// The hints are pure planner / permission metadata that
6318 /// SPG's view-rewrite engine handles uniformly; we accept
6319 /// and discard. Returns `Ok(())` once a non-clause token is
6320 /// peeked (the caller then checks for the `VIEW` keyword).
6321 fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6322 loop {
6323 match self.peek().clone() {
6324 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6325 self.advance(); // ALGORITHM
6326 // Optional `=`. MySQL spec requires it but be
6327 // generous.
6328 if matches!(self.peek(), Token::Eq) {
6329 self.advance();
6330 }
6331 // UNDEFINED / MERGE / TEMPTABLE — accept any
6332 // bare ident; unknown values still parse so
6333 // future MySQL versions don't break.
6334 if matches!(
6335 self.peek(),
6336 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6337 ) {
6338 self.advance();
6339 }
6340 }
6341 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6342 self.advance(); // DEFINER
6343 if matches!(self.peek(), Token::Eq) {
6344 self.advance();
6345 }
6346 // User: quoted string, ident, OR ident @ host
6347 // (host may itself be quoted or bare).
6348 match self.peek().clone() {
6349 Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6350 self.advance();
6351 // Optional `@host`.
6352 if matches!(self.peek(), Token::At) {
6353 self.advance();
6354 if matches!(
6355 self.peek(),
6356 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6357 ) {
6358 self.advance();
6359 }
6360 }
6361 }
6362 _ => {}
6363 }
6364 }
6365 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6366 // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6367 // when followed by SECURITY — the dispatcher must
6368 // not consume a bare `SQL` token (it's not a
6369 // legal CREATE prefix on its own).
6370 let save = self.pos;
6371 self.advance(); // SQL
6372 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6373 if s2.eq_ignore_ascii_case("security"))
6374 {
6375 self.advance(); // SECURITY
6376 // DEFINER / INVOKER trailing ident.
6377 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6378 self.advance();
6379 }
6380 } else {
6381 // Not a SQL SECURITY clause — roll back and
6382 // bail; the caller will error out cleanly.
6383 self.pos = save;
6384 return Ok(());
6385 }
6386 }
6387 _ => return Ok(()),
6388 }
6389 }
6390 }
6391
6392 fn parse_if_not_exists(&mut self) -> bool {
6393 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6394 {
6395 let save = self.pos;
6396 self.advance();
6397 if matches!(self.peek(), Token::Not) {
6398 self.advance();
6399 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6400 {
6401 self.advance();
6402 return true;
6403 }
6404 }
6405 self.pos = save;
6406 }
6407 false
6408 }
6409
6410 fn parse_if_exists(&mut self) -> bool {
6411 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6412 {
6413 let save = self.pos;
6414 self.advance();
6415 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6416 {
6417 self.advance();
6418 return true;
6419 }
6420 self.pos = save;
6421 }
6422 false
6423 }
6424
6425 /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6426 /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6427 /// been consumed.
6428 fn parse_create_trigger_after_keyword(
6429 &mut self,
6430 or_replace: bool,
6431 ) -> Result<Statement, ParseError> {
6432 let name = self.expect_ident_like()?;
6433 let timing = {
6434 let ident = self.expect_ident_like()?;
6435 if ident.eq_ignore_ascii_case("before") {
6436 TriggerTiming::Before
6437 } else if ident.eq_ignore_ascii_case("after") {
6438 TriggerTiming::After
6439 } else if ident.eq_ignore_ascii_case("instead") {
6440 let next = self.expect_ident_like()?;
6441 if !next.eq_ignore_ascii_case("of") {
6442 return Err(self.err(alloc::format!(
6443 "expected OF after INSTEAD in trigger timing, got {next:?}"
6444 )));
6445 }
6446 TriggerTiming::InsteadOf
6447 } else {
6448 return Err(self.err(alloc::format!(
6449 "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6450 )));
6451 }
6452 };
6453 // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6454 // OR is a reserved keyword token (Token::Or), not an Ident.
6455 // v7.13.0 — after an UPDATE event we may optionally see
6456 // `OF col, col, …` (mailrs round-5 G7). Columns are
6457 // captured into `update_columns` once across the whole
6458 // events list; multiple `UPDATE OF` clauses are rejected.
6459 let mut events: Vec<TriggerEvent> = Vec::new();
6460 let mut update_columns: Vec<String> = Vec::new();
6461 let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6462 events.push(first_ev);
6463 if !first_cols.is_empty() {
6464 update_columns = first_cols;
6465 }
6466 while matches!(self.peek(), Token::Or) {
6467 self.advance();
6468 let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6469 events.push(ev);
6470 if !cols.is_empty() {
6471 if !update_columns.is_empty() {
6472 return Err(
6473 self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6474 );
6475 }
6476 update_columns = cols;
6477 }
6478 }
6479 // ON <table>
6480 let tok = self.peek();
6481 let Token::On = tok else {
6482 return Err(self.err(alloc::format!(
6483 "expected ON after trigger events, got {tok:?}"
6484 )));
6485 };
6486 self.advance();
6487 let table = self.expect_ident_like()?;
6488 // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6489 // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6490 // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6491 // the trigger as a plain AFTER trigger (correct for every non-deferred
6492 // use; deferral timing is not yet honoured).
6493 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6494 if s.eq_ignore_ascii_case("from"))
6495 {
6496 self.advance();
6497 let _reftable = self.expect_ident_like()?;
6498 }
6499 self.consume_optional_deferrable_clauses()?;
6500 // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6501 // keyword (Token::For); EACH / ROW / STATEMENT are bare
6502 // idents.
6503 if !matches!(self.peek(), Token::For) {
6504 return Err(self.err(alloc::format!(
6505 "expected FOR EACH ROW / STATEMENT, got {:?}",
6506 self.peek()
6507 )));
6508 }
6509 self.advance();
6510 let for_each = {
6511 let e = self.expect_ident_like()?;
6512 if !e.eq_ignore_ascii_case("each") {
6513 return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6514 }
6515 let unit = self.expect_ident_like()?;
6516 if unit.eq_ignore_ascii_case("row") {
6517 TriggerForEach::Row
6518 } else if unit.eq_ignore_ascii_case("statement") {
6519 TriggerForEach::Statement
6520 } else {
6521 return Err(self.err(alloc::format!(
6522 "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6523 )));
6524 }
6525 };
6526 // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6527 let when_condition = if matches!(self.peek(),
6528 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6529 {
6530 self.advance();
6531 Some(self.parse_paren_expr("WHEN")?)
6532 } else {
6533 None
6534 };
6535 // EXECUTE FUNCTION/PROCEDURE name(...)
6536 let exec = self.expect_ident_like()?;
6537 if !exec.eq_ignore_ascii_case("execute") {
6538 return Err(self.err(alloc::format!(
6539 "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6540 )));
6541 }
6542 let fn_or_proc = self.expect_ident_like()?;
6543 if !(fn_or_proc.eq_ignore_ascii_case("function")
6544 || fn_or_proc.eq_ignore_ascii_case("procedure"))
6545 {
6546 return Err(self.err(alloc::format!(
6547 "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6548 )));
6549 }
6550 let function = self.expect_ident_like()?;
6551 // Optional empty arg list `()`.
6552 if matches!(self.peek(), Token::LParen) {
6553 self.advance();
6554 if !matches!(self.peek(), Token::RParen) {
6555 return Err(self.err(alloc::format!(
6556 "v7.12.4 trigger function calls take no args; got {:?}",
6557 self.peek()
6558 )));
6559 }
6560 self.advance();
6561 }
6562 Ok(Statement::CreateTrigger(CreateTriggerStatement {
6563 name,
6564 or_replace,
6565 timing,
6566 events,
6567 table,
6568 for_each,
6569 function,
6570 update_columns,
6571 when_condition,
6572 }))
6573 }
6574
6575 /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
6576 /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
6577 fn parse_create_rule_after_keyword(
6578 &mut self,
6579 or_replace: bool,
6580 ) -> Result<Statement, ParseError> {
6581 let name = self.expect_ident_like()?;
6582 if !matches!(self.peek(), Token::As) {
6583 return Err(self.err(alloc::format!(
6584 "expected AS in CREATE RULE, got {:?}",
6585 self.peek()
6586 )));
6587 }
6588 self.advance();
6589 if !matches!(self.peek(), Token::On) {
6590 return Err(self.err(alloc::format!(
6591 "expected ON in CREATE RULE, got {:?}",
6592 self.peek()
6593 )));
6594 }
6595 self.advance();
6596 let event = self.parse_rule_event()?;
6597 if !matches!(self.peek(), Token::To)
6598 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
6599 {
6600 return Err(self.err(alloc::format!(
6601 "expected TO after rule event, got {:?}",
6602 self.peek()
6603 )));
6604 }
6605 self.advance();
6606 let table = self.expect_ident_like()?;
6607 // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
6608 let when_condition = if matches!(self.peek(), Token::Where) {
6609 self.advance();
6610 Some(self.parse_expr(0)?)
6611 } else {
6612 None
6613 };
6614 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
6615 {
6616 return Err(self.err(alloc::format!(
6617 "expected DO in CREATE RULE, got {:?}",
6618 self.peek()
6619 )));
6620 }
6621 self.advance();
6622 // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
6623 let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
6624 {
6625 self.advance();
6626 true
6627 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
6628 self.advance();
6629 false
6630 } else {
6631 false
6632 };
6633 // `NOTHING` | `( cmd; … )` | `cmd`.
6634 let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
6635 {
6636 self.advance();
6637 Vec::new()
6638 } else if matches!(self.peek(), Token::LParen) {
6639 self.advance();
6640 let mut cmds = Vec::new();
6641 loop {
6642 cmds.push(self.parse_one_statement()?);
6643 if matches!(self.peek(), Token::Semicolon) {
6644 self.advance();
6645 if matches!(self.peek(), Token::RParen) {
6646 break;
6647 }
6648 continue;
6649 }
6650 break;
6651 }
6652 if !matches!(self.peek(), Token::RParen) {
6653 return Err(self.err(alloc::format!(
6654 "expected ) closing the CREATE RULE command list, got {:?}",
6655 self.peek()
6656 )));
6657 }
6658 self.advance();
6659 cmds
6660 } else {
6661 alloc::vec![self.parse_one_statement()?]
6662 };
6663 Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
6664 name,
6665 or_replace,
6666 event,
6667 table,
6668 instead,
6669 when_condition,
6670 commands,
6671 }))
6672 }
6673
6674 /// v7.39 (round 139) — a rule event keyword → uppercase string.
6675 fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
6676 if matches!(self.peek(), Token::Insert) {
6677 self.advance();
6678 return Ok(alloc::string::String::from("INSERT"));
6679 }
6680 if matches!(self.peek(), Token::Select) {
6681 self.advance();
6682 return Ok(alloc::string::String::from("SELECT"));
6683 }
6684 match self.peek() {
6685 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
6686 self.advance();
6687 Ok(alloc::string::String::from("UPDATE"))
6688 }
6689 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
6690 self.advance();
6691 Ok(alloc::string::String::from("DELETE"))
6692 }
6693 other => Err(self.err(alloc::format!(
6694 "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
6695 ))),
6696 }
6697 }
6698
6699 /// v7.13.0 — parse one trigger event, then optionally consume
6700 /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
6701 /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
6702 fn parse_trigger_event_with_optional_of(
6703 &mut self,
6704 ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
6705 let ev = self.parse_trigger_event()?;
6706 if !matches!(ev, TriggerEvent::Update) {
6707 return Ok((ev, Vec::new()));
6708 }
6709 // `OF` is a bare ident.
6710 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
6711 return Ok((ev, Vec::new()));
6712 }
6713 self.advance(); // OF
6714 let mut cols: Vec<String> = Vec::new();
6715 loop {
6716 cols.push(self.expect_ident_like()?);
6717 if matches!(self.peek(), Token::Comma) {
6718 self.advance();
6719 continue;
6720 }
6721 break;
6722 }
6723 if cols.is_empty() {
6724 return Err(
6725 self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
6726 );
6727 }
6728 Ok((ev, cols))
6729 }
6730
6731 /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
6732 /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
6733 /// before `BEGIN`, and IF / RAISE / embedded SQL statements
6734 /// inside the body.
6735 /// Called by [`parse_plpgsql_body`] after the body's tokens
6736 /// have been lexed into this temporary parser.
6737 pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
6738 // v7.12.6 — optional DECLARE prelude.
6739 let declarations = if matches!(
6740 self.peek(),
6741 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
6742 ) {
6743 self.advance();
6744 self.parse_plpgsql_declare_block()?
6745 } else {
6746 Vec::new()
6747 };
6748 // BEGIN keyword (PL/pgSQL — distinct from the SQL
6749 // `BEGIN` transaction-start, but we can reuse the
6750 // reserved Token::Begin since the body is a separate
6751 // lex/parse context).
6752 if !matches!(self.peek(), Token::Begin) {
6753 return Err(self.err(alloc::format!(
6754 "expected BEGIN at start of plpgsql block, got {:?}",
6755 self.peek()
6756 )));
6757 }
6758 self.advance();
6759 let statements = self.parse_plpgsql_stmt_list_until_end()?;
6760 // v7.37.20 (20.10) — optional EXCEPTION clause between the
6761 // body's last statement and the trailing END. When present
6762 // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
6763 // arms terminated by END.
6764 let exception_handlers = if matches!(
6765 self.peek(),
6766 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
6767 ) {
6768 self.advance();
6769 self.parse_plpgsql_exception_handlers()?
6770 } else {
6771 Vec::new()
6772 };
6773 Ok(PlPgSqlBlock {
6774 declarations,
6775 statements,
6776 exception_handlers,
6777 })
6778 }
6779
6780 /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
6781 /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
6782 fn parse_plpgsql_exception_handlers(
6783 &mut self,
6784 ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
6785 let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
6786 loop {
6787 // Stop at END — the block-level trailing END LOOP / END;
6788 // is handled by the caller.
6789 if matches!(
6790 self.peek(),
6791 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
6792 ) {
6793 return Ok(out);
6794 }
6795 // WHEN <cond> [OR <cond>]* THEN <body>
6796 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6797 {
6798 return Err(self.err(alloc::format!(
6799 "expected WHEN or END inside EXCEPTION clause, got {:?}",
6800 self.peek()
6801 )));
6802 }
6803 self.advance();
6804 let mut conditions: Vec<String> = Vec::new();
6805 conditions.push(self.expect_ident_like()?);
6806 while matches!(self.peek(), Token::Or) {
6807 self.advance();
6808 conditions.push(self.expect_ident_like()?);
6809 }
6810 let then_kw = self.expect_ident_like()?;
6811 if !then_kw.eq_ignore_ascii_case("then") {
6812 return Err(self.err(alloc::format!(
6813 "expected THEN after WHEN condition list, got {then_kw:?}"
6814 )));
6815 }
6816 let body = self.parse_plpgsql_stmt_list_until_end()?;
6817 out.push(crate::ast::ExceptionHandler { conditions, body });
6818 }
6819 }
6820
6821 /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
6822 /// prelude. Caller has already consumed `DECLARE`. We stop
6823 /// reading entries when we hit `BEGIN`.
6824 fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
6825 let mut out: Vec<PlPgSqlDeclare> = Vec::new();
6826 loop {
6827 if matches!(self.peek(), Token::Begin) {
6828 return Ok(out);
6829 }
6830 let name = self.expect_ident_like()?;
6831 // v7.37.20 (20.7) — type inference: if the next token is
6832 // `:=` or `=` (no explicit type), infer from the default
6833 // expression. Otherwise the ident that follows is the
6834 // declared type.
6835 //
6836 // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
6837 // (PG-standard). SPG parse-accepts and treats identically
6838 // to inference — the eventual runtime value determines
6839 // the local's type, which is faithful to how SPG handles
6840 // untyped locals today (see 20.7). Full compile-time
6841 // catalog lookup queues with v7.40 PL/pgSQL epic.
6842 let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
6843 // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
6844 // downstream declaration walker to type the local by
6845 // the runtime type of the default expression.
6846 FunctionArgType::Raw("_infer_".into())
6847 } else {
6848 let ty_token = self.expect_ident_like()?;
6849 // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
6850 // consume optional `.<ident>` qualifier + `%<KW>`
6851 // suffix. Both qualifier and suffix map to _infer_.
6852 if matches!(self.peek(), Token::Dot) {
6853 self.advance();
6854 let _ = self.expect_ident_like()?;
6855 }
6856 if matches!(self.peek(), Token::Percent) {
6857 self.advance();
6858 // Consume the trailing TYPE / ROWTYPE ident.
6859 let _ = self.expect_ident_like()?;
6860 FunctionArgType::Raw("_infer_".into())
6861 } else {
6862 match map_type_ident_to_column_type_name(&ty_token) {
6863 Some(t) => FunctionArgType::Typed(t),
6864 None => FunctionArgType::Raw(ty_token),
6865 }
6866 }
6867 };
6868 let default = match self.peek() {
6869 Token::ColonEq => {
6870 self.advance();
6871 Some(self.parse_expr(0)?)
6872 }
6873 Token::Eq => {
6874 // PL/pgSQL also accepts `=` for the
6875 // DECLARE default (PG treats them the same
6876 // in this position).
6877 self.advance();
6878 Some(self.parse_expr(0)?)
6879 }
6880 _ => None,
6881 };
6882 // Mandatory `;` between declarations.
6883 if !matches!(self.peek(), Token::Semicolon) {
6884 return Err(self.err(alloc::format!(
6885 "expected ; after DECLARE entry for {name:?}, got {:?}",
6886 self.peek()
6887 )));
6888 }
6889 self.advance();
6890 out.push(PlPgSqlDeclare { name, ty, default });
6891 }
6892 }
6893
6894 /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
6895 /// the terminating `END;` (or `END IF;` etc — handled by the
6896 /// per-construct sub-parsers). Used by both the outer block
6897 /// and the IF/ELSE branch bodies.
6898 fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
6899 let mut statements: Vec<PlPgSqlStmt> = Vec::new();
6900 loop {
6901 // Allow trailing semicolons + END.
6902 while matches!(self.peek(), Token::Semicolon) {
6903 self.advance();
6904 }
6905 // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
6906 if matches!(
6907 self.peek(),
6908 Token::Ident(s) | Token::QuotedIdent(s)
6909 if s.eq_ignore_ascii_case("end")
6910 || s.eq_ignore_ascii_case("else")
6911 || s.eq_ignore_ascii_case("elsif")
6912 || s.eq_ignore_ascii_case("elseif")
6913 || s.eq_ignore_ascii_case("exception")
6914 || s.eq_ignore_ascii_case("when")
6915 ) {
6916 return Ok(statements);
6917 }
6918 // Otherwise: one statement, then expect `;` or
6919 // a block-terminator keyword.
6920 let stmt = self.parse_plpgsql_stmt()?;
6921 statements.push(stmt);
6922 match self.peek() {
6923 Token::Semicolon => {
6924 self.advance();
6925 }
6926 Token::Ident(s) | Token::QuotedIdent(s)
6927 if s.eq_ignore_ascii_case("end")
6928 || s.eq_ignore_ascii_case("else")
6929 || s.eq_ignore_ascii_case("elsif")
6930 || s.eq_ignore_ascii_case("elseif")
6931 || s.eq_ignore_ascii_case("exception")
6932 || s.eq_ignore_ascii_case("when") =>
6933 {
6934 // Final statement of the block without `;`.
6935 }
6936 other => {
6937 return Err(self.err(alloc::format!(
6938 "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
6939 )));
6940 }
6941 }
6942 }
6943 }
6944
6945 fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
6946 // RETURN keyword?
6947 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
6948 {
6949 self.advance();
6950 return self.parse_plpgsql_return();
6951 }
6952 // v7.12.6 — IF block.
6953 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6954 {
6955 self.advance();
6956 return self.parse_plpgsql_if();
6957 }
6958 // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
6959 // Detected by peeking that token pos+3 is Ident("execute").
6960 if matches!(self.peek(), Token::For)
6961 && matches!(
6962 self.tokens.get(self.pos + 1),
6963 Some(Token::Ident(_) | Token::QuotedIdent(_))
6964 )
6965 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
6966 && matches!(
6967 self.tokens.get(self.pos + 3),
6968 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
6969 )
6970 {
6971 self.advance(); // FOR
6972 let var = self.expect_ident_like()?;
6973 self.advance(); // IN
6974 self.advance(); // EXECUTE
6975 // Prescan for LOOP at paren depth 0 so parse_expr stops
6976 // before the LOOP keyword (same trick as the bare-SELECT
6977 // ForQuery arm).
6978 let mut depth: i32 = 0;
6979 let mut loop_pos: Option<usize> = None;
6980 let mut scan = self.pos;
6981 while scan < self.tokens.len() {
6982 match self.tokens.get(scan) {
6983 Some(Token::LParen) => depth += 1,
6984 Some(Token::RParen) => depth -= 1,
6985 Some(Token::Ident(s) | Token::QuotedIdent(s))
6986 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
6987 {
6988 loop_pos = Some(scan);
6989 break;
6990 }
6991 _ => {}
6992 }
6993 scan += 1;
6994 }
6995 let loop_pos = loop_pos.ok_or_else(|| {
6996 self.err(alloc::format!(
6997 "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
6998 ))
6999 })?;
7000 let saved_loop = self.tokens[loop_pos].clone();
7001 self.tokens[loop_pos] = Token::Semicolon;
7002 let expr_result = self.parse_expr(0);
7003 self.tokens[loop_pos] = saved_loop;
7004 let sql_expr = expr_result?;
7005 let loop_kw = self.expect_ident_like()?;
7006 if !loop_kw.eq_ignore_ascii_case("loop") {
7007 return Err(self.err(alloc::format!(
7008 "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7009 )));
7010 }
7011 let body = self.parse_plpgsql_stmt_list_until_end()?;
7012 let end_kw = self.expect_ident_like()?;
7013 if !end_kw.eq_ignore_ascii_case("end") {
7014 return Err(self.err(alloc::format!(
7015 "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7016 )));
7017 }
7018 let loop_kw2 = self.expect_ident_like()?;
7019 if !loop_kw2.eq_ignore_ascii_case("loop") {
7020 return Err(self.err(alloc::format!(
7021 "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7022 )));
7023 }
7024 return Ok(PlPgSqlStmt::ForExecute {
7025 var,
7026 sql_expr,
7027 body,
7028 });
7029 }
7030 // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7031 //
7032 // Two syntactic forms:
7033 // FOR var IN SELECT ... ORDER BY ... LOOP ...
7034 // FOR var IN (SELECT ...) LOOP ...
7035 //
7036 // Bare-SELECT form: to keep parse_select_stmt from swallowing
7037 // the trailing `LOOP` keyword as a table alias, we prescan
7038 // forward to find LOOP at paren depth 0, splice a fake
7039 // Semicolon at that position (so SELECT parses cleanly),
7040 // then re-splice LOOP back in.
7041 //
7042 // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7043 // LOOP directly — no scan required.
7044 if matches!(self.peek(), Token::For)
7045 && matches!(
7046 self.tokens.get(self.pos + 1),
7047 Some(Token::Ident(_) | Token::QuotedIdent(_))
7048 )
7049 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7050 && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7051 || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7052 {
7053 self.advance(); // FOR
7054 let var = self.expect_ident_like()?;
7055 // IN
7056 self.advance();
7057 let query = if matches!(self.peek(), Token::LParen) {
7058 // Paren-wrapped SELECT.
7059 self.advance();
7060 let inner = self.parse_select_stmt()?;
7061 let Statement::Select(q) = inner else {
7062 return Err(self.err(alloc::format!(
7063 "expected SELECT inside (…), got {:?}",
7064 self.peek()
7065 )));
7066 };
7067 if !matches!(self.peek(), Token::RParen) {
7068 return Err(self.err(alloc::format!(
7069 "expected ')' after FOR-IN-SELECT body, got {:?}",
7070 self.peek()
7071 )));
7072 }
7073 self.advance();
7074 q
7075 } else {
7076 // Bare SELECT: prescan to find the LOOP boundary.
7077 let mut depth: i32 = 0;
7078 let mut loop_pos: Option<usize> = None;
7079 let mut scan = self.pos;
7080 while scan < self.tokens.len() {
7081 match self.tokens.get(scan) {
7082 Some(Token::LParen) => depth += 1,
7083 Some(Token::RParen) => depth -= 1,
7084 Some(Token::Ident(s) | Token::QuotedIdent(s))
7085 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7086 {
7087 loop_pos = Some(scan);
7088 break;
7089 }
7090 _ => {}
7091 }
7092 scan += 1;
7093 }
7094 let loop_pos = loop_pos.ok_or_else(|| {
7095 self.err(alloc::format!(
7096 "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7097 ))
7098 })?;
7099 // Swap the LOOP token with a synthetic Semicolon so
7100 // parse_select_stmt stops there, then restore afterward.
7101 let saved_loop = self.tokens[loop_pos].clone();
7102 self.tokens[loop_pos] = Token::Semicolon;
7103 let parse_result = self.parse_select_stmt();
7104 self.tokens[loop_pos] = saved_loop;
7105 let inner = parse_result?;
7106 let Statement::Select(q) = inner else {
7107 return Err(self.err(alloc::format!(
7108 "expected SELECT after FOR <var> IN, got {:?}",
7109 self.peek()
7110 )));
7111 };
7112 q
7113 };
7114 let loop_kw = self.expect_ident_like()?;
7115 if !loop_kw.eq_ignore_ascii_case("loop") {
7116 return Err(self.err(alloc::format!(
7117 "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7118 )));
7119 }
7120 let body = self.parse_plpgsql_stmt_list_until_end()?;
7121 let end_kw = self.expect_ident_like()?;
7122 if !end_kw.eq_ignore_ascii_case("end") {
7123 return Err(self.err(alloc::format!(
7124 "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7125 )));
7126 }
7127 let loop_kw2 = self.expect_ident_like()?;
7128 if !loop_kw2.eq_ignore_ascii_case("loop") {
7129 return Err(self.err(alloc::format!(
7130 "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7131 )));
7132 }
7133 return Ok(PlPgSqlStmt::ForQuery {
7134 var,
7135 query: Box::new(query),
7136 body,
7137 });
7138 }
7139 // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7140 // FOR is a reserved keyword token (Token::For).
7141 if matches!(self.peek(), Token::For)
7142 && matches!(
7143 self.tokens.get(self.pos + 1),
7144 Some(Token::Ident(_) | Token::QuotedIdent(_))
7145 )
7146 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7147 {
7148 self.advance(); // FOR
7149 let var = self.expect_ident_like()?;
7150 if !matches!(self.peek(), Token::In) {
7151 return Err(self.err(alloc::format!(
7152 "expected IN after FOR <var>, got {:?}",
7153 self.peek()
7154 )));
7155 }
7156 self.advance();
7157 let reverse = matches!(
7158 self.peek(),
7159 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7160 );
7161 if reverse {
7162 self.advance();
7163 }
7164 let start = self.parse_expr(0)?;
7165 if !matches!(self.peek(), Token::DotDot) {
7166 return Err(self.err(alloc::format!(
7167 "expected '..' between FOR loop bounds, got {:?}",
7168 self.peek()
7169 )));
7170 }
7171 self.advance();
7172 let end = self.parse_expr(0)?;
7173 let loop_kw = self.expect_ident_like()?;
7174 if !loop_kw.eq_ignore_ascii_case("loop") {
7175 return Err(self.err(alloc::format!(
7176 "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7177 )));
7178 }
7179 let body = self.parse_plpgsql_stmt_list_until_end()?;
7180 let end_kw = self.expect_ident_like()?;
7181 if !end_kw.eq_ignore_ascii_case("end") {
7182 return Err(self.err(alloc::format!(
7183 "expected END LOOP after FOR body, got {end_kw:?}"
7184 )));
7185 }
7186 let loop_kw2 = self.expect_ident_like()?;
7187 if !loop_kw2.eq_ignore_ascii_case("loop") {
7188 return Err(self.err(alloc::format!(
7189 "expected END LOOP after FOR body, got END {loop_kw2:?}"
7190 )));
7191 }
7192 return Ok(PlPgSqlStmt::ForRange {
7193 var,
7194 start,
7195 end,
7196 reverse,
7197 body,
7198 });
7199 }
7200 // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7201 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7202 {
7203 self.advance();
7204 let body = self.parse_plpgsql_stmt_list_until_end()?;
7205 let end_kw = self.expect_ident_like()?;
7206 if !end_kw.eq_ignore_ascii_case("end") {
7207 return Err(self.err(alloc::format!(
7208 "expected END LOOP after LOOP body, got {end_kw:?}"
7209 )));
7210 }
7211 let loop_kw = self.expect_ident_like()?;
7212 if !loop_kw.eq_ignore_ascii_case("loop") {
7213 return Err(self.err(alloc::format!(
7214 "expected END LOOP after LOOP body, got END {loop_kw:?}"
7215 )));
7216 }
7217 return Ok(PlPgSqlStmt::Loop { body });
7218 }
7219 // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7220 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7221 {
7222 self.advance();
7223 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7224 {
7225 self.advance();
7226 Some(self.parse_expr(0)?)
7227 } else {
7228 None
7229 };
7230 return Ok(PlPgSqlStmt::Exit { when });
7231 }
7232 // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7233 // already-parsed Statement or a runtime-computed SQL string.
7234 // The disambiguator vs the extended-query-protocol `EXECUTE
7235 // <stmt_name>` (which is a top-level Statement, not a
7236 // plpgsql line) is that inside a DO block / trigger body the
7237 // EXECUTE keyword ALWAYS refers to dynamic SQL.
7238 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7239 {
7240 self.advance();
7241 let sql = self.parse_expr(0)?;
7242 return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7243 }
7244 // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7245 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7246 {
7247 self.advance();
7248 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7249 {
7250 self.advance();
7251 Some(self.parse_expr(0)?)
7252 } else {
7253 None
7254 };
7255 return Ok(PlPgSqlStmt::Continue { when });
7256 }
7257 // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7258 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7259 {
7260 self.advance();
7261 let condition = self.parse_expr(0)?;
7262 let loop_kw = self.expect_ident_like()?;
7263 if !loop_kw.eq_ignore_ascii_case("loop") {
7264 return Err(self.err(alloc::format!(
7265 "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7266 )));
7267 }
7268 let body = self.parse_plpgsql_stmt_list_until_end()?;
7269 // Expect END LOOP.
7270 let end_kw = self.expect_ident_like()?;
7271 if !end_kw.eq_ignore_ascii_case("end") {
7272 return Err(self.err(alloc::format!(
7273 "expected END LOOP after WHILE body, got {end_kw:?}"
7274 )));
7275 }
7276 let loop_kw2 = self.expect_ident_like()?;
7277 if !loop_kw2.eq_ignore_ascii_case("loop") {
7278 return Err(self.err(alloc::format!(
7279 "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7280 )));
7281 }
7282 return Ok(PlPgSqlStmt::While { condition, body });
7283 }
7284 // v7.12.6 — RAISE.
7285 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7286 {
7287 self.advance();
7288 return self.parse_plpgsql_raise();
7289 }
7290 // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7291 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7292 {
7293 self.advance();
7294 let condition = self.parse_expr(0)?;
7295 let message = if matches!(self.peek(), Token::Comma) {
7296 self.advance();
7297 Some(self.parse_expr(0)?)
7298 } else {
7299 None
7300 };
7301 return Ok(PlPgSqlStmt::Assert { condition, message });
7302 }
7303 // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7304 // "PERFORM is equivalent to SELECT but discards the
7305 // result." Side effects (function calls, RAISE inside
7306 // SQL functions, etc.) still execute. We desugar to
7307 // `SELECT <body>` and wrap in EmbeddedSql so the engine's
7308 // existing embedded-statement path handles execution +
7309 // result-discard cleanly. The result is naturally
7310 // discarded because EmbeddedSql doesn't propagate row
7311 // sets back to the plpgsql interpreter.
7312 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7313 {
7314 self.advance();
7315 // Splice a synthetic Token::Select into the stream at
7316 // the current position so parse_select_stmt parses the
7317 // remainder as a normal SELECT body. Token-stream
7318 // surgery mirrors the try_parse_plpgsql_select_into
7319 // pattern used for SELECT … INTO desugaring.
7320 self.tokens.insert(self.pos, Token::Select);
7321 let select = self.parse_select_stmt()?;
7322 let Statement::Select(s) = select else {
7323 return Err(self.err(alloc::format!(
7324 "expected SELECT body after PERFORM, got {:?}",
7325 self.peek()
7326 )));
7327 };
7328 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7329 }
7330 // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7331 // plpgsql-specific shape (mailrs round-10 migrate-042).
7332 // PG's SELECT INTO at top-level SQL would CREATE a new
7333 // table; inside plpgsql it ASSIGNS the query result to
7334 // a local variable. We detect the INTO at paren-depth
7335 // 0 between SELECT and the statement boundary; if
7336 // found, split the token stream into "pre-INTO
7337 // projection" + "var" + "post-INTO FROM/WHERE…" and
7338 // rebuild as a SelectInto with a regular SELECT body
7339 // (no INTO clause).
7340 if matches!(self.peek(), Token::Select)
7341 && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7342 {
7343 return Ok(PlPgSqlStmt::SelectInto {
7344 var: var_name,
7345 body: Box::new(select_body),
7346 });
7347 }
7348 // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7349 // SELECT can appear directly inside a trigger body; we
7350 // recurse into the regular Statement parser, which will
7351 // stop at the trailing `;` (which our caller then
7352 // consumes).
7353 // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7354 // also embed ALTER / CREATE / DROP statements; route
7355 // those through the same parser so the DO body parses
7356 // cleanly.
7357 if matches!(self.peek(), Token::Insert)
7358 || matches!(self.peek(), Token::Select)
7359 || matches!(self.peek(), Token::Create)
7360 || matches!(self.peek(), Token::Drop)
7361 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7362 if s.eq_ignore_ascii_case("update")
7363 || s.eq_ignore_ascii_case("delete")
7364 || s.eq_ignore_ascii_case("alter"))
7365 {
7366 let stmt = self.parse_one_statement()?;
7367 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7368 }
7369 // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7370 // followed by `:=` and an expression.
7371 let target = self.parse_plpgsql_assign_target()?;
7372 // PL/pgSQL assignment uses `:=`. The lexer represents
7373 // this as a colon followed by `=`; check both shapes.
7374 match self.peek() {
7375 Token::ColonEq => {
7376 self.advance();
7377 }
7378 Token::Colon => {
7379 self.advance();
7380 if !matches!(self.peek(), Token::Eq) {
7381 return Err(self.err(alloc::format!(
7382 "expected := after plpgsql assign target, got `:` then {:?}",
7383 self.peek()
7384 )));
7385 }
7386 self.advance();
7387 }
7388 other => {
7389 return Err(self.err(alloc::format!(
7390 "expected := after plpgsql assign target, got {other:?}"
7391 )));
7392 }
7393 }
7394 let value = self.parse_expr(0)?;
7395 Ok(PlPgSqlStmt::Assign { target, value })
7396 }
7397
7398 /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7399 /// [ELSE body] END IF`. `IF` keyword already consumed.
7400 fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7401 let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7402 let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7403 loop {
7404 // <expr> THEN
7405 let cond = self.parse_expr(0)?;
7406 let then_kw = self.expect_ident_like()?;
7407 if !then_kw.eq_ignore_ascii_case("then") {
7408 return Err(self.err(alloc::format!(
7409 "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7410 )));
7411 }
7412 let body = self.parse_plpgsql_stmt_list_until_end()?;
7413 branches.push((cond, body));
7414 // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7415 match self.peek() {
7416 Token::Ident(s) | Token::QuotedIdent(s)
7417 if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7418 {
7419 self.advance();
7420 continue;
7421 }
7422 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7423 self.advance();
7424 else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7425 break;
7426 }
7427 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7428 break;
7429 }
7430 other => {
7431 return Err(self.err(alloc::format!(
7432 "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7433 )));
7434 }
7435 }
7436 }
7437 // Expect `END IF` (the END keyword is the one we're
7438 // looking at right now).
7439 let end_kw = self.expect_ident_like()?;
7440 if !end_kw.eq_ignore_ascii_case("end") {
7441 return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7442 }
7443 let if_kw = self.expect_ident_like()?;
7444 if !if_kw.eq_ignore_ascii_case("if") {
7445 return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7446 }
7447 Ok(PlPgSqlStmt::If {
7448 branches,
7449 else_branch,
7450 })
7451 }
7452
7453 /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7454 /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7455 /// is already consumed.
7456 fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7457 let lvl_ident = self.expect_ident_like()?;
7458 let level = match lvl_ident.to_ascii_lowercase().as_str() {
7459 "notice" => RaiseLevel::Notice,
7460 "warning" => RaiseLevel::Warning,
7461 "info" => RaiseLevel::Info,
7462 "log" => RaiseLevel::Log,
7463 "debug" => RaiseLevel::Debug,
7464 "exception" => RaiseLevel::Exception,
7465 other => {
7466 return Err(self.err(alloc::format!(
7467 "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7468 )));
7469 }
7470 };
7471 // Message: required for v7.12.6. PG accepts a bare
7472 // RAISE-rethrow form (no message), reserved for future
7473 // RAISE-no-args support.
7474 let Token::String(msg) = self.peek() else {
7475 return Err(self.err(alloc::format!(
7476 "expected RAISE message string, got {:?}",
7477 self.peek()
7478 )));
7479 };
7480 let message = msg.clone();
7481 self.advance();
7482 // Optional comma-separated args (PG `%` format substitution).
7483 let mut args: Vec<Expr> = Vec::new();
7484 while matches!(self.peek(), Token::Comma) {
7485 self.advance();
7486 args.push(self.parse_expr(0)?);
7487 }
7488 Ok(PlPgSqlStmt::Raise {
7489 level,
7490 message,
7491 args,
7492 })
7493 }
7494
7495 /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7496 /// <projection> INTO <var> [FROM …]` (mailrs round-10
7497 /// migrate-042). Returns `(rebuilt_select_without_into,
7498 /// var_name)` when the pattern matches; `None` for
7499 /// regular SELECTs (those go through the embedded-SQL
7500 /// path). Token-stream surgery so the rebuilt SELECT
7501 /// parses through the regular `parse_select_stmt`.
7502 #[allow(clippy::too_many_lines)]
7503 fn try_parse_plpgsql_select_into(
7504 &mut self,
7505 ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7506 // Scan forward from `self.pos + 1` (past Token::Select)
7507 // for Token::Into at paren-depth 0, stopping at the
7508 // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7509 // end the plpgsql statement.
7510 let start = self.pos;
7511 let mut into_pos: Option<usize> = None;
7512 let mut depth: i32 = 0;
7513 let mut i = start + 1;
7514 while i < self.tokens.len() {
7515 match &self.tokens[i] {
7516 Token::LParen => depth += 1,
7517 Token::RParen => depth -= 1,
7518 Token::Semicolon if depth == 0 => break,
7519 Token::Ident(s)
7520 if depth == 0
7521 && (s.eq_ignore_ascii_case("end")
7522 || s.eq_ignore_ascii_case("else")
7523 || s.eq_ignore_ascii_case("elsif")) =>
7524 {
7525 break;
7526 }
7527 Token::Into if depth == 0 => {
7528 into_pos = Some(i);
7529 break;
7530 }
7531 _ => {}
7532 }
7533 i += 1;
7534 }
7535 let Some(into_at) = into_pos else {
7536 return Ok(None);
7537 };
7538 // The token immediately after INTO must be the target
7539 // var ident; anything else (e.g. INSERT INTO table)
7540 // ruled out by the depth-0 check above. Capture it.
7541 let var = match self.tokens.get(into_at + 1) {
7542 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7543 other => {
7544 return Err(self.err(alloc::format!(
7545 "expected variable name after SELECT … INTO, got {other:?}"
7546 )));
7547 }
7548 };
7549 // Find the end of the plpgsql SELECT INTO statement —
7550 // same boundary rules as the depth-0 scan above.
7551 let mut end = into_at + 2;
7552 let mut depth2: i32 = 0;
7553 while end < self.tokens.len() {
7554 match &self.tokens[end] {
7555 Token::LParen => depth2 += 1,
7556 Token::RParen => depth2 -= 1,
7557 Token::Semicolon if depth2 == 0 => break,
7558 Token::Ident(s)
7559 if depth2 == 0
7560 && (s.eq_ignore_ascii_case("end")
7561 || s.eq_ignore_ascii_case("else")
7562 || s.eq_ignore_ascii_case("elsif")) =>
7563 {
7564 break;
7565 }
7566 _ => {}
7567 }
7568 end += 1;
7569 }
7570 // Rebuild a token stream that represents the SELECT
7571 // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
7572 // post-var tokens up to statement end]. Run the
7573 // regular `parse_select_stmt` against it.
7574 let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
7575 for j in start..into_at {
7576 rebuilt.push(self.tokens[j].clone());
7577 }
7578 for j in (into_at + 2)..end {
7579 rebuilt.push(self.tokens[j].clone());
7580 }
7581 rebuilt.push(Token::Eof);
7582 let saved_pos = self.pos;
7583 let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
7584 self.pos = 0;
7585 // parse_select_stmt → parse_bare_select consumes Token::Select itself.
7586 if !matches!(self.peek(), Token::Select) {
7587 self.tokens = saved_tokens;
7588 self.pos = saved_pos;
7589 return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
7590 }
7591 let sel = self.parse_select_stmt();
7592 self.tokens = saved_tokens;
7593 self.pos = end;
7594 let sel = sel?;
7595 let Statement::Select(body) = sel else {
7596 return Err(self.err(alloc::format!(
7597 "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
7598 )));
7599 };
7600 Ok(Some((body, var)))
7601 }
7602
7603 fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
7604 // v7.16.1 — read the head token DIRECTLY rather than
7605 // via `expect_ident_like`. The v7.14.0 schema-qualifier
7606 // strip (`public.t` → `t`) inside `expect_ident_like`
7607 // greedily consumes any `ident . ident` pair, which
7608 // silently turned every `NEW.col := …` /
7609 // `OLD.col := …` plpgsql assignment into a Local("col")
7610 // assignment — the head "new"/"old" was eaten as if it
7611 // were a schema name and the Dot was consumed too, so
7612 // this function's own `peek() == Token::Dot` check
7613 // below never fired. Every BEFORE trigger that rewrote
7614 // a NEW cell was a silent no-op for two major releases
7615 // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
7616 // gate failures were investigated as v7.16.1 backlog.
7617 let head = match self.advance() {
7618 Token::Ident(s) | Token::QuotedIdent(s) => s,
7619 other => {
7620 return Err(self.err(alloc::format!(
7621 "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
7622 )));
7623 }
7624 };
7625 if matches!(self.peek(), Token::Dot) {
7626 self.advance();
7627 let col = self.expect_ident_like()?;
7628 if head.eq_ignore_ascii_case("new") {
7629 return Ok(AssignTarget::NewColumn(col));
7630 }
7631 if head.eq_ignore_ascii_case("old") {
7632 return Ok(AssignTarget::OldColumn(col));
7633 }
7634 return Err(self.err(alloc::format!(
7635 "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
7636 got {head:?}.<col>"
7637 )));
7638 }
7639 Ok(AssignTarget::Local(head))
7640 }
7641
7642 fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7643 // RETURN NEW / OLD / NULL — bare-ident forms.
7644 match self.peek() {
7645 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
7646 self.advance();
7647 return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
7648 }
7649 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
7650 self.advance();
7651 return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
7652 }
7653 Token::Null => {
7654 self.advance();
7655 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7656 }
7657 // Bare `RETURN;` (no value) — treated as `RETURN NULL`
7658 // per PL/pgSQL convention.
7659 Token::Semicolon => {
7660 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7661 }
7662 _ => {}
7663 }
7664 // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
7665 // EXECUTE <expr>. In a DO block context RETURN QUERY has no
7666 // caller-visible effect (blocks don't return sets), so we
7667 // desugar it identically to PERFORM: parse the SELECT (or
7668 // EXECUTE dynamic) as embedded SQL that runs for side
7669 // effects and discards the result. RETURN NEXT <expr>
7670 // (single-row accumulator) queues with v7.40 SETOF function
7671 // infrastructure.
7672 // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
7673 // and keep going.
7674 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
7675 {
7676 self.advance();
7677 let e = self.parse_expr(0)?;
7678 return Ok(PlPgSqlStmt::ReturnNext(e));
7679 }
7680 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
7681 {
7682 self.advance();
7683 // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
7684 // rows go to the set, like the static form. It used to desugar to a
7685 // bare ExecuteDynamic, whose result was DISCARDED.
7686 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7687 {
7688 self.advance();
7689 let sql = self.parse_expr(0)?;
7690 return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
7691 }
7692 // Bare RETURN QUERY <select>. If the current token is
7693 // not already SELECT (e.g., the user wrote `RETURN QUERY
7694 // <projection> FROM ...` in a shorthand — rare but PG
7695 // accepts a bare projection here), splice one in. Same
7696 // trick as PERFORM.
7697 if !matches!(self.peek(), Token::Select) {
7698 self.tokens.insert(self.pos, Token::Select);
7699 }
7700 let select = self.parse_select_stmt()?;
7701 let Statement::Select(s) = select else {
7702 return Err(self.err(alloc::format!(
7703 "expected SELECT body after RETURN QUERY, got {:?}",
7704 self.peek()
7705 )));
7706 };
7707 // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
7708 // to an embedded side-effect SELECT whose rows were DISCARDED, which
7709 // in a SETOF function is the entire answer thrown away.
7710 return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
7711 }
7712 // Fall through: parse a full expression.
7713 let e = self.parse_expr(0)?;
7714 Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
7715 }
7716
7717 fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
7718 // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
7719 // are ident-shaped (the parser keys off case-insensitive
7720 // match — same shape used by the top-level Update / Delete
7721 // dispatchers at parse_one_statement).
7722 if matches!(self.peek(), Token::Insert) {
7723 self.advance();
7724 return Ok(TriggerEvent::Insert);
7725 }
7726 match self.peek() {
7727 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7728 self.advance();
7729 Ok(TriggerEvent::Update)
7730 }
7731 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7732 self.advance();
7733 Ok(TriggerEvent::Delete)
7734 }
7735 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
7736 self.advance();
7737 Ok(TriggerEvent::Truncate)
7738 }
7739 other => Err(self.err(alloc::format!(
7740 "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
7741 ))),
7742 }
7743 }
7744
7745 /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
7746 /// - (no clause) → implicit `FOR ALL TABLES`
7747 /// - `FOR ALL TABLES`
7748 /// - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
7749 /// - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
7750 /// accepted as an SPG lenience. PG18-measured (round 753): PG
7751 /// REJECTS the bare plural (`invalid publication object list`,
7752 /// TABLES only pairs with IN SCHEMA); the old note claimed an
7753 /// unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
7754 fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
7755 let name = self.expect_ident_or_string()?;
7756 // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
7757 // shape so existing publications keep parsing identically.
7758 let scope = if matches!(self.peek(), Token::For) {
7759 self.advance();
7760 if matches!(self.peek(), Token::All) {
7761 self.advance();
7762 if !matches!(self.peek(), Token::Tables) {
7763 return Err(self.err(format!(
7764 "expected TABLES after FOR ALL, got {:?}",
7765 self.peek()
7766 )));
7767 }
7768 self.advance();
7769 if matches!(self.peek(), Token::Except) {
7770 self.advance();
7771 let tables = self.parse_publication_table_list()?;
7772 PublicationScope::AllTablesExcept(tables)
7773 } else {
7774 PublicationScope::AllTables
7775 }
7776 } else if matches!(self.peek(), Token::Table) {
7777 self.advance();
7778 let tables = self.parse_publication_table_list()?;
7779 PublicationScope::ForTables(tables)
7780 } else if matches!(self.peek(), Token::Tables) {
7781 // v7.39 (round 754, F31-B5) — PG18-measured: the bare
7782 // plural (`FOR TABLES t`) is REJECTED (`invalid
7783 // publication object list`); TABLES only pairs with
7784 // `IN SCHEMA`. The old arm accepted it on an
7785 // unverifiable "PG 19 accepts both" claim.
7786 self.advance();
7787 if !matches!(self.peek(), Token::In) {
7788 return Err(self.err(alloc::string::String::from(
7789 "invalid publication object list",
7790 )));
7791 }
7792 self.advance();
7793 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
7794 return Err(self.err(format!(
7795 "expected SCHEMA after FOR TABLES IN, got {:?}",
7796 self.peek()
7797 )));
7798 }
7799 self.advance();
7800 let schema = self.expect_ident_or_string()?;
7801 PublicationScope::TablesInSchema(schema)
7802 } else {
7803 return Err(self.err(format!(
7804 "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
7805 self.peek()
7806 )));
7807 }
7808 } else {
7809 PublicationScope::AllTables
7810 };
7811 Ok(Statement::CreatePublication(CreatePublicationStatement {
7812 name,
7813 scope,
7814 }))
7815 }
7816
7817 /// v6.1.3 — Comma-separated identifier list for the publication
7818 /// FOR-clause. Requires at least one entry; empty list is a
7819 /// parse error (PG behaviour). Quoted idents are accepted; the
7820 /// names round-trip through `Display` as `quote_ident(name)`.
7821 ///
7822 /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
7823 /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
7824 /// pg_dump output. SPG's publication state today is per-table
7825 /// only (matching the pre-PG-15 surface); the col list + WHERE
7826 /// are parsed so dumps load through and the table name reaches
7827 /// `PublicationScope::ForTables`, but the filter is not enforced
7828 /// at publish time. Re-open when a customer dogfood gate
7829 /// requires per-row-filter or column-subset publish semantics
7830 /// (which gates on persistent slot state landing first, 21.12).
7831 fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
7832 let first = self.parse_publication_table_entry()?;
7833 let mut out = alloc::vec![first];
7834 while matches!(self.peek(), Token::Comma) {
7835 self.advance();
7836 out.push(self.parse_publication_table_entry()?);
7837 }
7838 Ok(out)
7839 }
7840
7841 /// One table entry inside a FOR TABLE clause:
7842 /// tab_name [ (col, col, …) ] [ WHERE (predicate) ]
7843 /// Returns just the table name; the column list + WHERE predicate
7844 /// are consumed and discarded per the parse-accept-discard
7845 /// commitment above.
7846 fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
7847 let name = self.expect_ident_like()?;
7848 // Optional column list — `(col, col, …)`.
7849 if matches!(self.peek(), Token::LParen) {
7850 self.advance();
7851 // Empty parens are a PG error too; require ≥ 1 column.
7852 let _ = self.expect_ident_like()?;
7853 while matches!(self.peek(), Token::Comma) {
7854 self.advance();
7855 let _ = self.expect_ident_like()?;
7856 }
7857 if !matches!(self.peek(), Token::RParen) {
7858 return Err(self.err(alloc::format!(
7859 "expected ')' to close publication column list, got {:?}",
7860 self.peek()
7861 )));
7862 }
7863 self.advance();
7864 }
7865 // Optional row filter — `WHERE (predicate)`.
7866 if matches!(self.peek(), Token::Where) {
7867 self.advance();
7868 if !matches!(self.peek(), Token::LParen) {
7869 return Err(self.err(alloc::format!(
7870 "expected '(' after WHERE in publication row filter, got {:?}",
7871 self.peek()
7872 )));
7873 }
7874 self.advance();
7875 let _ = self.parse_expr(0)?;
7876 if !matches!(self.peek(), Token::RParen) {
7877 return Err(self.err(alloc::format!(
7878 "expected ')' to close publication WHERE filter, got {:?}",
7879 self.peek()
7880 )));
7881 }
7882 self.advance();
7883 }
7884 Ok(name)
7885 }
7886
7887 /// v6.1.4 — `CREATE SUBSCRIPTION <name>
7888 /// CONNECTION '<conn>'
7889 /// PUBLICATION <pub> [, <pub> ...]`.
7890 ///
7891 /// The clause order is fixed (CONNECTION first, then
7892 /// PUBLICATION) to match PG. No WITH-options accepted in
7893 /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
7894 fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
7895 let name = self.expect_ident_or_string()?;
7896 if !matches!(self.peek(), Token::Connection) {
7897 return Err(self.err(format!(
7898 "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
7899 self.peek()
7900 )));
7901 }
7902 self.advance();
7903 let conn_str = self.expect_string_literal()?;
7904 if !matches!(self.peek(), Token::Publication) {
7905 return Err(self.err(format!(
7906 "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
7907 self.peek()
7908 )));
7909 }
7910 self.advance();
7911 // Reuse the publication FOR-list parser shape: at least one
7912 // identifier, comma-separated.
7913 let first = self.expect_ident_like()?;
7914 let mut publications = alloc::vec![first];
7915 while matches!(self.peek(), Token::Comma) {
7916 self.advance();
7917 publications.push(self.expect_ident_like()?);
7918 }
7919 Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
7920 name,
7921 conn_str,
7922 publications,
7923 }))
7924 }
7925
7926 /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
7927 /// All keywords after `WAIT` are bare idents in v6.1.x; no
7928 /// lexer churn. Both `<pos>` and `<ms>` are positive integers
7929 /// that fit `u64`.
7930 /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
7931 /// qualifier is a *namespace* the app owns (`app.user_id`,
7932 /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
7933 /// to discard. So parse the raw segments here instead of
7934 /// `expect_ident_like`, which strips a leading `schema.` qualifier
7935 /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
7936 /// a single segment and round-trip unchanged.
7937 fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
7938 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
7939 loop {
7940 let seg = match self.advance() {
7941 Token::Ident(s) | Token::QuotedIdent(s) => s,
7942 other if unreserved_keyword_text(&other).is_some() => {
7943 unreserved_keyword_text(&other).unwrap()
7944 }
7945 other => {
7946 return Err(ParseError {
7947 message: format!("expected parameter name, got {other:?}"),
7948 token_pos: self.consumed_pos(),
7949 });
7950 }
7951 };
7952 parts.push(seg);
7953 if matches!(self.peek(), Token::Dot) {
7954 self.advance();
7955 continue;
7956 }
7957 break;
7958 }
7959 Ok(parts.join(".").to_ascii_lowercase())
7960 }
7961
7962 fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
7963 Self::parse_set_value_inner(self)
7964 }
7965
7966 fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
7967 match self.advance() {
7968 Token::String(s) => Ok(crate::ast::SetValue::String(s)),
7969 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
7970 Ok(crate::ast::SetValue::Default)
7971 }
7972 Token::Ident(s) | Token::QuotedIdent(s) => {
7973 let mut accum = s;
7974 while matches!(self.peek(), Token::Dot) {
7975 self.advance();
7976 let next = self.expect_ident_like()?;
7977 accum.push('.');
7978 accum.push_str(&next);
7979 }
7980 Ok(crate::ast::SetValue::Ident(accum))
7981 }
7982 Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
7983 Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
7984 // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
7985 // spellings that lex as keyword tokens, not idents:
7986 // `SET standard_conforming_strings = on` is in every
7987 // pg_dump preamble (`off` already lexes as an ident).
7988 // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
7989 // DEFAULT lexes as its keyword token, so the ident arm above
7990 // never saw it and the everyday reset form was a syntax error.
7991 Token::Default => Ok(crate::ast::SetValue::Default),
7992 Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
7993 Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
7994 Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
7995 // v7.14.0 — MySQL session/user variable RHS
7996 // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
7997 // Wrap as Ident so the SET handler can record it; the
7998 // engine treats `@VAR` / `@@VAR` values as opaque
7999 // strings.
8000 Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8001 // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8002 // is the common MySQL preamble shape. Allow a `+` or
8003 // `-` prefix on negative numerics for parity with PG
8004 // (some param defaults are negative).
8005 Token::Minus => match self.advance() {
8006 Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8007 Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8008 other => Err(self.err(format!(
8009 "expected numeric after `-` in SET value, got {other:?}"
8010 ))),
8011 },
8012 other => Err(self.err(format!(
8013 "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8014 ))),
8015 }
8016 }
8017
8018 /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8019 /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8020 /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8021 /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8022 /// present). Modes are comma-separated per PG; SPG also
8023 /// accepts space-separated for tolerance. READ ONLY / WRITE
8024 /// / DEFERRABLE are parsed-and-ignored (recorded for future
8025 /// surface but not behaviorally honoured today).
8026 /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8027 /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8028 /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8029 /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8030 /// session default rather than forcing READ COMMITTED.
8031 fn parse_isolation_level_clauses(&mut self) -> Result<Option<IsolationLevel>, ParseError> {
8032 let mut level = IsolationLevel::default();
8033 let mut have_level = false;
8034 loop {
8035 // ISOLATION LEVEL …
8036 let saw_isolation =
8037 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8038 if saw_isolation {
8039 self.advance(); // ISOLATION
8040 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8041 return Err(self.err(alloc::format!(
8042 "expected LEVEL after ISOLATION, got {:?}",
8043 self.peek()
8044 )));
8045 }
8046 self.advance(); // LEVEL
8047 // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8048 let w1 = self
8049 .expect_ident_like()
8050 .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8051 let lc = w1.to_ascii_lowercase();
8052 level = match lc.as_str() {
8053 "serializable" => IsolationLevel::Serializable,
8054 "repeatable" => {
8055 // Expect READ
8056 let w2 = self
8057 .expect_ident_like()
8058 .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8059 if !w2.eq_ignore_ascii_case("read") {
8060 return Err(self.err(alloc::format!(
8061 "expected READ after REPEATABLE, got {w2:?}"
8062 )));
8063 }
8064 IsolationLevel::RepeatableRead
8065 }
8066 "read" => {
8067 let w2 = self
8068 .expect_ident_like()
8069 .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8070 match w2.to_ascii_lowercase().as_str() {
8071 "committed" => IsolationLevel::ReadCommitted,
8072 "uncommitted" => IsolationLevel::ReadUncommitted,
8073 other => {
8074 return Err(self.err(alloc::format!(
8075 "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8076 )));
8077 }
8078 }
8079 }
8080 other => {
8081 return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8082 }
8083 };
8084 have_level = true;
8085 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8086 // READ ONLY | READ WRITE — parsed, not behaviorally honoured.
8087 self.advance();
8088 match self.peek().clone() {
8089 Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8090 self.advance();
8091 }
8092 Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8093 self.advance();
8094 }
8095 other => {
8096 return Err(self.err(alloc::format!(
8097 "expected ONLY or WRITE after READ, got {other:?}"
8098 )));
8099 }
8100 }
8101 } else if matches!(self.peek(), Token::Not) {
8102 // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8103 self.advance();
8104 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8105 return Err(self.err(alloc::format!(
8106 "expected DEFERRABLE after NOT, got {:?}",
8107 self.peek()
8108 )));
8109 }
8110 self.advance();
8111 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8112 {
8113 self.advance();
8114 } else {
8115 break;
8116 }
8117 // Optional comma between modes.
8118 if matches!(self.peek(), Token::Comma) {
8119 self.advance();
8120 }
8121 }
8122 Ok(have_level.then_some(level))
8123 }
8124
8125 fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8126 // FOR is a v6.1.2-reserved keyword (Token::For). The
8127 // other two are bare idents — they've never needed lexer
8128 // support and we keep it that way.
8129 if !matches!(self.peek(), Token::For) {
8130 return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8131 }
8132 self.advance();
8133 self.expect_keyword_ident("wal")?;
8134 self.expect_keyword_ident("position")?;
8135 let pos = self.expect_u64_literal()?;
8136 let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8137 {
8138 self.advance();
8139 self.expect_keyword_ident("timeout")?;
8140 Some(self.expect_u64_literal()?)
8141 } else {
8142 None
8143 };
8144 Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8145 }
8146
8147 /// v6.1.7 helper — consume a `Token::Integer` and check it
8148 /// fits `u64`. WAL positions and millisecond timeouts are
8149 /// non-negative.
8150 fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8151 match self.advance() {
8152 Token::Integer(n) if n >= 0 => Ok(n as u64),
8153 Token::Integer(n) => Err(ParseError {
8154 message: format!("expected non-negative integer, got {n}"),
8155 token_pos: self.consumed_pos(),
8156 }),
8157 other => Err(ParseError {
8158 message: format!("expected integer literal, got {other:?}"),
8159 token_pos: self.consumed_pos(),
8160 }),
8161 }
8162 }
8163
8164 /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8165 /// ROLE '<role>' (defaults to readonly). All string slots accept
8166 /// either a quoted ident or a quoted string literal.
8167 /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8168 /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8169 ///
8170 /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8171 /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8172 /// wire role) still parses — it is a different axis from the PG attributes.
8173 /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8174 /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8175 /// or RESET, so the plain attribute forms keep their old path.
8176 fn peeks_db_role_setting(&self) -> bool {
8177 let mut i = self.pos + 1; // past the object's name
8178 let word = |p: usize| -> Option<String> {
8179 match self.tokens.get(p) {
8180 Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8181 Some(Token::In) => Some(String::from("in")),
8182 _ => None,
8183 }
8184 };
8185 if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8186 i += 3; // IN DATABASE <name>
8187 }
8188 matches!(word(i).as_deref(), Some("set" | "reset"))
8189 }
8190
8191 fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8192 use crate::ast::SetDbRoleSettingStatement;
8193 // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8194 // identifier, so the ordinary name reader refuses it. Same trap
8195 // as TABLE / INDEX / FULL / DEFAULT before it.
8196 let name = if matches!(self.peek(), Token::All) {
8197 self.advance();
8198 String::from("all")
8199 } else {
8200 self.expect_ident_or_string()?
8201 };
8202 // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8203 let all = name.eq_ignore_ascii_case("all");
8204 let (mut database, mut role) = if is_database {
8205 (Some(name), None)
8206 } else if all {
8207 (None, None)
8208 } else {
8209 (None, Some(name))
8210 };
8211 if matches!(self.peek(), Token::In) {
8212 self.advance();
8213 self.advance(); // DATABASE
8214 database = Some(self.expect_ident_or_string()?);
8215 }
8216 let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8217 self.advance(); // SET | RESET
8218 if resetting && matches!(self.peek(), Token::All) {
8219 self.advance();
8220 self.consume_until_statement_boundary();
8221 return Ok(Statement::SetDbRoleSetting(Box::new(
8222 SetDbRoleSettingStatement {
8223 database,
8224 role,
8225 param: None,
8226 value: None,
8227 },
8228 )));
8229 }
8230 let param = self.expect_ident_like()?;
8231 let value = if resetting {
8232 None
8233 } else {
8234 // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8235 // KEYWORD, so the ident-only check missed it and consumed
8236 // the word itself as the value — the same trap as ALL, one
8237 // clause over.
8238 if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8239 self.advance();
8240 }
8241 Some(self.take_guc_value())
8242 };
8243 self.consume_until_statement_boundary();
8244 Ok(Statement::SetDbRoleSetting(Box::new(
8245 SetDbRoleSettingStatement {
8246 database,
8247 role,
8248 param: Some(param),
8249 value,
8250 },
8251 )))
8252 }
8253
8254 /// The remainder of a `SET <p> = …` clause as PG renders it back:
8255 /// a quoted literal loses its quotes, a bare word or number does not.
8256 fn take_guc_value(&mut self) -> String {
8257 match self.advance() {
8258 Token::String(s) => s,
8259 Token::Integer(n) => format!("{n}"),
8260 Token::Float(f) => format!("{f}"),
8261 Token::Ident(s) | Token::QuotedIdent(s) => s,
8262 other => format!("{other:?}"),
8263 }
8264 }
8265
8266 fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8267 let name = self.expect_ident_or_string()?;
8268 if self.peek_keyword_ident("with") {
8269 self.advance();
8270 }
8271 let mut password = String::new();
8272 let mut role = String::new();
8273 let mut login: Option<bool> = None;
8274 let mut inherit: Option<bool> = None;
8275 let mut superuser: Option<bool> = None;
8276 // Not a `while let`: the pattern would borrow `self` across the
8277 // body, which calls `self.advance()` / `self.expect_*` (&mut).
8278 #[allow(clippy::while_let_loop)]
8279 loop {
8280 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8281 break;
8282 };
8283 match w.to_ascii_lowercase().as_str() {
8284 "password" => {
8285 self.advance();
8286 password = self.expect_string_literal()?;
8287 }
8288 // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8289 // is the same slot.
8290 "encrypted" => {
8291 self.advance();
8292 self.expect_keyword_ident("password")?;
8293 password = self.expect_string_literal()?;
8294 }
8295 "login" => {
8296 self.advance();
8297 login = Some(true);
8298 }
8299 "nologin" => {
8300 self.advance();
8301 login = Some(false);
8302 }
8303 "inherit" => {
8304 self.advance();
8305 inherit = Some(true);
8306 }
8307 "noinherit" => {
8308 self.advance();
8309 inherit = Some(false);
8310 }
8311 "superuser" => {
8312 self.advance();
8313 superuser = Some(true);
8314 }
8315 "nosuperuser" => {
8316 self.advance();
8317 superuser = Some(false);
8318 }
8319 // SPG's own coarse wire role: `ROLE 'readwrite'`.
8320 "role" => {
8321 self.advance();
8322 role = self.expect_string_literal()?;
8323 }
8324 // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8325 // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8326 // accepted and ignored so a pg_dump role block restores. They
8327 // gate capabilities SPG does not have.
8328 "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8329 | "noreplication" | "bypassrls" | "nobypassrls" => {
8330 self.advance();
8331 }
8332 "connection" => {
8333 self.advance();
8334 self.expect_keyword_ident("limit")?;
8335 self.advance(); // the number
8336 }
8337 "valid" => {
8338 self.advance();
8339 self.expect_keyword_ident("until")?;
8340 self.expect_string_literal()?;
8341 }
8342 _ => break,
8343 }
8344 }
8345 if role.is_empty() {
8346 role = "readonly".to_string();
8347 }
8348 Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8349 name,
8350 password,
8351 role,
8352 login,
8353 inherit,
8354 superuser,
8355 is_user,
8356 }))
8357 }
8358
8359 /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8360 /// consumed the USING / WITH CHECK keyword.
8361 fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8362 if !matches!(self.peek(), Token::LParen) {
8363 return Err(self.err(alloc::format!(
8364 "expected '(' after {clause}, got {:?}",
8365 self.peek()
8366 )));
8367 }
8368 self.advance();
8369 let e = self.parse_expr(0)?;
8370 if !matches!(self.peek(), Token::RParen) {
8371 return Err(self.err(alloc::format!(
8372 "expected ')' to close {clause}, got {:?}",
8373 self.peek()
8374 )));
8375 }
8376 self.advance();
8377 Ok(e)
8378 }
8379
8380 /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8381 fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8382 let mut roles = Vec::new();
8383 loop {
8384 roles.push(self.expect_ident_like()?);
8385 if matches!(self.peek(), Token::Comma) {
8386 self.advance();
8387 } else {
8388 break;
8389 }
8390 }
8391 Ok(roles)
8392 }
8393
8394 /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8395 /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8396 /// `CREATE POLICY`.
8397 fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8398 use crate::ast::PolicyCmd;
8399 let name = self.expect_ident_like()?;
8400 if !matches!(self.peek(), Token::On) {
8401 return Err(self.err(alloc::format!(
8402 "expected ON after CREATE POLICY name, got {:?}",
8403 self.peek()
8404 )));
8405 }
8406 self.advance();
8407 let table = self.expect_ident_like()?;
8408
8409 let mut permissive = true;
8410 if matches!(self.peek(), Token::As) {
8411 self.advance();
8412 let w = self.expect_ident_like()?;
8413 permissive = if w.eq_ignore_ascii_case("permissive") {
8414 true
8415 } else if w.eq_ignore_ascii_case("restrictive") {
8416 false
8417 } else {
8418 return Err(self.err(alloc::format!(
8419 "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8420 )));
8421 };
8422 }
8423
8424 let mut cmd = PolicyCmd::All;
8425 if matches!(self.peek(), Token::For) {
8426 self.advance();
8427 cmd = self.parse_policy_cmd()?;
8428 }
8429
8430 let mut roles = Vec::new();
8431 if matches!(self.peek(), Token::To) {
8432 self.advance();
8433 roles = self.parse_policy_roles()?;
8434 }
8435
8436 let mut using = None;
8437 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8438 {
8439 self.advance();
8440 using = Some(self.parse_paren_expr("USING")?);
8441 }
8442
8443 let mut with_check = None;
8444 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8445 {
8446 self.advance();
8447 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8448 {
8449 return Err(self.err(alloc::format!(
8450 "expected CHECK after WITH, got {:?}",
8451 self.peek()
8452 )));
8453 }
8454 self.advance();
8455 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8456 }
8457
8458 // Clause-per-command matrix (PG wording).
8459 match cmd {
8460 PolicyCmd::Insert => {
8461 if using.is_some() {
8462 return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8463 }
8464 }
8465 PolicyCmd::Select | PolicyCmd::Delete => {
8466 if with_check.is_some() {
8467 return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8468 }
8469 }
8470 PolicyCmd::Update | PolicyCmd::All => {}
8471 }
8472
8473 Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8474 name,
8475 table,
8476 permissive,
8477 cmd,
8478 roles,
8479 using,
8480 with_check,
8481 }))
8482 }
8483
8484 /// v7.39 (RLS) — the command word after `FOR`.
8485 fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8486 use crate::ast::PolicyCmd;
8487 match self.peek().clone() {
8488 Token::All => {
8489 self.advance();
8490 Ok(PolicyCmd::All)
8491 }
8492 Token::Select => {
8493 self.advance();
8494 Ok(PolicyCmd::Select)
8495 }
8496 Token::Insert => {
8497 self.advance();
8498 Ok(PolicyCmd::Insert)
8499 }
8500 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8501 self.advance();
8502 Ok(PolicyCmd::Update)
8503 }
8504 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8505 self.advance();
8506 Ok(PolicyCmd::Delete)
8507 }
8508 other => Err(self.err(alloc::format!(
8509 "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8510 ))),
8511 }
8512 }
8513
8514 /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8515 /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8516 fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8517 let name = self.expect_ident_like()?;
8518 if !matches!(self.peek(), Token::On) {
8519 return Err(self.err(alloc::format!(
8520 "expected ON after ALTER POLICY name, got {:?}",
8521 self.peek()
8522 )));
8523 }
8524 self.advance();
8525 let table = self.expect_ident_like()?;
8526
8527 // RENAME TO new
8528 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8529 {
8530 self.advance();
8531 if !matches!(self.peek(), Token::To) {
8532 return Err(self.err(alloc::format!(
8533 "expected TO after RENAME, got {:?}",
8534 self.peek()
8535 )));
8536 }
8537 self.advance();
8538 let new = self.expect_ident_like()?;
8539 return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8540 name,
8541 table,
8542 rename_to: Some(new),
8543 roles: None,
8544 using: None,
8545 with_check: None,
8546 }));
8547 }
8548
8549 let mut roles = None;
8550 if matches!(self.peek(), Token::To) {
8551 self.advance();
8552 roles = Some(self.parse_policy_roles()?);
8553 }
8554 let mut using = None;
8555 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8556 {
8557 self.advance();
8558 using = Some(self.parse_paren_expr("USING")?);
8559 }
8560 let mut with_check = None;
8561 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8562 {
8563 self.advance();
8564 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8565 {
8566 return Err(self.err(alloc::format!(
8567 "expected CHECK after WITH, got {:?}",
8568 self.peek()
8569 )));
8570 }
8571 self.advance();
8572 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8573 }
8574 Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8575 name,
8576 table,
8577 rename_to: None,
8578 roles,
8579 using,
8580 with_check,
8581 }))
8582 }
8583
8584 /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
8585 /// `DROP POLICY`.
8586 fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8587 let if_exists = self.consume_if_exists();
8588 let name = self.expect_ident_like()?;
8589 if !matches!(self.peek(), Token::On) {
8590 return Err(self.err(alloc::format!(
8591 "expected ON after DROP POLICY name, got {:?}",
8592 self.peek()
8593 )));
8594 }
8595 self.advance();
8596 let table = self.expect_ident_like()?;
8597 Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
8598 name,
8599 table,
8600 if_exists,
8601 }))
8602 }
8603}
8604fn wrap_from_leaves(
8605 e: &mut Expr,
8606 names: &[String],
8607 make: &dyn Fn(Expr) -> Expr,
8608 refs: &dyn Fn(&Expr) -> bool,
8609) {
8610 if let Expr::Column(c) = e {
8611 if c.qualifier
8612 .as_deref()
8613 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
8614 {
8615 let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
8616 *e = make(taken);
8617 }
8618 return;
8619 }
8620 match e {
8621 Expr::Binary { lhs, rhs, .. } => {
8622 wrap_from_leaves(lhs, names, make, refs);
8623 wrap_from_leaves(rhs, names, make, refs);
8624 }
8625 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
8626 wrap_from_leaves(expr, names, make, refs)
8627 }
8628 Expr::FunctionCall { args, .. } => {
8629 for a in args.iter_mut() {
8630 wrap_from_leaves(a, names, make, refs);
8631 }
8632 }
8633 Expr::Case {
8634 operand,
8635 branches,
8636 else_branch,
8637 } => {
8638 if let Some(o) = operand.as_deref_mut() {
8639 wrap_from_leaves(o, names, make, refs);
8640 }
8641 for (w, t) in branches.iter_mut() {
8642 wrap_from_leaves(w, names, make, refs);
8643 wrap_from_leaves(t, names, make, refs);
8644 }
8645 if let Some(el) = else_branch.as_deref_mut() {
8646 wrap_from_leaves(el, names, make, refs);
8647 }
8648 }
8649 // Compound variants the walk doesn't decompose: keep the
8650 // pre-D.30 behavior — wrap the whole sub-expr if it touches
8651 // a source table, so nothing regresses.
8652 other => {
8653 if refs(other) {
8654 let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
8655 *other = make(taken);
8656 }
8657 }
8658 }
8659}
8660
8661/// v7.39 (round 241) — does this expression reference any of the FROM /
8662/// USING table names (shared by the UPDATE…FROM and DELETE…USING
8663/// lowerings)?
8664fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
8665 match e {
8666 Expr::Column(c) => c
8667 .qualifier
8668 .as_deref()
8669 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
8670 Expr::Binary { lhs, rhs, .. } => {
8671 expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
8672 }
8673 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
8674 Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
8675 Expr::Case {
8676 operand,
8677 branches,
8678 else_branch,
8679 } => {
8680 operand
8681 .as_deref()
8682 .is_some_and(|o| expr_refs_tables(o, names))
8683 || branches
8684 .iter()
8685 .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
8686 || else_branch
8687 .as_deref()
8688 .is_some_and(|el| expr_refs_tables(el, names))
8689 }
8690 _ => false,
8691 }
8692}
8693
8694impl Parser {
8695 /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
8696 /// Caller already consumed the leading `UPDATE` ident.
8697 /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
8698 /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
8699 /// after the target name has been read. `JOIN` is a reserved token;
8700 /// the qualifiers are bare idents.
8701 fn peek_is_update_join_start(&self) -> bool {
8702 match self.peek() {
8703 // JOIN and its qualifiers are reserved lexer tokens (the grammar
8704 // dedicates arms to `LEFT [OUTER] JOIN` and friends).
8705 Token::Join
8706 | Token::Inner
8707 | Token::Left
8708 | Token::Right
8709 | Token::Cross
8710 | Token::Full => true,
8711 // NATURAL / STRAIGHT_JOIN arrive as bare idents.
8712 Token::Ident(s) | Token::QuotedIdent(s) => {
8713 matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
8714 }
8715 _ => false,
8716 }
8717 }
8718
8719 /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
8720 /// USER-variable assignment. Its own per-session namespace, an arbitrary
8721 /// expression on the right, and `:=` as a second spelling of `=`.
8722 ///
8723 /// Out-of-line (`inline(never)`): the statement-parse frame it is called
8724 /// from sits on the nesting recursion chain (a CTE body, a subquery),
8725 /// and holding this loop's `Vec` + `String` locals there overflowed the
8726 /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
8727 #[inline(never)]
8728 fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
8729 let mut assigns: Vec<(String, Expr)> = Vec::new();
8730 let mut settings: Vec<(String, Expr)> = Vec::new();
8731 loop {
8732 // v7.39 (round 554) — a plain NAME here is a session
8733 // setting, not a user variable. mysqldump writes the two in
8734 // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
8735 // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
8736 // changes it — and this refused the mixture outright, so no
8737 // dump could be restored past its preamble.
8738 if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
8739 self.advance();
8740 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8741 return Err(self.err(alloc::format!(
8742 "expected `=` after {name}, got {:?}",
8743 self.peek()
8744 )));
8745 }
8746 self.advance();
8747 let value = self.parse_expr(0)?;
8748 settings.push((name.to_ascii_lowercase(), value));
8749 if matches!(self.peek(), Token::Comma) {
8750 self.advance();
8751 continue;
8752 }
8753 break;
8754 }
8755 let Token::SessionVar(raw) = self.peek().clone() else {
8756 return Err(self.err(alloc::format!(
8757 "expected a user variable after SET, got {:?}",
8758 self.peek()
8759 )));
8760 };
8761 if raw.starts_with("@@") {
8762 return Err(self.err(alloc::string::String::from(
8763 "cannot mix `@@` settings with `@` user variables in one SET",
8764 )));
8765 }
8766 self.advance();
8767 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8768 return Err(self.err(alloc::format!(
8769 "expected `=` or `:=` after {raw}, got {:?}",
8770 self.peek()
8771 )));
8772 }
8773 self.advance();
8774 let value = self.parse_expr(0)?;
8775 assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
8776 if matches!(self.peek(), Token::Comma) {
8777 self.advance();
8778 continue;
8779 }
8780 break;
8781 }
8782 Ok(Statement::SetUserVars(assigns, settings))
8783 }
8784
8785 fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
8786 // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
8787 // NAMED `only` until now, which failed on `relation "only" does
8788 // not exist`. The lookahead is what keeps a table actually
8789 // called `only` working: the keyword is only a keyword when a
8790 // TABLE NAME follows it — and `SET` arrives as an identifier
8791 // here, so `UPDATE only SET a = 2` would otherwise take `SET`
8792 // for the table and die on the `=`. Measured by the pin.
8793 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
8794 if s.eq_ignore_ascii_case("only"))
8795 && matches!(
8796 self.tokens.get(self.pos + 1),
8797 Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
8798 );
8799 if only {
8800 self.advance();
8801 }
8802 let table = self.expect_ident_like()?;
8803 // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
8804 // bare spelling; a bare identifier that is the SET keyword itself
8805 // is the clause, not an alias.
8806 // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
8807 // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
8808 // multi-table form, and swallowing `LEFT` as `a`'s alias made the
8809 // following JOIN a syntax error.
8810 let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
8811 let alias = if matches!(self.peek(), Token::As) {
8812 self.advance();
8813 Some(self.expect_ident_like()?)
8814 } else {
8815 match self.peek() {
8816 Token::Ident(s) | Token::QuotedIdent(s)
8817 if !s.eq_ignore_ascii_case("set") && !starts_join =>
8818 {
8819 let a = s.clone();
8820 self.advance();
8821 Some(a)
8822 }
8823 _ => None,
8824 }
8825 };
8826 // v7.39 (round 420) — MySQL's multi-table UPDATE:
8827 // UPDATE a, b SET a.v = b.v WHERE a.id = b.id
8828 // UPDATE a JOIN b ON a.id = b.id SET a.v = b.v + 1
8829 // UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
8830 // The FIRST table is the mutation target and the rest are sources —
8831 // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
8832 // SPG already lowers onto correlated subqueries. So rewind, let
8833 // `parse_from_clause` read the whole list (it handles aliases, comma
8834 // lists, and every JOIN form), then peel the target off the front.
8835 let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
8836 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
8837 {
8838 // NOTE: `advance()` destroys the tokens it returns
8839 // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
8840 // is NOT possible — the tail is read forward, once, through the
8841 // same grammar `parse_from_clause` uses after its primary.
8842 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
8843 let mut joins = self.parse_from_joins(&target_qual)?;
8844 if joins.is_empty() {
8845 return Err(self.err(alloc::string::String::from(
8846 "multi-table UPDATE needs at least one source table",
8847 )));
8848 }
8849 let head = joins.remove(0);
8850 // A LEFT join keeps every target row (the unmatched ones see NULL
8851 // on the source side), so it must NOT get the EXISTS row filter
8852 // the inner / comma forms use.
8853 let outer = matches!(head.kind, crate::ast::JoinKind::Left);
8854 let src = FromClause {
8855 primary: head.table,
8856 joins,
8857 };
8858 (Some(src), head.on, outer)
8859 } else {
8860 (None, None, false)
8861 };
8862 self.expect_keyword_ident("set")?;
8863 let mut assignments = Vec::new();
8864 loop {
8865 // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
8866 // …)` — the parenthesized multi-assignment. Expressions
8867 // assign positionally; a subquery RHS clones per column
8868 // keeping only the Nth projection item.
8869 if matches!(self.peek(), Token::LParen) {
8870 self.advance();
8871 let mut cols = alloc::vec![self.expect_ident_like()?];
8872 while matches!(self.peek(), Token::Comma) {
8873 self.advance();
8874 cols.push(self.expect_ident_like()?);
8875 }
8876 if !matches!(self.peek(), Token::RParen) {
8877 return Err(self.err(format!(
8878 "expected ')' after SET column list, got {:?}",
8879 self.peek()
8880 )));
8881 }
8882 self.advance();
8883 if !matches!(self.peek(), Token::Eq) {
8884 return Err(self.err(format!(
8885 "expected `=` after SET column list, got {:?}",
8886 self.peek()
8887 )));
8888 }
8889 self.advance();
8890 if !matches!(self.peek(), Token::LParen) {
8891 return Err(self.err(format!(
8892 "expected '(' after SET (…) =, got {:?}",
8893 self.peek()
8894 )));
8895 }
8896 self.advance();
8897 if matches!(self.peek(), Token::Select) {
8898 let inner = match self.parse_select_stmt()? {
8899 Statement::Select(s) => s,
8900 other => {
8901 return Err(self.err(alloc::format!(
8902 "expected SELECT in SET (…) = (SELECT …), got {other:?}"
8903 )));
8904 }
8905 };
8906 if !matches!(self.peek(), Token::RParen) {
8907 return Err(self.err(format!(
8908 "expected ')' after SET subquery, got {:?}",
8909 self.peek()
8910 )));
8911 }
8912 self.advance();
8913 if inner.items.len() != cols.len() {
8914 return Err(self.err(alloc::format!(
8915 "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
8916 cols.len(),
8917 inner.items.len()
8918 )));
8919 }
8920 for (i, col) in cols.into_iter().enumerate() {
8921 let mut sub = inner.clone();
8922 sub.items = alloc::vec![sub.items[i].clone()];
8923 assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
8924 }
8925 } else {
8926 let mut exprs = alloc::vec![self.parse_expr(0)?];
8927 while matches!(self.peek(), Token::Comma) {
8928 self.advance();
8929 exprs.push(self.parse_expr(0)?);
8930 }
8931 if !matches!(self.peek(), Token::RParen) {
8932 return Err(self.err(format!(
8933 "expected ')' after SET row values, got {:?}",
8934 self.peek()
8935 )));
8936 }
8937 self.advance();
8938 if exprs.len() != cols.len() {
8939 return Err(self.err(alloc::format!(
8940 "SET (…) = (…) arity mismatch: {} columns, {} values",
8941 cols.len(),
8942 exprs.len()
8943 )));
8944 }
8945 for (col, e) in cols.into_iter().zip(exprs) {
8946 assignments.push((col, e));
8947 }
8948 }
8949 if matches!(self.peek(), Token::Comma) {
8950 self.advance();
8951 continue;
8952 }
8953 break;
8954 }
8955 // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
8956 // assignment targets (`SET a.v = b.v`). `expect_ident_like`
8957 // SILENTLY strips a `<qual>.` prefix (it exists for PG's
8958 // `public.` dump qualifiers), so the qualifier has to be read off
8959 // the token stream first — otherwise `SET b.v = 888` would write
8960 // to the TARGET table's `v` while naming a source table, a
8961 // silent-wrong. A qualifier naming a SOURCE table means a
8962 // multi-TARGET update — mutating two tables in one statement —
8963 // which SPG does not model, so it is refused loudly.
8964 let set_qual: Option<String> = if mysql_from.is_some()
8965 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
8966 {
8967 match self.peek() {
8968 Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
8969 _ => None,
8970 }
8971 } else {
8972 None
8973 };
8974 let col = self.expect_ident_like()?;
8975 if let Some(q) = set_qual {
8976 let names_target = q.eq_ignore_ascii_case(&table)
8977 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
8978 if !names_target {
8979 return Err(self.err(alloc::format!(
8980 "multi-table UPDATE can only assign to its first table \
8981 ({table}); `{q}.{col}` targets another table"
8982 )));
8983 }
8984 }
8985 // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
8986 // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
8987 // `__column_default` marker lowering just below). PG assigns to the
8988 // i-th (1-based) element, NULL-padding when i exceeds the length.
8989 if matches!(self.peek(), Token::LBracket) {
8990 self.advance();
8991 let index = self.parse_expr(0)?;
8992 // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
8993 // (and the open `arr[lo:]`), lowered to
8994 // `__array_assign_slice`. Only the single-subscript form
8995 // parsed before, so a slice assignment was a syntax error.
8996 let mut slice_hi: Option<Option<Expr>> = None;
8997 if matches!(self.peek(), Token::Colon) {
8998 self.advance();
8999 slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9000 None
9001 } else {
9002 Some(self.parse_expr(0)?)
9003 });
9004 }
9005 if !matches!(self.peek(), Token::RBracket) {
9006 return Err(self.err(format!(
9007 "expected `]` after array subscript in UPDATE SET, got {:?}",
9008 self.peek()
9009 )));
9010 }
9011 self.advance();
9012 if !matches!(self.peek(), Token::Eq) {
9013 return Err(self.err(format!(
9014 "expected `=` after array subscript in UPDATE SET, got {:?}",
9015 self.peek()
9016 )));
9017 }
9018 self.advance();
9019 let value = self.parse_expr(0)?;
9020 // PG merges several subscript writes to the same column into one
9021 // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9022 // assignment to `col` rather than each overwriting the original.
9023 let existing = assignments.iter().position(|(c, _)| c == &col);
9024 let base = match existing {
9025 Some(i) => assignments[i].1.clone(),
9026 None => Expr::Column(ColumnName {
9027 qualifier: None,
9028 name: col.clone(),
9029 }),
9030 };
9031 let assigned = match slice_hi {
9032 None => Expr::FunctionCall {
9033 name: "__array_assign".to_string(),
9034 args: alloc::vec![base, index, value],
9035 },
9036 Some(hi) => Expr::FunctionCall {
9037 name: "__array_assign_slice".to_string(),
9038 args: alloc::vec![
9039 base,
9040 index,
9041 hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9042 value,
9043 ],
9044 },
9045 };
9046 match existing {
9047 Some(i) => assignments[i].1 = assigned,
9048 None => assignments.push((col, assigned)),
9049 }
9050 if matches!(self.peek(), Token::Comma) {
9051 self.advance();
9052 continue;
9053 }
9054 break;
9055 }
9056 if !matches!(self.peek(), Token::Eq) {
9057 return Err(self.err(format!(
9058 "expected `=` after column name in UPDATE SET, got {:?}",
9059 self.peek()
9060 )));
9061 }
9062 self.advance();
9063 // `SET col = DEFAULT` — the column's declared default;
9064 // rides out as a marker call the update executor
9065 // resolves against the schema.
9066 let value = if matches!(self.peek(), Token::Default) {
9067 self.advance();
9068 Expr::FunctionCall {
9069 name: "__column_default".to_string(),
9070 args: Vec::new(),
9071 }
9072 } else {
9073 self.parse_expr(0)?
9074 };
9075 assignments.push((col, value));
9076 if matches!(self.peek(), Token::Comma) {
9077 self.advance();
9078 continue;
9079 }
9080 break;
9081 }
9082 // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9083 // update. Lowers onto the correlated-subquery machinery:
9084 // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9085 // and each assignment that references a FROM-list table
9086 // wraps into a correlated scalar subquery
9087 // (SELECT expr FROM src WHERE cond). Equivalent for the
9088 // unique-join shape (the overwhelmingly common one); a
9089 // multi-match, which PG resolves by arbitrary pick,
9090 // surfaces as a scalar-subquery cardinality error instead
9091 // of a silent arbitrary result.
9092 // v7.39 (round 420) — the MySQL multi-table form supplies the source
9093 // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9094 // the SAME lowering below. Both spellings together is not legal in
9095 // either dialect.
9096 let from_clause = if let Some(fc) = mysql_from {
9097 if matches!(self.peek(), Token::From) {
9098 return Err(self.err(alloc::string::String::from(
9099 "multi-table UPDATE already names its sources; drop the FROM clause",
9100 )));
9101 }
9102 Some(fc)
9103 } else if matches!(self.peek(), Token::From) {
9104 self.advance();
9105 Some(self.parse_from_clause()?)
9106 } else {
9107 None
9108 };
9109 let where_ = if matches!(self.peek(), Token::Where) {
9110 self.advance();
9111 Some(self.parse_expr(0)?)
9112 } else {
9113 None
9114 };
9115 // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9116 // and the TARGET-row filter are NOT the same predicate once a LEFT
9117 // join is involved:
9118 // * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9119 // one conjunction, and the whole thing filters target rows via
9120 // EXISTS.
9121 // * LEFT join: only the ON predicate belongs inside the source
9122 // subquery. The WHERE still filters TARGET rows (with source
9123 // columns read through the correlated subquery, which yields NULL
9124 // for an unmatched row — exactly LEFT-join semantics).
9125 // Round 420 folded ON into WHERE unconditionally and then dropped the
9126 // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9127 // WHERE a.id > 1` updated EVERY row.
9128 let sub_where = match (mysql_on.clone(), where_.clone()) {
9129 _ if mysql_outer => mysql_on.clone(),
9130 (Some(on), Some(w)) => Some(Expr::Binary {
9131 lhs: Box::new(on),
9132 op: crate::ast::BinOp::And,
9133 rhs: Box::new(w),
9134 }),
9135 (Some(on), None) => Some(on),
9136 (None, w) => w,
9137 };
9138 // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9139 // has no such clause on UPDATE, so this is accepted only under the
9140 // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9141 let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9142 let mut returning = self.parse_optional_returning()?;
9143 // v7.39 (round 533) — kept for the engine, which can resolve the
9144 // UNQUALIFIED leaves this lowering has to leave alone.
9145 let from_sources = from_clause.as_ref().map(|fc| {
9146 alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9147 from: fc.clone(),
9148 sub_where: sub_where.clone(),
9149 })
9150 });
9151 let (assignments, where_) = if let Some(fc) = from_clause {
9152 let names: Vec<String> = core::iter::once(&fc.primary)
9153 .chain(fc.joins.iter().map(|j| &j.table))
9154 .flat_map(|t| {
9155 t.alias
9156 .clone()
9157 .into_iter()
9158 .chain(core::iter::once(t.name.clone()))
9159 })
9160 .collect();
9161 let refs_list = |e: &Expr| -> bool {
9162 fn walk(e: &Expr, names: &[String]) -> bool {
9163 match e {
9164 Expr::Column(c) => c
9165 .qualifier
9166 .as_deref()
9167 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9168 Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9169 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9170 Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9171 Expr::Case {
9172 operand,
9173 branches,
9174 else_branch,
9175 } => {
9176 operand.as_deref().is_some_and(|o| walk(o, names))
9177 || branches
9178 .iter()
9179 .any(|(w, t)| walk(w, names) || walk(t, names))
9180 || else_branch.as_deref().is_some_and(|el| walk(el, names))
9181 }
9182 _ => false,
9183 }
9184 }
9185 walk(e, &names)
9186 };
9187 let sub_select = |items: Vec<SelectItem>| SelectStatement {
9188 locking: None,
9189 ctes: Vec::new(),
9190 distinct: false,
9191 distinct_on: Vec::new(),
9192 items,
9193 from: Some(fc.clone()),
9194 where_: sub_where.clone(),
9195 group_by: None,
9196 group_by_all: false,
9197 having: None,
9198 unions: Vec::new(),
9199 order_by: Vec::new(),
9200 limit: None,
9201 offset: None,
9202 limit_with_ties: false,
9203 window_check_exprs: Vec::new(),
9204 };
9205 // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9206 // assignment RHS with a correlated scalar subquery, instead of
9207 // wrapping the whole RHS. Wrapping the whole expr moved a target-
9208 // column reference (`SET v = v + u.bonus`, where `v` is the target
9209 // table's column) inside a subquery whose FROM only has the source
9210 // table, so the unqualified `v` resolved against the source and
9211 // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9212 // context — where they belong — fixes it; only the source columns
9213 // (`u.bonus`) become subqueries. A whole-expr fallback covers
9214 // compound variants the leaf-walk doesn't decompose.
9215 let make_subq = |inner: Expr| {
9216 Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9217 expr: inner,
9218 alias: None,
9219 }])))
9220 };
9221 let assignments = assignments
9222 .into_iter()
9223 .map(|(col, mut expr)| {
9224 wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9225 (col, expr)
9226 })
9227 .collect();
9228 let exists = Expr::Exists {
9229 subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9230 expr: Expr::Literal(Literal::Integer(1)),
9231 alias: None,
9232 }])),
9233 negated: false,
9234 };
9235 // v7.39 (round 241) — RETURNING may reference the FROM-list
9236 // tables too (`RETURNING emp.id, dept.name`); the same
9237 // leaf-to-correlated-subquery lowering the assignments get.
9238 // Without it the qualifier died at eval with "unknown table
9239 // qualifier". (RETURNING was parsed before this block — the
9240 // lowering is a pure AST transformation.)
9241 if let Some(items) = returning.as_mut() {
9242 for item in items.iter_mut() {
9243 if let SelectItem::Expr { expr, .. } = item {
9244 wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9245 }
9246 }
9247 }
9248 // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9249 // EVERY matching target row: it gets no EXISTS filter, but the
9250 // caller's WHERE still applies, with source columns read through
9251 // the correlated subquery (NULL when unmatched — LEFT-join
9252 // semantics). `sub_where` above already excluded the WHERE from
9253 // the source subquery for this case.
9254 if mysql_outer {
9255 let mut outer = where_;
9256 if let Some(w) = outer.as_mut() {
9257 wrap_from_leaves(w, &names, &make_subq, &refs_list);
9258 }
9259 (assignments, outer)
9260 } else {
9261 (assignments, Some(exists))
9262 }
9263 } else {
9264 (assignments, where_)
9265 };
9266 Ok(Statement::Update(crate::ast::UpdateStatement {
9267 ctes: Vec::new(),
9268 table,
9269 only,
9270 alias,
9271 assignments,
9272 from_sources,
9273 where_,
9274 order_limit: update_order_limit,
9275 returning,
9276 }))
9277 }
9278
9279 /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9280 /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9281 /// clause and its meaning are identical, so both call this rather than
9282 /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9283 /// legal. PG has no such clause on either statement, so it is read only
9284 /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9285 /// errors.
9286 ///
9287 /// `#[inline(never)]`: its locals would otherwise land on the statement-
9288 /// parsing recursion frame, which is what tipped the 512 KiB nesting
9289 /// stack in round 430.
9290 #[inline(never)]
9291 fn parse_mysql_dml_order_limit(
9292 &mut self,
9293 what: &str,
9294 ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9295 if !self.mysql_dialect {
9296 return Ok(None);
9297 }
9298 let order_by = self.parse_order_by_keys()?;
9299 let limit = if matches!(self.peek(), Token::Limit) {
9300 self.advance();
9301 let tok = self.advance();
9302 let Token::Integer(n) = tok else {
9303 return Err(self.err(alloc::format!(
9304 "expected integer after {what} LIMIT, got {tok:?}"
9305 )));
9306 };
9307 // MySQL rejects the `LIMIT offset, count` form here — only a
9308 // single row count is legal on a DML statement.
9309 if matches!(self.peek(), Token::Comma) {
9310 return Err(self.err(alloc::format!(
9311 "{what} LIMIT takes a row count, not an offset"
9312 )));
9313 }
9314 let n = u32::try_from(n)
9315 .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9316 Some(n)
9317 } else {
9318 None
9319 };
9320 if order_by.is_empty() && limit.is_none() {
9321 return Ok(None);
9322 }
9323 Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9324 order_by,
9325 limit,
9326 })))
9327 }
9328
9329 /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9330 /// the leading `DELETE` ident.
9331 fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9332 // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9333 // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9334 // USING a, b WHERE …` — the third MySQL spelling — needs no special
9335 // parse here; it reaches the existing USING path with the target
9336 // repeated in the list, which the source-list peel below handles.)
9337 // More than one name is a multi-TARGET delete, which SPG does not
9338 // model; it is refused rather than half-applied.
9339 let mysql_pre_target: Option<String> =
9340 if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9341 let first = self.expect_ident_like()?;
9342 if matches!(self.peek(), Token::Comma) {
9343 return Err(self.err(alloc::format!(
9344 "multi-table DELETE can only delete from one table; \
9345 `DELETE {first}, …` names several"
9346 )));
9347 }
9348 Some(first)
9349 } else {
9350 None
9351 };
9352 if !matches!(self.peek(), Token::From) {
9353 return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9354 }
9355 self.advance();
9356 // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9357 // lookahead as the UPDATE spelling.
9358 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9359 if s.eq_ignore_ascii_case("only"))
9360 && matches!(
9361 self.tokens.get(self.pos + 1),
9362 Some(Token::Ident(_) | Token::QuotedIdent(_))
9363 );
9364 if only {
9365 self.advance();
9366 }
9367 let table = self.expect_ident_like()?;
9368 // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9369 // spelling must not swallow the clause keywords that can follow
9370 // the target.
9371 let alias = if matches!(self.peek(), Token::As) {
9372 self.advance();
9373 Some(self.expect_ident_like()?)
9374 } else {
9375 match self.peek() {
9376 Token::Ident(s) | Token::QuotedIdent(s)
9377 if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9378 {
9379 let a = s.clone();
9380 self.advance();
9381 Some(a)
9382 }
9383 _ => None,
9384 }
9385 };
9386 // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9387 // through the SAME join grammar the FROM clause uses (see the
9388 // `advance()`-destroys-tokens note on `parse_from_joins`).
9389 let mut mysql_on: Option<Expr> = None;
9390 let mut mysql_outer = false;
9391 let mysql_using = if mysql_pre_target.is_some()
9392 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9393 {
9394 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9395 let mut joins = self.parse_from_joins(&target_qual)?;
9396 if joins.is_empty() {
9397 return Err(self.err(alloc::string::String::from(
9398 "multi-table DELETE needs at least one source table",
9399 )));
9400 }
9401 let head = joins.remove(0);
9402 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9403 mysql_on = head.on;
9404 Some(FromClause {
9405 primary: head.table,
9406 joins,
9407 })
9408 } else {
9409 None
9410 };
9411 // The pre-FROM target must be the table the FROM names (or its
9412 // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9413 // is not the scan target.
9414 if let Some(t) = &mysql_pre_target {
9415 let names_target = t.eq_ignore_ascii_case(&table)
9416 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9417 if !names_target {
9418 return Err(self.err(alloc::format!(
9419 "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9420 )));
9421 }
9422 }
9423 // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9424 // delete. Same lowering as UPDATE … FROM: the WHERE
9425 // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9426 // target row by the correlated machinery.
9427 let using_clause = if let Some(fc) = mysql_using {
9428 Some(fc)
9429 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9430 self.advance();
9431 let mut fc = self.parse_from_clause()?;
9432 // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9433 // repeats the TARGET as the first USING entry (PG's spelling
9434 // lists only the extra sources). Peel it so the source subquery
9435 // does not re-scan — and shadow — the target table.
9436 let primary_is_target =
9437 fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9438 if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9439 let head = fc.joins.remove(0);
9440 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9441 mysql_on = head.on;
9442 fc = FromClause {
9443 primary: head.table,
9444 joins: fc.joins,
9445 };
9446 }
9447 Some(fc)
9448 } else {
9449 None
9450 };
9451 let where_ = if matches!(self.peek(), Token::Where) {
9452 self.advance();
9453 Some(self.parse_expr(0)?)
9454 } else {
9455 None
9456 };
9457 // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9458 // read before RETURNING (MariaDB's own extension trails the LIMIT).
9459 let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9460 let mut returning = self.parse_optional_returning()?;
9461 let where_ = if let Some(fc) = using_clause {
9462 // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9463 // a USING-table reference in RETURNING becomes a correlated
9464 // scalar subquery over the USING list.
9465 let names: Vec<String> = core::iter::once(&fc.primary)
9466 .chain(fc.joins.iter().map(|j| &j.table))
9467 .flat_map(|t| {
9468 t.alias
9469 .clone()
9470 .into_iter()
9471 .chain(core::iter::once(t.name.clone()))
9472 })
9473 .collect();
9474 // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9475 // join filters the SOURCE subquery on the ON predicate alone and
9476 // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9477 // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9478 // rows); every other form folds ON and WHERE into one EXISTS.
9479 let sub_where = match (mysql_on.clone(), where_.clone()) {
9480 _ if mysql_outer => mysql_on.clone(),
9481 (Some(on), Some(w)) => Some(Expr::Binary {
9482 lhs: Box::new(on),
9483 op: crate::ast::BinOp::And,
9484 rhs: Box::new(w),
9485 }),
9486 (Some(on), None) => Some(on),
9487 (None, w) => w,
9488 };
9489 let exists_where = sub_where.clone();
9490 let sub_fc = fc.clone();
9491 let make_subq = move |leaf: Expr| -> Expr {
9492 Expr::ScalarSubquery(Box::new(SelectStatement {
9493 locking: None,
9494 ctes: Vec::new(),
9495 distinct: false,
9496 distinct_on: Vec::new(),
9497 items: alloc::vec![SelectItem::Expr {
9498 expr: leaf,
9499 alias: None,
9500 }],
9501 from: Some(sub_fc.clone()),
9502 where_: sub_where.clone(),
9503 group_by: None,
9504 group_by_all: false,
9505 having: None,
9506 unions: Vec::new(),
9507 order_by: Vec::new(),
9508 limit: None,
9509 offset: None,
9510 limit_with_ties: false,
9511 window_check_exprs: Vec::new(),
9512 }))
9513 };
9514 let refs = |e: &Expr| expr_refs_tables(e, &names);
9515 if let Some(items) = returning.as_mut() {
9516 for item in items.iter_mut() {
9517 if let SelectItem::Expr { expr, .. } = item {
9518 wrap_from_leaves(expr, &names, &make_subq, &refs);
9519 }
9520 }
9521 }
9522 // A LEFT join deletes the target rows the WHERE selects, reading
9523 // source columns through the correlated subquery (NULL when
9524 // unmatched); no EXISTS row filter.
9525 if mysql_outer {
9526 let mut outer = where_;
9527 if let Some(w) = outer.as_mut() {
9528 wrap_from_leaves(w, &names, &make_subq, &refs);
9529 }
9530 outer
9531 } else {
9532 Some(Expr::Exists {
9533 subquery: Box::new(SelectStatement {
9534 locking: None,
9535 ctes: Vec::new(),
9536 distinct: false,
9537 distinct_on: Vec::new(),
9538 items: alloc::vec![SelectItem::Expr {
9539 expr: Expr::Literal(Literal::Integer(1)),
9540 alias: None,
9541 }],
9542 from: Some(fc),
9543 where_: exists_where,
9544 group_by: None,
9545 group_by_all: false,
9546 having: None,
9547 unions: Vec::new(),
9548 order_by: Vec::new(),
9549 limit: None,
9550 offset: None,
9551 limit_with_ties: false,
9552 window_check_exprs: Vec::new(),
9553 }),
9554 negated: false,
9555 })
9556 }
9557 } else {
9558 where_
9559 };
9560 Ok(Statement::Delete(crate::ast::DeleteStatement {
9561 ctes: Vec::new(),
9562 table,
9563 only,
9564 alias,
9565 where_,
9566 order_limit: delete_order_limit,
9567 returning,
9568 }))
9569 }
9570
9571 /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
9572 /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
9573 /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
9574 /// keyword. v7.17 surface:
9575 /// * source: table reference (subquery source is a follow-up)
9576 /// * actions: UPDATE SET / DELETE / DO NOTHING (matched);
9577 /// INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
9578 /// * AND-conditioned WHEN clauses; clauses tried in declaration
9579 /// order
9580 fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
9581 // INTO
9582 let is_into_kw = matches!(self.peek(), Token::Into)
9583 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
9584 if !is_into_kw {
9585 return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
9586 }
9587 self.advance();
9588 let target = self.expect_ident_like()?;
9589 // Optional alias — bare ident before USING.
9590 let target_alias = match self.peek() {
9591 Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
9592 Some(self.expect_ident_like()?)
9593 }
9594 _ => None,
9595 };
9596 // USING
9597 let is_using_kw = matches!(
9598 self.peek(),
9599 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
9600 );
9601 if !is_using_kw {
9602 return Err(self.err(format!(
9603 "expected USING after MERGE INTO target, got {:?}",
9604 self.peek()
9605 )));
9606 }
9607 self.advance();
9608 // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
9609 // <table> [alias]`. PG requires an alias after a subquery source.
9610 let (source, source_select) = if matches!(self.peek(), Token::LParen) {
9611 self.advance(); // (
9612 // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
9613 // constant-SELECT lowering the derived-table parser uses
9614 // (PG deletes through this form; it was a parse error).
9615 let inner = if matches!(self.peek(), Token::Values) {
9616 self.advance(); // VALUES
9617 Statement::Select(self.parse_values_rows_body()?)
9618 } else {
9619 self.parse_select_stmt()?
9620 };
9621 match self.advance() {
9622 Token::RParen => {}
9623 other => {
9624 return Err(self.err(format!(
9625 "expected ')' after MERGE USING subquery, got {other:?}"
9626 )));
9627 }
9628 }
9629 let Statement::Select(sub) = inner else {
9630 return Err(self.err("MERGE USING subquery must be a SELECT".into()));
9631 };
9632 (String::new(), Some(Box::new(sub)))
9633 } else {
9634 (self.expect_ident_like()?, None)
9635 };
9636 let source_alias = match self.peek() {
9637 Token::Ident(s) | Token::QuotedIdent(s)
9638 if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
9639 {
9640 Some(self.expect_ident_like()?)
9641 }
9642 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
9643 self.advance(); // AS
9644 Some(self.expect_ident_like()?)
9645 }
9646 _ => None,
9647 };
9648 // v7.39 (round 768, F31-D5) — optional positional column-alias
9649 // list after the source alias (`s(id, v)`).
9650 let mut source_column_aliases: Vec<String> = Vec::new();
9651 if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
9652 self.advance();
9653 loop {
9654 source_column_aliases.push(self.expect_ident_like()?);
9655 match self.peek() {
9656 Token::Comma => {
9657 self.advance();
9658 }
9659 Token::RParen => {
9660 self.advance();
9661 break;
9662 }
9663 other => {
9664 return Err(self.err(format!(
9665 "expected ',' or ')' in MERGE source column list, got {other:?}"
9666 )));
9667 }
9668 }
9669 }
9670 }
9671 if source_select.is_some() && source_alias.is_none() {
9672 return Err(self.err("MERGE USING (subquery) requires an alias".into()));
9673 }
9674 // ON
9675 if !matches!(self.peek(), Token::On) {
9676 return Err(self.err(format!(
9677 "expected ON after MERGE … USING source, got {:?}",
9678 self.peek()
9679 )));
9680 }
9681 self.advance();
9682 let on = self.parse_expr(0)?;
9683 // One or more WHEN clauses.
9684 let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
9685 loop {
9686 let is_when_kw = matches!(
9687 self.peek(),
9688 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
9689 );
9690 if !is_when_kw {
9691 break;
9692 }
9693 self.advance(); // WHEN
9694 // [NOT] MATCHED
9695 let matched = if matches!(self.peek(), Token::Not) {
9696 self.advance();
9697 crate::ast::MergeMatched::NotMatched
9698 } else {
9699 crate::ast::MergeMatched::Matched
9700 };
9701 let is_matched_kw = matches!(
9702 self.peek(),
9703 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
9704 );
9705 if !is_matched_kw {
9706 return Err(self.err(format!(
9707 "expected MATCHED in WHEN clause, got {:?}",
9708 self.peek()
9709 )));
9710 }
9711 self.advance();
9712 // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
9713 // BY TARGET is the default (a synonym); BY SOURCE flips the clause
9714 // to fire for target rows no source row matches.
9715 let mut matched = matched;
9716 if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
9717 self.advance();
9718 match self.peek() {
9719 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
9720 self.advance();
9721 matched = crate::ast::MergeMatched::NotMatchedBySource;
9722 }
9723 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
9724 self.advance();
9725 }
9726 other => {
9727 return Err(self.err(format!(
9728 "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
9729 )));
9730 }
9731 }
9732 }
9733 // Optional AND <expr>
9734 let condition = if matches!(self.peek(), Token::And) {
9735 self.advance();
9736 Some(self.parse_expr(0)?)
9737 } else {
9738 None
9739 };
9740 // THEN
9741 let is_then_kw = matches!(
9742 self.peek(),
9743 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
9744 );
9745 if !is_then_kw {
9746 return Err(self.err(format!(
9747 "expected THEN in WHEN clause, got {:?}",
9748 self.peek()
9749 )));
9750 }
9751 self.advance();
9752 // Action: INSERT / UPDATE / DELETE / DO NOTHING
9753 let action = match self.peek().clone() {
9754 Token::Insert => {
9755 self.advance();
9756 // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
9757 // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
9758 // VALUES (…)` omits it and fills every column in declaration
9759 // order. PG accepts this; SPG used to require the list.
9760 let mut columns: Vec<String> = Vec::new();
9761 if matches!(self.peek(), Token::LParen) {
9762 self.advance();
9763 loop {
9764 columns.push(self.expect_ident_like()?);
9765 if matches!(self.peek(), Token::Comma) {
9766 self.advance();
9767 continue;
9768 }
9769 break;
9770 }
9771 if !matches!(self.peek(), Token::RParen) {
9772 return Err(self.err(format!(
9773 "expected ')' after INSERT column list, got {:?}",
9774 self.peek()
9775 )));
9776 }
9777 self.advance();
9778 }
9779 // VALUES (...)
9780 if !matches!(self.peek(), Token::Values) {
9781 return Err(self.err(format!(
9782 "expected VALUES in MERGE INSERT, got {:?}",
9783 self.peek()
9784 )));
9785 }
9786 self.advance();
9787 if !matches!(self.peek(), Token::LParen) {
9788 return Err(self.err(format!(
9789 "expected '(' after VALUES in MERGE INSERT, got {:?}",
9790 self.peek()
9791 )));
9792 }
9793 self.advance();
9794 let mut values: Vec<crate::ast::Expr> = Vec::new();
9795 loop {
9796 values.push(self.parse_expr(0)?);
9797 if matches!(self.peek(), Token::Comma) {
9798 self.advance();
9799 continue;
9800 }
9801 break;
9802 }
9803 if !matches!(self.peek(), Token::RParen) {
9804 return Err(self.err(format!(
9805 "expected ')' after MERGE INSERT values, got {:?}",
9806 self.peek()
9807 )));
9808 }
9809 self.advance();
9810 // Empty column list = positional into every column, so the
9811 // count is checked against the table arity at execution.
9812 if !columns.is_empty() && columns.len() != values.len() {
9813 return Err(self.err(format!(
9814 "MERGE INSERT column count ({}) ≠ value count ({})",
9815 columns.len(),
9816 values.len()
9817 )));
9818 }
9819 crate::ast::MergeAction::Insert { columns, values }
9820 }
9821 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
9822 self.advance();
9823 // SET
9824 let is_set_kw = matches!(
9825 self.peek(),
9826 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
9827 );
9828 if !is_set_kw {
9829 return Err(self.err(format!(
9830 "expected SET after UPDATE in MERGE, got {:?}",
9831 self.peek()
9832 )));
9833 }
9834 self.advance();
9835 let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
9836 loop {
9837 let col = self.expect_ident_like()?;
9838 if !matches!(self.peek(), Token::Eq) {
9839 return Err(self.err(format!(
9840 "expected '=' in MERGE UPDATE assignment, got {:?}",
9841 self.peek()
9842 )));
9843 }
9844 self.advance();
9845 let expr = self.parse_expr(0)?;
9846 assignments.push((col, expr));
9847 if matches!(self.peek(), Token::Comma) {
9848 self.advance();
9849 continue;
9850 }
9851 break;
9852 }
9853 crate::ast::MergeAction::Update { assignments }
9854 }
9855 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
9856 self.advance();
9857 crate::ast::MergeAction::Delete
9858 }
9859 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
9860 self.advance();
9861 let is_nothing_kw = matches!(
9862 self.peek(),
9863 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
9864 );
9865 if !is_nothing_kw {
9866 return Err(self.err(format!(
9867 "expected NOTHING after DO in MERGE clause, got {:?}",
9868 self.peek()
9869 )));
9870 }
9871 self.advance();
9872 crate::ast::MergeAction::DoNothing
9873 }
9874 other => {
9875 return Err(self.err(format!(
9876 "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
9877 )));
9878 }
9879 };
9880 // PG's grammar simply has no INSERT production under BY SOURCE
9881 // (a target row already exists there) — same syntax error.
9882 if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
9883 && matches!(action, crate::ast::MergeAction::Insert { .. })
9884 {
9885 return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
9886 }
9887 clauses.push(crate::ast::MergeWhenClause {
9888 matched,
9889 condition,
9890 action,
9891 });
9892 }
9893 if clauses.is_empty() {
9894 return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
9895 }
9896 // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
9897 // unconditional (no `AND`) WHEN of the same match kind: it could
9898 // never fire. Check per match kind in clause order.
9899 let mut seen_unconditional_matched = false;
9900 let mut seen_unconditional_not_matched = false;
9901 let mut seen_unconditional_by_source = false;
9902 for c in &clauses {
9903 let seen = match c.matched {
9904 crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
9905 crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
9906 crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
9907 };
9908 if *seen {
9909 return Err(self.err(String::from(
9910 "unreachable WHEN clause specified after unconditional WHEN clause",
9911 )));
9912 }
9913 if c.condition.is_none() {
9914 *seen = true;
9915 }
9916 }
9917 // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
9918 let returning = self.parse_optional_returning()?;
9919 Ok(Statement::Merge(crate::ast::MergeStatement {
9920 // Attached by `parse_with_cte_then_select` when the MERGE
9921 // heads a WITH clause (round 149).
9922 ctes: Vec::new(),
9923 target,
9924 target_alias,
9925 source,
9926 source_alias,
9927 source_select,
9928 source_column_aliases,
9929 on,
9930 clauses,
9931 returning,
9932 }))
9933 }
9934
9935 /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
9936 /// clause on INSERT / UPDATE / DELETE. Same projection grammar
9937 /// as SELECT, so `RETURNING *`, `RETURNING col`,
9938 /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
9939 fn parse_optional_returning(
9940 &mut self,
9941 ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
9942 let is_returning_kw = matches!(
9943 self.peek(),
9944 Token::Ident(s) if s.eq_ignore_ascii_case("returning")
9945 );
9946 if !is_returning_kw {
9947 return Ok(None);
9948 }
9949 self.advance();
9950 let mut items = Vec::new();
9951 loop {
9952 items.push(self.parse_select_item()?);
9953 if matches!(self.peek(), Token::Comma) {
9954 self.advance();
9955 continue;
9956 }
9957 break;
9958 }
9959 Ok(Some(items))
9960 }
9961
9962 /// v6.0.4 — parse the tail of an ALTER statement after the
9963 /// leading `ALTER` keyword has been consumed. Only one form is
9964 /// supported in v6.0.4:
9965 ///
9966 /// ```text
9967 /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
9968 /// ```
9969 fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
9970 // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
9971 // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
9972 // exclusion) is accepted by stripping the `ONLY` keyword
9973 // before the table parse.
9974 // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
9975 // and the long PG-dump tail are accepted as no-ops.
9976 match self.advance() {
9977 Token::Index => {}
9978 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
9979 // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
9980 // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
9981 Token::Table => {
9982 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
9983 self.advance();
9984 }
9985 return self.parse_alter_table_after_keyword();
9986 }
9987 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
9988 return self.parse_alter_policy_after_keyword();
9989 }
9990 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
9991 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
9992 self.advance();
9993 }
9994 return self.parse_alter_table_after_keyword();
9995 }
9996 // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
9997 // of the silent-noop tail.
9998 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
9999 return self.parse_alter_sequence_after_keyword();
10000 }
10001 // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10002 // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10003 // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10004 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10005 // NB: the match arm consumed `TYPE` via self.advance(); the
10006 // cursor is now at the type name — do NOT advance again.
10007 let type_name = self.expect_ident_like()?;
10008 let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10009 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10010 if is_add_value {
10011 self.advance(); // ADD
10012 self.advance(); // VALUE
10013 // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10014 // IF/EXISTS as identifiers.
10015 let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10016 {
10017 let n1 = self.tokens.get(self.pos + 1);
10018 let n2 = self.tokens.get(self.pos + 2);
10019 if matches!(n1, Some(Token::Not))
10020 && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10021 {
10022 self.advance();
10023 self.advance();
10024 self.advance();
10025 true
10026 } else {
10027 false
10028 }
10029 } else {
10030 false
10031 };
10032 let label = self.expect_string_literal()?;
10033 let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10034 {
10035 let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10036 self.advance();
10037 let anchor = self.expect_string_literal()?;
10038 Some((is_before, anchor))
10039 } else {
10040 None
10041 };
10042 return Ok(Statement::AlterTypeAddValue {
10043 type_name,
10044 label,
10045 if_not_exists,
10046 position,
10047 });
10048 }
10049 // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10050 // Used to fall into the no-op tail below: accepted, silently
10051 // ignored. `RENAME TO <newtype>` keeps falling through.
10052 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10053 && matches!(
10054 self.tokens.get(self.pos + 1),
10055 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10056 )
10057 {
10058 self.advance(); // RENAME
10059 self.advance(); // VALUE
10060 let old = self.expect_string_literal()?;
10061 if matches!(self.peek(), Token::To) {
10062 self.advance();
10063 } else {
10064 self.expect_keyword_ident("to")?;
10065 }
10066 let new = self.expect_string_literal()?;
10067 return Ok(Statement::AlterTypeRenameValue {
10068 type_name,
10069 old,
10070 new,
10071 });
10072 }
10073 // Other ALTER TYPE forms — the ACTION stays a no-op
10074 // (pg_dump tail), but v7.39 (round 708) the NAME is
10075 // validated: `ALTER TYPE nosuch RENAME TO x` reported
10076 // success for a type that does not exist.
10077 self.consume_until_statement_boundary();
10078 return Ok(Statement::ValidateOnly {
10079 kind: crate::ast::ValidateOnlyKind::TypeName,
10080 names: alloc::vec![type_name],
10081 });
10082 }
10083 // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10084 // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10085 // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10086 // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10087 // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10088 // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10089 // pg_dump no-op list below: every form used to report success
10090 // and change nothing, which is worse than refusing outright
10091 // (a migration dropping a constraint kept rejecting data).
10092 // NOTE: the enclosing `match self.advance()` already consumed
10093 // the DOMAIN keyword, so the name is next.
10094 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10095 return self.parse_alter_domain_after_keyword();
10096 }
10097 // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10098 // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10099 // used to fall into the pg_dump no-op tail below, so a DBA
10100 // setting a per-role default was told it worked and nothing
10101 // happened. Intercepted here, BEFORE that tail.
10102 // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10103 // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10104 // interception below exists: swallowed with the no-op tail, an
10105 // unknown parameter name was ACCEPTED where PG18 answers
10106 // `unrecognized configuration parameter`. SPG applies nothing
10107 // either way — there is no postgresql.auto.conf — but it now
10108 // says so about a name it does not know.
10109 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10110 // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10111 // already consumed here. An extra advance eats the SET and
10112 // the parameter name is never seen — which is exactly the
10113 // bug a panic in this branch disproved: the branch WAS on
10114 // the path, the reading of it was wrong.
10115 let mut parameter = None;
10116 // SET <name> … | RESET <name> | RESET ALL
10117 if matches!(self.peek(), Token::Ident(k)
10118 if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10119 {
10120 self.advance();
10121 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10122 && !n.eq_ignore_ascii_case("all")
10123 {
10124 self.advance();
10125 // A dotted GUC (`plpgsql.check_asserts`) is two
10126 // tokens; keep the whole name.
10127 let mut full = n;
10128 while matches!(self.peek(), Token::Dot) {
10129 self.advance();
10130 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10131 full.push('.');
10132 full.push_str(&t);
10133 }
10134 }
10135 parameter = Some(full);
10136 }
10137 }
10138 self.consume_until_statement_boundary();
10139 return Ok(Statement::AlterSystem { parameter });
10140 }
10141 Token::Ident(s) | Token::QuotedIdent(s)
10142 if matches!(
10143 s.to_ascii_lowercase().as_str(),
10144 "role" | "user" | "database"
10145 ) && self.peeks_db_role_setting() =>
10146 {
10147 let is_database = s.eq_ignore_ascii_case("database");
10148 return self.parse_db_role_setting(is_database);
10149 }
10150 // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10151 // (the non-SET forms; SET/RESET took the branch above). The
10152 // attributes still no-op — recorded, and the ignored PASSWORD
10153 // is ledgered as its own follow-up — but the ROLE is validated:
10154 // any name was accepted for a role that does not exist.
10155 Token::Ident(s) | Token::QuotedIdent(s)
10156 if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10157 {
10158 // NB: the enclosing `match self.advance()` already consumed
10159 // ROLE/USER — the round-695 trap, hit again in this round's
10160 // first draft (the name was eaten and WITH parsed as the
10161 // role). The cursor is at the name.
10162 let name = self.expect_ident_or_string()?;
10163 // v7.39 (round 750) — scan the attribute tail for
10164 // PASSWORD. Everything else stays a recorded no-op, but
10165 // a dropped credential rotation is a SECURITY bug:
10166 // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10167 // changed nothing, so the old password kept working.
10168 // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10169 // NULL` clears the credential.
10170 let mut password: Option<Option<String>> = None;
10171 loop {
10172 match self.peek() {
10173 Token::Semicolon | Token::Eof => break,
10174 Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10175 self.advance();
10176 match self.advance() {
10177 Token::String(p) => password = Some(Some(p)),
10178 Token::Null => password = Some(None),
10179 Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10180 password = Some(None);
10181 }
10182 other => {
10183 return Err(self.err(alloc::format!(
10184 "expected password string or NULL after PASSWORD, got {other:?}"
10185 )));
10186 }
10187 }
10188 }
10189 _ => {
10190 self.advance();
10191 }
10192 }
10193 }
10194 if name.eq_ignore_ascii_case("all") {
10195 // `ALTER ROLE ALL …` names every role; nothing to check.
10196 return Ok(Statement::Empty);
10197 }
10198 if let Some(pw) = password {
10199 return Ok(Statement::AlterRolePassword { name, password: pw });
10200 }
10201 return Ok(Statement::ValidateOnly {
10202 kind: crate::ast::ValidateOnlyKind::RoleName,
10203 names: alloc::vec![name],
10204 });
10205 }
10206 // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10207 // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10208 // list far enough to validate the NAME; the actions still no-op.
10209 // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10210 // models none of them and their dumps are rare.)
10211 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10212 let name = self.expect_ident_or_string()?;
10213 self.consume_until_statement_boundary();
10214 return Ok(Statement::ValidateOnly {
10215 kind: crate::ast::ValidateOnlyKind::CollationName,
10216 names: alloc::vec![name],
10217 });
10218 }
10219 Token::Ident(s) | Token::QuotedIdent(s)
10220 if s.eq_ignore_ascii_case("text")
10221 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10222 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10223 {
10224 self.advance(); // SEARCH
10225 self.advance(); // CONFIGURATION
10226 let name = self.expect_ident_like()?;
10227 self.consume_until_statement_boundary();
10228 return Ok(Statement::ValidateOnly {
10229 kind: crate::ast::ValidateOnlyKind::TsConfigName,
10230 names: alloc::vec![name],
10231 });
10232 }
10233 Token::Ident(s) | Token::QuotedIdent(s)
10234 if s.eq_ignore_ascii_case("event")
10235 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10236 {
10237 self.advance(); // TRIGGER
10238 let name = self.expect_ident_like()?;
10239 self.consume_until_statement_boundary();
10240 return Ok(Statement::ValidateOnly {
10241 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10242 names: alloc::vec![name],
10243 });
10244 }
10245 Token::Ident(s) | Token::QuotedIdent(s)
10246 if s.eq_ignore_ascii_case("large")
10247 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10248 {
10249 self.advance(); // OBJECT
10250 let oid = match self.advance() {
10251 Token::Integer(n) => alloc::format!("{n}"),
10252 other => {
10253 return Err(
10254 self.err(alloc::format!("expected large object oid, got {other:?}"))
10255 );
10256 }
10257 };
10258 self.consume_until_statement_boundary();
10259 return Ok(Statement::ValidateOnly {
10260 kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10261 names: alloc::vec![oid],
10262 });
10263 }
10264 // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10265 // argument-list parse as DROP AGGREGATE (round 707); the
10266 // action no-ops, the existence check is real.
10267 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10268 // Same round-695 trap as above: AGGREGATE is already
10269 // consumed; the cursor is at the name.
10270 let name = self.expect_ident_like()?;
10271 let mut names = alloc::vec![name];
10272 if matches!(self.peek(), Token::LParen) {
10273 self.advance();
10274 loop {
10275 match self.peek().clone() {
10276 Token::RParen => {
10277 self.advance();
10278 break;
10279 }
10280 Token::Star => {
10281 self.advance();
10282 names.push(String::from("*"));
10283 }
10284 Token::Comma => {
10285 self.advance();
10286 }
10287 _ => {
10288 let mut t = self.expect_ident_like()?;
10289 while let Token::Ident(nx) = self.peek() {
10290 let nx = nx.clone();
10291 self.advance();
10292 t.push(' ');
10293 t.push_str(&nx);
10294 }
10295 names.push(t);
10296 }
10297 }
10298 }
10299 }
10300 self.consume_until_statement_boundary();
10301 return Ok(Statement::ValidateOnly {
10302 kind: crate::ast::ValidateOnlyKind::AggregateName,
10303 names,
10304 });
10305 }
10306 Token::Ident(s) | Token::QuotedIdent(s)
10307 if matches!(
10308 s.to_ascii_lowercase().as_str(),
10309 "view"
10310 | "function"
10311 | "database"
10312 | "schema"
10313 | "owner"
10314 | "default"
10315 | "extension"
10316 | "materialized"
10317 | "publication"
10318 | "subscription"
10319 // v7.37.17 (17.6 siblings) — additional ALTER
10320 // targets pg_dump / pg_dumpall / operator DB
10321 // migration scripts commonly emit. SPG has
10322 // no matching machinery for any of these; the
10323 // parser accepts + Empty-returns so pg_dump
10324 // tail statements don't stall.
10325 | "tablespace"
10326 | "language"
10327 | "operator"
10328 | "conversion"
10329 | "statistics"
10330 | "server"
10331 | "foreign"
10332 // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10333 // / TEMPLATE (CONFIGURATION intercepted above).
10334 | "text"
10335 ) =>
10336 {
10337 self.consume_until_statement_boundary();
10338 return Ok(Statement::Empty);
10339 }
10340 other => {
10341 return Err(self.err(format!(
10342 "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10343 after ALTER, got {other:?}"
10344 )));
10345 }
10346 }
10347 // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10348 // (mailrs migrate-042 ships these). The presence of an
10349 // IF EXISTS makes the subsequent name lookup tolerate
10350 // a missing index — engine returns CommandOk no-op.
10351 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10352 let next = self.tokens.get(self.pos + 1);
10353 if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10354 self.advance();
10355 self.advance();
10356 true
10357 } else {
10358 false
10359 }
10360 } else {
10361 false
10362 };
10363 let name = self.expect_ident_like()?;
10364 // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10365 // Detect BEFORE the REBUILD path so the existing REBUILD
10366 // arm stays untouched.
10367 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10368 self.advance();
10369 if matches!(self.peek(), Token::To) {
10370 self.advance();
10371 } else {
10372 self.expect_keyword_ident("to")?;
10373 }
10374 let new = self.expect_ident_like()?;
10375 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10376 name,
10377 target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10378 }));
10379 }
10380 // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10381 // A syntax error before; the index is validated, the params no-op.
10382 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10383 || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10384 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10385 {
10386 self.consume_until_statement_boundary();
10387 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10388 name,
10389 target: crate::ast::AlterIndexTarget::StorageParams,
10390 }));
10391 }
10392 // REBUILD
10393 self.expect_keyword_ident("rebuild")?;
10394 // Optional: WITH (encoding = <enc>)
10395 let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10396 self.advance();
10397 if !matches!(self.peek(), Token::LParen) {
10398 return Err(self.err(format!(
10399 "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10400 self.peek()
10401 )));
10402 }
10403 self.advance();
10404 self.expect_keyword_ident("encoding")?;
10405 if !matches!(self.peek(), Token::Eq) {
10406 return Err(self.err(format!(
10407 "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10408 self.peek()
10409 )));
10410 }
10411 self.advance();
10412 let enc_ident = match self.advance() {
10413 Token::Ident(s) | Token::QuotedIdent(s) => s,
10414 other => {
10415 return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10416 }
10417 };
10418 let enc = match enc_ident.to_ascii_lowercase().as_str() {
10419 "f32" => VecEncoding::F32,
10420 "sq8" => VecEncoding::Sq8,
10421 "half" => VecEncoding::F16,
10422 other => {
10423 return Err(self.err(format!(
10424 "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10425 )));
10426 }
10427 };
10428 if !matches!(self.peek(), Token::RParen) {
10429 return Err(self.err(format!(
10430 "expected ')' after encoding value, got {:?}",
10431 self.peek()
10432 )));
10433 }
10434 self.advance();
10435 Some(enc)
10436 } else {
10437 None
10438 };
10439 Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10440 name,
10441 target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10442 }))
10443 }
10444
10445 /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10446 /// only `SET` form currently supported; future v6.7.x can add
10447 /// more SET subjects without changing the dispatch shape.
10448 /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10449 /// subactions. Single-subaction shape stays a 1-element vec.
10450 fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10451 let table_name = self.expect_ident_like()?;
10452 let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10453 loop {
10454 let subaction = self.parse_alter_table_subaction()?;
10455 // ADD COLUMN with inline REFERENCES emits both an
10456 // AddColumn and an AddForeignKey subaction; the
10457 // helper returns 1 or 2 items.
10458 targets.extend(subaction);
10459 if matches!(self.peek(), Token::Comma) {
10460 self.advance();
10461 continue;
10462 }
10463 break;
10464 }
10465 Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10466 name: table_name,
10467 targets,
10468 }))
10469 }
10470
10471 /// Parse one ALTER TABLE subaction. Returns a Vec because
10472 /// inline `REFERENCES` on `ADD COLUMN` produces both an
10473 /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10474 fn parse_alter_table_subaction(
10475 &mut self,
10476 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10477 match self.peek() {
10478 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10479 self.advance();
10480 // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10481 // storage parameters: paren-prefixed; consume.
10482 if matches!(self.peek(), Token::LParen) {
10483 self.consume_until_statement_boundary();
10484 return Ok(Vec::new());
10485 }
10486 let setting = self.expect_ident_like()?;
10487 if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10488 if !matches!(self.peek(), Token::Eq) {
10489 return Err(self.err(alloc::format!(
10490 "expected '=' after hot_tier_bytes, got {:?}",
10491 self.peek()
10492 )));
10493 }
10494 self.advance();
10495 let n = self.expect_u64_literal()?;
10496 return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10497 }
10498 // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10499 // accept-and-no-op for ALTER TABLE SET <subject>
10500 // forms that pg_dump emits but SPG either treats
10501 // as N/A (single-tenant, single-owner, no shared
10502 // tablespaces) or accepts the dump-side declaration
10503 // without runtime effect:
10504 // SET SCHEMA <name> (18.11)
10505 // SET TABLESPACE <name> (18.8)
10506 // SET LOGGED / UNLOGGED (18.7 alt-form)
10507 // SET WITHOUT CLUSTER (18.13)
10508 // SET WITHOUT OIDS (PG legacy)
10509 // SET (option = value, …) (storage parameters)
10510 // SET REPLICA IDENTITY {…} (18.14)
10511 if setting.eq_ignore_ascii_case("schema")
10512 || setting.eq_ignore_ascii_case("tablespace")
10513 || setting.eq_ignore_ascii_case("logged")
10514 || setting.eq_ignore_ascii_case("unlogged")
10515 || setting.eq_ignore_ascii_case("without")
10516 {
10517 self.consume_until_statement_boundary();
10518 return Ok(Vec::new());
10519 }
10520 if setting.eq_ignore_ascii_case("replica") {
10521 // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10522 self.consume_until_statement_boundary();
10523 return Ok(Vec::new());
10524 }
10525 // SET (option=value, …) — storage parameters.
10526 if matches!(self.peek(), Token::LParen) {
10527 self.consume_until_statement_boundary();
10528 return Ok(Vec::new());
10529 }
10530 Err(self.err(alloc::format!(
10531 "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10532 hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10533 WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10534 )))
10535 }
10536 // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10537 // not ignored: round 645 gave SPG the inheritance the
10538 // v7.37.18 no-op said it did not have.
10539 Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10540 self.advance();
10541 let parent = self.expect_ident_like()?;
10542 self.consume_until_statement_boundary();
10543 Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10544 parent,
10545 detach: false
10546 }])
10547 }
10548 // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10549 // LEVEL SECURITY`, which has its own RLS arm below — without
10550 // the guard this swallowed NO FORCE as a no-op.
10551 Token::Ident(s)
10552 if s.eq_ignore_ascii_case("no")
10553 && !matches!(
10554 self.tokens.get(self.pos + 1),
10555 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10556 ) =>
10557 {
10558 self.advance();
10559 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
10560 if k.eq_ignore_ascii_case("inherit"))
10561 {
10562 self.advance();
10563 let parent = self.expect_ident_like()?;
10564 self.consume_until_statement_boundary();
10565 return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10566 parent,
10567 detach: true
10568 }]);
10569 }
10570 self.consume_until_statement_boundary();
10571 Ok(Vec::new())
10572 }
10573 // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
10574 // single-owner, so there is still nothing to record.
10575 //
10576 // v7.39 (round 652) — but the name now reaches the engine,
10577 // which refuses a role that does not exist as PG does. The
10578 // no-op was swallowing the whole statement, so a dump naming
10579 // a role this server never heard of restored clean and left
10580 // the table owned by whoever ran the restore.
10581 Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
10582 self.advance();
10583 if matches!(self.peek(), Token::To) {
10584 self.advance();
10585 }
10586 let role = self.expect_ident_like()?;
10587 Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
10588 role
10589 }])
10590 }
10591 // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
10592 // PG sets a hint; SPG doesn't have clustered storage, so the
10593 // hint itself stays a no-op.
10594 //
10595 // v7.39 (round 652) — the index name is checked now. PG
10596 // errors on one that does not exist, and swallowing that let
10597 // a typo'd CLUSTER ON pass silently.
10598 Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
10599 self.advance();
10600 // `ON` is a reserved token, not an ident.
10601 if !matches!(self.peek(), Token::On) {
10602 return Err(self.err(alloc::format!(
10603 "expected ON after CLUSTER, got {:?}",
10604 self.peek()
10605 )));
10606 }
10607 self.advance();
10608 let index = self.expect_ident_like()?;
10609 Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
10610 index: Some(index)
10611 }])
10612 }
10613 // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
10614 // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
10615 // what a logical decoder puts in the old-tuple image; SPG's
10616 // replication is SQL-text, so there is nothing to record.
10617 // Accept-and-no-op (it used to be a parse error).
10618 Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
10619 self.advance();
10620 // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
10621 // validates the index; DEFAULT / FULL / NOTHING stay no-op.
10622 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
10623 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
10624 {
10625 self.advance(); // IDENTITY
10626 self.advance(); // USING
10627 if matches!(self.peek(), Token::Index)
10628 || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
10629 {
10630 self.advance();
10631 }
10632 let index = self.expect_ident_like()?;
10633 self.consume_until_statement_boundary();
10634 return Ok(alloc::vec![
10635 crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
10636 ]);
10637 }
10638 self.consume_until_statement_boundary();
10639 Ok(Vec::new())
10640 }
10641 // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
10642 //
10643 // v7.39 (round 652) — it used to consume the statement and
10644 // return nothing, on the stated theory that SPG validated at
10645 // ADD CONSTRAINT time so there was never anything left to
10646 // validate. Measured against PG18, ADD CONSTRAINT did not
10647 // scan the existing rows at all — the comment described a
10648 // property SPG did not have, which is why nobody looked. Both
10649 // halves are real now: ADD scans unless told NOT VALID, and
10650 // this scans what NOT VALID skipped.
10651 Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
10652 self.advance();
10653 self.expect_keyword_ident("constraint")?;
10654 let name = self.expect_ident_like()?;
10655 Ok(alloc::vec![
10656 crate::ast::AlterTableTarget::ValidateConstraint { name }
10657 ])
10658 }
10659 // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
10660 // SET (option = value, …). PG uses it to clear per-table
10661 // storage params like fillfactor or autovacuum_*. SPG
10662 // engine-manages those parameters; accept-and-no-op.
10663 Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
10664 self.advance();
10665 self.consume_until_statement_boundary();
10666 Ok(Vec::new())
10667 }
10668 // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
10669 // type-of binding (PG 9.0+). SPG composite types
10670 // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
10671 // TABLE OF is rare and inverse of CREATE TABLE OF.
10672 // Accept-and-no-op until a customer dump round-trips it.
10673 Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
10674 self.advance();
10675 // v7.39 (round 710) — the type name is validated now.
10676 let type_name = self.expect_ident_like()?;
10677 self.consume_until_statement_boundary();
10678 Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
10679 type_name
10680 }])
10681 }
10682 // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
10683 // (reserved keyword) rather than Token::Ident("not"),
10684 // so it needs its own arm. Accept-and-no-op same as OF.
10685 Token::Not => {
10686 self.advance();
10687 self.consume_until_statement_boundary();
10688 Ok(Vec::new())
10689 }
10690 // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
10691 Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
10692 self.advance();
10693 self.expect_row_level_security()?;
10694 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10695 enabled: None,
10696 force: Some(true),
10697 }])
10698 }
10699 // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
10700 Token::Ident(s)
10701 if s.eq_ignore_ascii_case("no")
10702 && matches!(
10703 self.tokens.get(self.pos + 1),
10704 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10705 ) =>
10706 {
10707 self.advance(); // NO
10708 self.advance(); // FORCE
10709 self.expect_row_level_security()?;
10710 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10711 enabled: None,
10712 force: Some(false),
10713 }])
10714 }
10715 // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
10716 // (sets relrowsecurity). The guard requires the next token to be
10717 // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
10718 Token::Ident(s)
10719 if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
10720 && matches!(
10721 self.tokens.get(self.pos + 1),
10722 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
10723 ) =>
10724 {
10725 let enabled = s.eq_ignore_ascii_case("enable");
10726 self.advance(); // ENABLE/DISABLE
10727 self.expect_row_level_security()?;
10728 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10729 enabled: Some(enabled),
10730 force: None,
10731 }])
10732 }
10733 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
10734 self.advance();
10735 // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
10736 // {INDEX|KEY} [name] (cols)`, which every ORM migration
10737 // emits. The same grammar CREATE TABLE already accepts
10738 // inline (`KEY idx (a)`, prefix lengths and all), so it goes
10739 // through the SAME parser — an ALTER-only copy would be a
10740 // second place for the two to drift.
10741 if self.peek_mysql_inline_key_start() {
10742 return Ok(match self.parse_mysql_inline_key()? {
10743 Some(c) => {
10744 alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
10745 }
10746 // FULLTEXT / SPATIAL parse and are accepted as a
10747 // no-op here exactly as they are inline.
10748 None => Vec::new(),
10749 });
10750 }
10751 // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
10752 // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
10753 // PRIMARY KEY this way; mysqldump emits both.
10754 // Peek-only dispatch (no advance) — `advance()`
10755 // destructively replaces consumed tokens with Eof,
10756 // so saved-pos restore would land on Eofs.
10757 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
10758 {
10759 // The next-but-one ident is the constraint
10760 // name; the one after THAT is the kind.
10761 let kind_pos = self.pos + 2;
10762 let kind = self.tokens.get(kind_pos).cloned();
10763 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
10764 {
10765 let fk = self.parse_table_level_fk()?;
10766 return Ok(alloc::vec![
10767 crate::ast::AlterTableTarget::AddForeignKey(fk)
10768 ]);
10769 }
10770 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
10771 {
10772 self.advance(); // CONSTRAINT
10773 // v7.39 (read01 round 48) — keep the name; the engine
10774 // stores it now instead of dropping it on the floor.
10775 let con_name = self.expect_ident_like()?;
10776 self.advance(); // PRIMARY
10777 self.expect_keyword_ident("key")?;
10778 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10779 // v7.39 (round 711) — the ALTER form carries the
10780 // timing too (pg_dump writes it here).
10781 let (deferrable, initially_deferred) =
10782 self.consume_deferrable_clauses_timed()?;
10783 return Ok(alloc::vec![
10784 crate::ast::AlterTableTarget::AddTableConstraint(
10785 crate::ast::TableConstraint::PrimaryKey {
10786 name: Some(con_name),
10787 columns: cols,
10788 deferrable,
10789 initially_deferred,
10790 }
10791 )
10792 ]);
10793 }
10794 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
10795 {
10796 self.advance(); // CONSTRAINT
10797 // v7.39 (read01 round 48) — keep the name.
10798 let con_name = self.expect_ident_like()?;
10799 // v7.22 (mailrs round-13 gap 6) — delegate so
10800 // the optional `NULLS [NOT] DISTINCT` modifier
10801 // parses here too (pg_dump emits the ALTER
10802 // form; semantics enforced by the engine
10803 // since v7.13).
10804 let mut uc = self.parse_table_level_unique()?;
10805 if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
10806 *name = Some(con_name);
10807 }
10808 return Ok(alloc::vec![
10809 crate::ast::AlterTableTarget::AddTableConstraint(uc)
10810 ]);
10811 }
10812 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
10813 {
10814 self.advance(); // CONSTRAINT
10815 // v7.39 (read01 round 48) — keep the name.
10816 let con_name = self.expect_ident_like()?;
10817 self.advance(); // CHECK
10818 if !matches!(self.peek(), Token::LParen) {
10819 return Err(self.err(alloc::format!(
10820 "expected '(' after CHECK, got {:?}", self.peek()
10821 )));
10822 }
10823 self.advance();
10824 let expr = self.parse_expr(0)?;
10825 if matches!(self.peek(), Token::RParen) {
10826 self.advance();
10827 }
10828 let not_valid = self.parse_not_valid_suffix();
10829 return Ok(alloc::vec![
10830 crate::ast::AlterTableTarget::AddTableConstraint(
10831 crate::ast::TableConstraint::Check {
10832 name: Some(con_name),
10833 expr,
10834 not_valid,
10835 }
10836 )
10837 ]);
10838 }
10839 // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
10840 // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
10841 // exclusion constraints via this ALTER form.
10842 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
10843 {
10844 self.advance(); // CONSTRAINT
10845 let con_name = self.expect_ident_like()?;
10846 let mut ex = self.parse_table_level_exclude()?;
10847 if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
10848 *name = Some(con_name);
10849 }
10850 return Ok(alloc::vec![
10851 crate::ast::AlterTableTarget::AddTableConstraint(ex)
10852 ]);
10853 }
10854 // Unknown kind — fall through to FK path which
10855 // produces a descriptive parse error.
10856 }
10857 let is_fk = matches!(
10858 self.peek(),
10859 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
10860 || s.eq_ignore_ascii_case("foreign")
10861 );
10862 if is_fk {
10863 let fk = self.parse_table_level_fk()?;
10864 return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
10865 }
10866 // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
10867 // (no CONSTRAINT prefix) — same dispatch.
10868 match self.peek().clone() {
10869 Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
10870 self.advance();
10871 self.expect_keyword_ident("key")?;
10872 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10873 let (deferrable, initially_deferred) =
10874 self.consume_deferrable_clauses_timed()?;
10875 return Ok(alloc::vec![
10876 crate::ast::AlterTableTarget::AddTableConstraint(
10877 crate::ast::TableConstraint::PrimaryKey {
10878 name: None,
10879 columns: cols,
10880 deferrable,
10881 initially_deferred,
10882 }
10883 )
10884 ]);
10885 }
10886 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
10887 // v7.22 — delegate (NULLS [NOT] DISTINCT).
10888 let uc = self.parse_table_level_unique()?;
10889 return Ok(alloc::vec![
10890 crate::ast::AlterTableTarget::AddTableConstraint(uc)
10891 ]);
10892 }
10893 // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
10894 // prefix). The other three bare forms were here and
10895 // this one was not, so it fell through to the column
10896 // path and came back as "unexpected reserved keyword
10897 // 'check' at start of column definition".
10898 _ if self.peek_table_level_check_start() => {
10899 let chk = self.parse_table_level_check()?;
10900 let not_valid = self.parse_not_valid_suffix();
10901 let crate::ast::TableConstraint::Check { expr, .. } = chk else {
10902 unreachable!("parse_table_level_check returns Check")
10903 };
10904 return Ok(alloc::vec![
10905 crate::ast::AlterTableTarget::AddTableConstraint(
10906 crate::ast::TableConstraint::Check {
10907 name: None,
10908 expr,
10909 not_valid,
10910 }
10911 )
10912 ]);
10913 }
10914 // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
10915 Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
10916 let ex = self.parse_table_level_exclude()?;
10917 return Ok(alloc::vec![
10918 crate::ast::AlterTableTarget::AddTableConstraint(ex)
10919 ]);
10920 }
10921 _ => {}
10922 }
10923 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
10924 self.advance();
10925 }
10926 let mut if_not_exists = false;
10927 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10928 self.advance();
10929 if !matches!(self.peek(), Token::Not) {
10930 return Err(self.err(alloc::format!(
10931 "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
10932 self.peek()
10933 )));
10934 }
10935 self.advance();
10936 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
10937 return Err(self.err(alloc::format!(
10938 "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
10939 self.peek()
10940 )));
10941 }
10942 self.advance();
10943 if_not_exists = true;
10944 }
10945 // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
10946 // REFERENCES other(col) [ON DELETE …]`. parse_column_def
10947 // returns ColumnDef + an optional inline FK.
10948 let (column, col_level_fk) = self.parse_column_def_with_fk()?;
10949 let col_name = column.name.clone();
10950 let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
10951 column,
10952 if_not_exists,
10953 }];
10954 if let Some(mut fk) = col_level_fk {
10955 if fk.columns.is_empty() {
10956 fk.columns.push(col_name);
10957 }
10958 out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
10959 }
10960 Ok(out)
10961 }
10962 Token::Drop => {
10963 self.advance();
10964 // v7.13.3 — dispatch on the next token. mailrs round-7
10965 // S8 closed DROP COLUMN; round-6 S7 closed
10966 // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
10967 // RESTRICT modifiers.
10968 // DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
10969 // DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
10970 let subject = match self.peek() {
10971 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
10972 self.advance();
10973 "constraint"
10974 }
10975 Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
10976 self.advance();
10977 "column"
10978 }
10979 // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
10980 // `INDEX` lexes as the reserved Token::Index, so it is
10981 // unambiguous. `KEY` is a plain ident, and PG allows a
10982 // column literally named "key", so only read it as the
10983 // keyword when a name follows it.
10984 Token::Index => {
10985 self.advance();
10986 "index"
10987 }
10988 Token::Ident(s)
10989 if s.eq_ignore_ascii_case("key")
10990 && matches!(
10991 self.tokens.get(self.pos + 1),
10992 Some(Token::Ident(_) | Token::QuotedIdent(_))
10993 ) =>
10994 {
10995 self.advance();
10996 "index"
10997 }
10998 // PG-canonical bare `DROP <col>` without COLUMN
10999 // keyword is also valid; treat any other ident
11000 // as the column name.
11001 Token::Ident(_) | Token::QuotedIdent(_) => "column",
11002 other => {
11003 return Err(self.err(alloc::format!(
11004 "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11005 )));
11006 }
11007 };
11008 let mut if_exists = false;
11009 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11010 let n1 = self.tokens.get(self.pos + 1);
11011 if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11012 self.advance();
11013 self.advance();
11014 if_exists = true;
11015 }
11016 }
11017 let name = self.expect_ident_like()?;
11018 let mut cascade = false;
11019 if matches!(
11020 self.peek(),
11021 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11022 || s.eq_ignore_ascii_case("restrict")
11023 ) {
11024 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11025 {
11026 cascade = true;
11027 }
11028 self.advance();
11029 }
11030 if subject == "index" {
11031 Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11032 name,
11033 if_exists,
11034 }])
11035 } else if subject == "constraint" {
11036 Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11037 name,
11038 if_exists,
11039 }])
11040 } else {
11041 Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11042 column: name,
11043 if_exists,
11044 cascade,
11045 }])
11046 }
11047 }
11048 Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11049 self.advance();
11050 // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11051 // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11052 // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11053 // immediately; accept-and-no-op.
11054 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11055 self.advance();
11056 self.consume_until_statement_boundary();
11057 return Ok(Vec::new());
11058 }
11059 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11060 self.advance();
11061 }
11062 let col_name = self.expect_ident_like()?;
11063 match self.peek() {
11064 Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11065 self.advance();
11066 }
11067 // v7.14.0 — pg_dump emits BIGSERIAL via
11068 // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11069 // nextval('seq')` (the sequence is created
11070 // separately). SPG's BIGSERIAL already uses
11071 // AUTO_INCREMENT; accept SET DEFAULT / DROP
11072 // DEFAULT / SET NOT NULL / DROP NOT NULL as
11073 // engine no-ops by consuming the tail.
11074 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11075 // v7.22 (round-13 T2) — `SET DEFAULT
11076 // nextval('…')` is how pg_dump spells a
11077 // SERIAL column (plain integer in CREATE
11078 // TABLE + this ALTER). It used to be
11079 // swallowed as a no-op, which silently
11080 // STRIPPED auto-increment from imported
11081 // schemas — the first post-import INSERT
11082 // without an explicit id then violated NOT
11083 // NULL. Lower it to the auto-increment
11084 // marker instead.
11085 let is_default_nextval =
11086 matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11087 && matches!(
11088 self.tokens.get(self.pos + 2),
11089 Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11090 );
11091 if is_default_nextval {
11092 let seq_name = self.scan_sequence_name_until_boundary();
11093 return Ok(alloc::vec![
11094 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11095 column: col_name,
11096 seq_name,
11097 }
11098 ]);
11099 }
11100 // v7.37.18 (18.1 + 18.2) — proper lowering.
11101 self.advance(); // consume "set"
11102 match self.peek().clone() {
11103 Token::Default => {
11104 self.advance();
11105 let default_expr = self.parse_expr(0)?;
11106 return Ok(alloc::vec![
11107 crate::ast::AlterTableTarget::AlterColumnSetDefault {
11108 column: col_name,
11109 default_expr,
11110 }
11111 ]);
11112 }
11113 Token::Not => {
11114 self.advance();
11115 if !matches!(self.peek(), Token::Null) {
11116 return Err(self.err(alloc::format!(
11117 "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11118 self.peek()
11119 )));
11120 }
11121 self.advance();
11122 return Ok(alloc::vec![
11123 crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11124 column: col_name,
11125 }
11126 ]);
11127 }
11128 // `SET EXPRESSION AS (expr)` (PG 17) — change a
11129 // stored generated column's expression and
11130 // recompute existing rows.
11131 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11132 self.advance(); // EXPRESSION
11133 if matches!(self.peek(), Token::As) {
11134 self.advance();
11135 }
11136 let expr = self.parse_expr(0)?;
11137 return Ok(alloc::vec![
11138 crate::ast::AlterTableTarget::AlterColumnSetExpression {
11139 column: col_name,
11140 expr,
11141 }
11142 ]);
11143 }
11144 other => {
11145 // Other SET subjects (STATISTICS,
11146 // STORAGE, COMPRESSION, …) stay no-ops —
11147 // storage hints with no SPG semantics.
11148 let _ = other;
11149 self.consume_until_statement_boundary();
11150 return Ok(Vec::new());
11151 }
11152 }
11153 }
11154 Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11155 self.advance(); // consume "drop"
11156 return self.parse_alter_column_drop_tail(col_name);
11157 }
11158 Token::Drop => {
11159 self.advance(); // consume Drop token
11160 return self.parse_alter_column_drop_tail(col_name);
11161 }
11162 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11163 // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11164 // GENERATED { ALWAYS | BY DEFAULT } AS
11165 // IDENTITY ( … )`: pg_dump's spelling for
11166 // identity columns. Same auto-increment
11167 // lowering as the nextval default; the
11168 // sequence options inside the parens are
11169 // no-ops under SPG's max+1 semantics.
11170 let is_generated = matches!(
11171 self.tokens.get(self.pos + 1),
11172 Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11173 );
11174 if !is_generated {
11175 return Err(self.err(alloc::format!(
11176 "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11177 self.tokens.get(self.pos + 1)
11178 )));
11179 }
11180 let seq_name = self.scan_sequence_name_until_boundary();
11181 return Ok(alloc::vec![
11182 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11183 column: col_name,
11184 seq_name,
11185 }
11186 ]);
11187 }
11188 // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11189 // column: floor the next allocated value at n (bare
11190 // RESTART = restart from the start value, 1).
11191 Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11192 self.advance();
11193 let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11194 {
11195 self.advance();
11196 let neg = if matches!(self.peek(), Token::Minus) {
11197 self.advance();
11198 true
11199 } else {
11200 false
11201 };
11202 match self.advance() {
11203 Token::Integer(v) => Some(if neg { -v } else { v }),
11204 other => {
11205 return Err(self.err(alloc::format!(
11206 "expected integer after RESTART WITH, got {other:?}"
11207 )));
11208 }
11209 }
11210 } else {
11211 None
11212 };
11213 return Ok(alloc::vec![
11214 crate::ast::AlterTableTarget::AlterColumnRestart {
11215 column: col_name,
11216 with,
11217 }
11218 ]);
11219 }
11220 other => {
11221 return Err(self.err(alloc::format!(
11222 "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11223 )));
11224 }
11225 }
11226 // v7.39 (round 713) — the type parser has consumed a
11227 // trailing `COLLATE <name>` since Phase 2.5, and
11228 // `parse_column_type_name` discarded it: `ALTER COLUMN t
11229 // TYPE text COLLATE "C"` parsed clean and changed
11230 // nothing. Keep the clause; the engine re-collates.
11231 let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _) =
11232 self.parse_type_with_implied_flags()?;
11233 let collation = if coll_explicit {
11234 coll_name.map(|n| (coll, n))
11235 } else {
11236 None
11237 };
11238 let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11239 {
11240 self.advance();
11241 Some(self.parse_expr(0)?)
11242 } else {
11243 None
11244 };
11245 Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11246 column: col_name,
11247 new_type,
11248 using,
11249 collation,
11250 }])
11251 }
11252 // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11253 // PG also supports `RENAME TO new_table` for table-name
11254 // rename; that surface is deferred (pg_dump never emits
11255 // it). If the first post-RENAME ident is `TO`, the user
11256 // is asking for table rename — error with a clear
11257 // message rather than misparsing `TO` as a column name.
11258 Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11259 self.advance();
11260 // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11261 // table-name rename (mailrs round-10 A.5 — used
11262 // by migrate-042's `RENAME TO email_contacts`).
11263 // `TO` lexes as Token::To.
11264 if matches!(self.peek(), Token::To)
11265 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11266 {
11267 self.advance();
11268 let new = self.expect_ident_like()?;
11269 return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11270 new,
11271 }]);
11272 }
11273 // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11274 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11275 self.advance();
11276 let old = self.expect_ident_like()?;
11277 if matches!(self.peek(), Token::To) {
11278 self.advance();
11279 } else {
11280 self.expect_keyword_ident("to")?;
11281 }
11282 let new = self.expect_ident_like()?;
11283 return Ok(alloc::vec![
11284 crate::ast::AlterTableTarget::RenameConstraint { old, new }
11285 ]);
11286 }
11287 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11288 self.advance();
11289 }
11290 let old = self.expect_ident_like()?;
11291 // `TO` is a reserved keyword token; accept both
11292 // Token::To and Token::Ident("to") for consistency.
11293 if matches!(self.peek(), Token::To) {
11294 self.advance();
11295 } else {
11296 self.expect_keyword_ident("to")?;
11297 }
11298 let new = self.expect_ident_like()?;
11299 Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11300 old,
11301 new,
11302 }])
11303 }
11304 // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11305 // { ALL | <name> }`. pg_dump --disable-triggers wraps
11306 // every data block with these. Real disable semantics —
11307 // not no-op — because reload correctness assumes the
11308 // triggers don't fire (rows already carry their
11309 // computed values from prod).
11310 Token::Ident(s)
11311 if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11312 {
11313 let enabled = s.eq_ignore_ascii_case("enable");
11314 self.advance();
11315 // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11316 // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11317 // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11318 // pg_dump output) — anything else falls through to
11319 // the catch-all error below.
11320 // v7.22 (round-13 T3) — mysqldump wraps every data
11321 // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11322 // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11323 // maintains indexes incrementally — engine no-op.
11324 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11325 self.advance();
11326 return Ok(Vec::new());
11327 }
11328 // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11329 // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11330 // to gate triggers on session_replication_role; SPG
11331 // has no replica role, so the prefix is consumed and
11332 // treated identically to the plain ENABLE/DISABLE
11333 // TRIGGER form.
11334 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11335 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11336 {
11337 self.advance();
11338 }
11339 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11340 return Err(self.err(alloc::format!(
11341 "expected TRIGGER after {}, got {:?}",
11342 if enabled { "ENABLE" } else { "DISABLE" },
11343 self.peek()
11344 )));
11345 }
11346 self.advance();
11347 // `ALL` lexes as Token::All (reserved); also
11348 // accept Token::Ident("all") for symmetry.
11349 // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11350 // TRIGGER selectors. USER (= all user triggers) is
11351 // semantically ALL here; REPLICA / ALWAYS gate on
11352 // session_replication_role which SPG doesn't track.
11353 // All map to TriggerSelector::All.
11354 let which = if matches!(self.peek(), Token::All)
11355 || matches!(self.peek(), Token::Ident(s)
11356 if s.eq_ignore_ascii_case("all")
11357 || s.eq_ignore_ascii_case("user")
11358 || s.eq_ignore_ascii_case("replica")
11359 || s.eq_ignore_ascii_case("always"))
11360 {
11361 self.advance();
11362 crate::ast::TriggerSelector::All
11363 } else {
11364 let name = self.expect_ident_like()?;
11365 crate::ast::TriggerSelector::Named(name)
11366 };
11367 Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11368 which,
11369 enabled,
11370 }])
11371 }
11372 // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11373 Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11374 self.advance();
11375 if !matches!(self.peek(), Token::Partition)
11376 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11377 if s.eq_ignore_ascii_case("partition"))
11378 {
11379 return Err(self.err(alloc::format!(
11380 "expected PARTITION after ATTACH, got {:?}",
11381 self.peek()
11382 )));
11383 }
11384 self.advance();
11385 let child = self.expect_ident_like()?;
11386 let bounds = self.parse_partition_bounds_tail()?;
11387 Ok(alloc::vec![
11388 crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11389 ])
11390 }
11391 // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11392 Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11393 self.advance();
11394 if !matches!(self.peek(), Token::Partition)
11395 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11396 if s.eq_ignore_ascii_case("partition"))
11397 {
11398 return Err(self.err(alloc::format!(
11399 "expected PARTITION after DETACH, got {:?}",
11400 self.peek()
11401 )));
11402 }
11403 self.advance();
11404 let child = self.expect_ident_like()?;
11405 let mut concurrently = false;
11406 let mut finalize = false;
11407 loop {
11408 match self.peek().clone() {
11409 Token::Ident(s) | Token::QuotedIdent(s)
11410 if s.eq_ignore_ascii_case("concurrently") =>
11411 {
11412 self.advance();
11413 concurrently = true;
11414 }
11415 Token::Ident(s) | Token::QuotedIdent(s)
11416 if s.eq_ignore_ascii_case("finalize") =>
11417 {
11418 self.advance();
11419 finalize = true;
11420 }
11421 _ => break,
11422 }
11423 }
11424 Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11425 child,
11426 concurrently,
11427 finalize,
11428 }])
11429 }
11430 other => Err(self.err(alloc::format!(
11431 "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11432 ))),
11433 }
11434 }
11435
11436 /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11437 /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11438 /// TABLE … ATTACH PARTITION. Shares the same grammar as
11439 /// `parse_partition_of_tail`'s bounds branch.
11440 /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11441 /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11442 /// lowering each to the respective AlterTableTarget. Any
11443 /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11444 /// no-op via consume_until_statement_boundary.
11445 fn parse_alter_column_drop_tail(
11446 &mut self,
11447 col_name: String,
11448 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11449 match self.peek().clone() {
11450 Token::Default => {
11451 self.advance();
11452 Ok(alloc::vec![
11453 crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11454 ])
11455 }
11456 Token::Not => {
11457 self.advance();
11458 if !matches!(self.peek(), Token::Null) {
11459 return Err(self.err(alloc::format!(
11460 "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11461 self.peek()
11462 )));
11463 }
11464 self.advance();
11465 Ok(alloc::vec![
11466 crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11467 ])
11468 }
11469 // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11470 // generated column into a plain column.
11471 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11472 self.advance();
11473 // v7.39 (round 187, U10) — IF EXISTS was consumed but
11474 // dropped, so the engine still errored on a plain
11475 // column; PG's semantics are NOTICE + skip.
11476 let mut if_exists = false;
11477 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11478 self.advance();
11479 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11480 self.advance();
11481 if_exists = true;
11482 }
11483 }
11484 Ok(alloc::vec![
11485 crate::ast::AlterTableTarget::AlterColumnDropExpression {
11486 column: col_name,
11487 if_exists,
11488 }
11489 ])
11490 }
11491 // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11492 // identity column into a plain column.
11493 Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11494 self.advance();
11495 let mut if_exists = false;
11496 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11497 self.advance();
11498 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11499 self.advance();
11500 if_exists = true;
11501 }
11502 }
11503 Ok(alloc::vec![
11504 crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11505 column: col_name,
11506 if_exists,
11507 }
11508 ])
11509 }
11510 _ => {
11511 self.consume_until_statement_boundary();
11512 Ok(Vec::new())
11513 }
11514 }
11515 }
11516
11517 /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11518 /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11519 /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11520 /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11521 fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11522 let mut opts = crate::ast::CopyOptions::default();
11523 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11524 return Ok(opts);
11525 }
11526 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11527 self.advance();
11528 }
11529 if matches!(self.peek(), Token::LParen) {
11530 self.advance();
11531 loop {
11532 self.parse_one_copy_option(&mut opts)?;
11533 match self.peek() {
11534 Token::Comma => {
11535 self.advance();
11536 }
11537 Token::RParen => {
11538 self.advance();
11539 break;
11540 }
11541 other => {
11542 return Err(self.err(alloc::format!(
11543 "expected ',' or ')' in COPY options, got {other:?}"
11544 )));
11545 }
11546 }
11547 }
11548 } else {
11549 while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11550 self.parse_one_copy_option(&mut opts)?;
11551 }
11552 }
11553 if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11554 return Err(self.err(alloc::format!(
11555 "unexpected token after COPY options: {:?}",
11556 self.peek()
11557 )));
11558 }
11559 Ok(opts)
11560 }
11561
11562 fn parse_one_copy_option(
11563 &mut self,
11564 opts: &mut crate::ast::CopyOptions,
11565 ) -> Result<(), ParseError> {
11566 use crate::ast::CopyFormat;
11567 // The option keyword. NULL lexes as its own token; the rest are
11568 // bare identifiers.
11569 let kw = match self.advance() {
11570 Token::Null => alloc::string::String::from("NULL"),
11571 Token::Ident(s) => s.to_uppercase(),
11572 other => {
11573 return Err(self.err(alloc::format!(
11574 "expected a COPY option keyword, got {other:?}"
11575 )));
11576 }
11577 };
11578 match kw.as_str() {
11579 "FORMAT" => {
11580 let fmt = self.expect_ident_like()?;
11581 match fmt.to_ascii_uppercase().as_str() {
11582 "CSV" => opts.format = CopyFormat::Csv,
11583 "TEXT" => opts.format = CopyFormat::Text,
11584 other => {
11585 return Err(self.err(alloc::format!(
11586 "COPY format \"{}\" not recognized",
11587 other.to_ascii_lowercase()
11588 )));
11589 }
11590 }
11591 }
11592 // Legacy bare format keywords.
11593 "CSV" => opts.format = CopyFormat::Csv,
11594 "TEXT" => opts.format = CopyFormat::Text,
11595 "HEADER" => {
11596 opts.header = match self.peek() {
11597 Token::True => {
11598 self.advance();
11599 true
11600 }
11601 Token::False => {
11602 self.advance();
11603 false
11604 }
11605 Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
11606 self.advance();
11607 true
11608 }
11609 Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
11610 self.advance();
11611 false
11612 }
11613 // Bare HEADER (no boolean) means HEADER true.
11614 _ => true,
11615 };
11616 }
11617 "DELIMITER" | "QUOTE" | "ESCAPE" => {
11618 let s = match self.advance() {
11619 Token::String(s) => s,
11620 other => {
11621 return Err(self.err(alloc::format!(
11622 "COPY {kw} expects a single-character string, got {other:?}"
11623 )));
11624 }
11625 };
11626 // v7.39 (round 247) — PG's wording (0A000), keyword in
11627 // lowercase: "COPY delimiter must be a single one-byte
11628 // character".
11629 let one_byte_err = || {
11630 self.err(alloc::format!(
11631 "COPY {} must be a single one-byte character",
11632 kw.to_ascii_lowercase()
11633 ))
11634 };
11635 let mut chars = s.chars();
11636 let c = chars.next().ok_or_else(one_byte_err)?;
11637 if chars.next().is_some() || c.len_utf8() != 1 {
11638 return Err(one_byte_err());
11639 }
11640 match kw.as_str() {
11641 "DELIMITER" => opts.delimiter = Some(c),
11642 "QUOTE" => opts.quote = Some(c),
11643 _ => opts.escape = Some(c),
11644 }
11645 }
11646 // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
11647 "FORCE_QUOTE" => {
11648 if matches!(self.peek(), Token::Star) {
11649 self.advance();
11650 opts.force_quote = Some(Vec::new());
11651 } else {
11652 if !matches!(self.peek(), Token::LParen) {
11653 return Err(self.err(alloc::format!(
11654 "expected '(' or '*' after FORCE_QUOTE, got {:?}",
11655 self.peek()
11656 )));
11657 }
11658 self.advance();
11659 let mut cols = Vec::new();
11660 loop {
11661 cols.push(self.expect_ident_like()?);
11662 match self.peek() {
11663 Token::Comma => {
11664 self.advance();
11665 }
11666 Token::RParen => {
11667 self.advance();
11668 break;
11669 }
11670 other => {
11671 return Err(self.err(alloc::format!(
11672 "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
11673 )));
11674 }
11675 }
11676 }
11677 opts.force_quote = Some(cols);
11678 }
11679 }
11680 "NULL" => {
11681 opts.null_str = Some(match self.advance() {
11682 Token::String(s) => s,
11683 other => {
11684 return Err(self.err(alloc::format!(
11685 "COPY NULL expects a quoted string, got {other:?}"
11686 )));
11687 }
11688 });
11689 }
11690 // v7.39 (round 265) — the two CSV FROM-side column lists. Same
11691 // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
11692 // FORCE_NULL too.
11693 "FORCE_NOT_NULL" | "FORCE_NULL" => {
11694 let cols = self.parse_copy_column_list(&kw)?;
11695 if kw == "FORCE_NOT_NULL" {
11696 opts.force_not_null = Some(cols);
11697 } else {
11698 opts.force_null = Some(cols);
11699 }
11700 }
11701 other => {
11702 // PG's wording, lowercased option name.
11703 return Err(self.err(alloc::format!(
11704 "option \"{}\" not recognized",
11705 other.to_ascii_lowercase()
11706 )));
11707 }
11708 }
11709 Ok(())
11710 }
11711
11712 /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
11713 /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
11714 /// is the `*` spelling.
11715 fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
11716 if matches!(self.peek(), Token::Star) {
11717 self.advance();
11718 return Ok(Vec::new());
11719 }
11720 if !matches!(self.peek(), Token::LParen) {
11721 return Err(self.err(alloc::format!(
11722 "expected '(' or '*' after {kw}, got {:?}",
11723 self.peek()
11724 )));
11725 }
11726 self.advance();
11727 let mut cols = Vec::new();
11728 loop {
11729 cols.push(self.expect_ident_like()?);
11730 match self.peek() {
11731 Token::Comma => {
11732 self.advance();
11733 }
11734 Token::RParen => {
11735 self.advance();
11736 break;
11737 }
11738 other => {
11739 return Err(self.err(alloc::format!(
11740 "expected ',' or ')' in {kw} list, got {other:?}"
11741 )));
11742 }
11743 }
11744 }
11745 Ok(cols)
11746 }
11747
11748 fn parse_partition_bounds_tail(
11749 &mut self,
11750 ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
11751 use crate::ast::PartitionOfBoundsAst;
11752 match self.peek() {
11753 Token::Default => {
11754 self.advance();
11755 Ok(PartitionOfBoundsAst::Default)
11756 }
11757 Token::For => {
11758 self.advance();
11759 if !matches!(self.peek(), Token::Values) {
11760 return Err(
11761 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
11762 );
11763 }
11764 self.advance();
11765 let want_with = matches!(
11766 self.peek(),
11767 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
11768 );
11769 if want_with {
11770 self.advance();
11771 if !matches!(self.peek(), Token::LParen) {
11772 return Err(self.err(format!(
11773 "expected '(' after FOR VALUES WITH, got {:?}",
11774 self.peek()
11775 )));
11776 }
11777 self.advance();
11778 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
11779 loop {
11780 let key = self.expect_ident_like()?;
11781 let n = match self.peek().clone() {
11782 Token::Integer(v) if u32::try_from(v).is_ok() => {
11783 self.advance();
11784 v as u32
11785 }
11786 other => {
11787 return Err(self.err(format!(
11788 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
11789 )));
11790 }
11791 };
11792 match key.to_ascii_uppercase().as_str() {
11793 "MODULUS" => modulus = Some(n),
11794 "REMAINDER" => remainder = Some(n),
11795 other => {
11796 return Err(self.err(format!(
11797 "FOR VALUES WITH: unknown key {other:?}; \
11798 expected MODULUS or REMAINDER"
11799 )));
11800 }
11801 }
11802 match self.peek() {
11803 Token::Comma => {
11804 self.advance();
11805 }
11806 Token::RParen => {
11807 self.advance();
11808 break;
11809 }
11810 other => {
11811 return Err(self.err(format!(
11812 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
11813 )));
11814 }
11815 }
11816 }
11817 let modulus = modulus
11818 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
11819 let remainder = remainder.ok_or_else(|| {
11820 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
11821 })?;
11822 if modulus == 0 {
11823 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
11824 }
11825 if remainder >= modulus {
11826 return Err(self.err(format!(
11827 "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
11828 )));
11829 }
11830 return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
11831 }
11832 match self.peek() {
11833 Token::From => {
11834 self.advance();
11835 let lower = Box::new(self.parse_partition_bound_expr()?);
11836 if !matches!(self.peek(), Token::To) {
11837 return Err(self.err(format!(
11838 "expected TO after FROM (...), got {:?}",
11839 self.peek()
11840 )));
11841 }
11842 self.advance();
11843 let upper = Box::new(self.parse_partition_bound_expr()?);
11844 Ok(PartitionOfBoundsAst::Range { lower, upper })
11845 }
11846 Token::In => {
11847 self.advance();
11848 if !matches!(self.peek(), Token::LParen) {
11849 return Err(self.err(format!(
11850 "expected '(' after FOR VALUES IN, got {:?}",
11851 self.peek()
11852 )));
11853 }
11854 self.advance();
11855 let mut values = Vec::new();
11856 loop {
11857 values.push(self.parse_expr(0)?);
11858 match self.peek() {
11859 Token::Comma => {
11860 self.advance();
11861 }
11862 Token::RParen => {
11863 self.advance();
11864 break;
11865 }
11866 other => {
11867 return Err(self.err(format!(
11868 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
11869 )));
11870 }
11871 }
11872 }
11873 if values.is_empty() {
11874 return Err(
11875 self.err("FOR VALUES IN requires at least one literal".to_string())
11876 );
11877 }
11878 Ok(PartitionOfBoundsAst::List { values })
11879 }
11880 other => Err(self.err(format!(
11881 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
11882 ))),
11883 }
11884 }
11885 other => Err(self.err(format!(
11886 "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
11887 ))),
11888 }
11889 }
11890
11891 /// v7.16.2 — peek for `information_schema.<tbl>` /
11892 /// `pg_catalog.<tbl>` triples and, if matched, consume all
11893 /// three tokens + return a synthetic table name the engine's
11894 /// SELECT path recognises as a virtual view. Returns `None`
11895 /// when the head doesn't look like a meta-qualified name.
11896 /// Used by `parse_table_ref` to bypass the
11897 /// `expect_ident_like` schema-strip for these specific PG
11898 /// meta schemas (mailrs round-10 A.3).
11899 fn try_peek_meta_qualified(&mut self) -> Option<String> {
11900 // Extract the schema name. Must be a plain ident token.
11901 let schema = match self.tokens.get(self.pos) {
11902 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
11903 _ => return None,
11904 };
11905 // Dot.
11906 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
11907 return None;
11908 }
11909 // The table-side ident may lex as a reserved keyword
11910 // (e.g. `Token::Tables`). Tolerate the common ones via a
11911 // helper that reads the trailing token's underlying name.
11912 let tbl = match self.tokens.get(self.pos + 2)? {
11913 Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
11914 Token::Tables => "tables".to_string(),
11915 // Other PG meta table names that may collide with
11916 // reserved keywords land here as needed.
11917 _ => return None,
11918 };
11919 // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
11920 // names so the synthetic name doesn't double-prefix
11921 // (`__spg_pg_class`, not `__spg_pg_pg_class`).
11922 let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
11923 ("__spg_info_", tbl.to_ascii_lowercase())
11924 } else if schema.eq_ignore_ascii_case("pg_catalog") {
11925 // v7.39 (round 541) — only the catalogs SPG actually
11926 // synthesises are rewritten, which is what the BARE path
11927 // has always checked. Anything else keeps its own name and
11928 // takes the ordinary route: `pg_stat_activity` and friends
11929 // resolve through meta_view_result, and a name that is no
11930 // catalog at all gets PG's "relation does not exist"
11931 // instead of a message about a view SPG cannot materialise.
11932 let lowered = tbl.to_ascii_lowercase();
11933 if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
11934 self.advance(); // schema
11935 self.advance(); // dot
11936 self.advance(); // tbl
11937 return Some(lowered);
11938 }
11939 let bare = lowered
11940 .strip_prefix("pg_")
11941 .map(alloc::string::String::from)
11942 .unwrap_or(lowered);
11943 ("__spg_pg_", bare)
11944 } else if schema.eq_ignore_ascii_case("mysql") {
11945 // v7.17.0 Phase 3.P0-65 — MySQL system schema
11946 // (`mysql.user`, `mysql.db`). Same synthetic-name
11947 // shape as pg_catalog.
11948 ("__spg_mysql_", tbl.to_ascii_lowercase())
11949 } else {
11950 return None;
11951 };
11952 self.advance(); // schema
11953 self.advance(); // dot
11954 self.advance(); // tbl
11955 Some(alloc::format!("{prefix}{normalised}"))
11956 }
11957
11958 /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
11959 /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
11960 /// implicit front of every search_path, so a bare reference to a
11961 /// known catalog table always means the catalog table. Only the
11962 /// names the engine actually synthesises are recognised — any
11963 /// other `pg_*` ident stays a user table (mailrs embed round-12).
11964 fn try_peek_meta_bare(&mut self) -> Option<String> {
11965 // v7.38 (read01 P3.21) — every catalog view SPG synthesises
11966 // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
11967 // `pg_catalog` at the front of every search_path. (pg_stat_activity
11968 // / pg_stat_statements / pg_locks / pg_statio_user_tables route
11969 // through the meta_view_result path instead, and already resolve
11970 // bare — they must NOT be listed here or the __spg_ rewrite would
11971 // mis-target them.)
11972 const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
11973 let name = match self.tokens.get(self.pos) {
11974 Some(Token::Ident(s)) => s.to_ascii_lowercase(),
11975 _ => return None,
11976 };
11977 // A following dot means this ident is a schema qualifier,
11978 // not a table name — let the qualified path handle it.
11979 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
11980 return None;
11981 }
11982 if !PG_META_TABLES.contains(&name.as_str()) {
11983 return None;
11984 }
11985 self.advance();
11986 let bare = name.strip_prefix("pg_").unwrap_or(&name);
11987 Some(alloc::format!("__spg_pg_{bare}"))
11988 }
11989
11990 /// Consume a bare ident if its lowercase matches `kw`, else err.
11991 /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
11992 /// Peeks only; the caller advances.
11993 fn peek_keyword_ident(&self, kw: &str) -> bool {
11994 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
11995 }
11996
11997 fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
11998 match self.advance() {
11999 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12000 other => Err(ParseError {
12001 message: format!("expected {kw:?}, got {other:?}"),
12002 token_pos: self.consumed_pos(),
12003 }),
12004 }
12005 }
12006
12007 /// Accept either a quoted identifier (`"foo"`) or a quoted string
12008 /// literal (`'foo'`) — same shape used by CREATE USER for the
12009 /// username slot.
12010 fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12011 match self.advance() {
12012 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12013 other => Err(ParseError {
12014 message: format!("expected identifier or string, got {other:?}"),
12015 token_pos: self.consumed_pos(),
12016 }),
12017 }
12018 }
12019
12020 fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12021 match self.advance() {
12022 Token::String(s) => Ok(s),
12023 other => Err(ParseError {
12024 message: format!("expected quoted string, got {other:?}"),
12025 token_pos: self.consumed_pos(),
12026 }),
12027 }
12028 }
12029
12030 fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12031 // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12032 // subqueries recurse through here without passing
12033 // parse_expr; share the same nesting budget.
12034 self.enter_nested()?;
12035 let r = self.parse_select_stmt_inner();
12036 self.nest_depth -= 1;
12037 r
12038 }
12039
12040 fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12041 // Caller dispatches on Token::Select; the inner helper handles
12042 // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12043 // get a fresh bare-select parse and may not have their own ORDER
12044 // BY / LIMIT.
12045 let mut head = self.parse_bare_select()?;
12046 self.parse_setop_chain_into(&mut head)?;
12047 self.parse_select_tail_into(&mut head)?;
12048 Ok(Statement::Select(head))
12049 }
12050
12051 /// v7.37.17 (17.6 siblings) — the three SQL set operations
12052 /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12053 /// token), and INTERSECT [ALL] (a bare ident — it was never
12054 /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12055 /// tighter than UNION / EXCEPT — the executor folds the chain
12056 /// left-to-right, which is already correct for LEADING
12057 /// intersects; an INTERSECT pair that FOLLOWS a union/except
12058 /// pair nests into that previous peer, so A UNION B INTERSECT C
12059 /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12060 /// groups.
12061 fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12062 // A parenthesized group arrives with its own (already
12063 // regrouped) unions on `head`; only the pairs THIS chain
12064 // appends participate in the precedence regroup below —
12065 // nesting an outer INTERSECT into a group-internal peer
12066 // would dissolve the explicit grouping.
12067 let boundary = head.unions.len();
12068 loop {
12069 let base = match self.peek() {
12070 Token::Union => UnionKind::Distinct,
12071 Token::Except => UnionKind::Except,
12072 Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12073 _ => break,
12074 };
12075 self.advance();
12076 let kind = if matches!(self.peek(), Token::All) {
12077 self.advance();
12078 match base {
12079 UnionKind::Distinct => UnionKind::All,
12080 UnionKind::Except => UnionKind::ExceptAll,
12081 _ => UnionKind::IntersectAll,
12082 }
12083 } else {
12084 base
12085 };
12086 let peer = self.parse_bare_select()?;
12087 head.unions.push((kind, peer));
12088 }
12089 let mut pairs = core::mem::take(&mut head.unions);
12090 let tail = pairs.split_off(boundary);
12091 let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12092 for (kind, peer) in tail {
12093 let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12094 // An intersect nests into the previous element of THIS
12095 // chain only; with no new previous element it stays at
12096 // the outer level (the left fold applies it to the
12097 // whole head, group included).
12098 match (
12099 is_intersect,
12100 regrouped.len() > boundary,
12101 regrouped.last_mut(),
12102 ) {
12103 (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12104 _ => regrouped.push((kind, peer)),
12105 }
12106 }
12107 head.unions = regrouped;
12108 Ok(())
12109 }
12110
12111 /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12112 /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12113 /// the top-level bare VALUES statement reuses it verbatim.
12114 /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12115 /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12116 /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12117 /// where the grouping-set universe is still in scope.
12118 fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12119 if !matches!(self.peek(), Token::Order) {
12120 return Ok(Vec::new());
12121 }
12122 self.advance();
12123 if !self.peek_is_by() {
12124 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12125 }
12126 self.advance();
12127 let mut keys = Vec::new();
12128 loop {
12129 // v7.39 (round 691) — save/restore, the discipline this parser
12130 // already uses around `pending_sample_preds`, so a subquery inside
12131 // a key neither inherits nor leaks the channel.
12132 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12133 let saved_coll = self.order_key_collation.take();
12134 let parsed = self.parse_expr(0);
12135 self.in_order_by_key = saved_flag;
12136 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12137 let expr = parsed?;
12138 let desc = if matches!(self.peek(), Token::Desc) {
12139 self.advance();
12140 true
12141 } else if matches!(self.peek(), Token::Asc) {
12142 self.advance();
12143 false
12144 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12145 // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12146 // one ordering per type, so the btree comparison operators map
12147 // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12148 // would need a custom operator class — honest error.
12149 self.advance();
12150 match self.advance() {
12151 Token::Lt | Token::LtEq => false,
12152 Token::Gt | Token::GtEq => true,
12153 other => {
12154 return Err(self.err(alloc::format!(
12155 "ORDER BY USING supports the btree comparison \
12156 operators (< <= > >=); got {other:?}"
12157 )));
12158 }
12159 }
12160 } else {
12161 false
12162 };
12163 // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12164 let nulls_first = self.parse_optional_nulls_placement()?;
12165 keys.push(OrderBy {
12166 expr,
12167 desc,
12168 nulls_first,
12169 collation,
12170 });
12171 if matches!(self.peek(), Token::Comma) {
12172 self.advance();
12173 } else {
12174 break;
12175 }
12176 }
12177 Ok(keys)
12178 }
12179
12180 fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12181 // v7.39 (round 135) — a grouping-set query may have already parsed +
12182 // rewritten its ORDER BY (to reference synthetic grouping columns); if
12183 // no ORDER BY token is present, keep that pre-set order_by rather than
12184 // clobbering it with an empty list.
12185 let parsed_keys = self.parse_order_by_keys()?;
12186 head.order_by = if parsed_keys.is_empty() {
12187 core::mem::take(&mut head.order_by)
12188 } else {
12189 parsed_keys
12190 };
12191 // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12192 // order. PG's grammar takes a limit clause and an offset clause
12193 // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12194 // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12195 // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12196 // spelling died on `expected end of input, got Limit`.
12197 //
12198 // Each may appear at most once, and LIMIT and FETCH FIRST are
12199 // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12200 // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12201 // A second one is left unconsumed here, which the caller reports
12202 // as trailing input rather than silently taking the last.
12203 let mut saw_limit = false;
12204 let mut saw_offset = false;
12205 loop {
12206 if !saw_limit && matches!(self.peek(), Token::Limit) {
12207 self.advance();
12208 // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12209 // PG synonyms for "no limit". Treat both as None
12210 // (no head.limit set) so the engine's existing
12211 // unlimited-result path takes over. Reject was the
12212 // pre-5.1 behaviour and broke pg_dump-flavoured
12213 // tooling that occasionally emits LIMIT NULL.
12214 if self.consume_limit_unbounded_sentinel() {
12215 head.limit = None;
12216 } else {
12217 let first = self.parse_limit_expr("LIMIT")?;
12218 // MySQL `LIMIT offset, count` — the first number is
12219 // the offset when a comma follows.
12220 if matches!(self.peek(), Token::Comma) {
12221 self.advance();
12222 let count = self.parse_limit_expr("LIMIT")?;
12223 head.offset = Some(first);
12224 saw_offset = true;
12225 head.limit = Some(count);
12226 } else {
12227 head.limit = Some(first);
12228 }
12229 }
12230 saw_limit = true;
12231 continue;
12232 }
12233 if !saw_offset && matches!(self.peek(), Token::Offset) {
12234 self.advance();
12235 // PG also accepts an optional `ROW` / `ROWS` trailer
12236 // after the offset value (`OFFSET 10 ROWS`). The
12237 // FETCH-FIRST branch below relies on the same.
12238 let off = self.parse_limit_expr("OFFSET")?;
12239 self.consume_optional_rows_keyword();
12240 head.offset = Some(off);
12241 saw_offset = true;
12242 continue;
12243 }
12244 // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12245 // the SQL-standard alias for LIMIT. PG accepts both
12246 // spellings interchangeably; pg_dump emits FETCH FIRST in
12247 // newer versions. We map it onto `head.limit` so the
12248 // engine path is unified.
12249 if !saw_limit
12250 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12251 if s.eq_ignore_ascii_case("fetch"))
12252 {
12253 self.advance(); // FETCH
12254 // `FIRST` or `NEXT` (both legal per SQL standard).
12255 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12256 if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12257 {
12258 self.advance();
12259 }
12260 // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12261 // implicit 1 — but we always consume one if present).
12262 let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12263 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12264 {
12265 // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12266 crate::ast::LimitExpr::Literal(1)
12267 } else {
12268 self.parse_limit_expr("FETCH FIRST")?
12269 };
12270 // Eat `ROW` / `ROWS` if not already consumed above.
12271 self.consume_optional_rows_keyword();
12272 // Optional `ONLY` (the spec form) — or the SQL:2008
12273 // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12274 // now honours WITH TIES by extending past the LIMIT
12275 // truncation point through every row that shares the
12276 // last-kept row's ORDER BY key.
12277 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12278 if s.eq_ignore_ascii_case("only"))
12279 {
12280 self.advance();
12281 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12282 if s.eq_ignore_ascii_case("with"))
12283 {
12284 self.advance(); // WITH
12285 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12286 if s.eq_ignore_ascii_case("ties"))
12287 {
12288 self.advance();
12289 head.limit_with_ties = true;
12290 }
12291 }
12292 head.limit = Some(count);
12293 saw_limit = true;
12294 continue;
12295 }
12296 break;
12297 }
12298 // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12299 // FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12300 // [ OF table_name [, …] ]
12301 // [ NOWAIT | SKIP LOCKED ]
12302 // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12303 // FOR SHARE OF t2`). SPG is a single-writer engine — every
12304 // SELECT already returns a consistent snapshot — so these
12305 // are accept-and-discard: the parser absorbs them so
12306 // mailrs / Rails / Django code paths that emit `SELECT
12307 // … FOR UPDATE` for advisory pessimistic locking load
12308 // without a parser error. The on-disk locking model is
12309 // unchanged; callers that rely on FOR UPDATE for read-
12310 // through-write ordering still get the right answer
12311 // because SPG serialises writes anyway.
12312 head.locking = self
12313 .consume_optional_for_lock_clauses()
12314 .map(alloc::boxed::Box::new);
12315 Ok(())
12316 }
12317
12318 /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12319 /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12320 /// LOCKED ]` trailers. Each clause is fully accepted and
12321 /// discarded — SPG's single-writer model already satisfies the
12322 /// callers' implicit ordering requirement. Stops at the first
12323 /// token that isn't `FOR`.
12324 fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12325 // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12326 // not discarded. PG keeps the strongest of several clauses; the
12327 // policy of the last one wins, which is what this loop records.
12328 let mut seen: Option<crate::ast::LockingClause> = None;
12329 while matches!(self.peek(), Token::For) {
12330 // v7.37.14 (A2.5-stub) — record that this query asked
12331 // for a row lock the parser is about to silently
12332 // discard. Operators surface the count via
12333 // `spg_sql::silent_for_update_count()` so they can
12334 // gauge how much of the workload depends on advisory
12335 // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12336 // before v7.37.15's per-row tuple locking lands.
12337 crate::record_silent_for_update_clause();
12338 self.advance(); // FOR
12339 // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12340 // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12341 let mut no_key = false;
12342 let mut key = false;
12343 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12344 if s.eq_ignore_ascii_case("no"))
12345 {
12346 self.advance(); // NO
12347 no_key = true;
12348 // The next ident should be KEY but be generous;
12349 // anything followed by UPDATE/SHARE is accepted.
12350 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12351 if s.eq_ignore_ascii_case("key"))
12352 {
12353 self.advance(); // KEY
12354 }
12355 }
12356 // `KEY` prefix (PG `FOR KEY SHARE`).
12357 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12358 if s.eq_ignore_ascii_case("key"))
12359 {
12360 self.advance(); // KEY
12361 key = true;
12362 }
12363 // Lock-strength keyword: UPDATE / SHARE. Required, but
12364 // we're lenient — an unexpected token here just bails
12365 // (we already consumed FOR; caller's downstream
12366 // dispatch will error if anything actually depends on
12367 // the trailing tokens).
12368 let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12369 if s.eq_ignore_ascii_case("update"));
12370 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12371 if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12372 {
12373 self.advance();
12374 use crate::ast::LockStrength as LS;
12375 let strength = match (is_update, no_key, key) {
12376 (true, true, _) => LS::NoKeyUpdate,
12377 (true, _, _) => LS::Update,
12378 (false, _, true) => LS::KeyShare,
12379 (false, _, _) => LS::Share,
12380 };
12381 seen = Some(crate::ast::LockingClause {
12382 strength,
12383 of_tables: alloc::vec::Vec::new(),
12384 policy: crate::ast::LockWait::Wait,
12385 });
12386 } else {
12387 // FOR by itself (or `FOR KEY` with nothing after) —
12388 // give up on the lock-clause path. We've already
12389 // advanced past FOR; further attempts to parse
12390 // here would clobber state.
12391 return seen;
12392 }
12393 // Optional `OF tbl[, tbl …]`. mailrs emits this when
12394 // joining and locking only a subset of tables.
12395 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12396 if s.eq_ignore_ascii_case("of"))
12397 {
12398 self.advance(); // OF
12399 #[allow(clippy::while_let_loop)]
12400 loop {
12401 match self.peek() {
12402 Token::Ident(_) | Token::QuotedIdent(_) => {
12403 // v7.39 (round 294) — the name is CAPTURED now: PG
12404 // validates it against the FROM clause, and an
12405 // uncaptured list silently means "lock everything".
12406 let mut nm = match self.advance() {
12407 Token::Ident(n) | Token::QuotedIdent(n) => n,
12408 _ => alloc::string::String::new(),
12409 };
12410 // Optional schema-qualified `schema.table`.
12411 if matches!(self.peek(), Token::Dot) {
12412 self.advance();
12413 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12414 {
12415 self.advance();
12416 nm = n;
12417 }
12418 }
12419 if let Some(c) = seen.as_mut() {
12420 c.of_tables.push(nm);
12421 }
12422 }
12423 _ => break,
12424 }
12425 if matches!(self.peek(), Token::Comma) {
12426 self.advance();
12427 } else {
12428 break;
12429 }
12430 }
12431 }
12432 // Optional `NOWAIT` | `SKIP LOCKED`.
12433 match self.peek().clone() {
12434 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12435 self.advance();
12436 if let Some(c) = seen.as_mut() {
12437 c.policy = crate::ast::LockWait::NoWait;
12438 }
12439 }
12440 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12441 self.advance(); // SKIP
12442 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12443 if s.eq_ignore_ascii_case("locked"))
12444 {
12445 self.advance(); // LOCKED
12446 if let Some(c) = seen.as_mut() {
12447 c.policy = crate::ast::LockWait::SkipLocked;
12448 }
12449 }
12450 }
12451 _ => {}
12452 }
12453 // Loop: PG allows multiple FOR clauses chained.
12454 }
12455 seen
12456 }
12457
12458 /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12459 /// Bind value gets resolved during prepared-statement Execute;
12460 /// the Pratt expression parser would over-accept here (e.g.
12461 /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12462 /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12463 /// sentinel tokens (PG synonyms for "no limit"). Returns true
12464 /// when one was consumed; caller skips the regular
12465 /// limit-value parse and leaves `head.limit` at None.
12466 fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12467 if matches!(self.peek(), Token::Null) {
12468 self.advance();
12469 return true;
12470 }
12471 if matches!(self.peek(), Token::All) {
12472 self.advance();
12473 return true;
12474 }
12475 false
12476 }
12477
12478 /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12479 /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12480 /// SQL-standard shape. No-op when missing.
12481 fn consume_optional_rows_keyword(&mut self) {
12482 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12483 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12484 {
12485 self.advance();
12486 }
12487 }
12488
12489 /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12490 ///
12491 /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12492 /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12493 /// constant, which is why that spelling keeps the token path below.
12494 ///
12495 /// Constants are folded here rather than carried into the tree: the
12496 /// 15+ execution paths that read the row count go through
12497 /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12498 /// means "no limit". A clause the engine could not resolve would
12499 /// therefore return the WHOLE table instead of failing. Folding at
12500 /// parse time keeps that impossible; a non-constant clause is still
12501 /// a clean error (recorded residual — closing it wants a resolution
12502 /// pre-pass on the simple-query path, where `substitute_placeholders`
12503 /// does not run).
12504 fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12505 // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12506 // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12507 // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12508 // ONLY` both work (its grammar takes a c_expr). Both measured
12509 // against PG 18.4 in round 305.
12510 if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12511 return self.parse_limit_constant(label);
12512 }
12513 // One pass, no rewind: `advance()` takes each token by
12514 // `mem::replace`, so a consumed token reads back as Eof and this
12515 // parser cannot backtrack. Everything — bare literal included —
12516 // is therefore folded from the parsed expression rather than
12517 // re-read from the token stream.
12518 let start = self.pos;
12519 let e = self.parse_expr(0)?;
12520 if let crate::ast::Expr::Placeholder(n) = e {
12521 return Ok(crate::ast::LimitExpr::Placeholder(n));
12522 }
12523 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12524 match fold_limit_constant(&e) {
12525 Some(Ok(v)) if v < 0 => Err(ParseError {
12526 message: alloc::format!("{neg_label} must not be negative"),
12527 token_pos: start,
12528 }),
12529 Some(Ok(v)) => u32::try_from(v)
12530 .map(crate::ast::LimitExpr::Literal)
12531 .map_err(|_| ParseError {
12532 message: alloc::format!("{label} value too large: {v}"),
12533 token_pos: start,
12534 }),
12535 Some(Err(message)) => Err(ParseError {
12536 message: message.replace("{L}", neg_label),
12537 token_pos: start,
12538 }),
12539 // v7.39 (round 305, V23) — not foldable at parse time
12540 // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
12541 // expression; the engine evaluates it once before dispatch.
12542 None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
12543 }
12544 }
12545
12546 fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12547 // v7.39 (round 239) — PG's row-count clause takes a bigint with its
12548 // coercion rules, not just an integer token: a NUMERIC rounds half
12549 // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
12550 // refused with PG's wording ("LIMIT must not be negative", 2201W /
12551 // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
12552 // content, failing as an input-syntax error on the value. General
12553 // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
12554 // they need an Expr-carrying LimitExpr variant.
12555 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12556 let err_at = |message: alloc::string::String, pos: usize| ParseError {
12557 message,
12558 token_pos: pos,
12559 };
12560 match self.advance() {
12561 Token::Integer(n) if n >= 0 => u32::try_from(n)
12562 .map(crate::ast::LimitExpr::Literal)
12563 .map_err(|_| ParseError {
12564 message: alloc::format!("{label} value too large: {n}"),
12565 token_pos: self.consumed_pos(),
12566 }),
12567 Token::Integer(_) => Err(err_at(
12568 alloc::format!("{neg_label} must not be negative"),
12569 self.pos.saturating_sub(1),
12570 )),
12571 Token::Numeric(t) => {
12572 let pos = self.pos.saturating_sub(1);
12573 let v: f64 = t.parse().map_err(|_| {
12574 err_at(
12575 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12576 pos,
12577 )
12578 })?;
12579 if v < 0.0 {
12580 return Err(err_at(
12581 alloc::format!("{neg_label} must not be negative"),
12582 pos,
12583 ));
12584 }
12585 // Round half away from zero — PG's numeric→bigint cast.
12586 // (no_std: no f64::round; v is non-negative, so truncating
12587 // v + 0.5 is the same thing.)
12588 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
12589 let rounded = (v + 0.5) as u64;
12590 u32::try_from(rounded)
12591 .map(crate::ast::LimitExpr::Literal)
12592 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
12593 }
12594 Token::Minus => {
12595 let pos = self.pos.saturating_sub(1);
12596 match self.peek() {
12597 Token::Integer(_) | Token::Numeric(_) => {
12598 self.advance();
12599 Err(err_at(
12600 alloc::format!("{neg_label} must not be negative"),
12601 pos,
12602 ))
12603 }
12604 other => Err(err_at(
12605 alloc::format!(
12606 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12607 ),
12608 pos,
12609 )),
12610 }
12611 }
12612 Token::String(t) => {
12613 let pos = self.pos.saturating_sub(1);
12614 match t.trim().parse::<i64>() {
12615 Ok(n) if n < 0 => Err(err_at(
12616 alloc::format!("{neg_label} must not be negative"),
12617 pos,
12618 )),
12619 Ok(n) => u32::try_from(n)
12620 .map(crate::ast::LimitExpr::Literal)
12621 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
12622 Err(_) => Err(err_at(
12623 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12624 pos,
12625 )),
12626 }
12627 }
12628 Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
12629 other => Err(ParseError {
12630 message: alloc::format!(
12631 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12632 ),
12633 token_pos: self.consumed_pos(),
12634 }),
12635 }
12636 }
12637
12638 /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
12639 /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
12640 /// `unions` empty and `order_by` / `limit` `None`; the top-level
12641 /// `parse_select_stmt` is responsible for filling those in.
12642 /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
12643 /// call in the expression tree to the per-set integer bitmask
12644 /// (PG semantics: one bit per argument, MSB first; 1 = the key
12645 /// is dropped in this grouping set). Runs during the ROLLUP /
12646 /// CUBE / GROUPING SETS expansion, where the set is known.
12647 /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
12648 /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
12649 fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
12650 if let Expr::FunctionCall { name, .. } = expr
12651 && name.eq_ignore_ascii_case("grouping")
12652 {
12653 if !out.iter().any(|e| e == expr) {
12654 out.push(expr.clone());
12655 }
12656 return;
12657 }
12658 match expr {
12659 Expr::Binary { lhs, rhs, .. } => {
12660 Self::collect_grouping_calls(lhs, out);
12661 Self::collect_grouping_calls(rhs, out);
12662 }
12663 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12664 Self::collect_grouping_calls(expr, out)
12665 }
12666 Expr::FunctionCall { args, .. } => {
12667 for a in args {
12668 Self::collect_grouping_calls(a, out);
12669 }
12670 }
12671 Expr::Case {
12672 operand,
12673 branches,
12674 else_branch,
12675 } => {
12676 if let Some(o) = operand {
12677 Self::collect_grouping_calls(o, out);
12678 }
12679 for (c, v) in branches {
12680 Self::collect_grouping_calls(c, out);
12681 Self::collect_grouping_calls(v, out);
12682 }
12683 if let Some(x) = else_branch {
12684 Self::collect_grouping_calls(x, out);
12685 }
12686 }
12687 _ => {}
12688 }
12689 }
12690
12691 /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
12692 /// `grp_exprs[k]` with a reference to the synthetic ordering column
12693 /// `__grp_ord_k` (injected per grouping-set branch).
12694 fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
12695 if let Expr::FunctionCall { name, .. } = expr
12696 && name.eq_ignore_ascii_case("grouping")
12697 {
12698 if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
12699 *expr = Expr::Column(crate::ast::ColumnName {
12700 qualifier: None,
12701 name: alloc::format!("__grp_ord_{k}"),
12702 });
12703 }
12704 return;
12705 }
12706 match expr {
12707 Expr::Binary { lhs, rhs, .. } => {
12708 Self::rewrite_grouping_to_col(lhs, grp_exprs);
12709 Self::rewrite_grouping_to_col(rhs, grp_exprs);
12710 }
12711 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12712 Self::rewrite_grouping_to_col(expr, grp_exprs)
12713 }
12714 Expr::FunctionCall { args, .. } => {
12715 for a in args {
12716 Self::rewrite_grouping_to_col(a, grp_exprs);
12717 }
12718 }
12719 Expr::Case {
12720 operand,
12721 branches,
12722 else_branch,
12723 } => {
12724 if let Some(o) = operand {
12725 Self::rewrite_grouping_to_col(o, grp_exprs);
12726 }
12727 for (c, v) in branches {
12728 Self::rewrite_grouping_to_col(c, grp_exprs);
12729 Self::rewrite_grouping_to_col(v, grp_exprs);
12730 }
12731 if let Some(x) = else_branch {
12732 Self::rewrite_grouping_to_col(x, grp_exprs);
12733 }
12734 }
12735 _ => {}
12736 }
12737 }
12738
12739 /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
12740 /// as the list of key sets it contributes. A bare expression is one
12741 /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
12742 /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
12743 /// the concatenation of its items' sets, where an item is itself an
12744 /// element, a parenthesized key list, or the empty set `()`. A
12745 /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
12746 /// move together.
12747 fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
12748 let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
12749 // ROLLUP ( … ) / CUBE ( … )
12750 if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
12751 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
12752 {
12753 let is_cube = is_kw(self.peek(), "cube");
12754 self.advance(); // ROLLUP / CUBE
12755 self.advance(); // (
12756 let mut units: Vec<Vec<Expr>> = Vec::new();
12757 loop {
12758 if matches!(self.peek(), Token::LParen) {
12759 // Composite unit: (a, b) rolls up as one.
12760 self.advance();
12761 let mut unit = Vec::new();
12762 if !matches!(self.peek(), Token::RParen) {
12763 loop {
12764 unit.push(self.parse_expr(0)?);
12765 match self.peek() {
12766 Token::Comma => {
12767 self.advance();
12768 }
12769 Token::RParen => break,
12770 other => {
12771 return Err(self.err(format!(
12772 "expected ',' or ')' in grouping unit, got {other:?}"
12773 )));
12774 }
12775 }
12776 }
12777 }
12778 self.advance(); // )
12779 units.push(unit);
12780 } else {
12781 units.push(alloc::vec![self.parse_expr(0)?]);
12782 }
12783 match self.peek() {
12784 Token::Comma => {
12785 self.advance();
12786 }
12787 Token::RParen => break,
12788 other => {
12789 return Err(self.err(format!(
12790 "expected ',' or ')' in grouping list, got {other:?}"
12791 )));
12792 }
12793 }
12794 }
12795 self.advance(); // )
12796 let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
12797 units
12798 .iter()
12799 .zip(unit_sel.iter())
12800 .filter(|(_, keep)| **keep)
12801 .flat_map(|(u, _)| u.iter().cloned())
12802 .collect()
12803 };
12804 let n = units.len();
12805 if is_cube {
12806 let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
12807 .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
12808 .collect();
12809 subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
12810 return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
12811 }
12812 return Ok((0..=n)
12813 .rev()
12814 .map(|keep| {
12815 let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
12816 flatten(&sel)
12817 })
12818 .collect());
12819 }
12820 // GROUPING SETS ( item [, item]* )
12821 if is_kw(self.peek(), "grouping")
12822 && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
12823 {
12824 self.advance(); // GROUPING
12825 self.advance(); // SETS
12826 if !matches!(self.peek(), Token::LParen) {
12827 return Err(self.err(format!(
12828 "expected '(' after GROUPING SETS, got {:?}",
12829 self.peek()
12830 )));
12831 }
12832 self.advance(); // outer (
12833 let mut sets: Vec<Vec<Expr>> = Vec::new();
12834 loop {
12835 if matches!(self.peek(), Token::LParen) {
12836 // A parenthesized key list (or the empty set).
12837 self.advance();
12838 let mut set = Vec::new();
12839 if !matches!(self.peek(), Token::RParen) {
12840 loop {
12841 set.push(self.parse_expr(0)?);
12842 match self.peek() {
12843 Token::Comma => {
12844 self.advance();
12845 }
12846 Token::RParen => break,
12847 other => {
12848 return Err(self.err(format!(
12849 "expected ',' or ')' in grouping set, got {other:?}"
12850 )));
12851 }
12852 }
12853 }
12854 }
12855 self.advance(); // )
12856 sets.push(set);
12857 } else {
12858 // A nested element: ROLLUP/CUBE/GROUPING SETS or a
12859 // bare expression.
12860 sets.extend(self.parse_grouping_element()?);
12861 }
12862 match self.peek() {
12863 Token::Comma => {
12864 self.advance();
12865 }
12866 Token::RParen => break,
12867 other => {
12868 return Err(self.err(format!(
12869 "expected ',' or ')' after a grouping set, got {other:?}"
12870 )));
12871 }
12872 }
12873 }
12874 self.advance(); // outer )
12875 return Ok(sets);
12876 }
12877 Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
12878 }
12879
12880 fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
12881 // v7.38 (read01) — a reference to a key that is dropped in this grouping
12882 // set evaluates to NULL, at any depth. Previously only a *top-level*
12883 // select item equal to a dropped key was nullified, so a key nested in
12884 // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
12885 // column and failed to resolve against the set's synthetic schema.
12886 if dropped.iter().any(|d| d == expr) {
12887 *expr = Expr::Literal(Literal::Null);
12888 return;
12889 }
12890 if let Expr::FunctionCall { name, args } = expr
12891 && name.eq_ignore_ascii_case("grouping")
12892 {
12893 let mut mask: i64 = 0;
12894 for a in args.iter() {
12895 mask <<= 1;
12896 if dropped.iter().any(|d| d == a) {
12897 mask |= 1;
12898 }
12899 }
12900 // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
12901 // literal: a bare integer in a select item is indistinguishable
12902 // from a positional reference once `ORDER BY 1` substitutes the
12903 // item back in, and the round-232 position check then read the
12904 // mask value as an out-of-range position. The cast changes
12905 // nothing semantically (grouping() is integer).
12906 *expr = Expr::Cast {
12907 expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
12908 target: crate::ast::CastTarget::Int,
12909 };
12910 return;
12911 }
12912 // Generic recursion over the common expression shapes the
12913 // SELECT list uses; anything without child expressions is
12914 // left alone.
12915 match expr {
12916 Expr::FunctionCall { args, .. } => {
12917 for a in args {
12918 Self::substitute_grouping_calls(a, dropped);
12919 }
12920 }
12921 Expr::Binary { lhs, rhs, .. } => {
12922 Self::substitute_grouping_calls(lhs, dropped);
12923 Self::substitute_grouping_calls(rhs, dropped);
12924 }
12925 Expr::Unary { expr: inner, .. } => {
12926 Self::substitute_grouping_calls(inner, dropped);
12927 }
12928 Expr::Cast { expr: inner, .. } => {
12929 Self::substitute_grouping_calls(inner, dropped);
12930 }
12931 Expr::Case {
12932 operand,
12933 branches,
12934 else_branch,
12935 } => {
12936 if let Some(op) = operand {
12937 Self::substitute_grouping_calls(op, dropped);
12938 }
12939 for (w, t) in branches {
12940 Self::substitute_grouping_calls(w, dropped);
12941 Self::substitute_grouping_calls(t, dropped);
12942 }
12943 if let Some(e) = else_branch {
12944 Self::substitute_grouping_calls(e, dropped);
12945 }
12946 }
12947 // v7.38 (read01) — recurse into the remaining child-bearing shapes
12948 // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
12949 // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
12950 // …` is the canonical rollup-total label idiom).
12951 Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
12952 Expr::Like { expr, pattern, .. } => {
12953 Self::substitute_grouping_calls(expr, dropped);
12954 Self::substitute_grouping_calls(pattern, dropped);
12955 }
12956 Expr::InList { expr, list, .. } => {
12957 Self::substitute_grouping_calls(expr, dropped);
12958 for item in list {
12959 Self::substitute_grouping_calls(item, dropped);
12960 }
12961 }
12962 Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
12963 Expr::Array(items) => {
12964 for item in items {
12965 Self::substitute_grouping_calls(item, dropped);
12966 }
12967 }
12968 Expr::ArraySubscript { target, index } => {
12969 Self::substitute_grouping_calls(target, dropped);
12970 Self::substitute_grouping_calls(index, dropped);
12971 }
12972 Expr::ArraySlice { target, lo, hi } => {
12973 Self::substitute_grouping_calls(target, dropped);
12974 if let Some(lo) = lo {
12975 Self::substitute_grouping_calls(lo, dropped);
12976 }
12977 if let Some(hi) = hi {
12978 Self::substitute_grouping_calls(hi, dropped);
12979 }
12980 }
12981 Expr::AnyAll { expr, array, .. } => {
12982 Self::substitute_grouping_calls(expr, dropped);
12983 Self::substitute_grouping_calls(array, dropped);
12984 }
12985 _ => {}
12986 }
12987 }
12988
12989 fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
12990 // v7.37.17 (17.6 siblings) — parenthesized set-operation
12991 // group: `( <select chain> )` usable anywhere a query block
12992 // is (head or peer of an outer chain). The group's own
12993 // unions ride the returned SelectStatement; the executor's
12994 // nested-peer recursion runs them.
12995 if matches!(self.peek(), Token::LParen)
12996 && matches!(
12997 self.tokens.get(self.pos + 1),
12998 Some(Token::Select | Token::LParen | Token::Values)
12999 )
13000 {
13001 self.advance(); // (
13002 self.enter_nested()?;
13003 // v7.37 D.20 — a group whose head is a VALUES list:
13004 // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13005 // otherwise recurse into a nested SELECT/group head.
13006 let mut head = (if matches!(self.peek(), Token::Values) {
13007 self.advance(); // VALUES
13008 self.parse_values_rows_body()
13009 } else {
13010 self.parse_bare_select()
13011 })
13012 .and_then(|mut h| {
13013 self.parse_setop_chain_into(&mut h)?;
13014 Ok(h)
13015 });
13016 self.nest_depth -= 1;
13017 let mut head = match &mut head {
13018 Ok(h) => core::mem::take(h),
13019 Err(_) => return head,
13020 };
13021 // v7.37.17 (17.6 siblings) — group-internal tail:
13022 // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13023 // group head, then wrap the group as a derived table
13024 // (SELECT * FROM (group)) so the outer chain / outer
13025 // tail can't clobber the group's own ordering or limit.
13026 let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13027 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13028 if s.eq_ignore_ascii_case("fetch"));
13029 if has_tail {
13030 self.parse_select_tail_into(&mut head)?;
13031 head = SelectStatement {
13032 locking: None,
13033 ctes: Vec::new(),
13034 distinct: false,
13035 distinct_on: Vec::new(),
13036 items: alloc::vec![SelectItem::Wildcard],
13037 from: Some(FromClause {
13038 primary: TableRef {
13039 name: "subquery".to_string(),
13040 alias: None,
13041 only: false,
13042 as_of_segment: None,
13043 unnest_expr: None,
13044 unnest_column_aliases: Vec::new(),
13045 with_ordinality: false,
13046 generate_series_args: None,
13047 lateral_subquery: Some(Box::new(head)),
13048 jsonb_each_text_arg: None,
13049 table_fn_call: None,
13050 rows_from: None,
13051 json_table: None,
13052 scalar_fn_item: false,
13053 },
13054 joins: Vec::new(),
13055 }),
13056 where_: None,
13057 group_by: None,
13058 group_by_all: false,
13059 having: None,
13060 unions: Vec::new(),
13061 order_by: Vec::new(),
13062 limit: None,
13063 offset: None,
13064 limit_with_ties: false,
13065 window_check_exprs: Vec::new(),
13066 };
13067 }
13068 if !matches!(self.peek(), Token::RParen) {
13069 return Err(self.err(format!(
13070 "expected ')' after parenthesized query group, got {:?}",
13071 self.peek()
13072 )));
13073 }
13074 self.advance();
13075 return Ok(head);
13076 }
13077 // `TABLE name` shorthand as a query block — valid anywhere
13078 // a SELECT head is (set-op peers included).
13079 if matches!(self.peek(), Token::Table)
13080 && matches!(
13081 self.tokens.get(self.pos + 1),
13082 Some(Token::Ident(_) | Token::QuotedIdent(_))
13083 )
13084 {
13085 return self.parse_table_shorthand();
13086 }
13087 if !matches!(self.peek(), Token::Select) {
13088 return Err(self.err(format!(
13089 "expected SELECT to start a query block, got {:?}",
13090 self.peek()
13091 )));
13092 }
13093 self.advance();
13094 let distinct = if matches!(self.peek(), Token::Distinct) {
13095 self.advance();
13096 true
13097 } else {
13098 false
13099 };
13100 // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13101 // keep the first row (per ORDER BY) of each group the
13102 // expressions define. Django's .distinct('field') shape.
13103 let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13104 self.advance(); // ON
13105 if !matches!(self.peek(), Token::LParen) {
13106 return Err(self.err(format!(
13107 "expected '(' after DISTINCT ON, got {:?}",
13108 self.peek()
13109 )));
13110 }
13111 self.advance();
13112 let mut exprs = Vec::new();
13113 loop {
13114 exprs.push(self.parse_expr(0)?);
13115 match self.peek() {
13116 Token::Comma => {
13117 self.advance();
13118 }
13119 Token::RParen => break,
13120 other => {
13121 return Err(self.err(format!(
13122 "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13123 )));
13124 }
13125 }
13126 }
13127 self.advance(); // )
13128 exprs
13129 } else {
13130 Vec::new()
13131 };
13132 let mut items = self.parse_select_list()?;
13133 // Scope the TABLESAMPLE lowering channel to this SELECT:
13134 // stash whatever an enclosing select accumulated, collect
13135 // our own FROM's predicates, restore after the combine.
13136 let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13137 let mut from = if matches!(self.peek(), Token::From) {
13138 self.advance();
13139 Some(self.parse_from_clause()?)
13140 } else {
13141 None
13142 };
13143 // v7.37 D.22 — a set-returning function in the projection with no FROM
13144 // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13145 // rows. Move the first SRF projection item to a FROM-position derived
13146 // table and replace it in the projection with a reference to its output
13147 // column; sibling scalar columns repeat per SRF row. PG names the output
13148 // column after the function (or its AS alias). Reuses the FROM-SRF
13149 // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13150 // works via the targetlist-SRF path.
13151 // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13152 // `SELECT * FROM f(args)` — the record's fields become the columns, which
13153 // is exactly what the function's own row shape already is. Anywhere else
13154 // (per outer row, or beside other items) it would need a real record-typed
13155 // projection, so it says so rather than answering something else.
13156 if let [
13157 SelectItem::Expr {
13158 expr: Expr::FunctionCall { name, args },
13159 ..
13160 },
13161 ] = items.as_slice()
13162 && name == "__record_expand"
13163 {
13164 let Some(Expr::FunctionCall {
13165 name: inner_name,
13166 args: inner_args,
13167 }) = args.first()
13168 else {
13169 return Err(self.err(
13170 "(<expr>).* expands a function's record — it needs a function call".into(),
13171 ));
13172 };
13173 if from.is_some() {
13174 return Err(self.err(
13175 "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13176 .into(),
13177 ));
13178 }
13179 let fn_ref = TableRef {
13180 name: inner_name.clone(),
13181 alias: None,
13182 only: false,
13183 as_of_segment: None,
13184 unnest_expr: None,
13185 unnest_column_aliases: Vec::new(),
13186 with_ordinality: false,
13187 generate_series_args: None,
13188 lateral_subquery: None,
13189 jsonb_each_text_arg: None,
13190 table_fn_call: Some(Box::new((
13191 inner_name.to_ascii_lowercase(),
13192 inner_args.clone(),
13193 ))),
13194 rows_from: None,
13195 json_table: None,
13196 scalar_fn_item: false,
13197 };
13198 items = alloc::vec![SelectItem::Wildcard];
13199 from = Some(FromClause {
13200 primary: fn_ref,
13201 joins: Vec::new(),
13202 });
13203 }
13204 // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13205 // FROM, keeps its marker: the ENGINE lowers it, because naming the
13206 // record's fields takes the catalog. It becomes a LATERAL of the same
13207 // function plus one item per declared column — the machinery rounds 65
13208 // and 69 already built.
13209 // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13210 // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13211 // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13212 // express, since the lifted one becomes a scan and the other would
13213 // expand per its rows (a cross product, not a zip). So when the
13214 // projection holds more than one top-level function call, the lift steps
13215 // aside and the engine's target-list expansion takes the whole list.
13216 let fn_call_items = items
13217 .iter()
13218 .filter(|it| {
13219 matches!(
13220 it,
13221 SelectItem::Expr {
13222 expr: Expr::FunctionCall { .. },
13223 ..
13224 }
13225 )
13226 })
13227 .count();
13228 if from.is_none() && fn_call_items <= 1 {
13229 let mut found: Option<(usize, TableRef, String)> = None;
13230 for (i, item) in items.iter().enumerate() {
13231 if let SelectItem::Expr {
13232 expr: Expr::FunctionCall { name, args },
13233 alias,
13234 } = item
13235 {
13236 let lname = name.to_ascii_lowercase();
13237 let colname = alias.clone().unwrap_or_else(|| lname.clone());
13238 let (unnest, gs) = match lname.as_str() {
13239 "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13240 "generate_series" if (2..=3).contains(&args.len()) => {
13241 (None, Some(args.clone()))
13242 }
13243 // v7.38 (read01) — generate_subscripts(arr, dim) in a
13244 // no-FROM projection yields the 1-based subscripts, i.e.
13245 // generate_series(1, array_length(arr, dim)); an invalid
13246 // dimension makes array_length NULL → 0 rows, as in PG.
13247 "generate_subscripts" if args.len() == 2 => (
13248 None,
13249 Some(alloc::vec![
13250 Expr::Literal(Literal::Integer(1)),
13251 Expr::FunctionCall {
13252 name: "array_length".to_string(),
13253 args: args.clone(),
13254 },
13255 ]),
13256 ),
13257 // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13258 // in a no-FROM projection unnest their *_to_array form.
13259 "string_to_table" | "regexp_split_to_table" => {
13260 let array_fn = if lname == "string_to_table" {
13261 "string_to_array"
13262 } else {
13263 "regexp_split_to_array"
13264 };
13265 (
13266 Some(Box::new(Expr::FunctionCall {
13267 name: array_fn.to_string(),
13268 args: args.clone(),
13269 })),
13270 None,
13271 )
13272 }
13273 // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13274 // a no-FROM projection expand per element. The scalar form
13275 // returns the elements as a TEXT array, so unnest over the
13276 // same call materialises one row each (same rewrite the
13277 // FROM-clause form uses).
13278 "jsonb_array_elements"
13279 | "json_array_elements"
13280 | "jsonb_array_elements_text"
13281 | "json_array_elements_text"
13282 if args.len() == 1 =>
13283 {
13284 (
13285 Some(Box::new(Expr::FunctionCall {
13286 name: lname.clone(),
13287 args: args.clone(),
13288 })),
13289 None,
13290 )
13291 }
13292 // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13293 // in a no-FROM projection expands per match (scalar form
13294 // returns the matches as a TEXT array → unnest).
13295 "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13296 Some(Box::new(Expr::FunctionCall {
13297 name: lname.clone(),
13298 args: args.clone(),
13299 })),
13300 None,
13301 ),
13302 _ => continue,
13303 };
13304 found = Some((
13305 i,
13306 TableRef {
13307 name: colname.clone(),
13308 alias: Some(colname.clone()),
13309 only: false,
13310 as_of_segment: None,
13311 unnest_expr: unnest,
13312 unnest_column_aliases: alloc::vec![colname.clone()],
13313 with_ordinality: false,
13314 generate_series_args: gs,
13315 lateral_subquery: None,
13316 jsonb_each_text_arg: None,
13317 table_fn_call: None,
13318 rows_from: None,
13319 json_table: None,
13320 scalar_fn_item: false,
13321 },
13322 colname,
13323 ));
13324 break;
13325 }
13326 }
13327 if let Some((idx, tref, colname)) = found {
13328 from = Some(FromClause {
13329 primary: tref,
13330 joins: Vec::new(),
13331 });
13332 items[idx] = SelectItem::Expr {
13333 expr: Expr::Column(ColumnName {
13334 qualifier: None,
13335 name: colname.clone(),
13336 }),
13337 alias: Some(colname),
13338 };
13339 }
13340 }
13341 let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13342 let where_ = if matches!(self.peek(), Token::Where) {
13343 self.advance();
13344 Some(self.parse_expr(0)?)
13345 } else {
13346 None
13347 };
13348 let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13349 Some(match acc {
13350 Some(w) => Expr::Binary {
13351 lhs: Box::new(pred),
13352 op: crate::ast::BinOp::And,
13353 rhs: Box::new(w),
13354 },
13355 None => pred,
13356 })
13357 });
13358 self.pending_sample_preds = enclosing_sample_preds;
13359 let mut group_by_all = false;
13360 // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13361 // share one expansion: `grouping_sets` lists the key subsets
13362 // (first = primary, assigned to stmt.group_by; the rest
13363 // become UNION ALL peers), `grouping_universe` is the full
13364 // key list used to compute each peer's dropped keys.
13365 let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13366 let mut grouping_universe: Vec<Expr> = Vec::new();
13367 // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13368 // A BOOL, not the key list: this frame is the statement parser's, and
13369 // round 430 measured that a `Vec` local here is enough on its own to
13370 // tip the 512 KiB nesting guard. The keys are recoverable from
13371 // `grouping_universe`, which a rollup fills with exactly them.
13372 let mut mysql_rollup = false;
13373 let group_by = if matches!(self.peek(), Token::Group) {
13374 self.advance();
13375 if !self.peek_is_by() {
13376 return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13377 }
13378 self.advance();
13379 // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13380 // every non-aggregate SELECT-list item later.
13381 if matches!(self.peek(), Token::All) {
13382 self.advance();
13383 group_by_all = true;
13384 None
13385 } else {
13386 // v7.39 (round 242) — PG's general grouping-element grammar:
13387 // GROUP BY [DISTINCT] element [, element]*, where an element
13388 // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13389 // SETS (…) — mixed freely. Each element yields a list of
13390 // key sets; the query's grouping sets are the CARTESIAN
13391 // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13392 // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13393 // content. ROLLUP/CUBE members may be composite
13394 // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13395 // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13396 // parser handled only a lone ROLLUP/CUBE/GS as the whole
13397 // clause.
13398 let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13399 self.advance();
13400 true
13401 } else {
13402 false
13403 };
13404 let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13405 loop {
13406 element_sets.push(self.parse_grouping_element()?);
13407 if matches!(self.peek(), Token::Comma) {
13408 self.advance();
13409 } else {
13410 break;
13411 }
13412 }
13413 let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13414 for el in &element_sets {
13415 let mut next: Vec<Vec<Expr>> = Vec::new();
13416 for base in &total {
13417 for set in el {
13418 let mut merged = base.clone();
13419 for k in set {
13420 if !merged.iter().any(|m| m == k) {
13421 merged.push(k.clone());
13422 }
13423 }
13424 next.push(merged);
13425 }
13426 }
13427 total = next;
13428 }
13429 // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13430 // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13431 // The keys and the aggregates come out identical; the ROW
13432 // ORDER does not, and that is the part a report depends on.
13433 // MySQL interleaves each group's subtotal right after its
13434 // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13435 // where the union-of-grouping-sets expansion emits every
13436 // leaf first and then every subtotal. MariaDB REFUSES an
13437 // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13438 // order itself — measured on MariaDB 11 and MySQL 9.7, which
13439 // agree on the order and disagree only on whether ORDER BY
13440 // is allowed (MySQL allows it; SPG allows it too, since
13441 // refusing would break the clients that can write it).
13442 if self.mysql_dialect
13443 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13444 && matches!(
13445 self.tokens.get(self.pos + 1),
13446 Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13447 )
13448 {
13449 self.advance(); // WITH
13450 self.advance(); // ROLLUP
13451 let keys = total.into_iter().next().unwrap_or_default();
13452 mysql_rollup = true;
13453 // n+1 prefixes, largest first — the same expansion
13454 // `ROLLUP (…)` produces.
13455 total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13456 }
13457 if distinct_sets {
13458 let mut seen: Vec<Vec<String>> = Vec::new();
13459 total.retain(|set| {
13460 let mut key: Vec<String> =
13461 set.iter().map(|e| alloc::format!("{e}")).collect();
13462 key.sort();
13463 if seen.contains(&key) {
13464 false
13465 } else {
13466 seen.push(key);
13467 true
13468 }
13469 });
13470 }
13471 if total.len() > 1 {
13472 let mut universe: Vec<Expr> = Vec::new();
13473 for set in &total {
13474 for k in set {
13475 if !universe.iter().any(|u| u == k) {
13476 universe.push(k.clone());
13477 }
13478 }
13479 }
13480 grouping_universe = universe;
13481 let primary = total[0].clone();
13482 grouping_sets = total;
13483 Some(primary)
13484 } else {
13485 // One set (a plain GROUP BY list, or a single-set
13486 // spelling like GROUPING SETS ((a, b))). An EMPTY
13487 // single set — GROUPING SETS (()) — stays
13488 // `Some(vec![])`: the grand-total group, which must
13489 // run the aggregate path.
13490 Some(total.into_iter().next().unwrap_or_default())
13491 }
13492 }
13493 } else {
13494 None
13495 };
13496 let having = if matches!(self.peek(), Token::Having) {
13497 self.advance();
13498 Some(self.parse_expr(0)?)
13499 } else {
13500 None
13501 };
13502 // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
13503 // OVER w parsed to a marker above; inline each definition
13504 // into the referencing WindowFunction nodes.
13505 let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
13506 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
13507 self.advance();
13508 loop {
13509 let wname = self.expect_ident_like()?;
13510 if !matches!(self.peek(), Token::As) {
13511 return Err(self.err(format!(
13512 "expected AS after WINDOW {wname}, got {:?}",
13513 self.peek()
13514 )));
13515 }
13516 self.advance();
13517 // v7.39 (round 229) — PG rejects a redefinition outright.
13518 if window_defs
13519 .iter()
13520 .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
13521 {
13522 return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
13523 }
13524 let def = self.parse_over_clause()?;
13525 // A definition may itself copy an earlier one
13526 // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
13527 // so resolve it against the defs already in scope. Same
13528 // copy rules as an `OVER (w1 …)` in the select list.
13529 let mut probe = Expr::WindowFunction {
13530 name: String::new(),
13531 args: Vec::new(),
13532 partition_by: def.0,
13533 order_by: def.1,
13534 frame: def.2,
13535 null_treatment: crate::ast::NullTreatment::Respect,
13536 filter: None,
13537 };
13538 Self::substitute_named_windows(&mut probe, &window_defs)
13539 .map_err(|m| self.err(m))?;
13540 let Expr::WindowFunction {
13541 partition_by,
13542 order_by,
13543 frame,
13544 ..
13545 } = probe
13546 else {
13547 unreachable!("probe is a WindowFunction")
13548 };
13549 window_defs.push((wname, (partition_by, order_by, frame)));
13550 if matches!(self.peek(), Token::Comma) {
13551 self.advance();
13552 continue;
13553 }
13554 break;
13555 }
13556 }
13557 // v7.39 (round 705) — which definitions did anything reference?
13558 // The ones nothing did used to be dropped here, unexamined, so
13559 // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
13560 // definition whether referenced or not. Their key expressions ride
13561 // out on the statement for the engine to resolve.
13562 let mut window_refs: Vec<String> = Vec::new();
13563 if !window_defs.is_empty() {
13564 for it in &items {
13565 if let SelectItem::Expr { expr, .. } = it {
13566 Self::collect_named_window_refs(expr, &mut window_refs);
13567 }
13568 }
13569 }
13570 let window_check_exprs: Vec<Expr> = window_defs
13571 .iter()
13572 .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
13573 .flat_map(|(_, (partition, order, _))| {
13574 partition
13575 .iter()
13576 .cloned()
13577 .chain(order.iter().map(|(e, _, _)| e.clone()))
13578 })
13579 .collect();
13580 if !window_defs.is_empty()
13581 || items
13582 .iter()
13583 .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
13584 {
13585 for it in &mut items {
13586 if let SelectItem::Expr { expr, .. } = it {
13587 Self::substitute_named_windows(expr, &window_defs)
13588 .map_err(|m| self.err(m))?;
13589 }
13590 }
13591 }
13592 // `GROUP BY 1` — positional keys substitute with the Nth
13593 // select item's expression (same contract ORDER BY has had
13594 // since v6.x). Out-of-range positions error.
13595 let group_by = match group_by {
13596 Some(mut keys) => {
13597 for k in &mut keys {
13598 if let Expr::Literal(Literal::Integer(n)) = k {
13599 let idx = *n;
13600 if idx < 1 || idx as usize > items.len() {
13601 return Err(self.err(alloc::format!(
13602 "GROUP BY position {idx} is not in select list"
13603 )));
13604 }
13605 match &items[(idx - 1) as usize] {
13606 SelectItem::Expr { expr, .. } => *k = expr.clone(),
13607 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
13608 return Err(self.err(alloc::format!(
13609 "GROUP BY position {idx} references a wildcard item"
13610 )));
13611 }
13612 }
13613 }
13614 }
13615 Some(keys)
13616 }
13617 None => None,
13618 };
13619 let mut stmt = SelectStatement {
13620 locking: None,
13621 ctes: Vec::new(),
13622 distinct,
13623 distinct_on,
13624 items,
13625 from,
13626 where_,
13627 group_by,
13628 group_by_all,
13629 having,
13630 unions: Vec::new(),
13631 order_by: Vec::new(),
13632 limit: None,
13633 offset: None,
13634 limit_with_ties: false,
13635 window_check_exprs,
13636 };
13637 // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
13638 // first set is the primary (already on stmt.group_by); each
13639 // further set becomes a UNION ALL peer with its dropped
13640 // keys (universe minus the set) replaced by NULL literals
13641 // in the peer's items and group_by. PG-legal: non-grouped
13642 // select items must be group keys or aggregates, so a
13643 // dropped key's occurrences in the projection are exactly
13644 // the ones to nullify.
13645 // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
13646 // over a plain GROUP BY (every argument must be a group key; the
13647 // mask is then 0) and rejects anything else with 42803. SPG's
13648 // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
13649 // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
13650 // function `grouping`".
13651 if grouping_sets.len() <= 1 {
13652 let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
13653 let mut calls: Vec<Expr> = Vec::new();
13654 for item in &stmt.items {
13655 if let SelectItem::Expr { expr, .. } = item {
13656 Self::collect_grouping_calls(expr, &mut calls);
13657 }
13658 }
13659 if let Some(h) = &stmt.having {
13660 Self::collect_grouping_calls(h, &mut calls);
13661 }
13662 for call in &calls {
13663 let Expr::FunctionCall { args, .. } = call else {
13664 continue;
13665 };
13666 for a in args {
13667 if !keys.iter().any(|k| k == a) {
13668 return Err(self.err(
13669 "arguments to GROUPING must be grouping expressions of the associated query level"
13670 .to_string(),
13671 ));
13672 }
13673 }
13674 }
13675 if !calls.is_empty() {
13676 for item in &mut stmt.items {
13677 if let SelectItem::Expr { expr, .. } = item {
13678 Self::substitute_grouping_calls(expr, &[]);
13679 }
13680 }
13681 if let Some(h) = &mut stmt.having {
13682 Self::substitute_grouping_calls(h, &[]);
13683 }
13684 }
13685 }
13686 if grouping_sets.len() > 1 {
13687 // The primary set's own dropped keys nullify in the
13688 // HEAD's projection too (GROUPING SETS's first set may
13689 // omit keys other sets use).
13690 let primary = grouping_sets[0].clone();
13691 let head_dropped: Vec<Expr> = grouping_universe
13692 .iter()
13693 .filter(|u| !primary.iter().any(|k| k == *u))
13694 .cloned()
13695 .collect();
13696 for set in grouping_sets.iter().skip(1) {
13697 let mut peer = stmt.clone();
13698 peer.unions = Vec::new();
13699 let dropped: Vec<&Expr> = grouping_universe
13700 .iter()
13701 .filter(|u| !set.iter().any(|k| k == *u))
13702 .collect();
13703 // Empty set = grand-total group: `Some(vec![])` forces
13704 // the aggregate path (one collapsed row) instead of a
13705 // per-row passthrough. See the primary-set note above.
13706 peer.group_by = Some(set.clone());
13707 let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
13708 for item in &mut peer.items {
13709 if let SelectItem::Expr { expr, alias } = item {
13710 if dropped.iter().any(|d| *d == expr) {
13711 // v7.39 — keep the dropped key's name on the
13712 // NULL literal so the UNION output column
13713 // (and any top-level ORDER BY on it) still
13714 // resolves.
13715 if alias.is_none()
13716 && let Expr::Column(c) = &expr
13717 {
13718 *alias = Some(c.name.clone());
13719 }
13720 *expr = Expr::Literal(Literal::Null);
13721 } else {
13722 Self::substitute_grouping_calls(expr, &dropped_owned);
13723 }
13724 }
13725 }
13726 if let Some(h) = &mut peer.having {
13727 Self::substitute_grouping_calls(h, &dropped_owned);
13728 }
13729 stmt.unions.push((UnionKind::All, peer));
13730 }
13731 for item in &mut stmt.items {
13732 if let SelectItem::Expr { expr, alias } = item {
13733 if head_dropped.iter().any(|d| d == expr) {
13734 if alias.is_none()
13735 && let Expr::Column(c) = &expr
13736 {
13737 *alias = Some(c.name.clone());
13738 }
13739 *expr = Expr::Literal(Literal::Null);
13740 } else {
13741 Self::substitute_grouping_calls(expr, &head_dropped);
13742 }
13743 }
13744 }
13745 if let Some(h) = &mut stmt.having {
13746 Self::substitute_grouping_calls(h, &head_dropped);
13747 }
13748 // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
13749 // (while `grouping_universe` / the per-branch sets are in scope). For
13750 // each grouping() call in it, inject a per-branch hidden column
13751 // `__grp_ord_K` carrying that branch's mask into the head + every
13752 // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
13753 // preserves this pre-set order_by; the engine strips `__grp_ord_*`
13754 // from the final output. A standalone grouping-set query has ORDER BY
13755 // (not an explicit set-op) next, so consuming it here is safe.
13756 // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
13757 // rollup carries the hierarchical order: sort by the grouping
13758 // keys with the rolled-up NULLs last, which is exactly the
13759 // interleaving both oracles emit. A client's own ORDER BY wins,
13760 // which is what MySQL does (MariaDB refuses to let one be
13761 // written at all).
13762 // The synthesised keys have to travel the SAME path a written
13763 // ORDER BY does: the block below is what turns a `grouping()`
13764 // call into the per-branch `__grp_ord_K` column the engine can
13765 // actually sort on. Bypassing it left a bare `grouping(text)`
13766 // for the evaluator to reject.
13767 let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
13768 self.parse_order_by_keys()?
13769 } else if mysql_rollup {
13770 Self::mysql_rollup_order(&grouping_universe)
13771 } else {
13772 Vec::new()
13773 };
13774 if !synthesised_or_parsed.is_empty() {
13775 let mut order_keys = synthesised_or_parsed;
13776 let mut grp_exprs: Vec<Expr> = Vec::new();
13777 for ob in &order_keys {
13778 Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
13779 }
13780 for (k, gexpr) in grp_exprs.iter().enumerate() {
13781 let colname = alloc::format!("__grp_ord_{k}");
13782 // Head branch (primary set) uses `head_dropped`.
13783 let mut he = gexpr.clone();
13784 Self::substitute_grouping_calls(&mut he, &head_dropped);
13785 stmt.items.push(SelectItem::Expr {
13786 expr: he,
13787 alias: Some(colname.clone()),
13788 });
13789 // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
13790 for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
13791 let set = &grouping_sets[i + 1];
13792 let dropped: Vec<Expr> = grouping_universe
13793 .iter()
13794 .filter(|u| !set.iter().any(|k| k == *u))
13795 .cloned()
13796 .collect();
13797 let mut pe = gexpr.clone();
13798 Self::substitute_grouping_calls(&mut pe, &dropped);
13799 peer.items.push(SelectItem::Expr {
13800 expr: pe,
13801 alias: Some(colname.clone()),
13802 });
13803 }
13804 }
13805 for ob in &mut order_keys {
13806 Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
13807 }
13808 stmt.order_by = order_keys;
13809 }
13810 }
13811 Ok(stmt)
13812 }
13813
13814 /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
13815 /// as ORDER BY keys.
13816 ///
13817 /// Per key: the rollup marker, then the key. Sorting on the key alone
13818 /// is not enough, and a table with a NULL in it says why — MariaDB puts
13819 /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
13820 /// the ROLLUP-introduced NULL last, and both print as NULL.
13821 /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
13822 /// real group including the data-NULL one, 1 only for the row the
13823 /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
13824 /// rolls up to NULL|2, a|1, b|3, NULL|6.
13825 ///
13826 /// `#[inline(never)]`: its locals must not join the statement parser's
13827 /// frame, which round 430 measured sitting against the nesting guard.
13828 #[inline(never)]
13829 fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
13830 let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
13831 for e in keys {
13832 out.push(OrderBy {
13833 expr: Expr::FunctionCall {
13834 name: "grouping".into(),
13835 args: alloc::vec![e.clone()],
13836 },
13837 desc: false,
13838 nulls_first: None,
13839 collation: None,
13840 });
13841 out.push(OrderBy {
13842 expr: e.clone(),
13843 desc: false,
13844 // MySQL orders NULL first on an ascending key.
13845 nulls_first: Some(true),
13846 collation: None,
13847 });
13848 }
13849 out
13850 }
13851
13852 /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
13853 /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
13854 #[inline(never)]
13855 fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
13856 use crate::ast::MaintainKind;
13857 self.skip_paren_option_list();
13858 let kind = match self.peek() {
13859 // `TABLE` and `INDEX` lex as keywords, not identifiers.
13860 Token::Table | Token::Index => {
13861 self.advance();
13862 MaintainKind::ReindexRelation
13863 }
13864 Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
13865 "index" | "table" => {
13866 self.advance();
13867 MaintainKind::ReindexRelation
13868 }
13869 "schema" => {
13870 self.advance();
13871 MaintainKind::ReindexSchema
13872 }
13873 "system" | "database" => {
13874 self.advance();
13875 MaintainKind::Whole
13876 }
13877 // PG requires the object type; anything else is the
13878 // caller's problem, not something to swallow.
13879 _ => MaintainKind::ReindexRelation,
13880 },
13881 _ => MaintainKind::Whole,
13882 };
13883 // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
13884 // allows the plain form, so the modifier is recorded rather than
13885 // skipped. It still has no effect on how the reindex runs.
13886 let mut concurrently = false;
13887 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
13888 self.advance();
13889 concurrently = true;
13890 }
13891 let target = self.take_optional_maintain_name();
13892 self.consume_until_statement_boundary();
13893 Ok(Statement::Maintain {
13894 kind,
13895 concurrently,
13896 target,
13897 })
13898 }
13899
13900 /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
13901 /// and `CLUSTER [VERBOSE] <index> ON <table>`.
13902 #[inline(never)]
13903 fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
13904 use crate::ast::MaintainKind;
13905 self.skip_paren_option_list();
13906 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
13907 self.advance();
13908 }
13909 let target = self.take_optional_maintain_name();
13910 self.consume_until_statement_boundary();
13911 Ok(Statement::Maintain {
13912 kind: if target.is_some() {
13913 MaintainKind::ClusterRelation
13914 } else {
13915 MaintainKind::Whole
13916 },
13917 // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
13918 // transaction block quite happily (measured).
13919 concurrently: false,
13920 target,
13921 })
13922 }
13923
13924 /// The next token as a relation / schema name, when there is one.
13925 fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
13926 match self.peek() {
13927 Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
13928 Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
13929 _ => None,
13930 },
13931 _ => None,
13932 }
13933 }
13934
13935 /// A parenthesised option list, absorbed.
13936 fn skip_paren_option_list(&mut self) {
13937 if !matches!(self.peek(), Token::LParen) {
13938 return;
13939 }
13940 let mut depth = 0usize;
13941 loop {
13942 match self.advance() {
13943 Token::LParen => depth += 1,
13944 Token::RParen => {
13945 depth -= 1;
13946 if depth == 0 {
13947 return;
13948 }
13949 }
13950 Token::Eof => return,
13951 _ => {}
13952 }
13953 }
13954 }
13955
13956 /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
13957 /// column list.
13958 ///
13959 /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
13960 /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
13961 /// / ALL. The three that describe physical storage have no meaning
13962 /// here, so they parse and change nothing rather than making a
13963 /// dump that mentions them fail to load.
13964 ///
13965 /// `#[inline(never)]` because the CREATE TABLE frame sits on the
13966 /// parse chain the nesting sentinel is tuned against.
13967 #[inline(never)]
13968 fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
13969 self.advance(); // LIKE
13970 let source = self.expect_ident_like()?;
13971 let mut options = crate::ast::LikeOptions::default();
13972 loop {
13973 let including = match self.peek() {
13974 Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
13975 Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
13976 _ => break,
13977 };
13978 self.advance();
13979 // `ALL` lexes as its own keyword, not an identifier.
13980 let opt = if matches!(self.peek(), Token::All) {
13981 self.advance();
13982 alloc::string::String::from("all")
13983 } else {
13984 self.expect_ident_like()?
13985 };
13986 let set = |o: &mut crate::ast::LikeOptions, on: bool| {
13987 o.defaults = on;
13988 o.constraints = on;
13989 o.identity = on;
13990 o.generated = on;
13991 o.indexes = on;
13992 o.comments = on;
13993 };
13994 match opt.to_ascii_lowercase().as_str() {
13995 "all" => set(&mut options, including),
13996 "defaults" => options.defaults = including,
13997 "constraints" => options.constraints = including,
13998 "identity" => options.identity = including,
13999 "generated" => options.generated = including,
14000 "indexes" => options.indexes = including,
14001 "comments" => options.comments = including,
14002 // No storage model to copy into.
14003 "storage" | "statistics" | "compression" => {}
14004 other => {
14005 return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14006 }
14007 }
14008 }
14009 Ok(crate::ast::LikeSpec {
14010 source,
14011 at,
14012 options,
14013 })
14014 }
14015
14016 fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14017 // Caller already consumed CREATE; we're sitting on TABLE.
14018 debug_assert!(matches!(self.peek(), Token::Table));
14019 self.advance();
14020 let if_not_exists = self.consume_if_not_exists();
14021 let name = self.expect_ident_like()?;
14022 // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14023 // child shape has no column list; the child inherits its
14024 // columns from the parent at engine-DDL time. Detect it
14025 // before the `(` requirement below.
14026 if matches!(self.peek(), Token::Partition)
14027 && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14028 {
14029 self.advance(); // PARTITION
14030 self.advance(); // of
14031 let partition_of = self.parse_partition_of_tail()?;
14032 return Ok(Statement::CreateTable(CreateTableStatement {
14033 temporary: false,
14034 name,
14035 columns: Vec::new(),
14036 like_specs: Vec::new(),
14037 inherits: Vec::new(),
14038 if_not_exists,
14039 foreign_keys: Vec::new(),
14040 table_constraints: Vec::new(),
14041 partition_by: None,
14042 partition_of: Some(partition_of),
14043 }));
14044 }
14045 // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14046 // the materialized-view materialisation path (run the SELECT, infer the
14047 // column types, create + populate the table) but marks the node so the
14048 // executor creates a plain table without a mat-view registry entry.
14049 if matches!(self.peek(), Token::As) {
14050 self.advance();
14051 let body_stmt = self.parse_select_stmt()?;
14052 let Statement::Select(body) = body_stmt else {
14053 return Err(self.err(format!(
14054 "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14055 )));
14056 };
14057 let with_data = self.parse_optional_with_data(true)?;
14058 return Ok(Statement::CreateMaterializedView(
14059 crate::ast::CreateMaterializedViewStatement {
14060 temporary: false,
14061 name,
14062 if_not_exists,
14063 columns: Vec::new(),
14064 body,
14065 with_data,
14066 as_plain_table: true,
14067 },
14068 ));
14069 }
14070 if !matches!(self.peek(), Token::LParen) {
14071 return Err(self.err(format!(
14072 "expected '(' after table name, got {:?}",
14073 self.peek()
14074 )));
14075 }
14076 self.advance();
14077 let mut columns = Vec::new();
14078 let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14079 let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14080 let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14081 loop {
14082 // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14083 // column list. It is how a child that adds nothing of its own is
14084 // written, and this loop demanded at least one entry: `syntax
14085 // error at or near ")"`. The child takes the parent's columns,
14086 // which the INHERITS clause already arranges.
14087 if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14088 self.advance();
14089 break;
14090 }
14091 // v7.6.0 / v7.9.18 — distinguish table-level constraint
14092 // clauses from column definitions. Constraints start
14093 // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14094 // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14095 // a column.
14096 if self.peek_table_level_pk_start() {
14097 table_constraints.push(self.parse_table_level_primary_key()?);
14098 } else if matches!(self.peek(), Token::Like) {
14099 // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14100 // <opt> ]*`. The source table's shape lives in the catalog,
14101 // so this records the clause and the engine expands it.
14102 like_specs.push(self.parse_create_table_like(columns.len())?);
14103 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14104 // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14105 table_constraints.push(self.parse_table_level_exclude()?);
14106 } else if self.peek_table_level_unique_start() {
14107 table_constraints.push(self.parse_table_level_unique()?);
14108 } else if self.peek_table_level_check_start() {
14109 // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14110 table_constraints.push(self.parse_table_level_check()?);
14111 } else if self.peek_mysql_inline_key_start() {
14112 // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14113 // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14114 // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14115 // inside the column list. Skip name + paren list;
14116 // for UNIQUE KEY, register as a UC.
14117 if let Some(uc) = self.parse_mysql_inline_key()? {
14118 table_constraints.push(uc);
14119 }
14120 } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14121 // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14122 // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14123 // CHECK is named, and the named-CONSTRAINT arm used
14124 // to accept FOREIGN KEY only. The name is accepted
14125 // and discarded — same handling as every other SPG
14126 // constraint name.
14127 self.advance(); // CONSTRAINT
14128 // v7.39 (read01 round 48) — the name is kept now: the schema
14129 // stores it, so DROP / RENAME CONSTRAINT can find it.
14130 let con_name = self.expect_ident_like()?;
14131 let mut tc = match kind {
14132 NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14133 NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14134 NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14135 NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14136 };
14137 match &mut tc {
14138 crate::ast::TableConstraint::Check { name, .. }
14139 | crate::ast::TableConstraint::Unique { name, .. }
14140 | crate::ast::TableConstraint::PrimaryKey { name, .. }
14141 | crate::ast::TableConstraint::Exclude { name, .. } => {
14142 *name = Some(con_name);
14143 }
14144 _ => {}
14145 }
14146 table_constraints.push(tc);
14147 } else if self.peek_constraint_or_fk_start() {
14148 foreign_keys.push(self.parse_table_level_fk()?);
14149 } else {
14150 let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14151 // v7.13.0 — fold inline UNIQUE / CHECK column
14152 // constraints into table-level entries so the
14153 // engine path stays uniform.
14154 if col.is_unique {
14155 table_constraints.push(crate::ast::TableConstraint::Unique {
14156 name: None,
14157 columns: alloc::vec![col.name.clone()],
14158 nulls_not_distinct: col.unique_nulls_not_distinct,
14159 deferrable: col.constraint_deferrable,
14160 initially_deferred: col.constraint_initially_deferred,
14161 });
14162 }
14163 if let Some(check_expr) = col.check.clone() {
14164 table_constraints.push(crate::ast::TableConstraint::Check {
14165 name: None,
14166 expr: check_expr,
14167 not_valid: false,
14168 });
14169 }
14170 columns.push(col);
14171 if let Some(fk) = col_level_fk {
14172 foreign_keys.push(fk);
14173 }
14174 }
14175 match self.peek() {
14176 Token::Comma => {
14177 self.advance();
14178 }
14179 Token::RParen => {
14180 self.advance();
14181 break;
14182 }
14183 other => {
14184 return Err(
14185 self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14186 );
14187 }
14188 }
14189 }
14190 // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14191 // `CREATE TABLE k (LIKE t)` is a complete definition even though
14192 // nothing is written between the parentheses.
14193 // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14194 // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14195 // empty parentheses were a parse error in their own right — quite apart
14196 // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14197 // SPG does not have (filed separately).
14198 let _ = &like_specs;
14199 // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14200 // It sits between the column list and the MySQL table options,
14201 // and it was a syntax error until this round.
14202 let mut inherits: Vec<String> = Vec::new();
14203 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14204 if k.eq_ignore_ascii_case("inherits"))
14205 {
14206 self.advance();
14207 if !matches!(self.peek(), Token::LParen) {
14208 return Err(self.err(alloc::format!(
14209 "expected ( after INHERITS, got {:?}",
14210 self.peek()
14211 )));
14212 }
14213 self.advance();
14214 loop {
14215 inherits.push(self.expect_ident_like()?);
14216 if matches!(self.peek(), Token::Comma) {
14217 self.advance();
14218 continue;
14219 }
14220 break;
14221 }
14222 if !matches!(self.peek(), Token::RParen) {
14223 return Err(self.err(alloc::format!(
14224 "expected ) closing INHERITS, got {:?}",
14225 self.peek()
14226 )));
14227 }
14228 self.advance();
14229 }
14230 // v7.14.0 — consume MySQL/MariaDB table options after the
14231 // closing `)`. mysqldump emits things like
14232 // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14233 // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14234 // SPG accepts all forms as no-ops (each option is
14235 // `<ident> [=] <ident-or-string>` separated by whitespace).
14236 self.consume_mysql_table_options();
14237 // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14238 // SPG has no per-table reloptions, so accept and ignore them so a
14239 // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14240 self.consume_with_reloptions();
14241 // v7.37.6-B — declarative-partition-parent suffix
14242 // (`PARTITION BY RANGE (key_col)`) sits after the column
14243 // list + MySQL table-options. v7.37.6-B only accepts RANGE
14244 // and locks the key column at one ident; the engine then
14245 // verifies the column type is TIMESTAMPTZ.
14246 let partition_by = if matches!(self.peek(), Token::Partition) {
14247 self.advance(); // PARTITION
14248 if !self.peek_is_by() {
14249 return Err(self.err(format!(
14250 "expected BY after PARTITION, got {:?}",
14251 self.peek()
14252 )));
14253 }
14254 self.advance();
14255 Some(self.parse_partition_by_tail()?)
14256 } else {
14257 None
14258 };
14259 Ok(Statement::CreateTable(CreateTableStatement {
14260 temporary: false,
14261 name,
14262 columns,
14263 like_specs,
14264 inherits,
14265 if_not_exists,
14266 foreign_keys,
14267 table_constraints,
14268 partition_by,
14269 partition_of: None,
14270 }))
14271 }
14272
14273 /// v7.37.6-B — case-insensitive ident match helper for the
14274 /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14275 /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14276 /// didn't burn a global keyword slot for each (see the
14277 /// `Token::Partition` doc-comment in `lexer.rs`).
14278 fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14279 matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14280 }
14281
14282 /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14283 /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14284 fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14285 use crate::ast::{PartitionBySpec, PartitionKindAst};
14286 let kind = match self.peek() {
14287 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14288 self.advance();
14289 PartitionKindAst::Range
14290 }
14291 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14292 self.advance();
14293 PartitionKindAst::List
14294 }
14295 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14296 self.advance();
14297 PartitionKindAst::Hash
14298 }
14299 other => {
14300 return Err(self.err(format!(
14301 "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14302 )));
14303 }
14304 };
14305 if !matches!(self.peek(), Token::LParen) {
14306 return Err(self.err(format!(
14307 "expected '(' after PARTITION BY <strategy>, got {:?}",
14308 self.peek()
14309 )));
14310 }
14311 self.advance();
14312 let mut key_columns = Vec::new();
14313 loop {
14314 key_columns.push(self.expect_ident_like()?);
14315 match self.peek() {
14316 Token::Comma => {
14317 self.advance();
14318 }
14319 Token::RParen => {
14320 self.advance();
14321 break;
14322 }
14323 other => {
14324 return Err(self.err(format!(
14325 "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14326 )));
14327 }
14328 }
14329 }
14330 if key_columns.is_empty() {
14331 return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14332 }
14333 Ok(PartitionBySpec { kind, key_columns })
14334 }
14335
14336 /// v7.37.6-B — after `PARTITION OF`, expect
14337 /// <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14338 /// or
14339 /// <parent> DEFAULT
14340 fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14341 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14342 let parent_name = self.expect_ident_like()?;
14343 // v7.37.6-B rejects an explicit column list — the child
14344 // inherits from the parent. mailrs round-7 taught us that
14345 // CREATE TABLE-side schema reconciliation hides drift, so
14346 // we surface this as a parse error rather than silently
14347 // ignoring user columns.
14348 if matches!(self.peek(), Token::LParen) {
14349 return Err(self.err(
14350 "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14351 at v7.37.6-B; the child inherits its columns from the parent"
14352 .to_string(),
14353 ));
14354 }
14355 let bounds = match self.peek() {
14356 Token::Default => {
14357 self.advance();
14358 PartitionOfBoundsAst::Default
14359 }
14360 Token::For => {
14361 self.advance();
14362 if !matches!(self.peek(), Token::Values) {
14363 return Err(
14364 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14365 );
14366 }
14367 self.advance();
14368 // WITH is not a reserved Token in the lexer — it lexes
14369 // as Token::Ident("with"). Disambiguate manually.
14370 let want_with = matches!(
14371 self.peek(),
14372 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14373 );
14374 if want_with {
14375 self.advance();
14376 if !matches!(self.peek(), Token::LParen) {
14377 return Err(self.err(format!(
14378 "expected '(' after FOR VALUES WITH, got {:?}",
14379 self.peek()
14380 )));
14381 }
14382 self.advance();
14383 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14384 loop {
14385 let key = self.expect_ident_like()?;
14386 let n = match self.peek().clone() {
14387 Token::Integer(v) if u32::try_from(v).is_ok() => {
14388 self.advance();
14389 v as u32
14390 }
14391 other => {
14392 return Err(self.err(format!(
14393 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14394 )));
14395 }
14396 };
14397 match key.to_ascii_uppercase().as_str() {
14398 "MODULUS" => modulus = Some(n),
14399 "REMAINDER" => remainder = Some(n),
14400 other => {
14401 return Err(self.err(format!(
14402 "FOR VALUES WITH: unknown key {other:?}; \
14403 expected MODULUS or REMAINDER"
14404 )));
14405 }
14406 }
14407 match self.peek() {
14408 Token::Comma => {
14409 self.advance();
14410 }
14411 Token::RParen => {
14412 self.advance();
14413 break;
14414 }
14415 other => {
14416 return Err(self.err(format!(
14417 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14418 )));
14419 }
14420 }
14421 }
14422 let modulus = modulus
14423 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14424 let remainder = remainder.ok_or_else(|| {
14425 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14426 })?;
14427 if modulus == 0 {
14428 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14429 }
14430 if remainder >= modulus {
14431 return Err(self.err(format!(
14432 "FOR VALUES WITH: REMAINDER ({remainder}) \
14433 must be < MODULUS ({modulus})"
14434 )));
14435 }
14436 PartitionOfBoundsAst::Hash { modulus, remainder }
14437 } else {
14438 match self.peek() {
14439 Token::From => {
14440 self.advance();
14441 let lower = Box::new(self.parse_partition_bound_expr()?);
14442 if !matches!(self.peek(), Token::To) {
14443 return Err(self.err(format!(
14444 "expected TO after FROM (...), got {:?}",
14445 self.peek()
14446 )));
14447 }
14448 self.advance();
14449 let upper = Box::new(self.parse_partition_bound_expr()?);
14450 PartitionOfBoundsAst::Range { lower, upper }
14451 }
14452 // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14453 Token::In => {
14454 self.advance();
14455 if !matches!(self.peek(), Token::LParen) {
14456 return Err(self.err(format!(
14457 "expected '(' after FOR VALUES IN, got {:?}",
14458 self.peek()
14459 )));
14460 }
14461 self.advance();
14462 let mut values = Vec::new();
14463 loop {
14464 values.push(self.parse_expr(0)?);
14465 match self.peek() {
14466 Token::Comma => {
14467 self.advance();
14468 }
14469 Token::RParen => {
14470 self.advance();
14471 break;
14472 }
14473 other => {
14474 return Err(self.err(format!(
14475 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
14476 )));
14477 }
14478 }
14479 }
14480 if values.is_empty() {
14481 return Err(self.err(
14482 "FOR VALUES IN requires at least one literal".to_string(),
14483 ));
14484 }
14485 PartitionOfBoundsAst::List { values }
14486 }
14487 other => {
14488 return Err(self.err(format!(
14489 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
14490 )));
14491 }
14492 }
14493 }
14494 }
14495 other => {
14496 return Err(self.err(format!(
14497 "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
14498 )));
14499 }
14500 };
14501 Ok(PartitionOfSpec {
14502 parent_name,
14503 bounds,
14504 })
14505 }
14506
14507 /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
14508 /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
14509 /// markers (no-arg builtins) so the engine resolves them
14510 /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
14511 fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
14512 if !matches!(self.peek(), Token::LParen) {
14513 return Err(self.err(format!(
14514 "expected '(' before partition bound, got {:?}",
14515 self.peek()
14516 )));
14517 }
14518 self.advance();
14519 let expr = match self.peek() {
14520 Token::Ident(s) | Token::QuotedIdent(s)
14521 if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
14522 {
14523 let name = s.to_ascii_uppercase();
14524 self.advance();
14525 crate::ast::Expr::FunctionCall {
14526 name,
14527 args: Vec::new(),
14528 }
14529 }
14530 _ => self.parse_expr(0)?,
14531 };
14532 if !matches!(self.peek(), Token::RParen) {
14533 return Err(self.err(format!(
14534 "expected ')' after partition bound, got {:?}",
14535 self.peek()
14536 )));
14537 }
14538 self.advance();
14539 Ok(expr)
14540 }
14541
14542 /// v7.14.0 — true when the next tokens look like an inline
14543 /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
14544 /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
14545 /// — each followed by an optional name + `(...)`. Critical:
14546 /// a column NAMED `key` / `index` (PG accepts as ident) must
14547 /// NOT be mistaken for the KEY constraint shape. We disambig
14548 /// by requiring the keyword to be followed by either `(` or
14549 /// `<ident> (`.
14550 fn peek_mysql_inline_key_start(&self) -> bool {
14551 let cur = self.peek();
14552 // Shapes:
14553 // KEY (cols)
14554 // KEY name (cols)
14555 // INDEX (cols)
14556 // INDEX name (cols)
14557 // UNIQUE KEY [name] (cols)
14558 // UNIQUE INDEX [name] (cols)
14559 // FULLTEXT [KEY|INDEX] [name] (cols)
14560 // SPATIAL [KEY|INDEX] [name] (cols)
14561 let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
14562 // tokens at skip = the position AFTER the index-form
14563 // keywords (KEY/INDEX) have been consumed.
14564 match self.tokens.get(skip) {
14565 Some(Token::LParen) => true,
14566 Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
14567 matches!(self.tokens.get(skip + 1), Some(Token::LParen))
14568 }
14569 _ => false,
14570 }
14571 };
14572 // `INDEX` lexes as Token::Index (reserved), not as
14573 // Token::Ident("index"). Both shapes count as a KEY/INDEX
14574 // start; the peek helper below handles either.
14575 let is_key_or_index_tok = |t: &Token| -> bool {
14576 matches!(t, Token::Index)
14577 || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
14578 };
14579 match cur {
14580 Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
14581 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14582 after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
14583 }
14584 Token::Ident(s)
14585 if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
14586 {
14587 let nxt = self.tokens.get(self.pos + 1);
14588 let after_after = if nxt.is_some_and(is_key_or_index_tok) {
14589 self.pos + 2
14590 } else {
14591 self.pos + 1
14592 };
14593 after_keyword_followed_by_paren_or_ident_paren(after_after)
14594 }
14595 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
14596 let nxt = self.tokens.get(self.pos + 1);
14597 if !nxt.is_some_and(is_key_or_index_tok) {
14598 return false;
14599 }
14600 after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
14601 }
14602 _ => false,
14603 }
14604 }
14605
14606 /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
14607 /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
14608 /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
14609 /// returns Some(TableConstraint::Index) so the engine builds
14610 /// a real BTree index on the leading column (mysqldump
14611 /// `KEY idx_posts_author (author_id)` shape).
14612 /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
14613 /// (the storage layer has no matching AM).
14614 fn parse_mysql_inline_key(
14615 &mut self,
14616 ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
14617 // Detect UNIQUE prefix.
14618 let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
14619 {
14620 self.advance();
14621 true
14622 } else {
14623 false
14624 };
14625 // Consume FULLTEXT / SPATIAL prefix and record which one
14626 // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
14627 // dedicated TableConstraint variant so the engine can
14628 // build a tsvector-GIN; SPATIAL still has no matching
14629 // AM, so it falls back to accept-as-no-op.
14630 let mut is_fulltext = false;
14631 let mut is_spatial = false;
14632 if let Token::Ident(s) = self.peek().clone() {
14633 if s.eq_ignore_ascii_case("fulltext") {
14634 self.advance();
14635 is_fulltext = true;
14636 } else if s.eq_ignore_ascii_case("spatial") {
14637 self.advance();
14638 is_spatial = true;
14639 }
14640 }
14641 // KEY / INDEX keyword. `INDEX` lexes as Token::Index
14642 // (reserved); accept either token shape.
14643 match self.peek() {
14644 Token::Index => {
14645 self.advance();
14646 }
14647 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14648 self.advance();
14649 }
14650 other => {
14651 return Err(self.err(alloc::format!(
14652 "expected KEY/INDEX in inline index declaration, got {other:?}"
14653 )));
14654 }
14655 }
14656 // Optional index name (an ident before the `(`).
14657 // v7.15.0 — capture the name when present so the engine
14658 // builds the secondary index under the user's chosen
14659 // name (matches mysqldump's `KEY idx_x (col)` shape).
14660 let mut idx_name: Option<String> = None;
14661 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
14662 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
14663 {
14664 if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
14665 idx_name = Some(s);
14666 }
14667 }
14668 // Optional `USING BTREE` / `USING HASH` (MySQL).
14669 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
14670 self.advance();
14671 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14672 self.advance();
14673 }
14674 }
14675 // Required column list `(col [, col]*)`.
14676 if !matches!(self.peek(), Token::LParen) {
14677 return Err(self.err(alloc::format!(
14678 "expected '(' in inline KEY/INDEX, got {:?}",
14679 self.peek()
14680 )));
14681 }
14682 self.advance();
14683 let mut cols: Vec<String> = Vec::new();
14684 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
14685 self.advance();
14686 cols.push(s);
14687 // Skip optional `(length)` per-column prefix.
14688 if matches!(self.peek(), Token::LParen) {
14689 let mut depth = 1usize;
14690 self.advance();
14691 while depth > 0 {
14692 match self.peek() {
14693 Token::LParen => depth += 1,
14694 Token::RParen => depth -= 1,
14695 Token::Eof => break,
14696 _ => {}
14697 }
14698 self.advance();
14699 }
14700 }
14701 // Skip optional ASC / DESC.
14702 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
14703 || matches!(self.peek(), Token::Asc | Token::Desc)
14704 {
14705 self.advance();
14706 }
14707 if matches!(self.peek(), Token::Comma) {
14708 self.advance();
14709 continue;
14710 }
14711 break;
14712 }
14713 if matches!(self.peek(), Token::RParen) {
14714 self.advance();
14715 }
14716 // Trailing options on the inline index — comment / etc.
14717 // Skip until comma or `)`.
14718 while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
14719 self.advance();
14720 }
14721 if cols.is_empty() {
14722 return Ok(None);
14723 }
14724 if is_unique {
14725 // Carry the captured idx_name on UNIQUE too so future
14726 // engine work can name the underlying BTree
14727 // accordingly; today the unique-constraint installer
14728 // synthesises the name itself, but Display round-trip
14729 // benefits from preserving it.
14730 Ok(Some(crate::ast::TableConstraint::Unique {
14731 name: idx_name,
14732 columns: cols,
14733 nulls_not_distinct: false,
14734 // MySQL inline UNIQUE KEY has no deferral vocabulary.
14735 deferrable: false,
14736 initially_deferred: false,
14737 }))
14738 } else if is_fulltext {
14739 // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
14740 // routes through `TableConstraint::FulltextIndex`;
14741 // the engine builds a tsvector-GIN over each named
14742 // column so MATCH AGAINST gets a real inverted
14743 // index instead of a silently-dropped declaration.
14744 Ok(Some(crate::ast::TableConstraint::FulltextIndex {
14745 name: idx_name,
14746 columns: cols,
14747 }))
14748 } else if is_spatial {
14749 // SPG has no native SPATIAL AM. Accept-as-no-op
14750 // (declaration is parsed, but no index is built).
14751 Ok(None)
14752 } else {
14753 // v7.15.0 — plain KEY / INDEX builds a real BTree
14754 // secondary index.
14755 Ok(Some(crate::ast::TableConstraint::Index {
14756 name: idx_name,
14757 columns: cols,
14758 }))
14759 }
14760 }
14761
14762 /// v7.14.0 — consume MySQL/MariaDB table-options tail after
14763 /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
14764 /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
14765 /// (in any order, separated by whitespace).
14766 /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
14767 /// storage-parameter clause on CREATE TABLE. SPG has no per-table
14768 /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
14769 /// bare ident here, and only the parenthesised form is reloptions (so this
14770 /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
14771 fn consume_with_reloptions(&mut self) {
14772 let is_with = matches!(
14773 self.peek(),
14774 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14775 );
14776 if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
14777 return;
14778 }
14779 self.advance(); // WITH
14780 self.advance(); // (
14781 let mut depth = 1u32;
14782 while depth > 0 && !matches!(self.peek(), Token::Eof) {
14783 match self.peek() {
14784 Token::LParen => depth += 1,
14785 Token::RParen => depth -= 1,
14786 _ => {}
14787 }
14788 self.advance();
14789 }
14790 }
14791
14792 fn consume_mysql_table_options(&mut self) {
14793 loop {
14794 // Heuristic: a table option is an ident (or `DEFAULT`
14795 // reserved keyword) followed by `=` and an
14796 // ident / string / integer.
14797 let name_lc = match self.peek().clone() {
14798 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
14799 Token::Default => alloc::string::String::from("default"),
14800 _ => break,
14801 };
14802 let known = matches!(
14803 name_lc.as_str(),
14804 "engine"
14805 | "default"
14806 | "charset"
14807 | "collate"
14808 | "auto_increment"
14809 | "row_format"
14810 | "comment"
14811 | "pack_keys"
14812 | "stats_persistent"
14813 | "stats_auto_recalc"
14814 | "stats_sample_pages"
14815 | "key_block_size"
14816 | "tablespace"
14817 | "min_rows"
14818 | "max_rows"
14819 | "checksum"
14820 | "delay_key_write"
14821 | "insert_method"
14822 | "data"
14823 | "index"
14824 | "encryption"
14825 | "compression"
14826 );
14827 if !known {
14828 break;
14829 }
14830 self.advance(); // option name
14831 // `DEFAULT` optional prefix is followed by `CHARSET` /
14832 // `COLLATE`; consume the next ident too.
14833 if name_lc == "default" {
14834 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14835 self.advance();
14836 }
14837 }
14838 if matches!(self.peek(), Token::Eq) {
14839 self.advance();
14840 }
14841 match self.peek() {
14842 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_) | Token::Integer(_) => {
14843 self.advance();
14844 }
14845 _ => {}
14846 }
14847 }
14848 }
14849
14850 /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
14851 /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
14852 /// sure (otherwise a column literally named `primary` would
14853 /// be mistaken).
14854 fn peek_table_level_pk_start(&self) -> bool {
14855 let cur = self.peek();
14856 let nxt = self.tokens.get(self.pos + 1);
14857 let nxt2 = self.tokens.get(self.pos + 2);
14858 let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
14859 let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
14860 let is_lparen = matches!(nxt2, Some(Token::LParen));
14861 is_primary && is_key && is_lparen
14862 }
14863
14864 /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
14865 /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
14866 /// (mailrs round-5 G10).
14867 fn peek_table_level_unique_start(&self) -> bool {
14868 let cur = self.peek();
14869 let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
14870 if !is_unique {
14871 return false;
14872 }
14873 let n1 = self.tokens.get(self.pos + 1);
14874 // Plain `UNIQUE (…)`.
14875 if matches!(n1, Some(Token::LParen)) {
14876 return true;
14877 }
14878 // `UNIQUE NULLS [NOT] DISTINCT (…)`.
14879 let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
14880 if !is_nulls {
14881 return false;
14882 }
14883 let n2 = self.tokens.get(self.pos + 2);
14884 let n3 = self.tokens.get(self.pos + 3);
14885 let n4 = self.tokens.get(self.pos + 4);
14886 // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
14887 if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
14888 return true;
14889 }
14890 // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
14891 if matches!(n2, Some(Token::Not))
14892 && matches!(n3, Some(Token::Distinct))
14893 && matches!(n4, Some(Token::LParen))
14894 {
14895 return true;
14896 }
14897 false
14898 }
14899
14900 fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
14901 self.advance(); // PRIMARY
14902 self.advance(); // KEY
14903 let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
14904 // v7.39 (round 711) — the trailer's values are CARRIED now; round
14905 // 621 consumed and dropped them (the storing half of F08).
14906 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
14907 Ok(crate::ast::TableConstraint::PrimaryKey {
14908 name: None,
14909 columns,
14910 deferrable,
14911 initially_deferred,
14912 })
14913 }
14914
14915 fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
14916 self.advance(); // UNIQUE
14917 // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
14918 // (mailrs round-5 G10, PG 15+ surface). Default behaviour
14919 // is `NULLS DISTINCT` per the SQL standard.
14920 let mut nulls_not_distinct = false;
14921 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
14922 let n1 = self.tokens.get(self.pos + 1);
14923 let n2 = self.tokens.get(self.pos + 2);
14924 let is_not = matches!(n1, Some(Token::Not));
14925 let is_distinct = matches!(n2, Some(Token::Distinct));
14926 if is_not && is_distinct {
14927 self.advance(); // NULLS
14928 self.advance(); // NOT
14929 self.advance(); // DISTINCT
14930 nulls_not_distinct = true;
14931 } else if matches!(n1, Some(Token::Distinct)) {
14932 self.advance(); // NULLS
14933 self.advance(); // DISTINCT
14934 }
14935 }
14936 let columns = self.parse_paren_ident_list("UNIQUE")?;
14937 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
14938 Ok(crate::ast::TableConstraint::Unique {
14939 name: None,
14940 columns,
14941 nulls_not_distinct,
14942 deferrable,
14943 initially_deferred,
14944 })
14945 }
14946
14947 /// v7.13.0 — table-level `CHECK (<expr>)` constraint
14948 /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
14949 /// expression.
14950 /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
14951 /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
14952 /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
14953 /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
14954 /// constraint added by ALTER TABLE. `NOT` alone is not enough to
14955 /// commit: `NOT` starts no other suffix here, but reading both
14956 /// tokens before advancing keeps the caller's error message intact
14957 /// if someone writes `NOT NULL` by mistake.
14958 fn parse_not_valid_suffix(&mut self) -> bool {
14959 if !matches!(self.peek(), Token::Not) {
14960 return false;
14961 }
14962 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
14963 {
14964 return false;
14965 }
14966 self.advance();
14967 self.advance();
14968 true
14969 }
14970
14971 fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
14972 self.advance(); // EXCLUDE
14973 // Optional `USING <method>`.
14974 let mut method = None;
14975 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
14976 self.advance();
14977 method = Some(match self.advance() {
14978 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
14979 other => {
14980 return Err(self.err(alloc::format!(
14981 "expected index method after USING, got {other:?}"
14982 )));
14983 }
14984 });
14985 }
14986 if !matches!(self.peek(), Token::LParen) {
14987 return Err(self.err(alloc::format!(
14988 "expected '(' after EXCLUDE, got {:?}",
14989 self.peek()
14990 )));
14991 }
14992 self.advance();
14993 let mut elements: Vec<(String, String)> = Vec::new();
14994 loop {
14995 let col = match self.advance() {
14996 Token::Ident(s) | Token::QuotedIdent(s) => s,
14997 other => {
14998 return Err(self.err(alloc::format!(
14999 "expected column name in EXCLUDE, got {other:?}"
15000 )));
15001 }
15002 };
15003 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15004 return Err(self.err(alloc::format!(
15005 "expected WITH after EXCLUDE column, got {:?}",
15006 self.peek()
15007 )));
15008 }
15009 self.advance();
15010 let op = match self.advance() {
15011 Token::InetOverlap => String::from("&&"),
15012 Token::Intersects => String::from("?#"),
15013 Token::IsBelow => String::from("<^"),
15014 Token::IsAbove => String::from(">^"),
15015 Token::PatternLt => String::from("~<~"),
15016 Token::PatternLtEq => String::from("~<=~"),
15017 Token::PatternGt => String::from("~>~"),
15018 Token::PatternGtEq => String::from("~>=~"),
15019 Token::TsMatchOld => String::from("@@@"),
15020 Token::Eq => String::from("="),
15021 Token::JsonContains => String::from("@>"),
15022 Token::JsonContainedBy => String::from("<@"),
15023 Token::OverLeft => String::from("&<"),
15024 Token::OverRight => String::from("&>"),
15025 other => {
15026 return Err(self.err(alloc::format!(
15027 "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15028 )));
15029 }
15030 };
15031 elements.push((col, op));
15032 if matches!(self.peek(), Token::Comma) {
15033 self.advance();
15034 continue;
15035 }
15036 break;
15037 }
15038 if !matches!(self.peek(), Token::RParen) {
15039 return Err(self.err(alloc::format!(
15040 "expected ')' to close EXCLUDE, got {:?}",
15041 self.peek()
15042 )));
15043 }
15044 self.advance();
15045 Ok(crate::ast::TableConstraint::Exclude {
15046 name: None,
15047 method,
15048 elements,
15049 })
15050 }
15051
15052 fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15053 self.advance(); // CHECK
15054 if !matches!(self.peek(), Token::LParen) {
15055 return Err(self.err(alloc::format!(
15056 "expected '(' after CHECK, got {:?}",
15057 self.peek()
15058 )));
15059 }
15060 self.advance();
15061 let expr = self.parse_expr(0)?;
15062 if !matches!(self.peek(), Token::RParen) {
15063 return Err(self.err(alloc::format!(
15064 "expected ')' to close CHECK predicate, got {:?}",
15065 self.peek()
15066 )));
15067 }
15068 self.advance();
15069 // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15070 // are no existing rows for PG to skip, so it rejects the suffix.
15071 Ok(crate::ast::TableConstraint::Check {
15072 name: None,
15073 expr,
15074 not_valid: false,
15075 })
15076 }
15077
15078 /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15079 fn peek_table_level_check_start(&self) -> bool {
15080 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15081 }
15082
15083 /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15084 /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15085 /// on the dedicated FK path (`parse_table_level_fk` consumes its
15086 /// own CONSTRAINT prefix).
15087 fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15088 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15089 return None;
15090 }
15091 // tokens[pos+1] is the constraint name (any ident-like);
15092 // tokens[pos+2] is the kind keyword.
15093 match self.tokens.get(self.pos + 2) {
15094 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15095 Some(NamedTableConstraintKind::Check)
15096 }
15097 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15098 Some(NamedTableConstraintKind::Unique)
15099 }
15100 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15101 Some(NamedTableConstraintKind::PrimaryKey)
15102 }
15103 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15104 Some(NamedTableConstraintKind::Exclude)
15105 }
15106 _ => None,
15107 }
15108 }
15109
15110 fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15111 if !matches!(self.peek(), Token::LParen) {
15112 return Err(self.err(alloc::format!(
15113 "expected '(' after {ctx}, got {:?}",
15114 self.peek()
15115 )));
15116 }
15117 self.advance();
15118 let mut out = Vec::new();
15119 loop {
15120 out.push(self.expect_ident_like()?);
15121 match self.peek() {
15122 Token::Comma => {
15123 self.advance();
15124 }
15125 Token::RParen => {
15126 self.advance();
15127 break;
15128 }
15129 other => {
15130 return Err(self.err(alloc::format!(
15131 "expected ',' or ')' in {ctx} list, got {other:?}"
15132 )));
15133 }
15134 }
15135 }
15136 if out.is_empty() {
15137 return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15138 }
15139 Ok(out)
15140 }
15141
15142 /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15143 /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15144 /// table-level FK; a column def never starts with either keyword
15145 /// (column names are not in this reserved set).
15146 fn peek_constraint_or_fk_start(&self) -> bool {
15147 let is_constraint_kw = matches!(
15148 self.peek(),
15149 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15150 );
15151 let is_foreign_kw = matches!(
15152 self.peek(),
15153 Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15154 );
15155 is_constraint_kw || is_foreign_kw
15156 }
15157
15158 /// v7.6.0 — parse a table-level FK clause:
15159 /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15160 /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15161 fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15162 let mut name: Option<String> = None;
15163 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15164 self.advance();
15165 name = Some(self.expect_ident_like()?);
15166 }
15167 // `FOREIGN`
15168 match self.advance() {
15169 Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15170 other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15171 }
15172 // `KEY`
15173 match self.advance() {
15174 Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15175 other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15176 }
15177 // `(col, col, ...)`
15178 if !matches!(self.peek(), Token::LParen) {
15179 return Err(self.err(format!(
15180 "expected '(' after FOREIGN KEY, got {:?}",
15181 self.peek()
15182 )));
15183 }
15184 self.advance();
15185 let mut columns = Vec::new();
15186 loop {
15187 columns.push(self.expect_ident_like()?);
15188 match self.peek() {
15189 Token::Comma => {
15190 self.advance();
15191 }
15192 Token::RParen => {
15193 self.advance();
15194 break;
15195 }
15196 other => {
15197 return Err(self.err(format!(
15198 "expected ',' or ')' in FK column list, got {other:?}"
15199 )));
15200 }
15201 }
15202 }
15203 if columns.is_empty() {
15204 return Err(self.err("FOREIGN KEY requires at least one column".into()));
15205 }
15206 let (
15207 parent_table,
15208 parent_columns,
15209 on_delete,
15210 on_update,
15211 match_type,
15212 deferrable,
15213 initially_deferred,
15214 ) = self.parse_references_tail(columns.len())?;
15215 Ok(ForeignKeyConstraint {
15216 name,
15217 columns,
15218 parent_table,
15219 parent_columns,
15220 on_delete,
15221 on_update,
15222 match_type,
15223 deferrable,
15224 initially_deferred,
15225 })
15226 }
15227
15228 /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15229 /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15230 /// the local column count, used to default the parent column
15231 /// list when omitted (SQL spec: parent's PK is implied).
15232 fn parse_references_tail(
15233 &mut self,
15234 expected_arity: usize,
15235 ) -> Result<
15236 (
15237 String,
15238 Vec<String>,
15239 FkAction,
15240 FkAction,
15241 crate::ast::MatchType,
15242 // v7.39 (round 288) — deferrable, initially_deferred.
15243 bool,
15244 bool,
15245 ),
15246 ParseError,
15247 > {
15248 match self.advance() {
15249 Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15250 other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15251 }
15252 let parent_table = self.expect_ident_like()?;
15253 let mut parent_columns: Vec<String> = Vec::new();
15254 if matches!(self.peek(), Token::LParen) {
15255 self.advance();
15256 loop {
15257 parent_columns.push(self.expect_ident_like()?);
15258 match self.peek() {
15259 Token::Comma => {
15260 self.advance();
15261 }
15262 Token::RParen => {
15263 self.advance();
15264 break;
15265 }
15266 other => {
15267 return Err(self.err(format!(
15268 "expected ',' or ')' in REFERENCES column list, got {other:?}"
15269 )));
15270 }
15271 }
15272 }
15273 }
15274 if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15275 return Err(self.err(format!(
15276 "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15277 expected_arity,
15278 parent_columns.len()
15279 )));
15280 }
15281 // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15282 // it between the referenced column list and the ON / DEFERRABLE
15283 // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15284 // is skipped when any referencing column is NULL), so SIMPLE —
15285 // the default, and the only spelling pg_dump emits — is accepted
15286 // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15287 // mixed-NULL rule, which is not wired yet; reject them honestly
15288 // rather than silently applying SIMPLE (PG itself errors on
15289 // MATCH PARTIAL as "not yet implemented").
15290 let mut match_type = crate::ast::MatchType::Simple;
15291 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15292 self.advance();
15293 // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15294 // SIMPLE / PARTIAL arrive as bare identifiers.
15295 let kind = match self.advance() {
15296 Token::Full => "FULL".to_string(),
15297 Token::Ident(s) => s.to_uppercase(),
15298 other => {
15299 return Err(self.err(format!(
15300 "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15301 )));
15302 }
15303 };
15304 match kind.as_str() {
15305 "SIMPLE" => {} // Default — match_type stays Simple.
15306 // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15307 // when ALL referencing columns are NULL; a mixed-NULL key errors.
15308 "FULL" => match_type = crate::ast::MatchType::Full,
15309 "PARTIAL" => {
15310 return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15311 }
15312 _ => {
15313 return Err(self.err(format!(
15314 "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15315 )));
15316 }
15317 }
15318 }
15319 // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15320 // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15321 // <action>` / `ON UPDATE <action>` in either order. PG /
15322 // pg_dump emits the timing clause AFTER the ON clauses
15323 // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15324 // but the SQL spec allows either order. We loop over
15325 // every possible trailer and dispatch on the next token,
15326 // stopping when nothing matches. Phase 3.1 changes the
15327 // bare DEFERRABLE form from hard-error to accept-as-
15328 // immediate; SPG is single-writer with no deferred-
15329 // constraint window so the runtime semantics are always
15330 // immediate even when INITIALLY DEFERRED is requested.
15331 // PG's default referential action (no ON DELETE / ON UPDATE
15332 // clause) is NO ACTION, not RESTRICT — the two enforce
15333 // identically in SPG (single-writer, no deferred window; see the
15334 // shared match arm in constraints.rs) but information_schema.
15335 // referential_constraints must report NO ACTION to match PG.
15336 let mut on_delete = FkAction::NoAction;
15337 let mut on_update = FkAction::NoAction;
15338 let mut seen_on_delete = false;
15339 let mut seen_on_update = false;
15340 let mut deferrable = false;
15341 let mut initially_deferred = false;
15342 loop {
15343 // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15344 let before = self.pos;
15345 let (d, idef) = self.consume_deferrable_clauses_timed()?;
15346 if self.pos != before {
15347 deferrable = d;
15348 initially_deferred = idef;
15349 continue;
15350 }
15351 // ON DELETE / ON UPDATE.
15352 if !matches!(self.peek(), Token::On) {
15353 break;
15354 }
15355 self.advance();
15356 let which = self.advance();
15357 let action = self.parse_fk_action()?;
15358 match which {
15359 Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15360 if seen_on_delete {
15361 return Err(self.err("ON DELETE specified twice".into()));
15362 }
15363 seen_on_delete = true;
15364 on_delete = action;
15365 }
15366 Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15367 if seen_on_update {
15368 return Err(self.err("ON UPDATE specified twice".into()));
15369 }
15370 seen_on_update = true;
15371 on_update = action;
15372 }
15373 other => {
15374 return Err(
15375 self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15376 );
15377 }
15378 }
15379 }
15380 Ok((
15381 parent_table,
15382 parent_columns,
15383 on_delete,
15384 on_update,
15385 match_type,
15386 deferrable,
15387 initially_deferred,
15388 ))
15389 }
15390
15391 /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15392 /// NO ACTION`.
15393 fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15394 match self.advance() {
15395 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15396 Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15397 Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15398 Token::Null => Ok(FkAction::SetNull),
15399 Token::Default => Ok(FkAction::SetDefault),
15400 other => Err(self.err(format!(
15401 "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15402 ))),
15403 },
15404 Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15405 Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15406 other => Err(self.err(format!(
15407 "expected ACTION after NO in FK action, got {other:?}"
15408 ))),
15409 },
15410 other => Err(self.err(format!(
15411 "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15412 ))),
15413 }
15414 }
15415
15416 /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15417 /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15418 fn consume_if_not_exists(&mut self) -> bool {
15419 // `IF` arrives as a bare Ident (we don't reserve it because it
15420 // also appears mid-expression in PG, though we don't support
15421 // those forms yet).
15422 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15423 if !looks_like_if {
15424 return false;
15425 }
15426 // Peek one ahead before committing: only consume IF when it's
15427 // actually `IF NOT EXISTS`.
15428 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
15429 return false;
15430 }
15431 if !matches!(
15432 self.tokens.get(self.pos + 2),
15433 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15434 ) {
15435 return false;
15436 }
15437 self.advance(); // IF
15438 self.advance(); // NOT
15439 self.advance(); // EXISTS
15440 true
15441 }
15442
15443 /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
15444 /// Consumes IF EXISTS as a pair; returns false otherwise
15445 /// without consuming any tokens.
15446 /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
15447 /// ENABLE/DISABLE/FORCE/NO FORCE.
15448 fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
15449 for kw in ["row", "level", "security"] {
15450 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
15451 {
15452 return Err(self.err(alloc::format!(
15453 "expected {} in ROW LEVEL SECURITY, got {:?}",
15454 kw.to_ascii_uppercase(),
15455 self.peek()
15456 )));
15457 }
15458 self.advance();
15459 }
15460 Ok(())
15461 }
15462
15463 fn consume_if_exists(&mut self) -> bool {
15464 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15465 if !looks_like_if {
15466 return false;
15467 }
15468 if !matches!(
15469 self.tokens.get(self.pos + 1),
15470 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15471 ) {
15472 return false;
15473 }
15474 self.advance(); // IF
15475 self.advance(); // EXISTS
15476 true
15477 }
15478
15479 /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
15480 /// qualifiers after an index column ref. ASC / DESC are
15481 /// reserved tokens; NULLS / FIRST / LAST are bare idents.
15482 /// We accept and discard them since single-column BTree
15483 /// stores rows in natural key order today.
15484 /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
15485 /// ORDER BY key. Returns None when absent.
15486 fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
15487 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15488 return Ok(None);
15489 }
15490 self.advance();
15491 match self.advance() {
15492 Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
15493 Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
15494 other => Err(self.err(alloc::format!(
15495 "expected FIRST or LAST after NULLS, got {other:?}"
15496 ))),
15497 }
15498 }
15499
15500 /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
15501 /// rather than discarded.
15502 ///
15503 /// SPG's index does not scan in a direction — column ordering is
15504 /// intrinsic to the storage — but `pg_indexes.indexdef` is a
15505 /// reproduction of the DDL, and dropping the clause meant
15506 /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
15507 /// dump lost it, and a schema diff saw drift on every run.
15508 fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
15509 let mut order = crate::ast::IndexColumnOrder::default();
15510 loop {
15511 match self.peek() {
15512 Token::Asc => {
15513 self.advance();
15514 }
15515 Token::Desc => {
15516 order.descending = true;
15517 self.advance();
15518 }
15519 Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
15520 let look = self.tokens.get(self.pos + 1);
15521 if matches!(
15522 look,
15523 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
15524 || k.eq_ignore_ascii_case("last")
15525 ) {
15526 self.advance();
15527 order.nulls_first = Some(matches!(
15528 self.advance(),
15529 Token::Ident(k) if k.eq_ignore_ascii_case("first")
15530 ));
15531 } else {
15532 break;
15533 }
15534 }
15535 _ => break,
15536 }
15537 }
15538 order
15539 }
15540
15541 fn parse_create_index_stmt_after_create(
15542 &mut self,
15543 is_unique: bool,
15544 ) -> Result<Statement, ParseError> {
15545 // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
15546 debug_assert!(matches!(self.peek(), Token::Index));
15547 self.advance();
15548 // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
15549 // SPG's CREATE INDEX is synchronous end-to-end today (real
15550 // CONCURRENTLY variant with restartable scans queues with
15551 // v7.39 indexes epic), so the modifier has no runtime effect
15552 // — same accept-and-no-op shape as v7.37.16.5 DETACH
15553 // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
15554 // VIEW CONCURRENTLY.
15555 let mut concurrently = false;
15556 if matches!(
15557 self.peek(),
15558 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
15559 ) {
15560 self.advance();
15561 concurrently = true;
15562 }
15563 let if_not_exists = self.consume_if_not_exists();
15564 // v7.39 (read01 round 93) — the index name is optional (PG since
15565 // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
15566 // When the token after `[IF NOT EXISTS]` is already `ON`, no name
15567 // was given; leave it empty and the engine derives a PG-style
15568 // `<table>_<cols>_idx` name at CREATE time (with collision counter).
15569 let name = if matches!(self.peek(), Token::On) {
15570 String::new()
15571 } else {
15572 self.expect_ident_like()?
15573 };
15574 if !matches!(self.peek(), Token::On) {
15575 return Err(self.err(format!(
15576 "expected ON after CREATE INDEX <name>, got {:?}",
15577 self.peek()
15578 )));
15579 }
15580 self.advance();
15581 let table = self.expect_ident_like()?;
15582 // Optional `USING <method>` — only recognised method in v2.0 is
15583 // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
15584 // ident `using` (we don't promote it to a reserved keyword
15585 // because it isn't reserved anywhere else in our SQL surface).
15586 let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15587 self.advance();
15588 let m = self.expect_ident_like()?;
15589 match m.to_ascii_lowercase().as_str() {
15590 "hnsw" => IndexMethod::Hnsw,
15591 "btree" => IndexMethod::BTree,
15592 "brin" => IndexMethod::Brin,
15593 // v7.12.3 — real GIN inverted index over `tsvector`.
15594 // v7.9.26b's `USING gin` → BTree silent fallback is
15595 // gone; the engine validates that the indexed column
15596 // is `tsvector` at CREATE INDEX time.
15597 "gin" => IndexMethod::Gin,
15598 // v7.9.26b — PG `pg_dump` emits `USING gist` /
15599 // `USING spgist` / `USING hash` for their built-in
15600 // AMs that SPG doesn't have a matching
15601 // implementation for; degrade to BTree on the
15602 // leading column so the schema loads + the index
15603 // catalogue stays consistent. Operator pays the
15604 // planner cost only for the queries that would have
15605 // used the specialised AM.
15606 "gist" | "spgist" | "hash" => IndexMethod::BTree,
15607 // v7.11.3 — pgvector ships both `ivfflat` and
15608 // `hnsw`. Customers shouldn't have to choose
15609 // their on-disk index method based on what SPG
15610 // implements; accept `ivfflat` as a synonym for
15611 // `hnsw` so PG schemas using either method drop
15612 // in. The vector distance op (`<->` / `<#>` /
15613 // `<=>`) at query time still picks the metric.
15614 "ivfflat" => IndexMethod::Hnsw,
15615 other => {
15616 return Err(self.err(alloc::format!(
15617 "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
15618 )));
15619 }
15620 }
15621 } else {
15622 IndexMethod::BTree
15623 };
15624 if !matches!(self.peek(), Token::LParen) {
15625 return Err(self.err(format!(
15626 "expected '(' before indexed column, got {:?}",
15627 self.peek()
15628 )));
15629 }
15630 self.advance();
15631 // v6.8.2 — accept either a bare column ident (legacy) or
15632 // an expression `fn(col, …)` for expression indexes.
15633 // Distinguish by peeking the token *after* the current
15634 // ident: `ident )` is the legacy column-only path;
15635 // anything else triggers the Pratt expression parser.
15636 // (`advance()` uses `mem::replace` to nil out the current
15637 // slot, so we can't save+rewind cleanly — peek-ahead via
15638 // direct index avoids the mutation.)
15639 let mut opclass: Option<String> = None;
15640 let mut key_collation: Option<String> = None;
15641 let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
15642 // Single column with `)` immediately after — fast path.
15643 // v7.9.29 — also: bare column followed by `,` (the
15644 // multi-column form `(a, b, c)`). Without this branch
15645 // the leading ident gets pulled into `parse_expr`
15646 // which then sets `expression = Some(Column(a))` and
15647 // breaks Display round-trip on the multi-column shape.
15648 Token::Ident(s) | Token::QuotedIdent(s)
15649 if matches!(
15650 self.tokens.get(self.pos + 1),
15651 Some(Token::RParen | Token::Comma)
15652 ) =>
15653 {
15654 self.advance();
15655 (s, None)
15656 }
15657 // v7.9.22 — single column followed by a pgvector
15658 // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
15659 // v7.15.0 — capture the opclass instead of discarding
15660 // it so the engine can dispatch (e.g. `gin_trgm_ops`
15661 // → real trigram-shingle GIN over a TEXT column).
15662 // Vector/HNSW opclasses still take their distance
15663 // metric from the query operator (`<->` / `<#>` /
15664 // `<=>`), so for those callers the opclass stays
15665 // informational.
15666 // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
15667 // opclass: `(embedding public.vector_cosine_ops)`. Strip
15668 // the schema and dispatch on the bare opclass, the same
15669 // treatment table/type names get.
15670 Token::Ident(s) | Token::QuotedIdent(s)
15671 if matches!(
15672 self.tokens.get(self.pos + 1),
15673 Some(Token::Ident(_) | Token::QuotedIdent(_))
15674 ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
15675 && matches!(
15676 self.tokens.get(self.pos + 3),
15677 Some(Token::Ident(op) | Token::QuotedIdent(op))
15678 if is_vector_opclass_name(op)
15679 ) =>
15680 {
15681 self.advance(); // column name
15682 self.advance(); // schema qualifier
15683 self.advance(); // dot
15684 let op_tok = self.advance();
15685 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15686 opclass = Some(op.to_ascii_lowercase());
15687 }
15688 (s, None)
15689 }
15690 Token::Ident(s) | Token::QuotedIdent(s)
15691 if matches!(
15692 self.tokens.get(self.pos + 1),
15693 Some(Token::Ident(op) | Token::QuotedIdent(op))
15694 if is_vector_opclass_name(op)
15695 ) =>
15696 {
15697 self.advance(); // column name
15698 // Capture the opclass token, lower-cased for
15699 // case-insensitive engine dispatch.
15700 let op_tok = self.advance();
15701 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15702 opclass = Some(op.to_ascii_lowercase());
15703 }
15704 (s, None)
15705 }
15706 Token::Ident(_) | Token::QuotedIdent(_) => {
15707 // v7.39 (round 538) — an explicit COLLATE on the key,
15708 // read by LOOKAHEAD because `parse_expr` absorbs the
15709 // clause as a no-op (SPG orders text by bytes, which is
15710 // the C collation, so it changes nothing to honour). PG
15711 // still PRINTS it: an explicitly written `"C"` and the
15712 // collation a column inherits are different collation
15713 // OBJECTS even where they sort identically, which is why
15714 // `(a COLLATE "C")` shows on a C-collation database too.
15715 if matches!(
15716 self.tokens.get(self.pos + 1),
15717 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
15718 ) {
15719 key_collation = match self.tokens.get(self.pos + 2) {
15720 Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
15721 Some(n.clone())
15722 }
15723 _ => None,
15724 };
15725 }
15726 let key_expr = self.parse_expr(0)?;
15727 let primary = extract_first_column(&key_expr).ok_or_else(|| {
15728 self.err("expression index key must reference at least one column".into())
15729 })?;
15730 (primary, Some(key_expr))
15731 }
15732 // v7.37.43-T4 — parenthesised expression index key
15733 // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
15734 // PG's CREATE INDEX requires the expression to be in
15735 // its own parens to disambiguate function calls from
15736 // column lists, so this `LParen` is the inner open-paren
15737 // of an expression key. parse_expr handles the recursive
15738 // descent and consumes the matching `RParen`.
15739 Token::LParen => {
15740 let key_expr = self.parse_expr(0)?;
15741 let primary = extract_first_column(&key_expr).ok_or_else(|| {
15742 self.err("expression index key must reference at least one column".into())
15743 })?;
15744 (primary, Some(key_expr))
15745 }
15746 other => {
15747 return Err(self.err(format!(
15748 "expected column ident or expression, got {other:?}"
15749 )));
15750 }
15751 };
15752 // v7.9.14 — accept extra comma-separated columns inside
15753 // the index key parens (`CREATE INDEX … (a, b, c)`).
15754 // mailrs F2. Each extra column may carry an optional
15755 // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
15756 // — parsed and discarded; SPG doesn't honour direction
15757 // on a BTree index today (column ordering is intrinsic
15758 // to the storage). v7.10 will widen to genuine composite
15759 // index keys.
15760 let mut extra_columns: Vec<String> = Vec::new();
15761 // The leading column may also have ASC/DESC after it — and that
15762 // one is the column SPG indexes, so its clause is kept.
15763 let key_order = self.consume_optional_index_column_qualifiers();
15764 while matches!(self.peek(), Token::Comma) {
15765 self.advance();
15766 let extra = self.expect_ident_like()?;
15767 let _ = self.consume_optional_index_column_qualifiers();
15768 extra_columns.push(extra);
15769 }
15770 if !matches!(self.peek(), Token::RParen) {
15771 return Err(self.err(format!(
15772 "expected ')' after indexed column / expression, got {:?}",
15773 self.peek()
15774 )));
15775 }
15776 self.advance();
15777 // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
15778 // index-only-scan annotation. Bare ident (not a reserved
15779 // keyword) so we test by case-insensitive string match.
15780 let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
15781 {
15782 self.advance();
15783 if !matches!(self.peek(), Token::LParen) {
15784 return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
15785 }
15786 self.advance();
15787 let mut cols = Vec::new();
15788 loop {
15789 cols.push(self.expect_ident_like()?);
15790 match self.peek() {
15791 Token::Comma => {
15792 self.advance();
15793 }
15794 Token::RParen => {
15795 self.advance();
15796 break;
15797 }
15798 other => {
15799 return Err(self.err(format!(
15800 "expected ',' or ')' in INCLUDE list, got {other:?}"
15801 )));
15802 }
15803 }
15804 }
15805 cols
15806 } else {
15807 Vec::new()
15808 };
15809 // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
15810 // storage parameters. pgvector emits `WITH (lists = N)` for
15811 // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
15812 // SPG's HNSW picks its own parameters today (tunable via
15813 // env vars), so the WITH clause is informational and dropped.
15814 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15815 self.advance();
15816 if !matches!(self.peek(), Token::LParen) {
15817 return Err(self.err(format!(
15818 "expected '(' after WITH in CREATE INDEX, got {:?}",
15819 self.peek()
15820 )));
15821 }
15822 self.advance();
15823 loop {
15824 if matches!(self.peek(), Token::RParen) {
15825 self.advance();
15826 break;
15827 }
15828 // Drain `key = value` or bare `key` tokens.
15829 let _ = self.advance(); // key
15830 if matches!(self.peek(), Token::Eq) {
15831 self.advance();
15832 let _ = self.advance(); // value (int / string / ident)
15833 }
15834 match self.peek() {
15835 Token::Comma => {
15836 self.advance();
15837 }
15838 Token::RParen => {
15839 self.advance();
15840 break;
15841 }
15842 other => {
15843 return Err(self.err(format!(
15844 "expected ',' or ')' in WITH (…) clause, got {other:?}"
15845 )));
15846 }
15847 }
15848 }
15849 }
15850 // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
15851 // which sits between the key list and the WHERE clause.
15852 let mut nulls_not_distinct = false;
15853 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15854 let n1 = self.tokens.get(self.pos + 1);
15855 let n2 = self.tokens.get(self.pos + 2);
15856 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
15857 self.advance(); // NULLS
15858 self.advance(); // NOT
15859 self.advance(); // DISTINCT
15860 nulls_not_distinct = true;
15861 } else if matches!(n1, Some(Token::Distinct)) {
15862 self.advance(); // NULLS
15863 self.advance(); // DISTINCT
15864 }
15865 }
15866 // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
15867 let partial_predicate = if matches!(self.peek(), Token::Where) {
15868 self.advance();
15869 Some(self.parse_expr(0)?)
15870 } else {
15871 None
15872 };
15873 // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
15874 // sense: uniqueness over an ANN structure has no clean
15875 // semantics. Reject early. (BRIN UNIQUE is similarly
15876 // meaningless — block both.)
15877 if is_unique && !matches!(method, IndexMethod::BTree) {
15878 return Err(self.err(alloc::format!(
15879 "UNIQUE is only supported on BTree indexes, got USING {:?}",
15880 method
15881 )));
15882 }
15883 Ok(Statement::CreateIndex(CreateIndexStatement {
15884 concurrently,
15885 name,
15886 key_order,
15887 key_collation,
15888 table,
15889 column,
15890 nulls_not_distinct,
15891 method,
15892 if_not_exists,
15893 included_columns,
15894 partial_predicate,
15895 extra_columns: extra_columns.clone(),
15896 expression,
15897 is_unique,
15898 opclass,
15899 }))
15900 }
15901
15902 /// v7.6.0 — wraps `parse_column_def` and consumes an optional
15903 /// column-level `REFERENCES ...` clause. The trailing FK is
15904 /// normalised into table-level shape (single-element columns +
15905 /// parent_columns) so the engine sees one uniform constraint list.
15906 fn parse_column_def_with_fk(
15907 &mut self,
15908 ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
15909 let col = self.parse_column_def()?;
15910 // v7.39 (round 308, V29) — an explicitly named inline FK:
15911 // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
15912 // loop leaves this spelling intact precisely so the name can be
15913 // kept here; PG reports it in violation messages and matches it
15914 // in `SET CONSTRAINTS`.
15915 let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
15916 {
15917 self.advance();
15918 Some(self.expect_ident_like()?)
15919 } else {
15920 None
15921 };
15922 // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
15923 let inline_references = matches!(
15924 self.peek(),
15925 Token::Ident(s) if s.eq_ignore_ascii_case("references")
15926 );
15927 if !inline_references {
15928 return Ok((col, None));
15929 }
15930 let (
15931 parent_table,
15932 parent_columns,
15933 on_delete,
15934 on_update,
15935 match_type,
15936 deferrable,
15937 initially_deferred,
15938 ) = self.parse_references_tail(1)?;
15939 let fk = ForeignKeyConstraint {
15940 name: declared_name,
15941 columns: vec![col.name.clone()],
15942 parent_table,
15943 parent_columns,
15944 on_delete,
15945 on_update,
15946 match_type,
15947 deferrable,
15948 initially_deferred,
15949 };
15950 Ok((col, Some(fk)))
15951 }
15952
15953 /// v7.13.0 — parse a column type (consuming the type ident and
15954 /// any trailing parameters / `[]`), without surrounding column
15955 /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
15956 /// Returns the resolved `ColumnTypeName` plus implied
15957 /// `(auto_increment, not_null)` flags from PG SERIAL family
15958 /// shorthands — callers that don't expect those (ALTER COLUMN
15959 /// TYPE) can discard them.
15960 fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
15961 let (ty, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
15962 Ok(ty)
15963 }
15964
15965 #[allow(clippy::type_complexity)]
15966 fn parse_type_with_implied_flags(
15967 &mut self,
15968 ) -> Result<
15969 (
15970 ColumnTypeName,
15971 bool,
15972 bool,
15973 Option<String>,
15974 Collation,
15975 // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
15976 bool,
15977 // v7.39 (round 676) — the collation NAME as written, which the
15978 // `Collation` enum above cannot carry.
15979 Option<String>,
15980 bool,
15981 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
15982 // list captured at type-parse time. None for all
15983 // non-ENUM types.
15984 Option<Vec<String>>,
15985 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
15986 // list. Distinct from ENUM (subset semantics).
15987 Option<Vec<String>>,
15988 // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
15989 // width, lost when the type collapses to SmallInt / Int.
15990 Option<MysqlIntWidth>,
15991 // v7.39 (round 424) — declared fractional-seconds precision of a
15992 // MySQL temporal column (bare spelling = 0). None under PG.
15993 Option<u8>,
15994 ),
15995 ParseError,
15996 > {
15997 let mut ty_ident = match self.advance() {
15998 Token::Ident(s) => s,
15999 // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16000 // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16001 // '<span>'` literal grammar. As a column type it lands
16002 // here directly; downstream resolution still uses the
16003 // canonical lowercase string.
16004 Token::Interval => "interval".to_string(),
16005 other => {
16006 return Err(ParseError {
16007 message: format!("expected column type, got {other:?}"),
16008 token_pos: self.consumed_pos(),
16009 });
16010 }
16011 };
16012 // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16013 // pg_dump qualifies extension types (`public.vector(1024)`).
16014 // SPG is single-namespace; drop the schema and resolve the
16015 // bare type — same treatment table names already get.
16016 while matches!(self.peek(), Token::Dot) {
16017 self.advance();
16018 ty_ident = self.expect_ident_like()?;
16019 }
16020 let mut implied_auto_increment = false;
16021 let mut implied_not_null = false;
16022 let mut user_type_ref: Option<String> = None;
16023 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16024 // value list, captured here and bubbled up through the
16025 // ColumnDef so the engine can attach it to the column
16026 // schema (and validate INSERT cells against it).
16027 let mut inline_enum_variants: Option<Vec<String>> = None;
16028 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16029 let mut inline_set_variants: Option<Vec<String>> = None;
16030 // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16031 // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16032 // collapses to SmallInt / Int. Only under the MySQL dialect.
16033 let mut mysql_int_width: Option<MysqlIntWidth> = None;
16034 // v7.39 (round 424) — the declared fractional-seconds precision of a
16035 // MySQL temporal column. Set by the temporal arms below; stays None
16036 // for PG (whose temporal columns keep full microseconds).
16037 let mut mysql_fsp: Option<u8> = None;
16038 let mut ty = match ty_ident.as_str() {
16039 // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16040 "smallserial" | "serial2" => {
16041 implied_auto_increment = true;
16042 implied_not_null = true;
16043 ColumnTypeName::SmallInt
16044 }
16045 "serial" | "serial4" => {
16046 implied_auto_increment = true;
16047 implied_not_null = true;
16048 ColumnTypeName::Int
16049 }
16050 "bigserial" | "serial8" => {
16051 implied_auto_increment = true;
16052 implied_not_null = true;
16053 ColumnTypeName::BigInt
16054 }
16055 // MySQL flavours we accept by aliasing to the closest SPG
16056 // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16057 // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16058 // 24-bit) → INT. UNSIGNED modifiers are consumed below
16059 // without semantic effect.
16060 // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16061 // PG's internal type names; pg_dump and hand-written PG schemas
16062 // use them interchangeably with smallint / int / bigint (the cast
16063 // path already accepted them, only the column grammar didn't).
16064 "smallint" | "int2" => {
16065 // v7.14.0 — MySQL display-width on integers
16066 // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16067 // parenthesised number is purely cosmetic — it
16068 // doesn't change storage. Accept + discard.
16069 self.consume_optional_paren_size();
16070 ColumnTypeName::SmallInt
16071 }
16072 // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16073 // canonical encoding for BOOLEAN. Every MySQL driver
16074 // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16075 // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16076 // 4.3 SPG classified TINYINT(1) as SmallInt, which
16077 // gave the customer i16-shaped values where the app
16078 // expected bool — a Tier-A silent type drift on
16079 // mysqldump restores. Now: `TINYINT(1)` → Bool;
16080 // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16081 // stay SmallInt (the legacy width-agnostic path).
16082 "tinyint" => {
16083 let width = self.peek_optional_paren_size_value();
16084 self.consume_optional_paren_size();
16085 if width == Some(1) {
16086 ColumnTypeName::Bool
16087 } else {
16088 // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16089 // lost width so the write path can enforce -128..127.
16090 if self.mysql_dialect {
16091 mysql_int_width = Some(MysqlIntWidth::Tiny);
16092 }
16093 ColumnTypeName::SmallInt
16094 }
16095 }
16096 "mediumint" => {
16097 self.consume_optional_paren_size();
16098 // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16099 if self.mysql_dialect {
16100 mysql_int_width = Some(MysqlIntWidth::Medium);
16101 }
16102 ColumnTypeName::Int
16103 }
16104 "int" | "integer" | "int4" => {
16105 self.consume_optional_paren_size();
16106 ColumnTypeName::Int
16107 }
16108 "bigint" | "int8" => {
16109 self.consume_optional_paren_size();
16110 ColumnTypeName::BigInt
16111 }
16112 // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16113 // (mailrs round-5 G6). Consume the optional `PRECISION`
16114 // tail when the type keyword was `double` / `DOUBLE`.
16115 //
16116 // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16117 // FLOAT". `FLOAT(p)` picks the width the way PG does:
16118 // p in 1..=24 is real, 25..=53 is double precision, and
16119 // anything else is an error.
16120 "float" | "double" | "real" => {
16121 if ty_ident.eq_ignore_ascii_case("double")
16122 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16123 {
16124 self.advance();
16125 }
16126 if ty_ident.eq_ignore_ascii_case("real") {
16127 // v7.39 (round 274) — the two dialects genuinely
16128 // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16129 // synonym for DOUBLE (8-byte). Round 269 made REAL
16130 // 32-bit globally and thereby narrowed the stored
16131 // precision of every MySQL REAL column.
16132 if self.mysql_dialect {
16133 ColumnTypeName::Float
16134 } else {
16135 ColumnTypeName::Real
16136 }
16137 } else if ty_ident.eq_ignore_ascii_case("float")
16138 && self.mysql_dialect
16139 && matches!(self.peek(), Token::LParen)
16140 && self.peek_paren_has_comma()
16141 {
16142 // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16143 // display form (`FLOAT(10,2)`), which PG has no
16144 // equivalent of. It was `syntax error at or near ","`,
16145 // so the whole CREATE failed. The digits are a display
16146 // hint only; SPG stores the full double.
16147 self.consume_optional_paren_size();
16148 ColumnTypeName::Float
16149 } else if ty_ident.eq_ignore_ascii_case("float")
16150 && matches!(self.peek(), Token::LParen)
16151 {
16152 // PG words the two bounds differently, and
16153 // parse_paren_size already rejects a zero.
16154 let p = self.parse_paren_size("FLOAT")?;
16155 if p > 53 {
16156 return Err(self.err(String::from(
16157 "precision for type float must be less than 54 bits",
16158 )));
16159 }
16160 if p <= 24 {
16161 ColumnTypeName::Real
16162 } else {
16163 ColumnTypeName::Float
16164 }
16165 } else {
16166 ColumnTypeName::Float
16167 }
16168 }
16169 // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16170 "float4" => ColumnTypeName::Real,
16171 "float8" => ColumnTypeName::Float,
16172 "text" => ColumnTypeName::Text,
16173 // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16174 // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16175 // real MySQL schema and NONE of them existed: the CREATE
16176 // failed outright with `type "blob" does not exist`, so the
16177 // table was never made. The sizes differ only in MySQL's
16178 // maximum length, which SPG does not cap, so they collapse
16179 // onto TEXT and BYTEA the way the unsized spellings do.
16180 "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16181 "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16182 // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16183 // enforce, consumed so the declaration parses.
16184 "varbinary" | "binary" => {
16185 self.consume_optional_paren_size();
16186 ColumnTypeName::Bytes
16187 }
16188 "name" => ColumnTypeName::Name,
16189 "xid" => ColumnTypeName::Xid,
16190 "oid" => ColumnTypeName::Oid,
16191 "xid8" => ColumnTypeName::Xid8,
16192 "bool" | "boolean" => ColumnTypeName::Bool,
16193 // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16194 // an unbounded `character varying`, which the arm below has always
16195 // read as text. Only the short spelling demanded a length, so
16196 // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16197 // there is — failed on `VARCHAR type requires (N)` while the long
16198 // spelling of the same thing was accepted. The same asymmetry
16199 // round 613 closed on the CAST side, here on the DDL side.
16200 "varchar" => {
16201 if matches!(self.peek(), Token::LParen) {
16202 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16203 } else {
16204 ColumnTypeName::Text
16205 }
16206 }
16207 // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16208 // `character` below (SQL standard).
16209 "char" => {
16210 if matches!(self.peek(), Token::LParen) {
16211 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16212 } else {
16213 ColumnTypeName::Char(1)
16214 }
16215 }
16216 // pg_dump's canonical spellings: `character varying(n)` = varchar,
16217 // `character(n)` = char, bare `character` = char(1). Unbounded
16218 // `character varying` maps to text.
16219 "character" => {
16220 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16221 self.advance();
16222 if matches!(self.peek(), Token::LParen) {
16223 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16224 } else {
16225 ColumnTypeName::Text
16226 }
16227 } else if matches!(self.peek(), Token::LParen) {
16228 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16229 } else {
16230 ColumnTypeName::Char(1)
16231 }
16232 }
16233 "vector" => {
16234 let dim = self.parse_paren_size("VECTOR")?;
16235 let encoding = self.parse_optional_vector_encoding()?;
16236 ColumnTypeName::Vector { dim, encoding }
16237 }
16238 // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16239 // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16240 // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16241 // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16242 // DECIMAL(10,2))` — how nearly every money column is written,
16243 // in either dialect — was a syntax error and the table was
16244 // never created. `FIXED` is MySQL's alias alone, so it is
16245 // taken only in that dialect.
16246 "numeric" | "decimal" | "dec" => {
16247 let (precision, scale) = self.parse_optional_numeric_params()?;
16248 ColumnTypeName::Numeric(precision, scale)
16249 }
16250 "fixed" if self.mysql_dialect => {
16251 let (precision, scale) = self.parse_optional_numeric_params()?;
16252 ColumnTypeName::Numeric(precision, scale)
16253 }
16254 "date" => ColumnTypeName::Date,
16255 // MySQL's `DATETIME` is the same domain as standard
16256 // `TIMESTAMP` — accept both spellings.
16257 "timestamp" | "datetime" => {
16258 // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16259 // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16260 // TIME ZONE` clause, so consume it first.
16261 // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16262 // (it truncates on write and pads on render), so capture it;
16263 // a bare spelling means precision 0 there. PG stores µs always
16264 // and keeps `None`.
16265 let n = self.take_optional_paren_size();
16266 if self.mysql_dialect {
16267 mysql_fsp = Some(n.unwrap_or(0).min(6));
16268 }
16269 // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16270 // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16271 // the full form. SPG canonicalises:
16272 // - WITH TIME ZONE → Timestamptz
16273 // - WITHOUT TIME ZONE → Timestamp
16274 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16275 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16276 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16277 {
16278 self.advance(); // WITH
16279 self.advance(); // TIME
16280 self.advance(); // ZONE
16281 ColumnTypeName::Timestamptz
16282 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16283 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16284 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16285 {
16286 self.advance(); // WITHOUT
16287 self.advance(); // TIME
16288 self.advance(); // ZONE
16289 ColumnTypeName::Timestamp
16290 } else {
16291 // A second `(precision)` cannot legally follow, but the
16292 // old grammar tolerated it; keep that tolerance.
16293 self.consume_optional_paren_size();
16294 ColumnTypeName::Timestamp
16295 }
16296 }
16297 // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16298 // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16299 // only PG-wire OID differs.
16300 "timestamptz" => {
16301 self.consume_optional_paren_size();
16302 ColumnTypeName::Timestamptz
16303 }
16304 // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16305 // validation. We accept the JSONB spelling too because
16306 // most PG clients default to it; SPG doesn't distinguish
16307 // the two (no path-operator perf advantage to model).
16308 "json" => ColumnTypeName::Json,
16309 "jsonb" => ColumnTypeName::Jsonb,
16310 // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16311 // surface here. Same storage shape; mapping happens at
16312 // the engine side via the ColumnTypeName → DataType
16313 // resolver. Literal forms are handled at coerce_value
16314 // time so the lexer stays untouched.
16315 "bytea" | "bytes" => ColumnTypeName::Bytes,
16316 // v7.17.0 Phase 7 — PG network address types
16317 // v7.17.0 had a Text-backed fallback here for
16318 // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16319 // each to a first-class type; the keywords are
16320 // bound below in the ζ-A block.
16321 // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16322 // The actual `to_tsvector` / `@@` / `ts_rank` surface
16323 // arrives in v7.12.1+; the type itself loads here so
16324 // mailrs's `scripts/init-schema.sql` runs unmodified.
16325 "tsvector" => ColumnTypeName::TsVector,
16326 "tsquery" => ColumnTypeName::TsQuery,
16327 // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
16328 // surface for Django / Rails / Hibernate's default
16329 // PK pattern.
16330 "uuid" => ColumnTypeName::Uuid,
16331 // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
16332 // Storage = three-field {months, days, micros}, catalog
16333 // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
16334 // line `INTERVAL` was parser-rejected at CREATE TABLE.
16335 "interval" => {
16336 // pg_dump emits field-qualified forms like `INTERVAL DAY TO
16337 // SECOND` and an optional `(p)` precision. SPG stores the full
16338 // {months,days,micros}; consume + ignore the qualifier/precision.
16339 while matches!(self.peek(), Token::To)
16340 || matches!(self.peek(), Token::Ident(s) if matches!(
16341 s.to_ascii_lowercase().as_str(),
16342 "year" | "month" | "day" | "hour" | "minute" | "second"
16343 ))
16344 {
16345 self.advance();
16346 }
16347 self.consume_optional_paren_size();
16348 ColumnTypeName::Interval
16349 }
16350 // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
16351 // i64 microseconds since 00:00:00. Wire OID 1083.
16352 // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
16353 "time" => {
16354 // v7.39 (round 424) — MySQL TIME carries a semantic
16355 // fractional-seconds precision, bare meaning 0.
16356 let n = self.take_optional_paren_size();
16357 if self.mysql_dialect {
16358 mysql_fsp = Some(n.unwrap_or(0).min(6));
16359 }
16360 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16361 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16362 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16363 {
16364 self.advance();
16365 self.advance();
16366 self.advance();
16367 ColumnTypeName::TimeTz
16368 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16369 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16370 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16371 {
16372 self.advance();
16373 self.advance();
16374 self.advance();
16375 ColumnTypeName::Time
16376 } else {
16377 ColumnTypeName::Time
16378 }
16379 }
16380 // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
16381 // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
16382 "year" => ColumnTypeName::Year,
16383 // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
16384 // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
16385 "timetz" => ColumnTypeName::TimeTz,
16386 // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
16387 // Wire OID 790.
16388 "money" => ColumnTypeName::Money,
16389 // v7.17.0 Phase 3.P0-38 — PG range types.
16390 "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
16391 "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
16392 "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
16393 "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
16394 "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
16395 "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
16396 // v7.37.5 δ — PG 14+ multirange keywords.
16397 "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
16398 "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
16399 "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
16400 "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
16401 "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
16402 "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
16403 // v7.37.5 ε — PG geometry scalar keywords.
16404 "point" => ColumnTypeName::Point,
16405 "lseg" => ColumnTypeName::Lseg,
16406 "path" => ColumnTypeName::Path,
16407 "box" => ColumnTypeName::PgBox,
16408 "polygon" => ColumnTypeName::Polygon,
16409 "line" => ColumnTypeName::Line,
16410 "circle" => ColumnTypeName::Circle,
16411 // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
16412 "inet" => ColumnTypeName::Inet,
16413 "cidr" => ColumnTypeName::Cidr,
16414 "macaddr" => ColumnTypeName::Macaddr,
16415 "macaddr8" => ColumnTypeName::Macaddr8,
16416 // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
16417 // width in the value, so the optional `(N)` typmod is accepted and
16418 // ignored (the column stores whatever width it's given).
16419 "bit" => {
16420 let varying = matches!(
16421 self.peek(),
16422 Token::Ident(k) if k.eq_ignore_ascii_case("varying")
16423 );
16424 if varying {
16425 self.advance();
16426 }
16427 // v7.39 (round 281) — the length used to be parsed and
16428 // dropped, so `bit(3)` accepted a five-bit string.
16429 let n = if matches!(self.peek(), Token::LParen) {
16430 self.parse_paren_size("BIT")?
16431 } else {
16432 0
16433 };
16434 if varying {
16435 ColumnTypeName::BitVarying(n)
16436 } else {
16437 ColumnTypeName::Bit(n)
16438 }
16439 }
16440 "varbit" => {
16441 let n = if matches!(self.peek(), Token::LParen) {
16442 self.parse_paren_size("VARBIT")?
16443 } else {
16444 0
16445 };
16446 ColumnTypeName::BitVarying(n)
16447 }
16448 "xml" => ColumnTypeName::Xml,
16449 // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
16450 "hstore" => ColumnTypeName::Hstore,
16451 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
16452 // `ENUM('a','b','c')`. Storage is TEXT; the value
16453 // list lands on `inline_enum_variants` for the
16454 // engine to validate INSERT cells against. Empty
16455 // value list is a parse error (matches MySQL).
16456 "enum" => {
16457 // Expect the opening `(`.
16458 if !matches!(self.peek(), Token::LParen) {
16459 return Err(self.err(alloc::format!(
16460 "expected '(' after ENUM, got {:?}",
16461 self.peek()
16462 )));
16463 }
16464 self.advance();
16465 let mut variants: Vec<String> = Vec::new();
16466 loop {
16467 match self.advance() {
16468 Token::String(s) => variants.push(s),
16469 other => {
16470 return Err(self.err(alloc::format!(
16471 "ENUM(...) expects string literal variants, got {other:?}"
16472 )));
16473 }
16474 }
16475 match self.peek() {
16476 Token::Comma => {
16477 self.advance();
16478 continue;
16479 }
16480 Token::RParen => {
16481 self.advance();
16482 break;
16483 }
16484 other => {
16485 return Err(self.err(alloc::format!(
16486 "expected ',' or ')' in ENUM(...), got {other:?}"
16487 )));
16488 }
16489 }
16490 }
16491 if variants.is_empty() {
16492 return Err(self.err("ENUM(...) must declare at least one variant".into()));
16493 }
16494 inline_enum_variants = Some(variants);
16495 // Storage is plain TEXT; the variant list lives on
16496 // the ColumnSchema side.
16497 ColumnTypeName::Text
16498 }
16499 // v7.17.0 Phase 3.P0-37 — MySQL inline SET
16500 // `SET('a','b','c')`. Same parse shape as ENUM;
16501 // semantics differ (subset rather than pick-one).
16502 "set" => {
16503 if !matches!(self.peek(), Token::LParen) {
16504 return Err(self.err(alloc::format!(
16505 "expected '(' after SET, got {:?}",
16506 self.peek()
16507 )));
16508 }
16509 self.advance();
16510 let mut variants: Vec<String> = Vec::new();
16511 loop {
16512 match self.advance() {
16513 Token::String(s) => variants.push(s),
16514 other => {
16515 return Err(self.err(alloc::format!(
16516 "SET(...) expects string literal variants, got {other:?}"
16517 )));
16518 }
16519 }
16520 match self.peek() {
16521 Token::Comma => {
16522 self.advance();
16523 continue;
16524 }
16525 Token::RParen => {
16526 self.advance();
16527 break;
16528 }
16529 other => {
16530 return Err(self.err(alloc::format!(
16531 "expected ',' or ')' in SET(...), got {other:?}"
16532 )));
16533 }
16534 }
16535 }
16536 if variants.is_empty() {
16537 return Err(self.err("SET(...) must declare at least one variant".into()));
16538 }
16539 inline_set_variants = Some(variants);
16540 ColumnTypeName::Text
16541 }
16542 _other => {
16543 // v7.17.0 Phase 1.4 — unknown ident → defer
16544 // resolution to the engine. Stored as Text in
16545 // ColumnTypeName + the original name carried as
16546 // `user_type_ref` so CREATE TABLE can look up
16547 // user-defined enum / domain types.
16548 user_type_ref = Some(ty_ident.clone());
16549 ColumnTypeName::Text
16550 }
16551 };
16552 // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
16553 // right after the type keyword. Pre-4.4 SPG consumed +
16554 // discarded the keyword, leaving a customer column
16555 // declared `id INT UNSIGNED NOT NULL` silently accepting
16556 // negative values — a Tier-A correctness drift where
16557 // application invariants (auto-increment-IDs never
16558 // negative) silently broke on cutover. Now: capture as
16559 // a column flag, persist on the schema, enforce at
16560 // INSERT / UPDATE time.
16561 let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
16562 {
16563 self.advance();
16564 true
16565 } else {
16566 false
16567 };
16568 // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
16569 // `<type> COLLATE <name>` post-fixes on text columns. SPG
16570 // stores text as UTF-8 always so CHARACTER SET is still a
16571 // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
16572 // name: it gets classified into a `Collation` variant the
16573 // engine consults at WHERE-eval time. PG `default` /
16574 // `pg_catalog.default` / `C` / `POSIX` collations all
16575 // resolve to `Binary` (the prior behaviour); `_ci` /
16576 // `case_insensitive` / `nocase` shift to CaseInsensitive.
16577 // The schema-qualifier form (`pg_catalog.default`) lexes
16578 // as `Ident '.' Ident` — peek for the `.` and consume both
16579 // halves so it's treated as one collation name. PG's
16580 // `IDENT.IDENT` collation form (which can appear here) is
16581 // resolved by Collation::from_collation_name on the bare
16582 // identifier after the dot.
16583 let mut collation = Collation::Binary;
16584 // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
16585 // clause was written. The engine needs this to tell an explicit
16586 // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
16587 // clause at all: both resolve to `Collation::Binary`, but under the
16588 // MySQL dialect the latter takes the folding default collation.
16589 let mut collation_explicit = false;
16590 let mut collation_name: Option<alloc::string::String> = None;
16591 loop {
16592 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
16593 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
16594 {
16595 self.advance(); // CHARACTER
16596 self.advance(); // SET
16597 if matches!(
16598 self.peek(),
16599 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
16600 ) {
16601 self.advance();
16602 }
16603 continue;
16604 }
16605 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
16606 self.advance(); // COLLATE
16607 // Accept Ident / QuotedIdent / String AND the
16608 // keyword-tokenised `Default` (PG `pg_catalog.default`
16609 // and bare `DEFAULT` collation names — `default` is a
16610 // reserved word so the lexer hands back Token::Default
16611 // not Token::Ident).
16612 let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
16613 match this.peek().clone() {
16614 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
16615 this.advance();
16616 Some(s)
16617 }
16618 Token::Default => {
16619 this.advance();
16620 Some(alloc::string::String::from("default"))
16621 }
16622 _ => None,
16623 }
16624 };
16625 let raw = if let Some(head) = read_collation_atom(self) {
16626 // Schema-qualified PG form: `pg_catalog.default`.
16627 if matches!(self.peek(), Token::Dot) {
16628 self.advance();
16629 let tail = read_collation_atom(self).unwrap_or_default();
16630 alloc::format!("{head}.{tail}")
16631 } else {
16632 head
16633 }
16634 } else {
16635 alloc::string::String::new()
16636 };
16637 if !raw.is_empty() {
16638 collation_explicit = true;
16639 // v7.39 (round 676) — keep the name too. The enum below
16640 // folds C / POSIX / en_US / default into one value, and
16641 // `pg_attribute.attcollation` has to tell them apart.
16642 // The schema qualifier goes: PG's `pg_catalog.default`
16643 // and a bare `default` name the same collation.
16644 // v7.39 (round 679) — strip a SCHEMA qualifier, not an
16645 // encoding suffix. Round 676 used `rsplit('.')` for
16646 // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
16647 // PG writes `pg_catalog.default` (qualifier) and
16648 // `en_US.utf8` (locale + encoding) with the same
16649 // separator. Only `pg_catalog.` is a qualifier, and it
16650 // is the only one PG's own dumps emit.
16651 let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
16652 let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
16653 collation_name = Some(alloc::string::String::from(bare));
16654 let parsed = Collation::from_collation_name(&raw);
16655 // Last COLLATE clause wins, but `Binary` from a
16656 // bare keyword like `default` should not
16657 // silently downgrade a stronger one set earlier
16658 // on the same column. v7.17 only ships one
16659 // non-Binary variant so a simple OR is enough.
16660 if parsed != Collation::Binary {
16661 collation = parsed;
16662 }
16663 }
16664 continue;
16665 }
16666 break;
16667 }
16668 // v7.10.10 — postfix `[]` widens the base type to its array
16669 // type. PG accepts `TYPE[]` after any base type and so does
16670 // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
16671 // all through; the old "only TEXT[]" note was stale).
16672 if matches!(self.peek(), Token::LBracket) {
16673 self.advance();
16674 if !matches!(self.peek(), Token::RBracket) {
16675 return Err(self.err(alloc::format!(
16676 "TEXT[] takes no dimension; got {:?}",
16677 self.peek()
16678 )));
16679 }
16680 self.advance();
16681 // v7.11.13 — widened to INT[] and BIGINT[] in addition
16682 // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
16683 // still error here.
16684 ty = match ty {
16685 ColumnTypeName::Text => ColumnTypeName::TextArray,
16686 ColumnTypeName::Int => ColumnTypeName::IntArray,
16687 ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
16688 // v7.37.5 β-P4 — INTERVAL[] via the same postfix
16689 // `[]` grammar. Wire OID 1187.
16690 ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
16691 // v7.37.5 γ — full PG array-of-scalar family.
16692 ColumnTypeName::Bool => ColumnTypeName::BoolArray,
16693 ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
16694 ColumnTypeName::Float => ColumnTypeName::FloatArray,
16695 // NUMERIC(p, s) loses its precision params at the
16696 // array level (matches PG: `NUMERIC[]` is untyped,
16697 // per-element precision flows through values).
16698 ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
16699 ColumnTypeName::Date => ColumnTypeName::DateArray,
16700 ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
16701 ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
16702 ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
16703 ColumnTypeName::Json => ColumnTypeName::JsonArray,
16704 ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
16705 ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
16706 // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
16707 // the array level (matches PG semantics where the
16708 // element precision is per-row, not column-wide).
16709 ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
16710 ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
16711 // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
16712 // follow-up.
16713 ColumnTypeName::Money => ColumnTypeName::MoneyArray,
16714 other => {
16715 return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
16716 }
16717 };
16718 // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
16719 // for INT/TEXT/BIGINT. Anything else is an error.
16720 if matches!(self.peek(), Token::LBracket) {
16721 self.advance();
16722 if !matches!(self.peek(), Token::RBracket) {
16723 return Err(self.err(alloc::format!(
16724 "TYPE[][] second dimension takes no size; got {:?}",
16725 self.peek()
16726 )));
16727 }
16728 self.advance();
16729 ty = match ty {
16730 ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
16731 ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
16732 ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
16733 // v7.39 (read01 round 75) — bool[][].
16734 ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
16735 other => {
16736 return Err(self.err(alloc::format!(
16737 "v7.17 2D arrays support INT[][] / BIGINT[][] / \
16738 TEXT[][] only; got {other:?}"
16739 )));
16740 }
16741 };
16742 }
16743 }
16744 Ok((
16745 ty,
16746 implied_auto_increment,
16747 implied_not_null,
16748 user_type_ref,
16749 collation,
16750 collation_explicit,
16751 collation_name,
16752 is_unsigned,
16753 inline_enum_variants,
16754 inline_set_variants,
16755 mysql_int_width,
16756 mysql_fsp,
16757 ))
16758 }
16759
16760 fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
16761 // v7.20 — PG reserves the table-constraint keywords, so a
16762 // BARE `UNIQUE` / `PRIMARY` / … in column position is a
16763 // malformed constraint clause (e.g. `UNIQUE a` missing its
16764 // parens), not a column named "unique". Since v7.17's
16765 // unknown-type leniency (`user_type_ref`) such a clause
16766 // would otherwise parse as a column with a user-defined
16767 // type — silently accepting invalid DDL. Quoted
16768 // identifiers ("unique" / `unique`) remain valid names.
16769 if let Token::Ident(s) = self.peek()
16770 && [
16771 "unique",
16772 "primary",
16773 "foreign",
16774 "constraint",
16775 "check",
16776 "references",
16777 "exclude",
16778 ]
16779 .iter()
16780 .any(|kw| s.eq_ignore_ascii_case(kw))
16781 {
16782 return Err(self.err(alloc::format!(
16783 "unexpected reserved keyword '{s}' at start of column definition \
16784 (malformed table constraint?)"
16785 )));
16786 }
16787 let name = self.expect_ident_like()?;
16788 let (
16789 ty,
16790 implied_auto_increment,
16791 implied_not_null,
16792 user_type_ref,
16793 collation,
16794 collation_explicit,
16795 collation_name,
16796 is_unsigned,
16797 inline_enum_variants,
16798 inline_set_variants,
16799 mysql_int_width,
16800 mysql_fsp,
16801 ) = self.parse_type_with_implied_flags()?;
16802 // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
16803 // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
16804 // each at most once.
16805 let mut default: Option<Expr> = None;
16806 let mut nullable = !implied_not_null;
16807 let mut nullability_seen = implied_not_null;
16808 let mut auto_increment = implied_auto_increment;
16809 let mut is_primary_key = false;
16810 let mut is_unique = false;
16811 let mut unique_nulls_not_distinct = false;
16812 let mut constraint_deferrable = false;
16813 let mut constraint_initially_deferred = false;
16814 let mut check: Option<Expr> = None;
16815 let mut on_update_runtime: Option<Expr> = None;
16816 let mut generated_stored_expr: Option<Box<Expr>> = None;
16817 let mut identity_always = false;
16818 loop {
16819 // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
16820 // not-null constraints by name and pg_dump emits them
16821 // inline: `id bigint CONSTRAINT contacts_id_not_null1
16822 // NOT NULL`. Accept and discard the name; whatever
16823 // constraint follows is parsed by the arms below.
16824 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16825 // v7.39 (round 308, V29) — a name on an inline
16826 // REFERENCES belongs to the FOREIGN KEY, and the caller
16827 // (`parse_column_def_with_fk`) is what builds it, so
16828 // leave the whole clause for it. Dropping the name here
16829 // is what made `CONSTRAINT fk_a REFERENCES …` come back
16830 // as the synthesised `c_pid_fkey` — which then could
16831 // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
16832 // `advance()` takes tokens by `mem::replace`, so there
16833 // is no rewinding once consumed.
16834 if matches!(
16835 self.tokens.get(self.pos + 2),
16836 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
16837 ) {
16838 break;
16839 }
16840 self.advance();
16841 let _name = self.expect_ident_like()?;
16842 continue;
16843 }
16844 // v7.39 (round 379) — MySQL's SHORT generated-column form
16845 // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
16846 // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
16847 // below), but hand-written schemas and app migrations use this.
16848 // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
16849 // SPG computes-and-stores either way, like the long form.
16850 if matches!(self.peek(), Token::As) {
16851 self.advance();
16852 if !matches!(self.peek(), Token::LParen) {
16853 return Err(self.err(alloc::format!(
16854 "expected '(' after AS in a generated column, got {:?}",
16855 self.peek()
16856 )));
16857 }
16858 self.advance();
16859 let expr = self.parse_expr(0)?;
16860 if !matches!(self.peek(), Token::RParen) {
16861 return Err(self.err(alloc::format!(
16862 "expected ')' after AS (<expr>), got {:?}",
16863 self.peek()
16864 )));
16865 }
16866 self.advance();
16867 if matches!(self.peek(), Token::Ident(s)
16868 if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
16869 {
16870 self.advance();
16871 }
16872 generated_stored_expr = Some(alloc::boxed::Box::new(expr));
16873 continue;
16874 }
16875 // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
16876 // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
16877 // the modern replacement for SERIAL in hand-written
16878 // schemas). Both flavours map onto the auto-increment
16879 // machinery — SPG's serial semantics ≈ BY DEFAULT;
16880 // ALWAYS's reject-explicit-values nuance is documented
16881 // leniency. Generated EXPRESSION columns
16882 // (`AS (expr) STORED`) are not supported: error loudly
16883 // instead of silently storing NULLs.
16884 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
16885 self.advance();
16886 let mut saw_generated_always = false;
16887 match self.peek().clone() {
16888 Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
16889 self.advance();
16890 saw_generated_always = true;
16891 }
16892 Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
16893 self.advance();
16894 if !matches!(self.peek(), Token::Default) {
16895 return Err(self.err(alloc::format!(
16896 "expected DEFAULT after GENERATED BY, got {:?}",
16897 self.peek()
16898 )));
16899 }
16900 self.advance();
16901 }
16902 other => {
16903 return Err(self.err(alloc::format!(
16904 "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
16905 )));
16906 }
16907 }
16908 if !matches!(self.peek(), Token::As) {
16909 return Err(self.err(alloc::format!(
16910 "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
16911 self.peek()
16912 )));
16913 }
16914 self.advance();
16915 // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
16916 // ( <expr> ) STORED` stored computed-column. The
16917 // expression is captured for the engine to recompute
16918 // on every INSERT / UPDATE. v7.37.7 accepts the
16919 // STORED keyword only; PG also has VIRTUAL, which
16920 // v7.37.7 carves out (sentori only uses STORED).
16921 if matches!(self.peek(), Token::LParen) {
16922 self.advance();
16923 let expr = self.parse_expr(0)?;
16924 if !matches!(self.peek(), Token::RParen) {
16925 return Err(self.err(alloc::format!(
16926 "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
16927 self.peek()
16928 )));
16929 }
16930 self.advance();
16931 let stored = match self.peek() {
16932 Token::Ident(s) | Token::QuotedIdent(s)
16933 if s.eq_ignore_ascii_case("stored") =>
16934 {
16935 self.advance();
16936 true
16937 }
16938 // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
16939 // generated columns. SPG computes them on write and
16940 // persists like STORED; the two are observably
16941 // identical for query results (the value, recompute
16942 // on base-column change, and NOT NULL enforcement all
16943 // match), so a PG 18 schema/dump using VIRTUAL loads
16944 // and behaves correctly. The compute-on-read storage
16945 // saving is an invisible internal difference.
16946 Token::Ident(s) | Token::QuotedIdent(s)
16947 if s.eq_ignore_ascii_case("virtual") =>
16948 {
16949 self.advance();
16950 false
16951 }
16952 other => {
16953 return Err(self.err(alloc::format!(
16954 "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
16955 got {other:?}"
16956 )));
16957 }
16958 };
16959 let _ = stored; // STORED / VIRTUAL both compute-and-store.
16960 generated_stored_expr = Some(Box::new(expr));
16961 continue;
16962 }
16963 self.expect_keyword_ident("identity")?;
16964 // Optional `(START WITH 1 INCREMENT BY 1 …)` —
16965 // consume the balanced parens and discard (SPG's
16966 // auto-increment is max+1-scan based).
16967 if matches!(self.peek(), Token::LParen) {
16968 let mut depth = 0usize;
16969 loop {
16970 match self.advance() {
16971 Token::LParen => depth += 1,
16972 Token::RParen => {
16973 depth -= 1;
16974 if depth == 0 {
16975 break;
16976 }
16977 }
16978 Token::Eof => {
16979 return Err(self.err(
16980 "unterminated sequence-options parens after IDENTITY".into(),
16981 ));
16982 }
16983 _ => {}
16984 }
16985 }
16986 }
16987 auto_increment = true;
16988 // v7.38 (read01) — remember the ALWAYS flavour so the engine
16989 // can reject explicit non-DEFAULT INSERT values (unless
16990 // OVERRIDING SYSTEM VALUE) the way PG does.
16991 identity_always = saw_generated_always;
16992 // PG identity columns are implicitly NOT NULL.
16993 nullable = false;
16994 continue;
16995 }
16996 // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
16997 // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
16998 // is accepted today. The "ON" token is an Ident
16999 // (not reserved) — peek before consuming.
17000 if matches!(self.peek(), Token::On)
17001 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17002 {
17003 self.advance(); // ON
17004 self.advance(); // update
17005 // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17006 let next = self.peek().clone();
17007 match next {
17008 Token::Ident(s) | Token::QuotedIdent(s)
17009 if s.eq_ignore_ascii_case("current_timestamp") =>
17010 {
17011 self.advance();
17012 // Optional `(N)` precision.
17013 if matches!(self.peek(), Token::LParen) {
17014 self.advance();
17015 if !matches!(self.peek(), Token::Integer(_)) {
17016 return Err(self.err(alloc::format!(
17017 "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17018 self.peek()
17019 )));
17020 }
17021 self.advance();
17022 if !matches!(self.peek(), Token::RParen) {
17023 return Err(self.err(alloc::format!(
17024 "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17025 self.peek()
17026 )));
17027 }
17028 self.advance();
17029 }
17030 on_update_runtime = Some(Expr::FunctionCall {
17031 name: "now".into(),
17032 args: Vec::new(),
17033 });
17034 continue;
17035 }
17036 other => {
17037 return Err(self.err(alloc::format!(
17038 "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17039 )));
17040 }
17041 }
17042 }
17043 if matches!(self.peek(), Token::Default) {
17044 if default.is_some() {
17045 return Err(self.err("DEFAULT specified twice".into()));
17046 }
17047 self.advance();
17048 default = Some(self.parse_expr(0)?);
17049 continue;
17050 }
17051 // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17052 // token with NOT NULL and sits EARLIER in the loop than the
17053 // deferrability arm, so without the lookahead it was reported as
17054 // "NOT NULL specified twice" (or "expected NULL after NOT").
17055 if matches!(self.peek(), Token::Not)
17056 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17057 {
17058 // NOT DEFERRABLE — explicit immediate; nothing to carry.
17059 self.consume_optional_deferrable_clauses()?;
17060 continue;
17061 }
17062 if matches!(self.peek(), Token::Not) {
17063 if nullability_seen {
17064 return Err(self.err("NOT NULL specified twice".into()));
17065 }
17066 self.advance();
17067 if !matches!(self.peek(), Token::Null) {
17068 return Err(self.err(format!(
17069 "expected NULL after NOT in column def, got {:?}",
17070 self.peek()
17071 )));
17072 }
17073 self.advance();
17074 nullable = false;
17075 nullability_seen = true;
17076 continue;
17077 }
17078 // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17079 // "this column is nullable" marker (the default in
17080 // standard SQL anyway). mysqldump emits it routinely
17081 // (`col TYPE NULL DEFAULT NULL` for nullable
17082 // timestamps etc). Accept + no-op.
17083 if matches!(self.peek(), Token::Null) {
17084 if nullability_seen && !nullable {
17085 // v7.39 (round 761, F31 tranche 2 #31) — PG's
17086 // sentence, PG18-measured (the table name is the
17087 // caller's; the column half is exact).
17088 return Err(self.err(alloc::format!(
17089 "conflicting NULL/NOT NULL declarations for column \"{name}\""
17090 )));
17091 }
17092 self.advance();
17093 nullable = true;
17094 nullability_seen = true;
17095 continue;
17096 }
17097 // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17098 // arrives as a bare Ident. Match either, case-insensitive.
17099 if let Token::Ident(s) = self.peek()
17100 && (s.eq_ignore_ascii_case("auto_increment")
17101 || s.eq_ignore_ascii_case("autoincrement"))
17102 {
17103 if auto_increment {
17104 return Err(self.err("AUTO_INCREMENT specified twice".into()));
17105 }
17106 self.advance();
17107 auto_increment = true;
17108 continue;
17109 }
17110 // v7.9.13 — inline `PRIMARY KEY` column constraint
17111 // (mailrs F1). Implies `NOT NULL`. The engine creates
17112 // a BTree index for the PK column at CREATE TABLE time
17113 // so FK parent-side index lookups resolve.
17114 // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17115 // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17116 // spelling was a parse error, so a pg_dump carrying one stopped
17117 // mid-restore. The clauses are consumed by the same helper the FK
17118 // path has used since round 288 and recorded nowhere: SPG enforces
17119 // the constraint IMMEDIATELY either way, which fails earlier than
17120 // PG inside a transaction that violates-then-repairs — a refusal,
17121 // not a wrong answer. True deferral is the open remainder of F08.
17122 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17123 || (matches!(self.peek(), Token::Not)
17124 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
17125 {
17126 // v7.39 (round 711) — CARRIED now (the storing half of
17127 // F08); round 621 only consumed.
17128 let (d, idef) = self.consume_deferrable_clauses_timed()?;
17129 constraint_deferrable |= d;
17130 constraint_initially_deferred |= idef;
17131 continue;
17132 }
17133 if let Token::Ident(s) = self.peek()
17134 && s.eq_ignore_ascii_case("primary")
17135 {
17136 if is_primary_key {
17137 return Err(self.err("PRIMARY KEY specified twice".into()));
17138 }
17139 // Peek-ahead for the required `KEY` token.
17140 let next = self.tokens.get(self.pos + 1);
17141 let next_is_key = matches!(
17142 next,
17143 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17144 );
17145 if !next_is_key {
17146 return Err(self.err(format!(
17147 "expected KEY after PRIMARY in column def, got {:?}",
17148 next
17149 )));
17150 }
17151 self.advance(); // PRIMARY
17152 self.advance(); // KEY
17153 is_primary_key = true;
17154 if nullability_seen && nullable {
17155 return Err(self.err(
17156 "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17157 ));
17158 }
17159 nullable = false;
17160 nullability_seen = true;
17161 continue;
17162 }
17163 // v7.13.0 — inline `UNIQUE` column constraint
17164 // (mailrs round-5 G2). Fold into a single-column
17165 // table-level UNIQUE at CREATE TABLE post-process time.
17166 if let Token::Ident(s) = self.peek()
17167 && s.eq_ignore_ascii_case("unique")
17168 {
17169 if is_unique {
17170 return Err(self.err("UNIQUE specified twice".into()));
17171 }
17172 self.advance();
17173 is_unique = true;
17174 // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17175 // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17176 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17177 let n1 = self.tokens.get(self.pos + 1);
17178 let n2 = self.tokens.get(self.pos + 2);
17179 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17180 self.advance(); // NULLS
17181 self.advance(); // NOT
17182 self.advance(); // DISTINCT
17183 unique_nulls_not_distinct = true;
17184 } else if matches!(n1, Some(Token::Distinct)) {
17185 self.advance(); // NULLS
17186 self.advance(); // DISTINCT
17187 }
17188 }
17189 continue;
17190 }
17191 // v7.13.0 — inline `CHECK (<expr>)` column constraint
17192 // (mailrs round-5 G3). PG semantics: column-level
17193 // CHECK is equivalent to a table-level CHECK. Multiple
17194 // inline CHECKs on the same column AND together.
17195 if let Token::Ident(s) = self.peek()
17196 && s.eq_ignore_ascii_case("check")
17197 {
17198 self.advance();
17199 if !matches!(self.peek(), Token::LParen) {
17200 return Err(self.err(alloc::format!(
17201 "expected '(' after CHECK in column def, got {:?}",
17202 self.peek()
17203 )));
17204 }
17205 self.advance();
17206 let pred = self.parse_expr(0)?;
17207 if !matches!(self.peek(), Token::RParen) {
17208 return Err(self.err(alloc::format!(
17209 "expected ')' to close CHECK predicate, got {:?}",
17210 self.peek()
17211 )));
17212 }
17213 self.advance();
17214 check = Some(match check.take() {
17215 Some(prev) => Expr::Binary {
17216 op: BinOp::And,
17217 lhs: Box::new(prev),
17218 rhs: Box::new(pred),
17219 },
17220 None => pred,
17221 });
17222 continue;
17223 }
17224 break;
17225 }
17226 Ok(ColumnDef {
17227 name,
17228 ty,
17229 nullable,
17230 default,
17231 auto_increment,
17232 is_primary_key,
17233 is_unique,
17234 unique_nulls_not_distinct,
17235 constraint_deferrable,
17236 constraint_initially_deferred,
17237 check,
17238 user_type_ref,
17239 on_update_runtime,
17240 collation,
17241 collation_explicit,
17242 collation_name,
17243 is_unsigned,
17244 inline_enum_variants,
17245 inline_set_variants,
17246 generated_stored_expr,
17247 identity_always,
17248 mysql_int_width,
17249 mysql_fsp,
17250 })
17251 }
17252
17253 /// `NUMERIC` may appear without parameters, with one (precision
17254 /// only, scale=0), or with both. Returns `(precision, scale)` with
17255 /// 0 = unspecified for the bare form.
17256 fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17257 if !matches!(self.peek(), Token::LParen) {
17258 // Bare `NUMERIC` — PG treats this as "unlimited precision";
17259 // we surface it as precision=0 to mean "unconstrained" so
17260 // the engine doesn't need a separate variant.
17261 return Ok((0, 0));
17262 }
17263 self.advance();
17264 // v7.39 (round 272) — PG's declared precision runs to 1000, and
17265 // it words the out-of-range case with the value it saw. SPG
17266 // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17267 // accepts failed to parse at all; values wider than i128 are
17268 // carried by the arbitrary-precision form.
17269 let precision = match self.advance() {
17270 Token::Integer(n) if (1..=1000).contains(&n) => {
17271 u16::try_from(n).expect("range-checked")
17272 }
17273 Token::Integer(n) => {
17274 return Err(ParseError {
17275 message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17276 token_pos: self.consumed_pos(),
17277 });
17278 }
17279 other => {
17280 return Err(ParseError {
17281 message: format!(
17282 "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17283 ),
17284 token_pos: self.consumed_pos(),
17285 });
17286 }
17287 };
17288 // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17289 // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17290 // then overflows). A negative scale rounds to tens / hundreds / …
17291 let scale = if matches!(self.peek(), Token::Comma) {
17292 self.advance();
17293 let neg = if matches!(self.peek(), Token::Minus) {
17294 self.advance();
17295 true
17296 } else {
17297 false
17298 };
17299 match self.advance() {
17300 Token::Integer(n) => {
17301 let signed = if neg { -n } else { n };
17302 if !(-1000..=1000).contains(&signed) {
17303 return Err(ParseError {
17304 message: format!(
17305 "NUMERIC scale {signed} must be between -1000 and 1000"
17306 ),
17307 token_pos: self.consumed_pos(),
17308 });
17309 }
17310 i16::try_from(signed).expect("range-checked")
17311 }
17312 other => {
17313 return Err(ParseError {
17314 message: format!("NUMERIC scale must be an integer, got {other:?}"),
17315 token_pos: self.consumed_pos(),
17316 });
17317 }
17318 }
17319 } else {
17320 0
17321 };
17322 if !matches!(self.peek(), Token::RParen) {
17323 return Err(self.err(format!(
17324 "expected ')' to close NUMERIC params, got {:?}",
17325 self.peek()
17326 )));
17327 }
17328 self.advance();
17329 Ok((precision, scale))
17330 }
17331
17332 /// Parse `(N)` where `N` is a positive integer literal — used by the
17333 /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
17334 /// for the error message.
17335 /// v6.0.1: parse the optional `USING <encoding>` clause that
17336 /// follows `VECTOR(N)` in a column definition. Missing clause
17337 /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
17338 /// ident → `ParseError` listing the encodings recognised today.
17339 fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
17340 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
17341 return Ok(VecEncoding::F32);
17342 }
17343 // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
17344 // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
17345 // consume the token when the very next token is a known
17346 // vector-encoding keyword (SQ8 / HALF). Otherwise leave
17347 // `USING` for the caller — it's the rewrite-expression form.
17348 let n1 = self.tokens.get(self.pos + 1);
17349 let next_is_encoding = matches!(
17350 n1,
17351 Some(Token::Ident(s))
17352 if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
17353 );
17354 if !next_is_encoding {
17355 return Ok(VecEncoding::F32);
17356 }
17357 self.advance();
17358 let enc_ident = match self.advance() {
17359 Token::Ident(s) => s,
17360 other => {
17361 return Err(self.err(format!(
17362 "expected vector encoding after USING, got {other:?}"
17363 )));
17364 }
17365 };
17366 match enc_ident.to_ascii_lowercase().as_str() {
17367 "sq8" => Ok(VecEncoding::Sq8),
17368 // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
17369 // binary16 per-element storage.
17370 "half" => Ok(VecEncoding::F16),
17371 other => Err(self.err(format!(
17372 "unknown vector encoding {other:?}; supported: SQ8, HALF"
17373 ))),
17374 }
17375 }
17376
17377 /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
17378 /// without consuming it. Returns `Some(N)` when the next
17379 /// tokens are `( <int> )`; None otherwise. Used by the
17380 /// TINYINT classifier to decide whether to map to Bool or
17381 /// SmallInt.
17382 fn peek_optional_paren_size_value(&self) -> Option<i64> {
17383 if !matches!(self.peek(), Token::LParen) {
17384 return None;
17385 }
17386 let next = self.tokens.get(self.pos + 1)?;
17387 let n = match next {
17388 Token::Integer(n) => *n,
17389 _ => return None,
17390 };
17391 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17392 return None;
17393 }
17394 Some(n)
17395 }
17396
17397 /// v7.14.0 — consume an optional MySQL display-width
17398 /// parenthesised number after an integer type, returning
17399 /// nothing. `TINYINT(1)` etc.
17400 /// v7.39 (round 360) — does the parenthesised group ahead contain a
17401 /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
17402 fn peek_paren_has_comma(&self) -> bool {
17403 let mut i = self.pos + 1;
17404 let mut depth = 1usize;
17405 while depth > 0 {
17406 match self.tokens.get(i) {
17407 Some(Token::LParen) => depth += 1,
17408 Some(Token::RParen) => depth -= 1,
17409 Some(Token::Comma) if depth == 1 => return true,
17410 None | Some(Token::Eof) => return false,
17411 _ => {}
17412 }
17413 i += 1;
17414 }
17415 false
17416 }
17417
17418 /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
17419 /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
17420 /// fractional-seconds precision that drives write truncation and render
17421 /// padding, where `consume_optional_paren_size` throws it away.
17422 /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
17423 fn take_optional_paren_size(&mut self) -> Option<u8> {
17424 let Some(Token::Integer(n)) = self
17425 .tokens
17426 .get(self.pos + 1)
17427 .filter(|_| matches!(self.peek(), Token::LParen))
17428 .cloned()
17429 else {
17430 self.consume_optional_paren_size();
17431 return None;
17432 };
17433 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17434 self.consume_optional_paren_size();
17435 return None;
17436 }
17437 self.consume_optional_paren_size();
17438 u8::try_from(n).ok()
17439 }
17440
17441 fn consume_optional_paren_size(&mut self) {
17442 if !matches!(self.peek(), Token::LParen) {
17443 return;
17444 }
17445 self.advance();
17446 // Skip until matching RParen (allow nested or any tokens).
17447 let mut depth = 1usize;
17448 while depth > 0 {
17449 match self.peek() {
17450 Token::LParen => depth += 1,
17451 Token::RParen => depth -= 1,
17452 Token::Eof => return,
17453 _ => {}
17454 }
17455 self.advance();
17456 }
17457 }
17458
17459 fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
17460 if !matches!(self.peek(), Token::LParen) {
17461 return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
17462 }
17463 self.advance();
17464 let n = match self.advance() {
17465 Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
17466 message: format!("{label} size too large: {n}"),
17467 token_pos: self.consumed_pos(),
17468 })?,
17469 other => {
17470 return Err(ParseError {
17471 message: format!("expected positive integer {label} size, got {other:?}"),
17472 token_pos: self.consumed_pos(),
17473 });
17474 }
17475 };
17476 if !matches!(self.peek(), Token::RParen) {
17477 return Err(self.err(format!(
17478 "expected ')' after {label} size, got {:?}",
17479 self.peek()
17480 )));
17481 }
17482 self.advance();
17483 Ok(n)
17484 }
17485
17486 /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
17487 /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
17488 /// key, like MySQL) whose action skips conflicting rows.
17489 /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
17490 /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
17491 /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
17492 /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
17493 /// common bulk-upsert spellings —
17494 /// INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
17495 /// REPLACE INTO t SELECT …
17496 /// — were a parse error / a duplicate-key failure respectively.
17497 ///
17498 /// Precedence: an explicitly written clause beats a statement-level flag.
17499 /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
17500 /// implicit `REPLACE` and `IGNORE` lowerings.
17501 fn parse_insert_conflict_clause(
17502 &mut self,
17503 replace: bool,
17504 ignore: bool,
17505 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17506 if let Some(c) = self.parse_optional_on_duplicate_key()? {
17507 return Ok(Some(c));
17508 }
17509 if let Some(c) = self.parse_optional_on_conflict()? {
17510 return Ok(Some(c));
17511 }
17512 if replace {
17513 // REPLACE INTO = delete-then-insert, which PG spells as
17514 // `ON CONFLICT DO UPDATE SET` over every column; the engine
17515 // reads an empty assignment list as "take the incoming row".
17516 return Ok(Some(crate::ast::OnConflictClause {
17517 target_columns: Vec::new(),
17518 index_where: None,
17519 constraint_name: None,
17520 mysql_lowered: true,
17521 action: crate::ast::OnConflictAction::Update {
17522 assignments: Vec::new(),
17523 where_: None,
17524 },
17525 }));
17526 }
17527 if ignore {
17528 return Ok(Some(Self::insert_ignore_clause()));
17529 }
17530 Ok(None)
17531 }
17532
17533 /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
17534 /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
17535 /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
17536 /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
17537 fn parse_optional_on_duplicate_key(
17538 &mut self,
17539 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17540 if !(matches!(self.peek(), Token::On)
17541 && matches!(self.tokens.get(self.pos + 1),
17542 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
17543 {
17544 return Ok(None);
17545 }
17546 self.advance(); // ON
17547 self.advance(); // DUPLICATE
17548 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
17549 return Err(self.err(format!(
17550 "expected KEY after ON DUPLICATE, got {:?}",
17551 self.peek()
17552 )));
17553 }
17554 self.advance();
17555 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
17556 return Err(self.err(format!(
17557 "expected UPDATE after ON DUPLICATE KEY, got {:?}",
17558 self.peek()
17559 )));
17560 }
17561 self.advance();
17562 let mut assignments: Vec<(String, Expr)> = Vec::new();
17563 loop {
17564 let col = self.expect_ident_like()?;
17565 if !matches!(self.peek(), Token::Eq) {
17566 return Err(self.err(format!(
17567 "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
17568 self.peek()
17569 )));
17570 }
17571 self.advance();
17572 let mut expr = self.parse_expr(0)?;
17573 Self::rewrite_mysql_values_refs(&mut expr);
17574 assignments.push((col, expr));
17575 if matches!(self.peek(), Token::Comma) {
17576 self.advance();
17577 continue;
17578 }
17579 break;
17580 }
17581 Ok(Some(crate::ast::OnConflictClause {
17582 target_columns: Vec::new(),
17583 index_where: None,
17584 constraint_name: None,
17585 mysql_lowered: true,
17586 action: crate::ast::OnConflictAction::Update {
17587 assignments,
17588 where_: None,
17589 },
17590 }))
17591 }
17592
17593 fn insert_ignore_clause() -> crate::ast::OnConflictClause {
17594 crate::ast::OnConflictClause {
17595 target_columns: Vec::new(),
17596 index_where: None,
17597 constraint_name: None,
17598 mysql_lowered: true,
17599 action: crate::ast::OnConflictAction::Nothing,
17600 }
17601 }
17602
17603 fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
17604 debug_assert!(
17605 matches!(self.peek(), Token::Insert)
17606 || (replace
17607 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
17608 );
17609 self.advance();
17610 // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
17611 // would raise a duplicate-key error instead of failing the statement,
17612 // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
17613 // plain ident to the lexer; only the MySQL dialect accepts it here.
17614 let ignore = self.mysql_dialect
17615 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
17616 if ignore {
17617 self.advance();
17618 }
17619 if !matches!(self.peek(), Token::Into) {
17620 return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
17621 }
17622 self.advance();
17623 let table = self.expect_ident_like()?;
17624 // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
17625 // grammar requires the AS keyword here (a bare identifier would be
17626 // ambiguous with a column list). The alias is what the ON CONFLICT
17627 // DO UPDATE expressions refer to the target row by.
17628 let alias = if matches!(self.peek(), Token::As) {
17629 self.advance();
17630 Some(self.expect_ident_like()?)
17631 } else {
17632 None
17633 };
17634 // v7.39 (round 428) — MySQL's SET-form INSERT:
17635 // INSERT INTO t SET a = 1, b = 'x'
17636 // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
17637 // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
17638 // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
17639 // measured). So it lowers to the column list + one VALUES row and
17640 // rejoins the ordinary path, which already handles every one of
17641 // those. PG has no such spelling, hence the dialect gate.
17642 if self.mysql_dialect
17643 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
17644 {
17645 self.advance(); // SET
17646 let mut names = Vec::new();
17647 let mut values = Vec::new();
17648 loop {
17649 names.push(self.expect_ident_like()?);
17650 if !matches!(self.peek(), Token::Eq) {
17651 return Err(self.err(alloc::format!(
17652 "expected '=' in INSERT … SET, got {:?}",
17653 self.peek()
17654 )));
17655 }
17656 self.advance();
17657 // `SET a = DEFAULT` rides the same `__column_default` marker
17658 // the VALUES-row and UPDATE-SET paths use; the INSERT
17659 // executor resolves it against the target column.
17660 if matches!(self.peek(), Token::Default) {
17661 self.advance();
17662 values.push(Expr::FunctionCall {
17663 name: "__column_default".to_string(),
17664 args: Vec::new(),
17665 });
17666 } else {
17667 values.push(self.parse_expr(0)?);
17668 }
17669 if matches!(self.peek(), Token::Comma) {
17670 self.advance();
17671 continue;
17672 }
17673 break;
17674 }
17675 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17676 let returning = self.parse_optional_returning()?;
17677 return Ok(Statement::Insert(InsertStatement {
17678 ctes: Vec::new(),
17679 table,
17680 alias,
17681 columns: Some(names),
17682 rows: alloc::vec![values],
17683 select_source: None,
17684 // MySQL's SET form has no `OVERRIDING …` clause (that is
17685 // PG's identity-column spelling).
17686 overriding: Overriding::None,
17687 mysql_ignore: ignore,
17688 on_conflict,
17689 returning,
17690 }));
17691 }
17692 // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
17693 // v7.39 (round 151) — a SELECT or WITH right after the paren is
17694 // a parenthesized query source instead (PG select_with_parens:
17695 // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
17696 // both keywords are reserved in PG, so no column list can start
17697 // with them.
17698 let columns = if matches!(self.peek(), Token::LParen) {
17699 self.advance();
17700 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17701 let select_stmt = if self.peek_is_with_kw() {
17702 self.advance();
17703 self.parse_nested_with_select()?
17704 } else {
17705 match self.parse_select_stmt()? {
17706 Statement::Select(s) => s,
17707 other => {
17708 return Err(self.err(alloc::format!(
17709 "expected SELECT in parenthesized INSERT source, got {other:?}"
17710 )));
17711 }
17712 }
17713 };
17714 if !matches!(self.peek(), Token::RParen) {
17715 return Err(self.err(format!(
17716 "expected ')' after parenthesized INSERT source, got {:?}",
17717 self.peek()
17718 )));
17719 }
17720 self.advance();
17721 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17722 let returning = self.parse_optional_returning()?;
17723 return Ok(Statement::Insert(InsertStatement {
17724 ctes: Vec::new(),
17725 table,
17726 alias: alias.clone(),
17727 columns: None,
17728 rows: Vec::new(),
17729 select_source: Some(Box::new(select_stmt)),
17730 on_conflict,
17731 returning,
17732 overriding: Overriding::None,
17733 mysql_ignore: ignore,
17734 }));
17735 }
17736 let mut names = Vec::new();
17737 loop {
17738 names.push(self.expect_ident_like()?);
17739 match self.peek() {
17740 Token::Comma => {
17741 self.advance();
17742 }
17743 Token::RParen => {
17744 self.advance();
17745 break;
17746 }
17747 other => {
17748 return Err(self.err(format!(
17749 "expected ',' or ')' in INSERT column list, got {other:?}"
17750 )));
17751 }
17752 }
17753 }
17754 Some(names)
17755 } else {
17756 None
17757 };
17758 // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
17759 // OVERRIDING SYSTEM VALUE for its identity columns. The clause
17760 // is captured on the statement so the engine can apply PG's
17761 // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
17762 let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
17763 {
17764 self.advance();
17765 let which = self.expect_ident_like()?;
17766 let ov = if which.eq_ignore_ascii_case("system") {
17767 Overriding::System
17768 } else if which.eq_ignore_ascii_case("user") {
17769 Overriding::User
17770 } else {
17771 return Err(self.err(format!(
17772 "expected SYSTEM or USER after OVERRIDING, got {which:?}"
17773 )));
17774 };
17775 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
17776 return Err(self.err(format!(
17777 "expected VALUE after OVERRIDING {}, got {:?}",
17778 which.to_ascii_uppercase(),
17779 self.peek()
17780 )));
17781 }
17782 self.advance();
17783 ov
17784 } else {
17785 Overriding::None
17786 };
17787 // `INSERT INTO t DEFAULT VALUES` — a single row made
17788 // entirely of column defaults. Lower to the permuted
17789 // column-list path with an empty list: every schema column
17790 // is unmapped, so the engine fills each from its default
17791 // (serials advance, plain defaults evaluate, the rest NULL).
17792 if matches!(self.peek(), Token::Default) {
17793 self.advance();
17794 if !matches!(self.peek(), Token::Values) {
17795 return Err(self.err(format!(
17796 "expected VALUES after DEFAULT in INSERT, got {:?}",
17797 self.peek()
17798 )));
17799 }
17800 self.advance();
17801 if columns.is_some() {
17802 return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
17803 }
17804 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17805 let returning = self.parse_optional_returning()?;
17806 return Ok(Statement::Insert(InsertStatement {
17807 ctes: Vec::new(),
17808 table,
17809 alias: alias.clone(),
17810 columns: Some(Vec::new()),
17811 rows: alloc::vec![Vec::new()],
17812 select_source: None,
17813 on_conflict,
17814 returning,
17815 overriding,
17816 mysql_ignore: ignore,
17817 }));
17818 }
17819 // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
17820 // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
17821 // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
17822 // SELECT …`) heads the SOURCE select, as in PG (the statement's
17823 // own WITH comes before INSERT).
17824 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17825 let select_stmt = if self.peek_is_with_kw() {
17826 self.advance();
17827 self.parse_nested_with_select()?
17828 } else {
17829 match self.parse_select_stmt()? {
17830 Statement::Select(s) => s,
17831 other => {
17832 return Err(self.err(alloc::format!(
17833 "expected SELECT after INSERT INTO ... target, got {other:?}"
17834 )));
17835 }
17836 }
17837 };
17838 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17839 let returning = self.parse_optional_returning()?;
17840 return Ok(Statement::Insert(InsertStatement {
17841 ctes: Vec::new(),
17842 table,
17843 alias: alias.clone(),
17844 columns,
17845 rows: Vec::new(),
17846 select_source: Some(Box::new(select_stmt)),
17847 on_conflict,
17848 returning,
17849 overriding,
17850 mysql_ignore: ignore,
17851 }));
17852 }
17853 if !matches!(self.peek(), Token::Values) {
17854 return Err(self.err(format!(
17855 "expected VALUES or SELECT after table name, got {:?}",
17856 self.peek()
17857 )));
17858 }
17859 self.advance();
17860 if !matches!(self.peek(), Token::LParen) {
17861 return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
17862 }
17863 let mut rows = Vec::new();
17864 loop {
17865 // Each iteration consumes one `(expr, expr, …)` tuple.
17866 if !matches!(self.peek(), Token::LParen) {
17867 return Err(self.err(format!(
17868 "expected '(' for next VALUES tuple, got {:?}",
17869 self.peek()
17870 )));
17871 }
17872 self.advance();
17873 let mut tuple = Vec::new();
17874 loop {
17875 // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
17876 // the column's declared default for that slot. Rides out as the
17877 // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
17878 // path uses; the INSERT executor resolves it per target column.
17879 if matches!(self.peek(), Token::Default) {
17880 self.advance();
17881 tuple.push(Expr::FunctionCall {
17882 name: "__column_default".to_string(),
17883 args: Vec::new(),
17884 });
17885 } else {
17886 tuple.push(self.parse_expr(0)?);
17887 }
17888 match self.peek() {
17889 Token::Comma => {
17890 self.advance();
17891 }
17892 Token::RParen => {
17893 self.advance();
17894 break;
17895 }
17896 other => {
17897 return Err(self.err(format!(
17898 "expected ',' or ')' in VALUES tuple, got {other:?}"
17899 )));
17900 }
17901 }
17902 }
17903 if tuple.is_empty() {
17904 return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
17905 }
17906 rows.push(tuple);
17907 // Continue with comma-separated tuples.
17908 if matches!(self.peek(), Token::Comma) {
17909 self.advance();
17910 } else {
17911 break;
17912 }
17913 }
17914 // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
17915 // to ON CONFLICT DO UPDATE with an empty conflict target
17916 // (the engine picks the table's first unique index, which
17917 // matches MySQL's any-unique-key behaviour for the common
17918 // single-key case). `VALUES(col)` in the assignments is
17919 // MySQL's spelling of EXCLUDED.col.
17920 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17921 let returning = self.parse_optional_returning()?;
17922 Ok(Statement::Insert(InsertStatement {
17923 ctes: Vec::new(),
17924 table,
17925 alias,
17926 columns,
17927 rows,
17928 select_source: None,
17929 on_conflict,
17930 returning,
17931 overriding,
17932 mysql_ignore: ignore,
17933 }))
17934 }
17935
17936 /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
17937 /// the incoming row's value — exactly PG's EXCLUDED.col.
17938 fn rewrite_mysql_values_refs(e: &mut Expr) {
17939 match e {
17940 Expr::FunctionCall { name, args }
17941 if name.eq_ignore_ascii_case("values")
17942 && args.len() == 1
17943 && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
17944 {
17945 let Expr::Column(c) = &args[0] else {
17946 unreachable!("guarded above");
17947 };
17948 *e = Expr::Column(crate::ast::ColumnName {
17949 qualifier: Some("EXCLUDED".to_string()),
17950 name: c.name.clone(),
17951 });
17952 }
17953 Expr::FunctionCall { args, .. } => {
17954 for a in args {
17955 Self::rewrite_mysql_values_refs(a);
17956 }
17957 }
17958 Expr::Binary { lhs, rhs, .. } => {
17959 Self::rewrite_mysql_values_refs(lhs);
17960 Self::rewrite_mysql_values_refs(rhs);
17961 }
17962 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
17963 Self::rewrite_mysql_values_refs(expr);
17964 }
17965 Expr::Case {
17966 operand,
17967 branches,
17968 else_branch,
17969 } => {
17970 if let Some(op) = operand {
17971 Self::rewrite_mysql_values_refs(op);
17972 }
17973 for (w, t) in branches {
17974 Self::rewrite_mysql_values_refs(w);
17975 Self::rewrite_mysql_values_refs(t);
17976 }
17977 if let Some(el) = else_branch {
17978 Self::rewrite_mysql_values_refs(el);
17979 }
17980 }
17981 _ => {}
17982 }
17983 }
17984
17985 /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
17986 /// clause sitting between the INSERT body and the trailing
17987 /// RETURNING. All keywords come in as bare idents; `ON` is
17988 /// a reserved Token though.
17989 fn parse_optional_on_conflict(
17990 &mut self,
17991 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17992 if !matches!(self.peek(), Token::On) {
17993 return Ok(None);
17994 }
17995 // Peek further: we want exactly "ON CONFLICT ...". If the
17996 // next ident isn't "conflict", let some other parser handle.
17997 let next_is_conflict = matches!(
17998 self.tokens.get(self.pos + 1),
17999 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18000 );
18001 if !next_is_conflict {
18002 return Ok(None);
18003 }
18004 self.advance(); // ON
18005 self.advance(); // CONFLICT
18006 // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18007 // the constraint instead of listing columns (the pg_dump
18008 // form); the engine resolves it.
18009 let mut constraint_name: Option<String> = None;
18010 if matches!(self.peek(), Token::On) {
18011 self.advance(); // ON
18012 match self.advance() {
18013 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18014 }
18015 other => {
18016 return Err(self.err(alloc::format!(
18017 "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18018 )));
18019 }
18020 }
18021 constraint_name = Some(self.expect_ident_like()?);
18022 }
18023 // Optional `(col [, col]*)` target list.
18024 let mut target_columns: Vec<String> = Vec::new();
18025 if matches!(self.peek(), Token::LParen) {
18026 self.advance();
18027 loop {
18028 target_columns.push(self.expect_ident_like()?);
18029 match self.peek() {
18030 Token::Comma => {
18031 self.advance();
18032 }
18033 Token::RParen => {
18034 self.advance();
18035 break;
18036 }
18037 other => {
18038 return Err(self.err(alloc::format!(
18039 "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18040 )));
18041 }
18042 }
18043 }
18044 }
18045 // v7.39 (round 240) — optional index predicate after the target
18046 // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18047 // PARTIAL unique index; SPG's arbiters are full indexes, which
18048 // satisfy any predicate, so it is parsed and carried but not
18049 // consulted (recorded residual: partial-unique-index arbiters).
18050 let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18051 self.advance();
18052 Some(self.parse_expr(0)?)
18053 } else {
18054 None
18055 };
18056 // Required `DO`.
18057 match self.advance() {
18058 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18059 other => {
18060 return Err(self.err(alloc::format!(
18061 "expected DO after ON CONFLICT [(…)], got {other:?}"
18062 )));
18063 }
18064 }
18065 // Action: NOTHING | UPDATE SET …
18066 let action = match self.advance() {
18067 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18068 crate::ast::OnConflictAction::Nothing
18069 }
18070 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18071 self.parse_on_conflict_update_action()?
18072 }
18073 other => {
18074 return Err(self.err(alloc::format!(
18075 "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18076 )));
18077 }
18078 };
18079 Ok(Some(crate::ast::OnConflictClause {
18080 target_columns,
18081 index_where,
18082 constraint_name,
18083 mysql_lowered: false,
18084 action,
18085 }))
18086 }
18087
18088 /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18089 /// `SET col = expr [, …] [WHERE cond]`. Caller already
18090 /// consumed `UPDATE`.
18091 fn parse_on_conflict_update_action(
18092 &mut self,
18093 ) -> Result<crate::ast::OnConflictAction, ParseError> {
18094 // `SET`
18095 match self.advance() {
18096 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18097 other => {
18098 return Err(self.err(alloc::format!(
18099 "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18100 )));
18101 }
18102 }
18103 let mut assignments: Vec<(String, Expr)> = Vec::new();
18104 loop {
18105 let col = self.expect_ident_like()?;
18106 if !matches!(self.peek(), Token::Eq) {
18107 return Err(self.err(alloc::format!(
18108 "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18109 self.peek()
18110 )));
18111 }
18112 self.advance();
18113 let value = self.parse_expr(0)?;
18114 assignments.push((col, value));
18115 if matches!(self.peek(), Token::Comma) {
18116 self.advance();
18117 continue;
18118 }
18119 break;
18120 }
18121 let where_ = if matches!(self.peek(), Token::Where) {
18122 self.advance();
18123 Some(self.parse_expr(0)?)
18124 } else {
18125 None
18126 };
18127 Ok(crate::ast::OnConflictAction::Update {
18128 assignments,
18129 where_,
18130 })
18131 }
18132
18133 fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18134 let mut items = Vec::new();
18135 // v7.39 (round 341, V66) — PG's target list may be EMPTY
18136 // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18137 // answers one zero-column row per row of t, and a bare `SELECT`
18138 // answers a single zero-column row. SPG required at least one
18139 // item, so both were syntax errors. Recognised by the token that
18140 // follows — nothing that can start an expression appears here.
18141 if self.select_list_is_empty_here() {
18142 return Ok(items);
18143 }
18144 loop {
18145 items.push(self.parse_select_item()?);
18146 if matches!(self.peek(), Token::Comma) {
18147 self.advance();
18148 } else {
18149 break;
18150 }
18151 }
18152 Ok(items)
18153 }
18154
18155 /// Is the target list empty at this point — i.e. does the next token
18156 /// end the SELECT's item list rather than start an item?
18157 fn select_list_is_empty_here(&self) -> bool {
18158 match self.peek() {
18159 Token::From
18160 | Token::Where
18161 | Token::Group
18162 | Token::Having
18163 | Token::Order
18164 | Token::Limit
18165 | Token::Offset
18166 | Token::Semicolon
18167 | Token::RParen
18168 | Token::Union
18169 | Token::Except
18170 | Token::Eof => true,
18171 // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18172 // with unreserved keywords, so they arrive as plain idents.
18173 Token::Ident(s) => {
18174 s.eq_ignore_ascii_case("fetch")
18175 || s.eq_ignore_ascii_case("window")
18176 || s.eq_ignore_ascii_case("intersect")
18177 }
18178 _ => false,
18179 }
18180 }
18181
18182 fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18183 if matches!(self.peek(), Token::Star) {
18184 self.advance();
18185 return Ok(SelectItem::Wildcard);
18186 }
18187 // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18188 // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18189 // choke on the `*` ("expected identifier, got Star"). The lookahead is
18190 // `<ident> . *` with nothing binding tighter.
18191 if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18192 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18193 && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18194 {
18195 self.advance(); // qualifier
18196 self.advance(); // .
18197 self.advance(); // *
18198 return Ok(SelectItem::QualifiedWildcard(q));
18199 }
18200 }
18201 let start_tok = self.pos;
18202 let expr = self.parse_expr(0)?;
18203 let end_tok = self.consumed_pos();
18204 // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18205 // multi-column function returns into columns. Marked here and lowered in
18206 // `parse_bare_select`, where the FROM clause is in hand.
18207 if matches!(self.peek(), Token::Dot)
18208 && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18209 {
18210 self.advance(); // .
18211 self.advance(); // *
18212 return Ok(SelectItem::Expr {
18213 expr: Expr::FunctionCall {
18214 name: "__record_expand".to_string(),
18215 args: alloc::vec![expr],
18216 },
18217 alias: None,
18218 });
18219 }
18220 let alias = match self.parse_optional_alias()? {
18221 Some(a) => Some(a),
18222 None => self.mysql_item_label(&expr, start_tok, end_tok),
18223 };
18224 Ok(SelectItem::Expr { expr, alias })
18225 }
18226
18227 /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18228 /// carries no `AS`, filled in here so every downstream path reports it
18229 /// without knowing the rule. `None` leaves the item un-aliased, which is
18230 /// what a PG session always gets.
18231 ///
18232 /// Measured against MariaDB 11, three rules and no more:
18233 ///
18234 /// | item | label | why |
18235 /// |------------------|------------|------------------------------|
18236 /// | `lbl.a` | `a` | a column reports its name |
18237 /// | `'it''s'` | `it's` | a string reports its VALUE |
18238 /// | `a + b` | `a + b` | anything else, source text |
18239 ///
18240 /// The third is why this lives in the parser at all: the label is the
18241 /// text the client WROTE, down to the spacing, so it cannot be printed
18242 /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18243 ///
18244 /// Comments survive, and that is right: through a `mariadb` CLI both
18245 /// servers answer `a + b` for `SELECT a /* c */ + b`, but that is the
18246 /// CLIENT stripping the comment before it sends. Asked over the raw
18247 /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18248 /// produces.
18249 fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18250 if !self.mysql_dialect {
18251 return None;
18252 }
18253 match expr {
18254 // A column already reports its own name downstream; naming it
18255 // again here would only re-state the qualifier the label drops.
18256 Expr::Column(_) => None,
18257 Expr::Literal(Literal::String(v)) => Some(v.clone()),
18258 _ => self.source_span(start_tok, end_tok).map(str::to_string),
18259 }
18260 }
18261
18262 /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18263 /// consumed VALUES keyword. Each row lowers to a constant SELECT
18264 /// with PG's default column1..columnN names; subsequent rows
18265 /// chain as UNION ALL peers. Shared by the FROM-position
18266 /// `( VALUES … )` arm and the top-level bare VALUES statement.
18267 fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
18268 let mut row_selects: Vec<SelectStatement> = Vec::new();
18269 loop {
18270 if !matches!(self.peek(), Token::LParen) {
18271 return Err(self.err(alloc::format!(
18272 "expected '(' to start a VALUES row, got {:?}",
18273 self.peek()
18274 )));
18275 }
18276 self.advance(); // (
18277 let mut items: Vec<SelectItem> = Vec::new();
18278 loop {
18279 let expr = self.parse_expr(0)?;
18280 items.push(SelectItem::Expr {
18281 expr,
18282 alias: Some(alloc::format!("column{}", items.len() + 1)),
18283 });
18284 match self.peek() {
18285 Token::Comma => {
18286 self.advance();
18287 }
18288 Token::RParen => break,
18289 other => {
18290 return Err(self.err(alloc::format!(
18291 "expected ',' or ')' in VALUES row, got {other:?}"
18292 )));
18293 }
18294 }
18295 }
18296 self.advance(); // )
18297 row_selects.push(SelectStatement {
18298 locking: None,
18299 ctes: Vec::new(),
18300 distinct: false,
18301 distinct_on: Vec::new(),
18302 items,
18303 from: None,
18304 where_: None,
18305 group_by: None,
18306 group_by_all: false,
18307 having: None,
18308 unions: Vec::new(),
18309 order_by: Vec::new(),
18310 limit: None,
18311 offset: None,
18312 limit_with_ties: false,
18313 window_check_exprs: Vec::new(),
18314 });
18315 if matches!(self.peek(), Token::Comma) {
18316 self.advance();
18317 continue;
18318 }
18319 break;
18320 }
18321 let mut head = row_selects.remove(0);
18322 head.unions = row_selects
18323 .into_iter()
18324 .map(|s| (UnionKind::All, s))
18325 .collect();
18326 Ok(head)
18327 }
18328
18329 fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
18330 // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
18331 // children. It was read as a table NAMED `only`, so the query
18332 // failed on `relation "only" does not exist`.
18333 //
18334 // v7.39 (round 644) — and it is no longer a no-op. Round 621
18335 // absorbed the keyword, reasoning that SPG's children are
18336 // separate relations a plain scan does not descend into, so ONLY
18337 // already described the scan. That stopped being true when a
18338 // partition parent started unioning its children: measured,
18339 // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
18340 // where PG answers 0. The flag is carried now.
18341 let mut only = false;
18342 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
18343 && matches!(
18344 self.tokens.get(self.pos + 1),
18345 Some(Token::Ident(_) | Token::QuotedIdent(_))
18346 )
18347 {
18348 only = true;
18349 self.advance();
18350 }
18351 // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
18352 // for these SRFs the keyword is noise at parse time: the
18353 // join executor already substitutes outer-column references
18354 // into unnest_expr / generate_series_args per outer row
18355 // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
18356 // licences the correlation even without the keyword. Absorb
18357 // it and fall through to the SRF arms below.
18358 // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
18359 // just the four builtin SRFs: a user set-returning function on a JOIN's
18360 // right side is the whole point of LATERAL. The keyword stays noise at
18361 // parse time — the join executor substitutes the outer row into the
18362 // call's arguments per outer row.
18363 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18364 && matches!(
18365 self.tokens.get(self.pos + 1),
18366 // The json_each family has its OWN `LATERAL …` arm below, which
18367 // needs to see the keyword — absorbing it here would send those
18368 // calls down the generic table-function channel instead.
18369 Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
18370 )
18371 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18372 {
18373 self.advance(); // LATERAL
18374 }
18375 // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
18376 // set-returning function whose argument may reference a
18377 // preceding FROM item. We rewrite this to
18378 // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
18379 // AS __srf__) AS <alias>` so the existing LATERAL subquery
18380 // executor handles per-outer-row evaluation and the
18381 // SRF-primary jsonb_each_text path handles the inner
18382 // materialisation. Sentori 0067 backfill is the dogfood
18383 // shape.
18384 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18385 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
18386 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18387 {
18388 self.advance(); // LATERAL
18389 let each_fn = match self.peek() {
18390 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18391 _ => unreachable!(),
18392 };
18393 self.advance(); // jsonb_each[_text] / json_each[_text]
18394 self.advance(); // (
18395 let arg = self.parse_expr(0)?;
18396 if !matches!(self.peek(), Token::RParen) {
18397 return Err(self.err(alloc::format!(
18398 "expected ')' after LATERAL {each_fn}() argument, got {:?}",
18399 self.peek()
18400 )));
18401 }
18402 self.advance();
18403 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18404 let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18405 // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
18406 // FROM jsonb_each_text(<arg>) AS __srf__
18407 // PG's `AS kv(key, value)` column-alias list maps
18408 // positions to names; default to (key, value) when
18409 // omitted (matching the SRF's natural column names).
18410 let srf_alias = "__srf__".to_string();
18411 let key_alias = column_aliases
18412 .first()
18413 .cloned()
18414 .unwrap_or_else(|| "key".to_string());
18415 let value_alias = column_aliases
18416 .get(1)
18417 .cloned()
18418 .unwrap_or_else(|| "value".to_string());
18419 let inner_select = crate::ast::SelectStatement {
18420 locking: None,
18421 ctes: Vec::new(),
18422 distinct: false,
18423 distinct_on: Vec::new(),
18424 items: alloc::vec![
18425 crate::ast::SelectItem::Expr {
18426 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18427 qualifier: Some(srf_alias.clone()),
18428 name: "key".to_string(),
18429 }),
18430 alias: Some(key_alias),
18431 },
18432 crate::ast::SelectItem::Expr {
18433 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18434 qualifier: Some(srf_alias.clone()),
18435 name: "value".to_string(),
18436 }),
18437 alias: Some(value_alias),
18438 },
18439 ],
18440 from: Some(crate::ast::FromClause {
18441 primary: TableRef {
18442 name: srf_alias.clone(),
18443 alias: Some(srf_alias.clone()),
18444 only: false,
18445 as_of_segment: None,
18446 unnest_expr: None,
18447 unnest_column_aliases: Vec::new(),
18448 with_ordinality: false,
18449 generate_series_args: None,
18450 lateral_subquery: None,
18451 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18452 table_fn_call: None,
18453 rows_from: None,
18454 json_table: None,
18455 scalar_fn_item: false,
18456 },
18457 joins: Vec::new(),
18458 }),
18459 where_: None,
18460 group_by: None,
18461 group_by_all: false,
18462 having: None,
18463 unions: Vec::new(),
18464 order_by: Vec::new(),
18465 limit: None,
18466 offset: None,
18467 limit_with_ties: false,
18468 window_check_exprs: Vec::new(),
18469 };
18470 return Ok(TableRef {
18471 name: alias.clone(),
18472 alias: Some(alias),
18473 only: false,
18474 as_of_segment: None,
18475 unnest_expr: None,
18476 unnest_column_aliases: Vec::new(),
18477 with_ordinality: false,
18478 generate_series_args: None,
18479 lateral_subquery: Some(Box::new(inner_select)),
18480 jsonb_each_text_arg: None,
18481 table_fn_call: None,
18482 rows_from: None,
18483 json_table: None,
18484 scalar_fn_item: false,
18485 });
18486 }
18487 // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
18488 // without an explicit `LATERAL` keyword is the same shape
18489 // PG accepts (SRF naturally licences lateral correlation).
18490 // We mirror the LATERAL rewrite when the argument syntactic-
18491 // ally references an outer column (Column { qualifier:
18492 // Some(_), … }). For simplicity we apply the rewrite
18493 // whenever the SRF directly follows JOIN/CROSS JOIN/comma
18494 // in the FROM-list — caller-side join parsing positions
18495 // this peek correctly.
18496 // (Implementation note: detection lives below; the LATERAL
18497 // branch above already covers the explicit form; the bare
18498 // form falls through to the plain SRF arm and the engine
18499 // treats it as a constant-arg SRF if no outer reference is
18500 // present.)
18501 // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
18502 // table. Detect at the head so it claims precedence over
18503 // every other table-ref shape (unnest / generate_series /
18504 // bare ident); the lateral subquery itself follows the
18505 // regular SELECT grammar.
18506 // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
18507 // t(cols)`. Each row lowers to a constant SELECT with PG's
18508 // default column1..columnN names; subsequent rows chain as
18509 // UNION ALL peers. The result rides the derived-table
18510 // lateral_subquery channel — zero executor work.
18511 if matches!(self.peek(), Token::LParen)
18512 && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
18513 {
18514 self.advance(); // (
18515 self.advance(); // VALUES
18516 let head = self.parse_values_rows_body()?;
18517 if !matches!(self.peek(), Token::RParen) {
18518 return Err(self.err(alloc::format!(
18519 "expected ')' after VALUES list, got {:?}",
18520 self.peek()
18521 )));
18522 }
18523 self.advance();
18524 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18525 let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
18526 return Ok(TableRef {
18527 name,
18528 alias: alias_ident,
18529 only: false,
18530 as_of_segment: None,
18531 unnest_expr: None,
18532 unnest_column_aliases: column_aliases,
18533 with_ordinality: false,
18534 generate_series_args: None,
18535 lateral_subquery: Some(Box::new(head)),
18536 jsonb_each_text_arg: None,
18537 table_fn_call: None,
18538 rows_from: None,
18539 json_table: None,
18540 scalar_fn_item: false,
18541 });
18542 }
18543 // v7.37.17 (17.6 siblings) — plain derived table:
18544 // `FROM ( SELECT … ) [AS] alias`. Rides the same
18545 // lateral_subquery channel the explicit LATERAL form uses —
18546 // an uncorrelated inner SELECT executes identically. The
18547 // inner parse carries UNION tails (they live on
18548 // SelectStatement.unions).
18549 // v7.37 D.20 — the derived-table inner may itself be a
18550 // parenthesized set-operation group (`FROM ((SELECT…) UNION
18551 // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
18552 // bare `(SELECT …)`. parse_one_statement already routes a leading
18553 // `(` set-op group (its LParen arm) and a leading WITH
18554 // (parse_with_cte_then_select), so widen the second-token gate to
18555 // Select | LParen | WITH.
18556 // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
18557 // PG's spelling of `SELECT * FROM t` and is accepted wherever a
18558 // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
18559 // has existed since the shorthand landed and `parse_bare_select`
18560 // already routes it ("valid anywhere a SELECT head is"); what was
18561 // missing is this second-token gate, and the CTE body's dispatch
18562 // below. Round 868 found both by putting the shorthand in a
18563 // subquery — the top-level forms had been the only ones tested.
18564 if matches!(self.peek(), Token::LParen)
18565 && (matches!(
18566 self.tokens.get(self.pos + 1),
18567 Some(Token::Select | Token::LParen | Token::Table)
18568 ) || matches!(self.tokens.get(self.pos + 1),
18569 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
18570 {
18571 self.advance(); // (
18572 let inner = match self.parse_one_statement()? {
18573 Statement::Select(s) => s,
18574 other => {
18575 return Err(self.err(alloc::format!(
18576 "expected SELECT inside derived table ( … ), got {other:?}"
18577 )));
18578 }
18579 };
18580 if !matches!(self.peek(), Token::RParen) {
18581 return Err(self.err(alloc::format!(
18582 "expected ')' after derived-table subquery, got {:?}",
18583 self.peek()
18584 )));
18585 }
18586 self.advance();
18587 // `AS t(a, b)` column-alias list rides the
18588 // unnest_column_aliases field (same positional-rename
18589 // contract the unnest SRFs use).
18590 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18591 let name = alias_ident
18592 .clone()
18593 .unwrap_or_else(|| "subquery".to_string());
18594 return Ok(TableRef {
18595 name,
18596 alias: alias_ident,
18597 only: false,
18598 as_of_segment: None,
18599 unnest_expr: None,
18600 unnest_column_aliases: column_aliases,
18601 with_ordinality: false,
18602 generate_series_args: None,
18603 lateral_subquery: Some(Box::new(inner)),
18604 jsonb_each_text_arg: None,
18605 table_fn_call: None,
18606 rows_from: None,
18607 json_table: None,
18608 scalar_fn_item: false,
18609 });
18610 }
18611 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18612 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18613 {
18614 self.advance(); // LATERAL
18615 self.advance(); // (
18616 // Parse the inner SELECT.
18617 let inner = match self.parse_one_statement()? {
18618 Statement::Select(s) => s,
18619 other => {
18620 return Err(self.err(alloc::format!(
18621 "expected SELECT inside LATERAL ( … ), got {other:?}"
18622 )));
18623 }
18624 };
18625 if !matches!(self.peek(), Token::RParen) {
18626 return Err(self.err(alloc::format!(
18627 "expected ')' after LATERAL subquery, got {:?}",
18628 self.peek()
18629 )));
18630 }
18631 self.advance();
18632 // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
18633 // `(VALUES …) t(g)` derived table round-trips through view-body
18634 // Display, which renders on the lateral_subquery channel).
18635 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18636 let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
18637 return Ok(TableRef {
18638 name,
18639 alias: alias_ident,
18640 only: false,
18641 as_of_segment: None,
18642 unnest_expr: None,
18643 unnest_column_aliases: column_aliases,
18644 with_ordinality: false,
18645 generate_series_args: None,
18646 lateral_subquery: Some(Box::new(inner)),
18647 jsonb_each_text_arg: None,
18648 table_fn_call: None,
18649 rows_from: None,
18650 json_table: None,
18651 scalar_fn_item: false,
18652 });
18653 }
18654 // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
18655 // function as a FROM item. Emits one row per (key, value)
18656 // pair in the JSONB object argument as TEXT columns. May
18657 // be wrapped in CROSS JOIN LATERAL when the argument
18658 // references a preceding FROM item (sentori migration
18659 // 0067 backfill shape: `CROSS JOIN LATERAL
18660 // jsonb_each_text(t.json_col) AS kv(key, value)`).
18661 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
18662 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18663 {
18664 let each_fn = match self.peek() {
18665 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18666 _ => unreachable!(),
18667 };
18668 self.advance(); // jsonb_each[_text] / json_each[_text]
18669 self.advance(); // (
18670 let arg = self.parse_expr(0)?;
18671 if !matches!(self.peek(), Token::RParen) {
18672 return Err(self.err(alloc::format!(
18673 "expected ')' after {each_fn}() argument, got {:?}",
18674 self.peek()
18675 )));
18676 }
18677 self.advance();
18678 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18679 let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18680 return Ok(TableRef {
18681 name,
18682 alias: alias_ident,
18683 only: false,
18684 as_of_segment: None,
18685 unnest_expr: None,
18686 // `AS t(k, v)` renames key/value positionally, same as the
18687 // LATERAL-position form already does.
18688 unnest_column_aliases: column_aliases,
18689 with_ordinality: false,
18690 generate_series_args: None,
18691 lateral_subquery: None,
18692 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18693 table_fn_call: None,
18694 rows_from: None,
18695 json_table: None,
18696 scalar_fn_item: false,
18697 });
18698 }
18699 // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
18700 // (+ json_ variants) — record-returning JSON functions with a
18701 // column-definition list. Desugar to a derived table that
18702 // projects each declared column from the JSON via `->>` + a cast,
18703 // over `jsonb_array_elements(J)` for the *set (per-element) form.
18704 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
18705 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18706 {
18707 return self.parse_json_to_record_from();
18708 }
18709 // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
18710 // row is a text[] of capture groups, so it cannot desugar to unnest
18711 // (that would flatten the array). Wrap it as a derived table
18712 // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
18713 // SRF path already emits one text[] row per match. PG names the column
18714 // `regexp_matches`; an `AS a(col)` alias overrides it.
18715 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18716 if s.eq_ignore_ascii_case("regexp_matches"))
18717 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18718 {
18719 self.advance(); // fn name
18720 self.advance(); // (
18721 let mut fn_args: Vec<Expr> = Vec::new();
18722 loop {
18723 fn_args.push(self.parse_expr(0)?);
18724 if matches!(self.peek(), Token::Comma) {
18725 self.advance();
18726 continue;
18727 }
18728 break;
18729 }
18730 if !matches!(self.peek(), Token::RParen) {
18731 return Err(self.err(alloc::format!(
18732 "expected ')' after regexp_matches() arguments, got {:?}",
18733 self.peek()
18734 )));
18735 }
18736 self.advance();
18737 // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
18738 // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
18739 // it, so it died on the `with` token while every other table function
18740 // accepted it.
18741 let with_ordinality = self.absorb_with_ordinality();
18742 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18743 let table_alias = alias_ident
18744 .clone()
18745 .unwrap_or_else(|| "regexp_matches".to_string());
18746 // PG names a single-column function's output column after the ALIAS
18747 // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
18748 // `m` reads as that column and not as a whole-row composite. Naming
18749 // it after the function regardless made `SELECT m[1] FROM … AS m`
18750 // subscript a record.
18751 let col_name = column_aliases
18752 .first()
18753 .cloned()
18754 .or_else(|| alias_ident.clone())
18755 .unwrap_or_else(|| "regexp_matches".to_string());
18756 let inner = crate::ast::SelectStatement {
18757 locking: None,
18758 ctes: Vec::new(),
18759 distinct: false,
18760 distinct_on: Vec::new(),
18761 items: alloc::vec![SelectItem::Expr {
18762 expr: Expr::FunctionCall {
18763 name: "regexp_matches".to_string(),
18764 args: fn_args,
18765 },
18766 alias: Some(col_name),
18767 }],
18768 from: None,
18769 where_: None,
18770 group_by: None,
18771 group_by_all: false,
18772 having: None,
18773 unions: Vec::new(),
18774 order_by: Vec::new(),
18775 limit: None,
18776 offset: None,
18777 limit_with_ties: false,
18778 window_check_exprs: Vec::new(),
18779 };
18780 return Ok(TableRef {
18781 name: table_alias.clone(),
18782 alias: Some(table_alias),
18783 only: false,
18784 as_of_segment: None,
18785 unnest_expr: None,
18786 unnest_column_aliases: column_aliases,
18787 with_ordinality,
18788 generate_series_args: None,
18789 lateral_subquery: Some(Box::new(inner)),
18790 jsonb_each_text_arg: None,
18791 table_fn_call: None,
18792 rows_from: None,
18793 json_table: None,
18794 // regexp_matches returns text[], a base type: `SELECT m FROM
18795 // regexp_matches(…) AS m` is the array, not a composite wrapping it.
18796 scalar_fn_item: true,
18797 });
18798 }
18799 // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
18800 // / json_ variants as a FROM item. Rewritten into
18801 // `unnest(<same fn>(<expr>))`: the scalar form returns the
18802 // elements as a TEXT array, and the existing unnest SRF path
18803 // materialises one row per element. PG's natural column name
18804 // is `value`; an `AS a(col)` column-alias list overrides it.
18805 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18806 if s.eq_ignore_ascii_case("jsonb_array_elements")
18807 || s.eq_ignore_ascii_case("json_array_elements")
18808 || s.eq_ignore_ascii_case("jsonb_array_elements_text")
18809 || s.eq_ignore_ascii_case("json_array_elements_text")
18810 || s.eq_ignore_ascii_case("jsonb_object_keys")
18811 || s.eq_ignore_ascii_case("json_object_keys")
18812 || s.eq_ignore_ascii_case("jsonb_path_query")
18813 || s.eq_ignore_ascii_case("json_path_query")
18814 || s.eq_ignore_ascii_case("generate_subscripts")
18815 || s.eq_ignore_ascii_case("string_to_table")
18816 || s.eq_ignore_ascii_case("regexp_split_to_table"))
18817 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18818 {
18819 let fn_name = match self.peek() {
18820 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18821 _ => unreachable!(),
18822 };
18823 self.advance(); // fn name
18824 self.advance(); // (
18825 let mut fn_args: Vec<Expr> = Vec::new();
18826 loop {
18827 fn_args.push(self.parse_expr(0)?);
18828 if matches!(self.peek(), Token::Comma) {
18829 self.advance();
18830 continue;
18831 }
18832 break;
18833 }
18834 if !matches!(self.peek(), Token::RParen) {
18835 return Err(self.err(alloc::format!(
18836 "expected ')' after {fn_name}() arguments, got {:?}",
18837 self.peek()
18838 )));
18839 }
18840 self.advance();
18841 let with_ordinality = self.absorb_with_ordinality();
18842 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18843 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
18844 // PG's natural column name: the array-elements SRFs
18845 // declare an OUT parameter `value`; jsonb_object_keys
18846 // and generate_subscripts have none, so the column is
18847 // named after the function. A bare table alias on a
18848 // single-column SRF renames the column too (PG: `FROM
18849 // generate_subscripts(a, 1) AS s` projects column s) —
18850 // except for the OUT-parameter SRFs, whose column stays
18851 // `value` under a bare alias.
18852 let natural_col = if fn_name.ends_with("_array_elements")
18853 || fn_name.ends_with("_array_elements_text")
18854 {
18855 "value".to_string()
18856 } else {
18857 alias_ident.clone().unwrap_or_else(|| fn_name.clone())
18858 };
18859 let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
18860 // Keep any further entries — the second names the
18861 // ordinality column under WITH ORDINALITY.
18862 srf_cols.extend(column_aliases.into_iter().skip(1));
18863 // The *_to_table SRFs are row-streams over the existing
18864 // *_to_array scalars — map the call target; the display
18865 // name (alias / column defaults) keeps the SRF spelling.
18866 let call_name = match fn_name.as_str() {
18867 "string_to_table" => "string_to_array".to_string(),
18868 "regexp_split_to_table" => "regexp_split_to_array".to_string(),
18869 _ => fn_name,
18870 };
18871 // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
18872 // preceding FROM item (bare or qualified column) is correlated;
18873 // route it through the per-outer-row lateral channel.
18874 let expr = crate::ast::Expr::FunctionCall {
18875 name: call_name,
18876 args: fn_args,
18877 };
18878 let correlated = Self::expr_has_any_column(&expr);
18879 let tref = TableRef {
18880 name,
18881 alias: alias_ident,
18882 only: false,
18883 as_of_segment: None,
18884 unnest_expr: Some(Box::new(expr)),
18885 unnest_column_aliases: srf_cols,
18886 with_ordinality,
18887 generate_series_args: None,
18888 lateral_subquery: None,
18889 jsonb_each_text_arg: None,
18890 table_fn_call: None,
18891 rows_from: None,
18892 json_table: None,
18893 // Each of these returns a BASE type (jsonb / text / int), so the item's
18894 // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
18895 // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
18896 scalar_fn_item: !with_ordinality,
18897 };
18898 return Ok(if correlated {
18899 Self::wrap_correlated_srf(tref)
18900 } else {
18901 tref
18902 });
18903 }
18904 // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
18905 // explicit parallel-zip syntax. Each entry lowers to its
18906 // array-returning scalar form (unnest(x) → x itself; the
18907 // FROM-SRF rewrite family → their scalar array calls) and
18908 // the list rides the multi-arg unnest zip channel:
18909 // NULL-padded to the longest, WITH ORDINALITY appends the
18910 // counter. generate_series has no scalar array form and
18911 // errors honestly.
18912 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
18913 && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
18914 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18915 {
18916 self.advance(); // ROWS
18917 self.advance(); // FROM
18918 self.advance(); // (
18919 let mut entries: Vec<Expr> = Vec::new();
18920 // v7.39 (read01 round 74) — the generic channel, filled in parallel.
18921 // Used only when some entry has no array form.
18922 let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
18923 loop {
18924 let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
18925 if !matches!(self.peek(), Token::LParen) {
18926 return Err(self.err(alloc::format!(
18927 "expected '(' after {fn_name} in ROWS FROM, got {:?}",
18928 self.peek()
18929 )));
18930 }
18931 self.advance();
18932 let mut fn_args: Vec<Expr> = Vec::new();
18933 if !matches!(self.peek(), Token::RParen) {
18934 loop {
18935 fn_args.push(self.parse_expr(0)?);
18936 if matches!(self.peek(), Token::Comma) {
18937 self.advance();
18938 continue;
18939 }
18940 break;
18941 }
18942 }
18943 if !matches!(self.peek(), Token::RParen) {
18944 return Err(self.err(alloc::format!(
18945 "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
18946 self.peek()
18947 )));
18948 }
18949 self.advance();
18950 let entry = match fn_name.as_str() {
18951 "unnest" => {
18952 if fn_args.len() != 1 {
18953 return Err(
18954 self.err("unnest inside ROWS FROM takes exactly one array".into())
18955 );
18956 }
18957 fn_args.pop().expect("len checked")
18958 }
18959 "jsonb_array_elements"
18960 | "json_array_elements"
18961 | "jsonb_array_elements_text"
18962 | "json_array_elements_text"
18963 | "jsonb_object_keys"
18964 | "json_object_keys"
18965 | "generate_subscripts" => crate::ast::Expr::FunctionCall {
18966 name: fn_name,
18967 args: fn_args,
18968 },
18969 "string_to_table" => crate::ast::Expr::FunctionCall {
18970 name: "string_to_array".to_string(),
18971 args: fn_args,
18972 },
18973 "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
18974 name: "regexp_split_to_array".to_string(),
18975 args: fn_args,
18976 },
18977 // v7.39 (read01 round 74) — an SRF with no array form
18978 // (`generate_series`, a user `RETURNS SETOF` function) has no
18979 // scalar expression to zip, so the WHOLE list switches to the
18980 // rows_from channel, which runs each function and zips the
18981 // rows themselves. The all-array case keeps the old lowering:
18982 // it is well-trodden and this must not disturb it.
18983 _ => {
18984 generic.push((fn_name, fn_args));
18985 if matches!(self.peek(), Token::Comma) {
18986 self.advance();
18987 continue;
18988 }
18989 break;
18990 }
18991 };
18992 generic.push((
18993 // The array-able entries carry their lowered expr along, so a
18994 // MIXED list still works: the engine sees the scalar array
18995 // form and unnests it.
18996 "__array".to_string(),
18997 alloc::vec![entry.clone()],
18998 ));
18999 entries.push(entry);
19000 if matches!(self.peek(), Token::Comma) {
19001 self.advance();
19002 continue;
19003 }
19004 break;
19005 }
19006 if !matches!(self.peek(), Token::RParen) {
19007 return Err(self.err(alloc::format!(
19008 "expected ')' to close ROWS FROM, got {:?}",
19009 self.peek()
19010 )));
19011 }
19012 self.advance();
19013 let with_ordinality = self.absorb_with_ordinality();
19014 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19015 let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19016 // v7.39 (read01 round 74) — some entry had no array form, so the whole
19017 // list rides the generic channel.
19018 if generic.iter().any(|(n, _)| n != "__array") {
19019 let correlated = generic
19020 .iter()
19021 .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19022 let tref = TableRef {
19023 name,
19024 alias: alias_ident,
19025 only: false,
19026 as_of_segment: None,
19027 unnest_expr: None,
19028 unnest_column_aliases,
19029 with_ordinality,
19030 generate_series_args: None,
19031 lateral_subquery: None,
19032 jsonb_each_text_arg: None,
19033 table_fn_call: None,
19034 rows_from: Some(generic),
19035 json_table: None,
19036 scalar_fn_item: false,
19037 };
19038 return Ok(if correlated {
19039 Self::wrap_correlated_srf(tref)
19040 } else {
19041 tref
19042 });
19043 }
19044 let correlated = entries.iter().any(Self::expr_has_any_column);
19045 let expr = if entries.len() == 1 {
19046 entries.pop().expect("len checked")
19047 } else {
19048 crate::ast::Expr::FunctionCall {
19049 name: "__unnest_zip".to_string(),
19050 args: entries,
19051 }
19052 };
19053 let tref = TableRef {
19054 name,
19055 alias: alias_ident,
19056 only: false,
19057 as_of_segment: None,
19058 unnest_expr: Some(Box::new(expr)),
19059 unnest_column_aliases,
19060 with_ordinality,
19061 generate_series_args: None,
19062 lateral_subquery: None,
19063 jsonb_each_text_arg: None,
19064 table_fn_call: None,
19065 rows_from: None,
19066 json_table: None,
19067 scalar_fn_item: false,
19068 };
19069 return Ok(if correlated {
19070 Self::wrap_correlated_srf(tref)
19071 } else {
19072 tref
19073 });
19074 }
19075 // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19076 // source. Detect at the head before the bare-ident fallback;
19077 // unnest is not a reserved token.
19078 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19079 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19080 {
19081 self.advance(); // unnest
19082 self.advance(); // (
19083 let mut srf_args = alloc::vec![self.parse_expr(0)?];
19084 while matches!(self.peek(), Token::Comma) {
19085 self.advance();
19086 srf_args.push(self.parse_expr(0)?);
19087 }
19088 if !matches!(self.peek(), Token::RParen) {
19089 return Err(self.err(alloc::format!(
19090 "expected ')' after unnest() argument, got {:?}",
19091 self.peek()
19092 )));
19093 }
19094 self.advance();
19095 // Multi-arg unnest(a, b, …) zips the arrays in
19096 // parallel, NULL-padding to the longest (PG's ROWS
19097 // FROM shorthand). Lower onto the unnest channel as an
19098 // internal marker call the executors unpack.
19099 let expr = if srf_args.len() == 1 {
19100 srf_args.pop().expect("len checked")
19101 } else {
19102 crate::ast::Expr::FunctionCall {
19103 name: "__unnest_zip".to_string(),
19104 args: srf_args,
19105 }
19106 };
19107 let with_ordinality = self.absorb_with_ordinality();
19108 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19109 let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19110 let correlated = Self::expr_has_any_column(&expr);
19111 let tref = TableRef {
19112 name,
19113 alias: alias_ident,
19114 only: false,
19115 as_of_segment: None,
19116 unnest_expr: Some(Box::new(expr)),
19117 unnest_column_aliases,
19118 with_ordinality,
19119 generate_series_args: None,
19120 lateral_subquery: None,
19121 jsonb_each_text_arg: None,
19122 table_fn_call: None,
19123 rows_from: None,
19124 json_table: None,
19125 scalar_fn_item: false,
19126 };
19127 return Ok(if correlated {
19128 Self::wrap_correlated_srf(tref)
19129 } else {
19130 tref
19131 });
19132 }
19133 // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19134 // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19135 // generic table-fn arg parser can't read), so it is intercepted
19136 // here BEFORE the generic dispatch. The doc expr may reference
19137 // outer columns (implicit LATERAL) — same correlated-wrap rule.
19138 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19139 if s.eq_ignore_ascii_case("json_table"))
19140 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19141 {
19142 let tref = self.parse_json_table_ref()?;
19143 let correlated = tref
19144 .json_table
19145 .as_deref()
19146 .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19147 return Ok(if correlated {
19148 Self::wrap_correlated_srf(tref)
19149 } else {
19150 tref
19151 });
19152 }
19153 // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19154 // functions dispatched by name (`pg_partition_tree('t')`,
19155 // `pg_partition_ancestors('t')`). Same head-detection shape as
19156 // unnest; the engine executor owns the row shape per function.
19157 // v7.39 (read01 round 65) — and a USER function in FROM position
19158 // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19159 // (generate_series / unnest / the json_each family) keep it — their arms
19160 // sit further down, so they are excluded here by name rather than by
19161 // ordering. Anything else that is an ident followed by `(` is a table
19162 // function; the engine executor decides whether it is a builtin, a
19163 // set-returning user function, or an error.
19164 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19165 if !s.eq_ignore_ascii_case("generate_series")
19166 && !s.eq_ignore_ascii_case("unnest")
19167 && !is_json_each_name(s))
19168 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19169 {
19170 // Body out-of-line — this parse sits on the FROM/subquery
19171 // recursion chain (debug frame-cliff discipline).
19172 // v7.39 (read01 round 69) — a call whose arguments reference an outer
19173 // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19174 // outer row, so it rides the lateral channel. Same rule the unnest
19175 // arm uses.
19176 let tref = self.parse_table_fn_ref()?;
19177 let correlated = tref
19178 .table_fn_call
19179 .as_deref()
19180 .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19181 return Ok(if correlated {
19182 Self::wrap_correlated_srf(tref)
19183 } else {
19184 tref
19185 });
19186 }
19187 // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19188 // [, step])` set-returning source. Same shape as unnest:
19189 // detect at the head, parse the comma-separated arg list,
19190 // dispatch downstream through the engine's set-returning
19191 // path. Supports integer triplets (mailrs's `WITH row_no AS
19192 // (SELECT * FROM generate_series(1, N))` pattern) and
19193 // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19194 // date-range iteration pattern, which pre-3.10 had no
19195 // direct equivalent in SPG).
19196 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19197 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19198 {
19199 self.advance(); // generate_series
19200 self.advance(); // (
19201 let mut args: Vec<Expr> = Vec::new();
19202 loop {
19203 args.push(self.parse_expr(0)?);
19204 if matches!(self.peek(), Token::Comma) {
19205 self.advance();
19206 continue;
19207 }
19208 break;
19209 }
19210 if !matches!(self.peek(), Token::RParen) {
19211 return Err(self.err(alloc::format!(
19212 "expected ')' after generate_series() arguments, got {:?}",
19213 self.peek()
19214 )));
19215 }
19216 self.advance();
19217 if args.len() < 2 || args.len() > 3 {
19218 return Err(self.err(alloc::format!(
19219 "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19220 args.len()
19221 )));
19222 }
19223 let with_ordinality = self.absorb_with_ordinality();
19224 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19225 let name = alias_ident
19226 .clone()
19227 .unwrap_or_else(|| "generate_series".to_string());
19228 let correlated = args.iter().any(Self::expr_has_any_column);
19229 let tref = TableRef {
19230 name,
19231 alias: alias_ident,
19232 only: false,
19233 as_of_segment: None,
19234 unnest_expr: None,
19235 unnest_column_aliases: column_aliases,
19236 with_ordinality,
19237 generate_series_args: Some(args),
19238 lateral_subquery: None,
19239 jsonb_each_text_arg: None,
19240 table_fn_call: None,
19241 rows_from: None,
19242 json_table: None,
19243 scalar_fn_item: false,
19244 };
19245 return Ok(if correlated {
19246 Self::wrap_correlated_srf(tref)
19247 } else {
19248 tref
19249 });
19250 }
19251 // v7.16.2 — preserve information_schema / pg_catalog
19252 // qualifiers (mailrs round-10 A.3). The generic
19253 // `expect_ident_like` strip silently drops the schema;
19254 // we want the engine to recognise these PG meta tables
19255 // and synthesise rows from the live catalog. Produce a
19256 // synthetic name (`__spg_info_columns` etc.) so the
19257 // engine's SELECT-side router can dispatch without
19258 // clashing with any user-defined `columns` table.
19259 let name = if let Some(synth) = self.try_peek_meta_qualified() {
19260 synth
19261 } else if let Some(synth) = self.try_peek_meta_bare() {
19262 synth
19263 } else {
19264 self.expect_ident_like()?
19265 };
19266 // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
19267 // time-travel clause. Parse BEFORE the alias so the
19268 // alias can still ride at the tail (`tbl AS OF SEGMENT
19269 // '5' alias`). `AS` is a reserved keyword token, while
19270 // `OF` and `SEGMENT` are bare idents.
19271 let as_of_segment = if matches!(self.peek(), Token::As)
19272 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
19273 {
19274 self.advance(); // AS
19275 self.advance(); // OF
19276 let kw = match self.peek().clone() {
19277 Token::Ident(s) | Token::QuotedIdent(s) => s,
19278 other => {
19279 return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
19280 }
19281 };
19282 if !kw.eq_ignore_ascii_case("segment") {
19283 return Err(self.err(format!(
19284 "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
19285 )));
19286 }
19287 self.advance();
19288 // Segment id literal — accept either a string or
19289 // integer for operator ergonomics.
19290 let id = match self.advance() {
19291 Token::String(s) => s
19292 .parse::<u32>()
19293 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19294 Token::Integer(n) => u32::try_from(n)
19295 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19296 other => {
19297 return Err(self.err(format!(
19298 "expected segment id literal after AS OF SEGMENT, got {other:?}"
19299 )));
19300 }
19301 };
19302 Some(id)
19303 } else {
19304 None
19305 };
19306 // TABLESAMPLE is not a reserved token — keep the bare-ident
19307 // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
19308 let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
19309 {
19310 None
19311 } else {
19312 self.parse_optional_alias()?
19313 };
19314 // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
19315 // (PG grammar). BERNOULLI lowers to a per-row
19316 // `random() < p/100` conjunct on the enclosing SELECT's
19317 // WHERE — exact row-level Bernoulli semantics. SYSTEM
19318 // shares the lowering: SPG has no page structure to
19319 // sample, and the row-level form returns the same expected
19320 // fraction. REPEATABLE(seed) promises a deterministic
19321 // sample SPG cannot honour yet — honest error rather than
19322 // a silently ignored seed.
19323 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
19324 self.advance();
19325 let method = self.expect_ident_like()?;
19326 if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
19327 return Err(self.err(alloc::format!(
19328 "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
19329 )));
19330 }
19331 if !matches!(self.peek(), Token::LParen) {
19332 return Err(self.err(alloc::format!(
19333 "expected '(' after TABLESAMPLE {}, got {:?}",
19334 method.to_ascii_uppercase(),
19335 self.peek()
19336 )));
19337 }
19338 self.advance();
19339 let percent = self.parse_expr(0)?;
19340 if !matches!(self.peek(), Token::RParen) {
19341 return Err(self.err(alloc::format!(
19342 "expected ')' after TABLESAMPLE percentage, got {:?}",
19343 self.peek()
19344 )));
19345 }
19346 self.advance();
19347 // REPEATABLE(seed) → a deterministic per-row draw seeded by
19348 // `seed`, so the sample is stable across repeats and rescans.
19349 // Non-REPEATABLE keeps the non-deterministic `random()` draw.
19350 let mut sample_seed: Option<Expr> = None;
19351 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
19352 self.advance();
19353 if !matches!(self.peek(), Token::LParen) {
19354 return Err(self.err(alloc::format!(
19355 "expected '(' after REPEATABLE, got {:?}",
19356 self.peek()
19357 )));
19358 }
19359 self.advance();
19360 let seed = self.parse_expr(0)?;
19361 if !matches!(self.peek(), Token::RParen) {
19362 return Err(self.err(alloc::format!(
19363 "expected ')' after REPEATABLE seed, got {:?}",
19364 self.peek()
19365 )));
19366 }
19367 self.advance();
19368 sample_seed = Some(seed);
19369 }
19370 let draw = match sample_seed {
19371 Some(seed) => Expr::FunctionCall {
19372 name: "__tsm_fract".to_string(),
19373 args: alloc::vec![seed],
19374 },
19375 None => Expr::FunctionCall {
19376 name: "random".to_string(),
19377 args: Vec::new(),
19378 },
19379 };
19380 self.pending_sample_preds.push(Expr::Binary {
19381 lhs: Box::new(draw),
19382 op: crate::ast::BinOp::Lt,
19383 rhs: Box::new(Expr::Binary {
19384 lhs: Box::new(percent),
19385 op: crate::ast::BinOp::Div,
19386 rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
19387 }),
19388 });
19389 }
19390 Ok(TableRef {
19391 name,
19392 alias,
19393 only,
19394 as_of_segment,
19395 unnest_expr: None,
19396 unnest_column_aliases: Vec::new(),
19397 with_ordinality: false,
19398 generate_series_args: None,
19399 lateral_subquery: None,
19400 jsonb_each_text_arg: None,
19401 table_fn_call: None,
19402 rows_from: None,
19403 json_table: None,
19404 scalar_fn_item: false,
19405 })
19406 }
19407
19408 /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
19409 /// but also accepts `AS alias(col [, col, …])` — the
19410 /// PG-standard table-function column-list form. The column
19411 /// list is only honoured when paired with `UNNEST(...)` in
19412 /// the parent; other call sites currently discard it.
19413 /// True when the expression tree contains a qualified column
19414 /// reference (`t.col`) — the syntactic marker that an SRF
19415 /// argument correlates with a preceding FROM item.
19416 fn expr_has_qualified_column(e: &Expr) -> bool {
19417 match e {
19418 Expr::Column(c) => c.qualifier.is_some(),
19419 Expr::Binary { lhs, rhs, .. } => {
19420 Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
19421 }
19422 Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
19423 Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
19424 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
19425 Expr::Case {
19426 operand,
19427 branches,
19428 else_branch,
19429 } => {
19430 operand
19431 .as_deref()
19432 .is_some_and(Self::expr_has_qualified_column)
19433 || branches.iter().any(|(w, t)| {
19434 Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
19435 })
19436 || else_branch
19437 .as_deref()
19438 .is_some_and(Self::expr_has_qualified_column)
19439 }
19440 _ => false,
19441 }
19442 }
19443
19444 /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
19445 /// counts a bare (unqualified) column. A set-returning function has no
19446 /// input columns of its own, so ANY column in its arguments is an outer
19447 /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
19448 fn expr_has_any_column(e: &Expr) -> bool {
19449 match e {
19450 Expr::Column(_) => true,
19451 Expr::Binary { lhs, rhs, .. } => {
19452 Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
19453 }
19454 Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
19455 Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
19456 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
19457 // v7.39 (round 759, F31-B8b) — a column INSIDE an array
19458 // constructor or subscript fell to the `_ => false` arm, so
19459 // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
19460 // channel and the eager peer eval answered `column "x" does
19461 // not exist` (the substitution walker already recurses both
19462 // shapes; only this detector was blind to them).
19463 Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
19464 Expr::ArraySubscript { target, index } => {
19465 Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
19466 }
19467 Expr::Case {
19468 operand,
19469 branches,
19470 else_branch,
19471 } => {
19472 operand.as_deref().is_some_and(Self::expr_has_any_column)
19473 || branches
19474 .iter()
19475 .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
19476 || else_branch
19477 .as_deref()
19478 .is_some_and(Self::expr_has_any_column)
19479 }
19480 _ => false,
19481 }
19482 }
19483
19484 /// Wrap a correlated SRF table ref (`unnest(t.col)` /
19485 /// `generate_series(1, t.n)`) into the lateral_subquery
19486 /// channel: `SELECT * FROM <srf>` executes per outer row with
19487 /// outer references substituted (v7.37.43-T4.5 machinery).
19488 /// Uncorrelated SRFs stay on their plain channels.
19489 fn wrap_correlated_srf(srf: TableRef) -> TableRef {
19490 let name = srf.name.clone();
19491 let alias = srf.alias.clone();
19492 let inner = crate::ast::SelectStatement {
19493 locking: None,
19494 ctes: Vec::new(),
19495 distinct: false,
19496 distinct_on: Vec::new(),
19497 items: alloc::vec![crate::ast::SelectItem::Wildcard],
19498 from: Some(crate::ast::FromClause {
19499 primary: srf,
19500 joins: Vec::new(),
19501 }),
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 TableRef {
19514 name,
19515 alias,
19516 only: false,
19517 as_of_segment: None,
19518 unnest_expr: None,
19519 unnest_column_aliases: Vec::new(),
19520 with_ordinality: false,
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 scalar_fn_item: false,
19528 }
19529 }
19530
19531 /// True when the expression tree contains an unresolved
19532 /// `OVER w` marker (see parse_over_clause).
19533 fn expr_has_named_window(e: &Expr) -> bool {
19534 match e {
19535 Expr::WindowFunction { partition_by, .. } => matches!(
19536 partition_by.as_slice(),
19537 [Expr::Column(c)] if matches!(
19538 c.qualifier.as_deref(),
19539 Some("__named_window__") | Some("__named_window_ref__")
19540 )
19541 ),
19542 Expr::Binary { lhs, rhs, .. } => {
19543 Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
19544 }
19545 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
19546 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
19547 Expr::Case {
19548 operand,
19549 branches,
19550 else_branch,
19551 } => {
19552 operand.as_deref().is_some_and(Self::expr_has_named_window)
19553 || branches.iter().any(|(w, t)| {
19554 Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
19555 })
19556 || else_branch
19557 .as_deref()
19558 .is_some_and(Self::expr_has_named_window)
19559 }
19560 _ => false,
19561 }
19562 }
19563
19564 /// v7.39 (round 705) — the NAMES the expression references through the
19565 /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
19566 /// definitions nothing referenced. Traversal mirrors
19567 /// `expr_has_named_window` above.
19568 fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
19569 match e {
19570 Expr::WindowFunction { partition_by, .. } => {
19571 if let [Expr::Column(c)] = partition_by.as_slice()
19572 && matches!(
19573 c.qualifier.as_deref(),
19574 Some("__named_window__") | Some("__named_window_ref__")
19575 )
19576 {
19577 into.push(c.name.clone());
19578 }
19579 }
19580 Expr::Binary { lhs, rhs, .. } => {
19581 Self::collect_named_window_refs(lhs, into);
19582 Self::collect_named_window_refs(rhs, into);
19583 }
19584 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19585 Self::collect_named_window_refs(expr, into);
19586 }
19587 Expr::FunctionCall { args, .. } => {
19588 for a in args {
19589 Self::collect_named_window_refs(a, into);
19590 }
19591 }
19592 Expr::Case {
19593 operand,
19594 branches,
19595 else_branch,
19596 } => {
19597 if let Some(o) = operand.as_deref() {
19598 Self::collect_named_window_refs(o, into);
19599 }
19600 for (w, t) in branches {
19601 Self::collect_named_window_refs(w, into);
19602 Self::collect_named_window_refs(t, into);
19603 }
19604 if let Some(eb) = else_branch.as_deref() {
19605 Self::collect_named_window_refs(eb, into);
19606 }
19607 }
19608 _ => {}
19609 }
19610 }
19611
19612 /// Inline named-window definitions into the `OVER w` markers.
19613 /// An unknown name errors (PG: window "w" does not exist).
19614 #[allow(clippy::type_complexity)]
19615 fn substitute_named_windows(
19616 e: &mut Expr,
19617 defs: &[(
19618 String,
19619 (
19620 Vec<Expr>,
19621 Vec<(Expr, bool, Option<bool>)>,
19622 Option<WindowFrame>,
19623 ),
19624 )],
19625 ) -> Result<(), String> {
19626 match e {
19627 Expr::WindowFunction {
19628 partition_by,
19629 order_by,
19630 frame,
19631 ..
19632 } => {
19633 // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
19634 // from the bare `OVER w1` (a plain reference).
19635 let named = match partition_by.as_slice() {
19636 [Expr::Column(c)] => match c.qualifier.as_deref() {
19637 Some("__named_window__") => Some((c.name.clone(), false)),
19638 Some("__named_window_ref__") => Some((c.name.clone(), true)),
19639 _ => None,
19640 },
19641 _ => None,
19642 };
19643 if let Some((wname, is_copy)) = named {
19644 let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
19645 else {
19646 return Err(alloc::format!("window {wname:?} does not exist"));
19647 };
19648 if !is_copy {
19649 *partition_by = def.0.clone();
19650 *order_by = def.1.clone();
19651 *frame = def.2.clone();
19652 return Ok(());
19653 }
19654 // v7.39 (round 229) — PG's copy rules, probed against
19655 // 18.4: a copy inherits the partitioning, may supply an
19656 // ordering only when the base has none, and may not copy
19657 // a base that already carries a frame (its own frame
19658 // would be ambiguous with the inherited one).
19659 if !def.1.is_empty() && !order_by.is_empty() {
19660 return Err(alloc::format!(
19661 "cannot override ORDER BY clause of window \"{wname}\""
19662 ));
19663 }
19664 if def.2.is_some() {
19665 return Err(alloc::format!(
19666 "cannot copy window \"{wname}\" because it has a frame clause"
19667 ));
19668 }
19669 *partition_by = def.0.clone();
19670 if order_by.is_empty() {
19671 *order_by = def.1.clone();
19672 }
19673 }
19674 Ok(())
19675 }
19676 Expr::Binary { lhs, rhs, .. } => {
19677 Self::substitute_named_windows(lhs, defs)?;
19678 Self::substitute_named_windows(rhs, defs)
19679 }
19680 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19681 Self::substitute_named_windows(expr, defs)
19682 }
19683 Expr::FunctionCall { args, .. } => {
19684 for a in args {
19685 Self::substitute_named_windows(a, defs)?;
19686 }
19687 Ok(())
19688 }
19689 Expr::Case {
19690 operand,
19691 branches,
19692 else_branch,
19693 } => {
19694 if let Some(op) = operand {
19695 Self::substitute_named_windows(op, defs)?;
19696 }
19697 for (w, t) in branches {
19698 Self::substitute_named_windows(w, defs)?;
19699 Self::substitute_named_windows(t, defs)?;
19700 }
19701 if let Some(el) = else_branch {
19702 Self::substitute_named_windows(el, defs)?;
19703 }
19704 Ok(())
19705 }
19706 _ => Ok(()),
19707 }
19708 }
19709
19710 /// SQL-standard `TABLE name` shorthand — builds the equivalent
19711 /// `SELECT * FROM name` head. Callers own set-op chain / tail
19712 /// composition.
19713 fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
19714 debug_assert!(matches!(self.peek(), Token::Table));
19715 self.advance(); // TABLE
19716 let tname = self.expect_ident_like()?;
19717 Ok(SelectStatement {
19718 locking: None,
19719 ctes: Vec::new(),
19720 distinct: false,
19721 distinct_on: Vec::new(),
19722 items: alloc::vec![SelectItem::Wildcard],
19723 from: Some(FromClause {
19724 primary: TableRef {
19725 name: tname,
19726 alias: None,
19727 only: false,
19728 as_of_segment: None,
19729 unnest_expr: None,
19730 unnest_column_aliases: Vec::new(),
19731 with_ordinality: false,
19732 generate_series_args: None,
19733 lateral_subquery: None,
19734 jsonb_each_text_arg: None,
19735 table_fn_call: None,
19736 rows_from: None,
19737 json_table: None,
19738 scalar_fn_item: false,
19739 },
19740 joins: Vec::new(),
19741 }),
19742 where_: None,
19743 group_by: None,
19744 group_by_all: false,
19745 having: None,
19746 unions: Vec::new(),
19747 order_by: Vec::new(),
19748 limit: None,
19749 offset: None,
19750 limit_with_ties: false,
19751 window_check_exprs: Vec::new(),
19752 })
19753 }
19754
19755 /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
19756 /// variants) → a derived table that reads each declared column out of
19757 /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
19758 /// `jsonb_array_elements(J)` (one row per element, column `value`);
19759 /// the scalar *record form projects a single row straight off `J`.
19760 /// Rides the existing lateral-subquery channel, so no new executor or
19761 /// AST is needed.
19762 fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
19763 use crate::ast::{
19764 BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
19765 };
19766 let fn_name = match self.peek() {
19767 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19768 _ => unreachable!("caller guarded is_json_to_record_name"),
19769 };
19770 self.advance(); // fn name
19771 self.advance(); // (
19772 let mut arg = self.parse_expr(0)?;
19773 // populate_record(base, json): the base only carries the record
19774 // type here — the JSON argument is the second expression.
19775 let mut base: Option<Expr> = None;
19776 if matches!(self.peek(), Token::Comma) {
19777 self.advance();
19778 base = Some(arg);
19779 arg = self.parse_expr(0)?;
19780 }
19781 if !matches!(self.peek(), Token::RParen) {
19782 return Err(self.err(alloc::format!(
19783 "expected ')' after {fn_name}() argument, got {:?}",
19784 self.peek()
19785 )));
19786 }
19787 self.advance(); // )
19788 let is_set = fn_name.ends_with("recordset");
19789 // `[AS] alias ( col type [, …] )` column-definition list.
19790 if matches!(self.peek(), Token::As) {
19791 self.advance();
19792 }
19793 let alias_opt = match self.peek() {
19794 Token::Ident(s) | Token::QuotedIdent(s) => {
19795 let a = s.clone();
19796 self.advance();
19797 Some(a)
19798 }
19799 _ => None,
19800 };
19801 // v7.39 (read01 round 76) — the populate family's canonical PG
19802 // spelling carries no column list at all: the row shape comes from
19803 // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
19804 // j)`). The parser has no catalog, so hand the two arguments to the
19805 // engine's table-function channel, which does. Only `*_to_record*`
19806 // (whose base is bare `record`) genuinely requires the list.
19807 if !matches!(self.peek(), Token::LParen) {
19808 if let Some(base_expr) = base {
19809 let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
19810 return Ok(TableRef {
19811 name: alias.clone(),
19812 alias: Some(alias),
19813 only: false,
19814 as_of_segment: None,
19815 unnest_expr: None,
19816 unnest_column_aliases: Vec::new(),
19817 with_ordinality: false,
19818 generate_series_args: None,
19819 lateral_subquery: None,
19820 jsonb_each_text_arg: None,
19821 table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
19822 rows_from: None,
19823 json_table: None,
19824 scalar_fn_item: false,
19825 });
19826 }
19827 return Err(self.err(alloc::format!(
19828 "expected '(' to start the {fn_name} column-definition list, got {:?}",
19829 self.peek()
19830 )));
19831 }
19832 let Some(alias) = alias_opt else {
19833 return Err(self.err(alloc::format!(
19834 "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
19835 )));
19836 };
19837 self.advance(); // (
19838 let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
19839 loop {
19840 let col = self.expect_ident_like()?;
19841 let ty = self.parse_cast_target()?;
19842 coldefs.push((col, ty));
19843 if matches!(self.peek(), Token::Comma) {
19844 self.advance();
19845 continue;
19846 }
19847 if matches!(self.peek(), Token::RParen) {
19848 self.advance();
19849 break;
19850 }
19851 return Err(self.err(alloc::format!(
19852 "expected ',' or ')' in {fn_name} column list, got {:?}",
19853 self.peek()
19854 )));
19855 }
19856 if coldefs.is_empty() {
19857 return Err(self.err(alloc::format!(
19858 "{fn_name} column-definition list must declare at least one column"
19859 )));
19860 }
19861 // Per column: (base ->> 'col')::type AS col. The base is the
19862 // per-element `value` column for the *set form, or the argument
19863 // itself for the scalar record form.
19864 let items: Vec<SelectItem> = coldefs
19865 .into_iter()
19866 .map(|(col, ty)| {
19867 let base = if is_set {
19868 Expr::Column(ColumnName {
19869 qualifier: None,
19870 name: "value".to_string(),
19871 })
19872 } else {
19873 arg.clone()
19874 };
19875 SelectItem::Expr {
19876 expr: Expr::Cast {
19877 expr: Box::new(Expr::Binary {
19878 lhs: Box::new(base),
19879 op: BinOp::JsonGetText,
19880 rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
19881 }),
19882 target: ty,
19883 },
19884 alias: Some(col),
19885 }
19886 })
19887 .collect();
19888 let from = if is_set {
19889 let elem_fn = if fn_name.starts_with("jsonb") {
19890 "jsonb_array_elements"
19891 } else {
19892 "json_array_elements"
19893 };
19894 Some(FromClause {
19895 primary: TableRef {
19896 name: "value".to_string(),
19897 alias: None,
19898 only: false,
19899 as_of_segment: None,
19900 unnest_expr: Some(Box::new(Expr::FunctionCall {
19901 name: elem_fn.to_string(),
19902 args: alloc::vec![arg],
19903 })),
19904 unnest_column_aliases: alloc::vec!["value".to_string()],
19905 with_ordinality: false,
19906 generate_series_args: None,
19907 lateral_subquery: None,
19908 jsonb_each_text_arg: None,
19909 table_fn_call: None,
19910 rows_from: None,
19911 json_table: None,
19912 scalar_fn_item: false,
19913 },
19914 joins: Vec::new(),
19915 })
19916 } else {
19917 None
19918 };
19919 let inner = SelectStatement {
19920 locking: None,
19921 ctes: Vec::new(),
19922 distinct: false,
19923 distinct_on: Vec::new(),
19924 items,
19925 from,
19926 where_: None,
19927 group_by: None,
19928 group_by_all: false,
19929 having: None,
19930 unions: Vec::new(),
19931 order_by: Vec::new(),
19932 limit: None,
19933 offset: None,
19934 limit_with_ties: false,
19935 window_check_exprs: Vec::new(),
19936 };
19937 Ok(TableRef {
19938 name: alias.clone(),
19939 alias: Some(alias),
19940 only: false,
19941 as_of_segment: None,
19942 unnest_expr: None,
19943 unnest_column_aliases: Vec::new(),
19944 with_ordinality: false,
19945 generate_series_args: None,
19946 lateral_subquery: Some(Box::new(inner)),
19947 jsonb_each_text_arg: None,
19948 table_fn_call: None,
19949 rows_from: None,
19950 json_table: None,
19951 scalar_fn_item: false,
19952 })
19953 }
19954
19955 /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
19956 /// Returns true when the clause was present. `WITH` alone (a
19957 /// CTE can never start here) is not enough — the ORDINALITY
19958 /// ident must follow, so a stray WITH still errors downstream.
19959 fn absorb_with_ordinality(&mut self) -> bool {
19960 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
19961 && matches!(self.tokens.get(self.pos + 1),
19962 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
19963 {
19964 self.advance();
19965 self.advance();
19966 true
19967 } else {
19968 false
19969 }
19970 }
19971
19972 /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
19973 /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
19974 /// Out-of-line: the caller sits on the FROM recursion chain.
19975 #[inline(never)]
19976 fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
19977 let fn_name = match self.advance() {
19978 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19979 _ => unreachable!("caller peeked an ident"),
19980 };
19981 self.advance(); // (
19982 let mut args: Vec<Expr> = Vec::new();
19983 if !matches!(self.peek(), Token::RParen) {
19984 loop {
19985 args.push(self.parse_expr(0)?);
19986 if matches!(self.peek(), Token::Comma) {
19987 self.advance();
19988 continue;
19989 }
19990 break;
19991 }
19992 }
19993 if !matches!(self.peek(), Token::RParen) {
19994 return Err(self.err(alloc::format!(
19995 "expected ')' after {fn_name}() arguments, got {:?}",
19996 self.peek()
19997 )));
19998 }
19999 self.advance();
20000 // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20001 // counter column rides after the function's own, and the alias list
20002 // names it.
20003 let with_ordinality = self.absorb_with_ordinality();
20004 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20005 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20006 Ok(TableRef {
20007 name,
20008 alias: alias_ident,
20009 only: false,
20010 as_of_segment: None,
20011 unnest_expr: None,
20012 unnest_column_aliases,
20013 with_ordinality,
20014 generate_series_args: None,
20015 lateral_subquery: None,
20016 jsonb_each_text_arg: None,
20017 table_fn_call: Some(Box::new((fn_name, args))),
20018 rows_from: None,
20019 json_table: None,
20020 scalar_fn_item: false,
20021 })
20022 }
20023
20024 /// v7.39 (round 205, JSON_TABLE) — parse
20025 /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20026 /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20027 /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20028 #[inline(never)]
20029 fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20030 self.advance(); // json_table
20031 self.advance(); // (
20032 let doc = Box::new(self.parse_expr(0)?);
20033 self.expect_comma_json_table()?;
20034 let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20035 // Optional `PASSING <expr> AS <name> [, …]`.
20036 let mut passing: Vec<(String, Expr)> = Vec::new();
20037 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20038 self.advance();
20039 loop {
20040 let e = self.parse_expr(0)?;
20041 if !matches!(self.peek(), Token::As) {
20042 return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20043 }
20044 self.advance();
20045 let vname = match self.advance() {
20046 Token::Ident(s) | Token::QuotedIdent(s) => s,
20047 other => {
20048 return Err(self.err(alloc::format!(
20049 "expected PASSING variable name, got {other:?}"
20050 )));
20051 }
20052 };
20053 passing.push((vname, e));
20054 if matches!(self.peek(), Token::Comma) {
20055 self.advance();
20056 continue;
20057 }
20058 break;
20059 }
20060 }
20061 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20062 return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20063 }
20064 self.advance();
20065 let columns = self.parse_json_table_columns()?;
20066 if !matches!(self.peek(), Token::RParen) {
20067 return Err(self.err(alloc::format!(
20068 "expected ')' to close JSON_TABLE, got {:?}",
20069 self.peek()
20070 )));
20071 }
20072 self.advance();
20073 let alias_ident = self.parse_optional_alias()?;
20074 let name = alias_ident
20075 .clone()
20076 .unwrap_or_else(|| String::from("json_table"));
20077 Ok(TableRef {
20078 name,
20079 alias: alias_ident,
20080 only: false,
20081 as_of_segment: None,
20082 unnest_expr: None,
20083 unnest_column_aliases: Vec::new(),
20084 with_ordinality: false,
20085 generate_series_args: None,
20086 lateral_subquery: None,
20087 jsonb_each_text_arg: None,
20088 table_fn_call: None,
20089 rows_from: None,
20090 json_table: Some(Box::new(crate::ast::JsonTable {
20091 doc,
20092 row_path,
20093 columns,
20094 passing,
20095 })),
20096 scalar_fn_item: false,
20097 })
20098 }
20099
20100 fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20101 if !matches!(self.peek(), Token::Comma) {
20102 return Err(self.err(alloc::format!(
20103 "expected ',' after JSON_TABLE document, got {:?}",
20104 self.peek()
20105 )));
20106 }
20107 self.advance();
20108 Ok(())
20109 }
20110
20111 fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20112 match self.advance() {
20113 Token::String(s) => Ok(s),
20114 other => Err(self.err(alloc::format!(
20115 "expected {what} string literal, got {other:?}"
20116 ))),
20117 }
20118 }
20119
20120 /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20121 #[inline(never)]
20122 fn parse_json_table_columns(
20123 &mut self,
20124 ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20125 if !matches!(self.peek(), Token::LParen) {
20126 return Err(self.err("expected '(' after COLUMNS".into()));
20127 }
20128 self.advance();
20129 let mut cols = Vec::new();
20130 loop {
20131 cols.push(self.parse_json_table_one_column()?);
20132 if matches!(self.peek(), Token::Comma) {
20133 self.advance();
20134 continue;
20135 }
20136 break;
20137 }
20138 if !matches!(self.peek(), Token::RParen) {
20139 return Err(self.err(alloc::format!(
20140 "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20141 self.peek()
20142 )));
20143 }
20144 self.advance();
20145 Ok(cols)
20146 }
20147
20148 fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20149 use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20150 // NESTED [PATH] '<p>' COLUMNS (...)
20151 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20152 self.advance();
20153 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20154 self.advance();
20155 }
20156 let path = self.parse_json_string_literal("NESTED PATH")?;
20157 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20158 return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20159 }
20160 self.advance();
20161 let columns = self.parse_json_table_columns()?;
20162 return Ok(JsonTableColumn::Nested { path, columns });
20163 }
20164 // <name> ...
20165 let name = match self.advance() {
20166 Token::Ident(s) | Token::QuotedIdent(s) => s,
20167 other => {
20168 return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20169 }
20170 };
20171 // <name> FOR ORDINALITY
20172 if matches!(self.peek(), Token::For) {
20173 self.advance();
20174 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20175 return Err(self.err("expected ORDINALITY after FOR".into()));
20176 }
20177 self.advance();
20178 return Ok(JsonTableColumn::Ordinality { name });
20179 }
20180 // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20181 let ty = self.parse_column_type_name()?;
20182 let mut format_json = false;
20183 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20184 self.advance();
20185 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20186 return Err(self.err("expected JSON after FORMAT".into()));
20187 }
20188 self.advance();
20189 format_json = true;
20190 }
20191 let mut exists = false;
20192 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20193 self.advance();
20194 exists = true;
20195 }
20196 let mut path = alloc::format!("$.{name}");
20197 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20198 self.advance();
20199 path = self.parse_json_string_literal("column PATH")?;
20200 }
20201 if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20202 // `FORMAT JSON` after PATH (alternate placement).
20203 self.advance();
20204 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20205 self.advance();
20206 }
20207 format_json = true;
20208 }
20209 let mut wrapper = false;
20210 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20211 self.advance();
20212 // optional CONDITIONAL/UNCONDITIONAL
20213 if matches!(self.peek(), Token::Ident(s)
20214 if s.eq_ignore_ascii_case("unconditional")
20215 || s.eq_ignore_ascii_case("conditional"))
20216 {
20217 self.advance();
20218 }
20219 if !matches!(self.peek(), Token::Ident(s)
20220 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20221 {
20222 return Err(self.err("expected WRAPPER after WITH".into()));
20223 }
20224 self.advance();
20225 // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20226 if matches!(self.peek(), Token::Ident(s)
20227 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20228 {
20229 self.advance();
20230 }
20231 wrapper = true;
20232 }
20233 // ON EMPTY / ON ERROR clauses (two, in any order).
20234 let mut on_empty = JsonTableOnBehavior::Null;
20235 let mut on_error = JsonTableOnBehavior::Null;
20236 for _ in 0..2 {
20237 let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20238 {
20239 self.advance();
20240 Some(JsonTableOnBehavior::Error)
20241 } else if matches!(self.peek(), Token::Null) {
20242 self.advance();
20243 Some(JsonTableOnBehavior::Null)
20244 } else if matches!(self.peek(), Token::Default) {
20245 self.advance();
20246 Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
20247 } else {
20248 None
20249 };
20250 let Some(behavior) = behavior else { break };
20251 // `ON {EMPTY|ERROR}`
20252 if !matches!(self.peek(), Token::On) {
20253 return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
20254 }
20255 self.advance();
20256 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
20257 self.advance();
20258 on_empty = behavior;
20259 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
20260 self.advance();
20261 on_error = behavior;
20262 } else {
20263 return Err(self.err("expected EMPTY or ERROR after ON".into()));
20264 }
20265 }
20266 Ok(JsonTableColumn::Regular {
20267 name,
20268 ty,
20269 path,
20270 exists,
20271 format_json,
20272 wrapper,
20273 on_empty,
20274 on_error,
20275 })
20276 }
20277
20278 fn parse_optional_alias_with_columns(
20279 &mut self,
20280 ) -> Result<(Option<String>, Vec<String>), ParseError> {
20281 let alias = self.parse_optional_alias()?;
20282 if alias.is_none() {
20283 return Ok((None, Vec::new()));
20284 }
20285 let mut cols: Vec<String> = Vec::new();
20286 if matches!(self.peek(), Token::LParen) {
20287 self.advance();
20288 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
20289 self.advance();
20290 cols.push(s);
20291 if matches!(self.peek(), Token::Comma) {
20292 self.advance();
20293 continue;
20294 }
20295 break;
20296 }
20297 if matches!(self.peek(), Token::RParen) {
20298 self.advance();
20299 }
20300 }
20301 Ok((alias, cols))
20302 }
20303
20304 /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
20305 /// whose keyword token was already consumed and whose `(` is the
20306 /// current token. Factored out of `parse_atom` (and marked
20307 /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
20308 /// recursive `parse_atom` frame — inlining them there enlarges the
20309 /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
20310 /// against, risking an overflow before the budget triggers.
20311 #[inline(never)]
20312 fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
20313 self.advance(); // (
20314 let mut args = Vec::new();
20315 if !matches!(self.peek(), Token::RParen) {
20316 loop {
20317 args.push(self.parse_expr(0)?);
20318 match self.peek() {
20319 Token::Comma => {
20320 self.advance();
20321 }
20322 Token::RParen => break,
20323 other => {
20324 return Err(self.err(alloc::format!(
20325 "expected ',' or ')' in {name}() args, got {other:?}"
20326 )));
20327 }
20328 }
20329 }
20330 }
20331 self.advance(); // )
20332 Ok(Expr::FunctionCall {
20333 name: name.into(),
20334 args,
20335 })
20336 }
20337
20338 /// FROM-clause: a primary table reference plus zero-or-more joined
20339 /// peers expressed via either `, <table>` (cross-product, no ON) or
20340 /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
20341 /// v1.10 keeps the join list flat (left-associative nested-loop
20342 /// semantics).
20343 fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
20344 let primary = self.parse_table_ref()?;
20345 let primary_qual = primary
20346 .alias
20347 .clone()
20348 .unwrap_or_else(|| primary.name.clone());
20349 let joins = self.parse_from_joins(&primary_qual)?;
20350 Ok(FromClause { primary, joins })
20351 }
20352
20353 /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
20354 /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
20355 /// SAME grammar after its target table has already been consumed.
20356 /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
20357 /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
20358 /// be parsed forward, once.)
20359 /// `left_primary_qual` is the qualifier (alias, else name) of whatever
20360 /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
20361 /// target in the MySQL multi-table form. It only feeds the `USING (…)`
20362 /// desugaring, which needs a name for the left side of each equality.
20363 fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
20364 let mut joins = Vec::new();
20365 loop {
20366 // `, <table>` — cross-product with no ON.
20367 if matches!(self.peek(), Token::Comma) {
20368 self.advance();
20369 let table = self.parse_table_ref()?;
20370 joins.push(FromJoin {
20371 kind: JoinKind::Cross,
20372 table,
20373 on: None,
20374 using_cols: None,
20375 natural: false,
20376 });
20377 continue;
20378 }
20379 // v7.37.16 — optional leading `NATURAL` before the join
20380 // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
20381 // not a lexer keyword (it arrives as a bare Ident), so match
20382 // it case-insensitively here. When present, no ON/USING
20383 // clause is allowed — the common columns are resolved at
20384 // execution time.
20385 let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
20386 if natural {
20387 self.advance();
20388 }
20389 // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
20390 // CROSS JOIN, and bare JOIN (defaults to INNER).
20391 let kind =
20392 match self.peek() {
20393 Token::Inner => {
20394 self.advance();
20395 if !matches!(self.peek(), Token::Join) {
20396 return Err(self
20397 .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
20398 }
20399 self.advance();
20400 JoinKind::Inner
20401 }
20402 Token::Left => {
20403 self.advance();
20404 if matches!(self.peek(), Token::Outer) {
20405 self.advance();
20406 }
20407 if !matches!(self.peek(), Token::Join) {
20408 return Err(self.err(format!(
20409 "expected JOIN after LEFT [OUTER], got {:?}",
20410 self.peek()
20411 )));
20412 }
20413 self.advance();
20414 JoinKind::Left
20415 }
20416 Token::Cross => {
20417 self.advance();
20418 if !matches!(self.peek(), Token::Join) {
20419 return Err(self
20420 .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
20421 }
20422 self.advance();
20423 JoinKind::Cross
20424 }
20425 // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
20426 Token::Right => {
20427 self.advance();
20428 if matches!(self.peek(), Token::Outer) {
20429 self.advance();
20430 }
20431 if !matches!(self.peek(), Token::Join) {
20432 return Err(self.err(format!(
20433 "expected JOIN after RIGHT [OUTER], got {:?}",
20434 self.peek()
20435 )));
20436 }
20437 self.advance();
20438 JoinKind::Right
20439 }
20440 // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
20441 Token::Full => {
20442 self.advance();
20443 if matches!(self.peek(), Token::Outer) {
20444 self.advance();
20445 }
20446 if !matches!(self.peek(), Token::Join) {
20447 return Err(self.err(format!(
20448 "expected JOIN after FULL [OUTER], got {:?}",
20449 self.peek()
20450 )));
20451 }
20452 self.advance();
20453 JoinKind::FullOuter
20454 }
20455 Token::Join => {
20456 self.advance();
20457 JoinKind::Inner
20458 }
20459 _ => break,
20460 };
20461 let table = self.parse_table_ref()?;
20462 // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
20463 // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
20464 // where prev_table is the most-recent left-side table
20465 // (the previous join's table if any, else the FROM primary).
20466 // PG semantics around column merging are richer (USING'd
20467 // cols become deduplicated single output columns); for
20468 // sugar purposes the predicate-only form covers the
20469 // baseline corpus shape and chained `… JOIN x USING (k)
20470 // JOIN y USING (k)` calls.
20471 // v7.37.16 — NATURAL joins carry no ON/USING clause; the
20472 // common columns resolve at execution time.
20473 if natural {
20474 joins.push(FromJoin {
20475 kind,
20476 table,
20477 on: None,
20478 using_cols: None,
20479 natural: true,
20480 });
20481 continue;
20482 }
20483 let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
20484 // v7.37.16 — capture the USING column list (in addition to
20485 // the ON desugar below) so the executor can perform PG's
20486 // column-merge on the output side.
20487 let mut using_cols: Option<Vec<String>> = None;
20488 let on = if matches!(self.peek(), Token::On) {
20489 self.advance();
20490 Some(self.parse_expr(0)?)
20491 } else if using_match {
20492 self.advance();
20493 if !matches!(self.peek(), Token::LParen) {
20494 return Err(
20495 self.err(format!("expected '(' after USING, got {:?}", self.peek()))
20496 );
20497 }
20498 self.advance();
20499 let mut cols: Vec<String> = Vec::new();
20500 loop {
20501 match self.peek().clone() {
20502 Token::Ident(s) | Token::QuotedIdent(s) => {
20503 self.advance();
20504 cols.push(s);
20505 }
20506 other => {
20507 return Err(self.err(format!(
20508 "expected column name inside USING (…), got {other:?}"
20509 )));
20510 }
20511 }
20512 match self.peek() {
20513 Token::Comma => {
20514 self.advance();
20515 continue;
20516 }
20517 Token::RParen => {
20518 self.advance();
20519 break;
20520 }
20521 other => {
20522 return Err(self.err(format!(
20523 "expected ',' or ')' inside USING (…), got {other:?}"
20524 )));
20525 }
20526 }
20527 }
20528 if cols.is_empty() {
20529 return Err(self.err("USING (…) requires at least one column".to_string()));
20530 }
20531 using_cols = Some(cols.clone());
20532 // Pick the left-side alias: prev join's table if any,
20533 // else FROM primary. Use alias when present, else
20534 // table name (PG-equivalent qualifier).
20535 let left_qual: String = joins
20536 .last()
20537 .map(|j| {
20538 j.table
20539 .alias
20540 .clone()
20541 .unwrap_or_else(|| j.table.name.clone())
20542 })
20543 .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
20544 let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
20545 let mut iter = cols.into_iter().map(|c| Expr::Binary {
20546 lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20547 qualifier: Some(left_qual.clone()),
20548 name: c.clone(),
20549 })),
20550 op: crate::ast::BinOp::Eq,
20551 rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20552 qualifier: Some(right_qual.clone()),
20553 name: c,
20554 })),
20555 });
20556 let first = iter.next().expect("at least one col");
20557 Some(iter.fold(first, |acc, pred| Expr::Binary {
20558 lhs: alloc::boxed::Box::new(acc),
20559 op: crate::ast::BinOp::And,
20560 rhs: alloc::boxed::Box::new(pred),
20561 }))
20562 } else if kind == JoinKind::Cross {
20563 None
20564 } else {
20565 return Err(self.err(format!(
20566 "expected ON or USING after {:?} JOIN, got {:?}",
20567 kind,
20568 self.peek()
20569 )));
20570 };
20571 joins.push(FromJoin {
20572 kind,
20573 table,
20574 on,
20575 using_cols,
20576 natural: false,
20577 });
20578 }
20579 Ok(joins)
20580 }
20581
20582 /// Optional alias after an expression or table:
20583 /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
20584 /// accepted (PG-style implicit alias). Returns `None` if the next token
20585 /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
20586 fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
20587 if matches!(self.peek(), Token::As) {
20588 self.advance();
20589 // v7.39 (round 340, V56) — after AS the next token MUST be an
20590 // identifier. This used to return None and "let the caller
20591 // surface the error on the next expectation", but when AS is
20592 // the LAST token there is no next expectation: `SELECT 1 AS`
20593 // parsed clean and silently dropped the alias. PG rejects it.
20594 if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
20595 return self.expect_ident_like().map(Some);
20596 }
20597 return Err(self.err(alloc::format!(
20598 "expected an alias after AS, got {:?}",
20599 self.peek()
20600 )));
20601 }
20602 // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
20603 // grammar reserves a long list of follow-keywords from the
20604 // alias slot. SPG's bareword approximation: skip a small
20605 // set of idents that would otherwise be swallowed as the
20606 // table alias and break trailing clauses like CREATE
20607 // MATERIALIZED VIEW … WITH [NO] DATA or future ON
20608 // CONFLICT WHERE shapes.
20609 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
20610 if is_alias_stopword(s) {
20611 return Ok(None);
20612 }
20613 return Ok(self.expect_ident_like().ok());
20614 }
20615 Ok(None)
20616 }
20617
20618 /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
20619 fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
20620 // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
20621 // error beats a stack overflow (an overflow aborts the
20622 // embedding host process).
20623 self.enter_nested()?;
20624 let r = self.parse_expr_inner(min_prec);
20625 self.nest_depth -= 1;
20626 r
20627 }
20628
20629 /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
20630 /// When the upcoming tokens form one, return the underlying
20631 /// operator token and the position just past the closing paren
20632 /// so the binary loop can dispatch on the plain operator.
20633 fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
20634 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
20635 return None;
20636 }
20637 if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
20638 return None;
20639 }
20640 let mut i = self.pos + 2;
20641 // Optional schema qualifier (pg_catalog.<op> etc.).
20642 if matches!(self.tokens.get(i), Some(Token::Ident(_)))
20643 && matches!(self.tokens.get(i + 1), Some(Token::Dot))
20644 {
20645 i += 2;
20646 }
20647 let op_tok = self.tokens.get(i)?.clone();
20648 if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
20649 return None;
20650 }
20651 Some((i + 2, op_tok))
20652 }
20653
20654 /// PG operator symbols that lower onto function calls in
20655 /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
20656 /// family → regexp_like, comparison rung), `^@` (starts_with,
20657 /// comparison rung), `^` (power, tighter than `*`), `#`
20658 /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
20659 /// subset of the OR bits so the subtraction never borrows).
20660 fn try_symbol_operator(
20661 &mut self,
20662 lhs: &Expr,
20663 min_prec: u8,
20664 ) -> Result<Option<Expr>, ParseError> {
20665 enum Sym {
20666 Regex { ci: bool, negated: bool },
20667 Like { ci: bool, negated: bool },
20668 StartsWith,
20669 Power,
20670 Xor,
20671 RangeAdjacent,
20672 }
20673 // v7.39 (IS-precedence knife) — the low-precedence postfix
20674 // predicates ride this existing leaf call (zero new frame slots
20675 // on the nesting chain).
20676 if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
20677 return Ok(Some(e));
20678 }
20679 let (sym, prec): (Sym, u8) = match self.peek() {
20680 Token::Tilde => (
20681 Sym::Regex {
20682 ci: false,
20683 negated: false,
20684 },
20685 5,
20686 ),
20687 Token::TildeStar => (
20688 Sym::Regex {
20689 ci: true,
20690 negated: false,
20691 },
20692 5,
20693 ),
20694 Token::NotTilde => (
20695 Sym::Regex {
20696 ci: false,
20697 negated: true,
20698 },
20699 5,
20700 ),
20701 Token::NotTildeStar => (
20702 Sym::Regex {
20703 ci: true,
20704 negated: true,
20705 },
20706 5,
20707 ),
20708 // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
20709 Token::DoubleTilde => (
20710 Sym::Like {
20711 ci: false,
20712 negated: false,
20713 },
20714 5,
20715 ),
20716 Token::DoubleTildeStar => (
20717 Sym::Like {
20718 ci: true,
20719 negated: false,
20720 },
20721 5,
20722 ),
20723 Token::NotDoubleTilde => (
20724 Sym::Like {
20725 ci: false,
20726 negated: true,
20727 },
20728 5,
20729 ),
20730 Token::NotDoubleTildeStar => (
20731 Sym::Like {
20732 ci: true,
20733 negated: true,
20734 },
20735 5,
20736 ),
20737 Token::CaretAt => (Sym::StartsWith, 5),
20738 // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
20739 // tighter than `* / & |`, which the prec-9 rung preserves —
20740 // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
20741 Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
20742 Token::Caret => (Sym::Power, 9),
20743 // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
20744 // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
20745 Token::Hash => (Sym::Xor, 6),
20746 Token::Adjacent => (Sym::RangeAdjacent, 5),
20747 _ => return Ok(None),
20748 };
20749 if prec < min_prec {
20750 return Ok(None);
20751 }
20752 self.advance();
20753 let rhs = self.parse_expr(prec + 1)?;
20754 let out = match sym {
20755 Sym::Regex { ci, negated } => {
20756 let mut args = alloc::vec![lhs.clone(), rhs];
20757 if ci {
20758 args.push(Expr::Literal(Literal::String(String::from("i"))));
20759 }
20760 maybe_not(
20761 Expr::FunctionCall {
20762 name: String::from("regexp_like"),
20763 args,
20764 },
20765 negated,
20766 )
20767 }
20768 Sym::Like { ci, negated } => Expr::Like {
20769 expr: alloc::boxed::Box::new(lhs.clone()),
20770 pattern: alloc::boxed::Box::new(rhs),
20771 negated,
20772 case_insensitive: ci,
20773 },
20774 Sym::StartsWith => Expr::FunctionCall {
20775 name: String::from("starts_with"),
20776 args: alloc::vec![lhs.clone(), rhs],
20777 },
20778 Sym::Power => Expr::FunctionCall {
20779 name: String::from("power"),
20780 args: alloc::vec![lhs.clone(), rhs],
20781 },
20782 // `#` bitwise XOR — a real operator now (was desugared to
20783 // `(a|b)-(a&b)`, algebraically identical for integers but
20784 // undefined for bit strings; the direct op handles both).
20785 Sym::Xor => Expr::Binary {
20786 lhs: Box::new(lhs.clone()),
20787 op: BinOp::BitXor,
20788 rhs: Box::new(rhs),
20789 },
20790 // range `-|-` "is adjacent to" — lowered to a catalog function.
20791 Sym::RangeAdjacent => Expr::FunctionCall {
20792 name: String::from("range_adjacent"),
20793 args: alloc::vec![lhs.clone(), rhs],
20794 },
20795 };
20796 Ok(Some(out))
20797 }
20798
20799 /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
20800 /// predicates, moved out of the tight postfix-cast loop: PG binds
20801 /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
20802 /// looser than EVERY binary operator (only NOT/AND/OR are looser),
20803 /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
20804 /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
20805 /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
20806 /// when nothing at this position belongs to the family. Out-of-line
20807 /// (`inline(never)`): the caller sits on the per-nesting-level frame
20808 /// chain that MAX_NEST_DEPTH is tuned against.
20809 #[inline(never)]
20810 fn parse_postfix_predicate(
20811 &mut self,
20812 lhs: &Expr,
20813 min_prec: u8,
20814 ) -> Result<Option<Expr>, ParseError> {
20815 // Reached through try_symbol_operator (an existing leaf call of
20816 // the binary loop) so NO new stack slots land on the per-nesting
20817 // frame chain; the lhs clones only when a predicate actually
20818 // consumes it.
20819 match self.peek() {
20820 // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
20821 // comparison family rung 5 (each +1 from the pre-XOR ladder).
20822 Token::Is if min_prec <= 4 => {}
20823 Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
20824 Token::Not
20825 if min_prec <= 5
20826 && matches!(
20827 self.tokens.get(self.pos + 1),
20828 Some(Token::Between | Token::In | Token::Like)
20829 ) => {}
20830 Token::Not | Token::Ident(_)
20831 if min_prec <= 5
20832 && (matches!(self.peek(), Token::Ident(s)
20833 if s.eq_ignore_ascii_case("ilike")
20834 || (self.mysql_dialect
20835 && (s.eq_ignore_ascii_case("regexp")
20836 || s.eq_ignore_ascii_case("rlike")))
20837 || (s.eq_ignore_ascii_case("similar")
20838 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
20839 || (matches!(self.peek(), Token::Not)
20840 && matches!(self.tokens.get(self.pos + 1),
20841 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
20842 || (self.mysql_dialect
20843 && (s.eq_ignore_ascii_case("regexp")
20844 || s.eq_ignore_ascii_case("rlike")))
20845 || s.eq_ignore_ascii_case("similar")))) => {}
20846 _ => return Ok(None),
20847 }
20848 let mut expr = lhs.clone();
20849 // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
20850 // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
20851 if min_prec <= 4 {
20852 if matches!(self.peek(), Token::Is) {
20853 self.advance();
20854 let negated = if matches!(self.peek(), Token::Not) {
20855 self.advance();
20856 true
20857 } else {
20858 false
20859 };
20860 // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
20861 // mailrs pg_dump.
20862 if matches!(self.peek(), Token::Distinct) {
20863 self.advance();
20864 if !matches!(self.peek(), Token::From) {
20865 return Err(self.err(format!(
20866 "expected FROM after IS{} DISTINCT, got {:?}",
20867 if negated { " NOT" } else { "" },
20868 self.peek()
20869 )));
20870 }
20871 self.advance();
20872 // Right-hand side: parse at the same precedence
20873 // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
20874 // groups as `x IS DISTINCT FROM (a + b)`.
20875 let rhs = self.parse_expr(5)?;
20876 let op = if negated {
20877 BinOp::IsNotDistinctFrom
20878 } else {
20879 BinOp::IsDistinctFrom
20880 };
20881 expr = Expr::Binary {
20882 op,
20883 lhs: Box::new(expr),
20884 rhs: Box::new(rhs),
20885 };
20886 {
20887 return Ok(Some(expr));
20888 }
20889 }
20890 // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
20891 // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
20892 // Lowers onto pg_is_json(x, kind); NOT wraps the
20893 // call in a logical negation.
20894 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20895 if s.eq_ignore_ascii_case("json"))
20896 {
20897 self.advance(); // JSON
20898 let kind = match self.peek() {
20899 Token::Ident(s) | Token::QuotedIdent(s)
20900 if matches!(
20901 s.to_ascii_lowercase().as_str(),
20902 "value" | "object" | "array" | "scalar"
20903 ) =>
20904 {
20905 let k = s.to_ascii_lowercase();
20906 self.advance();
20907 k
20908 }
20909 _ => "value".to_string(),
20910 };
20911 let call = Expr::FunctionCall {
20912 name: "pg_is_json".to_string(),
20913 args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
20914 };
20915 expr = if negated {
20916 Expr::Unary {
20917 op: UnOp::Not,
20918 expr: Box::new(call),
20919 }
20920 } else {
20921 call
20922 };
20923 {
20924 return Ok(Some(expr));
20925 }
20926 }
20927 // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
20928 // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
20929 // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
20930 {
20931 let form_kw = match self.peek() {
20932 Token::Ident(s) | Token::QuotedIdent(s)
20933 if matches!(
20934 s.to_ascii_uppercase().as_str(),
20935 "NFC" | "NFD" | "NFKC" | "NFKD"
20936 ) && matches!(
20937 self.tokens.get(self.pos + 1),
20938 Some(Token::Ident(n) | Token::QuotedIdent(n))
20939 if n.eq_ignore_ascii_case("normalized")
20940 ) =>
20941 {
20942 Some(s.to_ascii_uppercase())
20943 }
20944 _ => None,
20945 };
20946 let bare_normalized = form_kw.is_none()
20947 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20948 if s.eq_ignore_ascii_case("normalized"));
20949 if form_kw.is_some() || bare_normalized {
20950 if form_kw.is_some() {
20951 self.advance(); // form keyword
20952 }
20953 self.advance(); // NORMALIZED
20954 let mut args = alloc::vec![expr];
20955 if let Some(f) = form_kw {
20956 args.push(Expr::Literal(Literal::String(f)));
20957 }
20958 let call = Expr::FunctionCall {
20959 name: "is_normalized".to_string(),
20960 args,
20961 };
20962 expr = if negated {
20963 Expr::Unary {
20964 op: UnOp::Not,
20965 expr: Box::new(call),
20966 }
20967 } else {
20968 call
20969 };
20970 {
20971 return Ok(Some(expr));
20972 }
20973 }
20974 }
20975 // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
20976 // three-valued boolean tests. IS TRUE/FALSE never
20977 // return NULL, so they lower to CASE forms whose
20978 // ELSE catches the NULL branch; IS UNKNOWN on a
20979 // boolean is exactly IS NULL.
20980 if matches!(self.peek(), Token::True | Token::False)
20981 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
20982 {
20983 let tok = self.advance();
20984 let test = match tok {
20985 Token::True => Some(true),
20986 Token::False => Some(false),
20987 _ => None, // UNKNOWN
20988 };
20989 // v7.39 (round 328, V45) — kept as what the user
20990 // wrote. These used to be lowered here into `CASE` /
20991 // `IS NULL`; the semantics were right but the AST no
20992 // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
20993 // was echoed back as
20994 // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
20995 expr = Expr::BoolTest {
20996 expr: Box::new(expr),
20997 value: test,
20998 negated,
20999 };
21000 {
21001 return Ok(Some(expr));
21002 }
21003 }
21004 if !matches!(self.peek(), Token::Null) {
21005 return Err(self.err(format!(
21006 "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21007 if negated { " NOT" } else { "" },
21008 self.peek()
21009 )));
21010 }
21011 self.advance();
21012 expr = Expr::IsNull {
21013 expr: Box::new(expr),
21014 negated,
21015 };
21016 {
21017 return Ok(Some(expr));
21018 }
21019 }
21020 }
21021 // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21022 if min_prec <= 5 {
21023 // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21024 // Look one token ahead so a stray `NOT` not followed by any of
21025 // these flows through to the early return below untouched.
21026 let negated = if matches!(self.peek(), Token::Not) {
21027 let next = self.tokens.get(self.pos + 1);
21028 matches!(next, Some(Token::Between | Token::In | Token::Like))
21029 || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21030 || (self.mysql_dialect
21031 && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21032 || s.eq_ignore_ascii_case("similar"))
21033 } else {
21034 false
21035 };
21036 if negated {
21037 self.advance();
21038 }
21039 if matches!(self.peek(), Token::Between) {
21040 expr = self.parse_between_tail(expr, negated)?;
21041 {
21042 return Ok(Some(expr));
21043 }
21044 }
21045 if matches!(self.peek(), Token::In) {
21046 if self.suppress_in_tail && !negated {
21047 // POSITION(sub IN str) — IN belongs to the
21048 // enclosing function syntax; stop here.
21049 {
21050 return Ok(None);
21051 }
21052 }
21053 expr = self.parse_in_tail(expr, negated)?;
21054 {
21055 return Ok(Some(expr));
21056 }
21057 }
21058 // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21059 // lowers onto the internal __similar_to(expr, pat[, esc]) call
21060 // (the SQL→regex transform runs inside, in the backtracking-
21061 // friendly shape SPG's matcher needs).
21062 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21063 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21064 {
21065 self.advance(); // SIMILAR
21066 self.advance(); // TO
21067 let pattern = self.parse_expr(6)?;
21068 let mut args = alloc::vec![expr, pattern];
21069 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21070 self.advance();
21071 args.push(self.parse_expr(6)?);
21072 }
21073 let call = Expr::FunctionCall {
21074 name: "__similar_to".to_string(),
21075 args,
21076 };
21077 expr = maybe_not(call, negated);
21078 {
21079 return Ok(Some(expr));
21080 }
21081 }
21082 if matches!(self.peek(), Token::Like) {
21083 self.advance();
21084 // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21085 if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21086 expr = q;
21087 {
21088 return Ok(Some(expr));
21089 }
21090 }
21091 // Pattern at the same precedence as other comparison RHSes —
21092 // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21093 let mut pattern = self.parse_expr(6)?;
21094 // `ESCAPE 'c'` — rewrite a literal pattern to the
21095 // default backslash escape at parse time. Custom
21096 // escapes on non-literal patterns would need
21097 // matcher support; error honestly.
21098 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21099 self.advance();
21100 let esc = self.parse_expr(6)?;
21101 pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21102 }
21103 expr = Expr::Like {
21104 expr: Box::new(expr),
21105 pattern: Box::new(pattern),
21106 negated,
21107 case_insensitive: false,
21108 };
21109 {
21110 return Ok(Some(expr));
21111 }
21112 }
21113 // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21114 // keyword reaches us as a plain identifier.
21115 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21116 self.advance();
21117 if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21118 expr = q;
21119 {
21120 return Ok(Some(expr));
21121 }
21122 }
21123 let pattern = self.parse_expr(6)?;
21124 expr = Expr::Like {
21125 expr: Box::new(expr),
21126 pattern: Box::new(pattern),
21127 negated,
21128 case_insensitive: true,
21129 };
21130 {
21131 return Ok(Some(expr));
21132 }
21133 }
21134 // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21135 // operator (RLIKE is the alias). It is a keyword, not `~`, and
21136 // matches case-insensitively under the default collation, so it
21137 // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21138 // `~*` operator uses, wrapped in NOT when negated.
21139 if self.mysql_dialect
21140 && matches!(self.peek(), Token::Ident(s)
21141 if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21142 {
21143 self.advance();
21144 let pattern = self.parse_expr(6)?;
21145 let call = Expr::FunctionCall {
21146 name: String::from("regexp_like"),
21147 args: alloc::vec![
21148 expr,
21149 pattern,
21150 Expr::Literal(Literal::String(String::from("i"))),
21151 ],
21152 };
21153 return Ok(Some(maybe_not(call, negated)));
21154 }
21155 }
21156 let _ = expr;
21157 Ok(None)
21158 }
21159
21160 fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21161 let mut lhs = self.parse_unary()?;
21162 let mut chain_len = 0usize;
21163 loop {
21164 // OPERATOR([schema.]op) reduces to its underlying
21165 // operator token before the normal dispatch.
21166 let explicit = self.peek_explicit_operator();
21167 let dispatch = match &explicit {
21168 Some((_, tok)) => self.binop_here(tok),
21169 None => self.binop_here(self.peek()),
21170 };
21171 let Some((op, prec)) = dispatch else {
21172 // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21173 // of the symbol family. `binop_here` answers None for them
21174 // because they lower onto function calls rather than a
21175 // BinOp, and the fallback below reads `self.peek()` — the
21176 // word OPERATOR, not the operator. `pg_dump` writes every
21177 // catalog predicate this way, so its first query failed
21178 // and no dump ran:
21179 //
21180 // AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21181 //
21182 // Collapsing the wrapper to the operator it names puts the
21183 // token where the fallback already looks.
21184 if let Some((next, op_tok)) = explicit {
21185 self.tokens.splice(self.pos..next, [op_tok]);
21186 }
21187 if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21188 lhs = e;
21189 chain_len += 1;
21190 if chain_len > MAX_BINARY_CHAIN {
21191 return Err(self.err(alloc::format!(
21192 "more than {MAX_BINARY_CHAIN} chained binary operators"
21193 )));
21194 }
21195 continue;
21196 }
21197 break;
21198 };
21199 if prec < min_prec {
21200 break;
21201 }
21202 // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21203 // iteratively but evaluates and drops recursively;
21204 // depth beyond the budget overflows worker stacks.
21205 chain_len += 1;
21206 if chain_len > MAX_BINARY_CHAIN {
21207 return Err(self.err(alloc::format!(
21208 "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21209 )));
21210 }
21211 match explicit {
21212 Some((end_pos, _)) => self.pos = end_pos,
21213 None => {
21214 self.advance();
21215 }
21216 }
21217 // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21218 // ANY is a bare ident; ALL is a reserved Token. Both
21219 // require an immediate `(` to disambiguate from
21220 // identifier columns named `any` / `all`.
21221 let any_kind = match self.peek() {
21222 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21223 Some(false)
21224 }
21225 Token::Ident(s) | Token::QuotedIdent(s)
21226 if (s.eq_ignore_ascii_case("any")
21227 || s.eq_ignore_ascii_case("some")
21228 || s.eq_ignore_ascii_case("all"))
21229 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21230 {
21231 Some(!s.eq_ignore_ascii_case("all"))
21232 }
21233 _ => None,
21234 };
21235 if let Some(is_any) = any_kind {
21236 lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21237 continue;
21238 }
21239 let rhs = self.parse_expr(prec + 1)?;
21240 lhs = Expr::Binary {
21241 lhs: Box::new(lhs),
21242 op,
21243 rhs: Box::new(rhs),
21244 };
21245 }
21246 Ok(lhs)
21247 }
21248
21249 /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
21250 /// and the array form.
21251 ///
21252 /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
21253 /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
21254 /// this block's `Expr` temporaries and four `format!` sites slots in
21255 /// that frame on every level of `((((1))))`, which never reaches it.
21256 #[inline(never)]
21257 fn parse_any_all_rhs(
21258 &mut self,
21259 lhs: Expr,
21260 op: BinOp,
21261 is_any: bool,
21262 ) -> Result<Expr, ParseError> {
21263 self.advance(); // ident
21264 self.advance(); // (
21265 // `x op ANY (SELECT …)` — the quantified-subquery
21266 // form. `= ANY` is exactly IN; the other operators
21267 // lower onto EXISTS over the subquery as a derived
21268 // table, comparing against its single projection
21269 // aliased __v (x's columns resolve correlated).
21270 // ALL is the negated-EXISTS complement; a NULL
21271 // element makes PG return NULL where this lowering
21272 // returns true — the NOT NULL column case (the
21273 // practical one) is exact.
21274 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
21275 // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
21276 // legal PG too (round-151 sibling). Out-of-line
21277 // (#[inline(never)] helper) — this sits on
21278 // parse_expr's recursive frame and the two-armed
21279 // SELECT temporary blew the nesting-budget stack.
21280 let mut sub = self.parse_any_all_select_body()?;
21281 if !matches!(self.peek(), Token::RParen) {
21282 return Err(self.err(alloc::format!(
21283 "expected ')' after ANY/ALL subquery, got {:?}",
21284 self.peek()
21285 )));
21286 }
21287 self.advance();
21288 if sub.items.len() != 1 {
21289 return Err(self.err(alloc::format!(
21290 "ANY/ALL subquery must return one column, got {}",
21291 sub.items.len()
21292 )));
21293 }
21294 if is_any && matches!(op, BinOp::Eq) {
21295 return Ok(Expr::InSubquery {
21296 expr: Box::new(lhs),
21297 subquery: Box::new(sub),
21298 negated: false,
21299 });
21300 }
21301 // The engine's subquery resolvers materialise
21302 // the single-column result into an ARRAY the
21303 // existing AnyAll three-valued eval consumes.
21304 return Ok(Expr::AnyAll {
21305 expr: Box::new(lhs),
21306 op,
21307 array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
21308 is_any,
21309 });
21310 }
21311 let arr = self.parse_expr(0)?;
21312 if !matches!(self.peek(), Token::RParen) {
21313 return Err(self.err(alloc::format!(
21314 "expected ')' after ANY/ALL argument, got {:?}",
21315 self.peek()
21316 )));
21317 }
21318 self.advance();
21319 Ok(Expr::AnyAll {
21320 expr: Box::new(lhs),
21321 op,
21322 array: Box::new(arr),
21323 is_any,
21324 })
21325 }
21326
21327 /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
21328 /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
21329 #[inline(never)]
21330 fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
21331 self.advance();
21332 let e = self.parse_expr(9)?;
21333 Ok(build_center_call(e))
21334 }
21335
21336 /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
21337 /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
21338 /// unary minus.
21339 ///
21340 /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
21341 /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
21342 /// the Expr-sized local stays out of that frame.
21343 #[inline(never)]
21344 fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21345 self.advance();
21346 let e = self.parse_expr(9)?;
21347 Ok(Expr::FunctionCall {
21348 name: alloc::string::String::from(name),
21349 args: alloc::vec![e],
21350 })
21351 }
21352
21353 /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
21354 /// (horizontal). Out-of-line from `parse_unary` (frame budget).
21355 #[inline(never)]
21356 fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
21357 self.advance();
21358 let e = self.parse_expr(9)?;
21359 Ok(Expr::FunctionCall {
21360 name: alloc::string::String::from(if vertical {
21361 "isvertical"
21362 } else {
21363 "ishorizontal"
21364 }),
21365 args: alloc::vec![e],
21366 })
21367 }
21368
21369 /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
21370 /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
21371 /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
21372 #[inline(never)]
21373 fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
21374 self.advance();
21375 let e = self.parse_expr(9)?;
21376 Ok(Expr::Cast {
21377 expr: Box::new(e),
21378 target: CastTarget::Named("binary".to_string()),
21379 })
21380 }
21381
21382 /// The prefix operators that share one shape: take the token, parse
21383 /// an operand at `prec`, wrap it.
21384 ///
21385 /// `#[inline(never)]`, and one function instead of five arms, for the
21386 /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
21387 /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
21388 /// debug build gives EVERY arm's locals a slot in the frame, whichever
21389 /// arm runs. `((((1))))` reaches none of these arms and was carrying
21390 /// five `Expr`-sized locals per level for them anyway.
21391 #[inline(never)]
21392 fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
21393 self.advance();
21394 let e = self.parse_expr(prec)?;
21395 Ok(Expr::Unary {
21396 op,
21397 expr: Box::new(e),
21398 })
21399 }
21400
21401 /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
21402 /// and separate from it because of the literal folding below and the
21403 /// `format!` temporaries that folding needs.
21404 #[inline(never)]
21405 fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
21406 self.advance();
21407 // v7.39 (round 549) — fold the sign into an integer literal that
21408 // only fits once it is negative.
21409 //
21410 // `9223372036854775808` is one past i64::MAX, so the lexer hands
21411 // it over as a NUMERIC and `-` on a numeric stays numeric. PG
21412 // folds the sign first, so `-9223372036854775808` is a bigint
21413 // there — and `-9223372036854775808 - 1` raises "bigint out of
21414 // range" where SPG quietly answered -9223372036854775809, a value
21415 // no bigint can hold. The arithmetic itself was already checked;
21416 // only the literal's type was wrong.
21417 if let Token::Numeric(lit) = self.peek()
21418 && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
21419 {
21420 self.advance();
21421 return Ok(Expr::Literal(Literal::Integer(folded)));
21422 }
21423 // Unary minus binds tighter than `*`/`/` (now at prec 7 after
21424 // `<->` slotted into 5 and arithmetic shifted up).
21425 let e = self.parse_expr(9)?;
21426 Ok(Expr::Unary {
21427 op: UnOp::Neg,
21428 expr: Box::new(e),
21429 })
21430 }
21431
21432 /// tsquery `!!` prefix negation, lowered to the catalog function.
21433 /// Binds like unary minus. Out-of-line for the frame reason on
21434 /// `parse_unary_op`.
21435 #[inline(never)]
21436 fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
21437 self.advance();
21438 let e = self.parse_expr(9)?;
21439 Ok(Expr::FunctionCall {
21440 name: String::from("tsquery_not"),
21441 args: alloc::vec![e],
21442 })
21443 }
21444
21445 fn parse_unary(&mut self) -> Result<Expr, ParseError> {
21446 match self.peek() {
21447 // NOT binds tighter than AND / XOR / OR but looser than
21448 // comparisons — its operand takes everything ≥ the comparison
21449 // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
21450 // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
21451 // was rung 3, behaviour-identical when 3 was unused; AND now
21452 // occupies 3, so this must be 4 to keep NOT tighter than AND.)
21453 Token::Not => self.parse_unary_op(UnOp::Not, 4),
21454 // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
21455 // The body is out-of-line: `parse_unary` is one of the three
21456 // frames the parser's MAX_NEST_DEPTH is tuned against, and an
21457 // inline arm here overflowed the native stack in
21458 // `nesting_budget_errors_cleanly` — the guard test caught it,
21459 // exactly as the eval-side cliff did in rounds 346 and 351.
21460 Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
21461 self.parse_binary_prefix()
21462 }
21463 // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
21464 // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
21465 // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
21466 Token::Bang => self.parse_unary_op(UnOp::Not, 9),
21467 Token::Minus => self.parse_prefix_minus(),
21468 // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
21469 // worked only because the lexer reads it as one signed literal;
21470 // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
21471 // PG18 and MariaDB take all of them. Binds like unary minus.
21472 Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
21473 // Bitwise NOT binds like unary minus.
21474 Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
21475 // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
21476 // "center of" operator; desugars to center(x). The whole arm
21477 // is out-of-line: parse_unary sits on the per-nesting-level
21478 // frame chain that MAX_NEST_DEPTH is tuned against, so no
21479 // Expr-sized local may live in this frame.
21480 Token::TsMatch => self.parse_prefix_center(),
21481 // v7.39 (round 508) — the prefix operators that are named
21482 // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
21483 // is length. Out-of-line for the same nesting-frame reason as
21484 // parse_prefix_center — parse_unary sits on the recursive cycle
21485 // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
21486 // live in this frame.
21487 Token::At => self.parse_prefix_call("abs"),
21488 Token::Hash => self.parse_prefix_call("npoints"),
21489 Token::AtMinusAt => self.parse_prefix_call("length"),
21490 // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
21491 // "is horizontal" (lseg / line); desugars to the existing
21492 // isvertical()/ishorizontal() functions. Out-of-line for the
21493 // same nesting-frame reason as parse_prefix_center.
21494 Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
21495 Token::GeomHoriz => self.parse_prefix_geom_axis(false),
21496 Token::DoubleBang => self.parse_prefix_tsquery_not(),
21497 _ => self.parse_atom(),
21498 }
21499 }
21500
21501 /// Parse a parenthesised scalar subquery body after the caller has consumed
21502 /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
21503 /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
21504 /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
21505 /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
21506 /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
21507 /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
21508 /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
21509 /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
21510 /// which sits on the recursive nesting-budget cycle (a few extra bytes there
21511 /// tips the deep-nesting test into a stack overflow).
21512 #[inline(never)]
21513 fn array_subquery_ahead(&self) -> bool {
21514 if !matches!(self.peek(), Token::LParen) {
21515 return false;
21516 }
21517 matches!(
21518 self.tokens.get(self.pos + 1),
21519 Some(Token::Select | Token::Values)
21520 ) || matches!(
21521 self.tokens.get(self.pos + 1),
21522 Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
21523 )
21524 }
21525
21526 /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
21527 /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
21528 /// locals stay off parse_atom's recursive frame (round 105).
21529 #[inline(never)]
21530 fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
21531 self.advance(); // consume `[`
21532 let mut items: Vec<Expr> = Vec::new();
21533 if !matches!(self.peek(), Token::RBracket) {
21534 loop {
21535 // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
21536 // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
21537 if matches!(self.peek(), Token::LBracket) {
21538 items.push(self.parse_array_bracket_body()?);
21539 } else {
21540 items.push(self.parse_expr(0)?);
21541 }
21542 match self.peek() {
21543 Token::Comma => {
21544 self.advance();
21545 }
21546 Token::RBracket => break,
21547 other => {
21548 return Err(self.err(alloc::format!(
21549 "expected ',' or ']' in ARRAY literal, got {other:?}"
21550 )));
21551 }
21552 }
21553 }
21554 }
21555 self.advance(); // consume `]`
21556 Ok(Expr::Array(items))
21557 }
21558
21559 /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
21560 /// is already consumed; the current token is `(`. Desugars to a scalar
21561 /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
21562 /// the subquery's single-column rows in order — reusing the existing
21563 /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
21564 /// keeps the large `Statement` local off parse_atom's recursive frame.
21565 #[inline(never)]
21566 fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
21567 self.advance(); // consume `(`
21568 let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
21569 if w.eq_ignore_ascii_case("with"));
21570 let sub = if is_with {
21571 self.advance(); // WITH
21572 self.parse_with_cte_then_select()?
21573 } else {
21574 self.parse_select_stmt()?
21575 };
21576 if !matches!(self.peek(), Token::RParen) {
21577 return Err(self.err(alloc::format!(
21578 "expected ')' to close ARRAY(subquery), got {:?}",
21579 self.peek()
21580 )));
21581 }
21582 self.advance(); // consume `)`
21583 // Reuse the parser to build the array_agg wrapper from the subquery's
21584 // canonical text — avoids hand-constructing the derived-table AST.
21585 let wrapper = alloc::format!(
21586 "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
21587 );
21588 let stmt = parse_statement(&wrapper)
21589 .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
21590 let Statement::Select(sel) = stmt else {
21591 return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
21592 };
21593 Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
21594 }
21595
21596 #[inline(never)]
21597 fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
21598 let inner = if is_with {
21599 self.advance(); // WITH
21600 self.parse_with_cte_then_select()?
21601 } else {
21602 self.parse_select_stmt()?
21603 };
21604 match self.advance() {
21605 Token::RParen => {
21606 let Statement::Select(s) = inner else {
21607 return Err(ParseError {
21608 message: "scalar subquery body must be a SELECT".into(),
21609 token_pos: self.consumed_pos(),
21610 });
21611 };
21612 Ok(Expr::ScalarSubquery(Box::new(s)))
21613 }
21614 other => Err(ParseError {
21615 message: format!("expected ')' after scalar subquery, got {other:?}"),
21616 token_pos: self.consumed_pos(),
21617 }),
21618 }
21619 }
21620
21621 /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
21622 /// literals. The lexer splits them into an ident + string; recombine
21623 /// here. Out-of-line and returning `Option` so `parse_atom` — the
21624 /// recursive frame the 768 KiB stack budget is tuned against — pays no
21625 /// frame for the `body` / `bits` strings and their char loops (the
21626 /// round-367 frame cliff, M20).
21627 #[inline(never)]
21628 fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
21629 let is_hex = match self.peek() {
21630 Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
21631 Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
21632 _ => return None,
21633 };
21634 if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
21635 return None;
21636 }
21637 self.advance();
21638 let Token::String(body) = self.advance() else {
21639 unreachable!("guarded above");
21640 };
21641 // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
21642 // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
21643 // (hex pairs, even count required — MariaDB errors on an odd
21644 // count); `b'1010'` packs its bits big-endian, left-padded to a
21645 // byte. Lower both onto the bytea cast.
21646 if self.mysql_dialect {
21647 if is_hex {
21648 if body.len() % 2 == 1 {
21649 return Some(Err(self.err(alloc::format!(
21650 "invalid hex string literal X'{body}': odd digit count"
21651 ))));
21652 }
21653 for c in body.chars() {
21654 if !c.is_ascii_hexdigit() {
21655 return Some(Err(
21656 self.err(alloc::format!("invalid hexadecimal digit {c:?} in X'…'"))
21657 ));
21658 }
21659 }
21660 return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
21661 }
21662 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21663 return Some(Err(
21664 self.err(alloc::format!("invalid binary digit {bad:?} in b'…'"))
21665 ));
21666 }
21667 return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
21668 }
21669 let bits = if is_hex {
21670 let mut out = String::with_capacity(body.len() * 4);
21671 for c in body.chars() {
21672 let Some(d) = c.to_digit(16) else {
21673 return Some(Err(self.err(alloc::format!(
21674 "invalid hexadecimal digit {c:?} in X'…' bit string"
21675 ))));
21676 };
21677 out.push_str(&alloc::format!("{d:04b}"));
21678 }
21679 out
21680 } else {
21681 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21682 return Some(Err(self.err(alloc::format!(
21683 "invalid binary digit {bad:?} in B'…' bit string"
21684 ))));
21685 }
21686 body
21687 };
21688 // Route through the postfix-cast loop so a chained cast like
21689 // `B'1010'::int` attaches onto the implicit `::bit` cast instead
21690 // of erroring at the `::`.
21691 // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
21692 // literal keeps its exact length, while an explicit `::bit` cast is
21693 // bit(1) with pad/truncate semantics (PG).
21694 Some(self.finish_postfix_casts(Expr::Cast {
21695 expr: Box::new(Expr::Literal(Literal::String(bits))),
21696 target: CastTarget::Named("__bit_literal".to_string()),
21697 }))
21698 }
21699
21700 fn parse_atom(&mut self) -> Result<Expr, ParseError> {
21701 if let Some(res) = self.try_parse_bit_string_literal() {
21702 return res;
21703 }
21704 let tok_pos = self.pos;
21705 match self.advance() {
21706 Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
21707 Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
21708 // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
21709 // carrying the source mantissa + scale so no precision is lost. A
21710 // literal too wide for i128 falls back to double precision.
21711 // Out-of-line (#[inline(never)]) — this arm sits on the
21712 // parse_expr recursion chain; its expansion locals must not
21713 // widen the recursive frame (debug frame-cliff discipline).
21714 Token::Numeric(s) => match numeric_token_to_literal(s) {
21715 Ok(lit) => Ok(Expr::Literal(lit)),
21716 Err(msg) => Err(self.err(msg)),
21717 },
21718 Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
21719 // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
21720 // (the lexer only emits this token in the MySQL dialect). Lower
21721 // onto the existing bytea cast; out-of-line to keep this arm off
21722 // the parse recursion frame.
21723 Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
21724 Token::True => Ok(Expr::Literal(Literal::Bool(true))),
21725 Token::False => Ok(Expr::Literal(Literal::Bool(false))),
21726 Token::Null => Ok(Expr::Literal(Literal::Null)),
21727 // v6.1.1 — `$N` placeholder. The actual Value lookup
21728 // happens in the engine eval path against the prepared-
21729 // statement bind buffer.
21730 Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
21731 Token::LParen => {
21732 // v4.10: `(SELECT ...)` in expression position is a
21733 // scalar subquery; otherwise it's a parenthesised
21734 // expression. Peek for SELECT keyword to dispatch.
21735 // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
21736 // lexes as Ident("with") (not a reserved token). The subquery body
21737 // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
21738 // so its large `Statement` local stays out of parse_atom's stack
21739 // frame — parse_atom is on the recursive `((…))` cycle and the
21740 // nesting budget is tuned to its frame size).
21741 let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21742 if s.eq_ignore_ascii_case("with"));
21743 if matches!(self.peek(), Token::Select) || is_with {
21744 self.parse_paren_scalar_subquery(is_with)
21745 } else {
21746 let e = self.parse_expr(0)?;
21747 // `(a, b, …)` — a row constructor. Valid only
21748 // in front of a comparison operator or [NOT]
21749 // IN; both expand at parse time (lexicographic
21750 // comparison / OR'd row equalities).
21751 if matches!(self.peek(), Token::Comma) {
21752 let mut row = alloc::vec![e];
21753 while matches!(self.peek(), Token::Comma) {
21754 self.advance();
21755 row.push(self.parse_expr(0)?);
21756 }
21757 if !matches!(self.peek(), Token::RParen) {
21758 return Err(self.err(alloc::format!(
21759 "expected ')' after row constructor, got {:?}",
21760 self.peek()
21761 )));
21762 }
21763 self.advance();
21764 // A bare `(a, b, …)` row constructor can carry postfix
21765 // (`::text`, `.field`) just like `ROW(a, b, …)`; the
21766 // early return here skips parse_atom's tail postfix
21767 // pass, so fold casts in explicitly. For the
21768 // comparison / predicate forms nothing postfix follows,
21769 // so this is a no-op.
21770 return self
21771 .parse_row_comparison_tail(row)
21772 .and_then(|e| self.finish_postfix_casts(e));
21773 }
21774 match self.advance() {
21775 Token::RParen => Ok(e),
21776 other => Err(ParseError {
21777 message: format!("expected ')', got {other:?}"),
21778 token_pos: self.consumed_pos(),
21779 }),
21780 }
21781 }
21782 }
21783 Token::LBracket => self.parse_vector_literal_body(),
21784 Token::Extract => self.parse_extract_atom(),
21785 Token::Interval => self.parse_interval_atom(),
21786 // `LEFT` / `RIGHT` are reserved-keyword tokens because the
21787 // grammar dedicates arms for `LEFT [OUTER] JOIN` /
21788 // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
21789 // expression position calling the PG `left(string, n)` /
21790 // `right(string, n)` function; rebuild the AST as a regular
21791 // function call so the engine's apply_function dispatch picks
21792 // it up. Delegated to a #[inline(never)] helper so its locals
21793 // don't bloat this recursive `parse_atom` frame (the nesting
21794 // budget in `enter_nested` is tuned to parse_atom's size).
21795 Token::Left if matches!(self.peek(), Token::LParen) => {
21796 self.parse_lr_string_function_call("left")
21797 }
21798 Token::Right if matches!(self.peek(), Token::LParen) => {
21799 self.parse_lr_string_function_call("right")
21800 }
21801 // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
21802 // token; we match on the bare ident. NOT is a token
21803 // (consumed in the comparison rung), but `EXISTS (...)`
21804 // at the top of an expression starts here.
21805 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
21806 self.parse_exists_atom(false)
21807 }
21808 // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
21809 // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
21810 // CASE is a bare ident; we dispatch on lowercase match.
21811 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
21812 self.parse_case_atom()
21813 }
21814 // v7.37.17 (17.6 siblings) — PG typed datetime literals:
21815 // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
21816 // '…'`. Lower onto the ::cast node so the existing
21817 // runtime text→date/timestamp paths do the parsing. The
21818 // string must follow immediately, else the ident stays a
21819 // plain column reference.
21820 Token::Ident(s)
21821 if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
21822 && matches!(self.peek(), Token::String(_)) =>
21823 {
21824 let target =
21825 typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
21826 let Token::String(lit) = self.advance() else {
21827 unreachable!("peek guaranteed a string token");
21828 };
21829 Ok(Expr::Cast {
21830 expr: Box::new(Expr::Literal(Literal::String(lit))),
21831 target,
21832 })
21833 }
21834 // v7.39 (round 221) — the SQL-standard long spellings:
21835 // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
21836 // TIME ZONE '…'`. Consume the modifier and lower to the same
21837 // typed-literal cast (`timetz` / `timestamptz` for WITH).
21838 Token::Ident(s)
21839 if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
21840 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
21841 || w.eq_ignore_ascii_case("without"))
21842 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
21843 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
21844 && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
21845 {
21846 let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
21847 self.advance(); // WITH / WITHOUT
21848 self.advance(); // TIME
21849 self.advance(); // ZONE
21850 let Token::String(lit) = self.advance() else {
21851 unreachable!("guard checked a string token");
21852 };
21853 let base = s.to_ascii_lowercase();
21854 let target = match (base.as_str(), with_tz) {
21855 ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
21856 ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
21857 (_, true) => CastTarget::Timestamptz,
21858 (_, false) => CastTarget::Timestamp,
21859 };
21860 Ok(Expr::Cast {
21861 expr: Box::new(Expr::Literal(Literal::String(lit))),
21862 target,
21863 })
21864 }
21865 // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
21866 // gathers the subquery's single-column rows (in its row order)
21867 // into an array. Desugared to `array_agg` over the subquery as a
21868 // derived table; out-of-line to keep parse_atom's frame small (it
21869 // sits on the recursive nesting-budget cycle).
21870 Token::Ident(s) | Token::QuotedIdent(s)
21871 if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
21872 {
21873 self.parse_array_subquery()
21874 }
21875 // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
21876 // is not a reserved token; we match by case-insensitive
21877 // ident. The opening `[` must follow immediately. v7.39 (read01
21878 // round 105) — the body moved out-of-line so its `Vec`/loop locals
21879 // leave parse_atom's frame (which sits on the nesting-budget cycle).
21880 Token::Ident(s) | Token::QuotedIdent(s)
21881 if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
21882 {
21883 self.parse_array_literal_body()
21884 }
21885 // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
21886 // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
21887 // We special-case before the generic ident dispatch so
21888 // the AGAINST clause never reaches the function-call
21889 // loop (which would mis-read `(cols) AGAINST` as a
21890 // call with no trailing modifier). The shape is
21891 // rewritten to a Boolean OR over per-column
21892 // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
21893 // term)` so the existing FTS evaluator handles
21894 // semantics — the fulltext-GIN built at CREATE TABLE
21895 // time is currently a "real index that survives dump
21896 // round-trip"; the planner hook that actually uses
21897 // it for posting-list intersection lands in a later
21898 // sub-phase (Phase 2.2b) without touching this surface.
21899 Token::Ident(s) | Token::QuotedIdent(s)
21900 if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
21901 {
21902 self.parse_match_against_atom()
21903 }
21904 Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
21905 // v7.37.43-T4 — PG-unreserved keywords are legal column /
21906 // alias names in expression context too. `release` appears
21907 // in sentori `0003_partition_events.sql` as both a column
21908 // reference (SELECT … release …) and an INSERT column list
21909 // entry. Mirrors `expect_ident_like`'s expansion of the
21910 // identifier set.
21911 other if unreserved_keyword_text(&other).is_some() => {
21912 let s = unreserved_keyword_text(&other).unwrap();
21913 self.finish_ident_atom(s)
21914 }
21915 // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
21916 // only inside `SET` before, so `SELECT @@autocommit` — which
21917 // every MySQL connector asks at handshake — was a parse error.
21918 // MariaDB accepts the bare, `@@session.` and `@@global.`
21919 // spellings alike and answers from the session's own value.
21920 Token::SessionVar(v) => {
21921 // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
21922 // has nothing to do with a `@@` engine setting: its own
21923 // per-session namespace, and an unset one reads NULL instead
21924 // of raising. Stripping every `@` (as this did) made `@x` and
21925 // `@@x` the same node, so `SELECT @x` answered "Unknown
21926 // system variable".
21927 Ok(variable_ref_atom(&v))
21928 }
21929 other => Err(ParseError {
21930 message: format!("unexpected token {other:?} in expression"),
21931 token_pos: tok_pos,
21932 }),
21933 }
21934 // After parsing the atom, fold any postfix `::vector` casts.
21935 .and_then(|atom| self.finish_postfix_casts(atom))
21936 }
21937
21938 /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
21939 /// Both bind tighter than any binary op.
21940 /// Shared cast-target parser for postfix `::TYPE` and the
21941 /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
21942 /// If the next tokens are `( N )`, consume them and return the canonical
21943 /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
21944 /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
21945 fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
21946 if !matches!(self.peek(), Token::LParen) {
21947 return None;
21948 }
21949 self.advance(); // (
21950 let n = match self.advance() {
21951 Token::Integer(n) => n,
21952 _ => return Some(base.to_string()), // malformed → drop precision
21953 };
21954 if matches!(self.peek(), Token::RParen) {
21955 self.advance();
21956 }
21957 Some(alloc::format!("{base}({n})"))
21958 }
21959
21960 fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
21961 let target = match self.advance() {
21962 Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
21963 "int" | "integer" | "int4" => {
21964 if matches!(self.peek(), Token::LBracket)
21965 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
21966 {
21967 self.advance();
21968 self.advance();
21969 CastTarget::IntArray
21970 } else {
21971 CastTarget::Int
21972 }
21973 }
21974 "bigint" | "int8" => {
21975 if matches!(self.peek(), Token::LBracket)
21976 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
21977 {
21978 self.advance();
21979 self.advance();
21980 CastTarget::BigIntArray
21981 } else {
21982 CastTarget::BigInt
21983 }
21984 }
21985 "float" | "double" => CastTarget::Float,
21986 "text" => {
21987 // v7.10.11 — `::TEXT[]` widens to TextArray.
21988 if matches!(self.peek(), Token::LBracket)
21989 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
21990 {
21991 self.advance();
21992 self.advance();
21993 CastTarget::TextArray
21994 } else {
21995 CastTarget::Text
21996 }
21997 }
21998 "bool" | "boolean" => CastTarget::Bool,
21999 "vector" => CastTarget::Vector,
22000 "date" => CastTarget::Date,
22001 // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22002 // seconds precision through the Named path (the engine rounds
22003 // the sub-second field); bare `::timestamp` keeps the fast arm.
22004 "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22005 Some(named) => CastTarget::Named(named),
22006 None => CastTarget::Timestamp,
22007 },
22008 "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22009 Some(named) => CastTarget::Named(named),
22010 None => CastTarget::Timestamptz,
22011 },
22012 "interval" => CastTarget::Interval,
22013 "json" => CastTarget::Json,
22014 "jsonb" => CastTarget::Jsonb,
22015 // v7.39 (round 694) — these have dedicated CastTarget
22016 // variants, so they never reached the postfix `[]` handling
22017 // further down and `::regtype[]` was a SYNTAX error at the
22018 // `]`. PG has an array type for every scalar; take the
22019 // suffix here and hand the canonical `<ty>_array` name to
22020 // the engine, the same shape every other array cast uses.
22021 "regtype" if self.peek_postfix_array_brackets() => {
22022 self.advance();
22023 self.advance();
22024 CastTarget::Named(alloc::string::String::from("regtype_array"))
22025 }
22026 "regclass" if self.peek_postfix_array_brackets() => {
22027 self.advance();
22028 self.advance();
22029 CastTarget::Named(alloc::string::String::from("regclass_array"))
22030 }
22031 "regtype" => CastTarget::RegType,
22032 "regclass" => CastTarget::RegClass,
22033 // v7.12.0 — `::tsvector` / `::tsquery`.
22034 // Engine decodes the LHS text via the PG
22035 // external form parser.
22036 // v7.39 (round 352, M8) — MySQL's own cast targets.
22037 // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22038 // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22039 // such type, so they are taken only in that dialect and
22040 // fall through to the "type does not exist" arm otherwise.
22041 "signed" | "unsigned" if self.mysql_dialect => {
22042 if matches!(self.peek(), Token::Ident(k)
22043 if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22044 {
22045 self.advance();
22046 }
22047 CastTarget::Named(s.to_ascii_lowercase())
22048 }
22049 // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22050 // in MySQL: MariaDB answers '123' where the SQL-standard
22051 // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22052 // Truncating a number to its first digit is a wrong answer
22053 // with no error, so the MySQL session gets MySQL's reading.
22054 "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22055 CastTarget::Text
22056 }
22057 "tsvector" => CastTarget::TsVector,
22058 "tsquery" => CastTarget::TsQuery,
22059 // v7.17.0 — `::uuid`. Engine decodes the LHS
22060 // text via `spg_storage::parse_uuid_str`.
22061 "uuid" => CastTarget::Uuid,
22062 // v7.18 — `::bytea`. Engine decodes the LHS
22063 // text via the PG hex form (`'\xdeadbeef'`)
22064 // or escape form (`'\\x05\\x00'`). Closes
22065 // mailrs D-pre #3 reverse-acceptance gap.
22066 "bytea" => CastTarget::Bytea,
22067 // v7.37.5 ship triage — generic typed-cast escape.
22068 // Anything the long-tail PG type ident table knows
22069 // about(network/bit/geometry/multirange/etc.)flows
22070 // through `CastTarget::Named(canonical)`; the engine
22071 // resolves via `column_type_to_data_type` and dispatches
22072 // through the typed `coerce_value` path. Truly
22073 // unrecognised idents still hit the error arm below
22074 // because the engine rejects them.
22075 other => {
22076 // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22077 // `::varchar(255)`, etc. Capture into the canonical
22078 // `name(p,s)` form so `type_name_to_data_type` can
22079 // reconstruct the `DataType::Numeric { precision,
22080 // scale }` (and similar param-carrying types).
22081 let mut name = other.to_string();
22082 // v7.39 (round 281) — `::bit varying(3)` is two
22083 // words; fold the tail in so the typmod reaches the
22084 // type resolver instead of tripping the parser.
22085 if name.eq_ignore_ascii_case("bit")
22086 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22087 {
22088 self.advance();
22089 name = alloc::string::String::from("varbit");
22090 }
22091 // v7.39 (round 613) — `::character varying` is the same
22092 // two-word shape and had no fold, so the `varying` was
22093 // left behind and the cast became a bare `character`,
22094 // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22095 // `a` where PG answers `ab`. Silently, and for a spelling
22096 // pg_dump writes.
22097 if name.eq_ignore_ascii_case("character")
22098 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22099 {
22100 self.advance();
22101 name = alloc::string::String::from("varchar");
22102 }
22103 if matches!(self.peek(), Token::LParen) {
22104 let mut buf = alloc::string::String::from("(");
22105 let mut depth = 0usize;
22106 loop {
22107 match self.advance() {
22108 Token::LParen => {
22109 depth += 1;
22110 if depth > 1 {
22111 buf.push('(');
22112 }
22113 }
22114 Token::RParen => {
22115 depth -= 1;
22116 if depth == 0 {
22117 buf.push(')');
22118 break;
22119 }
22120 buf.push(')');
22121 }
22122 Token::Comma => buf.push(','),
22123 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22124 // v7.39 (round 273) — a minus used to fall
22125 // into the catch-all below and vanish, so
22126 // `::numeric(10,-2)` reached the engine as
22127 // the text `numeric(10,2)` and silently
22128 // rounded to two DECIMALS instead of to
22129 // hundreds. A dropped token is not a
22130 // no-op when it carries a sign.
22131 Token::Minus => buf.push('-'),
22132 Token::Eof => break,
22133 _ => {}
22134 }
22135 }
22136 name.push_str(&buf);
22137 }
22138 // Optional postfix `[]` widens to the array form —
22139 // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22140 // The engine's `type_name_to_data_type` recognises
22141 // the canonical `<ty>_array` form.
22142 if matches!(self.peek(), Token::LBracket)
22143 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22144 {
22145 self.advance();
22146 self.advance();
22147 name.push_str("_array");
22148 }
22149 CastTarget::Named(name)
22150 }
22151 },
22152 Token::Interval => CastTarget::Interval,
22153 // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22154 // "char" (oid 18, SPG Char1 — distinct from bare `char`
22155 // = char(1)); other quoted names resolve like idents.
22156 Token::QuotedIdent(q) => {
22157 if q.eq_ignore_ascii_case("char") {
22158 CastTarget::Named("char1".into())
22159 } else {
22160 CastTarget::Named(q.to_ascii_lowercase())
22161 }
22162 }
22163 other => {
22164 return Err(ParseError {
22165 message: format!("expected type ident after `::`, got {other:?}"),
22166 token_pos: self.consumed_pos(),
22167 });
22168 }
22169 };
22170 // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22171 // target to its array sibling. Closed-enum arms (Bool /
22172 // SmallInt / Numeric / Float / Date / …) didn't carry the
22173 // explicit widening that Text / Int / BigInt did, so
22174 // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22175 // error. The widening here mirrors the per-arm Text /
22176 // Int / BigInt logic above + folds the new ζ-A first-class
22177 // types through `CastTarget::Named("<ty>_array")`.
22178 if matches!(self.peek(), Token::LBracket)
22179 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22180 {
22181 let widened = match &target {
22182 CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22183 CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22184 // v7.39 (round 326, V43) — the two temporal types stay
22185 // distinct. Both used to widen to `timestamptz_array`, so
22186 // `::timestamp[]` named the wrong target in its own error
22187 // message and lost the zone-less identity on the way.
22188 CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22189 CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22190 CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22191 CastTarget::Json | CastTarget::Jsonb => {
22192 Some(CastTarget::Named("jsonb_array".to_string()))
22193 }
22194 CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22195 CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22196 CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22197 CastTarget::Named(name) => {
22198 let mut a = name.clone();
22199 a.push_str("_array");
22200 Some(CastTarget::Named(a))
22201 }
22202 // Int / BigInt / Text / Vector / TsVector / TsQuery /
22203 // RegType / RegClass / TextArray / IntArray /
22204 // BigIntArray already finalised — leave as is.
22205 _ => None,
22206 };
22207 if let Some(w) = widened {
22208 self.advance();
22209 self.advance();
22210 return Ok(w);
22211 }
22212 }
22213 Ok(target)
22214 }
22215
22216 fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
22217 loop {
22218 // v7.38 (read01, T9) — composite field access `(expr).field`.
22219 // A bare `a.b` is consumed as a qualified column inside the ident
22220 // atom, so a Dot only survives to this postfix position when the
22221 // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
22222 // `.*` whole-row expansion is not handled here (projection-level).
22223 if matches!(self.peek(), Token::Dot)
22224 && matches!(
22225 self.tokens.get(self.pos + 1),
22226 Some(Token::Ident(_) | Token::QuotedIdent(_))
22227 )
22228 {
22229 self.advance(); // .
22230 let field = match self.advance() {
22231 Token::Ident(s) | Token::QuotedIdent(s) => s,
22232 other => {
22233 return Err(
22234 self.err(format!("expected a field name after '.', got {other:?}"))
22235 );
22236 }
22237 };
22238 expr = Expr::FieldAccess {
22239 base: Box::new(expr),
22240 field,
22241 };
22242 continue;
22243 }
22244 if matches!(self.peek(), Token::DoubleColon) {
22245 self.advance();
22246 // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
22247 // target set to include INTERVAL (reserved Token),
22248 // TIMESTAMPTZ, and PG catalog regtype / regclass.
22249 // mailrs follow-up H3a + H3b.
22250 let target = self.parse_cast_target()?;
22251 expr = Expr::Cast {
22252 expr: Box::new(expr),
22253 target,
22254 };
22255 continue;
22256 }
22257 // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
22258 // returns NULL for out-of-range. Multiple subscripts
22259 // chain: `a[i][j]` parses left-to-right.
22260 if matches!(self.peek(), Token::LBracket) {
22261 self.advance();
22262 // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
22263 // bare index stays a subscript.
22264 let lo = if matches!(self.peek(), Token::Colon) {
22265 None
22266 } else {
22267 Some(self.parse_expr(0)?)
22268 };
22269 if matches!(self.peek(), Token::Colon) {
22270 self.advance();
22271 let hi = if matches!(self.peek(), Token::RBracket) {
22272 None
22273 } else {
22274 Some(Box::new(self.parse_expr(0)?))
22275 };
22276 if !matches!(self.peek(), Token::RBracket) {
22277 return Err(self.err(alloc::format!(
22278 "expected ']' after array slice, got {:?}",
22279 self.peek()
22280 )));
22281 }
22282 self.advance();
22283 expr = Expr::ArraySlice {
22284 target: Box::new(expr),
22285 lo: lo.map(Box::new),
22286 hi,
22287 };
22288 continue;
22289 }
22290 let index = lo.expect("non-colon branch parsed an index");
22291 if !matches!(self.peek(), Token::RBracket) {
22292 return Err(self.err(alloc::format!(
22293 "expected ']' after array index, got {:?}",
22294 self.peek()
22295 )));
22296 }
22297 self.advance();
22298 expr = Expr::ArraySubscript {
22299 target: Box::new(expr),
22300 index: Box::new(index),
22301 };
22302 continue;
22303 }
22304 // `expr AT TIME ZONE zone` — lowers to PG's own function
22305 // form timezone(zone, expr); the scalar implements the
22306 // offset shift (named zones error there — no tzdata).
22307 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
22308 && matches!(self.tokens.get(self.pos + 1),
22309 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
22310 && matches!(self.tokens.get(self.pos + 2),
22311 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
22312 {
22313 self.advance(); // AT
22314 self.advance(); // TIME
22315 self.advance(); // ZONE
22316 // Zone at comparison precedence so AND/OR stay out.
22317 let zone = self.parse_expr(6)?;
22318 expr = Expr::FunctionCall {
22319 name: "timezone".to_string(),
22320 args: alloc::vec![zone, expr],
22321 };
22322 continue;
22323 }
22324 // `expr COLLATE "name"` — SPG's single text ordering IS
22325 // byte order, i.e. the C collation. The byte-order
22326 // spellings absorb as no-ops; a locale collation would
22327 // silently sort differently from PG, so it errors
22328 // honestly instead.
22329 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
22330 self.advance();
22331 let mut cname = match self.advance() {
22332 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22333 other => {
22334 return Err(self.err(alloc::format!(
22335 "expected collation name after COLLATE, got {other:?}"
22336 )));
22337 }
22338 };
22339 // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
22340 // is how `pg_dump` writes the default one:
22341 // `… COLLATE pg_catalog.default`. Reading a single token
22342 // left the SCHEMA as the name, so the clause was refused
22343 // as an unsupported locale collation and no dump ran.
22344 if matches!(self.peek(), Token::Dot) {
22345 self.advance();
22346 cname = match self.advance() {
22347 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22348 // `default` lexes as a KEYWORD, and it is the name
22349 // pg_dump writes — the same trap round 535 hit with
22350 // TABLE / INDEX / FULL.
22351 Token::Default => alloc::string::String::from("default"),
22352 other => {
22353 return Err(self.err(alloc::format!(
22354 "expected collation name after COLLATE, got {other:?}"
22355 )));
22356 }
22357 };
22358 }
22359 let lc = cname.to_ascii_lowercase();
22360 // v7.39 (round 371, M4 P4b) — a per-expression MySQL
22361 // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
22362 // family / `binary`) forces byte-wise, which is exactly
22363 // what `BINARY expr` does — lower onto that so every fold
22364 // site (comparison, LIKE, ORDER BY) suppresses via
22365 // `is_binary_coerced`. A `_ci` family override folds, and
22366 // under the MySQL dialect the default already folds, so it
22367 // absorbs as a no-op; likewise the C / byte-order spellings.
22368 if self.mysql_dialect && (lc.ends_with("_bin") || lc == "binary") {
22369 expr = Expr::Cast {
22370 expr: alloc::boxed::Box::new(expr),
22371 target: CastTarget::Named("binary".to_string()),
22372 };
22373 continue;
22374 }
22375 let mysql_ci = self.mysql_dialect
22376 && (lc.ends_with("_ci")
22377 || matches!(lc.as_str(), "case_insensitive" | "nocase"));
22378 // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
22379 // goes to the lowering channel, the byte-order spellings
22380 // included. Round 691 recorded only the names the old
22381 // allow-list rejected, which left `ORDER BY a COLLATE "C"`
22382 // absorbed as a no-op — and once a column could declare a
22383 // collation, absorbing the clause meant the COLUMN's
22384 // collation won where the query had asked for bytes.
22385 if self.in_order_by_key && !mysql_ci {
22386 self.order_key_collation = Some(cname);
22387 continue;
22388 }
22389 if !matches!(
22390 lc.as_str(),
22391 "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
22392 ) && !mysql_ci
22393 {
22394 return Err(self.err(alloc::format!(
22395 "COLLATE {cname:?}: SPG orders text by bytes (the C \
22396 collation); locale collations are not supported yet — \
22397 use COLLATE \"C\" or drop the clause"
22398 )));
22399 }
22400 continue;
22401 }
22402 return Ok(expr);
22403 }
22404 }
22405
22406 /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
22407 /// the first token that is not one. Schema qualifiers collapse to the
22408 /// last part, which is what every other name path here does (SPG is
22409 /// single-schema).
22410 fn take_comma_separated_names(&mut self) -> Vec<String> {
22411 let mut out = Vec::new();
22412 while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
22413 self.advance();
22414 let mut last = n;
22415 while matches!(self.peek(), Token::Dot) {
22416 self.advance();
22417 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
22418 last = t;
22419 }
22420 }
22421 out.push(last);
22422 if matches!(self.peek(), Token::Comma) {
22423 self.advance();
22424 } else {
22425 break;
22426 }
22427 }
22428 out
22429 }
22430
22431 /// v7.39 (round 694) — is the next token pair a postfix `[]`?
22432 ///
22433 /// The general cast-target path tests this inline; the types with their
22434 /// own `CastTarget` variant need it as a guard on their match arm,
22435 /// which is what this exists for.
22436 fn peek_postfix_array_brackets(&self) -> bool {
22437 matches!(self.peek(), Token::LBracket)
22438 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22439 }
22440
22441 /// Parse the operator tail after a `(a, b, …)` row constructor
22442 /// and expand at parse time. `=` is the conjunction of element
22443 /// equalities; `<>` its negation; the order operators expand
22444 /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
22445 /// equalities. Anything else (a bare row value, a subquery
22446 /// RHS) errors honestly — SPG has no composite runtime value.
22447 fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
22448 fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
22449 let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
22450 lhs: Box::new(l.clone()),
22451 op: BinOp::Eq,
22452 rhs: Box::new(r.clone()),
22453 });
22454 let first = it.next().expect("row has at least two elements");
22455 it.fold(first, |acc, e| Expr::Binary {
22456 lhs: Box::new(acc),
22457 op: BinOp::And,
22458 rhs: Box::new(e),
22459 })
22460 }
22461 // Lexicographic (a,b) OP (c,d):
22462 // a STRICT c OR (a = c AND (b OP d)) — recursing right.
22463 fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
22464 if lhs.len() == 1 {
22465 return Expr::Binary {
22466 lhs: Box::new(lhs[0].clone()),
22467 op: last,
22468 rhs: Box::new(rhs[0].clone()),
22469 };
22470 }
22471 let head_strict = Expr::Binary {
22472 lhs: Box::new(lhs[0].clone()),
22473 op: strict,
22474 rhs: Box::new(rhs[0].clone()),
22475 };
22476 let head_eq = Expr::Binary {
22477 lhs: Box::new(lhs[0].clone()),
22478 op: BinOp::Eq,
22479 rhs: Box::new(rhs[0].clone()),
22480 };
22481 Expr::Binary {
22482 lhs: Box::new(head_strict),
22483 op: BinOp::Or,
22484 rhs: Box::new(Expr::Binary {
22485 lhs: Box::new(head_eq),
22486 op: BinOp::And,
22487 rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
22488 }),
22489 }
22490 }
22491 let negated_in = if matches!(self.peek(), Token::Not)
22492 && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
22493 {
22494 self.advance();
22495 true
22496 } else {
22497 false
22498 };
22499 if matches!(self.peek(), Token::In) {
22500 self.advance();
22501 if !matches!(self.peek(), Token::LParen) {
22502 return Err(self.err(alloc::format!(
22503 "expected '(' after row IN, got {:?}",
22504 self.peek()
22505 )));
22506 }
22507 self.advance();
22508 // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
22509 // not a list of literal rows. Row-vs-list decomposes to
22510 // OR-of-AND above, but the subquery's rows are only known at
22511 // runtime, so keep it as a RowInSubquery node.
22512 if matches!(self.peek(), Token::Select) {
22513 let inner = self.parse_select_stmt()?;
22514 if !matches!(self.peek(), Token::RParen) {
22515 return Err(self.err(alloc::format!(
22516 "expected ')' after row IN-subquery, got {:?}",
22517 self.peek()
22518 )));
22519 }
22520 self.advance();
22521 let Statement::Select(s) = inner else {
22522 unreachable!("parse_select_stmt always returns Statement::Select")
22523 };
22524 return Ok(Expr::RowInSubquery {
22525 row,
22526 subquery: Box::new(s),
22527 negated: negated_in,
22528 });
22529 }
22530 let mut alternatives: Vec<Expr> = Vec::new();
22531 loop {
22532 // Optional ROW keyword before the paren row.
22533 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22534 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22535 {
22536 self.advance();
22537 }
22538 if !matches!(self.peek(), Token::LParen) {
22539 return Err(self.err(alloc::format!(
22540 "expected '(' to open a row inside IN, got {:?}",
22541 self.peek()
22542 )));
22543 }
22544 self.advance();
22545 let mut rhs = alloc::vec![self.parse_expr(0)?];
22546 while matches!(self.peek(), Token::Comma) {
22547 self.advance();
22548 rhs.push(self.parse_expr(0)?);
22549 }
22550 if !matches!(self.peek(), Token::RParen) {
22551 return Err(self.err(alloc::format!(
22552 "expected ')' after row inside IN, got {:?}",
22553 self.peek()
22554 )));
22555 }
22556 self.advance();
22557 if rhs.len() != row.len() {
22558 return Err(self.err(alloc::format!(
22559 "row IN arity mismatch: left has {}, right has {}",
22560 row.len(),
22561 rhs.len()
22562 )));
22563 }
22564 alternatives.push(row_eq(&row, &rhs));
22565 if matches!(self.peek(), Token::Comma) {
22566 self.advance();
22567 continue;
22568 }
22569 break;
22570 }
22571 if !matches!(self.peek(), Token::RParen) {
22572 return Err(self.err(alloc::format!(
22573 "expected ')' to close row IN list, got {:?}",
22574 self.peek()
22575 )));
22576 }
22577 self.advance();
22578 let mut it = alternatives.into_iter();
22579 let first = it.next().expect("IN list has at least one row");
22580 let combined = it.fold(first, |acc, e| Expr::Binary {
22581 lhs: Box::new(acc),
22582 op: BinOp::Or,
22583 rhs: Box::new(e),
22584 });
22585 return Ok(maybe_not(combined, negated_in));
22586 }
22587 // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
22588 // two periods share at least one time point. Each pair is
22589 // normalised with least/greatest (PG accepts the endpoints
22590 // in either order), then lowered to the standard
22591 // `start1 < end2 AND start2 < end1` form.
22592 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
22593 if row.len() != 2 {
22594 return Err(self.err(alloc::format!(
22595 "OVERLAPS needs (start, end) pairs; left side has {} elements",
22596 row.len()
22597 )));
22598 }
22599 self.advance();
22600 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22601 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22602 {
22603 self.advance();
22604 }
22605 if !matches!(self.peek(), Token::LParen) {
22606 return Err(self.err(alloc::format!(
22607 "expected '(' after OVERLAPS, got {:?}",
22608 self.peek()
22609 )));
22610 }
22611 self.advance();
22612 let r0 = self.parse_expr(0)?;
22613 if !matches!(self.peek(), Token::Comma) {
22614 return Err(self.err(alloc::format!(
22615 "OVERLAPS needs (start, end) on the right, got {:?}",
22616 self.peek()
22617 )));
22618 }
22619 self.advance();
22620 let r1 = self.parse_expr(0)?;
22621 if !matches!(self.peek(), Token::RParen) {
22622 return Err(self.err(alloc::format!(
22623 "expected ')' after OVERLAPS pair, got {:?}",
22624 self.peek()
22625 )));
22626 }
22627 self.advance();
22628 let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
22629 name: String::from(name),
22630 args: alloc::vec![a.clone(), b.clone()],
22631 };
22632 let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
22633 lhs: Box::new(lhs),
22634 op: BinOp::Lt,
22635 rhs: Box::new(rhs),
22636 };
22637 return Ok(Expr::Binary {
22638 lhs: Box::new(lt(
22639 pair_fn("least", &row[0], &row[1]),
22640 pair_fn("greatest", &r0, &r1),
22641 )),
22642 op: BinOp::And,
22643 rhs: Box::new(lt(
22644 pair_fn("least", &r0, &r1),
22645 pair_fn("greatest", &row[0], &row[1]),
22646 )),
22647 });
22648 }
22649 // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
22650 // PG, `IS NULL` is true only when EVERY field is NULL, and
22651 // `IS NOT NULL` is true only when every field is non-NULL — the
22652 // latter is NOT the negation of the former (a mixed row is
22653 // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
22654 // which reproduces exactly that all-fields semantics.
22655 if matches!(self.peek(), Token::Is) {
22656 self.advance();
22657 let negated = if matches!(self.peek(), Token::Not) {
22658 self.advance();
22659 true
22660 } else {
22661 false
22662 };
22663 if !matches!(self.peek(), Token::Null) {
22664 return Err(self.err(alloc::format!(
22665 "expected NULL after row IS [NOT], got {:?}",
22666 self.peek()
22667 )));
22668 }
22669 self.advance();
22670 let mut it = row.iter().map(|e| Expr::IsNull {
22671 expr: Box::new(e.clone()),
22672 negated,
22673 });
22674 let first = it.next().expect("row has at least two elements");
22675 return Ok(it.fold(first, |acc, e| Expr::Binary {
22676 lhs: Box::new(acc),
22677 op: BinOp::And,
22678 rhs: Box::new(e),
22679 }));
22680 }
22681 let op = match self.peek() {
22682 Token::Eq => BinOp::Eq,
22683 Token::NotEq => BinOp::NotEq,
22684 Token::Lt => BinOp::Lt,
22685 Token::LtEq => BinOp::LtEq,
22686 Token::Gt => BinOp::Gt,
22687 Token::GtEq => BinOp::GtEq,
22688 // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
22689 // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
22690 // constructor value, identical to the `ROW(a, b, …)` keyword form:
22691 // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
22692 // (`::text`, `.field`) applies at the caller just as it does for the
22693 // ROW(...) node. All the comparison / predicate forms returned above.
22694 _ => {
22695 return Ok(Expr::FunctionCall {
22696 name: String::from("row"),
22697 args: row,
22698 });
22699 }
22700 };
22701 self.advance();
22702 // Optional ROW keyword before the paren row.
22703 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22704 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22705 {
22706 self.advance();
22707 }
22708 if !matches!(self.peek(), Token::LParen) {
22709 return Err(self.err(alloc::format!(
22710 "expected '(' to open the right-hand row, got {:?}",
22711 self.peek()
22712 )));
22713 }
22714 self.advance();
22715 // `(a, b) <op> (SELECT x, y)` — compare against a single-row
22716 // subquery. Kept as a node (the subquery's row is a runtime value);
22717 // the literal-RHS form below still decomposes at parse time.
22718 if matches!(self.peek(), Token::Select) {
22719 let inner = self.parse_select_stmt()?;
22720 if !matches!(self.peek(), Token::RParen) {
22721 return Err(self.err(alloc::format!(
22722 "expected ')' after row comparison subquery, got {:?}",
22723 self.peek()
22724 )));
22725 }
22726 self.advance();
22727 let Statement::Select(s) = inner else {
22728 unreachable!("parse_select_stmt always returns Statement::Select")
22729 };
22730 return Ok(Expr::RowCmpSubquery {
22731 row,
22732 op,
22733 subquery: Box::new(s),
22734 });
22735 }
22736 let mut rhs = alloc::vec![self.parse_expr(0)?];
22737 while matches!(self.peek(), Token::Comma) {
22738 self.advance();
22739 rhs.push(self.parse_expr(0)?);
22740 }
22741 if !matches!(self.peek(), Token::RParen) {
22742 return Err(self.err(alloc::format!(
22743 "expected ')' after right-hand row, got {:?}",
22744 self.peek()
22745 )));
22746 }
22747 self.advance();
22748 if rhs.len() != row.len() {
22749 // v7.39 (round 239) — PG's wording (42601).
22750 return Err(self.err("unequal number of entries in row expressions".to_string()));
22751 }
22752 Ok(match op {
22753 BinOp::Eq => row_eq(&row, &rhs),
22754 BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
22755 BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
22756 BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
22757 BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
22758 BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
22759 _ => unreachable!("op restricted above"),
22760 })
22761 }
22762
22763 /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
22764 /// escape character becomes the matcher's default backslash:
22765 /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
22766 /// → the char itself, and any pre-existing backslash escapes
22767 /// itself so it stays literal. Both operands must be string
22768 /// literals — a runtime pattern would need matcher support.
22769 fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
22770 let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
22771 (&pattern, &esc)
22772 else {
22773 return Err(
22774 "LIKE ... ESCAPE requires string-literal pattern and escape \
22775 (runtime escape characters are not supported yet)"
22776 .into(),
22777 );
22778 };
22779 // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
22780 // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
22781 // multi-character escape is an error.
22782 let esc_ch: Option<char> = {
22783 let mut ch_iter = e.chars();
22784 match (ch_iter.next(), ch_iter.next()) {
22785 (Some(c), None) => Some(c),
22786 (None, _) => None,
22787 (Some(_), Some(_)) => {
22788 return Err(alloc::format!(
22789 "ESCAPE must be a single character, got {e:?}"
22790 ));
22791 }
22792 }
22793 };
22794 let mut out = String::with_capacity(p.len() + 4);
22795 let mut chars = p.chars();
22796 while let Some(c) = chars.next() {
22797 if Some(c) == esc_ch {
22798 match chars.next() {
22799 // Escaped wildcard / escaped escape → keep the
22800 // next char literal via backslash.
22801 Some(next) => {
22802 out.push('\\');
22803 out.push(next);
22804 }
22805 None => {
22806 return Err("LIKE pattern ends with the escape character".into());
22807 }
22808 }
22809 } else if c == '\\' && esc_ch != Some('\\') {
22810 // A raw backslash is literal under a custom (or absent) escape
22811 // — escape it for the backslash-based matcher.
22812 out.push('\\');
22813 out.push('\\');
22814 } else {
22815 out.push(c);
22816 }
22817 }
22818 Ok(Expr::Literal(Literal::String(out)))
22819 }
22820
22821 /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
22822 /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
22823 /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
22824 /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
22825 /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
22826 /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
22827 /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
22828 /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
22829 /// array expression errors honestly rather than silently mismatching.
22830 fn try_like_any_all(
22831 &mut self,
22832 base: &Expr,
22833 negated: bool,
22834 case_insensitive: bool,
22835 ) -> Result<Option<Expr>, ParseError> {
22836 let is_any = match self.peek() {
22837 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
22838 Token::Ident(s)
22839 if s.eq_ignore_ascii_case("any")
22840 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
22841 {
22842 true
22843 }
22844 _ => return Ok(None),
22845 };
22846 self.advance(); // ANY / ALL
22847 self.advance(); // '('
22848 let arr = self.parse_expr(0)?;
22849 if !matches!(self.peek(), Token::RParen) {
22850 return Err(self.err(format!(
22851 "expected ')' after LIKE {} argument, got {:?}",
22852 if is_any { "ANY" } else { "ALL" },
22853 self.peek()
22854 )));
22855 }
22856 self.advance(); // ')'
22857 let Expr::Array(items) = arr else {
22858 return Err(self.err(
22859 "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
22860 ));
22861 };
22862 let mut clauses = items.into_iter().map(|p| Expr::Like {
22863 expr: Box::new(base.clone()),
22864 pattern: Box::new(p),
22865 negated,
22866 case_insensitive,
22867 });
22868 let Some(first) = clauses.next() else {
22869 // ANY(empty) = FALSE, ALL(empty) = TRUE.
22870 return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
22871 };
22872 let op = if is_any { BinOp::Or } else { BinOp::And };
22873 let combined = clauses.fold(first, |acc, c| Expr::Binary {
22874 lhs: Box::new(acc),
22875 op,
22876 rhs: Box::new(c),
22877 });
22878 Ok(Some(combined))
22879 }
22880
22881 /// `x BETWEEN low AND high` → `(x >= low) AND (x <= high)`, wrapped in
22882 /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
22883 /// `AND` is not swallowed.
22884 fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
22885 self.advance(); // BETWEEN
22886 // SYMMETRIC — the bounds may arrive in either order; both
22887 // orientations OR together. ASYMMETRIC is the default and
22888 // absorbs as noise.
22889 let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
22890 {
22891 self.advance();
22892 true
22893 } else {
22894 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
22895 self.advance();
22896 }
22897 false
22898 };
22899 let low = self.parse_expr(6)?;
22900 if !matches!(self.peek(), Token::And) {
22901 return Err(self.err(format!(
22902 "expected AND after BETWEEN low bound, got {:?}",
22903 self.peek()
22904 )));
22905 }
22906 self.advance();
22907 let high = self.parse_expr(6)?;
22908 let target = Box::new(expr);
22909 let range = |lo: Expr, hi: Expr| Expr::Binary {
22910 lhs: Box::new(Expr::Binary {
22911 lhs: target.clone(),
22912 op: BinOp::GtEq,
22913 rhs: Box::new(lo),
22914 }),
22915 op: BinOp::And,
22916 rhs: Box::new(Expr::Binary {
22917 lhs: target.clone(),
22918 op: BinOp::LtEq,
22919 rhs: Box::new(hi),
22920 }),
22921 };
22922 let combined = if symmetric {
22923 Expr::Binary {
22924 lhs: Box::new(range(low.clone(), high.clone())),
22925 op: BinOp::Or,
22926 rhs: Box::new(range(high, low)),
22927 }
22928 } else {
22929 range(low, high)
22930 };
22931 Ok(maybe_not(combined, negated))
22932 }
22933
22934 /// `x IN (a, b, c)` → chained OR of equalities. Empty list collapses
22935 /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
22936 /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
22937 /// Caller already consumed the leading `WITH` ident.
22938 /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
22939 /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
22940 /// self-reference that appears more than once in a single term.
22941 fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
22942 use crate::ast::{CteBody, SelectStatement};
22943 if !cte.recursive {
22944 return Ok(());
22945 }
22946 let CteBody::Select(body) = &cte.body else {
22947 return Ok(());
22948 };
22949 // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
22950 // check the anchor and every peer term.
22951 let has_order = |s: &SelectStatement| !s.order_by.is_empty();
22952 let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
22953 if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
22954 return Err(self.err(String::from(
22955 "ORDER BY in a recursive query is not implemented",
22956 )));
22957 }
22958 if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
22959 return Err(self.err(String::from(
22960 "LIMIT in a recursive query is not implemented",
22961 )));
22962 }
22963 let self_refs = |s: &SelectStatement| -> usize {
22964 let Some(from) = &s.from else {
22965 return 0;
22966 };
22967 let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
22968 for j in &from.joins {
22969 if j.table.name.eq_ignore_ascii_case(&cte.name) {
22970 n += 1;
22971 }
22972 }
22973 n
22974 };
22975 if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
22976 return Err(self.err(alloc::format!(
22977 "recursive reference to query \"{}\" must not appear more than once",
22978 cte.name
22979 )));
22980 }
22981 // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
22982 // apply only when the body actually references itself (a non-self-
22983 // referencing CTE under WITH RECURSIVE may use any set-op shape).
22984 let anchor_refs = self_refs(body);
22985 let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
22986 if anchor_refs > 0 || union_refs {
22987 // Shape: the top level must be UNION [ALL] arms only. A self-ref
22988 // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
22989 // "does not have the form" error — SPG used to compute a value.
22990 if body.unions.is_empty()
22991 || body.unions.iter().any(|(k, _)| {
22992 !matches!(
22993 k,
22994 crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
22995 )
22996 })
22997 {
22998 return Err(self.err(alloc::format!(
22999 "recursive query \"{}\" does not have the form non-recursive-term \
23000 UNION [ALL] recursive-term",
23001 cte.name
23002 )));
23003 }
23004 if anchor_refs > 0 {
23005 return Err(self.err(alloc::format!(
23006 "recursive reference to query \"{}\" must not appear within its non-recursive term",
23007 cte.name
23008 )));
23009 }
23010 }
23011 let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23012 for (_, u) in &body.unions {
23013 if self_refs(u) == 0 {
23014 continue;
23015 }
23016 // The self-reference must not sit on the nullable side of an outer
23017 // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23018 if let Some(from) = &u.from {
23019 for (i, j) in from.joins.iter().enumerate() {
23020 let left_has_self = is_self(&from.primary)
23021 || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23022 let violated = match j.kind {
23023 crate::ast::JoinKind::Left => is_self(&j.table),
23024 crate::ast::JoinKind::Right => left_has_self,
23025 crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23026 _ => false,
23027 };
23028 if violated {
23029 return Err(self.err(alloc::format!(
23030 "recursive reference to query \"{}\" must not appear within an outer join",
23031 cte.name
23032 )));
23033 }
23034 }
23035 }
23036 // No aggregates at the top level of the recursive term (SPG used
23037 // to run them and surface a misleading downstream error).
23038 let mut items_and_having: Vec<&Expr> = Vec::new();
23039 for it in &u.items {
23040 if let crate::ast::SelectItem::Expr { expr, .. } = it {
23041 items_and_having.push(expr);
23042 }
23043 }
23044 if let Some(h) = &u.having {
23045 items_and_having.push(h);
23046 }
23047 for e in items_and_having {
23048 if expr_has_toplevel_aggregate(e) {
23049 return Err(self.err(String::from(
23050 "aggregate functions are not allowed in a recursive query's recursive term",
23051 )));
23052 }
23053 }
23054 }
23055 // A self-reference inside a sublink expression (EXISTS / IN / scalar
23056 // subquery) anywhere in the body is rejected; a plain FROM derived
23057 // table is legal in PG and untouched here.
23058 let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23059 all_terms.extend(body.unions.iter().map(|(_, u)| u));
23060 for term in all_terms {
23061 if select_has_self_ref_in_sublink(term, &cte.name) {
23062 return Err(self.err(alloc::format!(
23063 "recursive reference to query \"{}\" must not appear within a subquery",
23064 cte.name
23065 )));
23066 }
23067 }
23068 Ok(())
23069 }
23070
23071 /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23072 /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23073 /// right after parse so the engine sees a plain recursive CTE with the
23074 /// tracking columns already projected. DEPTH FIRST and CYCLE are
23075 /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23076 /// text-rendered rows can't provide, and errors honestly.
23077 fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23078 use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23079 if cte.search.is_none() && cte.cycle.is_none() {
23080 return Ok(());
23081 }
23082 let cte_name = cte.name.clone();
23083 let col_names = cte.column_overrides.clone();
23084 if col_names.is_empty() {
23085 return Err(
23086 self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23087 );
23088 }
23089 let search = cte.search.take();
23090 let cycle = cte.cycle.take();
23091 let mut extra_cols: Vec<String> = Vec::new();
23092 let col_ref = |name: &str| {
23093 Expr::Column(ColumnName {
23094 qualifier: Some(cte_name.clone()),
23095 name: name.to_string(),
23096 })
23097 };
23098 // Position of a SEARCH/CYCLE column within the CTE's column list.
23099 let pos_of = |name: &str| -> Result<usize, ParseError> {
23100 col_names
23101 .iter()
23102 .position(|c| c.eq_ignore_ascii_case(name))
23103 .ok_or_else(|| {
23104 self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23105 })
23106 };
23107 let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23108 let mut args = Vec::with_capacity(positions.len());
23109 for &p in positions {
23110 match items.get(p) {
23111 Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23112 _ => {
23113 return Err(self.err(
23114 "SEARCH/CYCLE column maps to a non-expression select item".into(),
23115 ));
23116 }
23117 }
23118 }
23119 Ok(Expr::FunctionCall {
23120 name: "row".into(),
23121 args,
23122 })
23123 };
23124 let CteBody::Select(body) = &mut cte.body else {
23125 return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23126 };
23127 if body.unions.is_empty() {
23128 return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23129 }
23130 let rec = body.unions.len() - 1; // recursive term = last UNION peer
23131
23132 if let Some(srch) = search {
23133 // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23134 // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23135 // no typed `record[]`, but element-wise array ORDER BY is correct
23136 // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23137 // exactly onto a typed array: DEPTH is the root→node path
23138 // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23139 // orders numerically (multi-digit keys included), matching PG.
23140 //
23141 // A multi-column BY would need a record[] to keep the per-node key
23142 // tuple orderable, which SPG can't express — error honestly there
23143 // rather than mis-order.
23144 if srch.by_columns.len() != 1 {
23145 return Err(self.err(
23146 "SEARCH … BY with multiple columns needs typed record[] ordering \
23147 SPG doesn't have yet; a single BY column is supported"
23148 .into(),
23149 ));
23150 }
23151 let key_pos = pos_of(&srch.by_columns[0])?;
23152 let base_key = match body.items.get(key_pos) {
23153 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23154 _ => {
23155 return Err(
23156 self.err("SEARCH BY column maps to a non-expression select item".into())
23157 );
23158 }
23159 };
23160 let rec_key = match body.unions[rec].1.items.get(key_pos) {
23161 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23162 _ => {
23163 return Err(
23164 self.err("SEARCH BY column maps to a non-expression select item".into())
23165 );
23166 }
23167 };
23168 if srch.depth_first {
23169 // base: ARRAY[key]; rec: array_append(cte.set, key).
23170 body.items.push(SelectItem::Expr {
23171 expr: Expr::Array(alloc::vec![base_key]),
23172 alias: Some(srch.set_column.clone()),
23173 });
23174 body.unions[rec].1.items.push(SelectItem::Expr {
23175 expr: Expr::FunctionCall {
23176 name: "array_append".into(),
23177 args: alloc::vec![col_ref(&srch.set_column), rec_key],
23178 },
23179 alias: Some(srch.set_column.clone()),
23180 });
23181 } else {
23182 // BREADTH: [depth, key]; depth starts at 0 and increments. The
23183 // leading depth element dominates the element-wise comparison,
23184 // so shallower rows sort first, then by key — PG's (depth, key).
23185 body.items.push(SelectItem::Expr {
23186 expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
23187 alias: Some(srch.set_column.clone()),
23188 });
23189 // rec depth = cte.set[1] + 1.
23190 let parent_depth = Expr::ArraySubscript {
23191 target: Box::new(col_ref(&srch.set_column)),
23192 index: Box::new(Expr::Literal(Literal::Integer(1))),
23193 };
23194 body.unions[rec].1.items.push(SelectItem::Expr {
23195 expr: Expr::Array(alloc::vec![
23196 Expr::Binary {
23197 lhs: Box::new(parent_depth),
23198 op: BinOp::Add,
23199 rhs: Box::new(Expr::Literal(Literal::Integer(1))),
23200 },
23201 rec_key,
23202 ]),
23203 alias: Some(srch.set_column.clone()),
23204 });
23205 }
23206 extra_cols.push(srch.set_column);
23207 }
23208
23209 if let Some(cyc) = cycle {
23210 let positions: Vec<usize> = cyc
23211 .columns
23212 .iter()
23213 .map(|c| pos_of(c))
23214 .collect::<Result<_, _>>()?;
23215 // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
23216 // cast it to text for the cycle path: membership only needs equality,
23217 // and the record text form gives SPG a TextArray path (SPG has no
23218 // typed record[] array). Cycle detection is unaffected.
23219 let base_row = Expr::Cast {
23220 expr: Box::new(row_of(&body.items, &positions)?),
23221 target: CastTarget::Text,
23222 };
23223 let rec_row = Expr::Cast {
23224 expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
23225 target: CastTarget::Text,
23226 };
23227 let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
23228 let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
23229 // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
23230 body.items.push(SelectItem::Expr {
23231 expr: Expr::Literal(dflt.clone()),
23232 alias: Some(cyc.mark_column.clone()),
23233 });
23234 body.items.push(SelectItem::Expr {
23235 expr: Expr::Array(alloc::vec![base_row]),
23236 alias: Some(cyc.path_column.clone()),
23237 });
23238 // rec mark: ROW(cols) already in the path → cycle.
23239 let hit = Expr::AnyAll {
23240 expr: Box::new(rec_row.clone()),
23241 op: BinOp::Eq,
23242 array: Box::new(col_ref(&cyc.path_column)),
23243 is_any: true,
23244 };
23245 let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
23246 Expr::Case {
23247 operand: None,
23248 branches: alloc::vec![(hit, Expr::Literal(mark))],
23249 else_branch: Some(Box::new(Expr::Literal(dflt))),
23250 }
23251 } else {
23252 hit
23253 };
23254 body.unions[rec].1.items.push(SelectItem::Expr {
23255 expr: mark_expr,
23256 alias: Some(cyc.mark_column.clone()),
23257 });
23258 // rec path: array_append(cte.path, ROW(cols)).
23259 body.unions[rec].1.items.push(SelectItem::Expr {
23260 expr: Expr::FunctionCall {
23261 name: "array_append".into(),
23262 args: alloc::vec![col_ref(&cyc.path_column), rec_row],
23263 },
23264 alias: Some(cyc.path_column.clone()),
23265 });
23266 // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
23267 let stop = Expr::Unary {
23268 op: UnOp::Not,
23269 expr: Box::new(col_ref(&cyc.mark_column)),
23270 };
23271 let w = &mut body.unions[rec].1.where_;
23272 *w = Some(match w.take() {
23273 Some(prev) => Expr::Binary {
23274 lhs: Box::new(prev),
23275 op: BinOp::And,
23276 rhs: Box::new(stop),
23277 },
23278 None => stop,
23279 });
23280 extra_cols.push(cyc.mark_column);
23281 extra_cols.push(cyc.path_column);
23282 }
23283 cte.column_overrides.extend(extra_cols);
23284 Ok(())
23285 }
23286
23287 /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
23288 /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
23289 fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
23290 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
23291 return Ok(None);
23292 }
23293 self.advance(); // SEARCH
23294 let depth_first = match self.peek() {
23295 Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
23296 Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
23297 other => {
23298 return Err(self.err(format!(
23299 "expected DEPTH or BREADTH after SEARCH, got {other:?}"
23300 )));
23301 }
23302 };
23303 self.advance();
23304 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
23305 return Err(self.err(format!(
23306 "expected FIRST after SEARCH mode, got {:?}",
23307 self.peek()
23308 )));
23309 }
23310 self.advance();
23311 if !self.peek_is_by() {
23312 return Err(self.err(format!(
23313 "expected BY after SEARCH … FIRST, got {:?}",
23314 self.peek()
23315 )));
23316 }
23317 self.advance();
23318 let mut by_columns = alloc::vec![self.expect_ident_like()?];
23319 while matches!(self.peek(), Token::Comma) {
23320 self.advance();
23321 by_columns.push(self.expect_ident_like()?);
23322 }
23323 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23324 return Err(self.err(format!(
23325 "expected SET in SEARCH clause, got {:?}",
23326 self.peek()
23327 )));
23328 }
23329 self.advance();
23330 let set_column = self.expect_ident_like()?;
23331 Ok(Some(crate::ast::SearchClause {
23332 depth_first,
23333 by_columns,
23334 set_column,
23335 }))
23336 }
23337
23338 /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
23339 /// USING pathcol`. Returns None when the next token isn't CYCLE.
23340 fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
23341 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
23342 return Ok(None);
23343 }
23344 self.advance(); // CYCLE
23345 let mut columns = alloc::vec![self.expect_ident_like()?];
23346 while matches!(self.peek(), Token::Comma) {
23347 self.advance();
23348 columns.push(self.expect_ident_like()?);
23349 }
23350 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23351 return Err(self.err(format!(
23352 "expected SET in CYCLE clause, got {:?}",
23353 self.peek()
23354 )));
23355 }
23356 self.advance();
23357 let mark_column = self.expect_ident_like()?;
23358 let mut mark_value = None;
23359 let mut default_value = None;
23360 if matches!(self.peek(), Token::To) {
23361 self.advance();
23362 mark_value = Some(self.parse_cycle_literal()?);
23363 if !matches!(self.peek(), Token::Default) {
23364 return Err(self.err(format!(
23365 "expected DEFAULT after CYCLE … TO, got {:?}",
23366 self.peek()
23367 )));
23368 }
23369 self.advance();
23370 default_value = Some(self.parse_cycle_literal()?);
23371 }
23372 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
23373 return Err(self.err(format!(
23374 "expected USING in CYCLE clause, got {:?}",
23375 self.peek()
23376 )));
23377 }
23378 self.advance();
23379 let path_column = self.expect_ident_like()?;
23380 Ok(Some(crate::ast::CycleClause {
23381 columns,
23382 mark_column,
23383 mark_value,
23384 default_value,
23385 path_column,
23386 }))
23387 }
23388
23389 /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
23390 /// literal (string / bool / number) in PG.
23391 fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
23392 match self.parse_expr(0)? {
23393 Expr::Literal(l) => Ok(l),
23394 other => Err(self.err(format!(
23395 "CYCLE mark/default value must be a literal, got {other:?}"
23396 ))),
23397 }
23398 }
23399
23400 fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
23401 // v4.22: WITH RECURSIVE — optional keyword right after WITH.
23402 // Comes through as an identifier; consume it if present and
23403 // mark every CTE in the clause as recursive (PG semantics —
23404 // the flag is per-WITH, not per-CTE).
23405 let mut recursive = false;
23406 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
23407 && s.eq_ignore_ascii_case("recursive")
23408 {
23409 self.advance();
23410 recursive = true;
23411 }
23412 let mut ctes = Vec::new();
23413 loop {
23414 let name = self.expect_ident_like()?;
23415 // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
23416 // PG uses these to rename the body's output columns; we
23417 // do the same below by overriding `columns[i].name`.
23418 let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
23419 self.advance();
23420 let mut names = Vec::new();
23421 loop {
23422 names.push(self.expect_ident_like()?);
23423 if matches!(self.peek(), Token::Comma) {
23424 self.advance();
23425 continue;
23426 }
23427 break;
23428 }
23429 if !matches!(self.peek(), Token::RParen) {
23430 return Err(self.err(format!(
23431 "expected ')' to close CTE column list, got {:?}",
23432 self.peek()
23433 )));
23434 }
23435 self.advance();
23436 names
23437 } else {
23438 Vec::new()
23439 };
23440 // AS is a reserved Token::As (used by SELECT-item / FROM
23441 // aliasing) — handle it specially rather than as a bare
23442 // ident.
23443 if !matches!(self.peek(), Token::As) {
23444 return Err(self.err(format!(
23445 "expected AS after CTE name {name:?}, got {:?}",
23446 self.peek()
23447 )));
23448 }
23449 self.advance();
23450 // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
23451 // MATERIALIZED` optimizer hints. SPG materialises every
23452 // CTE, so both spellings are accepted and absorbed.
23453 if matches!(self.peek(), Token::Not) {
23454 self.advance(); // NOT
23455 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23456 if s.eq_ignore_ascii_case("materialized"))
23457 {
23458 self.advance();
23459 } else {
23460 return Err(self.err(format!(
23461 "expected MATERIALIZED after AS NOT, got {:?}",
23462 self.peek()
23463 )));
23464 }
23465 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23466 if s.eq_ignore_ascii_case("materialized"))
23467 {
23468 self.advance();
23469 }
23470 if !matches!(self.peek(), Token::LParen) {
23471 return Err(self.err(format!(
23472 "expected '(' after AS in WITH clause, got {:?}",
23473 self.peek()
23474 )));
23475 }
23476 self.advance();
23477 // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
23478 // RETURNING) as the CTE body in addition to SELECT.
23479 // PG writable CTE semantics. UPDATE / DELETE come in as
23480 // bare Idents (lexer keeps SELECT / INSERT as reserved
23481 // tokens but treats the rest of DML as case-insensitive
23482 // idents).
23483 let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23484 let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23485 let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23486 let body = match self.peek() {
23487 Token::Select => {
23488 let inner = self.parse_select_stmt()?;
23489 let Statement::Select(s) = inner else {
23490 unreachable!("parse_select_stmt returns Select");
23491 };
23492 crate::ast::CteBody::Select(s)
23493 }
23494 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23495 // `SELECT * FROM t` this way and accepts it wherever a
23496 // SELECT goes, so the CTE body dispatch needs its own
23497 // arm: this match is keyed on the FIRST token, and
23498 // `Token::Table` fell through to a tail that then
23499 // rejected what it got. `parse_table_shorthand` has
23500 // returned a desugared SelectStatement since the
23501 // shorthand landed — only the routing was missing.
23502 // Round 868 found this by putting the shorthand in a
23503 // subquery; every earlier check used a top-level form.
23504 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23505 // `SELECT * FROM t` this way and accepts it wherever a
23506 // SELECT goes, so the CTE body dispatch needs its own
23507 // arm: this match is keyed on the FIRST token, and
23508 // `Token::Table` fell through to a tail that rejected
23509 // what it got. `parse_table_shorthand` has returned a
23510 // desugared SelectStatement since the shorthand landed —
23511 // only the routing was missing, here and in the derived
23512 // table's second-token gate. Round 868 found both by
23513 // putting the shorthand in a subquery; every earlier
23514 // check had used a top-level form.
23515 Token::Table
23516 if matches!(
23517 self.tokens.get(self.pos + 1),
23518 Some(Token::Ident(_) | Token::QuotedIdent(_))
23519 ) =>
23520 {
23521 let mut head = self.parse_table_shorthand()?;
23522 self.parse_setop_chain_into(&mut head)?;
23523 self.parse_select_tail_into(&mut head)?;
23524 crate::ast::CteBody::Select(head)
23525 }
23526 // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
23527 // WITH t(a) AS (VALUES (1), (2)) … lowers through
23528 // the shared rows helper onto a Select body.
23529 Token::Values => {
23530 self.advance(); // VALUES
23531 let mut head = self.parse_values_rows_body()?;
23532 // A VALUES seed can head a set-operation chain —
23533 // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
23534 // SELECT n+1 FROM t …). Attach any trailing
23535 // UNION / INTERSECT / EXCEPT peers so the
23536 // recursive-CTE body parses like the SELECT seed.
23537 self.parse_setop_chain_into(&mut head)?;
23538 crate::ast::CteBody::Select(head)
23539 }
23540 Token::Insert => {
23541 let inner = self.parse_one_statement()?;
23542 let Statement::Insert(s) = inner else {
23543 unreachable!("Token::Insert routes to Insert");
23544 };
23545 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23546 }
23547 _ if is_update_kw => {
23548 let inner = self.parse_one_statement()?;
23549 let Statement::Update(s) = inner else {
23550 return Err(
23551 self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
23552 );
23553 };
23554 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23555 }
23556 _ if is_delete_kw => {
23557 let inner = self.parse_one_statement()?;
23558 let Statement::Delete(s) = inner else {
23559 return Err(
23560 self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
23561 );
23562 };
23563 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23564 }
23565 // v7.39 (round 149) — PG 17 allows MERGE as a
23566 // data-modifying CTE body.
23567 _ if is_merge_kw => {
23568 let inner = self.parse_one_statement()?;
23569 let Statement::Merge(s) = inner else {
23570 return Err(
23571 self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
23572 );
23573 };
23574 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23575 }
23576 // v7.39 (round 151) — a CTE body may itself be
23577 // WITH-headed (PG grammar: PreparableStmt carries its
23578 // own with_clause). The nested statement keeps its own
23579 // ctes; the modifying-CTE-at-top-level rule is enforced
23580 // at execution.
23581 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
23582 self.advance(); // WITH
23583 match self.parse_with_cte_then_select()? {
23584 Statement::Select(s) => crate::ast::CteBody::Select(s),
23585 Statement::Insert(s) => {
23586 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23587 }
23588 Statement::Update(s) => {
23589 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23590 }
23591 Statement::Delete(s) => {
23592 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23593 }
23594 Statement::Merge(s) => {
23595 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23596 }
23597
23598 other => {
23599 return Err(self.err(format!(
23600 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23601 )));
23602 }
23603 }
23604 }
23605 other => {
23606 return Err(self.err(format!(
23607 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23608 )));
23609 }
23610 };
23611 if !matches!(self.peek(), Token::RParen) {
23612 return Err(self.err(format!(
23613 "expected ')' after CTE body, got {:?}",
23614 self.peek()
23615 )));
23616 }
23617 self.advance();
23618 // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
23619 // CTE, desugared into extra body columns by the engine.
23620 let search = self.parse_cte_search_clause()?;
23621 let cycle = self.parse_cte_cycle_clause()?;
23622 let mut cte = crate::ast::Cte {
23623 name,
23624 body,
23625 recursive,
23626 column_overrides,
23627 search,
23628 cycle,
23629 };
23630 self.validate_recursive_cte(&cte)?;
23631 self.desugar_cte_search_cycle(&mut cte)?;
23632 ctes.push(cte);
23633 if matches!(self.peek(), Token::Comma) {
23634 self.advance();
23635 continue;
23636 }
23637 break;
23638 }
23639 // v7.37.43-T4.4 — the outer body may be SELECT (classical),
23640 // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
23641 // the parsed CTEs to whichever statement the body produces.
23642 let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23643 let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23644 let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23645 match self.peek() {
23646 Token::Select => {
23647 let body_stmt = self.parse_select_stmt()?;
23648 let Statement::Select(mut body) = body_stmt else {
23649 unreachable!()
23650 };
23651 body.ctes = ctes;
23652 Ok(Statement::Select(body))
23653 }
23654 Token::Insert => {
23655 let body_stmt = self.parse_one_statement()?;
23656 let Statement::Insert(mut body) = body_stmt else {
23657 unreachable!()
23658 };
23659 body.ctes = ctes;
23660 Ok(Statement::Insert(body))
23661 }
23662 _ if outer_is_update => {
23663 let body_stmt = self.parse_one_statement()?;
23664 let Statement::Update(mut body) = body_stmt else {
23665 return Err(self.err(format!("expected UPDATE after WITH clause")));
23666 };
23667 body.ctes = ctes;
23668 Ok(Statement::Update(body))
23669 }
23670 _ if outer_is_delete => {
23671 let body_stmt = self.parse_one_statement()?;
23672 let Statement::Delete(mut body) = body_stmt else {
23673 return Err(self.err(format!("expected DELETE after WITH clause")));
23674 };
23675 body.ctes = ctes;
23676 Ok(Statement::Delete(body))
23677 }
23678 // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
23679 // WITH RECURSIVE is rejected with PG's exact message
23680 // (parse analysis, transformWithClause).
23681 _ if outer_is_merge => {
23682 if recursive {
23683 return Err(self.err(String::from(
23684 "WITH RECURSIVE is not supported for MERGE statement",
23685 )));
23686 }
23687 let body_stmt = self.parse_one_statement()?;
23688 let Statement::Merge(mut body) = body_stmt else {
23689 return Err(self.err(format!("expected MERGE after WITH clause")));
23690 };
23691 body.ctes = ctes;
23692 Ok(Statement::Merge(body))
23693 }
23694 other => Err(self.err(format!(
23695 "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
23696 ))),
23697 }
23698 }
23699
23700 /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
23701 /// already consumed the leading `EXISTS` ident via
23702 /// `self.advance()`.
23703 /// v7.13.0 — parse the rest of a `CASE … END` expression after
23704 /// the leading `CASE` ident has been consumed (mailrs round-5
23705 /// G9). Supports both the searched form
23706 /// (`CASE WHEN cond THEN val …`) and the simple form
23707 /// (`CASE operand WHEN val THEN val …`).
23708 fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
23709 // Disambiguate searched vs simple form: if the next token
23710 // is `WHEN`, we're in the searched form. Otherwise the
23711 // intervening expression is the operand.
23712 let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
23713 None
23714 } else {
23715 Some(Box::new(self.parse_expr(0)?))
23716 };
23717 let mut branches: Vec<(Expr, Expr)> = Vec::new();
23718 loop {
23719 match self.peek() {
23720 Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
23721 self.advance();
23722 let cond = self.parse_expr(0)?;
23723 match self.peek() {
23724 Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
23725 self.advance();
23726 }
23727 other => {
23728 return Err(self.err(alloc::format!(
23729 "expected THEN after CASE WHEN <expr>, got {other:?}"
23730 )));
23731 }
23732 }
23733 let value = self.parse_expr(0)?;
23734 branches.push((cond, value));
23735 }
23736 _ => break,
23737 }
23738 }
23739 if branches.is_empty() {
23740 return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
23741 }
23742 let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
23743 {
23744 self.advance();
23745 Some(Box::new(self.parse_expr(0)?))
23746 } else {
23747 None
23748 };
23749 match self.peek() {
23750 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
23751 self.advance();
23752 }
23753 other => {
23754 return Err(self.err(alloc::format!(
23755 "expected END to close CASE expression, got {other:?}"
23756 )));
23757 }
23758 }
23759 Ok(Expr::Case {
23760 operand,
23761 branches,
23762 else_branch,
23763 })
23764 }
23765
23766 /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
23767 /// query-source position (EXISTS / IN / INSERT source / CTE body /
23768 /// view body). Caller consumed the WITH keyword. Only a SELECT
23769 /// outer is grammatical here; the data-modifying-CTE-at-top-level
23770 /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
23771 /// maps correctly.
23772 fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
23773 let inner = self.parse_with_cte_then_select()?;
23774 match inner {
23775 Statement::Select(s) => Ok(s),
23776 other => Err(self.err(format!(
23777 "expected SELECT after WITH in a subquery, got {other:?}"
23778 ))),
23779 }
23780 }
23781
23782 /// True when the next token is the (unquoted) WITH keyword. WITH is
23783 /// reserved in PG, so a bare `with` can never be a column reference
23784 /// in these positions; a quoted `"with"` stays an identifier.
23785 fn peek_is_with_kw(&self) -> bool {
23786 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
23787 }
23788
23789 /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
23790 /// `#[inline(never)]` keeps the large SelectStatement temporaries
23791 /// off parse_expr's recursive frame (the nesting-budget stack
23792 /// cliff — see the round-153 gate regression).
23793 #[inline(never)]
23794 fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
23795 if self.peek_is_with_kw() {
23796 self.advance();
23797 self.parse_nested_with_select()
23798 } else {
23799 match self.parse_select_stmt()? {
23800 Statement::Select(s) => Ok(s),
23801 other => Err(self.err(alloc::format!(
23802 "expected SELECT inside ANY/ALL, got {other:?}"
23803 ))),
23804 }
23805 }
23806 }
23807
23808 fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
23809 if !matches!(self.peek(), Token::LParen) {
23810 return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
23811 }
23812 self.advance();
23813 // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
23814 let s = if self.peek_is_with_kw() {
23815 self.advance();
23816 self.parse_nested_with_select()?
23817 } else {
23818 let inner = self.parse_select_stmt()?;
23819 let Statement::Select(s) = inner else {
23820 unreachable!("parse_select_stmt returns Select")
23821 };
23822 s
23823 };
23824 if !matches!(self.peek(), Token::RParen) {
23825 return Err(self.err(format!(
23826 "expected ')' after EXISTS-subquery, got {:?}",
23827 self.peek()
23828 )));
23829 }
23830 self.advance();
23831 Ok(Expr::Exists {
23832 subquery: Box::new(s),
23833 negated,
23834 })
23835 }
23836
23837 fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23838 self.advance(); // IN
23839 if !matches!(self.peek(), Token::LParen) {
23840 return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
23841 }
23842 self.advance();
23843 // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
23844 // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
23845 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
23846 let s = if self.peek_is_with_kw() {
23847 self.advance();
23848 self.parse_nested_with_select()?
23849 } else {
23850 let inner = self.parse_select_stmt()?;
23851 let Statement::Select(s) = inner else {
23852 unreachable!("parse_select_stmt always returns Statement::Select")
23853 };
23854 s
23855 };
23856 if !matches!(self.peek(), Token::RParen) {
23857 return Err(self.err(format!(
23858 "expected ')' after IN-subquery, got {:?}",
23859 self.peek()
23860 )));
23861 }
23862 self.advance();
23863 return Ok(Expr::InSubquery {
23864 expr: Box::new(expr),
23865 subquery: Box::new(s),
23866 negated,
23867 });
23868 }
23869 let mut elements = Vec::new();
23870 if !matches!(self.peek(), Token::RParen) {
23871 loop {
23872 elements.push(self.parse_expr(0)?);
23873 match self.peek() {
23874 Token::Comma => {
23875 self.advance();
23876 }
23877 Token::RParen => break,
23878 other => {
23879 return Err(
23880 self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
23881 );
23882 }
23883 }
23884 }
23885 }
23886 self.advance(); // ')'
23887 // v7.30.2 (mailrs round-25) — flat InList node instead of a
23888 // left-deep OR-Eq chain: chain depth scaled with the element
23889 // count and overflowed the stack (eval + drop are recursive).
23890 if elements.is_empty() {
23891 return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
23892 }
23893 Ok(Expr::InList {
23894 expr: Box::new(expr),
23895 list: elements,
23896 negated,
23897 })
23898 }
23899
23900 /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
23901 /// already consumed by the caller. Elements must be numeric literals
23902 /// (with optional unary `-`); any compound expression is rejected at
23903 /// parse time so the runtime never needs to evaluate inside a vector.
23904 /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
23905 /// has already consumed the `EXTRACT` token before calling us —
23906 /// we pick up at the opening `(`.
23907 /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
23908 /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
23909 /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
23910 /// per-column OR-fold of
23911 /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
23912 /// term)` so the existing FTS evaluator handles semantics.
23913 ///
23914 /// The mode modifier is accepted-and-ignored at v7.17 — all
23915 /// modes map to the same `plainto_tsquery` rewrite. Boolean-
23916 /// mode operators (`+foo -bar`) would need their own parser
23917 /// (Phase 2.2c); customers who hit them today already get a
23918 /// correct lexeme-match against the bare term, only without
23919 /// the +/- precedence the customer asked for.
23920 fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
23921 // Already at `MATCH`-consumed position; the dispatcher
23922 // confirmed the next token is `(`.
23923 if !matches!(self.peek(), Token::LParen) {
23924 return Err(self.err(alloc::format!(
23925 "expected '(' after MATCH, got {:?}",
23926 self.peek()
23927 )));
23928 }
23929 self.advance();
23930 let mut cols: Vec<Expr> = Vec::new();
23931 loop {
23932 cols.push(self.parse_expr(0)?);
23933 match self.peek() {
23934 Token::Comma => {
23935 self.advance();
23936 }
23937 Token::RParen => break,
23938 other => {
23939 return Err(self.err(alloc::format!(
23940 "expected ',' or ')' in MATCH column list, got {other:?}"
23941 )));
23942 }
23943 }
23944 }
23945 self.advance(); // ')'
23946 // Expect AGAINST.
23947 match self.peek() {
23948 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
23949 self.advance();
23950 }
23951 other => {
23952 return Err(self.err(alloc::format!(
23953 "expected AGAINST after MATCH column list, got {other:?}"
23954 )));
23955 }
23956 }
23957 if !matches!(self.peek(), Token::LParen) {
23958 return Err(self.err(alloc::format!(
23959 "expected '(' after AGAINST, got {:?}",
23960 self.peek()
23961 )));
23962 }
23963 self.advance();
23964 // Read AGAINST's argument as a single primary token —
23965 // string literal, placeholder, or column-ref ident. We
23966 // can't call `parse_expr` / `parse_unary` here because
23967 // the postfix chain inside `parse_atom` would greedily
23968 // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
23969 // and fail at "expected '(' after IN". Customers always
23970 // write a literal or bound parameter in AGAINST, so this
23971 // restriction is non-blocking; the error path explains
23972 // the limit if a more complex expression shows up.
23973 let term = match self.advance() {
23974 Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
23975 Token::Placeholder(n) => Expr::Placeholder(n),
23976 Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
23977 qualifier: None,
23978 name: s,
23979 }),
23980 other => {
23981 return Err(self.err(alloc::format!(
23982 "MATCH ... AGAINST(<term>) expects a string literal, \
23983 bound parameter, or column ref, got {other:?}"
23984 )));
23985 }
23986 };
23987 // Optional mode tail — accept-and-ignore at v7.17:
23988 // IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
23989 // IN BOOLEAN MODE
23990 // WITH QUERY EXPANSION
23991 loop {
23992 match self.peek() {
23993 // IN lexes as a reserved Token::In, not an ident,
23994 // so it gets its own arm.
23995 Token::In => {
23996 self.advance();
23997 }
23998 Token::Ident(s) | Token::QuotedIdent(s)
23999 if s.eq_ignore_ascii_case("natural")
24000 || s.eq_ignore_ascii_case("language")
24001 || s.eq_ignore_ascii_case("boolean")
24002 || s.eq_ignore_ascii_case("mode")
24003 || s.eq_ignore_ascii_case("with")
24004 || s.eq_ignore_ascii_case("query")
24005 || s.eq_ignore_ascii_case("expansion") =>
24006 {
24007 self.advance();
24008 }
24009 _ => break,
24010 }
24011 }
24012 if !matches!(self.peek(), Token::RParen) {
24013 return Err(self.err(alloc::format!(
24014 "expected ')' to close AGAINST, got {:?}",
24015 self.peek()
24016 )));
24017 }
24018 self.advance();
24019 // Build per-column `to_tsvector('simple', col) @@
24020 // plainto_tsquery('simple', term)` and OR-fold.
24021 let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24022 let plainto = Expr::FunctionCall {
24023 name: String::from("plainto_tsquery"),
24024 args: alloc::vec![simple_lit(), term.clone()],
24025 };
24026 let mut folded: Option<Expr> = None;
24027 for col in cols {
24028 let to_tsv = Expr::FunctionCall {
24029 name: String::from("to_tsvector"),
24030 args: alloc::vec![simple_lit(), col],
24031 };
24032 let leaf = Expr::Binary {
24033 lhs: Box::new(to_tsv),
24034 op: crate::ast::BinOp::TsMatch,
24035 rhs: Box::new(plainto.clone()),
24036 };
24037 folded = Some(match folded {
24038 None => leaf,
24039 Some(prev) => Expr::Binary {
24040 lhs: Box::new(prev),
24041 op: crate::ast::BinOp::Or,
24042 rhs: Box::new(leaf),
24043 },
24044 });
24045 }
24046 match folded {
24047 Some(e) => Ok(e),
24048 None => Err(self.err(String::from(
24049 "MATCH(...) AGAINST(...) requires at least one column",
24050 ))),
24051 }
24052 }
24053
24054 fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24055 if !matches!(self.peek(), Token::LParen) {
24056 return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24057 }
24058 self.advance();
24059 let field_name = self.expect_ident_like()?;
24060 let field = match field_name.to_ascii_lowercase().as_str() {
24061 // PG accepts the plural spellings (years/months/…/millenniums) as
24062 // aliases for the singular fields — its datetime unit table has both.
24063 // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24064 "year" | "years" => ExtractField::Year,
24065 "month" | "months" => ExtractField::Month,
24066 "day" | "days" => ExtractField::Day,
24067 "hour" | "hours" => ExtractField::Hour,
24068 "minute" | "minutes" => ExtractField::Minute,
24069 "second" | "seconds" => ExtractField::Second,
24070 "microsecond" | "microseconds" => ExtractField::Microsecond,
24071 "epoch" => ExtractField::Epoch,
24072 "dow" => ExtractField::Dow,
24073 "isodow" => ExtractField::Isodow,
24074 "doy" => ExtractField::Doy,
24075 "week" | "weeks" => ExtractField::Week,
24076 "isoyear" => ExtractField::Isoyear,
24077 "quarter" => ExtractField::Quarter,
24078 "decade" | "decades" => ExtractField::Decade,
24079 "century" | "centuries" => ExtractField::Century,
24080 "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24081 "julian" => ExtractField::Julian,
24082 "millisecond" | "milliseconds" => ExtractField::Millisecond,
24083 "timezone" => ExtractField::Timezone,
24084 "timezone_hour" => ExtractField::TimezoneHour,
24085 "timezone_minute" => ExtractField::TimezoneMinute,
24086 // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24087 // reports an unknown one with the source type (22023); carry the
24088 // raw name so eval can word it.
24089 other => ExtractField::Other(alloc::string::String::from(other)),
24090 };
24091 if !matches!(self.peek(), Token::From) {
24092 return Err(self.err(format!(
24093 "expected FROM after EXTRACT field, got {:?}",
24094 self.peek()
24095 )));
24096 }
24097 self.advance();
24098 let source = self.parse_expr(0)?;
24099 if !matches!(self.peek(), Token::RParen) {
24100 return Err(self.err(format!(
24101 "expected ')' to close EXTRACT, got {:?}",
24102 self.peek()
24103 )));
24104 }
24105 self.advance();
24106 Ok(Expr::Extract {
24107 field,
24108 source: Box::new(source),
24109 })
24110 }
24111
24112 /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24113 /// is already consumed; we expect a single string literal next and
24114 /// resolve it into `Literal::Interval` at parse time so the engine
24115 /// never has to re-tokenise inside the string.
24116 /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24117 /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24118 /// is the SQL-standard form and is left to the path below.
24119 fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24120 // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24121 let (offset, sign) = match self.peek() {
24122 Token::Minus => (1, "-"),
24123 _ => (0, ""),
24124 };
24125 let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24126 return None;
24127 };
24128 self.tokens
24129 .get(self.pos + offset + 1)
24130 .filter(|t| mysql_interval_unit(t).is_some())?;
24131 Some((alloc::format!("{sign}{n}"), offset + 1))
24132 }
24133
24134 /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24135 /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24136 /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24137 ///
24138 /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24139 /// this by parsing the group and then restoring `self.pos` — which could
24140 /// never have worked, because `advance()` DESTROYS the token it returns
24141 /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24142 /// inert only because both branches errored back then.
24143 fn interval_paren_is_quantity(&self) -> bool {
24144 let mut depth = 0usize;
24145 let mut saw_top_level_comma = false;
24146 let mut i = self.pos;
24147 while let Some(tok) = self.tokens.get(i) {
24148 match tok {
24149 Token::LParen => depth += 1,
24150 Token::RParen => {
24151 depth = depth.saturating_sub(1);
24152 if depth == 0 {
24153 return !saw_top_level_comma
24154 && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
24155 .is_some();
24156 }
24157 }
24158 // A comma directly inside the outermost parens means the
24159 // argument list of the INTERVAL() function.
24160 Token::Comma if depth == 1 => saw_top_level_comma = true,
24161 Token::Eof => return false,
24162 _ => {}
24163 }
24164 i += 1;
24165 }
24166 false
24167 }
24168
24169 fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
24170 // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
24171 // (the index of the last Ni ≤ N), distinct from the interval literal.
24172 // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
24173 // is decided by a non-destructive lookahead (round 422) before either
24174 // branch consumes anything. MySQL only.
24175 if self.mysql_dialect
24176 && matches!(self.peek(), Token::LParen)
24177 && !self.interval_paren_is_quantity()
24178 {
24179 self.advance(); // (
24180 let mut args = Vec::new();
24181 if !matches!(self.peek(), Token::RParen) {
24182 loop {
24183 args.push(self.parse_expr(0)?);
24184 if matches!(self.peek(), Token::Comma) {
24185 self.advance();
24186 continue;
24187 }
24188 break;
24189 }
24190 }
24191 if !matches!(self.peek(), Token::RParen) {
24192 return Err(self.err(alloc::format!(
24193 "expected ')' after INTERVAL() arguments, got {:?}",
24194 self.peek()
24195 )));
24196 }
24197 self.advance(); // )
24198 return Ok(Expr::FunctionCall {
24199 name: alloc::string::String::from("interval"),
24200 args,
24201 });
24202 }
24203 // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
24204 // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
24205 // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
24206 // writes every date arithmetic there is, and it did not parse at
24207 // all. PG rejects the unquoted form outright (`syntax error at or
24208 // near "1"`, measured), so it is taken only in the MySQL dialect —
24209 // PG's own `INTERVAL '1' DAY` is untouched below.
24210 if self.mysql_dialect
24211 && let Some((text, consume)) = self.peek_unquoted_interval_count()
24212 {
24213 for _ in 0..consume {
24214 self.advance(); // the optional `-` and the number
24215 }
24216 let Some(unit) = mysql_interval_unit(self.peek()) else {
24217 return Err(self.err(alloc::format!(
24218 "expected an interval unit after INTERVAL {text}, got {:?}",
24219 self.peek()
24220 )));
24221 };
24222 self.advance(); // the unit
24223 let (months, days, micros) = scale_mysql_interval(&text, unit)
24224 .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
24225 return Ok(Expr::Literal(Literal::Interval {
24226 months,
24227 days,
24228 micros,
24229 // The canonical rendering, so Display round-trips into a
24230 // form both dialects read back.
24231 text: alloc::format!("{text} {unit}"),
24232 }));
24233 }
24234 // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
24235 // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
24236 // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
24237 // Those cannot fold into a compile-time `Literal::Interval`, so they
24238 // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
24239 // builtin, which builds the value at run time (and yields NULL for a
24240 // NULL quantity, as MariaDB does). The literal path above still folds
24241 // the constant case — it is cheaper and round-trips through Display.
24242 //
24243 // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
24244 // MySQL's quoted spelling) keep the qualifier path below.
24245 if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
24246 let qty = self.parse_expr(0)?;
24247 let Some(unit) = mysql_interval_unit(self.peek()) else {
24248 return Err(self.err(alloc::format!(
24249 "expected an interval unit after INTERVAL <expr>, got {:?}",
24250 self.peek()
24251 )));
24252 };
24253 self.advance(); // the unit
24254 return Ok(make_interval_call(qty, unit));
24255 }
24256 let tok = self.advance();
24257 let Token::String(text) = tok else {
24258 return Err(self.err(format!(
24259 "expected string literal after INTERVAL, got {tok:?}"
24260 )));
24261 };
24262 // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
24263 // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
24264 // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
24265 // bare number means and the leading/trailing precision.
24266 let field1 = interval_field_of(self.peek());
24267 let qualifier = if let Some(f1) = field1 {
24268 self.advance();
24269 let f2 = if matches!(self.peek(), Token::To) {
24270 self.advance();
24271 let Some(f) = interval_field_of(self.peek()) else {
24272 return Err(self.err(format!(
24273 "expected an interval field after TO, got {:?}",
24274 self.peek()
24275 )));
24276 };
24277 self.advance();
24278 Some(f)
24279 } else {
24280 None
24281 };
24282 Some((f1, f2))
24283 } else {
24284 None
24285 };
24286 let (months, days, micros) = match qualifier {
24287 Some(q) => interpret_qualified_interval(&text, q),
24288 None => parse_interval_text(&text),
24289 }
24290 .ok_or_else(|| ParseError {
24291 message: format!(
24292 "cannot parse INTERVAL {text:?}; \
24293 expected `<n> <unit> [<n> <unit> ...]` with units \
24294 microsecond[s], millisecond[s], second[s], minute[s], \
24295 hour[s], day[s], week[s], month[s], year[s]"
24296 ),
24297 token_pos: self.consumed_pos(),
24298 })?;
24299 Ok(Expr::Literal(Literal::Interval {
24300 months,
24301 days,
24302 micros,
24303 text,
24304 }))
24305 }
24306
24307 /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
24308 /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
24309 /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
24310 /// than a pgvector literal.
24311 fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
24312 self.advance(); // consume `[`
24313 let mut items: Vec<Expr> = Vec::new();
24314 if !matches!(self.peek(), Token::RBracket) {
24315 loop {
24316 if matches!(self.peek(), Token::LBracket) {
24317 items.push(self.parse_array_bracket_body()?);
24318 } else {
24319 items.push(self.parse_expr(0)?);
24320 }
24321 match self.peek() {
24322 Token::Comma => {
24323 self.advance();
24324 }
24325 Token::RBracket => break,
24326 other => {
24327 return Err(self.err(alloc::format!(
24328 "expected ',' or ']' in array literal, got {other:?}"
24329 )));
24330 }
24331 }
24332 }
24333 }
24334 self.advance(); // consume `]`
24335 Ok(Expr::Array(items))
24336 }
24337
24338 fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
24339 let mut elems = Vec::new();
24340 if matches!(self.peek(), Token::RBracket) {
24341 self.advance();
24342 return Ok(Expr::Literal(Literal::Vector(elems)));
24343 }
24344 loop {
24345 let e = self.parse_expr(0)?;
24346 let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
24347 message: format!("vector element must be a numeric literal, got {e:?}"),
24348 token_pos: self.pos,
24349 })?;
24350 elems.push(x);
24351 match self.peek() {
24352 Token::Comma => {
24353 self.advance();
24354 }
24355 Token::RBracket => {
24356 self.advance();
24357 break;
24358 }
24359 other => {
24360 return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
24361 }
24362 }
24363 }
24364 Ok(Expr::Literal(Literal::Vector(elems)))
24365 }
24366
24367 /// Atom that started with an identifier: could be `t.col`, `col`, or
24368 /// `func(arg, ...)`. Detect each shape by looking at the next token.
24369 /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
24370 /// [, ...])`. Caller has already consumed `OVER`. Either clause
24371 /// is optional; an empty `()` is also legal (PG semantics).
24372 /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
24373 /// modifier between `name(args)` and `OVER (...)`. Default is
24374 /// `Respect`. Unrecognised idents leave the stream unchanged.
24375 fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
24376 let Token::Ident(s) = self.peek().clone() else {
24377 return NullTreatment::Respect;
24378 };
24379 let is_ignore = s.eq_ignore_ascii_case("ignore");
24380 let is_respect = s.eq_ignore_ascii_case("respect");
24381 if !is_ignore && !is_respect {
24382 return NullTreatment::Respect;
24383 }
24384 // Lookahead for NULLS — only consume both tokens together.
24385 // pos+1 must hold a "nulls" ident.
24386 if self.pos + 1 < self.tokens.len()
24387 && let Token::Ident(s2) = &self.tokens[self.pos + 1]
24388 && s2.eq_ignore_ascii_case("nulls")
24389 {
24390 self.advance();
24391 self.advance();
24392 return if is_ignore {
24393 NullTreatment::Ignore
24394 } else {
24395 NullTreatment::Respect
24396 };
24397 }
24398 NullTreatment::Respect
24399 }
24400
24401 /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
24402 /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
24403 /// (same shape as the `OVER` tail). Consumes the whole clause and
24404 /// returns the predicate; returns `None` when no `FILTER` follows.
24405 fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
24406 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24407 return Ok(None);
24408 };
24409 if !s.eq_ignore_ascii_case("filter") {
24410 return Ok(None);
24411 }
24412 self.advance(); // FILTER
24413 if !matches!(self.peek(), Token::LParen) {
24414 return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
24415 }
24416 self.advance(); // (
24417 if !matches!(self.peek(), Token::Where) {
24418 return Err(self.err(format!(
24419 "expected WHERE inside FILTER (...), got {:?}",
24420 self.peek()
24421 )));
24422 }
24423 self.advance(); // WHERE
24424 let cond = self.parse_expr(0)?;
24425 if !matches!(self.peek(), Token::RParen) {
24426 return Err(self.err(format!(
24427 "expected ')' to close FILTER (WHERE ...), got {:?}",
24428 self.peek()
24429 )));
24430 }
24431 self.advance(); // )
24432 Ok(Some(Box::new(cond)))
24433 }
24434
24435 /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
24436 /// the separator as the aggregate's second argument, which is the
24437 /// shape `string_agg` already takes. Returns whether one was there.
24438 fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
24439 if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
24440 return Ok(false);
24441 }
24442 self.advance();
24443 let Token::String(sep) = self.peek().clone() else {
24444 return Err(self.err(alloc::format!(
24445 "expected a string literal after SEPARATOR, got {:?}",
24446 self.peek()
24447 )));
24448 };
24449 self.advance();
24450 args.push(Expr::Literal(Literal::String(sep)));
24451 Ok(true)
24452 }
24453
24454 /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
24455 /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
24456 /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
24457 /// keys, or an empty vec when no `WITHIN GROUP` follows.
24458 fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
24459 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24460 return Ok(Vec::new());
24461 };
24462 if !s.eq_ignore_ascii_case("within") {
24463 return Ok(Vec::new());
24464 }
24465 self.advance(); // WITHIN
24466 if !matches!(self.peek(), Token::Group) {
24467 return Err(self.err(format!(
24468 "expected GROUP after WITHIN, got {:?}",
24469 self.peek()
24470 )));
24471 }
24472 self.advance(); // GROUP
24473 if !matches!(self.peek(), Token::LParen) {
24474 return Err(self.err(format!(
24475 "expected '(' after WITHIN GROUP, got {:?}",
24476 self.peek()
24477 )));
24478 }
24479 self.advance(); // (
24480 if !matches!(self.peek(), Token::Order) {
24481 return Err(self.err(format!(
24482 "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
24483 self.peek()
24484 )));
24485 }
24486 self.advance(); // ORDER
24487 if !self.peek_is_by() {
24488 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24489 }
24490 self.advance(); // BY
24491 let mut keys: Vec<OrderBy> = Vec::new();
24492 loop {
24493 // v7.39 (round 691) — save/restore, the discipline this parser
24494 // already uses around `pending_sample_preds`, so a subquery inside
24495 // a key neither inherits nor leaks the channel.
24496 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
24497 let saved_coll = self.order_key_collation.take();
24498 let parsed = self.parse_expr(0);
24499 self.in_order_by_key = saved_flag;
24500 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
24501 let expr = parsed?;
24502 let desc = if matches!(self.peek(), Token::Desc) {
24503 self.advance();
24504 true
24505 } else if matches!(self.peek(), Token::Asc) {
24506 self.advance();
24507 false
24508 } else {
24509 false
24510 };
24511 let nulls_first = self.parse_optional_nulls_placement()?;
24512 keys.push(OrderBy {
24513 expr,
24514 desc,
24515 nulls_first,
24516 collation,
24517 });
24518 if matches!(self.peek(), Token::Comma) {
24519 self.advance();
24520 } else {
24521 break;
24522 }
24523 }
24524 if !matches!(self.peek(), Token::RParen) {
24525 return Err(self.err(format!(
24526 "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
24527 self.peek()
24528 )));
24529 }
24530 self.advance(); // )
24531 Ok(keys)
24532 }
24533
24534 /// No frame clause is supported.
24535 #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
24536 fn parse_over_clause(
24537 &mut self,
24538 ) -> Result<
24539 (
24540 Vec<Expr>,
24541 Vec<(Expr, bool, Option<bool>)>,
24542 Option<WindowFrame>,
24543 ),
24544 ParseError,
24545 > {
24546 // `OVER w` — a named-window reference. The WINDOW clause
24547 // parses after the select list, so the name rides out as a
24548 // marker in partition_by; parse_bare_select substitutes the
24549 // definition once the clause is known.
24550 if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
24551 let name = w.clone();
24552 self.advance();
24553 return Ok((
24554 alloc::vec![Expr::Column(crate::ast::ColumnName {
24555 qualifier: Some("__named_window__".to_string()),
24556 name,
24557 })],
24558 Vec::new(),
24559 None,
24560 ));
24561 }
24562 if !matches!(self.peek(), Token::LParen) {
24563 return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
24564 }
24565 self.advance();
24566 let mut partition_by = Vec::new();
24567 let mut order_by = Vec::new();
24568 // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
24569 // window, refined in place. PG's rules (probed against 18.4) differ
24570 // from the bare `OVER w1` form, so the reference rides out under its
24571 // own marker and `substitute_named_windows` applies them. The base
24572 // name is any leading identifier that isn't a window-spec keyword.
24573 let base_window = match self.peek() {
24574 Token::Ident(s) | Token::QuotedIdent(s)
24575 if !s.eq_ignore_ascii_case("partition")
24576 && !s.eq_ignore_ascii_case("rows")
24577 && !s.eq_ignore_ascii_case("range")
24578 && !s.eq_ignore_ascii_case("groups") =>
24579 {
24580 let n = s.clone();
24581 self.advance();
24582 Some(n)
24583 }
24584 _ => None,
24585 };
24586 // PARTITION BY ?
24587 // v7.37.6-B promoted PARTITION to a reserved keyword
24588 // (Token::Partition); pre-7.37.6-B catalogs lexed it as
24589 // `Token::Ident("partition")`. Accept both so older sources
24590 // and the new lexer surface land on the same path.
24591 let is_partition_kw = match self.peek() {
24592 Token::Partition => true,
24593 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
24594 _ => false,
24595 };
24596 if is_partition_kw {
24597 self.advance();
24598 if !self.peek_is_by() {
24599 return Err(self.err(format!(
24600 "expected BY after PARTITION, got {:?}",
24601 self.peek()
24602 )));
24603 }
24604 self.advance();
24605 loop {
24606 partition_by.push(self.parse_expr(0)?);
24607 if matches!(self.peek(), Token::Comma) {
24608 self.advance();
24609 continue;
24610 }
24611 break;
24612 }
24613 }
24614 // ORDER BY ?
24615 if matches!(self.peek(), Token::Order) {
24616 self.advance();
24617 if !self.peek_is_by() {
24618 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24619 }
24620 self.advance();
24621 loop {
24622 let e = self.parse_expr(0)?;
24623 let desc = if matches!(self.peek(), Token::Desc) {
24624 self.advance();
24625 true
24626 } else if matches!(self.peek(), Token::Asc) {
24627 self.advance();
24628 false
24629 } else {
24630 false
24631 };
24632 // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
24633 let nulls_first = self.parse_optional_nulls_placement()?;
24634 order_by.push((e, desc, nulls_first));
24635 if matches!(self.peek(), Token::Comma) {
24636 self.advance();
24637 continue;
24638 }
24639 break;
24640 }
24641 }
24642 // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
24643 // Both keywords come through the lexer as identifiers; match
24644 // case-insensitively.
24645 let mut frame: Option<WindowFrame> = None;
24646 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
24647 let kind = if s.eq_ignore_ascii_case("rows") {
24648 Some(FrameKind::Rows)
24649 } else if s.eq_ignore_ascii_case("range") {
24650 Some(FrameKind::Range)
24651 } else if s.eq_ignore_ascii_case("groups") {
24652 // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
24653 Some(FrameKind::Groups)
24654 } else {
24655 None
24656 };
24657 if let Some(kind) = kind {
24658 self.advance();
24659 frame = Some(self.parse_frame_tail(kind)?);
24660 }
24661 }
24662 if !matches!(self.peek(), Token::RParen) {
24663 return Err(self.err(format!(
24664 "expected ')' to close OVER clause, got {:?}",
24665 self.peek()
24666 )));
24667 }
24668 self.advance();
24669 if let Some(base) = base_window {
24670 // A copy may refine but never override the base's partitioning
24671 // (PG rejects it outright, before looking the name up).
24672 if !partition_by.is_empty() {
24673 return Err(self.err(alloc::format!(
24674 "cannot override PARTITION BY clause of window \"{base}\""
24675 )));
24676 }
24677 partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
24678 qualifier: Some("__named_window_ref__".to_string()),
24679 name: base,
24680 })];
24681 }
24682 Ok((partition_by, order_by, frame))
24683 }
24684
24685 /// v4.20: parse the tail of an explicit frame, given the `ROWS`
24686 /// or `RANGE` keyword was just consumed. Accepts both
24687 /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
24688 /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
24689 /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
24690 fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
24691 let (start, end) = if matches!(self.peek(), Token::Between) {
24692 self.advance();
24693 let start = self.parse_frame_bound()?;
24694 if !matches!(self.peek(), Token::And) {
24695 return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
24696 }
24697 self.advance();
24698 let end = self.parse_frame_bound()?;
24699 (start, Some(end))
24700 } else {
24701 (self.parse_frame_bound()?, None)
24702 };
24703 let exclude = self.parse_frame_exclusion()?;
24704 Ok(WindowFrame {
24705 kind,
24706 start,
24707 end,
24708 exclude,
24709 })
24710 }
24711
24712 /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
24713 /// after a frame spec. NO OTHERS is the default no-op.
24714 fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
24715 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
24716 return Ok(FrameExclusion::NoOthers);
24717 }
24718 self.advance(); // EXCLUDE
24719 match self.peek() {
24720 Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
24721 self.advance();
24722 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
24723 return Err(self.err(format!(
24724 "expected ROW after EXCLUDE CURRENT, got {:?}",
24725 self.peek()
24726 )));
24727 }
24728 self.advance();
24729 Ok(FrameExclusion::CurrentRow)
24730 }
24731 // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
24732 // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
24733 // Without this arm it fell to the catch-all, whose message
24734 // self-contradictingly listed GROUP as expected.
24735 Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
24736 self.advance();
24737 Ok(FrameExclusion::Group)
24738 }
24739 Token::Group => {
24740 self.advance();
24741 Ok(FrameExclusion::Group)
24742 }
24743 Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
24744 self.advance();
24745 Ok(FrameExclusion::Ties)
24746 }
24747 Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
24748 self.advance();
24749 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
24750 return Err(self.err(format!(
24751 "expected OTHERS after EXCLUDE NO, got {:?}",
24752 self.peek()
24753 )));
24754 }
24755 self.advance();
24756 Ok(FrameExclusion::NoOthers)
24757 }
24758 other => Err(self.err(format!(
24759 "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
24760 ))),
24761 }
24762 }
24763
24764 /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
24765 /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
24766 /// `UNBOUNDED FOLLOWING`.
24767 fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
24768 // Interval-typed offset for a value-based RANGE frame over a
24769 // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
24770 // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
24771 // PRECEDING`.
24772 if let Some((months, days, micros)) = self.try_take_interval_offset()? {
24773 let dir = self.expect_ident_like()?;
24774 return if dir.eq_ignore_ascii_case("preceding") {
24775 Ok(FrameBound::IntervalPreceding {
24776 months,
24777 days,
24778 micros,
24779 })
24780 } else if dir.eq_ignore_ascii_case("following") {
24781 Ok(FrameBound::IntervalFollowing {
24782 months,
24783 days,
24784 micros,
24785 })
24786 } else {
24787 Err(self.err(format!(
24788 "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
24789 )))
24790 };
24791 }
24792 // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
24793 if let Token::Integer(n) = *self.peek() {
24794 self.advance();
24795 let n: u64 = u64::try_from(n).map_err(|_| {
24796 self.err(format!(
24797 "invalid frame offset {n} — expected non-negative integer"
24798 ))
24799 })?;
24800 let dir = self.expect_ident_like()?;
24801 return if dir.eq_ignore_ascii_case("preceding") {
24802 Ok(FrameBound::OffsetPreceding(n))
24803 } else if dir.eq_ignore_ascii_case("following") {
24804 Ok(FrameBound::OffsetFollowing(n))
24805 } else {
24806 Err(self.err(format!(
24807 "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
24808 )))
24809 };
24810 }
24811 let first = self.expect_ident_like()?;
24812 if first.eq_ignore_ascii_case("unbounded") {
24813 let dir = self.expect_ident_like()?;
24814 return if dir.eq_ignore_ascii_case("preceding") {
24815 Ok(FrameBound::UnboundedPreceding)
24816 } else if dir.eq_ignore_ascii_case("following") {
24817 Ok(FrameBound::UnboundedFollowing)
24818 } else {
24819 Err(self.err(format!(
24820 "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
24821 )))
24822 };
24823 }
24824 if first.eq_ignore_ascii_case("current") {
24825 let row = self.expect_ident_like()?;
24826 if !row.eq_ignore_ascii_case("row") {
24827 return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
24828 }
24829 return Ok(FrameBound::CurrentRow);
24830 }
24831 Err(self.err(format!(
24832 "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
24833 )))
24834 }
24835
24836 /// Detect and consume a leading interval offset in a frame bound —
24837 /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
24838 /// `(months, days, micros)`. Leaves the cursor on the trailing
24839 /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
24840 /// when the next tokens are not an interval offset.
24841 fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
24842 // Shape A — `INTERVAL '1 day'`.
24843 if matches!(self.peek(), Token::Interval) {
24844 self.advance(); // INTERVAL
24845 let atom = self.parse_interval_atom()?;
24846 if let Expr::Literal(Literal::Interval {
24847 months,
24848 days,
24849 micros,
24850 ..
24851 }) = atom
24852 {
24853 return Ok(Some((months, days, micros)));
24854 }
24855 return Err(self.err("expected an interval literal in frame offset".to_string()));
24856 }
24857 // Shape B — `'1 day'::interval`. Look ahead for the exact
24858 // string / `::` / interval-target triple before committing.
24859 if let Token::String(text) = self.peek() {
24860 let target_is_interval = match self.tokens.get(self.pos + 2) {
24861 Some(Token::Interval) => true,
24862 Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
24863 _ => false,
24864 };
24865 let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
24866 && target_is_interval;
24867 if is_cast {
24868 let text = text.clone();
24869 self.advance(); // string
24870 self.advance(); // ::
24871 self.advance(); // interval
24872 let parts = parse_interval_text(&text).ok_or_else(|| {
24873 self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
24874 })?;
24875 return Ok(Some(parts));
24876 }
24877 }
24878 Ok(None)
24879 }
24880
24881 fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
24882 if matches!(self.peek(), Token::Dot) {
24883 self.advance();
24884 let name = self.expect_ident_like()?;
24885 // v7.14.0 — schema-qualified function call
24886 // `<schema>.<fn>(args)`. PG dumps emit
24887 // `pg_catalog.set_config(...)` in the preamble. SPG
24888 // is single-namespace: drop the schema prefix and
24889 // route the dispatch on the bare function name.
24890 if matches!(self.peek(), Token::LParen) {
24891 return self.finish_ident_atom(name);
24892 }
24893 return Ok(Expr::Column(ColumnName {
24894 qualifier: Some(first),
24895 name,
24896 }));
24897 }
24898 if matches!(self.peek(), Token::LParen) {
24899 self.advance();
24900 // `COUNT(*)` — special-cased here because `*` isn't a normal
24901 // expression token. Lower-case match on `first` since the lexer
24902 // folds identifiers.
24903 if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
24904 self.advance();
24905 if !matches!(self.peek(), Token::RParen) {
24906 return Err(self.err(format!(
24907 "expected ')' after COUNT(*), got {:?}",
24908 self.peek()
24909 )));
24910 }
24911 self.advance();
24912 // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
24913 let filter = self.parse_filter_clause()?;
24914 // v4.12: COUNT(*) OVER (...) — same window tail.
24915 let null_treatment = self.parse_null_treatment_modifier();
24916 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
24917 && s.eq_ignore_ascii_case("over")
24918 {
24919 self.advance();
24920 let (partition_by, order_by, frame) = self.parse_over_clause()?;
24921 return Ok(Expr::WindowFunction {
24922 name: "count_star".into(),
24923 args: Vec::new(),
24924 partition_by,
24925 order_by,
24926 frame,
24927 null_treatment,
24928 filter,
24929 });
24930 }
24931 if let Some(filter) = filter {
24932 return Ok(Expr::AggregateOrdered {
24933 call: Box::new(Expr::FunctionCall {
24934 name: "count_star".into(),
24935 args: Vec::new(),
24936 }),
24937 order_by: Vec::new(),
24938 distinct: false,
24939 filter: Some(filter),
24940 });
24941 }
24942 return Ok(Expr::FunctionCall {
24943 name: "count_star".into(),
24944 args: Vec::new(),
24945 });
24946 }
24947 // Function call. PG-style: zero-or-more comma-separated args.
24948 let mut args = Vec::new();
24949 // v7.38 (read01, T14) — named-argument notation `argname => value`.
24950 // Names are collected in lock-step with `args` and resolved to
24951 // positional order after the loop (the AST stays positional).
24952 let mut arg_names: Vec<Option<String>> = Vec::new();
24953 let mut agg_order_by: Vec<OrderBy> = Vec::new();
24954 // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
24955 // seen, so the value arguments before it can be folded.
24956 let mut saw_separator = false;
24957 // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
24958 // v7.32 (round-29) — accept the dual `ALL` quantifier too
24959 // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
24960 let agg_distinct = if matches!(self.peek(), Token::Distinct) {
24961 self.advance();
24962 true
24963 } else if matches!(self.peek(), Token::All) {
24964 self.advance();
24965 false
24966 } else {
24967 false
24968 };
24969 // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
24970 // TIMESTAMPDIFF take a bare unit keyword as the first
24971 // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
24972 // bare type keyword (DATE / TIME / DATETIME); lower them
24973 // onto string literals so the evaluator sees plain text.
24974 if ((first.eq_ignore_ascii_case("timestampadd")
24975 || first.eq_ignore_ascii_case("timestampdiff"))
24976 && matches!(self.peek(), Token::Ident(u) if matches!(
24977 u.to_ascii_lowercase().as_str(),
24978 "microsecond" | "second" | "minute" | "hour" | "day"
24979 | "week" | "month" | "quarter" | "year"
24980 )))
24981 || (first.eq_ignore_ascii_case("get_format")
24982 && matches!(self.peek(), Token::Ident(u) if matches!(
24983 u.to_ascii_lowercase().as_str(),
24984 "date" | "time" | "datetime" | "timestamp"
24985 )))
24986 {
24987 if let Token::Ident(u) = self.peek() {
24988 args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
24989 }
24990 self.advance();
24991 if matches!(self.peek(), Token::Comma) {
24992 self.advance();
24993 }
24994 }
24995 // `ROW(a, b, …)` keyword constructor. Followed by a
24996 // comparison operator or [NOT] IN it joins the paren
24997 // row-constructor machinery (fieldwise parse-time
24998 // expansion); bare, it stays a `row` call the evaluator
24999 // renders as PG record text.
25000 if first.eq_ignore_ascii_case("row") {
25001 let mut row_items = Vec::new();
25002 if !matches!(self.peek(), Token::RParen) {
25003 loop {
25004 row_items.push(self.parse_expr(0)?);
25005 match self.peek() {
25006 Token::Comma => {
25007 self.advance();
25008 }
25009 Token::RParen => break,
25010 other => {
25011 return Err(self.err(format!(
25012 "expected ',' or ')' in ROW(...), got {other:?}"
25013 )));
25014 }
25015 }
25016 }
25017 }
25018 self.advance(); // ')'
25019 let comparison_follows = matches!(
25020 self.peek(),
25021 Token::Eq
25022 | Token::NotEq
25023 | Token::Lt
25024 | Token::LtEq
25025 | Token::Gt
25026 | Token::GtEq
25027 | Token::In
25028 ) || (matches!(self.peek(), Token::Not)
25029 && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25030 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25031 if comparison_follows && !row_items.is_empty() {
25032 return self.parse_row_comparison_tail(row_items);
25033 }
25034 return Ok(Expr::FunctionCall {
25035 name: String::from("row"),
25036 args: row_items,
25037 });
25038 }
25039 // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25040 // the parse-mode keyword introduces the source text. SPG
25041 // carries XML as text, so both modes lower to __xmlparse(expr)
25042 // which validates well-formedness and returns Value::Xml.
25043 if first.eq_ignore_ascii_case("xmlparse")
25044 && matches!(self.peek(), Token::Ident(kw)
25045 if kw.eq_ignore_ascii_case("document")
25046 || kw.eq_ignore_ascii_case("content"))
25047 {
25048 let mode = match self.advance() {
25049 Token::Ident(kw) => kw.to_ascii_lowercase(),
25050 _ => unreachable!("peeked an ident"),
25051 };
25052 let src = self.parse_expr(0)?;
25053 if !matches!(self.peek(), Token::RParen) {
25054 return Err(self.err(format!(
25055 "expected ')' to close XMLPARSE, got {:?}",
25056 self.peek()
25057 )));
25058 }
25059 self.advance();
25060 return Ok(Expr::FunctionCall {
25061 name: String::from("__xmlparse"),
25062 args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25063 });
25064 }
25065 // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25066 // keyword introduces the element name (a bare or quoted
25067 // identifier), then optional content expressions. Lower to a
25068 // plain `xmlelement(name_text, content …)` call.
25069 if first.eq_ignore_ascii_case("xmlelement")
25070 && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25071 {
25072 self.advance(); // consume NAME
25073 let elem_name = match self.peek().clone() {
25074 Token::Ident(n) | Token::QuotedIdent(n) => {
25075 self.advance();
25076 n
25077 }
25078 other => {
25079 return Err(self.err(format!(
25080 "expected element name after XMLELEMENT NAME, got {other:?}"
25081 )));
25082 }
25083 };
25084 let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25085 while matches!(self.peek(), Token::Comma) {
25086 self.advance();
25087 args.push(self.parse_expr(0)?);
25088 }
25089 if !matches!(self.peek(), Token::RParen) {
25090 return Err(self.err(format!(
25091 "expected ')' to close XMLELEMENT, got {:?}",
25092 self.peek()
25093 )));
25094 }
25095 self.advance();
25096 return Ok(Expr::FunctionCall {
25097 name: String::from("xmlelement"),
25098 args,
25099 });
25100 }
25101 // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
25102 // becomes a `<name>value</name>` element; a bare column infers its
25103 // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
25104 if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
25105 let mut args: Vec<Expr> = Vec::new();
25106 loop {
25107 let val = self.parse_expr(0)?;
25108 let name = if matches!(self.peek(), Token::As) {
25109 self.advance();
25110 match self.peek().clone() {
25111 Token::Ident(n) | Token::QuotedIdent(n) => {
25112 self.advance();
25113 n
25114 }
25115 other => {
25116 return Err(self.err(format!(
25117 "expected name after AS in XMLFOREST, got {other:?}"
25118 )));
25119 }
25120 }
25121 } else if let Expr::Column(c) = &val {
25122 c.name.clone()
25123 } else {
25124 return Err(
25125 self.err("XMLFOREST element without a column name needs AS".into())
25126 );
25127 };
25128 args.push(Expr::Literal(Literal::String(name)));
25129 args.push(val);
25130 if matches!(self.peek(), Token::Comma) {
25131 self.advance();
25132 } else {
25133 break;
25134 }
25135 }
25136 if !matches!(self.peek(), Token::RParen) {
25137 return Err(self.err(format!(
25138 "expected ')' to close XMLFOREST, got {:?}",
25139 self.peek()
25140 )));
25141 }
25142 self.advance();
25143 return Ok(Expr::FunctionCall {
25144 name: String::from("xmlforest"),
25145 args,
25146 });
25147 }
25148 // SQL-standard `POSITION(sub IN str)` — lowers onto
25149 // strpos(str, sub). IN is the argument separator here,
25150 // so the needle parses with the IN-tail suppressed.
25151 if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
25152 let saved = self.suppress_in_tail;
25153 self.suppress_in_tail = true;
25154 let needle = self.parse_expr(0);
25155 self.suppress_in_tail = saved;
25156 let needle = needle?;
25157 if matches!(self.peek(), Token::In) {
25158 self.advance();
25159 let haystack = self.parse_expr(0)?;
25160 if !matches!(self.peek(), Token::RParen) {
25161 return Err(self.err(format!(
25162 "expected ')' to close POSITION, got {:?}",
25163 self.peek()
25164 )));
25165 }
25166 self.advance();
25167 return Ok(Expr::FunctionCall {
25168 name: String::from("strpos"),
25169 args: alloc::vec![haystack, needle],
25170 });
25171 }
25172 // position(sub, str) comma form (incl. bytea) —
25173 // hand the parsed first arg to the generic list.
25174 args.push(needle);
25175 if matches!(self.peek(), Token::Comma) {
25176 self.advance();
25177 }
25178 }
25179 // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
25180 // FROM str)` — lowers onto btrim / ltrim / rtrim. The
25181 // plain comma forms TRIM(str) / TRIM(str, chars) keep
25182 // riding the generic argument list below.
25183 if first.eq_ignore_ascii_case("trim") {
25184 let mode = match self.peek() {
25185 Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
25186 self.advance();
25187 Some("btrim")
25188 }
25189 Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
25190 self.advance();
25191 Some("ltrim")
25192 }
25193 Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
25194 self.advance();
25195 Some("rtrim")
25196 }
25197 _ => None,
25198 };
25199 if mode.is_some() || matches!(self.peek(), Token::From) {
25200 // TRIM([mode] FROM str) — no strip-chars.
25201 let chars = if matches!(self.peek(), Token::From) {
25202 None
25203 } else {
25204 Some(self.parse_expr(0)?)
25205 };
25206 if !matches!(self.peek(), Token::From) {
25207 return Err(self.err(format!(
25208 "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
25209 self.peek()
25210 )));
25211 }
25212 self.advance();
25213 let target = self.parse_expr(0)?;
25214 if !matches!(self.peek(), Token::RParen) {
25215 return Err(
25216 self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
25217 );
25218 }
25219 self.advance();
25220 let mut trim_args = alloc::vec![target];
25221 if let Some(c) = chars {
25222 trim_args.push(c);
25223 }
25224 return Ok(Expr::FunctionCall {
25225 name: String::from(mode.unwrap_or("btrim")),
25226 args: trim_args,
25227 });
25228 }
25229 }
25230 if !matches!(self.peek(), Token::RParen) {
25231 loop {
25232 // v7.38 (read01, T14) — `argname => value` names this arg.
25233 // v7.39 (read01 round 77) — `argname := value` is the same
25234 // thing, and it is the spelling PG's own docs lead with. It
25235 // was simply never lexed here, so every `f(x := 1)` died in
25236 // the parser regardless of what `f` was.
25237 let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
25238 (
25239 Token::Ident(n) | Token::QuotedIdent(n),
25240 Some(Token::FatArrow | Token::ColonEq),
25241 ) => {
25242 let name = n.clone();
25243 self.advance(); // name
25244 self.advance(); // => / :=
25245 Some(name)
25246 }
25247 _ => None,
25248 };
25249 // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
25250 // array's elements into a variadic call's trailing args
25251 // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
25252 // reserved, so it arrives as a bare ident before the arg.
25253 let is_variadic = this_name.is_none()
25254 && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
25255 if is_variadic {
25256 self.advance();
25257 }
25258 let arg = self.parse_expr(0)?;
25259 args.push(match &this_name {
25260 // The callee's parameter names decide the slot, and a
25261 // user function's live in the catalog. Carry the name
25262 // to eval rather than guessing here.
25263 Some(n) => Expr::NamedArg {
25264 name: n.clone(),
25265 expr: Box::new(arg),
25266 },
25267 None if is_variadic => Expr::Variadic(Box::new(arg)),
25268 None => arg,
25269 });
25270 arg_names.push(this_name);
25271 // v7.25 (round-17) — standard `CAST(expr AS type)`.
25272 // The `::` cast already worked; this lowers the
25273 // function form onto the same Expr::Cast node.
25274 if first.eq_ignore_ascii_case("cast")
25275 && args.len() == 1
25276 && matches!(self.peek(), Token::As)
25277 {
25278 self.advance();
25279 let target = self.parse_cast_target()?;
25280 if !matches!(self.peek(), Token::RParen) {
25281 return Err(self.err(format!(
25282 "expected ')' to close CAST, got {:?}",
25283 self.peek()
25284 )));
25285 }
25286 self.advance();
25287 return Ok(Expr::Cast {
25288 expr: Box::new(args.pop().expect("one arg")),
25289 target,
25290 });
25291 }
25292 // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
25293 // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
25294 // keywords; SPG's lexer makes them plain idents (so they'd be
25295 // read as column refs). Lower the keyword to the string form
25296 // the evaluator already accepts.
25297 if first.eq_ignore_ascii_case("normalize")
25298 && args.len() == 1
25299 && matches!(self.peek(), Token::Comma)
25300 {
25301 let form = match self.tokens.get(self.pos + 1) {
25302 Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
25303 let up = f.to_ascii_uppercase();
25304 matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
25305 }
25306 _ => None,
25307 };
25308 if let Some(up) = form {
25309 self.advance(); // comma
25310 self.advance(); // form keyword
25311 args.push(Expr::Literal(Literal::String(up)));
25312 }
25313 }
25314 // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
25315 // form. Desugars to the comma-list shape evaluator already
25316 // handles. Triggered after the first arg when the function
25317 // name is substring / substr and the next token is FROM
25318 // (a reserved keyword in PG; SPG also reserves it).
25319 // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
25320 // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
25321 // internal __substring_similar(str, pat, esc) call.
25322 if (first.eq_ignore_ascii_case("substring")
25323 || first.eq_ignore_ascii_case("substr"))
25324 && args.len() == 1
25325 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
25326 {
25327 self.advance(); // SIMILAR
25328 let pattern = self.parse_expr(0)?;
25329 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
25330 {
25331 return Err(self.err(format!(
25332 "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
25333 self.peek()
25334 )));
25335 }
25336 self.advance(); // ESCAPE
25337 let esc = self.parse_expr(0)?;
25338 if !matches!(self.peek(), Token::RParen) {
25339 return Err(self.err(format!(
25340 "expected ')' to close substring(... SIMILAR ...), got {:?}",
25341 self.peek()
25342 )));
25343 }
25344 self.advance();
25345 args.push(pattern);
25346 args.push(esc);
25347 return Ok(Expr::FunctionCall {
25348 name: "__substring_similar".to_string(),
25349 args,
25350 });
25351 }
25352 if (first.eq_ignore_ascii_case("substring")
25353 || first.eq_ignore_ascii_case("substr"))
25354 && args.len() == 1
25355 && matches!(self.peek(), Token::From | Token::For)
25356 {
25357 // `substring(str FROM pos [FOR len])`, or the FOR-only
25358 // `substring(str FOR len)` which PG treats as FROM 1.
25359 if matches!(self.peek(), Token::From) {
25360 self.advance();
25361 let start = self.parse_expr(0)?;
25362 args.push(start);
25363 } else {
25364 args.push(Expr::Literal(Literal::Integer(1)));
25365 }
25366 if matches!(self.peek(), Token::For) {
25367 self.advance();
25368 let length = self.parse_expr(0)?;
25369 args.push(length);
25370 }
25371 if !matches!(self.peek(), Token::RParen) {
25372 return Err(self.err(format!(
25373 "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
25374 self.peek()
25375 )));
25376 }
25377 self.advance();
25378 return Ok(Expr::FunctionCall {
25379 name: first.to_ascii_lowercase(),
25380 args,
25381 });
25382 }
25383 // PG `overlay(str PLACING repl FROM n [FOR len])`
25384 // syntactic form. Desugars to the `overlay(str,
25385 // repl, n[, len])` comma-list shape the evaluator
25386 // already implements. `PLACING` is not a reserved
25387 // token in SPG, so it arrives as a bare Ident.
25388 if first.eq_ignore_ascii_case("overlay")
25389 && args.len() == 1
25390 && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
25391 {
25392 self.advance(); // consume PLACING
25393 args.push(self.parse_expr(0)?); // replacement
25394 if !matches!(self.peek(), Token::From) {
25395 return Err(self.err(format!(
25396 "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
25397 self.peek()
25398 )));
25399 }
25400 self.advance();
25401 args.push(self.parse_expr(0)?); // start position
25402 if matches!(self.peek(), Token::For) {
25403 self.advance();
25404 args.push(self.parse_expr(0)?); // length
25405 }
25406 if !matches!(self.peek(), Token::RParen) {
25407 return Err(self.err(format!(
25408 "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
25409 self.peek()
25410 )));
25411 }
25412 self.advance();
25413 return Ok(Expr::FunctionCall {
25414 name: String::from("overlay"),
25415 args,
25416 });
25417 }
25418 // `TRIM(chars FROM str)` — the keyword-less
25419 // spelling lands here after the chars parse
25420 // (the keyword forms return earlier).
25421 if first.eq_ignore_ascii_case("trim")
25422 && args.len() == 1
25423 && matches!(self.peek(), Token::From)
25424 {
25425 self.advance();
25426 let target = self.parse_expr(0)?;
25427 if !matches!(self.peek(), Token::RParen) {
25428 return Err(self.err(format!(
25429 "expected ')' to close TRIM(chars FROM str), got {:?}",
25430 self.peek()
25431 )));
25432 }
25433 self.advance();
25434 let chars = args.pop().expect("one arg");
25435 return Ok(Expr::FunctionCall {
25436 name: String::from("btrim"),
25437 args: alloc::vec![target, chars],
25438 });
25439 }
25440 // v7.24 (round-16 A) — aggregate-internal
25441 // ordering: `array_agg(x ORDER BY y DESC NULLS
25442 // LAST)`. Keys close the argument list.
25443 if matches!(self.peek(), Token::Order) {
25444 self.advance();
25445 if !self.peek_is_by() {
25446 return Err(self.err(format!(
25447 "expected BY after ORDER in aggregate args, got {:?}",
25448 self.peek()
25449 )));
25450 }
25451 self.advance();
25452 loop {
25453 // v7.39 (round 691) — save/restore, the discipline this parser
25454 // already uses around `pending_sample_preds`, so a subquery inside
25455 // a key neither inherits nor leaks the channel.
25456 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25457 let saved_coll = self.order_key_collation.take();
25458 let parsed = self.parse_expr(0);
25459 self.in_order_by_key = saved_flag;
25460 let collation =
25461 core::mem::replace(&mut self.order_key_collation, saved_coll);
25462 let expr = parsed?;
25463 let desc = if matches!(self.peek(), Token::Desc) {
25464 self.advance();
25465 true
25466 } else if matches!(self.peek(), Token::Asc) {
25467 self.advance();
25468 false
25469 } else {
25470 false
25471 };
25472 let nulls_first = self.parse_optional_nulls_placement()?;
25473 agg_order_by.push(OrderBy {
25474 expr,
25475 desc,
25476 nulls_first,
25477 collation,
25478 });
25479 if matches!(self.peek(), Token::Comma) {
25480 self.advance();
25481 } else {
25482 break;
25483 }
25484 }
25485 // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
25486 // follow the ORDER BY inside GROUP_CONCAT.
25487 if self.consume_group_concat_separator(&mut args)? {
25488 saw_separator = true;
25489 }
25490 if !matches!(self.peek(), Token::RParen) {
25491 return Err(self.err(format!(
25492 "expected ')' after aggregate ORDER BY, got {:?}",
25493 self.peek()
25494 )));
25495 }
25496 break;
25497 }
25498 // v7.39 (round 354, M12) — …or directly after the
25499 // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
25500 // own spelling of what PG passes as string_agg's second
25501 // argument; it was a parse error, so every MySQL query
25502 // that names its own separator failed outright.
25503 if self.consume_group_concat_separator(&mut args)? {
25504 saw_separator = true;
25505 break;
25506 }
25507 match self.peek() {
25508 Token::Comma => {
25509 self.advance();
25510 }
25511 Token::RParen => break,
25512 other => {
25513 return Err(self.err(format!(
25514 "expected ',' or ')' in function args, got {other:?}"
25515 )));
25516 }
25517 }
25518 }
25519 }
25520 // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
25521 // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
25522 // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
25523 // meaning a separator — that is what the explicit SEPARATOR
25524 // tail is for. Fold them into one `concat(...)` so the
25525 // aggregate keeps its single value argument.
25526 if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
25527 let values = args.len() - usize::from(saw_separator);
25528 if values > 1 {
25529 let sep_arg = if saw_separator { args.pop() } else { None };
25530 let folded = Expr::FunctionCall {
25531 name: "concat".to_string(),
25532 args: core::mem::take(&mut args),
25533 };
25534 args.push(folded);
25535 if let Some(sep) = sep_arg {
25536 args.push(sep);
25537 }
25538 }
25539 }
25540 self.advance(); // consume ')'
25541 // v7.39 (read01 round 77) — named arguments are NOT reordered here
25542 // any more. The parser has no catalog, so it could only ever resolve
25543 // the handful of `make_*` builtins whose parameter names were baked
25544 // into a table right here — every user function got
25545 // "does not support named arguments", though the catalog has been
25546 // storing its parameter names all along. Reordering happens in eval,
25547 // in one place, for builtins and user functions alike.
25548 // v7.32 (round-29) — ordered-set aggregate tail
25549 // `name(direct_args) WITHIN GROUP (ORDER BY …)`
25550 // (percentile_cont / percentile_disc / mode). The sort spec
25551 // lands in the same `order_by` slot a decorated aggregate
25552 // uses; the executor dispatches on the function name. WITHIN
25553 // GROUP and an intra-argument ORDER BY are mutually
25554 // exclusive (PG rejects both).
25555 let within_group_order = self.parse_within_group_clause()?;
25556 if !within_group_order.is_empty() && !agg_order_by.is_empty() {
25557 return Err(self.err(
25558 "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
25559 .into(),
25560 ));
25561 }
25562 let within_group_seen = !within_group_order.is_empty();
25563 let agg_order_by = if within_group_order.is_empty() {
25564 agg_order_by
25565 } else {
25566 within_group_order
25567 };
25568 // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
25569 let filter = self.parse_filter_clause()?;
25570 // v4.12: window-function tail — `name(args) OVER (...)`.
25571 // Promotes the just-parsed FunctionCall into a
25572 // WindowFunction node carrying partition + order.
25573 // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
25574 // / `RESPECT NULLS OVER (...)` between the closing paren
25575 // and `OVER`.
25576 let null_treatment = self.parse_null_treatment_modifier();
25577 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25578 && s.eq_ignore_ascii_case("over")
25579 {
25580 self.advance();
25581 // v7.39 (round 230) — PG implements neither modifier for a
25582 // windowed call and says so (0A000). Both used to be parsed
25583 // and then silently dropped here, so `count(DISTINCT v)
25584 // OVER (…)` quietly answered the non-distinct count.
25585 if agg_distinct {
25586 return Err(
25587 self.err("DISTINCT is not implemented for window functions".to_string())
25588 );
25589 }
25590 if !agg_order_by.is_empty() {
25591 // PG separates the two shapes that land here: a
25592 // WITHIN GROUP call is an ordered-set aggregate and gets
25593 // its own message naming the aggregate; a plain
25594 // `agg(x ORDER BY y)` gets the generic one.
25595 let msg = if within_group_seen {
25596 alloc::format!("OVER is not supported for ordered-set aggregate {first}")
25597 } else {
25598 "aggregate ORDER BY is not implemented for window functions".to_string()
25599 };
25600 return Err(self.err(msg));
25601 }
25602 let (partition_by, order_by, frame) = self.parse_over_clause()?;
25603 return Ok(Expr::WindowFunction {
25604 name: first,
25605 args,
25606 partition_by,
25607 order_by,
25608 frame,
25609 null_treatment,
25610 filter,
25611 });
25612 }
25613 if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
25614 return Ok(Expr::AggregateOrdered {
25615 call: Box::new(Expr::FunctionCall { name: first, args }),
25616 order_by: agg_order_by,
25617 distinct: agg_distinct,
25618 filter,
25619 });
25620 }
25621 // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
25622 // over TIMESTAMPTZ and has no timestamp overload, so a
25623 // timestamp argument is coerced on the way in and the answer
25624 // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
25625 // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
25626 // zone`. SPG answered `timestamp without time zone`, dropping
25627 // the offset from every rendering.
25628 //
25629 // Writing the coercion PG performs makes the existing
25630 // argument-driven typing (the one `date_trunc` uses) reach the
25631 // right answer, rather than teaching the type layer a second
25632 // rule. MySQL's DATE_ADD is a different function that returns
25633 // DATE or DATETIME, so this is PG-dialect only.
25634 //
25635 // Out-of-line because this sits on the RECURSIVE descent
25636 // frame: an inline block with locals here costs every nesting
25637 // level, and the suite's deep-nesting sentinel overflowed the
25638 // 512 KiB parser stack the moment one was added (round 430's
25639 // lesson, in the same shape).
25640 if !self.mysql_dialect {
25641 lift_date_add_arg_to_timestamptz(&first, &mut args);
25642 }
25643 return Ok(Expr::FunctionCall { name: first, args });
25644 }
25645 // v7.9.20 — SQL-standard parenless keyword expressions
25646 // (PG treats these as functions called without parens).
25647 // Resolve to a synthetic FunctionCall so the engine's
25648 // eval path reuses the existing function-call routing.
25649 // mailrs G3.
25650 let lc = first.to_ascii_lowercase();
25651 if matches!(
25652 lc.as_str(),
25653 "current_date"
25654 | "current_time"
25655 | "current_timestamp"
25656 | "localtimestamp"
25657 | "localtime"
25658 // v7.37.17 (17.6 siblings) — session-identity SQL-
25659 // standard parenless keywords. current_user /
25660 // session_user / user were already caught by the
25661 // pgwire canned-response shortcut but bare-select
25662 // in the embedded engine went through Expr::Column
25663 // and errored. Adding them here so the parser
25664 // resolves to a synthetic FunctionCall that reuses
25665 // the existing eval/functions.rs dispatch.
25666 | "current_user"
25667 | "session_user"
25668 | "current_role"
25669 | "current_catalog"
25670 | "current_schema"
25671 | "current_database"
25672 // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
25673 | "system_user"
25674 ) {
25675 return Ok(Expr::FunctionCall {
25676 name: lc,
25677 args: Vec::new(),
25678 });
25679 }
25680 Ok(Expr::Column(ColumnName {
25681 qualifier: None,
25682 name: first,
25683 }))
25684 }
25685}
25686
25687/// v7.39 (round 522) — write the coercion PG's `date_add` /
25688/// `date_subtract` signature performs.
25689///
25690/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
25691/// timestamp argument is cast on the way in and the answer is
25692/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
25693/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
25694/// `timestamp without time zone`, dropping the offset from every
25695/// rendering of the result.
25696///
25697/// Writing the cast the signature implies lets the existing
25698/// argument-driven typing (the one `date_trunc` uses) reach the right
25699/// answer instead of teaching the type layer a second rule. MySQL's
25700/// DATE_ADD is a different function returning DATE or DATETIME, so the
25701/// caller applies this in PG dialect only.
25702///
25703/// A free function, and not a block at the call site, because the caller
25704/// is on the recursive-descent frame chain.
25705#[inline(never)]
25706fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
25707 if args.len() != 2
25708 || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
25709 {
25710 return;
25711 }
25712 let base = args.remove(0);
25713 args.insert(
25714 0,
25715 Expr::Cast {
25716 expr: Box::new(base),
25717 target: CastTarget::Timestamptz,
25718 },
25719 );
25720}
25721
25722/// v6.8.2 — walk an expression tree and return the first column
25723/// reference's bare name. Used by `parse_create_index_stmt_after_create`
25724/// to derive `CreateIndexStatement.column` from an expression
25725/// key (so downstream planner code resolving a primary column
25726/// position keeps working with expression indexes). Returns
25727/// `None` when the expression has no column ref at all — caller
25728/// surfaces that as a parse error.
25729fn extract_first_column(expr: &Expr) -> Option<String> {
25730 match expr {
25731 Expr::Column(cn) => Some(cn.name.clone()),
25732 Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
25733 Expr::Binary { lhs, rhs, .. } => {
25734 extract_first_column(lhs).or_else(|| extract_first_column(rhs))
25735 }
25736 Expr::Unary { expr: e, .. } => extract_first_column(e),
25737 // v7.39 (read01 round 93) — a cast wraps its operand: a common
25738 // expression-index key is `lower(col::text)`, where the column
25739 // sits under the `::text` cast inside the function arg. Without
25740 // descending here the key was rejected as "references no column".
25741 Expr::Cast { expr: e, .. } => extract_first_column(e),
25742 _ => None,
25743 }
25744}
25745
25746fn maybe_not(expr: Expr, negated: bool) -> Expr {
25747 if negated {
25748 Expr::Unary {
25749 op: UnOp::Not,
25750 expr: Box::new(expr),
25751 }
25752 } else {
25753 expr
25754 }
25755}
25756
25757/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
25758/// things in the two dialects, and SPG read all three PG's way:
25759///
25760/// | token | PG (and SPG) | MySQL, measured |
25761/// |---|---|---|
25762/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
25763/// | `&&` | inet / array overlap | **AND** |
25764/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
25765///
25766/// `1 || 0` answering the string '10' on a MySQL session is a wrong
25767/// answer with no error, which is why they are routed here rather than
25768/// left to the shared table.
25769impl Parser {
25770 fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
25771 if self.mysql_dialect {
25772 // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
25773 // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
25774 // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
25775 if let Token::Ident(w) = tok
25776 && w.eq_ignore_ascii_case("div")
25777 {
25778 return Some((BinOp::IntDiv, 8));
25779 }
25780 // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
25781 // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
25782 // the lexer; the `MOD(x, y)` function form is unaffected (MOD
25783 // there sits in operand position, not infix).
25784 if let Token::Ident(w) = tok
25785 && w.eq_ignore_ascii_case("mod")
25786 {
25787 return Some((BinOp::Mod, 8));
25788 }
25789 // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
25790 // plain ident to the lexer. Its precedence sits between OR (1)
25791 // and AND (3) — hence rung 2, the slot freed by moving AND up.
25792 if let Token::Ident(w) = tok
25793 && w.eq_ignore_ascii_case("xor")
25794 {
25795 return Some((BinOp::LogicalXor, 2));
25796 }
25797 match tok {
25798 Token::Concat => return Some((BinOp::Or, 1)),
25799 // MySQL's `&&` is logical AND, sharing AND's rung (3).
25800 Token::InetOverlap => return Some((BinOp::And, 3)),
25801 // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
25802 Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
25803 _ => {}
25804 }
25805 }
25806 binop_from(tok)
25807 }
25808}
25809
25810// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
25811// (which sits strictly between OR and AND), every level from AND upward was
25812// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
25813// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
25814// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
25815// the *relative* order of every PG operator is unchanged by the shift.
25816fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
25817 let pair = match tok {
25818 Token::Or => (BinOp::Or, 1),
25819 Token::And => (BinOp::And, 3),
25820 Token::Eq => (BinOp::Eq, 5),
25821 Token::NotEq => (BinOp::NotEq, 5),
25822 Token::Lt => (BinOp::Lt, 5),
25823 Token::LtEq => (BinOp::LtEq, 5),
25824 Token::Gt => (BinOp::Gt, 5),
25825 Token::GtEq => (BinOp::GtEq, 5),
25826 // pgvector distance ops all sit on the same rung — tighter than
25827 // comparisons (5) so `col <-> v < threshold` parses correctly.
25828 Token::L2Distance => (BinOp::L2Distance, 6),
25829 // v7.39 (read01 geo_ops.c) — geometric predicates ride the
25830 // comparison rung.
25831 Token::GeomParallel => (BinOp::GeomParallel, 5),
25832 // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
25833 // comparison rung.
25834 Token::OverLeft => (BinOp::OverLeft, 5),
25835 Token::OverRight => (BinOp::OverRight, 5),
25836 Token::GeomPerp => (BinOp::GeomPerp, 5),
25837 Token::GeomSameAs => (BinOp::GeomSameAs, 5),
25838 Token::ClosestPoint => (BinOp::ClosestPoint, 6),
25839 Token::GeomHoriz => (BinOp::GeomHoriz, 5),
25840 Token::InnerProduct => (BinOp::InnerProduct, 6),
25841 Token::CosineDistance => (BinOp::CosineDistance, 6),
25842 Token::Plus => (BinOp::Add, 7),
25843 Token::Minus => (BinOp::Sub, 7),
25844 // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
25845 // binds every "other" operator (`||`, `|`, `&`, `#`, the
25846 // pgvector distances above) BETWEEN additive (7) and the
25847 // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
25848 // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
25849 // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
25850 // ("matches PG conceptually" — the round-753 audit measured it
25851 // false; the old rung errored on `'a' || 1 + 1` with
25852 // `text + integer`). Same-level chains left-fold, as PG does.
25853 Token::Concat => (BinOp::Concat, 6),
25854 Token::Pipe => (BinOp::BitOr, 6),
25855 Token::Amp => (BinOp::BitAnd, 6),
25856 Token::Star => (BinOp::Mul, 8),
25857 Token::Slash => (BinOp::Div, 8),
25858 Token::Percent => (BinOp::Mod, 8),
25859 // v4.14: JSON path ops bind tighter than comparisons (5)
25860 // and additive (7) so `doc->'k' = 'v'` parses correctly.
25861 // Same rung as the multiplicative ops.
25862 Token::JsonGet => (BinOp::JsonGet, 8),
25863 Token::JsonGetText => (BinOp::JsonGetText, 8),
25864 Token::JsonGetPath => (BinOp::JsonGetPath, 8),
25865 Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
25866 Token::JsonContains => (BinOp::JsonContains, 8),
25867 Token::JsonPathExists => (BinOp::JsonPathExists, 8),
25868 Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
25869 Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
25870 Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
25871 Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
25872 Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
25873 // v7.12.2 — `@@` binds at the comparison rung (looser than
25874 // arithmetic, tighter than AND / OR). PG places `@@` at
25875 // the same precedence as `=` / `<`, so we follow.
25876 Token::TsMatch => (BinOp::TsMatch, 5),
25877 // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
25878 // PG places these at the comparison rung (same level as `=`),
25879 // so we follow.
25880 Token::InetContainedBy => (BinOp::InetContainedBy, 5),
25881 Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
25882 Token::InetContains => (BinOp::InetContains, 5),
25883 Token::InetContainsEq => (BinOp::InetContainsEq, 5),
25884 Token::InetOverlap => (BinOp::InetOverlap, 5),
25885 // v7.39 (round 508) — the geometric and pattern-order predicates
25886 // ride the comparison rung, as every other predicate does.
25887 Token::Intersects => (BinOp::Intersects, 5),
25888 Token::IsBelow => (BinOp::IsBelow, 5),
25889 Token::IsAbove => (BinOp::IsAbove, 5),
25890 Token::PatternLt => (BinOp::PatternLt, 5),
25891 Token::PatternLtEq => (BinOp::PatternLtEq, 5),
25892 Token::PatternGt => (BinOp::PatternGt, 5),
25893 Token::PatternGtEq => (BinOp::PatternGtEq, 5),
25894 // `@@@` is the old spelling of `@@` and means exactly it.
25895 Token::TsMatchOld => (BinOp::TsMatch, 5),
25896 _ => return None,
25897 };
25898 Some(pair)
25899}
25900
25901#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
25902// `as f32` here is intentional: vector elements widen / narrow into f32 on
25903// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
25904// past ~15 decimal digits — both are acceptable for a fixed-precision
25905// pgvector column.
25906/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
25907/// implicit table alias and break trailing clauses. WITH lands
25908/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
25909/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
25910/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
25911/// / VALUES / FOR / LATERAL — all of which would otherwise be
25912/// silently swallowed by `parse_optional_alias`.
25913fn is_alias_stopword(s: &str) -> bool {
25914 matches!(
25915 s.to_ascii_lowercase().as_str(),
25916 "with"
25917 | "on"
25918 | "where"
25919 | "having"
25920 | "group"
25921 | "order"
25922 | "limit"
25923 | "offset"
25924 | "union"
25925 | "except"
25926 | "intersect"
25927 | "returning"
25928 | "set"
25929 | "values"
25930 | "for"
25931 | "window"
25932 | "tablesample"
25933 | "lateral"
25934 | "left"
25935 | "right"
25936 | "inner"
25937 | "outer"
25938 | "full"
25939 | "cross"
25940 | "join"
25941 | "natural"
25942 | "using"
25943 | "fetch"
25944 )
25945}
25946
25947fn extract_numeric_literal(e: &Expr) -> Option<f32> {
25948 match e {
25949 Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
25950 Expr::Literal(Literal::Float(x)) => Some(*x as f32),
25951 // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
25952 // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
25953 // so scale the divisor by hand instead of `f32::powi`.)
25954 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
25955 let mut div = 1.0f32;
25956 for _ in 0..*scale {
25957 div *= 10.0;
25958 }
25959 Some(*unscaled as f32 / div)
25960 }
25961 Expr::Unary {
25962 op: UnOp::Neg,
25963 expr,
25964 } => extract_numeric_literal(expr).map(|x| -x),
25965 _ => None,
25966 }
25967}
25968
25969/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
25970/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
25971/// negative. Returns `None` if any pair fails to parse or no pair is found.
25972///
25973/// Recognised units (case-insensitive, optional trailing `s`):
25974/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
25975/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
25976/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
25977/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
25978/// (PG-canonical: DST and month-boundary semantics depend on this).
25979/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
25980/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
25981/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
25982#[allow(clippy::cast_possible_truncation)]
25983fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
25984 let mut months: i64 = 0;
25985 let mut days: i64 = 0;
25986 let mut micros: i64 = 0;
25987 let mut in_time = false;
25988 let mut num = alloc::string::String::new();
25989 for ch in rest.chars() {
25990 if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
25991 num.push(ch);
25992 continue;
25993 }
25994 if ch == 'T' || ch == 't' {
25995 if !num.is_empty() {
25996 return None;
25997 }
25998 in_time = true;
25999 continue;
26000 }
26001 let n: f64 = num.parse().ok()?;
26002 num.clear();
26003 match (ch, in_time) {
26004 ('Y' | 'y', false) => months += (n * 12.0) as i64,
26005 ('M', false) => months += n as i64,
26006 ('W' | 'w', false) => days += (n * 7.0) as i64,
26007 ('D' | 'd', false) => days += n as i64,
26008 ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26009 ('M', true) => micros += (n * 60_000_000.0) as i64,
26010 ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26011 _ => return None,
26012 }
26013 }
26014 if !num.is_empty() {
26015 return None;
26016 }
26017 Some((
26018 i32::try_from(months).ok()?,
26019 i32::try_from(days).ok()?,
26020 micros,
26021 ))
26022}
26023
26024/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
26025/// leading `-` negates the whole value). Rejects date-like strings.
26026fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
26027 let (neg, body) = match s.strip_prefix('-') {
26028 Some(b) => (true, b),
26029 None => (false, s),
26030 };
26031 let (y, m) = body.split_once('-')?;
26032 let years: i32 = y.parse().ok()?;
26033 let mons: i32 = m.parse().ok()?;
26034 if years < 0 || mons < 0 {
26035 return None;
26036 }
26037 let total = years.checked_mul(12)?.checked_add(mons)?;
26038 Some((if neg { -total } else { total }, 0, 0))
26039}
26040
26041/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
26042/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
26043fn parse_interval_clock(tok: &str) -> Option<i64> {
26044 let (neg, body) = match tok.strip_prefix('-') {
26045 Some(r) => (true, r),
26046 None => (false, tok.strip_prefix('+').unwrap_or(tok)),
26047 };
26048 let mut it = body.split(':');
26049 let h: i64 = it.next()?.parse().ok()?;
26050 let m: i64 = it.next()?.parse().ok()?;
26051 let s_tok = it.next().unwrap_or("0");
26052 if it.next().is_some() {
26053 return None;
26054 }
26055 let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
26056 let sec: i64 = sec.parse().ok()?;
26057 let mut f = alloc::string::String::from(frac);
26058 while f.len() < 6 {
26059 f.push('0');
26060 }
26061 f.truncate(6);
26062 let fus: i64 = f.parse().ok()?;
26063 sec.checked_mul(1_000_000)?.checked_add(fus)?
26064 } else {
26065 s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
26066 };
26067 let total = h
26068 .checked_mul(3_600_000_000)?
26069 .checked_add(m.checked_mul(60_000_000)?)?
26070 .checked_add(sec_us)?;
26071 Some(if neg { -total } else { total })
26072}
26073
26074/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
26075/// every spelling PG accepts (measured against live PG18.4, not guessed):
26076/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
26077/// Before this, the unit table matched long names only, with an ad-hoc
26078/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
26079/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
26080/// INTERVAL", and it had also grown arms for the debris that stripping leaves
26081/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
26082/// fractional) both read from this one table now.
26083fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
26084 let u = raw.to_ascii_lowercase();
26085 Some(match u.as_str() {
26086 "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
26087 "microsecond"
26088 }
26089 "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
26090 "millisecond"
26091 }
26092 "second" | "seconds" | "sec" | "secs" | "s" => "second",
26093 "minute" | "minutes" | "min" | "mins" | "m" => "minute",
26094 "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
26095 "day" | "days" | "d" => "day",
26096 "week" | "weeks" | "w" => "week",
26097 "month" | "months" | "mon" | "mons" => "month",
26098 "year" | "years" | "yr" | "yrs" | "y" => "year",
26099 "decade" | "decades" | "dec" | "decs" => "decade",
26100 "century" | "centuries" | "cent" | "c" => "century",
26101 "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
26102 _ => return None,
26103 })
26104}
26105
26106/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
26107/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
26108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26109pub(crate) enum IntervalField {
26110 Year,
26111 Month,
26112 Day,
26113 Hour,
26114 Minute,
26115 Second,
26116}
26117
26118/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
26119/// spellings aren't standard for the qualifier position, so only the singular
26120/// forms are accepted.
26121/// v7.39 (round 350, M7) — MySQL's interval units, measured against
26122/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
26123/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
26124/// take a `'1 2'` style literal — are not read here; they stay a parse
26125/// error rather than being silently misread.)
26126/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
26127///
26128/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
26129/// to do with a `@@` engine setting, and an unset one reads NULL rather
26130/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
26131/// were the same node and `SELECT @x` answered "Unknown system variable".)
26132/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
26133/// not see a session override — measured, after `SET autocommit=0`,
26134/// `@@global.autocommit` is still 1.
26135///
26136/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
26137/// the parser's nesting budget is tuned against, and building these
26138/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
26139/// wall `parse_left_right_atom` and friends were factored out for).
26140#[inline(never)]
26141fn variable_ref_atom(raw: &str) -> Expr {
26142 let user_var = !raw.starts_with("@@");
26143 let bare = raw.trim_start_matches('@').to_ascii_lowercase();
26144 Expr::FunctionCall {
26145 name: String::from(if user_var {
26146 "__spg_user_var"
26147 } else {
26148 "__spg_session_var"
26149 }),
26150 args: alloc::vec![Expr::Literal(Literal::String(bare))],
26151 }
26152}
26153
26154fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
26155 let Token::Ident(s) = tok else { return None };
26156 Some(match () {
26157 () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
26158 () if s.eq_ignore_ascii_case("second") => "second",
26159 () if s.eq_ignore_ascii_case("minute") => "minute",
26160 () if s.eq_ignore_ascii_case("hour") => "hour",
26161 () if s.eq_ignore_ascii_case("day") => "day",
26162 () if s.eq_ignore_ascii_case("week") => "week",
26163 () if s.eq_ignore_ascii_case("month") => "month",
26164 () if s.eq_ignore_ascii_case("quarter") => "quarter",
26165 () if s.eq_ignore_ascii_case("year") => "year",
26166 () => return None,
26167 })
26168}
26169
26170/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
26171/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
26172/// which constructs the value at run time. Only the slot the unit names
26173/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
26174/// slot the builtin has (months and fractional seconds respectively).
26175fn make_interval_call(qty: Expr, unit: &str) -> Expr {
26176 let zero = || Expr::Literal(Literal::Integer(0));
26177 let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
26178 lhs: alloc::boxed::Box::new(qty.clone()),
26179 op,
26180 rhs: alloc::boxed::Box::new(by),
26181 };
26182 // (years, months, weeks, days, hours, mins, secs)
26183 let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
26184 match unit {
26185 "year" => args[0] = qty,
26186 "quarter" => {
26187 args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
26188 }
26189 "month" => args[1] = qty,
26190 "week" => args[2] = qty,
26191 "day" => args[3] = qty,
26192 "hour" => args[4] = qty,
26193 "minute" => args[5] = qty,
26194 "second" => args[6] = qty,
26195 // The builtin's seconds slot takes a fraction, so microseconds ride
26196 // it scaled down; the divisor is a NUMERIC literal so the division
26197 // stays exact rather than going through a float.
26198 "microsecond" => {
26199 args[6] = scaled(
26200 crate::ast::BinOp::Div,
26201 Expr::Literal(Literal::Numeric {
26202 unscaled: 1_000_000,
26203 scale: 0,
26204 }),
26205 );
26206 }
26207 _ => args[3] = qty,
26208 }
26209 Expr::FunctionCall {
26210 name: alloc::string::String::from("make_interval"),
26211 args,
26212 }
26213}
26214
26215/// `(count, unit)` → `(months, days, micros)`.
26216fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
26217 let n: i64 = count.trim().parse().ok()?;
26218 Some(match unit {
26219 "microsecond" => (0, 0, n),
26220 "second" => (0, 0, n.checked_mul(1_000_000)?),
26221 "minute" => (0, 0, n.checked_mul(60_000_000)?),
26222 "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
26223 "day" => (0, i32::try_from(n).ok()?, 0),
26224 "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
26225 "month" => (i32::try_from(n).ok()?, 0, 0),
26226 "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
26227 "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
26228 _ => return None,
26229 })
26230}
26231
26232fn interval_field_of(tok: &Token) -> Option<IntervalField> {
26233 let Token::Ident(s) = tok else { return None };
26234 Some(match () {
26235 () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
26236 () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
26237 () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
26238 () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
26239 () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
26240 () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
26241 () => return None,
26242 })
26243}
26244
26245/// v7.39 (read01 round 102) — interpret an interval literal under a field
26246/// qualifier. Returns `(months, days, micros)`.
26247///
26248/// * A single field applied to a bare number sets which unit the number means,
26249/// truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
26250/// SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
26251/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
26252/// * Every other range, and any literal a single field can't read as a plain
26253/// number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
26254/// interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
26255/// like PG, and the qualifier there only bounds precision.
26256fn interpret_qualified_interval(
26257 text: &str,
26258 (f1, f2): (IntervalField, Option<IntervalField>),
26259) -> Option<(i32, i32, i64)> {
26260 if let Some(f2) = f2 {
26261 if f1 == IntervalField::Year && f2 == IntervalField::Month {
26262 if let Some(m) = parse_year_month_literal(text) {
26263 return Some((m, 0, 0));
26264 }
26265 }
26266 return parse_interval_text(text);
26267 }
26268 // Single field: reinterpret a bare number; otherwise the default parse.
26269 let trimmed = text.trim();
26270 if let Ok(val) = trimmed.parse::<f64>() {
26271 // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
26272 #[allow(clippy::cast_possible_truncation)]
26273 let whole = val as i64;
26274 #[allow(clippy::cast_possible_truncation)]
26275 let secs_micros = {
26276 let m = val * 1_000_000.0;
26277 if m >= 0.0 {
26278 (m + 0.5) as i64
26279 } else {
26280 (m - 0.5) as i64
26281 }
26282 };
26283 return Some(match f1 {
26284 IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
26285 IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
26286 IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
26287 IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
26288 IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
26289 IntervalField::Second => (0, 0, secs_micros),
26290 });
26291 }
26292 parse_interval_text(text)
26293}
26294
26295/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
26296fn parse_year_month_literal(text: &str) -> Option<i32> {
26297 let t = text.trim();
26298 let (neg, body) = match t.strip_prefix('-') {
26299 Some(r) => (true, r),
26300 None => (false, t.strip_prefix('+').unwrap_or(t)),
26301 };
26302 let mut it = body.split('-');
26303 let years: i32 = it.next()?.trim().parse().ok()?;
26304 let months: i32 = match it.next() {
26305 Some(m) => m.trim().parse().ok()?,
26306 None => 0,
26307 };
26308 if it.next().is_some() {
26309 return None;
26310 }
26311 let total = years.checked_mul(12)?.checked_add(months)?;
26312 Some(if neg { -total } else { total })
26313}
26314
26315pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
26316 // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
26317 // `@` is decorative; a trailing `ago` negates the whole interval.
26318 let mut trimmed = s.trim();
26319 trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
26320 let mut negate = false;
26321 if let Some(rest) = trimmed
26322 .strip_suffix("ago")
26323 .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
26324 {
26325 negate = true;
26326 trimmed = rest.trim();
26327 }
26328 let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
26329 let (mo, d, us) = v?;
26330 if negate {
26331 Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
26332 } else {
26333 Some((mo, d, us))
26334 }
26335 };
26336 let s = trimmed;
26337 // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
26338 // are single tokens, not the `<n> <unit>` pair form handled below.
26339 if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
26340 return finish(parse_iso8601_interval(rest));
26341 }
26342 if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
26343 if let Some(iv) = parse_year_month_interval(trimmed) {
26344 return finish(Some(iv));
26345 }
26346 }
26347 // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
26348 // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
26349 // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
26350 if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
26351 if let Ok(n) = trimmed.parse::<i64>() {
26352 return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
26353 }
26354 if let Ok(f) = trimmed.parse::<f64>() {
26355 if f.is_finite() {
26356 #[allow(clippy::cast_possible_truncation)]
26357 return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
26358 }
26359 }
26360 }
26361 // v7.39 (round 243) — PG accepts the number and unit run together
26362 // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
26363 // the `<n> <unit>` pair loop below sees them as two.
26364 let raw_parts: Vec<&str> = s.split_whitespace().collect();
26365 let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
26366 for p in raw_parts {
26367 let boundary = p
26368 .char_indices()
26369 .find(|(i, c)| {
26370 *i > 0
26371 && c.is_ascii_alphabetic()
26372 && p[..*i]
26373 .chars()
26374 .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
26375 && p[..*i].chars().any(|d| d.is_ascii_digit())
26376 })
26377 .map(|(i, _)| i);
26378 match boundary {
26379 Some(i) => {
26380 parts.push(&p[..i]);
26381 parts.push(&p[i..]);
26382 }
26383 None => parts.push(p),
26384 }
26385 }
26386 // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
26387 // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
26388 // remains is the `<n> <unit>` pair form handled below.
26389 let mut clock_us: i64 = 0;
26390 let mut had_clock = false;
26391 if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
26392 clock_us = parse_interval_clock(parts[pos])?;
26393 parts.remove(pos);
26394 had_clock = true;
26395 }
26396 // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
26397 // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
26398 let mut lone_days: i32 = 0;
26399 if had_clock && parts.len() == 1 {
26400 if let Ok(n) = parts[0].parse::<i64>() {
26401 lone_days = i32::try_from(n).ok()?;
26402 parts.clear();
26403 }
26404 }
26405 if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
26406 return None;
26407 }
26408 let mut months: i32 = 0;
26409 let mut days: i32 = lone_days;
26410 let mut micros: i64 = clock_us;
26411 let mut i = 0;
26412 while i < parts.len() {
26413 let unit_stripped = canonical_interval_unit(parts[i + 1])?;
26414 if let Ok(n) = parts[i].parse::<i64>() {
26415 match unit_stripped {
26416 "microsecond" => micros = micros.checked_add(n)?,
26417 "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
26418 "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
26419 "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
26420 "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
26421 "day" => {
26422 let n32 = i32::try_from(n).ok()?;
26423 days = days.checked_add(n32)?;
26424 }
26425 "week" => {
26426 let n32 = i32::try_from(n).ok()?;
26427 days = days.checked_add(n32.checked_mul(7)?)?;
26428 }
26429 "month" => {
26430 let n32 = i32::try_from(n).ok()?;
26431 months = months.checked_add(n32)?;
26432 }
26433 "year" => {
26434 let n32 = i32::try_from(n).ok()?;
26435 months = months.checked_add(n32.checked_mul(12)?)?;
26436 }
26437 // v7.39 (read01 timestamp.c) — the larger calendar units.
26438 "decade" => {
26439 let n32 = i32::try_from(n).ok()?;
26440 months = months.checked_add(n32.checked_mul(120)?)?;
26441 }
26442 "century" => {
26443 let n32 = i32::try_from(n).ok()?;
26444 months = months.checked_add(n32.checked_mul(1200)?)?;
26445 }
26446 "millennium" => {
26447 let n32 = i32::try_from(n).ok()?;
26448 months = months.checked_add(n32.checked_mul(12000)?)?;
26449 }
26450 _ => return None,
26451 }
26452 } else if let Ok(f) = parts[i].parse::<f64>() {
26453 // Fractional units cascade down to the next-finer field the way
26454 // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
26455 // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
26456 // no_std: f64 has no trunc/fract/round methods, so do them with
26457 // casts (toward-zero) + explicit round-half-away-from-zero.
26458 #[allow(clippy::cast_possible_truncation)]
26459 fn round_i64(x: f64) -> i64 {
26460 if x >= 0.0 {
26461 (x + 0.5) as i64
26462 } else {
26463 (x - 0.5) as i64
26464 }
26465 }
26466 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26467 fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
26468 const DAY_US: f64 = 86_400_000_000.0;
26469 let whole = d as i64; // truncates toward zero
26470 let frac = d - whole as f64;
26471 *days = days.checked_add(i32::try_from(whole).ok()?)?;
26472 *micros = micros.checked_add(round_i64(frac * DAY_US))?;
26473 Some(())
26474 }
26475 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26476 match unit_stripped {
26477 "microsecond" => micros = micros.checked_add(round_i64(f))?,
26478 "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
26479 "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
26480 "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
26481 "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
26482 "day" => add_days_frac(&mut days, &mut micros, f)?,
26483 "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
26484 "month" => {
26485 let whole = f as i64;
26486 months = months.checked_add(i32::try_from(whole).ok()?)?;
26487 add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
26488 }
26489 "year" => {
26490 let m = f * 12.0;
26491 let whole = m as i64;
26492 months = months.checked_add(i32::try_from(whole).ok()?)?;
26493 add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
26494 }
26495 _ => return None,
26496 }
26497 } else {
26498 return None;
26499 }
26500 i += 2;
26501 }
26502 finish(Some((months, days, micros)))
26503}
26504
26505/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
26506/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
26507/// `interval` is intentionally absent (handled by its own parser arm).
26508/// Returns `None` for names that aren't sensible as a bare typed literal, so
26509/// the caller falls back to treating the ident as a column reference.
26510fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
26511 Some(match ident {
26512 "date" => CastTarget::Date,
26513 "timestamp" | "datetime" => CastTarget::Timestamp,
26514 "timestamptz" => CastTarget::Timestamptz,
26515 "bool" | "boolean" => CastTarget::Bool,
26516 "int" | "integer" | "int4" => CastTarget::Int,
26517 "bigint" | "int8" => CastTarget::BigInt,
26518 "float8" | "double precision" => CastTarget::Float,
26519 "uuid" => CastTarget::Uuid,
26520 "bytea" => CastTarget::Bytea,
26521 "json" => CastTarget::Json,
26522 "jsonb" => CastTarget::Jsonb,
26523 // Types without a dedicated CastTarget variant flow through the
26524 // generic Named path (engine resolves via column_type_to_data_type).
26525 "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
26526 | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
26527 | "money" | "bit" | "varbit"
26528 // Geometric types accept the `TYPE 'literal'` prefix spelling too.
26529 | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
26530 // Range / multirange types likewise.
26531 | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
26532 | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
26533 | "datemultirange" | "tsmultirange" | "tstzmultirange"
26534 // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
26535 | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
26536 CastTarget::Named(alloc::string::String::from(ident))
26537 }
26538 _ => return None,
26539 })
26540}
26541
26542/// v7.12.4 — map a bare type-name identifier (the form that
26543/// appears in a function arg list or RETURNS clause) to a
26544/// [`ColumnTypeName`]. Returns `None` for unknown / extension
26545/// types so the caller can preserve them as
26546/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
26547///
26548/// Subset of the full column-type grammar — we deliberately
26549/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
26550/// here because function-arg types in v7.12.4 are mostly the
26551/// bare form (`text`, `int`, `bytea`, …).
26552/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
26553/// than being `name TYPE`?
26554///
26555/// The multi-word spellings SQL allows for a bare argument type, each
26556/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
26557///
26558/// NOTE this list also exists in `spg-storage`, which computes the
26559/// signature key from the rendered argument text and has to reach the
26560/// same verdict. The two crates are siblings — neither depends on the
26561/// other — and each already carries its own table of type spellings
26562/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
26563/// there), so this follows the structure rather than inventing new
26564/// duplication. Recorded as V49.
26565pub fn is_multiword_type_phrase(phrase: &str) -> bool {
26566 let t = phrase.trim().to_ascii_lowercase();
26567 let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
26568 matches!(
26569 base,
26570 "double precision"
26571 | "character varying"
26572 | "bit varying"
26573 | "timestamp with time zone"
26574 | "timestamp without time zone"
26575 | "time with time zone"
26576 | "time without time zone"
26577 | "national character"
26578 | "national character varying"
26579 )
26580}
26581
26582fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
26583 Some(match ident.to_ascii_lowercase().as_str() {
26584 "smallint" | "tinyint" => ColumnTypeName::SmallInt,
26585 "int" | "integer" | "mediumint" => ColumnTypeName::Int,
26586 "bigint" => ColumnTypeName::BigInt,
26587 "float" | "double" => ColumnTypeName::Float,
26588 // v7.39 (round 269) — real is 32-bit.
26589 "real" | "float4" => ColumnTypeName::Real,
26590 "text" => ColumnTypeName::Text,
26591 "bool" | "boolean" => ColumnTypeName::Bool,
26592 "date" => ColumnTypeName::Date,
26593 "timestamp" | "datetime" => ColumnTypeName::Timestamp,
26594 "timestamptz" => ColumnTypeName::Timestamptz,
26595 "json" => ColumnTypeName::Json,
26596 "jsonb" => ColumnTypeName::Jsonb,
26597 "bytea" | "bytes" => ColumnTypeName::Bytes,
26598 "tsvector" => ColumnTypeName::TsVector,
26599 "tsquery" => ColumnTypeName::TsQuery,
26600 "uuid" => ColumnTypeName::Uuid,
26601 "interval" => ColumnTypeName::Interval,
26602 "time" => ColumnTypeName::Time,
26603 "year" => ColumnTypeName::Year,
26604 "timetz" => ColumnTypeName::TimeTz,
26605 "money" => ColumnTypeName::Money,
26606 _ => return None,
26607 })
26608}
26609
26610/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
26611/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
26612///
26613/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
26614/// / embedded SQL land in v7.12.5+):
26615///
26616/// ```text
26617/// body := [ws] block [ws]
26618/// block := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
26619/// stmt := assign | return
26620/// assign := assign_target := expr
26621/// assign_target := ( NEW | OLD ) . ident | ident
26622/// return := RETURN ( NEW | OLD | NULL | expr )
26623/// ```
26624///
26625/// `expr` is parsed by recursing into the regular `Parser` — so a
26626/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
26627/// NEW.subject || ' ' || NEW.sender)` body shape works without
26628/// the body parser knowing what `to_tsvector` is.
26629///
26630/// Errors here cause the caller to fall back to
26631/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
26632/// successful, but the executor will refuse to invoke the
26633/// function with an "unparseable body" error.
26634/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
26635/// from the crate root as `spg_sql::parse_function_body`.
26636pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26637 parse_plpgsql_body(body)
26638}
26639
26640fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26641 // Use the regular lexer on the body text. The trailing
26642 // `END;` may or may not have a semicolon; the lexer treats
26643 // both forms identically.
26644 let tokens = lexer::tokenize(body).map_err(|e| ParseError {
26645 message: alloc::format!("plpgsql body lex error: {e}"),
26646 token_pos: 0,
26647 })?;
26648 let mut parser = Parser::new(tokens);
26649 parser.parse_plpgsql_block()
26650}
26651
26652/// v7.39 (GUC) — the textual body of a SET value, for list joining.
26653fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
26654 match v {
26655 crate::ast::SetValue::String(s)
26656 | crate::ast::SetValue::Ident(s)
26657 | crate::ast::SetValue::Number(s) => s.clone(),
26658 crate::ast::SetValue::Default => "DEFAULT".into(),
26659 }
26660}
26661
26662/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
26663/// contains an aggregate call at ITS OWN query level (recursion stops at
26664/// sublink boundaries — a sublink's aggregates belong to the sublink).
26665/// Backs the "aggregate functions are not allowed in a recursive query's
26666/// recursive term" well-formedness check.
26667fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
26668 const AGG_NAMES: &[&str] = &[
26669 "count",
26670 "sum",
26671 "min",
26672 "max",
26673 "avg",
26674 "string_agg",
26675 "array_agg",
26676 "bool_and",
26677 "bool_or",
26678 "every",
26679 "any_value",
26680 "json_agg",
26681 "jsonb_agg",
26682 "json_object_agg",
26683 "jsonb_object_agg",
26684 "bit_and",
26685 "bit_or",
26686 "bit_xor",
26687 "var_pop",
26688 "var_samp",
26689 "variance",
26690 "stddev",
26691 "stddev_pop",
26692 "stddev_samp",
26693 "range_agg",
26694 "range_intersect_agg",
26695 "percentile_cont",
26696 "percentile_disc",
26697 "mode",
26698 "corr",
26699 "covar_pop",
26700 "covar_samp",
26701 ];
26702 match e {
26703 Expr::AggregateOrdered { .. } => true,
26704 Expr::FunctionCall { name, args } => {
26705 AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
26706 || args.iter().any(expr_has_toplevel_aggregate)
26707 }
26708 Expr::NamedArg { expr, .. }
26709 | Expr::Variadic(expr)
26710 | Expr::Unary { expr, .. }
26711 | Expr::Cast { expr, .. }
26712 | Expr::IsNull { expr, .. }
26713 | Expr::FieldAccess { base: expr, .. }
26714 | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
26715 Expr::Binary { lhs, rhs, .. } => {
26716 expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
26717 }
26718 Expr::Like { expr, pattern, .. } => {
26719 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
26720 }
26721 Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
26722 Expr::InList { expr, list, .. } => {
26723 expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
26724 }
26725 Expr::ArraySubscript { target, index } => {
26726 expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
26727 }
26728 Expr::ArraySlice { target, lo, hi } => {
26729 expr_has_toplevel_aggregate(target)
26730 || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
26731 || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
26732 }
26733 Expr::AnyAll { expr, array, .. } => {
26734 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
26735 }
26736 Expr::Case {
26737 operand,
26738 branches,
26739 else_branch,
26740 } => {
26741 operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
26742 || branches
26743 .iter()
26744 .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
26745 || else_branch
26746 .as_deref()
26747 .is_some_and(expr_has_toplevel_aggregate)
26748 }
26749 // The outer-level operands of a sublink can aggregate; the sublink's
26750 // own body cannot leak its aggregates up here.
26751 Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
26752 Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
26753 row.iter().any(expr_has_toplevel_aggregate)
26754 }
26755 _ => false,
26756 }
26757}
26758
26759/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
26760/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
26761/// named table anywhere in its subtree. A plain FROM derived table is NOT a
26762/// sublink and is legal in a recursive term, so it is not walked here.
26763fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
26764 let mut exprs: Vec<&Expr> = Vec::new();
26765 for it in &s.items {
26766 if let crate::ast::SelectItem::Expr { expr, .. } = it {
26767 exprs.push(expr);
26768 }
26769 }
26770 if let Some(w) = &s.where_ {
26771 exprs.push(w);
26772 }
26773 if let Some(h) = &s.having {
26774 exprs.push(h);
26775 }
26776 if let Some(g) = &s.group_by {
26777 exprs.extend(g.iter());
26778 }
26779 if let Some(from) = &s.from {
26780 for j in &from.joins {
26781 if let Some(on) = &j.on {
26782 exprs.push(on);
26783 }
26784 }
26785 }
26786 exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
26787}
26788
26789/// Does this expression contain a sublink whose subquery mentions `name`?
26790fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
26791 match e {
26792 Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
26793 Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
26794 Expr::InSubquery { expr, subquery, .. } => {
26795 expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
26796 }
26797 Expr::RowInSubquery { row, subquery, .. } => {
26798 row.iter().any(|x| expr_sublink_mentions(x, name))
26799 || select_mentions_table(subquery, name)
26800 }
26801 Expr::RowCmpSubquery { row, subquery, .. } => {
26802 row.iter().any(|x| expr_sublink_mentions(x, name))
26803 || select_mentions_table(subquery, name)
26804 }
26805 Expr::NamedArg { expr, .. }
26806 | Expr::Variadic(expr)
26807 | Expr::Unary { expr, .. }
26808 | Expr::Cast { expr, .. }
26809 | Expr::IsNull { expr, .. }
26810 | Expr::FieldAccess { base: expr, .. }
26811 | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
26812 Expr::Binary { lhs, rhs, .. } => {
26813 expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
26814 }
26815 Expr::Like { expr, pattern, .. } => {
26816 expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
26817 }
26818 Expr::FunctionCall { args, .. } | Expr::Array(args) => {
26819 args.iter().any(|x| expr_sublink_mentions(x, name))
26820 }
26821 Expr::InList { expr, list, .. } => {
26822 expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
26823 }
26824 Expr::ArraySubscript { target, index } => {
26825 expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
26826 }
26827 Expr::ArraySlice { target, lo, hi } => {
26828 expr_sublink_mentions(target, name)
26829 || lo
26830 .as_deref()
26831 .is_some_and(|x| expr_sublink_mentions(x, name))
26832 || hi
26833 .as_deref()
26834 .is_some_and(|x| expr_sublink_mentions(x, name))
26835 }
26836 Expr::AnyAll { expr, array, .. } => {
26837 expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
26838 }
26839 Expr::Case {
26840 operand,
26841 branches,
26842 else_branch,
26843 } => {
26844 operand
26845 .as_deref()
26846 .is_some_and(|x| expr_sublink_mentions(x, name))
26847 || branches
26848 .iter()
26849 .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
26850 || else_branch
26851 .as_deref()
26852 .is_some_and(|x| expr_sublink_mentions(x, name))
26853 }
26854 _ => false,
26855 }
26856}
26857
26858/// Does this SELECT (in full — FROM tables, derived tables, its own
26859/// sublinks, and union arms) mention the named table?
26860fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
26861 if let Some(from) = &s.from {
26862 if from.primary.name.eq_ignore_ascii_case(name) {
26863 return true;
26864 }
26865 if let Some(sub) = &from.primary.lateral_subquery
26866 && select_mentions_table(sub, name)
26867 {
26868 return true;
26869 }
26870 for j in &from.joins {
26871 if j.table.name.eq_ignore_ascii_case(name) {
26872 return true;
26873 }
26874 if let Some(sub) = &j.table.lateral_subquery
26875 && select_mentions_table(sub, name)
26876 {
26877 return true;
26878 }
26879 }
26880 }
26881 if select_has_self_ref_in_sublink(s, name) {
26882 return true;
26883 }
26884 s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
26885}
26886
26887/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
26888/// row count, the way PG evaluates one before applying it.
26889///
26890/// `None` = not a constant (a column, a subquery, a function call).
26891/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
26892/// message stands in for LIMIT / OFFSET, which the caller substitutes.
26893/// All wordings were read off live PG 18.4.
26894fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
26895 use crate::ast::{BinOp, Expr, Literal, UnOp};
26896 match e {
26897 Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
26898 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26899 Some(Ok(round_scaled_half_away(*unscaled, *scale)))
26900 }
26901 // PG coerces a string by its CONTENT, and fails on the value.
26902 Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
26903 |_| {
26904 Err(alloc::format!(
26905 "invalid input syntax for type bigint: \"{t}\""
26906 ))
26907 },
26908 |n| Ok(i128::from(n)),
26909 )),
26910 Expr::Literal(Literal::Bool(_)) => Some(Err(
26911 "argument of {L} must be type bigint, not type boolean".into(),
26912 )),
26913 Expr::Unary {
26914 op: UnOp::Neg,
26915 expr,
26916 } => match fold_limit_constant(expr)? {
26917 Ok(v) => Some(Ok(-v)),
26918 e @ Err(_) => Some(e),
26919 },
26920 Expr::Binary { lhs, op, rhs } => {
26921 let a = match fold_limit_constant(lhs)? {
26922 Ok(v) => v,
26923 e @ Err(_) => return Some(e),
26924 };
26925 let b = match fold_limit_constant(rhs)? {
26926 Ok(v) => v,
26927 e @ Err(_) => return Some(e),
26928 };
26929 let out = match op {
26930 BinOp::Add => a.checked_add(b),
26931 BinOp::Sub => a.checked_sub(b),
26932 BinOp::Mul => a.checked_mul(b),
26933 BinOp::Div if b != 0 => a.checked_div(b),
26934 BinOp::Div => return Some(Err("division by zero".into())),
26935 BinOp::Mod if b != 0 => a.checked_rem(b),
26936 BinOp::Mod => return Some(Err("division by zero".into())),
26937 _ => return None,
26938 };
26939 // PG evaluates the arithmetic in the operand's own type, so an
26940 // int-by-int product that leaves int range fails there — before
26941 // the row count is ever looked at.
26942 match out {
26943 Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
26944 Some(Err("integer out of range".into()))
26945 }
26946 Some(v) => Some(Ok(v)),
26947 None => Some(Err("integer out of range".into())),
26948 }
26949 }
26950 _ => None,
26951 }
26952}
26953
26954/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
26955/// cast, which is what makes `LIMIT 2.5` keep three rows.
26956fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
26957 if scale == 0 {
26958 return unscaled;
26959 }
26960 let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
26961 return 0;
26962 };
26963 let neg = unscaled < 0;
26964 let mag = unscaled.unsigned_abs() as i128;
26965 let rounded = (mag + div / 2) / div;
26966 if neg { -rounded } else { rounded }
26967}
26968
26969#[cfg(test)]
26970mod tests {
26971 use super::*;
26972 use alloc::string::ToString;
26973
26974 fn parse(s: &str) -> Statement {
26975 parse_statement(s).expect("parse ok")
26976 }
26977
26978 // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
26979 // `tables`, `partition`, etc. are unreserved keywords per PG's
26980 // `pg_get_keywords()` and MUST be usable as column / table /
26981 // alias names. Pre-T4 every drop-in user whose schema had one
26982 // of these as a column name (sentori events.release, mailrs
26983 // messages.index in some forks) blew the parser up at CREATE
26984 // TABLE time with "expected identifier, got Release". The
26985 // generalisation lives in `unreserved_keyword_text` + the
26986 // `expect_ident_like` and `parse_atom` arms that consult it.
26987 #[test]
26988 fn release_usable_as_column_name_in_create_table() {
26989 let stmt =
26990 parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
26991 if let Statement::CreateTable(t) = stmt {
26992 let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
26993 assert_eq!(names, alloc::vec!["id", "release", "payload"]);
26994 } else {
26995 panic!("expected CreateTable");
26996 }
26997 }
26998
26999 #[test]
27000 fn release_usable_as_column_ref_in_select_projection() {
27001 // The sentori `0003_partition_events.sql` INSERT-SELECT
27002 // walk references `release` in both column lists; the
27003 // projection-side use exercises `parse_atom`'s relaxed
27004 // identifier set.
27005 parse("SELECT id, release, payload FROM events WHERE id = 1");
27006 }
27007
27008 #[test]
27009 fn release_usable_as_column_ref_in_insert_column_list() {
27010 // INSERT INTO t (id, release, payload) VALUES (…)
27011 parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
27012 }
27013
27014 #[test]
27015 fn alter_column_drop_not_null_uses_keyword_drop_token() {
27016 // Sentori `0013_audit_tombstone.sql` issues
27017 // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
27018 // emits Token::Drop (not Ident("drop")); the parser must
27019 // accept both in the ALTER COLUMN sub-dispatch.
27020 parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
27021 }
27022
27023 #[test]
27024 fn create_index_accepts_parenthesised_expression_key() {
27025 // sentori `0040_events_bundle_idx.sql` shape — JSONB
27026 // expression index. Pre-T4 the parser bailed at the
27027 // inner `(` with "expected column ident or expression,
27028 // got LParen". The Token::LParen arm in CREATE INDEX
27029 // routes through the expression parser instead.
27030 parse(
27031 "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
27032 ON events ((payload->'bundle'->>'id'))",
27033 );
27034 }
27035
27036 // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
27037 // surface as parse errors, never stack overflows (embed hosts
27038 // abort on overflow).
27039 /// The nesting budget is a COUNT; what it has to fit inside is a
27040 /// number of BYTES, and only one of those two is stable across
27041 /// compiler versions. Round 847 measured 30,336 bytes per level
27042 /// after a toolchain move, which puts 64 levels at 1.94 MB and
27043 /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
27044 /// aborted instead of erroring, which is precisely the outcome it
27045 /// exists to rule out.
27046 ///
27047 /// So the budget is metered rather than assumed. The ceiling leaves
27048 /// the depth SPG advertises fitting in a default 2 MiB thread with
27049 /// room to spare, in the debug build, where frames are widest.
27050 #[test]
27051 fn nesting_frame_cost_stays_under_ceiling() {
27052 // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
27053 // thread keeps a margin for whatever called the parser.
27054 const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
27055
27056 frame_meter::reset();
27057 let depth = frame_meter::SAMPLE_HI + 8;
27058 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27059 parse(&sql);
27060
27061 let per_level = frame_meter::bytes_per_level();
27062 {
27063 extern crate std;
27064 std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
27065 }
27066 assert!(
27067 per_level <= CEILING,
27068 "{per_level} bytes per nesting level exceeds {CEILING}; \
27069 {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
27070 in parse_expr_inner / parse_unary rather than lowering the \
27071 depth or widening the stack.",
27072 per_level * MAX_NEST_DEPTH
27073 );
27074 }
27075
27076 #[test]
27077 fn nesting_budget_errors_cleanly() {
27078 let depth = MAX_NEST_DEPTH + 50;
27079 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27080 let err = parse_statement(&sql).expect_err("must reject");
27081 assert!(err.message.contains("nests deeper"), "{err:?}");
27082 // Within budget still parses.
27083 let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
27084 parse(&sql);
27085 }
27086
27087 #[test]
27088 fn binary_chain_budget_errors_cleanly() {
27089 let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
27090 let err = parse_statement(&sql).expect_err("must reject");
27091 assert!(err.message.contains("chained binary"), "{err:?}");
27092 // Within budget still parses (chain depth ≤ budget is safe
27093 // for recursive eval/drop on 2 MiB stacks).
27094 let sql = format!("SELECT 1{}", " + 1".repeat(200));
27095 parse(&sql);
27096 }
27097
27098 #[test]
27099 fn in_list_unaffected_by_chain_budget() {
27100 // Flat InList: 20k elements parse fine and stay flat.
27101 let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
27102 let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
27103 let Statement::Select(s) = parse(&sql) else {
27104 panic!("expected select")
27105 };
27106 let Some(Expr::InList { list, negated, .. }) = s.where_ else {
27107 panic!("expected flat InList, got {:?}", s.where_)
27108 };
27109 assert_eq!(list.len(), 20_000);
27110 assert!(!negated);
27111 }
27112
27113 fn lit_int(n: i64) -> Expr {
27114 Expr::Literal(Literal::Integer(n))
27115 }
27116
27117 fn col(name: &str) -> Expr {
27118 Expr::Column(ColumnName {
27119 qualifier: None,
27120 name: name.into(),
27121 })
27122 }
27123
27124 #[test]
27125 fn select_single_integer() {
27126 let s = parse("SELECT 1");
27127 let Statement::Select(s) = s else {
27128 panic!("expected SELECT")
27129 };
27130 assert_eq!(s.items.len(), 1);
27131 assert!(s.from.is_none());
27132 assert!(s.where_.is_none());
27133 }
27134
27135 #[test]
27136 fn select_multiple_literal_kinds() {
27137 let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
27138 let Statement::Select(s) = s else {
27139 panic!("expected SELECT")
27140 };
27141 assert_eq!(s.items.len(), 5);
27142 }
27143
27144 #[test]
27145 fn select_wildcard_from_table() {
27146 let s = parse("SELECT * FROM users");
27147 let Statement::Select(s) = s else {
27148 panic!("expected SELECT")
27149 };
27150 assert!(matches!(s.items[..], [SelectItem::Wildcard]));
27151 assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
27152 }
27153
27154 #[test]
27155 fn select_with_table_alias() {
27156 let s = parse("SELECT * FROM users AS u");
27157 let Statement::Select(s) = s else {
27158 panic!("expected SELECT")
27159 };
27160 let t = &s.from.as_ref().unwrap().primary;
27161 assert_eq!(t.name, "users");
27162 assert_eq!(t.alias.as_deref(), Some("u"));
27163 }
27164
27165 #[test]
27166 fn select_with_where_eq() {
27167 let s = parse("SELECT a FROM t WHERE a = 1");
27168 let Statement::Select(s) = s else {
27169 panic!("expected SELECT")
27170 };
27171 let w = s.where_.unwrap();
27172 assert_eq!(
27173 w,
27174 Expr::Binary {
27175 lhs: Box::new(col("a")),
27176 op: BinOp::Eq,
27177 rhs: Box::new(lit_int(1)),
27178 }
27179 );
27180 }
27181
27182 #[test]
27183 fn arithmetic_precedence() {
27184 let s = parse("SELECT 1 + 2 * 3");
27185 let Statement::Select(s) = s else {
27186 panic!("expected SELECT")
27187 };
27188 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27189 panic!("wildcard?")
27190 };
27191 assert_eq!(
27192 expr,
27193 &Expr::Binary {
27194 lhs: Box::new(lit_int(1)),
27195 op: BinOp::Add,
27196 rhs: Box::new(Expr::Binary {
27197 lhs: Box::new(lit_int(2)),
27198 op: BinOp::Mul,
27199 rhs: Box::new(lit_int(3)),
27200 }),
27201 }
27202 );
27203 }
27204
27205 #[test]
27206 fn parentheses_override_precedence() {
27207 let s = parse("SELECT (1 + 2) * 3");
27208 let Statement::Select(s) = s else {
27209 panic!("expected SELECT")
27210 };
27211 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27212 panic!()
27213 };
27214 assert_eq!(
27215 expr,
27216 &Expr::Binary {
27217 lhs: Box::new(Expr::Binary {
27218 lhs: Box::new(lit_int(1)),
27219 op: BinOp::Add,
27220 rhs: Box::new(lit_int(2)),
27221 }),
27222 op: BinOp::Mul,
27223 rhs: Box::new(lit_int(3)),
27224 }
27225 );
27226 }
27227
27228 #[test]
27229 fn not_binds_below_comparison() {
27230 // `NOT a = 1` should parse as `NOT (a = 1)`.
27231 let s = parse("SELECT NOT a = 1 FROM t");
27232 let Statement::Select(s) = s else {
27233 panic!("expected SELECT")
27234 };
27235 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27236 panic!()
27237 };
27238 assert_eq!(
27239 expr,
27240 &Expr::Unary {
27241 op: UnOp::Not,
27242 expr: Box::new(Expr::Binary {
27243 lhs: Box::new(col("a")),
27244 op: BinOp::Eq,
27245 rhs: Box::new(lit_int(1)),
27246 }),
27247 }
27248 );
27249 }
27250
27251 #[test]
27252 fn unary_minus_binds_above_multiplication() {
27253 // `-a * 2` should be `(-a) * 2`.
27254 let s = parse("SELECT -a * 2 FROM t");
27255 let Statement::Select(s) = s else {
27256 panic!("expected SELECT")
27257 };
27258 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27259 panic!()
27260 };
27261 assert_eq!(
27262 expr,
27263 &Expr::Binary {
27264 lhs: Box::new(Expr::Unary {
27265 op: UnOp::Neg,
27266 expr: Box::new(col("a")),
27267 }),
27268 op: BinOp::Mul,
27269 rhs: Box::new(lit_int(2)),
27270 }
27271 );
27272 }
27273
27274 #[test]
27275 fn qualified_column() {
27276 let s = parse("SELECT t.col FROM t");
27277 let Statement::Select(s) = s else {
27278 panic!("expected SELECT")
27279 };
27280 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27281 panic!()
27282 };
27283 assert_eq!(
27284 expr,
27285 &Expr::Column(ColumnName {
27286 qualifier: Some("t".into()),
27287 name: "col".into()
27288 })
27289 );
27290 }
27291
27292 #[test]
27293 fn select_item_alias_with_as() {
27294 let s = parse("SELECT a AS y FROM t");
27295 let Statement::Select(s) = s else {
27296 panic!("expected SELECT")
27297 };
27298 let SelectItem::Expr { alias, .. } = &s.items[0] else {
27299 panic!()
27300 };
27301 assert_eq!(alias.as_deref(), Some("y"));
27302 }
27303
27304 #[test]
27305 fn trailing_semicolon_accepted() {
27306 let s = parse("SELECT 1;");
27307 let Statement::Select(s) = s else {
27308 panic!("expected SELECT")
27309 };
27310 assert_eq!(s.items.len(), 1);
27311 }
27312
27313 #[test]
27314 fn boolean_chain_with_and_or_not() {
27315 // (NOT a) OR (b AND (NOT c))
27316 let s = parse("SELECT NOT a OR b AND NOT c FROM t");
27317 let Statement::Select(s) = s else {
27318 panic!("expected SELECT")
27319 };
27320 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27321 panic!()
27322 };
27323 let expected = Expr::Binary {
27324 lhs: Box::new(Expr::Unary {
27325 op: UnOp::Not,
27326 expr: Box::new(col("a")),
27327 }),
27328 op: BinOp::Or,
27329 rhs: Box::new(Expr::Binary {
27330 lhs: Box::new(col("b")),
27331 op: BinOp::And,
27332 rhs: Box::new(Expr::Unary {
27333 op: UnOp::Not,
27334 expr: Box::new(col("c")),
27335 }),
27336 }),
27337 };
27338 assert_eq!(expr, &expected);
27339 }
27340
27341 #[test]
27342 fn empty_input_errors() {
27343 // v7.14.0 — pg_dump preambles emit several comment-only
27344 // / blank-line statements that collapse to Statement::
27345 // Empty rather than a parse error. The old "SELECT in
27346 // message" assertion is stale; verify the new contract:
27347 // empty / whitespace / comment-only input parses to
27348 // Statement::Empty.
27349 assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
27350 assert!(matches!(
27351 parse_statement(" \n\t ").unwrap(),
27352 Statement::Empty
27353 ));
27354 // Sanity: malformed-but-non-empty still errors.
27355 assert!(parse_statement("SELECT FROM WHERE").is_err());
27356 }
27357
27358 #[test]
27359 fn unmatched_paren_errors() {
27360 assert!(parse_statement("SELECT (1 + 2").is_err());
27361 }
27362
27363 #[test]
27364 fn display_round_trip_simple_select() {
27365 let original = parse("SELECT a + 1 FROM t WHERE a > 0");
27366 let text = original.to_string();
27367 let again = parse_statement(&text).expect("re-parse");
27368 assert_eq!(original, again);
27369 }
27370
27371 // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
27372
27373 #[test]
27374 fn create_table_single_column() {
27375 let s = parse("CREATE TABLE foo (a INT)");
27376 let Statement::CreateTable(c) = s else {
27377 panic!("expected CreateTable")
27378 };
27379 assert_eq!(c.name, "foo");
27380 assert_eq!(c.columns.len(), 1);
27381 assert_eq!(c.columns[0].name, "a");
27382 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27383 assert!(c.columns[0].nullable);
27384 }
27385
27386 #[test]
27387 fn create_table_multi_column_with_not_null_mix() {
27388 let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
27389 let Statement::CreateTable(c) = s else {
27390 panic!()
27391 };
27392 assert_eq!(c.columns.len(), 4);
27393 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27394 assert!(!c.columns[0].nullable);
27395 assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
27396 assert!(c.columns[1].nullable);
27397 assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
27398 assert!(!c.columns[2].nullable);
27399 assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
27400 }
27401
27402 #[test]
27403 fn create_table_bigint_supported() {
27404 let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
27405 let Statement::CreateTable(c) = s else {
27406 panic!()
27407 };
27408 assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
27409 }
27410
27411 #[test]
27412 fn create_table_vector_default_is_f32() {
27413 let s = parse("CREATE TABLE t (v VECTOR(128))");
27414 let Statement::CreateTable(c) = s else {
27415 panic!()
27416 };
27417 assert_eq!(
27418 c.columns[0].ty,
27419 ColumnTypeName::Vector {
27420 dim: 128,
27421 encoding: VecEncoding::F32,
27422 },
27423 );
27424 }
27425
27426 #[test]
27427 fn create_table_vector_using_sq8() {
27428 // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
27429 // Case-insensitive on both `USING` and the encoding name.
27430 for sql in [
27431 "CREATE TABLE t (v VECTOR(128) USING SQ8)",
27432 "CREATE TABLE t (v VECTOR(128) using sq8)",
27433 ] {
27434 let s = parse(sql);
27435 let Statement::CreateTable(c) = s else {
27436 panic!()
27437 };
27438 assert_eq!(
27439 c.columns[0].ty,
27440 ColumnTypeName::Vector {
27441 dim: 128,
27442 encoding: VecEncoding::Sq8,
27443 },
27444 "{sql}",
27445 );
27446 }
27447 }
27448
27449 #[test]
27450 fn create_table_vector_using_unknown_errors() {
27451 // v7.16.1 — the inline `USING <encoding>` shape on
27452 // CREATE TABLE column defs was withdrawn before
27453 // v7.14.0 in favour of `CREATE INDEX … USING hnsw
27454 // (col vector_<metric>_ops)`; the parser now rejects
27455 // USING at column-list position with a clearer
27456 // "expected ',' or ')'" message. Test asserts the
27457 // current rejection, not the old "unknown vector
27458 // encoding" string.
27459 let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
27460 assert!(
27461 err.message.contains("USING")
27462 || err.message.contains("using")
27463 || err.message.contains("')'")
27464 || err.message.contains("','"),
27465 "expected USING/column-list rejection, got: {}",
27466 err.message
27467 );
27468 }
27469
27470 #[test]
27471 fn vector_using_sq8_display_roundtrips() {
27472 // The Display impl must produce text that re-parses to the
27473 // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
27474 let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
27475 let Statement::CreateTable(c) = s else {
27476 panic!()
27477 };
27478 assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
27479 }
27480
27481 #[test]
27482 fn parser_recognises_placeholders() {
27483 use crate::ast::{Expr, SelectItem, Statement};
27484 // $N in expression position parses as Expr::Placeholder(N).
27485 let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
27486 let Statement::Select(sel) = s else { panic!() };
27487 assert!(matches!(
27488 sel.items[0],
27489 SelectItem::Expr {
27490 expr: Expr::Placeholder(1),
27491 alias: None
27492 }
27493 ));
27494 // $2 + 1
27495 let SelectItem::Expr {
27496 expr: Expr::Binary { lhs, rhs, .. },
27497 ..
27498 } = &sel.items[1]
27499 else {
27500 panic!()
27501 };
27502 assert!(matches!(**lhs, Expr::Placeholder(2)));
27503 assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
27504 // WHERE x = $3
27505 let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
27506 panic!()
27507 };
27508 assert!(matches!(**rhs, Expr::Placeholder(3)));
27509 }
27510
27511 #[test]
27512 fn parser_rejects_dollar_zero() {
27513 // $0 is not valid in PG; the lexer rejects it.
27514 assert!(parse_statement("SELECT $0").is_err());
27515 }
27516
27517 #[test]
27518 fn placeholder_display_roundtrips() {
27519 // The Display impl must produce text that re-lexes to the
27520 // same Placeholder token.
27521 let s = parse("SELECT $42 FROM t");
27522 let printed = s.to_string();
27523 assert!(printed.contains("$42"));
27524 let again = parse(&printed);
27525 assert_eq!(s, again);
27526 }
27527
27528 #[test]
27529 fn alter_index_rebuild_bare() {
27530 use crate::ast::{AlterIndexTarget, Statement};
27531 let s = parse("ALTER INDEX my_idx REBUILD");
27532 let Statement::AlterIndex(a) = s else {
27533 panic!("expected AlterIndex, got {s:?}")
27534 };
27535 assert_eq!(a.name, "my_idx");
27536 assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
27537 }
27538
27539 #[test]
27540 fn alter_index_rebuild_with_encoding() {
27541 use crate::ast::{AlterIndexTarget, Statement};
27542 for (sql, want) in [
27543 (
27544 "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
27545 VecEncoding::F32,
27546 ),
27547 (
27548 "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
27549 VecEncoding::Sq8,
27550 ),
27551 (
27552 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27553 VecEncoding::F16,
27554 ),
27555 ] {
27556 let s = parse(sql);
27557 let Statement::AlterIndex(a) = s else {
27558 panic!("{sql}: expected AlterIndex")
27559 };
27560 assert_eq!(a.name, "my_idx");
27561 assert_eq!(
27562 a.target,
27563 AlterIndexTarget::Rebuild {
27564 encoding: Some(want)
27565 },
27566 "{sql}"
27567 );
27568 }
27569 }
27570
27571 #[test]
27572 fn alter_index_rebuild_unknown_encoding_errors() {
27573 let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
27574 assert!(
27575 err.message.contains("unknown vector encoding"),
27576 "got: {}",
27577 err.message
27578 );
27579 }
27580
27581 #[test]
27582 fn alter_index_rebuild_display_roundtrips() {
27583 for (input, want) in [
27584 ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
27585 (
27586 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27587 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27588 ),
27589 (
27590 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27591 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27592 ),
27593 ] {
27594 let s = parse(input);
27595 assert_eq!(s.to_string(), want);
27596 }
27597 }
27598
27599 #[test]
27600 fn create_table_unknown_type_defers_to_engine() {
27601 // v4.9 picked XML as a parse-time "unsupported column
27602 // type" probe. v7.17.0 Phase 1.4 changed the contract:
27603 // an unknown type ident parses as Text + `user_type_ref`
27604 // so CREATE TABLE can resolve user-defined enum / domain
27605 // types — rejection of truly-unknown types moved to the
27606 // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
27607 // to a first-class built-in, so this probe switched to a
27608 // synthetic name nothing in the lexer will ever recognise.
27609 let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
27610 let Statement::CreateTable(t) = stmt else {
27611 panic!("expected CreateTable");
27612 };
27613 assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
27614 }
27615
27616 #[test]
27617 fn create_table_missing_table_keyword_errors() {
27618 assert!(parse_statement("CREATE x (a INT)").is_err());
27619 }
27620
27621 // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
27622 // `PARTITION OF parent <bounds>` child parse + Display round-trip.
27623
27624 #[test]
27625 fn parse_create_table_partition_by_range() {
27626 use crate::ast::{PartitionBySpec, PartitionKindAst};
27627 let stmt = parse_statement(
27628 "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
27629 payload JSONB) PARTITION BY RANGE (ts)",
27630 )
27631 .unwrap();
27632 let Statement::CreateTable(t) = stmt else {
27633 panic!("expected CreateTable");
27634 };
27635 assert!(t.partition_of.is_none(), "parent has no partition_of");
27636 assert_eq!(t.columns.len(), 3);
27637 let by = t.partition_by.as_ref().expect("expected PARTITION BY");
27638 assert_eq!(
27639 by,
27640 &PartitionBySpec {
27641 kind: PartitionKindAst::Range,
27642 key_columns: alloc::vec!["ts".to_string()],
27643 }
27644 );
27645 // Display round-trip preserves the suffix. `quote_ident`
27646 // only adds double quotes when the ident needs escaping, so
27647 // a plain `ts` survives bare here.
27648 assert!(
27649 t.to_string().contains("PARTITION BY RANGE (ts)"),
27650 "Display lost PARTITION BY suffix: {t}"
27651 );
27652 }
27653
27654 #[test]
27655 fn parse_create_table_partition_of_range() {
27656 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
27657 let stmt = parse_statement(
27658 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
27659 FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
27660 )
27661 .unwrap();
27662 let Statement::CreateTable(t) = stmt else {
27663 panic!("expected CreateTable");
27664 };
27665 assert!(t.columns.is_empty(), "child inherits columns from parent");
27666 assert!(t.partition_by.is_none());
27667 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27668 assert_eq!(of.parent_name, "events_partitioned");
27669 let PartitionOfSpec { bounds, .. } = of.clone();
27670 match bounds {
27671 PartitionOfBoundsAst::Range { lower, upper } => {
27672 assert!(lower.to_string().contains("2026-06-01"));
27673 assert!(upper.to_string().contains("2026-07-01"));
27674 }
27675 other => panic!("expected Range, got {other:?}"),
27676 }
27677 // Display round-trip emits the FOR VALUES tail. `quote_ident`
27678 // skips quotes when not required, so the parent name appears
27679 // bare here.
27680 let s = t.to_string();
27681 assert!(
27682 s.contains("PARTITION OF events_partitioned"),
27683 "Display lost PARTITION OF: {s}"
27684 );
27685 assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
27686 assert!(s.contains(") TO ("), "Display lost TO: {s}");
27687 }
27688
27689 #[test]
27690 fn parse_create_table_partition_of_default() {
27691 use crate::ast::PartitionOfBoundsAst;
27692 let stmt =
27693 parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
27694 .unwrap();
27695 let Statement::CreateTable(t) = stmt else {
27696 panic!("expected CreateTable");
27697 };
27698 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27699 assert_eq!(of.parent_name, "events_partitioned");
27700 assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
27701 assert!(
27702 t.to_string()
27703 .contains("PARTITION OF events_partitioned DEFAULT"),
27704 "Display lost DEFAULT: {t}"
27705 );
27706 }
27707
27708 #[test]
27709 fn parse_create_table_partition_by_list() {
27710 // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
27711 // child with `FOR VALUES IN (lit, lit, …)`.
27712 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27713 let parent =
27714 parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
27715 .unwrap();
27716 let Statement::CreateTable(t) = parent else {
27717 panic!("expected CreateTable");
27718 };
27719 let Some(PartitionBySpec {
27720 kind,
27721 ref key_columns,
27722 }) = t.partition_by
27723 else {
27724 panic!("expected PARTITION BY");
27725 };
27726 assert_eq!(kind, PartitionKindAst::List);
27727 assert_eq!(*key_columns, vec!["region".to_string()]);
27728 assert!(t.to_string().contains("PARTITION BY LIST (region)"));
27729
27730 let child = parse_statement(
27731 "CREATE TABLE events_apac PARTITION OF events_listed \
27732 FOR VALUES IN ('jp', 'kr', 'tw')",
27733 )
27734 .unwrap();
27735 let Statement::CreateTable(c) = child else {
27736 panic!("expected CreateTable");
27737 };
27738 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27739 let PartitionOfBoundsAst::List { values } = &of.bounds else {
27740 panic!("expected List bounds, got {:?}", of.bounds);
27741 };
27742 assert_eq!(values.len(), 3);
27743 let disp = c.to_string();
27744 assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
27745 }
27746
27747 #[test]
27748 fn parse_create_table_partition_by_hash() {
27749 // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
27750 // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
27751 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27752 let parent =
27753 parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
27754 let Statement::CreateTable(t) = parent else {
27755 panic!("expected CreateTable");
27756 };
27757 let Some(PartitionBySpec {
27758 kind,
27759 ref key_columns,
27760 }) = t.partition_by
27761 else {
27762 panic!("expected PARTITION BY");
27763 };
27764 assert_eq!(kind, PartitionKindAst::Hash);
27765 assert_eq!(*key_columns, vec!["id".to_string()]);
27766 assert!(t.to_string().contains("PARTITION BY HASH (id)"));
27767
27768 let child = parse_statement(
27769 "CREATE TABLE orders_h_0 PARTITION OF orders_h \
27770 FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
27771 )
27772 .unwrap();
27773 let Statement::CreateTable(c) = child else {
27774 panic!("expected CreateTable");
27775 };
27776 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27777 let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
27778 panic!("expected Hash bounds");
27779 };
27780 assert_eq!(modulus, 4);
27781 assert_eq!(remainder, 0);
27782 let disp = c.to_string();
27783 assert!(
27784 disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
27785 "Display lost HASH bounds: {disp}"
27786 );
27787
27788 // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
27789 let bad = parse_statement(
27790 "CREATE TABLE orders_h_bad PARTITION OF orders_h \
27791 FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
27792 );
27793 let msg = format!("{}", bad.unwrap_err());
27794 assert!(
27795 msg.contains("REMAINDER") && msg.contains("MODULUS"),
27796 "expected REMAINDER/MODULUS validation error: {msg}"
27797 );
27798 }
27799
27800 #[test]
27801 fn parse_create_table_partition_of_rejects_columns() {
27802 // v7.37.6-B contract: PARTITION OF children inherit columns
27803 // from the parent; an explicit list MUST surface as a parse
27804 // error rather than getting silently ignored.
27805 let err = parse_statement(
27806 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
27807 FOR VALUES FROM ('a') TO ('b')",
27808 );
27809 assert!(err.is_err(), "expected parse error for explicit columns");
27810 let msg = format!("{}", err.unwrap_err());
27811 assert!(
27812 msg.contains("PARTITION OF") && msg.contains("column"),
27813 "error should mention PARTITION OF + columns: {msg}"
27814 );
27815 }
27816
27817 #[test]
27818 fn insert_single_value() {
27819 let s = parse("INSERT INTO foo VALUES (42)");
27820 let Statement::Insert(i) = s else {
27821 panic!("expected Insert")
27822 };
27823 assert_eq!(i.table, "foo");
27824 assert_eq!(i.rows.len(), 1);
27825 assert_eq!(i.rows[0].len(), 1);
27826 assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
27827 }
27828
27829 #[test]
27830 fn insert_multi_value_with_mixed_literals() {
27831 let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
27832 let Statement::Insert(i) = s else { panic!() };
27833 assert_eq!(i.rows.len(), 1);
27834 assert_eq!(i.rows[0].len(), 5);
27835 }
27836
27837 #[test]
27838 fn insert_missing_into_errors() {
27839 assert!(parse_statement("INSERT foo VALUES (1)").is_err());
27840 }
27841
27842 #[test]
27843 fn create_table_round_trip() {
27844 let original =
27845 parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
27846 let text = original.to_string();
27847 let again = parse_statement(&text).expect("re-parse");
27848 assert_eq!(original, again);
27849 }
27850
27851 #[test]
27852 fn insert_round_trip_with_negation_and_string() {
27853 let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
27854 let text = original.to_string();
27855 let again = parse_statement(&text).expect("re-parse");
27856 assert_eq!(original, again);
27857 }
27858
27859 #[test]
27860 fn unknown_keyword_at_statement_start_errors() {
27861 // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
27862 // the top-level dispatch still has no branch to take.
27863 let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
27864 assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
27865 }
27866
27867 // --- v0.8 CREATE INDEX --------------------------------------------------
27868
27869 #[test]
27870 fn create_index_basic() {
27871 let s = parse("CREATE INDEX idx_id ON users (id)");
27872 let Statement::CreateIndex(c) = s else {
27873 panic!("expected CreateIndex")
27874 };
27875 assert_eq!(c.name, "idx_id");
27876 assert_eq!(c.table, "users");
27877 assert_eq!(c.column, "id");
27878 }
27879
27880 #[test]
27881 fn create_index_missing_on_errors() {
27882 assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
27883 }
27884
27885 #[test]
27886 fn create_index_missing_paren_errors() {
27887 assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
27888 }
27889
27890 #[test]
27891 fn create_index_round_trip() {
27892 let original = parse("CREATE INDEX by_name ON users (name)");
27893 let again = parse_statement(&original.to_string()).unwrap();
27894 assert_eq!(original, again);
27895 }
27896
27897 // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
27898
27899 #[test]
27900 fn create_unique_index_basic() {
27901 let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
27902 let Statement::CreateIndex(c) = s else {
27903 panic!("expected CreateIndex");
27904 };
27905 assert!(c.is_unique);
27906 assert_eq!(c.column, "a");
27907 assert!(c.partial_predicate.is_none());
27908 }
27909
27910 #[test]
27911 fn create_unique_index_partial() {
27912 // mailrs's email_templates "one default per user" shape.
27913 let s = parse(
27914 "CREATE UNIQUE INDEX idx_email_templates_user_default \
27915 ON email_templates (user_address) WHERE is_default = true",
27916 );
27917 let Statement::CreateIndex(c) = s else {
27918 panic!("expected CreateIndex");
27919 };
27920 assert!(c.is_unique);
27921 assert_eq!(c.table, "email_templates");
27922 assert_eq!(c.column, "user_address");
27923 assert!(c.partial_predicate.is_some());
27924 }
27925
27926 #[test]
27927 fn create_unique_index_composite_with_predicate() {
27928 // mailrs's calendar_events instance: composite columns.
27929 let s = parse(
27930 "CREATE UNIQUE INDEX uq_calendar_events_instance \
27931 ON calendar_events (calendar_id, uid, recurrence_id) \
27932 WHERE recurrence_id IS NOT NULL",
27933 );
27934 let Statement::CreateIndex(c) = s else {
27935 panic!("expected CreateIndex");
27936 };
27937 assert!(c.is_unique);
27938 assert_eq!(c.column, "calendar_id");
27939 assert_eq!(
27940 c.extra_columns,
27941 vec!["uid".to_string(), "recurrence_id".to_string()]
27942 );
27943 assert!(c.partial_predicate.is_some());
27944 }
27945
27946 #[test]
27947 fn create_unique_index_using_btree_ok() {
27948 let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
27949 assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
27950 }
27951
27952 #[test]
27953 fn create_unique_index_using_hnsw_rejected() {
27954 let err =
27955 parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
27956 assert!(err.message.contains("UNIQUE"), "{}", err.message);
27957 }
27958
27959 #[test]
27960 fn create_unique_index_round_trip() {
27961 let original = parse(
27962 "CREATE UNIQUE INDEX uq_calendar_events_master \
27963 ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
27964 );
27965 let again = parse_statement(&original.to_string()).unwrap();
27966 assert_eq!(original, again);
27967 }
27968
27969 #[test]
27970 fn create_unique_without_index_errors() {
27971 let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
27972 // v7.39 (round 340, V56) — PG 18.4, verbatim.
27973 assert_eq!(err.message, "syntax error at or near \"TABLE\"");
27974 }
27975
27976 // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
27977
27978 #[test]
27979 fn create_table_bytea_column() {
27980 let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
27981 let Statement::CreateTable(c) = s else {
27982 panic!("expected CreateTable");
27983 };
27984 assert_eq!(c.columns.len(), 2);
27985 assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
27986 assert!(!c.columns[1].nullable);
27987 }
27988
27989 #[test]
27990 fn create_table_bytes_alias_column() {
27991 let s = parse("CREATE TABLE t (blob BYTES)");
27992 let Statement::CreateTable(c) = s else {
27993 panic!("expected CreateTable");
27994 };
27995 assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
27996 }
27997
27998 #[test]
27999 fn bytea_round_trip_display() {
28000 let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
28001 let again = parse_statement(&original.to_string()).unwrap();
28002 assert_eq!(original, again);
28003 }
28004
28005 // --- v0.9 transactions -------------------------------------------------
28006
28007 #[test]
28008 fn begin_commit_rollback_parse_as_unit_variants() {
28009 assert_eq!(parse("BEGIN"), Statement::Begin(None));
28010 assert_eq!(parse("COMMIT"), Statement::Commit);
28011 assert_eq!(parse("ROLLBACK"), Statement::Rollback);
28012 // Trailing semicolons accepted too.
28013 assert_eq!(parse("BEGIN;"), Statement::Begin(None));
28014 // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
28015 // statement (with or without the WORK/TRANSACTION noise word).
28016 assert_eq!(
28017 parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
28018 Statement::Begin(Some(IsolationLevel::RepeatableRead))
28019 );
28020 assert_eq!(
28021 parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
28022 Statement::Begin(Some(IsolationLevel::Serializable))
28023 );
28024 // A non-isolation mode keeps the session default (None).
28025 assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
28026 }
28027
28028 // --- v1.2: pgvector distance ops + ::vector cast --------------------
28029
28030 #[test]
28031 fn inner_product_binop_parses() {
28032 let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
28033 let Statement::Select(s) = s else { panic!() };
28034 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28035 panic!()
28036 };
28037 assert!(matches!(
28038 expr,
28039 Expr::Binary {
28040 op: BinOp::InnerProduct,
28041 ..
28042 }
28043 ));
28044 }
28045
28046 #[test]
28047 fn cosine_distance_binop_parses() {
28048 let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
28049 let Statement::Select(s) = s else { panic!() };
28050 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28051 panic!()
28052 };
28053 assert!(matches!(
28054 expr,
28055 Expr::Binary {
28056 op: BinOp::CosineDistance,
28057 ..
28058 }
28059 ));
28060 }
28061
28062 #[test]
28063 fn vector_cast_postfix_wraps_string_literal() {
28064 let s = parse("SELECT '[1,2,3]'::vector FROM t");
28065 let Statement::Select(s) = s else { panic!() };
28066 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28067 panic!()
28068 };
28069 assert!(matches!(
28070 expr,
28071 Expr::Cast {
28072 target: CastTarget::Vector,
28073 ..
28074 }
28075 ));
28076 }
28077
28078 #[test]
28079 fn unsupported_cast_target_errors() {
28080 // v7.37.5 ship triage promoted the parser to accept every
28081 // ident as a `CastTarget::Named(canonical)`; the engine
28082 // surfaces the "unsupported cast target" error at eval
28083 // time when `type_name_to_data_type` can't resolve it.
28084 // Parser-side error now requires a NON-ident after `::`
28085 // (e.g. a punctuation token).
28086 let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
28087 assert_eq!(err.message, "syntax error at or near \",\"");
28088 }
28089
28090 #[test]
28091 fn tx_statements_round_trip() {
28092 for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
28093 let original = parse(q);
28094 let again = parse_statement(&original.to_string()).unwrap();
28095 assert_eq!(original, again);
28096 }
28097 }
28098
28099 #[test]
28100 fn interval_text_parsing_units() {
28101 // v7.37.5 β — three-field shape `(months, days, micros)` so
28102 // `'1 day'` and `'24 hours'` no longer collide (PG parity).
28103 // Single unit.
28104 assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
28105 assert_eq!(
28106 parse_interval_text("24 hours"),
28107 Some((0, 0, 86_400_000_000))
28108 );
28109 assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
28110 assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
28111 assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
28112 assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
28113 // Compound spans accumulate per-dimension.
28114 assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
28115 assert_eq!(
28116 parse_interval_text("1 day 2 hours"),
28117 Some((0, 1, 7_200_000_000))
28118 );
28119 // Negative numbers carry through per-dimension.
28120 assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
28121 // Bad shapes return None.
28122 assert_eq!(parse_interval_text(""), None);
28123 assert_eq!(parse_interval_text("garbage"), None);
28124 assert_eq!(parse_interval_text("1 fortnight"), None);
28125 // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
28126 // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
28127 assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
28128 assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
28129 assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
28130 }
28131
28132 #[test]
28133 fn interval_literal_roundtrips_via_display() {
28134 let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
28135 let s = parsed.to_string();
28136 // Display preserves the original text verbatim.
28137 assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
28138 // And re-parsing yields a structurally equal statement.
28139 let again = parse_statement(&s).unwrap();
28140 assert_eq!(parsed, again);
28141 }
28142
28143 // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
28144
28145 #[test]
28146 fn parser_recognises_create_publication_bare() {
28147 let s = parse("CREATE PUBLICATION pub_a");
28148 let Statement::CreatePublication(p) = s else {
28149 panic!("expected CreatePublication, got {s:?}")
28150 };
28151 assert_eq!(p.name, "pub_a");
28152 assert_eq!(p.scope, PublicationScope::AllTables);
28153 }
28154
28155 #[test]
28156 fn parser_recognises_create_publication_for_all_tables() {
28157 let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
28158 let Statement::CreatePublication(p) = s else {
28159 panic!("expected CreatePublication, got {s:?}")
28160 };
28161 assert_eq!(p.name, "pub_a");
28162 assert_eq!(p.scope, PublicationScope::AllTables);
28163 }
28164
28165 #[test]
28166 fn parser_recognises_drop_publication() {
28167 let s = parse("DROP PUBLICATION pub_a");
28168 let Statement::DropPublication { name, .. } = s else {
28169 panic!("expected DropPublication, got {s:?}")
28170 };
28171 assert_eq!(name, "pub_a");
28172 }
28173
28174 #[test]
28175 fn parser_recognises_for_table_list() {
28176 let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
28177 let Statement::CreatePublication(p) = s else {
28178 panic!("expected CreatePublication, got {s:?}")
28179 };
28180 assert_eq!(p.name, "pub_a");
28181 let PublicationScope::ForTables(ts) = p.scope else {
28182 panic!("expected ForTables scope")
28183 };
28184 assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
28185 }
28186
28187 #[test]
28188 fn parser_rejects_bare_for_tables_and_takes_in_schema() {
28189 // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
28190 // is rejected (`invalid publication object list`; the old
28191 // test pinned an unverifiable "PG 19 accepts both" claim);
28192 // TABLES pairs with IN SCHEMA.
28193 let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
28194 .expect_err("bare FOR TABLES must reject");
28195 assert!(
28196 alloc::format!("{err}").contains("invalid publication object list"),
28197 "got: {err}"
28198 );
28199 let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
28200 let Statement::CreatePublication(p) = s else {
28201 panic!("expected CreatePublication, got {s:?}")
28202 };
28203 let PublicationScope::TablesInSchema(schema) = p.scope else {
28204 panic!("expected TablesInSchema")
28205 };
28206 assert_eq!(schema, "public");
28207 }
28208
28209 #[test]
28210 fn parser_recognises_for_all_tables_except_list() {
28211 let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
28212 let Statement::CreatePublication(p) = s else {
28213 panic!()
28214 };
28215 let PublicationScope::AllTablesExcept(ts) = p.scope else {
28216 panic!("expected AllTablesExcept")
28217 };
28218 assert_eq!(ts, alloc::vec!["t1", "t2"]);
28219 }
28220
28221 #[test]
28222 fn parser_rejects_for_table_with_empty_list() {
28223 // `FOR TABLE` with nothing after is a parse error.
28224 let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
28225 .expect_err("must error on empty list");
28226 // No specific message asserted — the call falls through to
28227 // expect_ident_like which yields "expected identifier, got …".
28228 assert!(!err.message.is_empty());
28229 }
28230
28231 #[test]
28232 fn parser_recognises_show_publications() {
28233 // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
28234 // bare ident in this position, NOT a reserved keyword.
28235 let s = parse("SHOW PUBLICATIONS");
28236 assert!(matches!(s, Statement::ShowPublications));
28237 }
28238
28239 // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
28240
28241 #[test]
28242 fn parser_recognises_create_subscription_single_publication() {
28243 let s = parse(
28244 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
28245 );
28246 let Statement::CreateSubscription(c) = s else {
28247 panic!("expected CreateSubscription, got {s:?}")
28248 };
28249 assert_eq!(c.name, "sub_a");
28250 assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
28251 assert_eq!(c.publications, alloc::vec!["pub_a"]);
28252 }
28253
28254 #[test]
28255 fn parser_recognises_create_subscription_multi_publication() {
28256 let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
28257 let Statement::CreateSubscription(c) = s else {
28258 panic!()
28259 };
28260 assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
28261 }
28262
28263 #[test]
28264 fn parser_rejects_create_subscription_missing_connection() {
28265 let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
28266 .expect_err("must error on missing CONNECTION");
28267 assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
28268 }
28269
28270 #[test]
28271 fn parser_rejects_create_subscription_missing_publication() {
28272 let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
28273 .expect_err("must error on missing PUBLICATION");
28274 assert_eq!(err.message, "syntax error at end of input");
28275 }
28276
28277 #[test]
28278 fn parser_recognises_drop_subscription() {
28279 let s = parse("DROP SUBSCRIPTION sub_a");
28280 let Statement::DropSubscription { name, .. } = s else {
28281 panic!("expected DropSubscription, got {s:?}")
28282 };
28283 assert_eq!(name, "sub_a");
28284 }
28285
28286 #[test]
28287 fn parser_recognises_show_subscriptions() {
28288 let s = parse("SHOW SUBSCRIPTIONS");
28289 assert!(matches!(s, Statement::ShowSubscriptions));
28290 }
28291
28292 #[test]
28293 fn parser_recognises_wait_for_wal_position_no_timeout() {
28294 let s = parse("WAIT FOR WAL POSITION 12345");
28295 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28296 panic!("expected WaitForWalPosition, got {s:?}")
28297 };
28298 assert_eq!(pos, 12345);
28299 assert!(timeout_ms.is_none());
28300 }
28301
28302 #[test]
28303 fn parser_recognises_wait_for_wal_position_with_timeout() {
28304 let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
28305 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28306 panic!()
28307 };
28308 assert_eq!(pos, 67890);
28309 assert_eq!(timeout_ms, Some(5000));
28310 }
28311
28312 #[test]
28313 fn parser_rejects_wait_with_negative_position() {
28314 // The lexer treats `-` as a token; `expect_u64_literal`
28315 // only sees the Integer that follows, so the negative
28316 // arrives as a unary-minus expression at higher levels.
28317 // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
28318 // parse error one way or another.
28319 let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
28320 assert!(!err.message.is_empty());
28321 }
28322
28323 #[test]
28324 fn parser_recognises_bare_analyze() {
28325 let s = parse("ANALYZE");
28326 assert!(matches!(s, Statement::Analyze(None)));
28327 }
28328
28329 #[test]
28330 fn parser_recognises_analyze_with_table() {
28331 let s = parse("ANALYZE users");
28332 let Statement::Analyze(Some(name)) = s else {
28333 panic!("expected Analyze, got {s:?}")
28334 };
28335 assert_eq!(name, "users");
28336 }
28337
28338 #[test]
28339 fn parser_recognises_analyze_with_quoted_table() {
28340 let s = parse("ANALYZE \"Mixed Case\"");
28341 let Statement::Analyze(Some(name)) = s else {
28342 panic!()
28343 };
28344 assert_eq!(name, "Mixed Case");
28345 }
28346
28347 #[test]
28348 fn parser_rejects_analyze_with_garbage_token() {
28349 let err = parse_statement("ANALYZE 42").expect_err("must error");
28350 assert!(!err.message.is_empty());
28351 }
28352
28353 #[test]
28354 fn analyze_display_roundtrips() {
28355 for sql in ["ANALYZE", "ANALYZE users"] {
28356 let s = parse(sql);
28357 let printed = s.to_string();
28358 let again = parse_statement(&printed)
28359 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28360 assert_eq!(s, again);
28361 }
28362 }
28363
28364 #[test]
28365 fn wait_for_display_roundtrips() {
28366 for sql in [
28367 "WAIT FOR WAL POSITION 12345",
28368 "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
28369 ] {
28370 let s = parse(sql);
28371 let printed = s.to_string();
28372 let again = parse_statement(&printed)
28373 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28374 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28375 }
28376 }
28377
28378 #[test]
28379 fn subscription_ddl_display_roundtrips() {
28380 for sql in [
28381 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
28382 "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
28383 "DROP SUBSCRIPTION sub_a",
28384 "SHOW SUBSCRIPTIONS",
28385 ] {
28386 let s = parse(sql);
28387 let printed = s.to_string();
28388 let again = parse_statement(&printed)
28389 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28390 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28391 }
28392 }
28393
28394 #[test]
28395 fn parser_drop_dispatches_user_vs_publication() {
28396 // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
28397 // tokenises DROP. Both targets must still parse.
28398 let s = parse("DROP USER 'alice'");
28399 let Statement::DropUser { name, .. } = s else {
28400 panic!("expected DropUser, got {s:?}")
28401 };
28402 assert_eq!(name, "alice");
28403 // And DROP PUBLICATION lands the new variant.
28404 let s = parse("DROP PUBLICATION p1");
28405 assert!(matches!(s, Statement::DropPublication { .. }));
28406 }
28407
28408 #[test]
28409 fn publication_ddl_display_roundtrips() {
28410 // Every CREATE PUBLICATION variant must Display → parse →
28411 // same AST. v6.1.3 covers all three scope shapes.
28412 for sql in [
28413 "CREATE PUBLICATION pub_a",
28414 "CREATE PUBLICATION pub_a FOR ALL TABLES",
28415 "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
28416 "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
28417 "DROP PUBLICATION pub_a",
28418 "SHOW PUBLICATIONS",
28419 ] {
28420 let s = parse(sql);
28421 let printed = s.to_string();
28422 let again = parse_statement(&printed)
28423 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28424 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28425 }
28426 }
28427
28428 // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
28429
28430 #[test]
28431 fn create_function_returns_trigger_plpgsql_minimal() {
28432 let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
28433 let s = parse(sql);
28434 let Statement::CreateFunction(f) = s else {
28435 panic!("expected CreateFunction");
28436 };
28437 assert_eq!(f.name, "noop");
28438 assert!(!f.or_replace);
28439 assert!(f.args.is_empty());
28440 assert!(matches!(f.returns, FunctionReturn::Trigger));
28441 assert_eq!(f.language, "plpgsql");
28442 let FunctionBody::PlPgSql(block) = f.body else {
28443 panic!("expected PlPgSql body");
28444 };
28445 assert_eq!(block.statements.len(), 1);
28446 assert!(matches!(
28447 block.statements[0],
28448 PlPgSqlStmt::Return(ReturnTarget::New)
28449 ));
28450 }
28451
28452 #[test]
28453 fn create_function_or_replace_with_assignment() {
28454 // mailrs-shape trigger function: NEW.col := to_tsvector(...);
28455 // RETURN NEW.
28456 let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
28457BEGIN
28458 NEW.search_vector := to_tsvector('english', NEW.subject);
28459 RETURN NEW;
28460END;
28461$$";
28462 let s = parse(sql);
28463 let Statement::CreateFunction(f) = s else {
28464 panic!("expected CreateFunction");
28465 };
28466 assert!(f.or_replace);
28467 let FunctionBody::PlPgSql(block) = &f.body else {
28468 panic!("expected PlPgSql body");
28469 };
28470 assert_eq!(block.statements.len(), 2);
28471 // First statement: NEW.search_vector := to_tsvector(...)
28472 let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
28473 panic!("expected Assign as first stmt");
28474 };
28475 match target {
28476 AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
28477 other => panic!("expected NEW.col, got {other:?}"),
28478 }
28479 // Second statement: RETURN NEW
28480 assert!(matches!(
28481 block.statements[1],
28482 PlPgSqlStmt::Return(ReturnTarget::New)
28483 ));
28484 }
28485
28486 #[test]
28487 fn create_trigger_after_insert_or_update() {
28488 let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
28489 let s = parse(sql);
28490 let Statement::CreateTrigger(t) = s else {
28491 panic!("expected CreateTrigger");
28492 };
28493 assert_eq!(t.name, "tg");
28494 assert_eq!(t.table, "messages");
28495 assert_eq!(t.timing, TriggerTiming::After);
28496 assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
28497 assert_eq!(t.for_each, TriggerForEach::Row);
28498 assert_eq!(t.function, "update_sv");
28499 }
28500
28501 #[test]
28502 fn create_trigger_before_delete_execute_procedure_alias() {
28503 // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
28504 let sql =
28505 "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
28506 let s = parse(sql);
28507 let Statement::CreateTrigger(t) = s else {
28508 panic!("expected CreateTrigger");
28509 };
28510 assert_eq!(t.timing, TriggerTiming::Before);
28511 assert_eq!(t.events, vec![TriggerEvent::Delete]);
28512 }
28513
28514 #[test]
28515 fn drop_trigger_if_exists_round_trips() {
28516 // No parser support for DROP TRIGGER yet — added in v7.12.5
28517 // alongside the broader DROP …{IF EXISTS} cleanup. The
28518 // AST + Display impls are in place so we round-trip via
28519 // construction:
28520 let s = Statement::DropTrigger {
28521 name: "tg".into(),
28522 table: "messages".into(),
28523 if_exists: true,
28524 };
28525 assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
28526 }
28527
28528 #[test]
28529 fn trigger_ddl_display_roundtrips_through_parser() {
28530 // CREATE TRIGGER + its referenced CREATE FUNCTION must
28531 // Display → parse → same AST (modulo PL/pgSQL body
28532 // formatting which is parser-canonicalised).
28533 for sql in [
28534 "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
28535 "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
28536 ] {
28537 let s = parse(sql);
28538 let printed = s.to_string();
28539 let again = parse_statement(&printed)
28540 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28541 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28542 }
28543 }
28544}