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
432impl Parser {
433 /// Whether what follows an identifier ends an index key, which is how
434 /// an operator class is told from anything else in that position.
435 fn opclass_position_follows(next: Option<&Token>) -> bool {
436 match next {
437 // `ASC` / `DESC` have their own tokens; matching them as
438 // identifiers named "asc" / "desc" — which the first version of
439 // this did — never fires, and `(c text_pattern_ops DESC)` (which
440 // PG18.4 accepts, verified) went on failing to parse.
441 Some(Token::Comma | Token::RParen | Token::Asc | Token::Desc) => true,
442 Some(Token::Ident(w)) => {
443 w.eq_ignore_ascii_case("nulls") || w.eq_ignore_ascii_case("collate")
444 }
445 _ => false,
446 }
447 }
448}
449
450fn is_vector_opclass_name(name: &str) -> bool {
451 let lc = name.to_ascii_lowercase();
452 matches!(
453 lc.as_str(),
454 "vector_cosine_ops"
455 | "vector_l2_ops"
456 | "vector_ip_ops"
457 | "halfvec_cosine_ops"
458 | "halfvec_l2_ops"
459 | "halfvec_ip_ops"
460 | "sq8_cosine_ops"
461 | "sq8_l2_ops"
462 | "sq8_ip_ops"
463 // pg_trgm — trigram operator class. SPG's GIN index
464 // already uses tsvector tokens; trigram-style LIKE
465 // pattern matching still routes through a sequential
466 // scan, but the opclass name is accepted so PG schemas
467 // load.
468 | "gin_trgm_ops"
469 | "gist_trgm_ops"
470 // PG built-in btree opclasses occasionally appear in
471 // pg_dump output for column types with multiple
472 // sort orders (text_pattern_ops, varchar_pattern_ops,
473 // bpchar_pattern_ops).
474 | "text_pattern_ops"
475 | "varchar_pattern_ops"
476 | "bpchar_pattern_ops"
477 | "int4_ops"
478 | "int8_ops"
479 | "text_ops"
480 )
481}
482
483#[derive(Debug, Clone, PartialEq, Eq)]
484pub struct ParseError {
485 pub message: String,
486 /// Index into the token stream where parsing tripped. Not a byte offset.
487 /// v7.39 (read01 round 95) — the byte/char position is NOT stored here: a
488 /// field would grow every `Result<_, ParseError>` slot on the deeply
489 /// recursive parse stack and tip the nesting-budget frame cliff. PG's
490 /// 1-based char position is recovered on the cold error path by
491 /// [`syntax_error_position`], which re-tokenizes to map this token index.
492 pub token_pos: usize,
493}
494
495impl fmt::Display for ParseError {
496 /// v7.39 (round 322/V24) — the message ALONE. It used to be prefixed
497 /// with `parse error at token #N: `, which PG has no equivalent of:
498 /// the message bodies are already PG's verbatim (`LIMIT must not be
499 /// negative`, `invalid input syntax for type bigint: "abc"`), and the
500 /// prefix was SPG's internal token index leaking into every one of
501 /// them. `token_pos` stays a field — the wire recovers PG's 1-based
502 /// character position from it for the ErrorResponse `P`.
503 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504 f.write_str(&self.message)
505 }
506}
507
508impl From<LexError> for ParseError {
509 fn from(e: LexError) -> Self {
510 Self {
511 message: format!("lex: {e}"),
512 token_pos: 0,
513 }
514 }
515}
516
517/// v7.9.30 — parse a single expression (no trailing junk). Used by
518/// the engine to re-hydrate stored partial-index / unique-index
519/// predicates from their canonical Display form. The same Pratt
520/// parser the statement path uses; this entry point just skips the
521/// statement dispatch.
522pub fn parse_expression(input: &str) -> Result<Expr, ParseError> {
523 let (tokens, offsets) =
524 lexer::tokenize_with_offsets(input, false).map_err(|e| shape_lex_error(&e, input))?;
525 let mut p = Parser::new(tokens);
526 let expr = p
527 .parse_expr(0)
528 .and_then(|e| p.expect_eof().map(|()| e))
529 .map_err(|e| shape_syntax_error(e, input, &offsets))?;
530 Ok(expr)
531}
532
533/// Parse exactly one statement, swallow an optional trailing `;`, and require
534/// the token stream to end there. PG string semantics.
535pub fn parse_statement(input: &str) -> Result<Statement, ParseError> {
536 parse_statement_with(input, false)
537}
538
539/// v7.22 (round-13 T3) — dialect-aware entry: `backslash_escapes`
540/// selects MySQL-style string lexing (see `lexer::tokenize_with`).
541/// The engine threads its session flag through here.
542pub fn parse_statement_with(input: &str, backslash_escapes: bool) -> Result<Statement, ParseError> {
543 let (tokens, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes)
544 .map_err(|e| shape_lex_error(&e, input))?;
545 // The same session flag names the dialect for both the lexer and
546 // the type mapping.
547 let mut p = Parser::new_with_dialect(tokens, backslash_escapes).with_source(input, &offsets);
548 let stmt = (|| {
549 let stmt = p.parse_one_statement()?;
550 if matches!(p.peek(), Token::Semicolon) {
551 p.advance();
552 }
553 p.expect_eof()?;
554 Ok(stmt)
555 })()
556 .map_err(|e: ParseError| shape_syntax_error(e, input, &offsets))?;
557 Ok(stmt)
558}
559
560/// v7.39 (round 340, V56) — PG has exactly two syntax-error wordings:
561/// `syntax error at or near "<token>"` and `syntax error at end of input`
562/// (measured on 18.4 across a dozen shapes). SPG wrote its own per-site
563/// prose — `expected identifier, got Eof`, `unexpected token From in
564/// expression`, `expected end of input, got Ident("with")` — which named
565/// internal token types and, in the Debug forms, leaked the parser's own
566/// enum into a message clients read.
567///
568/// Applied once on the way out, so every construction site is covered and
569/// the token named is the one the error itself points at. Messages whose
570/// bodies are already PG's verbatim (`LIMIT must not be negative`,
571/// `invalid input syntax for type bigint: "abc"`) are left alone — those
572/// are PG's own errors, not its syntax error.
573fn shape_syntax_error(e: ParseError, input: &str, offsets: &[usize]) -> ParseError {
574 if !(e.message.starts_with("expected ") || e.message.starts_with("unexpected token ")) {
575 return e;
576 }
577 let message = match offending_lexeme(input, offsets, e.token_pos) {
578 Some(tok) => alloc::format!("syntax error at or near \"{tok}\""),
579 None => "syntax error at end of input".into(),
580 };
581 ParseError {
582 message,
583 token_pos: e.token_pos,
584 }
585}
586
587/// v7.39 (round 340, V56) — a lexer-level failure the way PG words it.
588/// Measured on 18.4: `unterminated quoted string at or near "'abc"`,
589/// `unterminated quoted identifier at or near ""abc"`, `unterminated /*
590/// comment at or near "/* x"` — the quoted part runs from the opening
591/// delimiter to the end of the input. SPG reported its own internal
592/// shape instead (`unterminated string literal at byte 7`), which named
593/// a byte offset no client can use.
594fn shape_lex_error(e: &lexer::LexError, input: &str) -> ParseError {
595 use lexer::LexErrorKind as K;
596 let from_here = input.get(e.pos..).map(str::trim_end).unwrap_or("");
597 let message = match &e.kind {
598 K::UnterminatedString => {
599 alloc::format!("unterminated quoted string at or near \"{from_here}\"")
600 }
601 K::UnterminatedQuotedIdent => {
602 alloc::format!("unterminated quoted identifier at or near \"{from_here}\"")
603 }
604 K::UnterminatedBlockComment => {
605 alloc::format!("unterminated /* comment at or near \"{from_here}\"")
606 }
607 // PG has no "unknown character" error of its own — the character
608 // is skipped and the parser reports the next token. SPG stops at
609 // the character itself and names it, which is the same shape.
610 K::UnknownChar(c) => alloc::format!("syntax error at or near \"{c}\""),
611 // The number-literal kinds already carry PG's `at or near` form.
612 other => alloc::format!(
613 "{}",
614 lexer::LexError {
615 kind: other.clone(),
616 pos: e.pos,
617 }
618 ),
619 };
620 ParseError {
621 message,
622 token_pos: 0,
623 }
624}
625
626/// The offending token exactly as it appears in the input, or `None` at
627/// end of input. PG echoes the source spelling — a lower-case `frm`
628/// reports as `frm`, not as a canonicalised keyword.
629fn offending_lexeme<'a>(input: &'a str, offsets: &[usize], token_pos: usize) -> Option<&'a str> {
630 let start = *offsets.get(token_pos)?;
631 if start >= input.len() {
632 return None;
633 }
634 let end = offsets
635 .get(token_pos + 1)
636 .copied()
637 .unwrap_or(input.len())
638 .min(input.len());
639 let seg = input.get(start..end)?.trim();
640 if seg.is_empty() {
641 return None;
642 }
643 // A quoted literal / identifier keeps its inner spaces; anything else
644 // ends at the first whitespace (the segment runs to the NEXT token's
645 // start, which may swallow a comment).
646 if seg.starts_with('\'') || seg.starts_with('"') || seg.starts_with('`') {
647 Some(seg)
648 } else {
649 seg.split_whitespace().next()
650 }
651}
652
653/// v7.39 (read01 round 95) — recover PG's 1-based CHARACTER error position for
654/// a [`ParseError::token_pos`]. Kept off the `ParseError` struct (and so off
655/// every recursive `Result` slot) to protect the nesting-budget frame cliff:
656/// this re-tokenizes `input` on the cold error path to map the failing token
657/// index to its start byte, then to a character offset. `backslash_escapes`
658/// must match the parse that produced `token_pos` (it barely shifts offsets,
659/// but stay consistent). Returns `None` when the index has no offset or the
660/// byte isn't a char boundary. The wire attaches it as the ErrorResponse `P`.
661#[must_use]
662pub fn syntax_error_position(
663 input: &str,
664 backslash_escapes: bool,
665 token_pos: usize,
666) -> Option<usize> {
667 let (_, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes).ok()?;
668 let byte_off = *offsets.get(token_pos)?;
669 if byte_off > input.len() || !input.is_char_boundary(byte_off) {
670 return None;
671 }
672 Some(input[..byte_off].chars().count() + 1)
673}
674
675struct Parser {
676 tokens: Vec<Token>,
677 pos: usize,
678 /// v7.39 (round 274) — the session's dialect, carried by the same
679 /// signal that drives string-literal escaping: `SET sql_mode` (only
680 /// MySQL clients and mysqldump preambles emit it) turns it on,
681 /// `SET standard_conforming_strings` (every pg_dump preamble) turns
682 /// it off. Needed here because the two dialects disagree about what
683 /// `REAL` means — see the type mapping below.
684 mysql_dialect: bool,
685 /// v7.30.2 (mailrs round-25 ask 2) — live nesting depth of the
686 /// mutually recursive expr/select parsers. Bounded so a deeply
687 /// nested input returns a parse error instead of overflowing
688 /// the stack (embed hosts die on overflow — it is an abort,
689 /// not a catchable error).
690 nest_depth: usize,
691 /// TABLESAMPLE lowering channel: the table-ref parser pushes a
692 /// `random() < p/100` predicate here; the enclosing SELECT
693 /// drains the list after its WHERE parses and ANDs the
694 /// predicates in. parse_bare_select save/restores around its
695 /// FROM+WHERE so nested selects only drain their own.
696 pending_sample_preds: Vec<Expr>,
697 /// v7.39 (round 691) — collation lowering channel, the same shape as
698 /// `pending_sample_preds` above. `expr COLLATE "name"` is ORDERING
699 /// information, and `ast::OrderBy` is where this parser keeps ordering
700 /// information (`desc`, `nulls_first`); the alternative — a new `Expr`
701 /// variant — puts a new arm on `eval_expr`, which this repo has
702 /// measured to overflow the debug stack. So while an ORDER BY KEY is
703 /// being parsed the postfix loop drops the name here instead of
704 /// refusing it, and the key's parser takes it.
705 ///
706 /// Only inside an ORDER BY key: everywhere else an unperformable
707 /// collation still errors, because accepting one at a COMPARISON and
708 /// ignoring it is the defect F36 exists to close.
709 in_order_by_key: bool,
710 order_key_collation: Option<String>,
711 /// POSITION(sub IN str) — while parsing the needle, the IN
712 /// keyword is the argument separator, not a membership test.
713 /// The postfix loop leaves IN unconsumed when this is set.
714 suppress_in_tail: bool,
715 /// Index of the token the last `advance()` returned — see
716 /// [`Parser::consumed_pos`].
717 last_consumed: usize,
718 /// v7.39 (round 506) — the statement's own text and the byte each token
719 /// starts at, so a MySQL projection item can report the SOURCE TEXT
720 /// MariaDB reports: `SELECT a + b` names its column `a + b`,
721 /// spacing and all. Only filled for a MySQL session — a PG one names
722 /// columns from the parsed shape and pays nothing for this.
723 src: Option<(String, Vec<usize>)>,
724}
725
726/// Max expr/select parser nesting (parens, subqueries, CASE, …).
727/// Real SQL nests a few dozen levels at the extreme. Each nesting level
728/// costs a parse_expr→parse_unary→parse_atom frame chain, so the budget
729/// exists to turn a deep statement into a catchable parse ERROR: a stack
730/// overflow is an abort, and in the server it does not fail one query, it
731/// takes the process down and every other connection with it.
732///
733/// v7.39 (round 507) — measured, because the figure here used to be a
734/// guess ("over 10 KiB in debug … comfortably inside a 2 MiB worker stack
735/// in BOTH debug and release"), and the debug half of that is wrong by
736/// more than an order of magnitude:
737///
738/// * RELEASE, on a 2 MiB worker stack: every recursive shape reaches
739/// this budget and errors. Verified against a live server for nested
740/// derived tables, parens, calls, CASE, IN-subqueries, scalar
741/// subqueries, NOT and unary minus — the server stayed up through all
742/// of them. This is the contract that matters, and it holds.
743/// * DEBUG: nested derived tables cost roughly 235 KiB of stack PER
744/// LEVEL, so parsing alone aborts around 35 levels on an 8 MiB stack
745/// and executing aborts around 8 inside a test thread. The budget is
746/// simply unreachable there, which is why a deep-nesting test has to
747/// ask for a large stack of its own — see `nesting_budget_errors_at`
748/// in the parser tests.
749/// v7.39 (round 541) — the pg_catalog relations SPG synthesises, in
750/// one place.
751///
752/// There were two copies of this fact: a curated list, used for BARE
753/// names, and — in `try_peek_meta_qualified` — no list at all, which
754/// rewrote `pg_catalog.<anything>` to `__spg_pg_<anything>` and left
755/// the engine to complain about a view it could not materialise. So
756/// writing the schema qualifier CHANGED THE ANSWER: `pg_stat_activity`
757/// had rows, `pg_catalog.pg_stat_activity` was an error.
758///
759/// PG puts `pg_catalog` at the implicit front of every search_path, so
760/// the two spellings name the same relation and must resolve the same
761/// way. Names NOT here (`pg_stat_activity`, `pg_locks`,
762/// `pg_stat_statements`, `pg_statio_user_tables`) route through the
763/// meta_view_result path under their own names and must not be
764/// rewritten; a name that is neither reaches the ordinary resolver,
765/// which reports that the relation does not exist — PG's answer.
766const SYNTHESISED_PG_CATALOGS: &[&str] = &[
767 "pg_am",
768 "pg_attrdef",
769 "pg_attribute",
770 "pg_cast",
771 "pg_db_role_setting",
772 "pg_conversion",
773 "pg_default_acl",
774 "pg_shadow",
775 "pg_sequences",
776 "pg_range",
777 "pg_partitioned_table",
778 "pg_language",
779 "pg_group",
780 "pg_authid",
781 "pg_class",
782 "pg_collation",
783 "pg_constraint",
784 "pg_database",
785 "pg_depend",
786 "pg_amop",
787 "pg_amproc",
788 "pg_opclass",
789 "pg_opfamily",
790 // v7.39 (read01 round 50) — COMMENT ON store, PG's pg_description.
791 "pg_description",
792 "pg_enum",
793 "pg_extension",
794 // v7.39 (round 541) — pg_dump reads it for every relation of kind
795 // 'f'. SPG has no foreign tables, so it is empty, which is also
796 // what PG reports on a database that has none.
797 "pg_foreign_table",
798 // v7.39 (round 541) — the empty-by-truth family; see
799 // EMPTY_PG_CATALOGS in spg-engine::system_catalog.
800 "pg_event_trigger",
801 "pg_file_settings",
802 "pg_foreign_data_wrapper",
803 "pg_foreign_server",
804 "pg_hba_file_rules",
805 "pg_ident_file_mappings",
806 "pg_init_privs",
807 "pg_parameter_acl",
808 "pg_prepared_xacts",
809 "pg_publication_namespace",
810 "pg_publication_rel",
811 "pg_publication_tables",
812 "pg_replication_origin",
813 "pg_replication_origin_status",
814 "pg_seclabel",
815 "pg_seclabels",
816 "pg_shdepend",
817 "pg_shdescription",
818 "pg_shmem_allocations",
819 "pg_shmem_allocations_numa",
820 "pg_shseclabel",
821 "pg_statistic_ext_data",
822 "pg_stats_ext",
823 "pg_stats_ext_exprs",
824 "pg_subscription_rel",
825 "pg_transform",
826 "pg_user_mapping",
827 "pg_user_mappings",
828 "pg_index",
829 "pg_indexes",
830 "pg_inherits",
831 // v7.39 (round 650) — the text-search catalogs SPG can fill
832 // honestly. `pg_ts_config_map` is deliberately NOT here: it maps
833 // token types to dictionaries and SPG has no token-type model,
834 // the same gap that leaves `ts_token_type` / `ts_debug` unbuilt.
835 "pg_ts_config",
836 "pg_ts_config_map",
837 "pg_ts_dict",
838 "pg_ts_parser",
839 "pg_ts_template",
840 "pg_matviews",
841 "pg_namespace",
842 // v7.39 (round 621)
843 "pg_operator",
844 "pg_policies",
845 "pg_policy",
846 "pg_proc",
847 "pg_publication",
848 "pg_replication_slots",
849 "pg_roles",
850 // v7.39 (round 143) — the rewrite-rule listing view.
851 // v7.39 (round 312) — and the rule catalogue itself, which
852 // `pg_get_ruledef(oid)` resolves against.
853 "pg_rewrite",
854 "pg_rules",
855 "pg_sequence",
856 "pg_settings",
857 "pg_stat_archiver",
858 "pg_stat_bgwriter",
859 "pg_stat_checkpointer",
860 "pg_stat_database",
861 "pg_stat_io",
862 "pg_stat_progress_analyze",
863 "pg_auth_members",
864 "pg_stat_progress_create_index",
865 "pg_stat_progress_vacuum",
866 "pg_stat_replication",
867 "pg_stat_slru",
868 "pg_stat_subscription_stats",
869 "pg_stat_user_functions",
870 "pg_stat_user_indexes",
871 "pg_stat_user_tables",
872 "pg_stat_wal",
873 "pg_prepared_statements",
874 "pg_largeobject",
875 "pg_largeobject_metadata",
876 "pg_statistic",
877 "pg_statistic_ext",
878 // v7.38.18 — the readable view over pg_statistic.
879 "pg_stats",
880 "pg_subscription",
881 "pg_tables",
882 "pg_tablespace",
883 // v7.39 (round 502) — the timezone catalogues. SPG resolved
884 // named zones correctly but could not list them, so a client
885 // populating a timezone picker got "relation does not exist".
886 "pg_timezone_abbrevs",
887 "pg_timezone_names",
888 "pg_trigger",
889 "pg_type",
890 "pg_user",
891 "pg_views",
892];
893
894const MAX_NEST_DEPTH: usize = 64;
895
896/// Stack accounting for the nesting budget, test-only.
897///
898/// `MAX_NEST_DEPTH` is a fixed count calibrated against a frame size
899/// that MOVES: a compiler upgrade grew the parser's debug frames and
900/// silently ate the margin until `nesting_budget_errors_cleanly` went
901/// from erroring cleanly to aborting on a stack overflow. A count
902/// cannot notice that on its own, so the budget is measured here and
903/// held to a ceiling.
904///
905/// The reading has to come from a helper whose OWN frame is the same at
906/// every call: debug slot placement does not follow source order, so a
907/// local's address inside the function under test is not that
908/// function's frame boundary. Two earlier probes were wrong that way —
909/// one read `&self.nest_depth`, which is the `Parser`'s address and
910/// never moves at all.
911#[cfg(test)]
912mod frame_meter {
913 extern crate std;
914 use std::cell::Cell;
915
916 // Per-THREAD, not global. `cargo test` runs tests in parallel and
917 // plenty of them parse nested expressions, so shared statics get
918 // stack addresses from several threads at once and the subtraction
919 // below turns into noise — it read 229,772 bytes per level that way,
920 // while passing when the test was run on its own.
921 std::thread_local! {
922 static AT_LO: Cell<usize> = const { Cell::new(0) };
923 static AT_HI: Cell<usize> = const { Cell::new(0) };
924 }
925
926 pub(super) const SAMPLE_LO: usize = 4;
927 pub(super) const SAMPLE_HI: usize = 24;
928
929 #[inline(never)]
930 pub(super) fn record(depth: usize) {
931 let anchor = 0u8;
932 let at = core::ptr::from_ref(&anchor) as usize;
933 if depth == SAMPLE_LO {
934 AT_LO.with(|c| c.set(at));
935 } else if depth == SAMPLE_HI {
936 AT_HI.with(|c| c.set(at));
937 }
938 }
939
940 /// Bytes of stack one nesting level costs, averaged over the span.
941 pub(super) fn bytes_per_level() -> usize {
942 let lo = AT_LO.with(Cell::get);
943 let hi = AT_HI.with(Cell::get);
944 assert!(lo > 0 && hi > 0, "meter never sampled: lo={lo} hi={hi}");
945 assert!(lo > hi, "stack grew upwards? lo={lo} hi={hi}");
946 (lo - hi) / (SAMPLE_HI - SAMPLE_LO)
947 }
948
949 pub(super) fn reset() {
950 AT_LO.with(|c| c.set(0));
951 AT_HI.with(|c| c.set(0));
952 }
953}
954
955/// v7.39 (read01 geo_ops.c) — prefix `@@` desugar target, out-of-line so
956/// the constructor's temporaries stay off `parse_unary`'s recursion frame.
957#[inline(never)]
958fn build_center_call(e: Expr) -> Expr {
959 Expr::FunctionCall {
960 name: alloc::string::String::from("center"),
961 args: alloc::vec![e],
962 }
963}
964
965/// Max consecutive binary operators at ONE precedence level
966/// (`a OR b OR c …`, `1+1+1…`). The chain builds iteratively at
967/// parse time but evaluates and drops recursively — depth beyond
968/// this overflows 2 MiB worker stacks (debug eval frames run
969/// multiple KiB). `IN (…)` lists are flat and unaffected.
970const MAX_BINARY_CHAIN: usize = 256;
971
972/// v7.22 (round-13 gap 5) — the kind keyword after `CONSTRAINT
973/// <name>` in a CREATE TABLE column list. FOREIGN KEY is not here:
974/// it keeps its dedicated path (`parse_table_level_fk`).
975enum NamedTableConstraintKind {
976 Check,
977 Unique,
978 PrimaryKey,
979 Exclude,
980}
981
982impl Parser {
983 fn new(tokens: Vec<Token>) -> Self {
984 Self::new_with_dialect(tokens, false)
985 }
986
987 fn new_with_dialect(tokens: Vec<Token>, mysql_dialect: bool) -> Self {
988 Self {
989 tokens,
990 mysql_dialect,
991 in_order_by_key: false,
992 order_key_collation: None,
993 pos: 0,
994 nest_depth: 0,
995 pending_sample_preds: Vec::new(),
996 suppress_in_tail: false,
997 last_consumed: 0,
998 src: None,
999 }
1000 }
1001
1002 /// Hand the parser the text it is parsing, for [`Parser::source_span`].
1003 fn with_source(mut self, input: &str, offsets: &[usize]) -> Self {
1004 if self.mysql_dialect {
1005 self.src = Some((input.to_string(), offsets.to_vec()));
1006 }
1007 self
1008 }
1009
1010 /// The source text spanning tokens `start ..= end`, trimmed.
1011 ///
1012 /// The offsets are token STARTS, so the span runs to the start of the
1013 /// token after `end` and gives back the whitespace between them —
1014 /// trimming is what makes `a + b FROM t` end at `b`.
1015 fn source_span(&self, start: usize, end: usize) -> Option<&str> {
1016 let (text, offsets) = self.src.as_ref()?;
1017 let from = *offsets.get(start)?;
1018 let to = *offsets.get(end + 1)?;
1019 text.get(from..to).map(str::trim_end)
1020 }
1021
1022 /// v7.30.2 (mailrs round-25 ask 2) — bump the expr/select
1023 /// nesting depth, erroring out cleanly past the budget.
1024 fn enter_nested(&mut self) -> Result<(), ParseError> {
1025 self.nest_depth += 1;
1026 #[cfg(test)]
1027 frame_meter::record(self.nest_depth);
1028 if self.nest_depth > MAX_NEST_DEPTH {
1029 self.nest_depth -= 1;
1030 return Err(self.err(alloc::format!(
1031 "statement nests deeper than {MAX_NEST_DEPTH} levels"
1032 )));
1033 }
1034 Ok(())
1035 }
1036
1037 fn peek(&self) -> &Token {
1038 // tokens always ends with Eof; pos is clamped in advance().
1039 &self.tokens[self.pos]
1040 }
1041
1042 fn advance(&mut self) -> Token {
1043 let t = mem::replace(&mut self.tokens[self.pos], Token::Eof);
1044 self.last_consumed = self.pos;
1045 if self.pos + 1 < self.tokens.len() {
1046 self.pos += 1;
1047 }
1048 t
1049 }
1050
1051 /// v7.39 (round 340, V56) — the index of the token `advance()` just
1052 /// returned. It was computed as `pos - 1`, which is wrong at both
1053 /// ends: `advance()` parks on the final Eof rather than running off
1054 /// the end (so `SELECT * FROM` named `FROM` where PG says `at end of
1055 /// input`), and after backtracking `pos` is no longer one past the
1056 /// token that failed. Recorded by `advance()` itself instead.
1057 const fn consumed_pos(&self) -> usize {
1058 self.last_consumed
1059 }
1060
1061 fn err(&self, message: String) -> ParseError {
1062 ParseError {
1063 message,
1064 token_pos: self.pos,
1065 }
1066 }
1067
1068 fn expect_eof(&self) -> Result<(), ParseError> {
1069 if matches!(self.peek(), Token::Eof) {
1070 Ok(())
1071 } else {
1072 Err(self.err(format!("expected end of input, got {:?}", self.peek())))
1073 }
1074 }
1075
1076 /// v7.14.0 — swallow every token up to (but not including) the
1077 /// next semicolon / EOF. Used by the dump-noise dispatcher
1078 /// to consume `COMMENT ON …`, `GRANT …`, `LOCK TABLES …`,
1079 /// etc. without modeling each grammar.
1080 /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
1081 /// Kinds SPG stores by name: TABLE / COLUMN / INDEX / VIEW / SEQUENCE /
1082 /// SCHEMA / TYPE / DATABASE / FUNCTION. Anything else (and the multi-word
1083 /// `CONSTRAINT c ON t` / `MATERIALIZED VIEW` forms) keeps the old
1084 /// swallow-as-no-op behaviour so a pg_dump tail still loads.
1085 fn parse_comment_on(&mut self) -> Result<Statement, ParseError> {
1086 let start = self.pos;
1087 self.advance(); // COMMENT
1088 if !matches!(self.peek(), Token::On) {
1089 self.pos = start;
1090 self.consume_until_statement_boundary();
1091 return Ok(Statement::Empty);
1092 }
1093 self.advance(); // ON
1094 let kind = match self.peek() {
1095 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
1096 Token::Table => "table".into(),
1097 _ => {
1098 self.consume_until_statement_boundary();
1099 return Ok(Statement::Empty);
1100 }
1101 };
1102 if !matches!(
1103 kind.as_str(),
1104 "table"
1105 | "column"
1106 | "index"
1107 | "view"
1108 | "sequence"
1109 | "schema"
1110 | "type"
1111 | "database"
1112 | "function"
1113 ) {
1114 self.consume_until_statement_boundary();
1115 return Ok(Statement::Empty);
1116 }
1117 self.advance(); // the kind keyword
1118 // The object name. ⚠️ `expect_ident_like` strips a leading
1119 // `<schema>.` qualifier and returns only the trailing ident (SPG is
1120 // single-schema), which would turn `COMMENT ON COLUMN t.c` into just
1121 // `c`. Read the dotted parts from raw tokens instead, then let a
1122 // 3-part `schema.t.c` drop its leading schema like everywhere else.
1123 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
1124 loop {
1125 match self.advance() {
1126 Token::Ident(s) | Token::QuotedIdent(s) => parts.push(s),
1127 other if unreserved_keyword_text(&other).is_some() => {
1128 parts.push(unreserved_keyword_text(&other).unwrap());
1129 }
1130 other => {
1131 return Err(ParseError {
1132 message: alloc::format!("expected identifier, got {other:?}"),
1133 token_pos: self.consumed_pos(),
1134 });
1135 }
1136 }
1137 if matches!(self.peek(), Token::Dot) {
1138 self.advance();
1139 } else {
1140 break;
1141 }
1142 }
1143 // COLUMN wants `table.column`; every other kind wants a bare name.
1144 let want = if kind == "column" { 2 } else { 1 };
1145 while parts.len() > want {
1146 parts.remove(0);
1147 }
1148 let name = parts.join(".");
1149 // v7.39 (round 710) — `COMMENT ON FUNCTION f(int, text) IS …`.
1150 // pg_dump writes the SIGNATURE, and the paren list was a syntax
1151 // error here — a dump carrying one function comment failed to
1152 // restore. The list is consumed (the comment store keys by name;
1153 // overload-precise comments are the function-predicate follow-up).
1154 if matches!(self.peek(), Token::LParen)
1155 && matches!(
1156 kind.as_str(),
1157 "function" | "procedure" | "aggregate" | "routine"
1158 )
1159 {
1160 let mut depth = 0usize;
1161 loop {
1162 match self.advance() {
1163 Token::LParen => depth += 1,
1164 Token::RParen => {
1165 depth -= 1;
1166 if depth == 0 {
1167 break;
1168 }
1169 }
1170 Token::Eof => {
1171 return Err(self.err(alloc::string::String::from(
1172 "unterminated argument list in COMMENT ON",
1173 )));
1174 }
1175 _ => {}
1176 }
1177 }
1178 }
1179 // `IS`
1180 if !matches!(self.peek(), Token::Is) {
1181 self.expect_keyword_ident("is")?;
1182 } else {
1183 self.advance();
1184 }
1185 let comment = match self.peek() {
1186 Token::Null => {
1187 self.advance();
1188 None
1189 }
1190 _ => Some(self.expect_string_literal()?),
1191 };
1192 Ok(Statement::CommentOn {
1193 kind,
1194 name,
1195 comment,
1196 })
1197 }
1198
1199 /// v7.39 (read01 round 57) — `GRANT <privs> ON <obj> TO <roles> [WITH GRANT
1200 /// OPTION]` / `REVOKE [GRANT OPTION FOR] <privs> ON <obj> FROM <roles>
1201 /// [CASCADE|RESTRICT]`.
1202 ///
1203 /// TABLE privileges are the real ones (stored, enforced, introspectable).
1204 /// Every other object class — SCHEMA / SEQUENCE / DATABASE / FUNCTION / …,
1205 /// and the no-ON `GRANT role TO role` membership form — parses into
1206 /// `GrantObject::Other` and no-ops in the engine, so a pg_dump that grants
1207 /// on them still restores.
1208 fn parse_grant_or_revoke(&mut self, grant: bool) -> Result<Statement, ParseError> {
1209 self.advance(); // GRANT / REVOKE
1210 // REVOKE's optional `GRANT OPTION FOR` prefix.
1211 let mut grant_option = false;
1212 if !grant
1213 && self.peek_keyword_ident("grant")
1214 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("option"))
1215 {
1216 self.advance(); // GRANT
1217 self.advance(); // OPTION
1218 self.expect_keyword_ident("for")?;
1219 grant_option = true;
1220 }
1221 // The privilege list: `ALL [PRIVILEGES] [(cols)]`, or comma-separated
1222 // words each with an optional COLUMN list.
1223 let mut privileges: Vec<GrantPriv> = Vec::new();
1224 if matches!(self.peek(), Token::All) {
1225 self.advance();
1226 if self.peek_keyword_ident("privileges") {
1227 self.advance();
1228 }
1229 // `GRANT ALL (col) ON t TO r` — every column privilege, on that
1230 // column only.
1231 if matches!(self.peek(), Token::LParen) {
1232 let columns = self.parse_grant_column_list()?;
1233 privileges.push(GrantPriv {
1234 word: "ALL".into(),
1235 columns,
1236 });
1237 }
1238 } else {
1239 loop {
1240 // SELECT and INSERT lex as reserved tokens, so they never
1241 // reach `expect_ident_like` as plain idents; the rest
1242 // (UPDATE / DELETE / TRUNCATE / REFERENCES / TRIGGER /
1243 // MAINTAIN) are ordinary identifiers.
1244 let w = match self.peek() {
1245 Token::Select => {
1246 self.advance();
1247 "SELECT".to_string()
1248 }
1249 Token::Insert => {
1250 self.advance();
1251 "INSERT".to_string()
1252 }
1253 // v7.39 (read01 round 60) — CREATE is a privilege word on a
1254 // schema / database, and it lexes as a reserved token.
1255 Token::Create => {
1256 self.advance();
1257 "CREATE".to_string()
1258 }
1259 // NOT upper-cased: in the no-ON shape (`GRANT devs TO
1260 // alice`) these "privilege words" are ROLE NAMES, and a
1261 // role name is case-sensitive. `priv_from_word` folds case
1262 // itself when they really are privileges.
1263 _ => self.expect_ident_like()?,
1264 };
1265 // v7.39 (read01 round 59) — the optional per-privilege COLUMN
1266 // list: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
1267 let columns = if matches!(self.peek(), Token::LParen) {
1268 self.parse_grant_column_list()?
1269 } else {
1270 Vec::new()
1271 };
1272 privileges.push(GrantPriv { word: w, columns });
1273 if matches!(self.peek(), Token::Comma) {
1274 self.advance();
1275 } else {
1276 break;
1277 }
1278 }
1279 }
1280 // v7.39 (read01 round 58) — no ON clause at all = `GRANT devs TO alice`:
1281 // role MEMBERSHIP. The words parsed as "privileges" are the role names.
1282 if !matches!(self.peek(), Token::On) {
1283 let roles: Vec<String> = core::mem::take(&mut privileges)
1284 .into_iter()
1285 .map(|p| p.word)
1286 .collect();
1287 let grantees = self.parse_grantee_list(grant)?;
1288 // `WITH ADMIN OPTION` / `GRANTED BY x` — accepted, ignored (SPG has
1289 // no admin-option layer: a member cannot re-grant).
1290 self.consume_until_statement_boundary();
1291 return Ok(finish_grant(
1292 grant,
1293 GrantStatement {
1294 privileges: Vec::new(),
1295 object: GrantObject::Roles(roles),
1296 grantees,
1297 grant_option,
1298 },
1299 ));
1300 }
1301 self.advance(); // ON
1302 // An optional object-class keyword. `TABLE` (or no keyword at all) is
1303 // the enforced case; anything else parses and no-ops.
1304 let mut class = "TABLE";
1305 match self.peek() {
1306 Token::Table => {
1307 self.advance();
1308 }
1309 Token::All => {
1310 // v7.39 (read01 round 61) — `ALL TABLES IN SCHEMA x` expands to
1311 // every table at GRANT time, like PG. `ALL SEQUENCES/FUNCTIONS
1312 // IN SCHEMA` stay no-ops and keep their own object class.
1313 self.advance(); // ALL
1314 let kind = match self.peek() {
1315 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
1316 // TABLES has its own token (SHOW TABLES owns it).
1317 Token::Tables | Token::Table => "tables".to_string(),
1318 _ => String::new(),
1319 };
1320 if !kind.is_empty() {
1321 self.advance();
1322 }
1323 // `IN SCHEMA <name>`
1324 if matches!(self.peek(), Token::In) {
1325 self.advance();
1326 if self.peek_keyword_ident("schema") {
1327 self.advance();
1328 let _schema = self.expect_ident_like()?;
1329 }
1330 }
1331 if kind != "tables" {
1332 self.consume_until_statement_boundary();
1333 return Ok(finish_grant(
1334 grant,
1335 GrantStatement {
1336 privileges,
1337 object: GrantObject::Other("ALL … IN SCHEMA".into()),
1338 grantees: Vec::new(),
1339 grant_option,
1340 },
1341 ));
1342 }
1343 let grantees = self.parse_grantee_list(grant)?;
1344 self.consume_until_statement_boundary();
1345 return Ok(finish_grant(
1346 grant,
1347 GrantStatement {
1348 privileges,
1349 object: GrantObject::AllTablesInSchema,
1350 grantees,
1351 grant_option,
1352 },
1353 ));
1354 }
1355 Token::Ident(w) | Token::QuotedIdent(w) => {
1356 let lc = w.to_ascii_lowercase();
1357 // v7.39 (read01 round 60) — SEQUENCE / SCHEMA / DATABASE are
1358 // real objects with real ACLs now.
1359 if matches!(lc.as_str(), "sequence" | "schema" | "database") {
1360 self.advance();
1361 let mut names: Vec<String> = Vec::new();
1362 loop {
1363 let mut parts: Vec<String> = Vec::new();
1364 loop {
1365 parts.push(self.expect_ident_like()?);
1366 if matches!(self.peek(), Token::Dot) {
1367 self.advance();
1368 } else {
1369 break;
1370 }
1371 }
1372 names.push(parts.pop().expect("at least one part"));
1373 if matches!(self.peek(), Token::Comma) {
1374 self.advance();
1375 } else {
1376 break;
1377 }
1378 }
1379 let grantees = self.parse_grantee_list(grant)?;
1380 let mut grant_option = grant_option;
1381 if grant && self.peek_keyword_ident("with") {
1382 self.advance();
1383 self.expect_keyword_ident("grant")?;
1384 self.expect_keyword_ident("option")?;
1385 grant_option = true;
1386 }
1387 self.consume_until_statement_boundary();
1388 let object = match lc.as_str() {
1389 "sequence" => GrantObject::Sequences(names),
1390 "schema" => GrantObject::Schemas(names),
1391 _ => GrantObject::Databases(names),
1392 };
1393 return Ok(finish_grant(
1394 grant,
1395 GrantStatement {
1396 privileges,
1397 object,
1398 grantees,
1399 grant_option,
1400 },
1401 ));
1402 }
1403 // v7.39 (read01 round 61) — `ON FUNCTION f(int)` is real. SPG
1404 // keys functions by NAME, so the argument list parses and is
1405 // dropped (an overload set shares one ACL — recorded residual).
1406 if matches!(lc.as_str(), "function" | "procedure" | "routine") {
1407 self.advance();
1408 let mut names: Vec<(String, Option<Vec<String>>)> = Vec::new();
1409 loop {
1410 let mut parts: Vec<String> = Vec::new();
1411 loop {
1412 parts.push(self.expect_ident_like()?);
1413 if matches!(self.peek(), Token::Dot) {
1414 self.advance();
1415 } else {
1416 break;
1417 }
1418 }
1419 let fname = parts.pop().expect("at least one part");
1420 // v7.39 (read01 round 62) — the signature picks the
1421 // overload, so it is captured.
1422 let sig = if matches!(self.peek(), Token::LParen) {
1423 Some(self.parse_function_signature_types()?)
1424 } else {
1425 None
1426 };
1427 names.push((fname, sig));
1428 if matches!(self.peek(), Token::Comma) {
1429 self.advance();
1430 } else {
1431 break;
1432 }
1433 }
1434 let grantees = self.parse_grantee_list(grant)?;
1435 self.consume_until_statement_boundary();
1436 return Ok(finish_grant(
1437 grant,
1438 GrantStatement {
1439 privileges,
1440 object: GrantObject::Functions(names),
1441 grantees,
1442 grant_option,
1443 },
1444 ));
1445 }
1446 if matches!(
1447 lc.as_str(),
1448 "type"
1449 | "domain"
1450 | "language"
1451 | "tablespace"
1452 | "large"
1453 | "foreign"
1454 | "parameter"
1455 ) {
1456 self.consume_until_statement_boundary();
1457 return Ok(finish_grant(
1458 grant,
1459 GrantStatement {
1460 privileges,
1461 object: GrantObject::Other(lc.to_ascii_uppercase()),
1462 grantees: Vec::new(),
1463 grant_option,
1464 },
1465 ));
1466 }
1467 class = "TABLE";
1468 }
1469 _ => {}
1470 }
1471 let _ = class;
1472 // The table list. Schema-qualified names drop their qualifier (SPG is
1473 // single-schema) — but read the dotted parts from raw tokens, since
1474 // `expect_ident_like` would silently swallow the leading part.
1475 let mut tables: Vec<String> = Vec::new();
1476 loop {
1477 let mut parts: Vec<String> = Vec::new();
1478 loop {
1479 parts.push(self.expect_ident_like()?);
1480 if matches!(self.peek(), Token::Dot) {
1481 self.advance();
1482 } else {
1483 break;
1484 }
1485 }
1486 tables.push(parts.pop().expect("at least one part"));
1487 if matches!(self.peek(), Token::Comma) {
1488 self.advance();
1489 } else {
1490 break;
1491 }
1492 }
1493 let grantees = self.parse_grantee_list(grant)?;
1494 if grant && self.peek_keyword_ident("with") {
1495 self.advance();
1496 self.expect_keyword_ident("grant")?;
1497 self.expect_keyword_ident("option")?;
1498 grant_option = true;
1499 }
1500 // REVOKE's trailing CASCADE / RESTRICT — SPG has no dependent grants
1501 // to cascade to (no re-granting), so both are accepted and ignored.
1502 if !grant && (self.peek_keyword_ident("cascade") || self.peek_keyword_ident("restrict")) {
1503 self.advance();
1504 }
1505 Ok(finish_grant(
1506 grant,
1507 GrantStatement {
1508 privileges,
1509 object: GrantObject::Tables(tables),
1510 grantees,
1511 grant_option,
1512 },
1513 ))
1514 }
1515
1516 /// v7.39 (read01 round 62) — the argument TYPES in a function signature:
1517 /// `(int, text)` or `(x int, y text)` (PG accepts either). Returns the type
1518 /// words; the caller normalises them into a signature key.
1519 fn parse_function_signature_types(&mut self) -> Result<Vec<String>, ParseError> {
1520 self.advance(); // (
1521 let mut types: Vec<String> = Vec::new();
1522 if matches!(self.peek(), Token::RParen) {
1523 self.advance();
1524 return Ok(types);
1525 }
1526 loop {
1527 // Collect the words of one argument up to a comma / close paren.
1528 let mut words: Vec<String> = Vec::new();
1529 loop {
1530 match self.peek() {
1531 Token::Comma | Token::RParen | Token::Eof => break,
1532 _ => {}
1533 }
1534 let tok = self.advance();
1535 match tok {
1536 Token::Ident(w) | Token::QuotedIdent(w) => words.push(w),
1537 other => {
1538 if let Some(w) = unreserved_keyword_text(&other) {
1539 words.push(w);
1540 }
1541 }
1542 }
1543 }
1544 // `name TYPE` or a bare `TYPE`. Several of PG's type names are
1545 // themselves several words (`double precision`, `character
1546 // varying`, `timestamp with time zone`), so "two words means the
1547 // first is a parameter name" reads the type off `f(double
1548 // precision)` as `precision`. v7.39 (round 282): recognise the
1549 // multi-word spellings first — a leading word that STARTS one of
1550 // them is part of the type, not a name.
1551 let joined = words.join(" ");
1552 let ty = if words.len() >= 2 && is_multiword_type_phrase(&joined) {
1553 joined
1554 } else if words.len() >= 2 {
1555 words[1..].join(" ")
1556 } else {
1557 words.first().cloned().unwrap_or_default()
1558 };
1559 types.push(ty);
1560 if matches!(self.peek(), Token::Comma) {
1561 self.advance();
1562 } else {
1563 break;
1564 }
1565 }
1566 if matches!(self.peek(), Token::RParen) {
1567 self.advance();
1568 }
1569 Ok(types)
1570 }
1571
1572 /// v7.39 (read01 round 59) — `( col, col, … )` after a privilege word.
1573 fn parse_grant_column_list(&mut self) -> Result<Vec<String>, ParseError> {
1574 self.advance(); // (
1575 let mut cols = Vec::new();
1576 loop {
1577 cols.push(self.expect_ident_like()?);
1578 if matches!(self.peek(), Token::Comma) {
1579 self.advance();
1580 } else {
1581 break;
1582 }
1583 }
1584 if !matches!(self.peek(), Token::RParen) {
1585 return Err(self.err(alloc::format!(
1586 "expected ')' to close the column list, got {:?}",
1587 self.peek()
1588 )));
1589 }
1590 self.advance(); // )
1591 Ok(cols)
1592 }
1593
1594 /// `TO <roles>` (grant) / `FROM <roles>` (revoke). An empty-string entry is
1595 /// PUBLIC.
1596 fn parse_grantee_list(&mut self, grant: bool) -> Result<Vec<String>, ParseError> {
1597 if grant {
1598 if matches!(self.peek(), Token::To) {
1599 self.advance();
1600 } else {
1601 self.expect_keyword_ident("to")?;
1602 }
1603 } else if matches!(self.peek(), Token::From) {
1604 self.advance();
1605 } else {
1606 self.expect_keyword_ident("from")?;
1607 }
1608 let mut grantees: Vec<String> = Vec::new();
1609 loop {
1610 // `GROUP name` is the legacy spelling of a plain role name.
1611 if self.peek_keyword_ident("group") {
1612 self.advance();
1613 }
1614 if self.peek_keyword_ident("public") {
1615 self.advance();
1616 grantees.push(String::new()); // PUBLIC
1617 } else {
1618 grantees.push(self.expect_ident_like()?);
1619 }
1620 if matches!(self.peek(), Token::Comma) {
1621 self.advance();
1622 } else {
1623 break;
1624 }
1625 }
1626 Ok(grantees)
1627 }
1628
1629 /// v7.39 (round 277) — `PREPARE <name> [(type, …)] AS <statement>`.
1630 /// The body keeps its `$N` placeholders; substitution happens at
1631 /// EXECUTE. The declared types are recorded for
1632 /// `pg_prepared_statements.parameter_types` but are not enforced —
1633 /// PG infers when the list is omitted, and SPG resolves the values
1634 /// at substitution time either way.
1635 fn parse_prepare(&mut self) -> Result<Statement, ParseError> {
1636 let start = self.pos;
1637 self.advance(); // PREPARE
1638 // v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'` is 2PC, a
1639 // different statement that happens to share the keyword. PG
1640 // ships with `max_prepared_transactions = 0` and reports it
1641 // this way; SPG has no prepared-transaction registry, so the
1642 // same wording is the accurate answer rather than a dodge.
1643 // Round 277 turned this from a silent no-op into a confusing
1644 // "expected AS in PREPARE" parse error.
1645 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
1646 self.advance();
1647 let gid = match self.advance() {
1648 Token::String(g) => g,
1649 other => {
1650 return Err(self.err(alloc::format!(
1651 "expected a transaction identifier after PREPARE TRANSACTION, got {other:?}"
1652 )));
1653 }
1654 };
1655 return Ok(Statement::PrepareTransaction(gid));
1656 }
1657 let name = self.expect_ident_like()?;
1658 let mut param_types = Vec::new();
1659 if matches!(self.peek(), Token::LParen) {
1660 self.advance();
1661 loop {
1662 let mut ty = self.expect_ident_like()?;
1663 // A parameterised type name (`numeric(10,2)`,
1664 // `varchar(20)`) keeps its argument list in the text.
1665 if matches!(self.peek(), Token::LParen) {
1666 let mut depth = 0usize;
1667 let mut buf = String::from("(");
1668 loop {
1669 match self.advance() {
1670 Token::LParen => {
1671 depth += 1;
1672 if depth > 1 {
1673 buf.push('(');
1674 }
1675 }
1676 Token::RParen => {
1677 depth -= 1;
1678 buf.push(')');
1679 if depth == 0 {
1680 break;
1681 }
1682 }
1683 Token::Comma => buf.push(','),
1684 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
1685 Token::Eof => break,
1686 _ => {}
1687 }
1688 }
1689 ty.push_str(&buf);
1690 }
1691 // r1049 — `PREPARE p(bigint[]) AS …`: the sixth `[]`
1692 // position, same family as the parameter list above.
1693 let array_suffix = self.consume_array_suffix();
1694 ty.push_str(&array_suffix);
1695 param_types.push(ty);
1696 match self.peek() {
1697 Token::Comma => {
1698 self.advance();
1699 }
1700 Token::RParen => {
1701 self.advance();
1702 break;
1703 }
1704 other => {
1705 return Err(self.err(alloc::format!(
1706 "expected ',' or ')' in PREPARE parameter list, got {other:?}"
1707 )));
1708 }
1709 }
1710 }
1711 }
1712 if !matches!(self.peek(), Token::As) {
1713 return Err(self.err(alloc::format!(
1714 "expected AS in PREPARE, got {:?}",
1715 self.peek()
1716 )));
1717 }
1718 self.advance();
1719 let body = self.parse_one_statement()?;
1720 // The Parser holds tokens, not the source text, so the
1721 // statement PG reports in `pg_prepared_statements.statement`
1722 // is rebuilt from the AST rather than sliced from the input.
1723 let _ = start;
1724 let mut source = alloc::format!("PREPARE {}", crate::ast::quote_ident(&name));
1725 if !param_types.is_empty() {
1726 source.push_str(" (");
1727 source.push_str(¶m_types.join(", "));
1728 source.push(')');
1729 }
1730 source.push_str(" AS ");
1731 source.push_str(&alloc::format!("{body}"));
1732 Ok(Statement::Prepare {
1733 name,
1734 param_types,
1735 body: alloc::boxed::Box::new(body),
1736 source,
1737 })
1738 }
1739
1740 /// v7.39 (round 277) — `EXECUTE <name> [(<expr>, …)]`.
1741 fn parse_execute(&mut self) -> Result<Statement, ParseError> {
1742 self.advance(); // EXECUTE
1743 let name = self.expect_ident_like()?;
1744 let mut args = Vec::new();
1745 if matches!(self.peek(), Token::LParen) {
1746 self.advance();
1747 if matches!(self.peek(), Token::RParen) {
1748 self.advance();
1749 } else {
1750 loop {
1751 args.push(self.parse_expr(0)?);
1752 match self.advance() {
1753 Token::Comma => {}
1754 Token::RParen => break,
1755 other => {
1756 return Err(self.err(alloc::format!(
1757 "expected ',' or ')' in EXECUTE arguments, got {other:?}"
1758 )));
1759 }
1760 }
1761 }
1762 }
1763 }
1764 Ok(Statement::Execute { name, args })
1765 }
1766
1767 /// v7.39 (round 277) — `DEALLOCATE {[PREPARE] <name> | ALL}`.
1768 /// v7.39 (round 278) — `CALL <proc>([args])`. There is no
1769 /// procedure catalog yet, so this reports PG's not-found error
1770 /// (with its HINT) rather than pretending the call ran.
1771 /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
1772 /// Bare `DISCARD` is a syntax error in PG; so it is here.
1773 fn parse_discard(&mut self) -> Result<Statement, ParseError> {
1774 self.advance(); // DISCARD
1775 let target = match self.advance() {
1776 Token::All => DiscardTarget::All,
1777 Token::Ident(w) | Token::QuotedIdent(w) => match w.to_ascii_lowercase().as_str() {
1778 "all" => DiscardTarget::All,
1779 "plans" => DiscardTarget::Plans,
1780 "sequences" => DiscardTarget::Sequences,
1781 "temp" | "temporary" => DiscardTarget::Temp,
1782 other => {
1783 return Err(self.err(format!(
1784 "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1785 )));
1786 }
1787 },
1788 other => {
1789 return Err(self.err(format!(
1790 "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1791 )));
1792 }
1793 };
1794 Ok(Statement::Discard(target))
1795 }
1796
1797 /// v7.39 (round 318, V51) — MySQL `KILL [HARD|SOFT] [CONNECTION|QUERY]
1798 /// <expr>`. MariaDB accepts an expression for the id (its own docs use
1799 /// `KILL connection_id()`), and the HARD / SOFT prefixes only pick how
1800 /// aggressively the server interrupts, which SPG does not distinguish.
1801 /// Bare `KILL <id>` means CONNECTION.
1802 fn parse_kill(&mut self) -> Result<Statement, ParseError> {
1803 self.advance(); // KILL
1804 let mut query_only = false;
1805 loop {
1806 // CONNECTION is a reserved keyword token (it also opens
1807 // `CREATE SUBSCRIPTION … CONNECTION '…'`), so it arrives as
1808 // `Token::Connection` rather than a bare ident.
1809 if matches!(self.peek(), Token::Connection) {
1810 self.advance();
1811 break;
1812 }
1813 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
1814 break;
1815 };
1816 match w.to_ascii_lowercase().as_str() {
1817 "hard" | "soft" => {
1818 self.advance();
1819 }
1820 "query" => {
1821 self.advance();
1822 query_only = true;
1823 break;
1824 }
1825 _ => break,
1826 }
1827 }
1828 let id = self.parse_expr(0)?;
1829 Ok(Statement::Kill {
1830 query_only,
1831 id: Box::new(id),
1832 })
1833 }
1834
1835 fn parse_call(&mut self) -> Result<Statement, ParseError> {
1836 self.advance(); // CALL
1837 let name = self.expect_ident_like()?;
1838 self.consume_until_statement_boundary();
1839 Ok(Statement::Call(name))
1840 }
1841
1842 fn parse_deallocate(&mut self) -> Result<Statement, ParseError> {
1843 self.advance(); // DEALLOCATE
1844 // PG accepts an optional noise `PREPARE` keyword here.
1845 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("prepare")) {
1846 self.advance();
1847 }
1848 if matches!(self.peek(), Token::All) {
1849 self.advance();
1850 return Ok(Statement::Deallocate(None));
1851 }
1852 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("all")) {
1853 self.advance();
1854 return Ok(Statement::Deallocate(None));
1855 }
1856 let name = self.expect_ident_like()?;
1857 Ok(Statement::Deallocate(Some(name)))
1858 }
1859
1860 fn consume_until_statement_boundary(&mut self) {
1861 loop {
1862 match self.peek() {
1863 Token::Semicolon | Token::Eof => return,
1864 _ => self.advance(),
1865 };
1866 }
1867 }
1868
1869 /// v7.38.18 — consume to the statement boundary like
1870 /// `consume_until_statement_boundary`, but pick out the collation a
1871 /// `CREATE DATABASE` asked for on the way.
1872 ///
1873 /// `LC_COLLATE 'de_DE.utf8'` and `LOCALE 'de_DE.utf8'` both count;
1874 /// `LC_CTYPE` does not, because SPG has no separate ctype and
1875 /// pretending to honour it would be the more misleading answer. An
1876 /// `=` between the keyword and the value is optional, as in PG.
1877 ///
1878 /// The whole statement used to be thrown away. Being single-database
1879 /// makes the NAME a no-op; it does not make the collation one.
1880 fn scan_database_collation_until_boundary(&mut self) -> Option<String> {
1881 let mut want_value = false;
1882 let mut found: Option<String> = None;
1883 loop {
1884 let tok = self.peek().clone();
1885 match &tok {
1886 Token::Semicolon | Token::Eof => break,
1887 Token::Ident(w) | Token::QuotedIdent(w)
1888 if w.eq_ignore_ascii_case("lc_collate") || w.eq_ignore_ascii_case("locale") =>
1889 {
1890 want_value = true;
1891 }
1892 Token::Eq if want_value => {}
1893 Token::String(v) if want_value => {
1894 found = Some(v.clone());
1895 want_value = false;
1896 }
1897 Token::Ident(v) | Token::QuotedIdent(v) if want_value => {
1898 found = Some(v.clone());
1899 want_value = false;
1900 }
1901 _ => want_value = false,
1902 }
1903 self.advance();
1904 }
1905 found
1906 }
1907
1908 /// v7.22 (round-13 T2) — consume to the statement boundary like
1909 /// `consume_until_statement_boundary`, but pick out the sequence
1910 /// name on the way: either `SEQUENCE NAME <ident>` (identity
1911 /// columns) or the first string literal (`nextval('<seq>')`).
1912 /// Schema qualifiers and `::regclass` casts are stripped.
1913 fn scan_sequence_name_until_boundary(&mut self) -> Option<String> {
1914 let mut seq: Option<String> = None;
1915 let mut after_sequence_kw = false;
1916 let mut after_name_kw = false;
1917 loop {
1918 match self.peek().clone() {
1919 Token::Semicolon | Token::Eof => break,
1920 Token::Ident(s) | Token::QuotedIdent(s) => {
1921 if after_name_kw && seq.is_none() {
1922 self.advance();
1923 let mut name = s;
1924 // `SEQUENCE NAME public.groups_id_seq` — keep
1925 // the bare name, drop qualifiers.
1926 while matches!(self.peek(), Token::Dot) {
1927 self.advance();
1928 if let Token::Ident(n) | Token::QuotedIdent(n) = self.advance() {
1929 name = n;
1930 }
1931 }
1932 seq = Some(name);
1933 after_name_kw = false;
1934 continue;
1935 }
1936 if after_sequence_kw && s.eq_ignore_ascii_case("name") {
1937 after_name_kw = true;
1938 after_sequence_kw = false;
1939 } else {
1940 after_sequence_kw = s.eq_ignore_ascii_case("sequence");
1941 }
1942 self.advance();
1943 }
1944 Token::String(s) => {
1945 if seq.is_none() {
1946 // `nextval('public.groups_id_seq'::regclass)`
1947 let bare = s
1948 .rsplit_once('.')
1949 .map_or_else(|| s.clone(), |(_, b)| b.to_string());
1950 seq = Some(bare);
1951 }
1952 self.advance();
1953 }
1954 _ => {
1955 after_sequence_kw = false;
1956 after_name_kw = false;
1957 self.advance();
1958 }
1959 }
1960 }
1961 seq
1962 }
1963
1964 /// v7.39 (round 621) — is the next token the keyword `BY`?
1965 ///
1966 /// `pg_get_keywords()` classes `by` as `U` (unreserved), so it is a legal
1967 /// column, table and alias name — and SPG lexed it into a dedicated
1968 /// `Token::By`, which made it unusable as a name ANYWHERE. Of the seven
1969 /// two-letter keywords the lexer knew, this was the only one PG leaves
1970 /// unreserved (`as`, `in`, `on`, `or`, `to` are reserved and `is` is `T`).
1971 ///
1972 /// The token is gone; the three clauses that own the word — GROUP BY,
1973 /// ORDER BY, PARTITION BY — and the handful of other places that expect it
1974 /// ask this instead. Adding it to the unreserved-identifier table was not
1975 /// enough on its own: identifier positions that match the token shape
1976 /// directly (an index's column list, a table alias) never consult that
1977 /// table, so `CREATE INDEX … ON t(by)` and `FROM t AS by` still failed.
1978 /// Not lexing it as a keyword closes the whole class rather than the two
1979 /// positions that happened to be noticed.
1980 fn peek_is_by(&self) -> bool {
1981 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by"))
1982 }
1983
1984 /// v7.39 (round 621) — the optional `CASCADE` / `RESTRICT` trailer a DROP
1985 /// takes. Accepted and dropped: SPG tracks no dependents to cascade to,
1986 /// which is what `DROP TABLE` and `DROP INDEX` have done since v7.14.
1987 fn consume_drop_behaviour(&mut self) {
1988 if matches!(
1989 self.peek(),
1990 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") || s.eq_ignore_ascii_case("restrict")
1991 ) {
1992 self.advance();
1993 }
1994 }
1995
1996 fn expect_ident_like(&mut self) -> Result<String, ParseError> {
1997 let first = match self.advance() {
1998 Token::Ident(s) | Token::QuotedIdent(s) => s,
1999 // v7.37.43-T4 — PG-unreserved keywords are legal identifiers
2000 // per PG's `pg_get_keywords()` classification. SPG tokenizes
2001 // these as named variants for parsing leverage in the
2002 // contexts that own them (`RELEASE SAVEPOINT`, `SHOW name`,
2003 // `BEGIN`, etc.), but they MUST still be usable as table /
2004 // column / alias names in DDL+DML. Sentori migrations like
2005 // 0001_init.sql ship `release TEXT NOT NULL` in the events
2006 // table — the `events.release` column carries the release
2007 // identifier string. Pre-T4 this triggered "expected
2008 // identifier, got Release" and blocked every drop-in user
2009 // whose schema had a column / alias with one of these names.
2010 other if unreserved_keyword_text(&other).is_some() => {
2011 unreserved_keyword_text(&other).unwrap()
2012 }
2013 other => {
2014 return Err(ParseError {
2015 message: format!("expected identifier, got {other:?}"),
2016 token_pos: self.consumed_pos(),
2017 });
2018 }
2019 };
2020 // v7.14.0 — strip optional `<schema>.` prefix. PG dumps
2021 // qualify every name with `public.` (and pg_catalog.* for
2022 // functions); SPG is single-schema so we discard the
2023 // prefix and return only the trailing ident. Same shape
2024 // also handles MySQL `db.tbl` cross-database refs (SPG
2025 // ignores the db part).
2026 if matches!(self.peek(), Token::Dot) {
2027 self.advance();
2028 match self.advance() {
2029 Token::Ident(s) | Token::QuotedIdent(s) => return Ok(s),
2030 other if unreserved_keyword_text(&other).is_some() => {
2031 return Ok(unreserved_keyword_text(&other).unwrap());
2032 }
2033 other => {
2034 return Err(ParseError {
2035 message: format!("expected identifier after '{first}.', got {other:?}"),
2036 token_pos: self.consumed_pos(),
2037 });
2038 }
2039 }
2040 }
2041 Ok(first)
2042 }
2043
2044 #[allow(clippy::too_many_lines)]
2045 fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
2046 // v7.14.0 — empty / comment-only / semicolon-only input
2047 // (after the lexer strips line + block + MySQL
2048 // conditional comments) lands as Statement::Empty.
2049 // pg_dump and mysqldump emit several wrappers that
2050 // collapse to nothing after stripping (`/*!40101 SET …
2051 // */;`, blank lines between statements); the engine
2052 // returns CommandOk no-op so the dump loads cleanly.
2053 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
2054 return Ok(Statement::Empty);
2055 }
2056 // v7.14.0 — pg_dump / mysqldump "noise" statements:
2057 // catalog / metadata DDL that has no behavioural effect
2058 // on SPG's single-schema, single-database, single-user
2059 // model. Consume the whole statement up to the next
2060 // semicolon / EOF and return Empty. This is broader than
2061 // the per-keyword DROP / SET / COMMENT arms but lets the
2062 // long tail of `LOCK TABLES`, `UNLOCK TABLES`, `GRANT`,
2063 // `REVOKE`, `ALTER OWNER TO`, `\restrict`, `\unrestrict`,
2064 // `BEGIN; COMMIT;` wrappers, etc. all pass through.
2065 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
2066 let lc = s.to_ascii_lowercase();
2067 // v7.39 (read01 round 50) — COMMENT ON is a real statement now.
2068 if lc == "comment" {
2069 return self.parse_comment_on();
2070 }
2071 // v7.39 (read01 round 57) — so is GRANT / REVOKE.
2072 if lc == "grant" || lc == "revoke" {
2073 return self.parse_grant_or_revoke(lc == "grant");
2074 }
2075 // v7.39 (round 277) — the SQL-level prepared-statement
2076 // surface is REAL now. It used to be accepted and dropped
2077 // on the theory that "real execution still happens via the
2078 // extended-query flow" — true only for a driver that uses
2079 // that flow; a plain SQL PREPARE / EXECUTE returned no
2080 // rows at all.
2081 if lc == "prepare" {
2082 return self.parse_prepare();
2083 }
2084 if lc == "execute" {
2085 return self.parse_execute();
2086 }
2087 if lc == "deallocate" {
2088 return self.parse_deallocate();
2089 }
2090 // v7.39 (round 278) — `CALL <proc>(<args>)` used to be
2091 // accepted and dropped, so an application's stored-procedure
2092 // invocation reported success and did nothing. SPG has no
2093 // procedure catalog, so every CALL names a procedure that
2094 // does not exist — which is exactly what PG says.
2095 if lc == "call" {
2096 return self.parse_call();
2097 }
2098 // v7.39 (round 318, V51) — MySQL `KILL`. Not dump noise: it
2099 // names one connection and acts on it.
2100 if lc == "kill" {
2101 return self.parse_kill();
2102 }
2103 if lc == "discard" {
2104 return self.parse_discard();
2105 }
2106 // v7.39 (round 696) — REASSIGN OWNED BY <role> [, …] TO <role>.
2107 // Still performs nothing; the roles are carried out so a name
2108 // that does not exist is refused, as PG18 refuses it.
2109 if lc == "reassign" {
2110 self.advance();
2111 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("owned")) {
2112 self.advance();
2113 }
2114 if self.peek_is_by() {
2115 self.advance();
2116 }
2117 // Only the roles BEFORE the TO are the ones that must
2118 // exist — `TO` names the new owner, which PG checks as
2119 // well, so both lists are collected.
2120 let mut names = self.take_comma_separated_names();
2121 if matches!(self.peek(), Token::To) {
2122 self.advance();
2123 names.extend(self.take_comma_separated_names());
2124 }
2125 self.consume_until_statement_boundary();
2126 return Ok(Statement::ValidateOnly {
2127 kind: crate::ast::ValidateOnlyKind::RoleName,
2128 names,
2129 });
2130 }
2131 // v7.39 (round 696) — SECURITY LABEL. PG18 refuses it
2132 // unconditionally with `no security label providers have been
2133 // loaded`, whatever object it names, because none is loaded.
2134 // SPG has none either; accepting it told the caller a label had
2135 // been applied when nothing anywhere records one.
2136 if lc == "security" {
2137 self.consume_until_statement_boundary();
2138 return Ok(Statement::ValidateOnly {
2139 kind: crate::ast::ValidateOnlyKind::SecurityLabel,
2140 names: Vec::new(),
2141 });
2142 }
2143 if is_dump_noise_statement(&lc) {
2144 self.consume_until_statement_boundary();
2145 return Ok(Statement::Empty);
2146 }
2147 }
2148 match self.peek() {
2149 Token::Select => self.parse_select_stmt(),
2150 // v7.37.17 (17.6 siblings) — a statement opening with a
2151 // parenthesized query group: `(SELECT … UNION …)
2152 // INTERSECT …`. parse_bare_select's group arm consumes
2153 // the parens; the select parser handles the outer chain
2154 // and tail.
2155 Token::LParen
2156 if matches!(
2157 self.tokens.get(self.pos + 1),
2158 Some(Token::Select | Token::LParen | Token::Values)
2159 ) =>
2160 {
2161 self.parse_select_stmt()
2162 }
2163 // v7.37.17 (17.6 siblings) — top-level bare VALUES
2164 // statement (`VALUES (1), (2) [ORDER BY …] [LIMIT …]`).
2165 // Lowers to the same UNION ALL chain the FROM-position
2166 // form uses, then reuses the shared SELECT tail.
2167 Token::Values => {
2168 self.advance(); // VALUES
2169 let mut head = self.parse_values_rows_body()?;
2170 self.parse_select_tail_into(&mut head)?;
2171 Ok(Statement::Select(head))
2172 }
2173 // SQL-standard `TABLE name` shorthand for
2174 // `SELECT * FROM name` — pg_dump never emits it, but
2175 // psql users and PG docs use it constantly. Set-op
2176 // chains and the ORDER BY/LIMIT tail compose like any
2177 // SELECT head.
2178 Token::Table
2179 if matches!(
2180 self.tokens.get(self.pos + 1),
2181 Some(Token::Ident(_) | Token::QuotedIdent(_))
2182 ) =>
2183 {
2184 let mut head = self.parse_table_shorthand()?;
2185 self.parse_setop_chain_into(&mut head)?;
2186 self.parse_select_tail_into(&mut head)?;
2187 Ok(Statement::Select(head))
2188 }
2189 // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2190 // body is a dollar-quoted plpgsql block (lexer already
2191 // collapsed `$$…$$` into a single Token::String).
2192 // v7.16.2 — mailrs round-10 A.2: parse the body as a
2193 // real PlPgSqlBlock so the engine can EXECUTE it at
2194 // top level instead of silently swallowing. Pre-
2195 // v7.16.2 the parser threw the body away and the
2196 // engine returned CommandOk for the entire DO; that
2197 // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2198 // $$` into a SEV-1 silent no-op (the IF + the rename
2199 // were both invisible — mailrs's migrate-042 didn't
2200 // actually run). Now the body parses + executes;
2201 // EmbeddedSql inside the block runs immediately
2202 // against the engine (not deferred — we're at top
2203 // level, not inside a trigger row-write loop).
2204 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2205 self.advance();
2206 let body_text = match self.advance() {
2207 Token::String(s) => s,
2208 other => {
2209 return Err(self.err(alloc::format!(
2210 "expected dollar-quoted body after DO, got {other:?}"
2211 )));
2212 }
2213 };
2214 // Optional `LANGUAGE <name>` trailer (idents only).
2215 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2216 self.advance();
2217 let _ = self.expect_ident_like()?;
2218 }
2219 // Parse the body — same shape CREATE FUNCTION
2220 // uses for trigger function bodies. If the body
2221 // doesn't parse cleanly we surface the error
2222 // (better than silent no-op).
2223 let block = parse_plpgsql_body(&body_text)?;
2224 Ok(Statement::DoBlock(block))
2225 }
2226 // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2227 // WITH isn't a reserved token in our lexer — comes through
2228 // as `Token::Ident("with")` (case-insensitive).
2229 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2230 self.advance();
2231 self.parse_with_cte_then_select()
2232 }
2233 // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2234 // an identifier — not a reserved keyword.
2235 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2236 self.advance();
2237 let mut analyze = false;
2238 let mut suggest = false;
2239 let mut costs_off = false;
2240 let mut buffers = false;
2241 let mut timing_off = false;
2242 let mut settings = false;
2243 let mut wal = false;
2244 let mut summary_off = false;
2245 let mut format = crate::ast::ExplainFormat::Text;
2246 // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2247 // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2248 // options are comma-separated. Booleans default to ON
2249 // when the value token is omitted (matches PG).
2250 if matches!(self.peek(), Token::LParen) {
2251 self.advance();
2252 loop {
2253 let opt = match self.peek().clone() {
2254 Token::Ident(s) | Token::QuotedIdent(s) => s,
2255 other => {
2256 return Err(self.err(format!(
2257 "expected option keyword inside EXPLAIN (…), got {other:?}"
2258 )));
2259 }
2260 };
2261 self.advance();
2262 if opt.eq_ignore_ascii_case("suggest") {
2263 suggest = true;
2264 // SUGGEST takes no explicit value today.
2265 } else if opt.eq_ignore_ascii_case("costs") {
2266 // PG syntax: `COSTS [ON | OFF]`. Default
2267 // when value omitted is ON, so plain
2268 // `COSTS` is a no-op. `COSTS OFF` flips.
2269 // `ON` lexes to `Token::On` (reserved
2270 // keyword in JOIN ... ON contexts); accept
2271 // it alongside the bare Ident form so the
2272 // grammar matches PG verbatim.
2273 let value = match self.peek().clone() {
2274 Token::On => {
2275 self.advance();
2276 true
2277 }
2278 Token::Ident(v) | Token::QuotedIdent(v)
2279 if v.eq_ignore_ascii_case("off") =>
2280 {
2281 self.advance();
2282 false
2283 }
2284 Token::Ident(v) | Token::QuotedIdent(v)
2285 if v.eq_ignore_ascii_case("true") =>
2286 {
2287 self.advance();
2288 true
2289 }
2290 _ => true,
2291 };
2292 costs_off = !value;
2293 } else if opt.eq_ignore_ascii_case("analyze")
2294 || opt.eq_ignore_ascii_case("analyse")
2295 {
2296 // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2297 // Same default-ON rule as ANALYZE keyword form.
2298 let value = match self.peek().clone() {
2299 Token::On => {
2300 self.advance();
2301 true
2302 }
2303 Token::Ident(v) | Token::QuotedIdent(v)
2304 if v.eq_ignore_ascii_case("off") =>
2305 {
2306 self.advance();
2307 false
2308 }
2309 Token::Ident(v) | Token::QuotedIdent(v)
2310 if v.eq_ignore_ascii_case("true") =>
2311 {
2312 self.advance();
2313 true
2314 }
2315 _ => true,
2316 };
2317 analyze = value;
2318 } else if opt.eq_ignore_ascii_case("buffers") {
2319 // v7.37.22 — `BUFFERS [ON|OFF]`.
2320 let value = match self.peek().clone() {
2321 Token::On => {
2322 self.advance();
2323 true
2324 }
2325 Token::Ident(v) | Token::QuotedIdent(v)
2326 if v.eq_ignore_ascii_case("off") =>
2327 {
2328 self.advance();
2329 false
2330 }
2331 Token::Ident(v) | Token::QuotedIdent(v)
2332 if v.eq_ignore_ascii_case("true") =>
2333 {
2334 self.advance();
2335 true
2336 }
2337 _ => true,
2338 };
2339 buffers = value;
2340 } else if opt.eq_ignore_ascii_case("timing") {
2341 // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2342 // the measured wall-clock annotation.
2343 let value = match self.peek().clone() {
2344 Token::On => {
2345 self.advance();
2346 true
2347 }
2348 Token::Ident(v) | Token::QuotedIdent(v)
2349 if v.eq_ignore_ascii_case("off") =>
2350 {
2351 self.advance();
2352 false
2353 }
2354 Token::Ident(v) | Token::QuotedIdent(v)
2355 if v.eq_ignore_ascii_case("true") =>
2356 {
2357 self.advance();
2358 true
2359 }
2360 _ => true,
2361 };
2362 timing_off = !value;
2363 } else if opt.eq_ignore_ascii_case("settings") {
2364 settings = true;
2365 } else if opt.eq_ignore_ascii_case("wal") {
2366 wal = true;
2367 } else if opt.eq_ignore_ascii_case("summary") {
2368 // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2369 // gates the trailing Planning/Execution Time
2370 // lines now (was accept-and-no-op).
2371 let value = match self.peek().clone() {
2372 Token::On => {
2373 self.advance();
2374 true
2375 }
2376 Token::Ident(v) | Token::QuotedIdent(v)
2377 if v.eq_ignore_ascii_case("off") =>
2378 {
2379 self.advance();
2380 false
2381 }
2382 Token::Ident(v) | Token::QuotedIdent(v)
2383 if v.eq_ignore_ascii_case("true") =>
2384 {
2385 self.advance();
2386 true
2387 }
2388 _ => true,
2389 };
2390 summary_off = !value;
2391 } else if opt.eq_ignore_ascii_case("verbose")
2392 || opt.eq_ignore_ascii_case("format")
2393 {
2394 // v7.37.22 — accept-but-no-op the remaining
2395 // PG options so EXPLAIN-using clients
2396 // (pgAdmin / DataGrip) don't see syntax
2397 // errors. FORMAT takes a value (text /
2398 // json / yaml / xml); skip the next token
2399 // if it's an ident.
2400 if opt.eq_ignore_ascii_case("format") {
2401 if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2402 {
2403 self.advance();
2404 format = match v.to_ascii_lowercase().as_str() {
2405 "text" => crate::ast::ExplainFormat::Text,
2406 "json" => crate::ast::ExplainFormat::Json,
2407 "xml" => crate::ast::ExplainFormat::Xml,
2408 "yaml" => crate::ast::ExplainFormat::Yaml,
2409 other => {
2410 return Err(self.err(format!(
2411 "EXPLAIN (FORMAT …): unknown format {other:?}; \
2412 supports text, json, xml, yaml"
2413 )));
2414 }
2415 };
2416 }
2417 } else {
2418 // VERBOSE / SUMMARY take optional ON/OFF;
2419 // consume if present.
2420 if matches!(self.peek(), Token::On) {
2421 self.advance();
2422 } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2423 self.peek().clone()
2424 && (v.eq_ignore_ascii_case("off")
2425 || v.eq_ignore_ascii_case("true"))
2426 {
2427 self.advance();
2428 let _ = v;
2429 }
2430 }
2431 } else {
2432 return Err(self.err(format!(
2433 "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2434 )));
2435 }
2436 if matches!(self.peek(), Token::Comma) {
2437 self.advance();
2438 continue;
2439 }
2440 break;
2441 }
2442 if !matches!(self.peek(), Token::RParen) {
2443 return Err(self.err(format!(
2444 "expected ')' after EXPLAIN options, got {:?}",
2445 self.peek()
2446 )));
2447 }
2448 self.advance();
2449 } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2450 && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2451 {
2452 self.advance();
2453 analyze = true;
2454 }
2455 // v7.39 (round 224) — the body may open with WITH (CTEs);
2456 // route through the same CTE-then-SELECT path the top-level
2457 // WITH statement uses. v7.39 (round 225) — DML bodies parse
2458 // too (PG explains INSERT / UPDATE / DELETE).
2459 let inner = match self.peek().clone() {
2460 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2461 self.advance();
2462 self.parse_with_cte_then_select()?
2463 }
2464 Token::Insert => self.parse_insert_stmt(false)?,
2465 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2466 self.advance();
2467 self.parse_update_after_keyword()?
2468 }
2469 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2470 self.advance();
2471 self.parse_delete_after_keyword()?
2472 }
2473 _ => self.parse_select_stmt()?,
2474 };
2475 if !matches!(
2476 inner,
2477 Statement::Select(_)
2478 | Statement::Insert(_)
2479 | Statement::Update(_)
2480 | Statement::Delete(_)
2481 ) {
2482 return Err(self.err(format!(
2483 "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2484 )));
2485 }
2486 Ok(Statement::Explain(crate::ast::ExplainStatement {
2487 analyze,
2488 inner: Box::new(inner),
2489 suggest,
2490 costs_off,
2491 buffers,
2492 timing_off,
2493 settings,
2494 wal,
2495 summary_off,
2496 format,
2497 }))
2498 }
2499 Token::Create => self.parse_create_stmt(),
2500 Token::Insert => self.parse_insert_stmt(false),
2501 // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2502 // spelling; route to the same handler. DESC is the
2503 // reserved ORDER BY token, so it gets its own arm.
2504 Token::Ident(s)
2505 if s.eq_ignore_ascii_case("describe")
2506 && matches!(
2507 self.tokens.get(self.pos + 1),
2508 Some(Token::Ident(_) | Token::QuotedIdent(_))
2509 ) =>
2510 {
2511 self.advance();
2512 let table = self.expect_ident_like()?;
2513 Ok(Statement::ShowColumns(table))
2514 }
2515 Token::Desc
2516 if matches!(
2517 self.tokens.get(self.pos + 1),
2518 Some(Token::Ident(_) | Token::QuotedIdent(_))
2519 ) =>
2520 {
2521 self.advance();
2522 let table = self.expect_ident_like()?;
2523 Ok(Statement::ShowColumns(table))
2524 }
2525 // `COPY table [(cols)] TO STDOUT` — the export half of
2526 // pg_dump's COPY pair (the FROM stdin half rides the
2527 // embed import path). Options need a format design and
2528 // error honestly.
2529 Token::Ident(s)
2530 if s.eq_ignore_ascii_case("copy")
2531 && matches!(
2532 self.tokens.get(self.pos + 1),
2533 Some(Token::Ident(_) | Token::QuotedIdent(_))
2534 ) =>
2535 {
2536 self.advance(); // COPY
2537 let table = self.expect_ident_like()?;
2538 let columns = if matches!(self.peek(), Token::LParen) {
2539 self.advance();
2540 let mut cols = alloc::vec![self.expect_ident_like()?];
2541 while matches!(self.peek(), Token::Comma) {
2542 self.advance();
2543 cols.push(self.expect_ident_like()?);
2544 }
2545 if !matches!(self.peek(), Token::RParen) {
2546 return Err(self.err(format!(
2547 "expected ')' after COPY column list, got {:?}",
2548 self.peek()
2549 )));
2550 }
2551 self.advance();
2552 Some(cols)
2553 } else {
2554 None
2555 };
2556 // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2557 // endpoint. (FROM STDIN still rides the wire/import path —
2558 // its data arrives out of band.)
2559 if matches!(self.peek(), Token::From)
2560 && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2561 {
2562 self.advance(); // FROM
2563 let Token::String(path) = self.advance() else {
2564 unreachable!()
2565 };
2566 let options = self.parse_copy_to_options()?;
2567 return Ok(Statement::CopyFromFile {
2568 table,
2569 columns,
2570 path,
2571 options,
2572 });
2573 }
2574 if !matches!(self.peek(), Token::To) {
2575 return Err(self.err(format!(
2576 "COPY: only TO STDOUT is supported here (FROM stdin \
2577 rides the import path); got {:?}",
2578 self.peek()
2579 )));
2580 }
2581 self.advance();
2582 if matches!(self.peek(), Token::String(_)) {
2583 let Token::String(path) = self.advance() else { unreachable!() };
2584 let options = self.parse_copy_to_options()?;
2585 return Ok(Statement::CopyToFile {
2586 table,
2587 columns,
2588 query: None,
2589 path,
2590 options,
2591 });
2592 }
2593 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2594 return Err(self.err(format!(
2595 "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2596 self.peek()
2597 )));
2598 }
2599 self.advance();
2600 let options = self.parse_copy_to_options()?;
2601 Ok(Statement::CopyTo {
2602 table,
2603 columns,
2604 query: None,
2605 options,
2606 })
2607 }
2608 // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2609 // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2610 // result set is streamed in COPY format (PG's query form).
2611 Token::Ident(s)
2612 if s.eq_ignore_ascii_case("copy")
2613 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2614 {
2615 self.advance(); // COPY
2616 self.advance(); // (
2617 let query = self.parse_select_stmt()?;
2618 if !matches!(self.peek(), Token::RParen) {
2619 return Err(self.err(format!(
2620 "expected ')' after COPY query, got {:?}",
2621 self.peek()
2622 )));
2623 }
2624 self.advance(); // )
2625 if !matches!(self.peek(), Token::To) {
2626 return Err(self.err(format!(
2627 "COPY (query): only TO STDOUT is supported, got {:?}",
2628 self.peek()
2629 )));
2630 }
2631 self.advance();
2632 if matches!(self.peek(), Token::String(_)) {
2633 let Token::String(path) = self.advance() else { unreachable!() };
2634 let options = self.parse_copy_to_options()?;
2635 return Ok(Statement::CopyToFile {
2636 table: String::new(),
2637 columns: None,
2638 query: Some(alloc::boxed::Box::new(query)),
2639 path,
2640 options,
2641 });
2642 }
2643 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2644 return Err(self.err(format!(
2645 "COPY (query): TO supports STDOUT only, got {:?}",
2646 self.peek()
2647 )));
2648 }
2649 self.advance();
2650 let options = self.parse_copy_to_options()?;
2651 Ok(Statement::CopyTo {
2652 table: String::new(),
2653 columns: None,
2654 query: Some(alloc::boxed::Box::new(query)),
2655 options,
2656 })
2657 }
2658 // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2659 // Shares the INSERT body; the replace flag lowers it
2660 // onto ON CONFLICT DO UPDATE with an empty assignment
2661 // list (engine: replace the whole row).
2662 Token::Ident(s)
2663 if s.eq_ignore_ascii_case("replace")
2664 && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2665 {
2666 self.parse_insert_stmt(true)
2667 }
2668 Token::Begin => {
2669 self.advance();
2670 // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2671 // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2672 // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2673 // is consumed first, then the trailing modes — including the
2674 // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2675 // WORK/TRANSACTION). The explicit level, when present, rides the
2676 // statement so `exec_begin` applies it for this transaction.
2677 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2678 {
2679 self.advance();
2680 }
2681 let iso = self.parse_isolation_level_clauses()?;
2682 Ok(Statement::Begin(iso))
2683 }
2684 // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2685 // for BEGIN. START is contextual in PG too; pattern-match
2686 // on the ident here. Iso clauses are parse-and-ignored,
2687 // same as BEGIN above.
2688 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2689 self.advance();
2690 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2691 {
2692 return Err(self.err(alloc::format!(
2693 "expected TRANSACTION after START, got {:?}",
2694 self.peek()
2695 )));
2696 }
2697 self.advance();
2698 let iso = self.parse_isolation_level_clauses()?;
2699 Ok(Statement::Begin(iso))
2700 }
2701 Token::Commit => {
2702 self.advance();
2703 // PG: `COMMIT [WORK | TRANSACTION]`.
2704 if let Token::Ident(w) = self.peek()
2705 && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2706 {
2707 self.advance();
2708 }
2709 Ok(Statement::Commit)
2710 }
2711 // r1066 (7.38 S5.1) — `END [WORK | TRANSACTION]` is PG's
2712 // COMMIT synonym; pgbench's builtin tpcb-like script closes
2713 // every transaction with `END;` and the drop-in aborted on
2714 // it. Only reachable at statement start (CASE … END lives
2715 // inside expressions), so no ambiguity.
2716 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
2717 self.advance();
2718 if let Token::Ident(w) = self.peek()
2719 && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2720 {
2721 self.advance();
2722 }
2723 Ok(Statement::Commit)
2724 }
2725 Token::Rollback => {
2726 self.advance();
2727 // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2728 // savepoint without ending the transaction. Bare
2729 // `ROLLBACK` drops the whole TX.
2730 if matches!(self.peek(), Token::To) {
2731 self.advance();
2732 if matches!(self.peek(), Token::Savepoint) {
2733 self.advance();
2734 }
2735 let name = self.expect_ident_like()?;
2736 Ok(Statement::RollbackToSavepoint(name))
2737 } else {
2738 Ok(Statement::Rollback)
2739 }
2740 }
2741 Token::Savepoint => {
2742 self.advance();
2743 let name = self.expect_ident_like()?;
2744 Ok(Statement::Savepoint(name))
2745 }
2746 Token::Release => {
2747 self.advance();
2748 // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2749 // is optional in standard SQL.
2750 if matches!(self.peek(), Token::Savepoint) {
2751 self.advance();
2752 }
2753 let name = self.expect_ident_like()?;
2754 Ok(Statement::ReleaseSavepoint(name))
2755 }
2756 Token::Show => {
2757 self.advance();
2758 // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2759 // v6.1.2 promoted TABLES to a reserved keyword (for
2760 // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2761 // arrives as `Token::Tables` rather than a bare ident.
2762 // USERS / COLUMNS remain bare idents.
2763 let target = match self.advance() {
2764 Token::Tables => "tables".to_string(),
2765 // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2766 // keyword token; recognise it as the SHOW CREATE
2767 // dispatch keyword too.
2768 Token::Create => "create".to_string(),
2769 // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2770 // keyword too; let SHOW INDEX FROM parse.
2771 Token::Index => "index".to_string(),
2772 // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2773 // reserved (used in aggregate function calls);
2774 // recognise it here so the parser dispatches
2775 // to ShowParameter("all") — the engine returns
2776 // the curated parameter inventory.
2777 Token::All => "all".to_string(),
2778 // v7.38.18 (C12) — `SHOW COUNT(*) WARNINGS`, MySQL's
2779 // spelling for the size of the diagnostics area.
2780 // MySQL-dialect only: PostgreSQL 18.4 answers this
2781 // phrase with `syntax error at or near "("`, and a
2782 // PG session must keep getting exactly that rather
2783 // than a message about an unknown parameter.
2784 // `COUNT` arrives as a bare ident; the `(*)` and the
2785 // trailing keyword are consumed here so the whole
2786 // form reaches the engine as one parameter name.
2787 Token::Ident(ref c)
2788 if self.mysql_dialect
2789 && c.eq_ignore_ascii_case("count")
2790 && matches!(self.peek(), Token::LParen) =>
2791 {
2792 self.advance();
2793 if matches!(self.peek(), Token::Star) {
2794 self.advance();
2795 }
2796 if matches!(self.peek(), Token::RParen) {
2797 self.advance();
2798 }
2799 match self.advance() {
2800 Token::Ident(w) if w.eq_ignore_ascii_case("warnings") => {
2801 return Ok(Statement::ShowParameter(
2802 "count(*) warnings".to_string(),
2803 ));
2804 }
2805 other => {
2806 return Err(self.err(format!(
2807 "expected WARNINGS after SHOW COUNT(*), got {other:?}"
2808 )));
2809 }
2810 }
2811 }
2812 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2813 other => {
2814 return Err(self.err(format!(
2815 "expected SHOW target, got {other:?}"
2816 )));
2817 }
2818 };
2819 match target.as_str() {
2820 "tables" => Ok(Statement::ShowTables),
2821 "users" => Ok(Statement::ShowUsers),
2822 // v7.38 轴 4 — `SHOW transaction_isolation`
2823 // returns the currently-selected isolation level.
2824 "transaction_isolation" => Ok(Statement::ShowParameter(
2825 "transaction_isolation".to_string(),
2826 )),
2827 // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2828 // TABLE <t>` returns a 2-column row: (Table,
2829 // Create Table). mysqldump emits this for every
2830 // table at scrape time; without it the dump
2831 // round-trip stalls.
2832 // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2833 // FROM <t>` (also spelled `SHOW INDEX` and
2834 // `SHOW KEYS`). admin / mysqldump probes use
2835 // it to list per-table indexes.
2836 "indexes" | "index" | "keys" => {
2837 if !matches!(self.peek(), Token::From) {
2838 return Err(self.err(format!(
2839 "expected FROM after SHOW INDEXES, got {:?}",
2840 self.peek()
2841 )));
2842 }
2843 self.advance();
2844 let table = self.expect_ident_like()?;
2845 Ok(Statement::ShowIndexes(table))
2846 }
2847 // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2848 // `SHOW VARIABLES`. Both return a 2-column row
2849 // set listing server-side state; clients probe
2850 // them at connect time.
2851 "status" => Ok(Statement::ShowStatus),
2852 "variables" => {
2853 // r1067 — `SHOW VARIABLES LIKE 'pat'`.
2854 if matches!(self.peek(), Token::Like) {
2855 self.advance();
2856 let pat = match self.advance() {
2857 Token::String(p) => p,
2858 other => {
2859 return Err(self.err(format!(
2860 "SHOW VARIABLES LIKE expects a quoted pattern, got {other:?}"
2861 )));
2862 }
2863 };
2864 return Ok(Statement::ShowVariablesLike(pat));
2865 }
2866 Ok(Statement::ShowVariables)
2867 }
2868 // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2869 "processlist" => Ok(Statement::ShowProcesslist),
2870 "create" => {
2871 // SHOW CREATE TABLE / VIEW / DATABASE — only
2872 // TABLE is supported in v7.17.
2873 let kind = match self.advance() {
2874 Token::Ident(s) | Token::QuotedIdent(s) => s,
2875 Token::Table => "table".to_string(),
2876 other => {
2877 return Err(self.err(format!(
2878 "expected TABLE after SHOW CREATE, got {other:?}"
2879 )));
2880 }
2881 };
2882 if !kind.eq_ignore_ascii_case("table") {
2883 return Err(self.err(format!(
2884 "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2885 )));
2886 }
2887 let name = self.expect_ident_like()?;
2888 Ok(Statement::ShowCreateTable(name))
2889 }
2890 // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2891 // (and `SHOW SCHEMAS` alias). The mysql client uses
2892 // it to populate the database selector at connect
2893 // time; without it `mysql -p` errors before the
2894 // first user query.
2895 "databases" | "schemas" => Ok(Statement::ShowDatabases),
2896 // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2897 // keyword on its own; it lands here as a bare
2898 // ident. Returning all publications + their
2899 // scope summary.
2900 "publications" => Ok(Statement::ShowPublications),
2901 // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2902 "subscriptions" => Ok(Statement::ShowSubscriptions),
2903 "columns" => {
2904 if !matches!(self.peek(), Token::From) {
2905 return Err(self.err(format!(
2906 "expected FROM after SHOW COLUMNS, got {:?}",
2907 self.peek()
2908 )));
2909 }
2910 self.advance();
2911 let table = self.expect_ident_like()?;
2912 Ok(Statement::ShowColumns(table))
2913 }
2914 // v7.38 轴 4 surface — `SHOW <param>` for any
2915 // remaining session / preset parameter name
2916 // (server_version, search_path, client_encoding,
2917 // …). The engine's ShowParameter handler does the
2918 // dispatch; unrecognised names error there with
2919 // a pointer to pg_settings, not at parse time —
2920 // so a driver that issues `SHOW spam_setting`
2921 // gets a clear runtime error instead of a
2922 // confusing "unknown SHOW target".
2923 other => {
2924 // v7.38 (read01 P3.20) — a custom namespaced GUC
2925 // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
2926 // consume the dotted tail so it round-trips with
2927 // `SET app.foo` / `current_setting('app.foo')`.
2928 let mut full = other.to_string();
2929 while matches!(self.peek(), Token::Dot) {
2930 self.advance();
2931 let seg = self.expect_ident_like()?;
2932 full.push('.');
2933 full.push_str(&seg.to_ascii_lowercase());
2934 }
2935 Ok(Statement::ShowParameter(full))
2936 }
2937 }
2938 }
2939 // v6.1.2: `DROP` is now a reserved keyword (it dispatches
2940 // to DROP USER and DROP PUBLICATION today; DROP TABLE /
2941 // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
2942 // arrived as a bare ident; tokenising it dedicatedly
2943 // keeps the dispatch tree small.
2944 Token::Drop => {
2945 self.advance();
2946 match self.peek() {
2947 // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
2948 // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
2949 // around DROP ROLE cleanup. SPG has no role-owner
2950 // model, so consume to boundary as a no-op.
2951 Token::Ident(s) | Token::QuotedIdent(s)
2952 if s.eq_ignore_ascii_case("owned") =>
2953 {
2954 // v7.39 (round 696) — still a no-op (SPG has no
2955 // role-owner model), but the ROLE is carried out so
2956 // the engine can refuse one that does not exist,
2957 // which is what PG18 does.
2958 self.advance();
2959 if self.peek_is_by() {
2960 self.advance();
2961 }
2962 let names = self.take_comma_separated_names();
2963 self.consume_until_statement_boundary();
2964 Ok(Statement::ValidateOnly {
2965 kind: crate::ast::ValidateOnlyKind::RoleName,
2966 names,
2967 })
2968 }
2969 // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
2970 // It drops only a TEMPORARY table, and name resolution
2971 // already prefers the session's own, so the keyword is
2972 // consumed and the ordinary DROP TABLE path runs.
2973 Token::Ident(s) | Token::QuotedIdent(s)
2974 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
2975 {
2976 self.advance();
2977 if !matches!(self.peek(), Token::Table) {
2978 return Err(self.err(alloc::format!(
2979 "expected TABLE after DROP TEMPORARY, got {:?}",
2980 self.peek()
2981 )));
2982 }
2983 self.parse_drop_table_after_keyword()
2984 }
2985 Token::Publication => {
2986 self.advance();
2987 // v7.39 (round 754, F31-B4) — the round-753
2988 // audit probe tripped over the missing
2989 // `IF EXISTS` here (syntax error).
2990 let if_exists = self.consume_if_exists();
2991 let name = self.expect_ident_or_string()?;
2992 Ok(Statement::DropPublication { name, if_exists })
2993 }
2994 Token::Subscription => {
2995 self.advance();
2996 let if_exists = self.consume_if_exists();
2997 let name = self.expect_ident_or_string()?;
2998 Ok(Statement::DropSubscription { name, if_exists })
2999 }
3000 Token::Ident(s) | Token::QuotedIdent(s)
3001 if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
3002 {
3003 self.advance();
3004 // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
3005 // login user IS a role in PG, and SPG's store holds
3006 // both. `IF EXISTS` is accepted on either spelling.
3007 let if_exists = self.consume_if_exists();
3008 let name = self.expect_ident_or_string()?;
3009 Ok(Statement::DropUser { name, if_exists })
3010 }
3011 // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
3012 // CREATE DATABASE has parsed since v7.14 and this did
3013 // not, so `DROP DATABASE IF EXISTS x` — what every
3014 // teardown script and pg_dumpall preamble opens with —
3015 // came back as a syntax error, which IF EXISTS cannot
3016 // soften. The name is carried so the engine can answer
3017 // the way PG does; PG never lets this succeed on a
3018 // single-database server, since the name is either
3019 // unknown ("database … does not exist", or a notice
3020 // under IF EXISTS) or the one you are connected to
3021 // ("cannot drop the currently open database").
3022 Token::Ident(s) | Token::QuotedIdent(s)
3023 if s.eq_ignore_ascii_case("database") =>
3024 {
3025 self.advance();
3026 let if_exists = self.consume_if_exists();
3027 let name = self.expect_ident_or_string()?;
3028 self.consume_until_statement_boundary();
3029 Ok(Statement::DropDatabase { name, if_exists })
3030 }
3031 // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
3032 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
3033 self.advance();
3034 let if_exists = self.consume_if_exists();
3035 let name = self.expect_ident_like()?;
3036 // ON <table>
3037 if !matches!(self.peek(), Token::On) {
3038 return Err(self.err(alloc::format!(
3039 "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
3040 self.peek()
3041 )));
3042 }
3043 self.advance();
3044 let table = self.expect_ident_like()?;
3045 Ok(Statement::DropTrigger {
3046 name,
3047 table,
3048 if_exists,
3049 })
3050 }
3051 // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
3052 // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
3053 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
3054 self.advance();
3055 let if_exists = self.consume_if_exists();
3056 let name = self.expect_ident_like()?;
3057 if !matches!(self.peek(), Token::On) {
3058 return Err(self.err(alloc::format!(
3059 "expected ON <table> after DROP RULE {name:?}, got {:?}",
3060 self.peek()
3061 )));
3062 }
3063 self.advance();
3064 let table = self.expect_ident_like()?;
3065 // Optional CASCADE / RESTRICT — accepted, no effect.
3066 self.consume_until_statement_boundary();
3067 Ok(Statement::DropRule {
3068 name,
3069 table,
3070 if_exists,
3071 })
3072 }
3073 // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
3074 // v7.12.4 ignores any optional arg-list (signature-
3075 // based overload disambiguation lands in v7.12.5+).
3076 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
3077 self.advance();
3078 let if_exists = self.consume_if_exists();
3079 let name = self.expect_ident_like()?;
3080 // v7.39 (read01 round 62) — the argument list identifies
3081 // WHICH overload to drop, so it is captured, not
3082 // discarded. `DROP FUNCTION f` (no list) is legal when
3083 // the name is unambiguous; the engine enforces that.
3084 let args = if matches!(self.peek(), Token::LParen) {
3085 Some(self.parse_function_signature_types()?)
3086 } else {
3087 None
3088 };
3089 // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
3090 // trailer, which `DROP TABLE` and `DROP INDEX` have
3091 // accepted since v7.14 and this one refused outright.
3092 // pg_dump writes it, so refusing was a parse error in
3093 // the middle of a restore. SPG drops the function
3094 // either way — it tracks no dependents to cascade to —
3095 // which is the same reading the other two give it.
3096 self.consume_drop_behaviour();
3097 Ok(Statement::DropFunction {
3098 name,
3099 args,
3100 if_exists,
3101 })
3102 }
3103 // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
3104 // [CASCADE|RESTRICT]. pg_dump and mysqldump both
3105 // emit DROP TABLE IF EXISTS at the head of every
3106 // CREATE TABLE block so re-importing a dump
3107 // overwrites prior state. SPG accepts and removes
3108 // matching tables; CASCADE/RESTRICT trailers
3109 // accepted silently.
3110 Token::Table => self.parse_drop_table_after_keyword(),
3111 // v7.14.0 — DROP INDEX [IF EXISTS] name
3112 // [CASCADE|RESTRICT]. PG / mysqldump emit this
3113 // for partial-index renames and pgvector
3114 // migrations. SPG removes the matching index;
3115 // IF EXISTS makes the drop idempotent.
3116 Token::Index => {
3117 self.advance();
3118 let if_exists = self.consume_if_exists();
3119 let name = self.expect_ident_like()?;
3120 if matches!(
3121 self.peek(),
3122 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3123 || s.eq_ignore_ascii_case("restrict")
3124 ) {
3125 self.advance();
3126 }
3127 Ok(Statement::DropIndex { name, if_exists })
3128 }
3129 // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3130 // [CASCADE|RESTRICT]. SPG is single-database;
3131 // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3132 // name [, name…] [CASCADE | RESTRICT]. Real
3133 // unregister (was silent no-op pre-v7.17).
3134 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3135 self.advance();
3136 let if_exists = self.consume_if_exists();
3137 let mut names = vec![self.expect_ident_like()?];
3138 while matches!(self.peek(), Token::Comma) {
3139 self.advance();
3140 names.push(self.expect_ident_like()?);
3141 }
3142 if matches!(
3143 self.peek(),
3144 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3145 || s.eq_ignore_ascii_case("restrict")
3146 ) {
3147 self.advance();
3148 }
3149 Ok(Statement::DropSchema { names, if_exists })
3150 }
3151 // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3152 // name [, name…] [CASCADE|RESTRICT].
3153 Token::Ident(s) | Token::QuotedIdent(s)
3154 if s.eq_ignore_ascii_case("type") =>
3155 {
3156 self.advance();
3157 let if_exists = self.consume_if_exists();
3158 let mut names = vec![self.expect_ident_like()?];
3159 while matches!(self.peek(), Token::Comma) {
3160 self.advance();
3161 names.push(self.expect_ident_like()?);
3162 }
3163 if matches!(
3164 self.peek(),
3165 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3166 || s.eq_ignore_ascii_case("restrict")
3167 ) {
3168 self.advance();
3169 }
3170 Ok(Statement::DropType { names, if_exists })
3171 }
3172 // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3173 // name [, name…] [CASCADE|RESTRICT].
3174 Token::Ident(s) | Token::QuotedIdent(s)
3175 if s.eq_ignore_ascii_case("domain") =>
3176 {
3177 self.advance();
3178 let if_exists = self.consume_if_exists();
3179 let mut names = vec![self.expect_ident_like()?];
3180 while matches!(self.peek(), Token::Comma) {
3181 self.advance();
3182 names.push(self.expect_ident_like()?);
3183 }
3184 if matches!(
3185 self.peek(),
3186 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3187 || s.eq_ignore_ascii_case("restrict")
3188 ) {
3189 self.advance();
3190 }
3191 Ok(Statement::DropDomain { names, if_exists })
3192 }
3193 // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3194 // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3195 Token::Ident(s) | Token::QuotedIdent(s)
3196 if s.eq_ignore_ascii_case("materialized") =>
3197 {
3198 self.advance();
3199 let nxt = self.peek().clone();
3200 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3201 {
3202 return Err(self.err(alloc::format!(
3203 "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3204 )));
3205 }
3206 self.advance();
3207 let if_exists = self.consume_if_exists();
3208 let mut names = vec![self.expect_ident_like()?];
3209 while matches!(self.peek(), Token::Comma) {
3210 self.advance();
3211 names.push(self.expect_ident_like()?);
3212 }
3213 if matches!(
3214 self.peek(),
3215 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3216 || s.eq_ignore_ascii_case("restrict")
3217 ) {
3218 self.advance();
3219 }
3220 Ok(Statement::DropMaterializedView { names, if_exists })
3221 }
3222 // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3223 // name [, name…] [CASCADE|RESTRICT].
3224 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3225 self.advance();
3226 let if_exists = self.consume_if_exists();
3227 let mut names = vec![self.expect_ident_like()?];
3228 while matches!(self.peek(), Token::Comma) {
3229 self.advance();
3230 names.push(self.expect_ident_like()?);
3231 }
3232 if matches!(
3233 self.peek(),
3234 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3235 || s.eq_ignore_ascii_case("restrict")
3236 ) {
3237 self.advance();
3238 }
3239 Ok(Statement::DropView { names, if_exists })
3240 }
3241 // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3242 // [CASCADE|RESTRICT]. Real removal from catalog
3243 // (was a silent no-op pre-v7.17).
3244 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3245 self.advance();
3246 let if_exists = self.consume_if_exists();
3247 let mut names = vec![self.expect_ident_like()?];
3248 while matches!(self.peek(), Token::Comma) {
3249 self.advance();
3250 names.push(self.expect_ident_like()?);
3251 }
3252 if matches!(
3253 self.peek(),
3254 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3255 || s.eq_ignore_ascii_case("restrict")
3256 ) {
3257 self.advance();
3258 }
3259 Ok(Statement::DropSequence { names, if_exists })
3260 }
3261 // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3262 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3263 self.advance();
3264 self.parse_drop_policy_after_keyword()
3265 }
3266 // v7.37.17 (17.6 siblings) — DROP <target> for
3267 // targets SPG doesn't natively track. pg_dump
3268 // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3269 // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3270 // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3271 // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3272 // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3273 // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3274 // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3275 // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3276 // etc. — accept + Empty-return so pg_dump tails
3277 // load through. Materialized-view drop dispatches
3278 // to the existing DropTable path when the token
3279 // is Materialized-View-shaped (elsewhere in
3280 // this parser).
3281 Token::Ident(s) | Token::QuotedIdent(s)
3282 if s.eq_ignore_ascii_case("text")
3283 // The DROP dispatch matches on PEEK — `text` is
3284 // not yet consumed, so SEARCH/CONFIGURATION sit
3285 // at pos+1/pos+2 (the round-695 trap's mirror).
3286 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3287 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3288 {
3289 // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3290 // validates the name; DICTIONARY / PARSER / TEMPLATE
3291 // stay in the noise arm below.
3292 self.advance(); // TEXT
3293 self.advance(); // SEARCH
3294 self.advance(); // CONFIGURATION
3295 let if_exists = self.consume_if_exists();
3296 let names = self.take_comma_separated_names();
3297 self.consume_until_statement_boundary();
3298 if if_exists {
3299 return Ok(Statement::Empty);
3300 }
3301 Ok(Statement::ValidateOnly {
3302 kind: crate::ast::ValidateOnlyKind::TsConfigName,
3303 names,
3304 })
3305 }
3306 Token::Ident(s) | Token::QuotedIdent(s)
3307 if matches!(
3308 s.to_ascii_lowercase().as_str(),
3309 "type"
3310 | "domain"
3311 | "operator"
3312 | "cast"
3313 // `text` = TEXT SEARCH DICTIONARY / PARSER /
3314 // TEMPLATE (CONFIGURATION intercepted above).
3315 | "text"
3316 | "materialized"
3317 | "large"
3318 | "role"
3319 | "access"
3320 | "procedure"
3321 | "routine"
3322 ) =>
3323 {
3324 self.consume_until_statement_boundary();
3325 Ok(Statement::Empty)
3326 }
3327 // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3328 // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3329 // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3330 // foreign-data warning family (round 706) so a
3331 // CREATE→DROP sequence in a dump stays consistent.
3332 Token::Ident(s) | Token::QuotedIdent(s)
3333 if s.eq_ignore_ascii_case("server")
3334 || s.eq_ignore_ascii_case("foreign") =>
3335 {
3336 self.advance();
3337 self.consume_until_statement_boundary();
3338 Ok(Statement::ValidateOnly {
3339 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3340 names: Vec::new(),
3341 })
3342 }
3343 Token::Ident(s) | Token::QuotedIdent(s)
3344 if s.eq_ignore_ascii_case("collation")
3345 || s.eq_ignore_ascii_case("tablespace") =>
3346 {
3347 let kind = if s.eq_ignore_ascii_case("collation") {
3348 crate::ast::ValidateOnlyKind::CollationName
3349 } else {
3350 crate::ast::ValidateOnlyKind::TablespaceName
3351 };
3352 self.advance();
3353 let if_exists = self.consume_if_exists();
3354 let names = self.take_comma_separated_names();
3355 self.consume_until_statement_boundary();
3356 if if_exists {
3357 return Ok(Statement::Empty);
3358 }
3359 Ok(Statement::ValidateOnly { kind, names })
3360 }
3361 Token::Ident(s) | Token::QuotedIdent(s)
3362 if s.eq_ignore_ascii_case("event") =>
3363 {
3364 self.advance();
3365 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3366 {
3367 self.advance();
3368 }
3369 let if_exists = self.consume_if_exists();
3370 let names = self.take_comma_separated_names();
3371 self.consume_until_statement_boundary();
3372 if if_exists {
3373 return Ok(Statement::Empty);
3374 }
3375 Ok(Statement::ValidateOnly {
3376 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3377 names,
3378 })
3379 }
3380 // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3381 // leave the noise list; see the ValidateOnly kinds.
3382 Token::Ident(s) | Token::QuotedIdent(s)
3383 if s.eq_ignore_ascii_case("conversion")
3384 || s.eq_ignore_ascii_case("language")
3385 // `DROP PROCEDURAL LANGUAGE` puts the modifier
3386 // FIRST — the first draft looked for it after.
3387 || s.eq_ignore_ascii_case("procedural") =>
3388 {
3389 let kind = if s.eq_ignore_ascii_case("conversion") {
3390 crate::ast::ValidateOnlyKind::ConversionName
3391 } else {
3392 crate::ast::ValidateOnlyKind::LanguageName
3393 };
3394 self.advance();
3395 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3396 {
3397 self.advance();
3398 }
3399 let if_exists = self.consume_if_exists();
3400 let names = self.take_comma_separated_names();
3401 self.consume_until_statement_boundary();
3402 if if_exists {
3403 return Ok(Statement::Empty);
3404 }
3405 Ok(Statement::ValidateOnly { kind, names })
3406 }
3407 // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3408 // name(argtypes)[, …]`. Parsed for real so the engine
3409 // can answer as PG does; see Statement::DropAggregate.
3410 Token::Ident(s) | Token::QuotedIdent(s)
3411 if s.eq_ignore_ascii_case("aggregate") =>
3412 {
3413 self.advance();
3414 let if_exists = self.consume_if_exists();
3415 let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3416 loop {
3417 let name = self.expect_ident_like()?;
3418 if !matches!(self.peek(), Token::LParen) {
3419 return Err(self.err(alloc::format!(
3420 "expected argument list after DROP AGGREGATE {name}"
3421 )));
3422 }
3423 self.advance();
3424 let mut args: Vec<String> = Vec::new();
3425 let mut star = false;
3426 loop {
3427 match self.peek().clone() {
3428 Token::RParen => {
3429 self.advance();
3430 break;
3431 }
3432 Token::Star => {
3433 self.advance();
3434 star = true;
3435 }
3436 Token::Comma => {
3437 self.advance();
3438 }
3439 _ => {
3440 // A type name may be multi-token
3441 // (`double precision`); glue idents
3442 // until , or ).
3443 let mut t = self.expect_ident_like()?;
3444 while let Token::Ident(nx) = self.peek() {
3445 let nx = nx.clone();
3446 self.advance();
3447 t.push(' ');
3448 t.push_str(&nx);
3449 }
3450 args.push(t);
3451 }
3452 }
3453 }
3454 items.push((name, if star { None } else { Some(args) }));
3455 if matches!(self.peek(), Token::Comma) {
3456 self.advance();
3457 } else {
3458 break;
3459 }
3460 }
3461 self.consume_until_statement_boundary();
3462 Ok(Statement::DropAggregate { if_exists, items })
3463 }
3464 // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3465 // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3466 // installed; `IF EXISTS` is the spelling that says do
3467 // not, and it keeps the no-op.
3468 Token::Ident(s) | Token::QuotedIdent(s)
3469 if s.eq_ignore_ascii_case("extension") =>
3470 {
3471 self.advance();
3472 let if_exists = self.consume_if_exists();
3473 let names = self.take_comma_separated_names();
3474 self.consume_until_statement_boundary();
3475 if if_exists {
3476 return Ok(Statement::Empty);
3477 }
3478 Ok(Statement::ValidateOnly {
3479 kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3480 names,
3481 })
3482 }
3483 Token::Ident(s) | Token::QuotedIdent(s)
3484 if s.eq_ignore_ascii_case("statistics") =>
3485 {
3486 self.parse_drop_statistics_after_drop()
3487 }
3488 other => Err(self.err(format!(
3489 "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3490 SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3491 ))),
3492 }
3493 }
3494 // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3495 // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3496 // and accepted before the view name. SPG materialised
3497 // views re-evaluate on read (always-fresh semantics), so
3498 // the CONCURRENTLY-vs-serial distinction has no runtime
3499 // effect — the refresh body does not block readers either
3500 // way. Same accept-and-no-op pattern as DETACH PARTITION
3501 // CONCURRENTLY (16.5).
3502 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3503 self.advance();
3504 let nxt = self.peek().clone();
3505 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3506 {
3507 return Err(self.err(alloc::format!(
3508 "expected MATERIALIZED after REFRESH, got {nxt:?}"
3509 )));
3510 }
3511 self.advance();
3512 let nxt2 = self.peek().clone();
3513 if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3514 {
3515 return Err(self.err(alloc::format!(
3516 "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3517 )));
3518 }
3519 self.advance();
3520 // Optional CONCURRENTLY noise word — consumed without
3521 // changing semantics.
3522 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3523 {
3524 self.advance();
3525 }
3526 let name = self.expect_ident_like()?;
3527 let with_data = self.parse_optional_with_data(true)?;
3528 Ok(Statement::RefreshMaterializedView { name, with_data })
3529 }
3530 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3531 self.advance();
3532 self.parse_update_after_keyword()
3533 }
3534 // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3535 // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3536 // [CASCADE | RESTRICT]. Clears every row from each named
3537 // table. Parses at the top level; the engine dispatcher
3538 // walks Statement::Truncate.
3539 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3540 self.advance();
3541 // Optional TABLE noise word — PG accepts both the reserved
3542 // token and the bare identifier spelling.
3543 if matches!(self.peek(), Token::Table)
3544 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3545 {
3546 self.advance();
3547 }
3548 // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3549 // not absorbed. The lookahead keeps a table genuinely
3550 // called `only` working: the keyword is a keyword only
3551 // when a name follows it.
3552 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3553 if s.eq_ignore_ascii_case("only"))
3554 && matches!(
3555 self.tokens.get(self.pos + 1),
3556 Some(Token::Ident(_) | Token::QuotedIdent(_))
3557 );
3558 if only {
3559 self.advance();
3560 }
3561 // Table names (comma-separated).
3562 let mut tables = Vec::new();
3563 loop {
3564 tables.push(self.expect_ident_like()?);
3565 if matches!(self.peek(), Token::Comma) {
3566 self.advance();
3567 continue;
3568 }
3569 break;
3570 }
3571 // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3572 let mut restart_identity = false;
3573 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3574 {
3575 self.advance();
3576 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3577 {
3578 self.advance();
3579 restart_identity = true;
3580 }
3581 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3582 {
3583 self.advance();
3584 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3585 {
3586 self.advance();
3587 }
3588 }
3589 // Optional CASCADE / RESTRICT.
3590 let mut cascade = false;
3591 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3592 {
3593 self.advance();
3594 cascade = true;
3595 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3596 {
3597 self.advance();
3598 }
3599 Ok(Statement::Truncate {
3600 tables,
3601 restart_identity,
3602 cascade,
3603 only,
3604 })
3605 }
3606 // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3607 // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3608 // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3609 // rows change so the index tree is always up-to-date;
3610 // REINDEX is a strict no-op. Accept the whole statement
3611 // shape to boundary for pg_dump round-trip compatibility.
3612 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3613 // v7.39 (round 535) — the target is CARRIED now. SPG has no
3614 // index bloat to rebuild, so the work stays a no-op, but PG
3615 // validates what it was pointed at and this swallowed the
3616 // name at parse time — `REINDEX TABLE typo` reported
3617 // success. Measured on PG18: INDEX / TABLE name a relation,
3618 // SCHEMA a schema, SYSTEM nothing.
3619 self.advance();
3620 self.parse_reindex_tail()
3621 }
3622 // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3623 // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3624 // SPG has no MVCC bloat today (Phase D visibility map
3625 // queues with v7.38); the freezer collapses hot-tier
3626 // rows into cold segments automatically. VACUUM is a
3627 // no-op — pg_dump maintenance scripts and Discourse's
3628 // periodic-maintenance path both emit it.
3629 // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3630 // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3631 // actual bloat, so the pre-MVCC accept-and-ignore posture
3632 // became a silent no-op on a customer's manual reclaim.
3633 // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3634 // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3635 // ANALYZE is captured, the optional table name is captured.
3636 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3637 self.advance();
3638 // Parenthesised option list: absorb it.
3639 if matches!(self.peek(), Token::LParen) {
3640 let mut depth = 0usize;
3641 loop {
3642 match self.advance() {
3643 Token::LParen => depth += 1,
3644 Token::RParen => {
3645 depth -= 1;
3646 if depth == 0 {
3647 break;
3648 }
3649 }
3650 Token::Eof => break,
3651 _ => {}
3652 }
3653 }
3654 }
3655 let mut analyze = false;
3656 let mut table: Option<String> = None;
3657 loop {
3658 match self.peek() {
3659 // v7.39 (round 535) — `FULL` lexes as a keyword, not
3660 // an identifier, so the loop below broke out on it and
3661 // dropped the table name: `VACUUM FULL nosuch` was
3662 // accepted where `VACUUM nosuch` was refused.
3663 Token::Full => {
3664 self.advance();
3665 }
3666 Token::Ident(w) | Token::QuotedIdent(w) => {
3667 let wl = w.to_ascii_lowercase();
3668 match wl.as_str() {
3669 "full" | "freeze" | "verbose" => {
3670 self.advance();
3671 }
3672 "analyze" | "analyse" => {
3673 analyze = true;
3674 self.advance();
3675 }
3676 _ => {
3677 table = Some(self.expect_ident_like()?);
3678 break;
3679 }
3680 }
3681 }
3682 _ => break,
3683 }
3684 }
3685 // Optional trailing column list / anything else to the
3686 // statement boundary (PG accepts per-column ANALYZE).
3687 self.consume_until_statement_boundary();
3688 Ok(Statement::Vacuum { table, analyze })
3689 }
3690 // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3691 // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3692 // <index>. PG stores rows in physical order matching
3693 // an index; SPG's hot-tier is append-only + cold-tier
3694 // is segment-frozen, so clustering has no persistent
3695 // effect. Accept-and-no-op for pg_dump compat.
3696 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3697 // v7.39 (round 535) — same as REINDEX above: the relation is
3698 // carried so the engine can refuse one that does not exist.
3699 // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3700 self.advance();
3701 self.parse_cluster_tail()
3702 }
3703 // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3704 // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3705 // optional string payload; UNLISTEN takes a channel or `*`.
3706 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3707 self.advance();
3708 let ch = match self.advance() {
3709 Token::Ident(c) | Token::QuotedIdent(c) => c,
3710 other => {
3711 return Err(self.err(format!(
3712 "expected channel name after LISTEN, got {other:?}"
3713 )));
3714 }
3715 };
3716 Ok(Statement::Listen(ch))
3717 }
3718 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3719 self.advance();
3720 let channel = match self.advance() {
3721 Token::Ident(c) | Token::QuotedIdent(c) => c,
3722 other => {
3723 return Err(self.err(format!(
3724 "expected channel name after NOTIFY, got {other:?}"
3725 )));
3726 }
3727 };
3728 let payload = if matches!(self.peek(), Token::Comma) {
3729 self.advance();
3730 match self.advance() {
3731 Token::String(p) => Some(p),
3732 other => {
3733 return Err(self.err(format!(
3734 "expected string payload after NOTIFY <channel>, got {other:?}"
3735 )));
3736 }
3737 }
3738 } else {
3739 None
3740 };
3741 Ok(Statement::Notify { channel, payload })
3742 }
3743 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3744 self.advance();
3745 match self.advance() {
3746 Token::Star => Ok(Statement::Unlisten(None)),
3747 Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3748 other => Err(self.err(format!(
3749 "expected channel name or * after UNLISTEN, got {other:?}"
3750 ))),
3751 }
3752 }
3753 // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3754 // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3755 // process-wide write lock today; explicit LOCK has no
3756 // effect. Accept-and-no-op for pg_dump / migration
3757 // compat.
3758 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3759 self.advance();
3760 // v7.39 (round 696) — the LOCK still has no effect (SPG's
3761 // engine holds a process-wide write lock), but the TABLE
3762 // NAME is now carried out so the engine can refuse one that
3763 // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3764 // READ|WRITE` is a different statement with the same first
3765 // word; it keeps the old no-op, because a MySQL dump's
3766 // bracket names tables it is about to create.
3767 let mysql_tables = matches!(self.peek(), Token::Ident(k)
3768 if k.eq_ignore_ascii_case("tables"));
3769 if mysql_tables {
3770 self.consume_until_statement_boundary();
3771 return Ok(Statement::Empty);
3772 }
3773 if matches!(self.peek(), Token::Table) {
3774 self.advance();
3775 }
3776 let names = self.take_comma_separated_names();
3777 self.consume_until_statement_boundary();
3778 Ok(Statement::ValidateOnly {
3779 kind: crate::ast::ValidateOnlyKind::LockTable,
3780 names,
3781 })
3782 }
3783 // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3784 // durability marker + snapshot in PG. SPG has WAL
3785 // checkpointing on a byte / time schedule (v7.37.10
3786 // 60s / 4 MiB defaults). The bare statement parses to
3787 // `Statement::Empty` here (the no_std engine owns no
3788 // WAL / snapshot); v7.37 Epic Du wires the HOST
3789 // (embedded `Database::execute_buffered`, via
3790 // `sql_is_checkpoint`) to force an immediate synchronous
3791 // checkpoint through `Database::checkpoint` — a real
3792 // durability barrier, matching PG.
3793 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3794 self.advance();
3795 self.consume_until_statement_boundary();
3796 Ok(Statement::Empty)
3797 }
3798 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3799 self.advance();
3800 self.parse_delete_after_keyword()
3801 }
3802 // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3803 // ALTER is not a reserved keyword in the lexer — handled
3804 // as a bare ident here.
3805 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3806 self.advance();
3807 self.parse_alter_after_keyword()
3808 }
3809 // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3810 // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3811 // additions needed.
3812 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3813 self.advance();
3814 self.parse_wait_after_keyword()
3815 }
3816 // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3817 // Bare ANALYZE → analyse every user table; ANALYZE
3818 // <name> → re-stats one. The argument is an optional
3819 // ident (or quoted ident); anything else is a parse
3820 // error.
3821 // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3822 // `WHERE` filter (carved out per V6_7_DESIGN.md
3823 // STABILITY). Lex order: identifier "compact" → "cold"
3824 // → "segments". Anything else after `COMPACT` is a
3825 // parse error.
3826 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3827 self.advance();
3828 let next = self.peek().clone();
3829 let cold = match next {
3830 Token::Ident(s) | Token::QuotedIdent(s) => s,
3831 _ => {
3832 return Err(
3833 self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3834 );
3835 }
3836 };
3837 if !cold.eq_ignore_ascii_case("cold") {
3838 return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3839 }
3840 self.advance();
3841 let next = self.peek().clone();
3842 let segments = match next {
3843 Token::Ident(s) | Token::QuotedIdent(s) => s,
3844 _ => {
3845 return Err(self.err(format!(
3846 "expected SEGMENTS after COMPACT COLD, got {:?}",
3847 self.peek()
3848 )));
3849 }
3850 };
3851 if !segments.eq_ignore_ascii_case("segments") {
3852 return Err(self.err(format!(
3853 "expected SEGMENTS after COMPACT COLD, got {segments:?}"
3854 )));
3855 }
3856 self.advance();
3857 Ok(Statement::CompactColdSegments)
3858 }
3859 // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
3860 // Parsed as a case-insensitive identifier since MERGE
3861 // isn't a reserved lexer keyword (collides with the
3862 // mysqldump `ALGORITHM = MERGE` view clause if it
3863 // were); the inner parser drives the rest of the
3864 // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
3865 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
3866 self.advance();
3867 self.parse_merge_after_keyword()
3868 }
3869 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
3870 self.advance();
3871 let target = match self.peek() {
3872 Token::Eof | Token::Semicolon => None,
3873 Token::Ident(_) | Token::QuotedIdent(_) => {
3874 Some(self.expect_ident_like()?)
3875 }
3876 other => {
3877 return Err(self.err(format!(
3878 "expected table name or end of statement after ANALYZE, got {other:?}"
3879 )));
3880 }
3881 };
3882 // v7.39 (round 776, F31 J7) — the per-column form
3883 // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
3884 // here while the VACUUM arm already consumed it; SPG
3885 // analyzes whole tables, so the list parses and is
3886 // accepted like the VACUUM path's.
3887 if target.is_some() && matches!(self.peek(), Token::LParen) {
3888 self.advance();
3889 loop {
3890 let _ = self.expect_ident_like()?;
3891 match self.peek() {
3892 Token::Comma => {
3893 self.advance();
3894 }
3895 Token::RParen => {
3896 self.advance();
3897 break;
3898 }
3899 other => {
3900 return Err(self.err(format!(
3901 "expected ',' or ')' in ANALYZE column list, got {other:?}"
3902 )));
3903 }
3904 }
3905 }
3906 }
3907 Ok(Statement::Analyze(target))
3908 }
3909 // v7.12.1 — `SET <name> [TO|=] <value>`. The
3910 // `default_text_search_config` parameter is consumed
3911 // by the FTS function dispatcher; other parameter
3912 // names are recorded but treated as a no-op so PG
3913 // dump output loads.
3914 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
3915 self.advance();
3916 // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
3917 // adds `SET GLOBAL` too (and the alias `SET @@global.name =
3918 // …` which the SessionVar path handles). `LOCAL` is the only
3919 // one that changes semantics — it scopes the change to the
3920 // current transaction — so capture it; SESSION / GLOBAL are
3921 // accepted and treated as the default session scope.
3922 let mut set_local = false;
3923 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
3924 let q = s.to_ascii_lowercase();
3925 if q == "local" || q == "session" || q == "global" {
3926 set_local = q == "local";
3927 self.advance();
3928 }
3929 }
3930 // 7.38.1 S5.2 — PG `SET [SESSION] AUTHORIZATION
3931 // { DEFAULT | <role> }`. pg_dump's ACL section switches
3932 // to the object owner with it. SPG maps it onto the
3933 // session-role machinery (recorded delta: PG moves
3934 // session_user too; SPG moves the effective role).
3935 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3936 if s.eq_ignore_ascii_case("authorization"))
3937 {
3938 self.advance(); // AUTHORIZATION
3939 let role = match self.peek().clone() {
3940 Token::Default => {
3941 self.advance();
3942 None
3943 }
3944 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
3945 self.advance();
3946 Some(s)
3947 }
3948 _ => None,
3949 };
3950 return Ok(Statement::SetRole(role));
3951 }
3952 // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
3953 // <collation>]` — change the connection client
3954 // charset. SPG stores UTF-8 always and orders
3955 // bytewise; accept as a no-op.
3956 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
3957 {
3958 self.advance();
3959 // Charset ident-or-string.
3960 if matches!(
3961 self.peek(),
3962 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3963 ) {
3964 self.advance();
3965 }
3966 // Optional `COLLATE <name>`.
3967 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
3968 {
3969 self.advance();
3970 if matches!(
3971 self.peek(),
3972 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3973 ) {
3974 self.advance();
3975 }
3976 }
3977 return Ok(Statement::Empty);
3978 }
3979 // v7.37.17 (17.6 sibling) — PG `SET ROLE
3980 // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
3981 // uses this to switch to the object owner before
3982 // recreating tables. SPG has no role system so this
3983 // is a no-op.
3984 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
3985 {
3986 self.advance(); // ROLE
3987 // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
3988 // reset to the login identity; a name / string sets the
3989 // effective role that drives current_user + RLS.
3990 let role = match self.peek().clone() {
3991 Token::Default => {
3992 self.advance();
3993 None
3994 }
3995 Token::Ident(s) | Token::QuotedIdent(s)
3996 if s.eq_ignore_ascii_case("none") =>
3997 {
3998 self.advance();
3999 None
4000 }
4001 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4002 self.advance();
4003 Some(s)
4004 }
4005 _ => None,
4006 };
4007 return Ok(Statement::SetRole(role));
4008 }
4009 // v7.37.17 (17.6 sibling) — PG `SET SESSION
4010 // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
4011 // ISO SQL surface). pg_dump prepends this to fix
4012 // the isolation level for the restore session. SPG
4013 // defaults to READ COMMITTED and doesn't yet honor
4014 // session-set isolation across statements — accept
4015 // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
4016 // per-tx form is handled elsewhere.
4017 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
4018 {
4019 self.advance(); // CHARACTERISTICS
4020 self.consume_until_statement_boundary();
4021 return Ok(Statement::Empty);
4022 }
4023 // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
4024 // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
4025 // pg_dump emits this to control the deferrability of
4026 // FK / UNIQUE constraints across a bulk restore. SPG
4027 // has no deferrable-constraint machinery today; the
4028 // FK checker is strict-immediate. Accept-and-no-op
4029 // for pg_dump round-trip compatibility.
4030 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
4031 {
4032 self.advance(); // CONSTRAINTS
4033 // v7.39 (round 288) — no longer a no-op: the trailing
4034 // DEFERRED / IMMEDIATE sets the transaction's timing.
4035 // v7.39 (round 308, V29) — and the names are kept.
4036 // They used to be skipped over on the way to the
4037 // DEFERRED keyword, so a named form silently behaved
4038 // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
4039 // every deferrable constraint in the transaction.
4040 let mut names: alloc::vec::Vec<alloc::string::String> =
4041 alloc::vec::Vec::new();
4042 if matches!(self.peek(), Token::All) {
4043 self.advance();
4044 } else {
4045 loop {
4046 let mut n = self.expect_ident_like()?;
4047 // A schema-qualified name (`public.fk_a`)
4048 // identifies the same constraint; PG resolves
4049 // it by the trailing segment.
4050 while matches!(self.peek(), Token::Dot) {
4051 self.advance();
4052 n = self.expect_ident_like()?;
4053 }
4054 names.push(n);
4055 if matches!(self.peek(), Token::Comma) {
4056 self.advance();
4057 } else {
4058 break;
4059 }
4060 }
4061 }
4062 let deferred = match self.peek() {
4063 Token::Ident(s) | Token::QuotedIdent(s)
4064 if s.eq_ignore_ascii_case("deferred") =>
4065 {
4066 true
4067 }
4068 Token::Ident(s) | Token::QuotedIdent(s)
4069 if s.eq_ignore_ascii_case("immediate") =>
4070 {
4071 false
4072 }
4073 other => {
4074 return Err(self.err(alloc::format!(
4075 "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
4076 )));
4077 }
4078 };
4079 self.advance();
4080 return Ok(Statement::SetConstraints { names, deferred });
4081 }
4082 // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
4083 // { DEFAULT | '<role>' | <ident> }` (mailrs
4084 // round-10 A.1). pg_dump preamble emits the
4085 // `DEFAULT` form to reset session authorization.
4086 //
4087 // v7.39 (round 697) — this said "SPG has no role system so
4088 // this is a strict no-op". SPG has had one since round 58;
4089 // the comment outlived it, and with it the reason a name
4090 // that is not a role was accepted here. It still switches
4091 // no authorization — what it does now is refuse a role
4092 // that does not exist, as PG18 does. PG also accepts `RESET SESSION
4093 // AUTHORIZATION` (handled by the RESET parser
4094 // elsewhere). Reference:
4095 // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
4096 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
4097 {
4098 self.advance(); // AUTHORIZATION
4099 match self.peek().clone() {
4100 Token::Default => {
4101 self.advance();
4102 }
4103 Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4104 self.advance();
4105 return Ok(Statement::ValidateOnly {
4106 kind: crate::ast::ValidateOnlyKind::RoleName,
4107 names: alloc::vec![r],
4108 });
4109 }
4110 other => {
4111 return Err(self.err(alloc::format!(
4112 "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4113 )));
4114 }
4115 }
4116 return Ok(Statement::Empty);
4117 }
4118 // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4119 // ISOLATION LEVEL { READ COMMITTED | READ
4120 // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4121 // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4122 // PG-standard surface. v7.37.8 accepts the syntax
4123 // and tracks the selected level on
4124 // `Engine::current_isolation_level()`; the actual
4125 // MVCC / SSI semantics implementation lands in
4126 // the 轴 4 isolation framework (separate train).
4127 // PG itself maps READ UNCOMMITTED to READ COMMITTED
4128 // internally; SPG behaves the same (effectively
4129 // READ COMMITTED at every level today).
4130 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4131 {
4132 self.advance(); // TRANSACTION
4133 let level = self.parse_isolation_level_clauses()?.unwrap_or_default();
4134 return Ok(Statement::SetTransaction { isolation: level });
4135 }
4136 // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4137 // alias — same accept-as-no-op as SET NAMES.
4138 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4139 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4140 {
4141 self.advance(); // CHARACTER
4142 self.advance(); // SET
4143 if matches!(
4144 self.peek(),
4145 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4146 ) {
4147 self.advance();
4148 }
4149 return Ok(Statement::Empty);
4150 }
4151 // v7.39 (GUC) — PG spells the timezone GUC as two
4152 // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4153 // where <value> is a string/ident or the LOCAL /
4154 // DEFAULT keyword (both mean "back to the default").
4155 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4156 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4157 {
4158 self.advance(); // TIME
4159 self.advance(); // ZONE
4160 let value = match self.peek().clone() {
4161 Token::Ident(s)
4162 if s.eq_ignore_ascii_case("local")
4163 || s.eq_ignore_ascii_case("default") =>
4164 {
4165 self.advance();
4166 crate::ast::SetValue::Default
4167 }
4168 Token::Default => {
4169 self.advance();
4170 crate::ast::SetValue::Default
4171 }
4172 _ => self.parse_set_value()?,
4173 };
4174 return Ok(Statement::SetParameter {
4175 name: "timezone".into(),
4176 value,
4177 local: set_local,
4178 });
4179 }
4180 // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4181 // MySQL USER-variable assignment: its own per-session
4182 // namespace, an arbitrary expression on the right, and `:=`
4183 // as a second spelling of `=`. It used to fall into the
4184 // session-PARAMETER list below, whose values are literals and
4185 // whose store nothing reads back under a `@` name — so the
4186 // assignment reported success and vanished.
4187 //
4188 // A `@@`-prefixed LHS is a real engine setting and keeps the
4189 // old path.
4190 if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4191 return self.parse_set_user_vars();
4192 }
4193 // v7.14.0 — multi-assignment form
4194 // `SET a = 1, b = 2, …`. Single-assignment is the
4195 // 1-element case. Each LHS may be a regular ident
4196 // or a SessionVar (`@VAR` / `@@VAR`).
4197 let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4198 loop {
4199 let lhs = match self.peek().clone() {
4200 Token::SessionVar(s) => {
4201 self.advance();
4202 s
4203 }
4204 Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4205 other => {
4206 return Err(self.err(format!(
4207 "expected parameter name after SET, got {other:?}"
4208 )));
4209 }
4210 };
4211 // Accept either `=` or the bare `TO` keyword.
4212 match self.peek() {
4213 Token::Eq => {
4214 self.advance();
4215 }
4216 Token::To => {
4217 self.advance();
4218 }
4219 other => {
4220 return Err(self.err(format!(
4221 "expected `=` or TO after SET {lhs}, got {other:?}"
4222 )));
4223 }
4224 }
4225 let mut value = self.parse_set_value()?;
4226 // v7.39 (GUC) — disambiguate the comma: `, name =` /
4227 // `, name TO` continues a MySQL-style multi-assign,
4228 // anything else is a PG list VALUE
4229 // (`SET search_path = myschema, public`) folded into
4230 // one comma-joined string.
4231 while matches!(self.peek(), Token::Comma) {
4232 let is_assign = matches!(
4233 self.tokens.get(self.pos + 1),
4234 Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4235 ) && matches!(
4236 self.tokens.get(self.pos + 2),
4237 Some(Token::Eq | Token::To)
4238 );
4239 if is_assign {
4240 break;
4241 }
4242 self.advance(); // comma
4243 let next = self.parse_set_value()?;
4244 let joined = alloc::format!(
4245 "{}, {}",
4246 set_value_text(&value),
4247 set_value_text(&next)
4248 );
4249 value = crate::ast::SetValue::String(joined);
4250 }
4251 pairs.push((lhs, value));
4252 if matches!(self.peek(), Token::Comma) {
4253 self.advance();
4254 continue;
4255 }
4256 break;
4257 }
4258 if pairs.len() == 1 {
4259 let (name, value) = pairs.into_iter().next().unwrap();
4260 Ok(Statement::SetParameter {
4261 name,
4262 value,
4263 local: set_local,
4264 })
4265 } else {
4266 Ok(Statement::SetParameterList(pairs))
4267 }
4268 }
4269 // v7.12.1 — `RESET <name>` / `RESET ALL`.
4270 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4271 self.advance();
4272 match self.peek().clone() {
4273 Token::All => {
4274 self.advance();
4275 Ok(Statement::ResetParameter(None))
4276 }
4277 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4278 self.advance();
4279 Ok(Statement::ResetParameter(None))
4280 }
4281 // v7.39 (RLS) — `RESET ROLE` clears the session role.
4282 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4283 self.advance();
4284 Ok(Statement::SetRole(None))
4285 }
4286 // 7.38.1 S5.2 — `RESET SESSION AUTHORIZATION`
4287 // (pg_dump's return from the owner switch).
4288 Token::Ident(s) | Token::QuotedIdent(s)
4289 if s.eq_ignore_ascii_case("session")
4290 && matches!(
4291 self.tokens.get(self.pos + 1),
4292 Some(Token::Ident(a) | Token::QuotedIdent(a))
4293 if a.eq_ignore_ascii_case("authorization")
4294 ) =>
4295 {
4296 self.advance(); // SESSION
4297 self.advance(); // AUTHORIZATION
4298 Ok(Statement::SetRole(None))
4299 }
4300 _ => {
4301 let name = self.parse_set_param_name()?;
4302 Ok(Statement::ResetParameter(Some(name)))
4303 }
4304 }
4305 }
4306 // v7.39 (round 218) — server-side cursors.
4307 Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4308 Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4309 Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4310 Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4311 self.advance();
4312 match self.peek().clone() {
4313 Token::All => {
4314 self.advance();
4315 Ok(Statement::CloseCursor { name: None })
4316 }
4317 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4318 self.advance();
4319 Ok(Statement::CloseCursor { name: None })
4320 }
4321 Token::Ident(n) | Token::QuotedIdent(n) => {
4322 self.advance();
4323 Ok(Statement::CloseCursor { name: Some(n) })
4324 }
4325 other => Err(self.err(format!(
4326 "expected cursor name or ALL after CLOSE, got {other:?}"
4327 ))),
4328 }
4329 }
4330 other => Err(self.err(format!(
4331 "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4332 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4333 ))),
4334 }
4335 }
4336
4337 /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4338 /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4339 /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4340 /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4341 fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4342 self.advance(); // DECLARE
4343 let name = match self.advance() {
4344 Token::Ident(n) | Token::QuotedIdent(n) => n,
4345 other => {
4346 return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4347 }
4348 };
4349 let mut scroll: Option<bool> = None;
4350 loop {
4351 match self.peek() {
4352 Token::Ident(s)
4353 if s.eq_ignore_ascii_case("binary")
4354 || s.eq_ignore_ascii_case("insensitive")
4355 || s.eq_ignore_ascii_case("asensitive") =>
4356 {
4357 self.advance();
4358 }
4359 Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4360 self.advance();
4361 scroll = Some(true);
4362 }
4363 Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4364 {
4365 self.advance(); // NO
4366 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4367 return Err(self.err(format!(
4368 "expected SCROLL after NO in DECLARE, got {:?}",
4369 self.peek()
4370 )));
4371 }
4372 self.advance();
4373 scroll = Some(false);
4374 }
4375 _ => break,
4376 }
4377 }
4378 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4379 return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4380 }
4381 self.advance();
4382 let mut hold = false;
4383 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4384 self.advance();
4385 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4386 return Err(self.err(format!(
4387 "expected HOLD after WITH in DECLARE, got {:?}",
4388 self.peek()
4389 )));
4390 }
4391 self.advance();
4392 hold = true;
4393 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4394 self.advance();
4395 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4396 return Err(self.err(format!(
4397 "expected HOLD after WITHOUT in DECLARE, got {:?}",
4398 self.peek()
4399 )));
4400 }
4401 self.advance();
4402 }
4403 if !matches!(self.peek(), Token::For) {
4404 return Err(self.err(format!(
4405 "expected FOR before the cursor query, got {:?}",
4406 self.peek()
4407 )));
4408 }
4409 self.advance();
4410 let query = self.parse_one_statement()?;
4411 Ok(Statement::DeclareCursor {
4412 name,
4413 scroll,
4414 hold,
4415 query: alloc::boxed::Box::new(query),
4416 })
4417 }
4418
4419 /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4420 /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4421 /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4422 fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4423 use crate::ast::CursorDirection as D;
4424 self.advance(); // FETCH / MOVE
4425 let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4426 let neg = if matches!(this.peek(), Token::Minus) {
4427 this.advance();
4428 true
4429 } else {
4430 false
4431 };
4432 match this.advance() {
4433 Token::Integer(v) => Ok(if neg { -v } else { v }),
4434 other => Err(this.err(format!("expected count, got {other:?}"))),
4435 }
4436 };
4437 let direction = match self.peek().clone() {
4438 Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4439 self.advance();
4440 D::Next
4441 }
4442 Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4443 self.advance();
4444 D::Prior
4445 }
4446 Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4447 self.advance();
4448 D::First
4449 }
4450 Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4451 self.advance();
4452 D::Last
4453 }
4454 Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4455 self.advance();
4456 D::Absolute(signed_count(self)?)
4457 }
4458 Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4459 self.advance();
4460 D::Relative(signed_count(self)?)
4461 }
4462 Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4463 self.advance();
4464 match self.peek().clone() {
4465 Token::All => {
4466 self.advance();
4467 D::All
4468 }
4469 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4470 self.advance();
4471 D::All
4472 }
4473 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4474 _ => D::Next, // bare FORWARD = FORWARD 1
4475 }
4476 }
4477 Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4478 self.advance();
4479 match self.peek().clone() {
4480 Token::All => {
4481 self.advance();
4482 D::BackwardAll
4483 }
4484 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4485 self.advance();
4486 D::BackwardAll
4487 }
4488 Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4489 _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4490 }
4491 }
4492 Token::All => {
4493 self.advance();
4494 D::All
4495 }
4496 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4497 self.advance();
4498 D::All
4499 }
4500 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4501 // Bare `FETCH <name>` — direction defaults to NEXT.
4502 _ => D::Next,
4503 };
4504 // Optional FROM / IN.
4505 if matches!(self.peek(), Token::From)
4506 || matches!(self.peek(), Token::In)
4507 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4508 {
4509 self.advance();
4510 }
4511 let name = match self.advance() {
4512 Token::Ident(n) | Token::QuotedIdent(n) => n,
4513 other => {
4514 return Err(self.err(format!("expected cursor name, got {other:?}")));
4515 }
4516 };
4517 Ok(if is_move {
4518 Statement::MoveCursor { name, direction }
4519 } else {
4520 Statement::FetchCursor { name, direction }
4521 })
4522 }
4523
4524 /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4525 /// [(kind, …)] ON <col>, … FROM <table>`.
4526 fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4527 self.advance(); // STATISTICS
4528 // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4529 let mut if_not_exists = false;
4530 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4531 && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4532 {
4533 self.advance();
4534 self.advance();
4535 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4536 self.advance();
4537 if_not_exists = true;
4538 }
4539 }
4540 let name = self.expect_ident_like()?;
4541 let mut kinds = Vec::new();
4542 if matches!(self.peek(), Token::LParen) {
4543 self.advance();
4544 loop {
4545 let k = self.expect_ident_like()?;
4546 // PG stores the single letters; accept the spelled-out
4547 // names the SQL uses and record what PG records.
4548 kinds.push(match k.to_ascii_lowercase().as_str() {
4549 "ndistinct" => String::from("d"),
4550 "dependencies" => String::from("f"),
4551 "mcv" => String::from("m"),
4552 other => {
4553 return Err(
4554 self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4555 );
4556 }
4557 });
4558 match self.advance() {
4559 Token::Comma => {}
4560 Token::RParen => break,
4561 other => {
4562 return Err(self.err(alloc::format!(
4563 "expected ',' or ')' in statistics kind list, got {other:?}"
4564 )));
4565 }
4566 }
4567 }
4568 }
4569 if !matches!(self.peek(), Token::On) {
4570 return Err(self.err(alloc::format!(
4571 "expected ON in CREATE STATISTICS, got {:?}",
4572 self.peek()
4573 )));
4574 }
4575 self.advance();
4576 let mut columns = Vec::new();
4577 loop {
4578 columns.push(self.expect_ident_like()?);
4579 if matches!(self.peek(), Token::Comma) {
4580 self.advance();
4581 } else {
4582 break;
4583 }
4584 }
4585 if !matches!(self.peek(), Token::From) {
4586 return Err(self.err(alloc::format!(
4587 "expected FROM in CREATE STATISTICS, got {:?}",
4588 self.peek()
4589 )));
4590 }
4591 self.advance();
4592 let table = self.expect_ident_like()?;
4593 Ok(Statement::CreateStatistics {
4594 name,
4595 if_not_exists,
4596 kinds,
4597 columns,
4598 table,
4599 })
4600 }
4601
4602 /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4603 /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4604 /// entered with the `TABLE` keyword still unconsumed. Extracted so
4605 /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4606 /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4607 /// forward call.
4608 fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4609 self.advance(); // TABLE
4610 let if_exists = self.consume_if_exists();
4611 let mut names: Vec<String> = Vec::new();
4612 loop {
4613 names.push(self.expect_ident_like()?);
4614 if matches!(self.peek(), Token::Comma) {
4615 self.advance();
4616 continue;
4617 }
4618 break;
4619 }
4620 if matches!(
4621 self.peek(),
4622 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4623 || s.eq_ignore_ascii_case("restrict")
4624 ) {
4625 self.advance();
4626 }
4627 Ok(Statement::DropTable { names, if_exists })
4628 }
4629
4630 fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4631 self.advance(); // STATISTICS
4632 let mut if_exists = false;
4633 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4634 && matches!(self.tokens.get(self.pos + 1),
4635 Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4636 {
4637 self.advance();
4638 self.advance();
4639 if_exists = true;
4640 }
4641 let name = self.expect_ident_like()?;
4642 Ok(Statement::DropStatistics { name, if_exists })
4643 }
4644
4645 fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4646 debug_assert!(matches!(self.peek(), Token::Create));
4647 self.advance();
4648 match self.peek() {
4649 Token::Table => self.parse_create_table_stmt_after_create(),
4650 Token::Index => self.parse_create_index_stmt_after_create(false),
4651 // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4652 // object now. It used to be consumed by the CREATE-noise
4653 // arm, so a pg_dump that declares extended statistics
4654 // restored silently without them and reflection showed
4655 // nothing.
4656 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4657 self.parse_create_statistics_after_create()
4658 }
4659 // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4660 // The `UNIQUE` modifier turns a partial index into a
4661 // partial-uniqueness invariant (only rows matching the
4662 // WHERE predicate are checked for duplicates). mailrs
4663 // K1 (3 hits: email_templates default, calendar_events
4664 // master, calendar_events instance).
4665 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4666 self.advance();
4667 if !matches!(self.peek(), Token::Index) {
4668 return Err(self.err(alloc::format!(
4669 "expected INDEX after CREATE UNIQUE, got {:?}",
4670 self.peek()
4671 )));
4672 }
4673 self.parse_create_index_stmt_after_create(true)
4674 }
4675 Token::Publication => {
4676 self.advance();
4677 self.parse_create_publication_after_keyword()
4678 }
4679 Token::Subscription => {
4680 self.advance();
4681 self.parse_create_subscription_after_keyword()
4682 }
4683 // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4684 // USER isn't a reserved keyword — we look for the bare
4685 // identifier so the lexer doesn't have to grow a token.
4686 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4687 self.advance();
4688 self.parse_create_user_after_keyword(true)
4689 }
4690 // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4691 // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4692 // the default of the LOGIN attribute.
4693 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4694 self.advance();
4695 self.parse_create_user_after_keyword(false)
4696 }
4697 // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4698 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4699 self.advance();
4700 self.parse_create_policy_after_keyword()
4701 }
4702 // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4703 // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4704 // no-op. mailrs follow-up F3.
4705 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4706 self.advance();
4707 self.parse_create_extension_after_keyword()
4708 }
4709 // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4710 // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4711 // optional; absorb it here and forward to the
4712 // per-kind parsers with the flag. OR is a reserved
4713 // keyword token.
4714 Token::Or => {
4715 self.advance();
4716 let next = self.peek();
4717 let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4718 return Err(self.err(alloc::format!(
4719 "expected REPLACE after CREATE OR, got {next:?}"
4720 )));
4721 };
4722 if !s2.eq_ignore_ascii_case("replace") {
4723 return Err(self.err(alloc::format!(
4724 "expected REPLACE after CREATE OR, got {s2:?}"
4725 )));
4726 }
4727 self.advance();
4728 self.parse_create_function_or_trigger_after_or_replace(true)
4729 }
4730 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4731 self.advance();
4732 self.parse_create_function_after_keyword(false)
4733 }
4734 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4735 self.advance();
4736 self.parse_create_trigger_after_keyword(false)
4737 }
4738 // v7.39 (round 139) — CREATE RULE …
4739 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4740 self.advance();
4741 self.parse_create_rule_after_keyword(false)
4742 }
4743 // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4744 // trigger is a row-level AFTER trigger that additionally carries
4745 // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4746 // path already tolerates and skips those clauses, so consuming the
4747 // CONSTRAINT keyword and reusing it makes the statement parse and the
4748 // trigger fire. (The deferral timing itself is not yet honoured —
4749 // SPG fires it as a plain AFTER trigger, which is correct behaviour
4750 // for every non-deferred use.)
4751 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4752 self.advance();
4753 if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4754 if t.eq_ignore_ascii_case("trigger"))
4755 {
4756 return Err(self.err(alloc::format!(
4757 "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4758 self.peek()
4759 )));
4760 }
4761 self.advance();
4762 self.parse_create_trigger_after_keyword(false)
4763 }
4764 // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4765 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4766 self.advance();
4767 self.parse_create_sequence_after_keyword(false)
4768 }
4769 // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4770 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4771 self.advance();
4772 self.parse_create_view_after_keyword(false, false, false)
4773 }
4774 // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4775 // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4776 // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4777 // appear (in any order) between `CREATE` and `VIEW` in
4778 // every mysqldump-emitted view. Pre-2.6 the parser
4779 // rejected the prefix and the customer's whole view
4780 // backup failed on the first view. The hints are pure
4781 // planner / permission metadata; SPG's view-rewrite
4782 // path is semantically equivalent for all three
4783 // algorithms in v7.17 (TEMPTABLE differs only in
4784 // perf for huge views — out of v7.17 scope), and
4785 // DEFINER / SQL SECURITY are pure single-user
4786 // permissioning that SPG ignores by design.
4787 Token::Ident(s) | Token::QuotedIdent(s)
4788 if s.eq_ignore_ascii_case("algorithm")
4789 || s.eq_ignore_ascii_case("definer")
4790 || s.eq_ignore_ascii_case("sql") =>
4791 {
4792 self.consume_mysql_view_prefix()?;
4793 // After absorbing ALGORITHM / DEFINER / SQL SECURITY
4794 // (in any order, in any combination), the next
4795 // keyword must be VIEW. mysqldump never emits these
4796 // prefixes on non-view statements.
4797 let next = self.peek().clone();
4798 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
4799 if s2.eq_ignore_ascii_case("view"))
4800 {
4801 self.advance();
4802 self.parse_create_view_after_keyword(false, false, false)
4803 } else {
4804 Err(self.err(alloc::format!(
4805 "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
4806 )))
4807 }
4808 }
4809 // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
4810 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
4811 self.advance();
4812 self.parse_create_type_after_keyword()
4813 }
4814 // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
4815 // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
4816 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
4817 self.advance();
4818 self.parse_create_domain_after_keyword()
4819 }
4820 // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
4821 // name [AUTHORIZATION user]. Real catalog registry
4822 // (was silent-no-op'd pre-v7.17).
4823 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
4824 self.advance();
4825 let if_not_exists = self.parse_if_not_exists();
4826 let name = self.expect_ident_like()?;
4827 // Optional `AUTHORIZATION <user>` trailer — accepted,
4828 // ignored (single-user catalog).
4829 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4830 if s.eq_ignore_ascii_case("authorization"))
4831 {
4832 self.advance();
4833 let _ = self.expect_ident_like()?;
4834 }
4835 Ok(Statement::CreateSchema { name, if_not_exists })
4836 }
4837 // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
4838 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
4839 self.advance();
4840 let next = self.peek().clone();
4841 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4842 {
4843 self.advance();
4844 self.parse_create_materialized_view_after_keyword()
4845 } else {
4846 Err(self.err(alloc::format!(
4847 "expected VIEW after CREATE MATERIALIZED, got {next:?}"
4848 )))
4849 }
4850 }
4851 // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
4852 // no-op below), an UNLOGGED table is a real, fully-usable table in
4853 // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
4854 // durability optimisation is a follow-up), so a dump / app that
4855 // declares UNLOGGED tables works instead of failing to parse.
4856 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
4857 self.advance(); // UNLOGGED
4858 if matches!(self.peek(), Token::Table) {
4859 self.parse_create_table_stmt_after_create()
4860 } else {
4861 Err(self.err(format!(
4862 "expected TABLE after CREATE UNLOGGED, got {:?}",
4863 self.peek()
4864 )))
4865 }
4866 }
4867 Token::Ident(s) | Token::QuotedIdent(s)
4868 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
4869 {
4870 self.advance();
4871 // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
4872 let next = self.peek().clone();
4873 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
4874 {
4875 self.advance();
4876 self.parse_create_sequence_after_keyword(true)
4877 } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4878 {
4879 self.advance();
4880 self.parse_create_view_after_keyword(false, false, true)
4881 } else {
4882 // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
4883 // consumed and answered OK while creating nothing, so
4884 // every statement that touched the table afterwards failed
4885 // with "table not found" — the DDL itself lied. It is a
4886 // real CREATE TABLE now, marked temporary so the executor
4887 // puts it in the session's own namespace. An optional
4888 // TABLE keyword may or may not be present (`CREATE TEMP t`
4889 // is not legal, but the keyword is consumed by the
4890 // CREATE TABLE parser itself).
4891 let stmt = self.parse_create_table_stmt_after_create()?;
4892 match stmt {
4893 Statement::CreateTable(mut c) => {
4894 c.temporary = true;
4895 Ok(Statement::CreateTable(c))
4896 }
4897 // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
4898 // CTAS node, which needs the same session namespace.
4899 Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
4900 m.temporary = true;
4901 Ok(Statement::CreateMaterializedView(m))
4902 }
4903 other => Ok(other),
4904 }
4905 }
4906 }
4907 // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
4908 // BEGIN <body> END`. The body may reference `@var`
4909 // session variables, SET statements, internal `;`
4910 // terminators, etc. SPG has no procedure runtime, so
4911 // consume the whole `CREATE PROCEDURE … END` block as
4912 // a no-op so mysqldump scripts that include stored
4913 // routines load through. The matching-END consumer
4914 // tracks BEGIN/END nesting depth to handle nested
4915 // BEGIN blocks correctly.
4916 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
4917 self.consume_mysql_routine_body();
4918 Ok(Statement::Empty)
4919 }
4920 // v7.14.0 — pg_dump / mysqldump emit
4921 // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
4922 // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
4923 // SPG is single-schema / single-database; these have
4924 // no behavioural effect, so consume + return Empty.
4925 // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
4926 // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
4927 // moved up to real parser branches. DATABASE / ROLE /
4928 // POLICY / OPERATOR stay no-op forever
4929 // (single-database, hardcoded roles).
4930 Token::Ident(s) | Token::QuotedIdent(s)
4931 if matches!(
4932 s.to_ascii_lowercase().as_str(),
4933 "database"
4934 | "role"
4935 | "operator"
4936 | "cast"
4937 | "aggregate"
4938 | "language"
4939 | "collation"
4940 | "conversion"
4941 // v7.17.0 Phase 8 (audit N6) — rarely-
4942 // emitted pg_dump shapes that should
4943 // load through without a parser error.
4944 // SPG has no planner statistics catalog,
4945 // no event-trigger hooks, no foreign-
4946 // data-wrapper infrastructure; consume
4947 // + return Empty.
4948 | "statistics"
4949 | "event"
4950 // v7.37.17 (17.6 siblings) — additional CREATE
4951 // targets pg_dump / operator install scripts
4952 // may emit that SPG has no matching machinery
4953 // for. Consume + Empty-return.
4954 | "text"
4955 | "tablespace"
4956 | "access"
4957 | "large"
4958 ) =>
4959 {
4960 // DATABASE is the one member of this list PG refuses
4961 // inside a transaction block; the rest (ROLE, CAST,
4962 // TABLESPACE, …) it runs there quite happily, so only
4963 // this one is named. Still a no-op otherwise — SPG is
4964 // single-database.
4965 let is_database = s.eq_ignore_ascii_case("database");
4966 let collation = if is_database {
4967 self.scan_database_collation_until_boundary()
4968 } else {
4969 self.consume_until_statement_boundary();
4970 None
4971 };
4972 if is_database {
4973 return Ok(Statement::NoOpPreventedInTransaction {
4974 what: String::from("CREATE DATABASE"),
4975 collation,
4976 });
4977 }
4978 Ok(Statement::Empty)
4979 }
4980 // v7.39 (round 706) — the foreign-data family leaves the silent
4981 // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
4982 // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
4983 // FDW machinery), but the ENGINE now warns, so a restore log
4984 // says what will not function instead of reporting success.
4985 Token::Ident(s) | Token::QuotedIdent(s)
4986 if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
4987 {
4988 self.consume_until_statement_boundary();
4989 Ok(Statement::ValidateOnly {
4990 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
4991 names: Vec::new(),
4992 })
4993 }
4994 other => Err(self.err(format!(
4995 "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
4996 ))),
4997 }
4998 }
4999
5000 /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
5001 /// keyword decides whether we parse a function or trigger
5002 /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
5003 /// PROCEDURE) — those land in later releases.
5004 fn parse_create_function_or_trigger_after_or_replace(
5005 &mut self,
5006 or_replace: bool,
5007 ) -> Result<Statement, ParseError> {
5008 let tok = self.peek();
5009 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5010 return Err(self.err(alloc::format!(
5011 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
5012 )));
5013 };
5014 if s.eq_ignore_ascii_case("function") {
5015 self.advance();
5016 self.parse_create_function_after_keyword(or_replace)
5017 } else if s.eq_ignore_ascii_case("trigger") {
5018 self.advance();
5019 self.parse_create_trigger_after_keyword(or_replace)
5020 } else if s.eq_ignore_ascii_case("rule") {
5021 // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
5022 self.advance();
5023 self.parse_create_rule_after_keyword(or_replace)
5024 } else if s.eq_ignore_ascii_case("view") {
5025 // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
5026 self.advance();
5027 self.parse_create_view_after_keyword(or_replace, false, false)
5028 } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
5029 // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
5030 self.advance();
5031 let nxt = self.peek().clone();
5032 if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
5033 {
5034 self.advance();
5035 self.parse_create_view_after_keyword(or_replace, false, true)
5036 } else {
5037 Err(self.err(alloc::format!(
5038 "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
5039 )))
5040 }
5041 } else {
5042 Err(self.err(alloc::format!(
5043 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
5044 )))
5045 }
5046 }
5047
5048 /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
5049 /// SPG doesn't have a registry; pgvector / similar are
5050 /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
5051 /// the syntax lets dual-target schemas keep the line.
5052 fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
5053 // Optional `IF NOT EXISTS`.
5054 self.consume_if_not_exists();
5055 let name = self.expect_ident_like()?;
5056 // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
5057 // CASCADE / FROM '<v>' clauses; we don't model them.
5058 loop {
5059 match self.peek() {
5060 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
5061 self.advance();
5062 continue;
5063 }
5064 Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
5065 self.advance();
5066 let _ = self.expect_ident_like()?;
5067 continue;
5068 }
5069 Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
5070 self.advance();
5071 // String or ident literal.
5072 let _ = self.advance();
5073 continue;
5074 }
5075 Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
5076 self.advance();
5077 let _ = self.advance();
5078 continue;
5079 }
5080 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
5081 self.advance();
5082 continue;
5083 }
5084 _ => break,
5085 }
5086 }
5087 // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
5088 // nosuch` reported success and `pg_extension` then did not list it,
5089 // which is the accept-and-do-nothing shape F31 exists to find.
5090 Ok(Statement::ValidateOnly {
5091 kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
5092 names: alloc::vec![name],
5093 })
5094 }
5095
5096 /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
5097 /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
5098 /// already been consumed by the caller. Grammar accepted:
5099 ///
5100 /// name `(` arg-list `)`
5101 /// `RETURNS` return-type
5102 /// [ `LANGUAGE` ident ]
5103 /// `AS` $$ body $$
5104 /// [ `LANGUAGE` ident ]
5105 ///
5106 /// Either `LANGUAGE` position is allowed; PG accepts both.
5107 fn parse_create_function_after_keyword(
5108 &mut self,
5109 or_replace: bool,
5110 ) -> Result<Statement, ParseError> {
5111 let name = self.expect_ident_like()?;
5112 // Argument list. v7.12.4 commonly sees the empty `()`
5113 // (trigger functions); typed args parse and round-trip
5114 // but the executor only invokes nullary functions.
5115 if !matches!(self.peek(), Token::LParen) {
5116 return Err(self.err(alloc::format!(
5117 "expected '(' after function name {name:?}, got {:?}",
5118 self.peek()
5119 )));
5120 }
5121 self.advance();
5122 let args = self.parse_function_arg_list()?;
5123 // RETURNS clause.
5124 let tok = self.peek();
5125 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5126 return Err(self.err(alloc::format!(
5127 "expected RETURNS after function arg list, got {tok:?}"
5128 )));
5129 };
5130 if !s.eq_ignore_ascii_case("returns") {
5131 return Err(self.err(alloc::format!(
5132 "expected RETURNS after function arg list, got {s:?}"
5133 )));
5134 }
5135 self.advance();
5136 let returns = self.parse_function_return()?;
5137 // Optional LANGUAGE clause (PG also accepts after AS — we'll
5138 // re-check after the body too).
5139 let mut language: Option<String> = self.parse_optional_language()?;
5140 // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5141 // either side of the body and in any order, interleaved with
5142 // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5143 // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5144 // PG's own pg_dump output did not restore.
5145 let mut attrs = FunctionAttrs::default();
5146 loop {
5147 let before = self.pos;
5148 self.parse_function_attrs_into(&mut attrs)?;
5149 if language.is_none() {
5150 language = self.parse_optional_language()?;
5151 }
5152 if self.pos == before {
5153 break;
5154 }
5155 }
5156 // `AS` followed by a $$-quoted body (lexer already
5157 // collapses both `$$…$$` and `$tag$…$tag$` to a single
5158 // Token::String). AS is a reserved keyword (Token::As).
5159 if !matches!(self.peek(), Token::As) {
5160 return Err(self.err(alloc::format!(
5161 "expected AS before function body, got {:?}",
5162 self.peek()
5163 )));
5164 }
5165 self.advance();
5166 let body_text = match self.peek() {
5167 Token::String(s) => {
5168 let body = s.clone();
5169 self.advance();
5170 body
5171 }
5172 other => {
5173 return Err(self.err(alloc::format!(
5174 "expected $$-quoted function body after AS, got {other:?}"
5175 )));
5176 }
5177 };
5178 // Trailing clauses — PG's other accepted position for both the
5179 // LANGUAGE and the attributes.
5180 loop {
5181 let before = self.pos;
5182 self.parse_function_attrs_into(&mut attrs)?;
5183 if language.is_none() {
5184 language = self.parse_optional_language()?;
5185 }
5186 if self.pos == before {
5187 break;
5188 }
5189 }
5190 let language = language.unwrap_or_else(|| String::from("sql"));
5191 // PL/pgSQL bodies get structure-parsed. Other languages
5192 // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5193 // recognise) round-trip as Raw text — the executor errors
5194 // when invoked with a clear unsupported message.
5195 let body = if language.eq_ignore_ascii_case("plpgsql") {
5196 match parse_plpgsql_body(&body_text) {
5197 Ok(block) => FunctionBody::PlPgSql(block),
5198 // Best-effort: if the body parser doesn't yet
5199 // support a construct used inside, fall back to
5200 // raw — keeps `CREATE FUNCTION` itself working
5201 // (catalogue accepts), executor errors on
5202 // invocation only.
5203 Err(_) => FunctionBody::Raw(body_text),
5204 }
5205 } else {
5206 FunctionBody::Raw(body_text)
5207 };
5208 Ok(Statement::CreateFunction(CreateFunctionStatement {
5209 name,
5210 or_replace,
5211 args,
5212 returns,
5213 language,
5214 body,
5215 attrs,
5216 }))
5217 }
5218
5219 /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5220 /// attribute clauses into `attrs`, stopping at the first token that
5221 /// is not one. Measured against PG 18.4, which accepts them in any
5222 /// order and on either side of the body.
5223 fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5224 loop {
5225 let word = match self.peek() {
5226 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5227 // NOT LEAKPROOF — NOT is a reserved keyword token.
5228 Token::Not
5229 if matches!(
5230 self.tokens.get(self.pos + 1),
5231 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5232 ) =>
5233 {
5234 self.advance();
5235 self.advance();
5236 attrs.leakproof = false;
5237 continue;
5238 }
5239 _ => return Ok(()),
5240 };
5241 match word.as_str() {
5242 "immutable" => {
5243 self.advance();
5244 attrs.volatility = FunctionVolatility::Immutable;
5245 }
5246 "stable" => {
5247 self.advance();
5248 attrs.volatility = FunctionVolatility::Stable;
5249 }
5250 "volatile" => {
5251 self.advance();
5252 attrs.volatility = FunctionVolatility::Volatile;
5253 }
5254 "strict" => {
5255 self.advance();
5256 attrs.strict = true;
5257 }
5258 "leakproof" => {
5259 self.advance();
5260 attrs.leakproof = true;
5261 }
5262 // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5263 // spelled-out forms of STRICT and its opposite.
5264 "returns" | "called" => {
5265 let strict = word == "returns";
5266 let mut probe = self.pos + 1;
5267 if strict {
5268 // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5269 // is not ours.
5270 match self.tokens.get(probe) {
5271 Some(Token::Null) => probe += 1,
5272 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5273 _ => return Ok(()),
5274 }
5275 }
5276 let ok = matches!(self.tokens.get(probe), Some(Token::On))
5277 || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5278 if !ok {
5279 return Ok(());
5280 }
5281 probe += 1;
5282 match self.tokens.get(probe) {
5283 Some(Token::Null) => probe += 1,
5284 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5285 _ => return Ok(()),
5286 }
5287 match self.tokens.get(probe) {
5288 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5289 _ => return Ok(()),
5290 }
5291 self.pos = probe;
5292 attrs.strict = strict;
5293 }
5294 "security" | "external" => {
5295 // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5296 let mut probe = self.pos + 1;
5297 if word == "external" {
5298 match self.tokens.get(probe) {
5299 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5300 probe += 1;
5301 }
5302 _ => return Ok(()),
5303 }
5304 }
5305 let definer = match self.tokens.get(probe) {
5306 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5307 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5308 _ => return Ok(()),
5309 };
5310 self.pos = probe + 1;
5311 attrs.security_definer = definer;
5312 }
5313 "parallel" => {
5314 let level = match self.tokens.get(self.pos + 1) {
5315 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5316 FunctionParallel::Safe
5317 }
5318 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5319 FunctionParallel::Restricted
5320 }
5321 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5322 FunctionParallel::Unsafe
5323 }
5324 _ => return Ok(()),
5325 };
5326 self.pos += 2;
5327 attrs.parallel = level;
5328 }
5329 "cost" | "rows" => {
5330 let Some(n) = self.peek_number_at(self.pos + 1) else {
5331 return Ok(());
5332 };
5333 self.pos += 2;
5334 if word == "cost" {
5335 attrs.cost = Some(n);
5336 } else {
5337 attrs.rows = Some(n);
5338 }
5339 }
5340 _ => return Ok(()),
5341 }
5342 }
5343 }
5344
5345 /// The numeric literal at `idx`, if there is one.
5346 fn peek_number_at(&self, idx: usize) -> Option<f64> {
5347 match self.tokens.get(idx)? {
5348 Token::Integer(n) => Some(*n as f64),
5349 Token::Float(f) => Some(*f),
5350 Token::Numeric(t) => t.parse::<f64>().ok(),
5351 _ => None,
5352 }
5353 }
5354
5355 /// Closing `)`-terminated argument list. v7.12.4 commonly
5356 /// sees the empty `()`; typed args round-trip but the
5357 /// executor (yet) doesn't invoke them.
5358 /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5359 /// it away, which is what PG does with one on a function parameter.
5360 fn skip_type_modifier(&mut self) {
5361 if !matches!(self.peek(), Token::LParen) {
5362 return;
5363 }
5364 // Only a numeric modifier — anything else is not one, and eating
5365 // it would swallow real grammar.
5366 let mut i = self.pos + 1;
5367 let mut seen_number = false;
5368 loop {
5369 match self.tokens.get(i) {
5370 Some(Token::Integer(_)) => seen_number = true,
5371 Some(Token::Comma) => {}
5372 Some(Token::RParen) => break,
5373 _ => return,
5374 }
5375 i += 1;
5376 }
5377 if !seen_number {
5378 return;
5379 }
5380 while self.pos <= i {
5381 self.advance();
5382 }
5383 }
5384
5385 fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5386 let mut args: Vec<FunctionArg> = Vec::new();
5387 if matches!(self.peek(), Token::RParen) {
5388 self.advance();
5389 return Ok(args);
5390 }
5391 loop {
5392 // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5393 // a reserved token; OUT / INOUT are bare idents.
5394 let mode = if matches!(self.peek(), Token::In) {
5395 self.advance();
5396 FunctionArgMode::In
5397 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5398 {
5399 self.advance();
5400 FunctionArgMode::Out
5401 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5402 {
5403 self.advance();
5404 FunctionArgMode::InOut
5405 } else {
5406 FunctionArgMode::In
5407 };
5408 // Optional name. The next token is either a name
5409 // (followed by a type ident) or the type itself.
5410 // Disambiguate by peeking ahead: if the token after
5411 // the next ident is also an ident, we treat the
5412 // first as the name.
5413 // v7.39 (round 315, V19) — take EVERY ident-like word up to
5414 // the comma or paren, then decide. Reading at most two of
5415 // them could not spell `x double precision` at all, and
5416 // silently mis-read the bare `double precision` as a
5417 // parameter named "double" — which is what made the same
5418 // signature key two different ways.
5419 let (name, ty_token) = {
5420 let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5421 while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5422 words.push(self.expect_ident_like()?);
5423 }
5424 // v7.39 (round 344) — a length / precision modifier on the
5425 // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5426 // accepts it and DROPS it — `pg_get_function_arguments`
5427 // reports plain `character varying` / `numeric`, measured on
5428 // 18.4 — but SPG raised `syntax error at or near "("`,
5429 // because the modifier's parens were never consumed.
5430 self.skip_type_modifier();
5431 // r1049 — `f(v bigint[])`. The array suffix parsed in
5432 // the column position, the cast position and (r1038)
5433 // the RETURNS position, but not here: the fifth
5434 // member of the same family, reported by sentori as
5435 // presumably the same code. It is now.
5436 let array_suffix = self.consume_array_suffix();
5437 let whole = words.join(" ");
5438 let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5439 {
5440 (Some(words[0].clone()), words[1..].join(" "))
5441 } else {
5442 (None, whole)
5443 };
5444 ty_token.push_str(&array_suffix);
5445 (name, ty_token)
5446 };
5447 // Type — try to map to ColumnTypeName, else Raw.
5448 let ty = match map_type_ident_to_column_type_name(&ty_token) {
5449 Some(t) => FunctionArgType::Typed(t),
5450 None => FunctionArgType::Raw(ty_token),
5451 };
5452 args.push(FunctionArg { mode, name, ty });
5453 match self.peek() {
5454 Token::Comma => {
5455 self.advance();
5456 continue;
5457 }
5458 Token::RParen => {
5459 self.advance();
5460 return Ok(args);
5461 }
5462 other => {
5463 return Err(self.err(alloc::format!(
5464 "expected , or ) in function arg list, got {other:?}"
5465 )));
5466 }
5467 }
5468 }
5469 }
5470
5471 fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5472 // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5473 // function whose row shape is named inline.
5474 if matches!(self.peek(), Token::Table)
5475 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5476 {
5477 self.advance(); // TABLE
5478 self.advance(); // (
5479 let mut cols: Vec<String> = Vec::new();
5480 loop {
5481 let cname = self.expect_ident_like()?;
5482 let mut ty: Vec<String> = Vec::new();
5483 loop {
5484 match self.peek() {
5485 Token::Comma | Token::RParen | Token::Eof => break,
5486 _ => {}
5487 }
5488 match self.advance() {
5489 Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5490 other => {
5491 if let Some(w) = unreserved_keyword_text(&other) {
5492 ty.push(w);
5493 }
5494 }
5495 }
5496 }
5497 cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5498 if matches!(self.peek(), Token::Comma) {
5499 self.advance();
5500 } else {
5501 break;
5502 }
5503 }
5504 if matches!(self.peek(), Token::RParen) {
5505 self.advance();
5506 }
5507 return Ok(FunctionReturn::Other(alloc::format!(
5508 "TABLE({})",
5509 cols.join(", ")
5510 )));
5511 }
5512 let ident = self.expect_ident_like()?;
5513 // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5514 if ident.eq_ignore_ascii_case("setof") {
5515 let inner = self.expect_ident_like()?;
5516 let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5517 return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5518 }
5519 if ident.eq_ignore_ascii_case("trigger") {
5520 return Ok(FunctionReturn::Trigger);
5521 }
5522 if ident.eq_ignore_ascii_case("void") {
5523 return Ok(FunctionReturn::Void);
5524 }
5525 // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5526 // RETURN position did not, so the `[` was a syntax error and the
5527 // whole migration stopped. sentori worked around it by returning
5528 // zero-padded text.
5529 let suffix = self.consume_array_suffix();
5530 if !suffix.is_empty() {
5531 return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5532 }
5533 match map_type_ident_to_column_type_name(&ident) {
5534 Some(t) => Ok(FunctionReturn::Type(t)),
5535 None => Ok(FunctionReturn::Other(ident)),
5536 }
5537 }
5538
5539 /// Consume any `[]` / `[N]` array markers after a type name and give
5540 /// back their text. Empty when there are none.
5541 fn consume_array_suffix(&mut self) -> String {
5542 let mut out = String::new();
5543 while matches!(self.peek(), Token::LBracket) {
5544 self.advance();
5545 // `[N]` is accepted and, as in PG, the length is not enforced.
5546 if let Token::Integer(n) = self.peek().clone() {
5547 self.advance();
5548 out.push_str(&alloc::format!("[{n}]"));
5549 } else {
5550 out.push_str("[]");
5551 }
5552 if matches!(self.peek(), Token::RBracket) {
5553 self.advance();
5554 }
5555 }
5556 out
5557 }
5558
5559 fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5560 match self.peek() {
5561 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5562 self.advance();
5563 let lang = self.expect_ident_like()?;
5564 Ok(Some(lang.to_ascii_lowercase()))
5565 }
5566 _ => Ok(None),
5567 }
5568 }
5569
5570 /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5571 /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5572 /// (expr)]*`. The `DOMAIN` keyword has already been
5573 /// consumed. PG allows the trailing constraints in any
5574 /// order; we approximate with a small loop.
5575 fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5576 let name = self.expect_ident_like()?;
5577 // Optional `AS`.
5578 if matches!(self.peek(), Token::As) {
5579 self.advance();
5580 }
5581 // v7.39 (round 259) — keep the raw type NAME when the base is not
5582 // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5583 // parent domain.
5584 let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _) =
5585 self.parse_type_with_implied_flags()?;
5586 let mut default: Option<Expr> = None;
5587 let mut not_null = false;
5588 let mut checks: Vec<Expr> = Vec::new();
5589 loop {
5590 match self.peek() {
5591 Token::Default => {
5592 if default.is_some() {
5593 return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5594 }
5595 self.advance();
5596 default = Some(self.parse_expr(0)?);
5597 }
5598 Token::Not => {
5599 self.advance();
5600 if !matches!(self.peek(), Token::Null) {
5601 return Err(self.err(alloc::format!(
5602 "expected NULL after NOT in DOMAIN, got {:?}",
5603 self.peek()
5604 )));
5605 }
5606 self.advance();
5607 not_null = true;
5608 }
5609 Token::Null => {
5610 self.advance();
5611 // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5612 // is the default-nullable marker (PG accepts it),
5613 // but AFTER a NOT NULL it is a conflict PG refuses
5614 // (`conflicting NULL/NOT NULL constraints`,
5615 // PG18-measured); the old arm no-opped both ways.
5616 if not_null {
5617 return Err(self.err(alloc::string::String::from(
5618 "conflicting NULL/NOT NULL constraints",
5619 )));
5620 }
5621 }
5622 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5623 self.advance();
5624 if !matches!(self.peek(), Token::LParen) {
5625 return Err(self.err(alloc::format!(
5626 "expected '(' after CHECK in DOMAIN, got {:?}",
5627 self.peek()
5628 )));
5629 }
5630 self.advance();
5631 let expr = self.parse_expr(0)?;
5632 if !matches!(self.peek(), Token::RParen) {
5633 return Err(self.err(alloc::format!(
5634 "expected ')' after CHECK expr, got {:?}",
5635 self.peek()
5636 )));
5637 }
5638 self.advance();
5639 checks.push(expr);
5640 }
5641 // CONSTRAINT <name> CHECK (…) — PG accepts a name
5642 // prefix on the constraint; we drop the name and
5643 // recurse into the constraint parsing.
5644 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5645 self.advance();
5646 let _ = self.expect_ident_like()?;
5647 }
5648 _ => break,
5649 }
5650 }
5651 Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5652 name,
5653 base_type,
5654 base_domain: base_user_ref,
5655 default,
5656 not_null,
5657 checks,
5658 }))
5659 }
5660
5661 /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5662 /// ('a', 'b', …)`. The `TYPE` keyword has already been
5663 /// consumed.
5664 fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5665 let name = self.expect_ident_like()?;
5666 // Required `AS`.
5667 if !matches!(self.peek(), Token::As) {
5668 return Err(self.err(alloc::format!(
5669 "expected AS after CREATE TYPE {name:?}, got {:?}",
5670 self.peek()
5671 )));
5672 }
5673 self.advance();
5674 // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5675 // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5676 // on the next token: `(` = composite, ident `ENUM` = enum.
5677 if matches!(self.peek(), Token::LParen) {
5678 self.advance();
5679 let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5680 let mut field_user_types: Vec<Option<String>> = Vec::new();
5681 // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5682 // is legal PG (an attribute-less composite; measured — the old
5683 // e2e note claimed PG requires at least one attribute).
5684 if matches!(self.peek(), Token::RParen) {
5685 self.advance();
5686 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5687 name,
5688 kind: crate::ast::TypeKind::Composite {
5689 fields,
5690 field_user_types,
5691 },
5692 }));
5693 }
5694 loop {
5695 let field_name = self.expect_ident_like()?;
5696 // v7.39 (round 264) — keep the raw type name when it is not
5697 // a builtin: that is how a NESTED composite field records
5698 // which composite it holds.
5699 let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _) =
5700 self.parse_type_with_implied_flags()?;
5701 fields.push((field_name, field_type));
5702 field_user_types.push(field_user_ref);
5703 if matches!(self.peek(), Token::Comma) {
5704 self.advance();
5705 continue;
5706 }
5707 if matches!(self.peek(), Token::RParen) {
5708 self.advance();
5709 break;
5710 }
5711 return Err(self.err(alloc::format!(
5712 "expected , or ) in composite field list, got {:?}",
5713 self.peek()
5714 )));
5715 }
5716 if fields.is_empty() {
5717 return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5718 }
5719 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5720 name,
5721 kind: crate::ast::TypeKind::Composite {
5722 fields,
5723 field_user_types,
5724 },
5725 }));
5726 }
5727 // Required `ENUM` ident.
5728 let kind_ident = match self.peek().clone() {
5729 Token::Ident(s) | Token::QuotedIdent(s) => s,
5730 other => {
5731 return Err(self.err(alloc::format!(
5732 "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5733 )));
5734 }
5735 };
5736 if !kind_ident.eq_ignore_ascii_case("enum") {
5737 return Err(self.err(alloc::format!(
5738 "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5739 )));
5740 }
5741 self.advance();
5742 if !matches!(self.peek(), Token::LParen) {
5743 return Err(self.err(alloc::format!(
5744 "expected '(' after ENUM, got {:?}",
5745 self.peek()
5746 )));
5747 }
5748 self.advance();
5749 let mut labels: Vec<String> = Vec::new();
5750 loop {
5751 match self.peek().clone() {
5752 Token::String(s) => {
5753 self.advance();
5754 labels.push(s);
5755 }
5756 other => {
5757 return Err(
5758 self.err(alloc::format!("expected enum label string, got {other:?}"))
5759 );
5760 }
5761 }
5762 if matches!(self.peek(), Token::Comma) {
5763 self.advance();
5764 continue;
5765 }
5766 if matches!(self.peek(), Token::RParen) {
5767 self.advance();
5768 break;
5769 }
5770 return Err(self.err(alloc::format!(
5771 "expected , or ) in ENUM label list, got {:?}",
5772 self.peek()
5773 )));
5774 }
5775 if labels.is_empty() {
5776 return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5777 }
5778 Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5779 name,
5780 kind: crate::ast::TypeKind::Enum { labels },
5781 }))
5782 }
5783
5784 /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
5785 /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
5786 /// The `CREATE MATERIALIZED VIEW` keywords have already been
5787 /// consumed.
5788 fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
5789 let if_not_exists = self.parse_if_not_exists();
5790 let name = self.expect_ident_like()?;
5791 let mut columns: Vec<String> = Vec::new();
5792 if matches!(self.peek(), Token::LParen) {
5793 self.advance();
5794 loop {
5795 let c = self.expect_ident_like()?;
5796 columns.push(c);
5797 if matches!(self.peek(), Token::Comma) {
5798 self.advance();
5799 continue;
5800 }
5801 if matches!(self.peek(), Token::RParen) {
5802 self.advance();
5803 break;
5804 }
5805 return Err(self.err(alloc::format!(
5806 "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
5807 self.peek()
5808 )));
5809 }
5810 }
5811 if !matches!(self.peek(), Token::As) {
5812 return Err(self.err(alloc::format!(
5813 "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
5814 self.peek()
5815 )));
5816 }
5817 self.advance();
5818 // v7.39 (round 151) — a WITH-headed body is legal (read-only
5819 // CTEs only; the engine rejects data-modifying ones with PG's
5820 // message). A trailing `WITH [NO] DATA` can't START the body,
5821 // so WITH here heads the query.
5822 let body = if self.peek_is_with_kw() {
5823 self.advance();
5824 self.parse_nested_with_select()?
5825 } else {
5826 let body_stmt = self.parse_select_stmt()?;
5827 let Statement::Select(body) = body_stmt else {
5828 return Err(self.err(alloc::format!(
5829 "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
5830 )));
5831 };
5832 body
5833 };
5834 // Optional trailing `WITH [NO] DATA`.
5835 let with_data = self.parse_optional_with_data(true)?;
5836 Ok(Statement::CreateMaterializedView(
5837 crate::ast::CreateMaterializedViewStatement {
5838 temporary: false,
5839 name,
5840 if_not_exists,
5841 columns,
5842 body,
5843 with_data,
5844 as_plain_table: false,
5845 },
5846 ))
5847 }
5848
5849 /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
5850 /// `default_when_absent` is what to return if the tail is
5851 /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
5852 /// WITH DATA).
5853 fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
5854 let save = self.pos;
5855 // `WITH` is an Ident (not reserved in the lexer).
5856 let is_with = match self.peek() {
5857 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
5858 _ => false,
5859 };
5860 if !is_with {
5861 return Ok(default_when_absent);
5862 }
5863 self.advance();
5864 // Optional `NO`.
5865 let mut with_data = true;
5866 let is_no = match self.peek() {
5867 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
5868 _ => false,
5869 };
5870 if is_no {
5871 self.advance();
5872 with_data = false;
5873 }
5874 // Required `DATA` ident.
5875 let is_data = match self.peek() {
5876 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
5877 _ => false,
5878 };
5879 if is_data {
5880 self.advance();
5881 Ok(with_data)
5882 } else {
5883 // Caller's WITH wasn't WITH-DATA — rewind so the outer
5884 // parser can interpret it.
5885 self.pos = save;
5886 Ok(default_when_absent)
5887 }
5888 }
5889
5890 /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
5891 /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
5892 /// All keyword prefixes have already been consumed; the flags
5893 /// say which were present.
5894 fn parse_create_view_after_keyword(
5895 &mut self,
5896 or_replace: bool,
5897 _materialized_unused: bool,
5898 temporary: bool,
5899 ) -> Result<Statement, ParseError> {
5900 let if_not_exists = self.parse_if_not_exists();
5901 let name = self.expect_ident_like()?;
5902 // Optional `(col, col, …)` rename list.
5903 let mut columns: Vec<String> = Vec::new();
5904 if matches!(self.peek(), Token::LParen) {
5905 self.advance();
5906 loop {
5907 let c = self.expect_ident_like()?;
5908 columns.push(c);
5909 if matches!(self.peek(), Token::Comma) {
5910 self.advance();
5911 continue;
5912 }
5913 if matches!(self.peek(), Token::RParen) {
5914 self.advance();
5915 break;
5916 }
5917 return Err(self.err(alloc::format!(
5918 "expected , or ) in VIEW column list, got {:?}",
5919 self.peek()
5920 )));
5921 }
5922 }
5923 // Required `AS`.
5924 if !matches!(self.peek(), Token::As) {
5925 return Err(self.err(alloc::format!(
5926 "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
5927 self.peek()
5928 )));
5929 }
5930 self.advance();
5931 // Body: a regular SELECT statement. v7.39 (round 151) — a
5932 // WITH-headed body is legal too (read-only CTEs only; the
5933 // engine rejects data-modifying ones with PG's message).
5934 // Disambiguation vs `WITH CHECK OPTION`: a body can't START
5935 // with the check-option clause, so WITH here heads the query.
5936 let body = if self.peek_is_with_kw() {
5937 self.advance();
5938 self.parse_nested_with_select()?
5939 } else {
5940 let body_stmt = self.parse_select_stmt()?;
5941 let Statement::Select(body) = body_stmt else {
5942 return Err(self.err(alloc::format!(
5943 "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
5944 )));
5945 };
5946 body
5947 };
5948 // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
5949 // The SELECT parser stops before a trailing WITH, so it lands here.
5950 let check_option = if matches!(self.peek(),
5951 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
5952 {
5953 self.advance(); // WITH
5954 let opt = match self.peek() {
5955 Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
5956 self.advance();
5957 crate::ast::ViewCheckOption::Local
5958 }
5959 Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
5960 self.advance();
5961 crate::ast::ViewCheckOption::Cascaded
5962 }
5963 // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
5964 _ => crate::ast::ViewCheckOption::Cascaded,
5965 };
5966 if !matches!(self.peek(),
5967 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
5968 {
5969 return Err(self.err(alloc::format!(
5970 "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
5971 self.peek()
5972 )));
5973 }
5974 self.advance(); // CHECK
5975 if !matches!(self.peek(),
5976 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
5977 {
5978 return Err(self.err(alloc::format!(
5979 "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
5980 self.peek()
5981 )));
5982 }
5983 self.advance(); // OPTION
5984 Some(opt)
5985 } else {
5986 None
5987 };
5988 Ok(Statement::CreateView(crate::ast::CreateViewStatement {
5989 name,
5990 or_replace,
5991 if_not_exists,
5992 temporary,
5993 columns,
5994 body,
5995 check_option,
5996 }))
5997 }
5998
5999 /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
6000 /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
6001 /// consumed; `temporary` carries whether TEMPORARY was seen.
6002 fn parse_create_sequence_after_keyword(
6003 &mut self,
6004 temporary: bool,
6005 ) -> Result<Statement, ParseError> {
6006 let if_not_exists = self.parse_if_not_exists();
6007 let name = self.expect_ident_like()?;
6008 // Optional `AS data_type`.
6009 let data_type = if matches!(self.peek(), Token::As) {
6010 self.advance();
6011 Some(self.parse_sequence_data_type()?)
6012 } else {
6013 None
6014 };
6015 let options = self.parse_sequence_options(/* allow_restart = */ false)?;
6016 Ok(Statement::CreateSequence(
6017 crate::ast::CreateSequenceStatement {
6018 name,
6019 if_not_exists,
6020 temporary,
6021 data_type,
6022 options,
6023 },
6024 ))
6025 }
6026
6027 /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
6028 /// already been consumed; this is reached after `SEQUENCE`.
6029 /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
6030 fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
6031 use crate::ast::AlterDomainAction as A;
6032 let name = self.expect_ident_like()?;
6033 // DROP / SET / ADD lex as reserved keyword tokens, not idents.
6034 let kw = match self.peek() {
6035 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6036 Token::Drop => alloc::string::String::from("drop"),
6037 Token::Default => alloc::string::String::from("default"),
6038 other => {
6039 return Err(self.err(alloc::format!(
6040 "expected an ALTER DOMAIN action, got {other:?}"
6041 )));
6042 }
6043 };
6044 let action = match kw.as_str() {
6045 "add" => {
6046 self.advance();
6047 let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
6048 {
6049 self.advance();
6050 Some(self.expect_ident_like()?)
6051 } else {
6052 None
6053 };
6054 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
6055 return Err(self.err(alloc::format!(
6056 "ALTER DOMAIN ADD supports CHECK only, got {:?}",
6057 self.peek()
6058 )));
6059 }
6060 self.advance();
6061 if !matches!(self.peek(), Token::LParen) {
6062 return Err(self.err("expected '(' after CHECK".into()));
6063 }
6064 self.advance();
6065 let check = self.parse_expr(0)?;
6066 if !matches!(self.peek(), Token::RParen) {
6067 return Err(self.err("expected ')' after CHECK expression".into()));
6068 }
6069 self.advance();
6070 A::AddConstraint { name: cname, check }
6071 }
6072 "drop" => {
6073 self.advance();
6074 match self.peek() {
6075 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
6076 self.advance();
6077 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
6078 {
6079 self.advance();
6080 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
6081 {
6082 return Err(self.err("expected EXISTS after IF".into()));
6083 }
6084 self.advance();
6085 true
6086 } else {
6087 false
6088 };
6089 let cn = self.expect_ident_like()?;
6090 A::DropConstraint {
6091 name: cn,
6092 if_exists,
6093 }
6094 }
6095 Token::Default => {
6096 self.advance();
6097 A::DropDefault
6098 }
6099 Token::Not => {
6100 self.advance();
6101 if !matches!(self.peek(), Token::Null) {
6102 return Err(self.err("expected NULL after NOT".into()));
6103 }
6104 self.advance();
6105 A::DropNotNull
6106 }
6107 other => {
6108 return Err(self.err(alloc::format!(
6109 "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
6110 )));
6111 }
6112 }
6113 }
6114 "set" => {
6115 self.advance();
6116 match self.peek() {
6117 Token::Default => {
6118 self.advance();
6119 A::SetDefault(self.parse_expr(0)?)
6120 }
6121 Token::Not => {
6122 self.advance();
6123 if !matches!(self.peek(), Token::Null) {
6124 return Err(self.err("expected NULL after NOT".into()));
6125 }
6126 self.advance();
6127 A::SetNotNull
6128 }
6129 other => {
6130 return Err(self.err(alloc::format!(
6131 "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6132 )));
6133 }
6134 }
6135 }
6136 "rename" => {
6137 self.advance();
6138 if !matches!(self.peek(), Token::To) {
6139 return Err(self.err("expected TO after RENAME".into()));
6140 }
6141 self.advance();
6142 A::RenameTo(self.expect_ident_like()?)
6143 }
6144 other => {
6145 return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6146 }
6147 };
6148 Ok(Statement::AlterDomain { name, action })
6149 }
6150
6151 fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6152 let if_exists = self.parse_if_exists();
6153 let name = self.expect_ident_like()?;
6154 // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6155 // the option list (PG allows only one or the other).
6156 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6157 self.advance();
6158 if matches!(self.peek(), Token::To) {
6159 self.advance();
6160 } else {
6161 self.expect_keyword_ident("to")?;
6162 }
6163 let new = self.expect_ident_like()?;
6164 return Ok(Statement::AlterSequence(
6165 crate::ast::AlterSequenceStatement {
6166 name,
6167 if_exists,
6168 options: crate::ast::SequenceOptions::default(),
6169 rename_to: Some(new),
6170 },
6171 ));
6172 }
6173 let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6174 Ok(Statement::AlterSequence(
6175 crate::ast::AlterSequenceStatement {
6176 name,
6177 if_exists,
6178 options,
6179 rename_to: None,
6180 },
6181 ))
6182 }
6183
6184 fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6185 let kw = self.expect_ident_like()?;
6186 match kw.to_ascii_lowercase().as_str() {
6187 "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6188 "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6189 "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6190 other => Err(self.err(alloc::format!(
6191 "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6192 ))),
6193 }
6194 }
6195
6196 fn parse_sequence_options(
6197 &mut self,
6198 allow_restart: bool,
6199 ) -> Result<crate::ast::SequenceOptions, ParseError> {
6200 use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6201 let mut opts = SequenceOptions::default();
6202 #[allow(clippy::while_let_loop)]
6203 loop {
6204 // Match an ident; stop at any non-ident token (sentinel,
6205 // semicolon, end of statement).
6206 let kw_lc = match self.peek() {
6207 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6208 _ => break,
6209 };
6210 match kw_lc.as_str() {
6211 "increment" => {
6212 self.advance();
6213 // Optional BY.
6214 if self.peek_is_by() {
6215 self.advance();
6216 }
6217 opts.increment = Some(self.expect_signed_int()?);
6218 }
6219 "minvalue" => {
6220 self.advance();
6221 opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6222 }
6223 "maxvalue" => {
6224 self.advance();
6225 opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6226 }
6227 "no" => {
6228 self.advance();
6229 let what = self.expect_ident_like()?;
6230 match what.to_ascii_lowercase().as_str() {
6231 "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6232 "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6233 "cycle" => opts.cycle = Some(false),
6234 other => {
6235 return Err(self.err(alloc::format!(
6236 "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6237 )));
6238 }
6239 }
6240 }
6241 "start" => {
6242 self.advance();
6243 // Optional WITH.
6244 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6245 if s.eq_ignore_ascii_case("with"))
6246 {
6247 self.advance();
6248 }
6249 opts.start = Some(self.expect_signed_int()?);
6250 }
6251 "restart" if allow_restart => {
6252 self.advance();
6253 // Optional WITH n; bare RESTART means restart at START.
6254 let mut with_val: Option<i64> = None;
6255 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6256 if s.eq_ignore_ascii_case("with"))
6257 {
6258 self.advance();
6259 with_val = Some(self.expect_signed_int()?);
6260 } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6261 with_val = Some(self.expect_signed_int()?);
6262 }
6263 opts.restart = Some(with_val);
6264 }
6265 "cache" => {
6266 self.advance();
6267 opts.cache = Some(self.expect_signed_int()?);
6268 }
6269 "cycle" => {
6270 self.advance();
6271 opts.cycle = Some(true);
6272 }
6273 "owned" => {
6274 self.advance();
6275 match self.peek() {
6276 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6277 self.advance();
6278 }
6279 other => {
6280 return Err(
6281 self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6282 );
6283 }
6284 }
6285 // OWNED BY {NONE | tab.col}. Read just one ident
6286 // (NOT expect_ident_like which would auto-strip
6287 // a schema prefix and consume the `.col` we need).
6288 let first = match self.advance() {
6289 Token::Ident(s) | Token::QuotedIdent(s) => s,
6290 other => {
6291 return Err(self.err(alloc::format!(
6292 "expected identifier or NONE after OWNED BY, got {other:?}"
6293 )));
6294 }
6295 };
6296 if first.eq_ignore_ascii_case("none") {
6297 opts.owned_by = Some(SequenceOwnedBy::None);
6298 } else if matches!(self.peek(), Token::Dot) {
6299 self.advance();
6300 let second = match self.advance() {
6301 Token::Ident(s) | Token::QuotedIdent(s) => s,
6302 other => {
6303 return Err(self.err(alloc::format!(
6304 "expected column name after OWNED BY {first}., got {other:?}"
6305 )));
6306 }
6307 };
6308 // v7.17 dump-compat fix — pg_dump emits
6309 // OWNED BY clauses as
6310 // `schema.table.column` (three segments).
6311 // If a third `.<ident>` follows, treat the
6312 // first ident as schema (drop it; SPG is
6313 // single-schema) and the middle / last
6314 // pair as table.column. Otherwise it's
6315 // the two-segment form table.column.
6316 if matches!(self.peek(), Token::Dot) {
6317 self.advance();
6318 let third = match self.advance() {
6319 Token::Ident(s) | Token::QuotedIdent(s) => s,
6320 other => {
6321 return Err(self.err(alloc::format!(
6322 "expected column name after OWNED BY {first}.{second}., got {other:?}"
6323 )));
6324 }
6325 };
6326 let _ = first; // schema prefix discarded
6327 opts.owned_by = Some(SequenceOwnedBy::Column {
6328 table: second,
6329 column: third,
6330 });
6331 } else {
6332 opts.owned_by = Some(SequenceOwnedBy::Column {
6333 table: first,
6334 column: second,
6335 });
6336 }
6337 } else {
6338 return Err(self.err(alloc::format!(
6339 "expected table.column or NONE after OWNED BY, got {first:?}"
6340 )));
6341 }
6342 }
6343 _ => break,
6344 }
6345 }
6346 Ok(opts)
6347 }
6348
6349 fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6350 let neg = if matches!(self.peek(), Token::Minus) {
6351 self.advance();
6352 true
6353 } else {
6354 false
6355 };
6356 match self.peek() {
6357 Token::Integer(n) => {
6358 let v = *n;
6359 self.advance();
6360 Ok(if neg { -v } else { v })
6361 }
6362 other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6363 }
6364 }
6365
6366 /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6367 /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6368 /// clause is fully accepted and discarded — SPG always runs
6369 /// constraint checks immediately (single-writer model). The
6370 /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6371 /// in either order (per the SQL spec they're independent),
6372 /// though pg_dump always emits them in the canonical
6373 /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6374 /// Stops at the first token that isn't part of the clause.
6375 fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6376 self.consume_deferrable_clauses_timed().map(|_| ())
6377 }
6378
6379 /// v7.39 (round 288) — the same scan, but reporting what it saw:
6380 /// `(deferrable, initially_deferred)`. The clauses were parsed and
6381 /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6382 /// NOT DEFERRABLE and a circular-FK migration could not load.
6383 fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6384 let mut deferrable = false;
6385 let mut initially_deferred = false;
6386 loop {
6387 // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6388 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6389 self.advance();
6390 deferrable = true;
6391 if self.consume_optional_initially_clause()? {
6392 initially_deferred = true;
6393 }
6394 continue;
6395 }
6396 // `NOT DEFERRABLE` — already worked pre-3.1.
6397 if matches!(self.peek(), Token::Not) {
6398 let look = self.tokens.get(self.pos + 1);
6399 if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6400 self.advance(); // NOT
6401 self.advance(); // DEFERRABLE
6402 deferrable = false;
6403 initially_deferred = false;
6404 let _ = self.consume_optional_initially_clause()?;
6405 continue;
6406 }
6407 break;
6408 }
6409 // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6410 // accepts this without a leading [NOT] DEFERRABLE
6411 // (the timing keyword alone). pg_dump occasionally
6412 // emits it on FK constraints that inherit timing.
6413 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6414 if self.consume_optional_initially_clause()? {
6415 initially_deferred = true;
6416 // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6417 deferrable = true;
6418 }
6419 continue;
6420 }
6421 break;
6422 }
6423 Ok((deferrable, initially_deferred))
6424 }
6425
6426 /// Helper for [`consume_optional_deferrable_clauses`]. When the
6427 /// next token is `INITIALLY`, consume it plus the required
6428 /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6429 /// Returns true when the timing seen was `DEFERRED`.
6430 fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6431 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6432 return Ok(false);
6433 }
6434 self.advance(); // INITIALLY
6435 match self.advance() {
6436 Token::Ident(s)
6437 if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6438 {
6439 Ok(s.eq_ignore_ascii_case("deferred"))
6440 }
6441 other => Err(self.err(alloc::format!(
6442 "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6443 ))),
6444 }
6445 }
6446
6447 /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6448 /// in its entirety so the parser returns Empty without
6449 /// touching the runtime. The CREATE+PROCEDURE keywords are
6450 /// already consumed; this swallows everything from the
6451 /// procedure name through the matching `END`, including
6452 /// nested `BEGIN`/`END` blocks, internal `;` terminators
6453 /// (DELIMITER `//` makes the script splitter forward the
6454 /// whole block as one statement), `@var` session-variable
6455 /// references, and the trailing terminator.
6456 ///
6457 /// Tracks nesting depth so:
6458 /// BEGIN
6459 /// IF cond THEN
6460 /// BEGIN ... END;
6461 /// END IF;
6462 /// END
6463 /// terminates at the outer END.
6464 fn consume_mysql_routine_body(&mut self) {
6465 // Outer skeleton: name, (...), optional clauses, BEGIN
6466 // <body> END [;]. Scan for the first BEGIN — anything
6467 // before it is signature decoration we don't care about.
6468 // Once inside BEGIN, count up on BEGIN, down on END.
6469 let mut depth: i32 = 0;
6470 let mut started = false;
6471 loop {
6472 match self.peek().clone() {
6473 Token::Begin => {
6474 self.advance();
6475 depth += 1;
6476 started = true;
6477 }
6478 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6479 self.advance();
6480 if started {
6481 depth -= 1;
6482 if depth <= 0 {
6483 // Optional trailing ident (`END IF`,
6484 // `END LOOP`, `END WHILE`, `END CASE`,
6485 // `END label_name`) — eat the next
6486 // ident if present so we don't
6487 // mistake `END IF;` for the outer
6488 // close.
6489 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6490 // If the next token is one of the
6491 // PL/SQL block-closer keywords,
6492 // the END belongs to an inner
6493 // block; bump depth back up.
6494 let is_inner_close = matches!(
6495 self.peek(),
6496 Token::Ident(s) | Token::QuotedIdent(s)
6497 if matches!(
6498 s.to_ascii_lowercase().as_str(),
6499 "if" | "loop" | "while" | "case" | "repeat"
6500 )
6501 );
6502 if is_inner_close {
6503 self.advance();
6504 depth += 1;
6505 continue;
6506 }
6507 }
6508 // Eat optional trailing `;`.
6509 if matches!(self.peek(), Token::Semicolon) {
6510 self.advance();
6511 }
6512 return;
6513 }
6514 }
6515 }
6516 Token::Eof => return,
6517 _ => {
6518 self.advance();
6519 }
6520 }
6521 }
6522 }
6523
6524 /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6525 /// that appear between `CREATE` and `VIEW` in mysqldump output:
6526 ///
6527 /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6528 /// * `DEFINER = <user>` (user may be a quoted string, a bare
6529 /// ident, or `ident @ ident-or-quoted-string` host form)
6530 /// * `SQL SECURITY {DEFINER|INVOKER}`
6531 ///
6532 /// Each clause may appear at most once but in any order.
6533 /// The hints are pure planner / permission metadata that
6534 /// SPG's view-rewrite engine handles uniformly; we accept
6535 /// and discard. Returns `Ok(())` once a non-clause token is
6536 /// peeked (the caller then checks for the `VIEW` keyword).
6537 fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6538 loop {
6539 match self.peek().clone() {
6540 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6541 self.advance(); // ALGORITHM
6542 // Optional `=`. MySQL spec requires it but be
6543 // generous.
6544 if matches!(self.peek(), Token::Eq) {
6545 self.advance();
6546 }
6547 // UNDEFINED / MERGE / TEMPTABLE — accept any
6548 // bare ident; unknown values still parse so
6549 // future MySQL versions don't break.
6550 if matches!(
6551 self.peek(),
6552 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6553 ) {
6554 self.advance();
6555 }
6556 }
6557 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6558 self.advance(); // DEFINER
6559 if matches!(self.peek(), Token::Eq) {
6560 self.advance();
6561 }
6562 // User: quoted string, ident, OR ident @ host
6563 // (host may itself be quoted or bare).
6564 match self.peek().clone() {
6565 Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6566 self.advance();
6567 // Optional `@host`.
6568 if matches!(self.peek(), Token::At) {
6569 self.advance();
6570 if matches!(
6571 self.peek(),
6572 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6573 ) {
6574 self.advance();
6575 }
6576 }
6577 }
6578 _ => {}
6579 }
6580 }
6581 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6582 // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6583 // when followed by SECURITY — the dispatcher must
6584 // not consume a bare `SQL` token (it's not a
6585 // legal CREATE prefix on its own).
6586 let save = self.pos;
6587 self.advance(); // SQL
6588 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6589 if s2.eq_ignore_ascii_case("security"))
6590 {
6591 self.advance(); // SECURITY
6592 // DEFINER / INVOKER trailing ident.
6593 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6594 self.advance();
6595 }
6596 } else {
6597 // Not a SQL SECURITY clause — roll back and
6598 // bail; the caller will error out cleanly.
6599 self.pos = save;
6600 return Ok(());
6601 }
6602 }
6603 _ => return Ok(()),
6604 }
6605 }
6606 }
6607
6608 fn parse_if_not_exists(&mut self) -> bool {
6609 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6610 {
6611 let save = self.pos;
6612 self.advance();
6613 if matches!(self.peek(), Token::Not) {
6614 self.advance();
6615 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6616 {
6617 self.advance();
6618 return true;
6619 }
6620 }
6621 self.pos = save;
6622 }
6623 false
6624 }
6625
6626 fn parse_if_exists(&mut self) -> bool {
6627 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6628 {
6629 let save = self.pos;
6630 self.advance();
6631 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6632 {
6633 self.advance();
6634 return true;
6635 }
6636 self.pos = save;
6637 }
6638 false
6639 }
6640
6641 /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6642 /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6643 /// been consumed.
6644 fn parse_create_trigger_after_keyword(
6645 &mut self,
6646 or_replace: bool,
6647 ) -> Result<Statement, ParseError> {
6648 let name = self.expect_ident_like()?;
6649 let timing = {
6650 let ident = self.expect_ident_like()?;
6651 if ident.eq_ignore_ascii_case("before") {
6652 TriggerTiming::Before
6653 } else if ident.eq_ignore_ascii_case("after") {
6654 TriggerTiming::After
6655 } else if ident.eq_ignore_ascii_case("instead") {
6656 let next = self.expect_ident_like()?;
6657 if !next.eq_ignore_ascii_case("of") {
6658 return Err(self.err(alloc::format!(
6659 "expected OF after INSTEAD in trigger timing, got {next:?}"
6660 )));
6661 }
6662 TriggerTiming::InsteadOf
6663 } else {
6664 return Err(self.err(alloc::format!(
6665 "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6666 )));
6667 }
6668 };
6669 // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6670 // OR is a reserved keyword token (Token::Or), not an Ident.
6671 // v7.13.0 — after an UPDATE event we may optionally see
6672 // `OF col, col, …` (mailrs round-5 G7). Columns are
6673 // captured into `update_columns` once across the whole
6674 // events list; multiple `UPDATE OF` clauses are rejected.
6675 let mut events: Vec<TriggerEvent> = Vec::new();
6676 let mut update_columns: Vec<String> = Vec::new();
6677 let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6678 events.push(first_ev);
6679 if !first_cols.is_empty() {
6680 update_columns = first_cols;
6681 }
6682 while matches!(self.peek(), Token::Or) {
6683 self.advance();
6684 let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6685 events.push(ev);
6686 if !cols.is_empty() {
6687 if !update_columns.is_empty() {
6688 return Err(
6689 self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6690 );
6691 }
6692 update_columns = cols;
6693 }
6694 }
6695 // ON <table>
6696 let tok = self.peek();
6697 let Token::On = tok else {
6698 return Err(self.err(alloc::format!(
6699 "expected ON after trigger events, got {tok:?}"
6700 )));
6701 };
6702 self.advance();
6703 let table = self.expect_ident_like()?;
6704 // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6705 // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6706 // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6707 // the trigger as a plain AFTER trigger (correct for every non-deferred
6708 // use; deferral timing is not yet honoured).
6709 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6710 if s.eq_ignore_ascii_case("from"))
6711 {
6712 self.advance();
6713 let _reftable = self.expect_ident_like()?;
6714 }
6715 self.consume_optional_deferrable_clauses()?;
6716 // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6717 // keyword (Token::For); EACH / ROW / STATEMENT are bare
6718 // idents.
6719 if !matches!(self.peek(), Token::For) {
6720 return Err(self.err(alloc::format!(
6721 "expected FOR EACH ROW / STATEMENT, got {:?}",
6722 self.peek()
6723 )));
6724 }
6725 self.advance();
6726 let for_each = {
6727 let e = self.expect_ident_like()?;
6728 if !e.eq_ignore_ascii_case("each") {
6729 return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6730 }
6731 let unit = self.expect_ident_like()?;
6732 if unit.eq_ignore_ascii_case("row") {
6733 TriggerForEach::Row
6734 } else if unit.eq_ignore_ascii_case("statement") {
6735 TriggerForEach::Statement
6736 } else {
6737 return Err(self.err(alloc::format!(
6738 "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6739 )));
6740 }
6741 };
6742 // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6743 let when_condition = if matches!(self.peek(),
6744 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6745 {
6746 self.advance();
6747 Some(self.parse_paren_expr("WHEN")?)
6748 } else {
6749 None
6750 };
6751 // EXECUTE FUNCTION/PROCEDURE name(...)
6752 let exec = self.expect_ident_like()?;
6753 if !exec.eq_ignore_ascii_case("execute") {
6754 return Err(self.err(alloc::format!(
6755 "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6756 )));
6757 }
6758 let fn_or_proc = self.expect_ident_like()?;
6759 if !(fn_or_proc.eq_ignore_ascii_case("function")
6760 || fn_or_proc.eq_ignore_ascii_case("procedure"))
6761 {
6762 return Err(self.err(alloc::format!(
6763 "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6764 )));
6765 }
6766 let function = self.expect_ident_like()?;
6767 // Optional empty arg list `()`.
6768 if matches!(self.peek(), Token::LParen) {
6769 self.advance();
6770 if !matches!(self.peek(), Token::RParen) {
6771 return Err(self.err(alloc::format!(
6772 "v7.12.4 trigger function calls take no args; got {:?}",
6773 self.peek()
6774 )));
6775 }
6776 self.advance();
6777 }
6778 Ok(Statement::CreateTrigger(CreateTriggerStatement {
6779 name,
6780 or_replace,
6781 timing,
6782 events,
6783 table,
6784 for_each,
6785 function,
6786 update_columns,
6787 when_condition,
6788 }))
6789 }
6790
6791 /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
6792 /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
6793 fn parse_create_rule_after_keyword(
6794 &mut self,
6795 or_replace: bool,
6796 ) -> Result<Statement, ParseError> {
6797 let name = self.expect_ident_like()?;
6798 if !matches!(self.peek(), Token::As) {
6799 return Err(self.err(alloc::format!(
6800 "expected AS in CREATE RULE, got {:?}",
6801 self.peek()
6802 )));
6803 }
6804 self.advance();
6805 if !matches!(self.peek(), Token::On) {
6806 return Err(self.err(alloc::format!(
6807 "expected ON in CREATE RULE, got {:?}",
6808 self.peek()
6809 )));
6810 }
6811 self.advance();
6812 let event = self.parse_rule_event()?;
6813 if !matches!(self.peek(), Token::To)
6814 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
6815 {
6816 return Err(self.err(alloc::format!(
6817 "expected TO after rule event, got {:?}",
6818 self.peek()
6819 )));
6820 }
6821 self.advance();
6822 let table = self.expect_ident_like()?;
6823 // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
6824 let when_condition = if matches!(self.peek(), Token::Where) {
6825 self.advance();
6826 Some(self.parse_expr(0)?)
6827 } else {
6828 None
6829 };
6830 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
6831 {
6832 return Err(self.err(alloc::format!(
6833 "expected DO in CREATE RULE, got {:?}",
6834 self.peek()
6835 )));
6836 }
6837 self.advance();
6838 // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
6839 let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
6840 {
6841 self.advance();
6842 true
6843 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
6844 self.advance();
6845 false
6846 } else {
6847 false
6848 };
6849 // `NOTHING` | `( cmd; … )` | `cmd`.
6850 let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
6851 {
6852 self.advance();
6853 Vec::new()
6854 } else if matches!(self.peek(), Token::LParen) {
6855 self.advance();
6856 let mut cmds = Vec::new();
6857 loop {
6858 cmds.push(self.parse_one_statement()?);
6859 if matches!(self.peek(), Token::Semicolon) {
6860 self.advance();
6861 if matches!(self.peek(), Token::RParen) {
6862 break;
6863 }
6864 continue;
6865 }
6866 break;
6867 }
6868 if !matches!(self.peek(), Token::RParen) {
6869 return Err(self.err(alloc::format!(
6870 "expected ) closing the CREATE RULE command list, got {:?}",
6871 self.peek()
6872 )));
6873 }
6874 self.advance();
6875 cmds
6876 } else {
6877 alloc::vec![self.parse_one_statement()?]
6878 };
6879 Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
6880 name,
6881 or_replace,
6882 event,
6883 table,
6884 instead,
6885 when_condition,
6886 commands,
6887 }))
6888 }
6889
6890 /// v7.39 (round 139) — a rule event keyword → uppercase string.
6891 fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
6892 if matches!(self.peek(), Token::Insert) {
6893 self.advance();
6894 return Ok(alloc::string::String::from("INSERT"));
6895 }
6896 if matches!(self.peek(), Token::Select) {
6897 self.advance();
6898 return Ok(alloc::string::String::from("SELECT"));
6899 }
6900 match self.peek() {
6901 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
6902 self.advance();
6903 Ok(alloc::string::String::from("UPDATE"))
6904 }
6905 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
6906 self.advance();
6907 Ok(alloc::string::String::from("DELETE"))
6908 }
6909 other => Err(self.err(alloc::format!(
6910 "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
6911 ))),
6912 }
6913 }
6914
6915 /// v7.13.0 — parse one trigger event, then optionally consume
6916 /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
6917 /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
6918 fn parse_trigger_event_with_optional_of(
6919 &mut self,
6920 ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
6921 let ev = self.parse_trigger_event()?;
6922 if !matches!(ev, TriggerEvent::Update) {
6923 return Ok((ev, Vec::new()));
6924 }
6925 // `OF` is a bare ident.
6926 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
6927 return Ok((ev, Vec::new()));
6928 }
6929 self.advance(); // OF
6930 let mut cols: Vec<String> = Vec::new();
6931 loop {
6932 cols.push(self.expect_ident_like()?);
6933 if matches!(self.peek(), Token::Comma) {
6934 self.advance();
6935 continue;
6936 }
6937 break;
6938 }
6939 if cols.is_empty() {
6940 return Err(
6941 self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
6942 );
6943 }
6944 Ok((ev, cols))
6945 }
6946
6947 /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
6948 /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
6949 /// before `BEGIN`, and IF / RAISE / embedded SQL statements
6950 /// inside the body.
6951 /// Called by [`parse_plpgsql_body`] after the body's tokens
6952 /// have been lexed into this temporary parser.
6953 pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
6954 // v7.12.6 — optional DECLARE prelude.
6955 let declarations = if matches!(
6956 self.peek(),
6957 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
6958 ) {
6959 self.advance();
6960 self.parse_plpgsql_declare_block()?
6961 } else {
6962 Vec::new()
6963 };
6964 // BEGIN keyword (PL/pgSQL — distinct from the SQL
6965 // `BEGIN` transaction-start, but we can reuse the
6966 // reserved Token::Begin since the body is a separate
6967 // lex/parse context).
6968 if !matches!(self.peek(), Token::Begin) {
6969 return Err(self.err(alloc::format!(
6970 "expected BEGIN at start of plpgsql block, got {:?}",
6971 self.peek()
6972 )));
6973 }
6974 self.advance();
6975 let statements = self.parse_plpgsql_stmt_list_until_end()?;
6976 // v7.37.20 (20.10) — optional EXCEPTION clause between the
6977 // body's last statement and the trailing END. When present
6978 // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
6979 // arms terminated by END.
6980 let exception_handlers = if matches!(
6981 self.peek(),
6982 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
6983 ) {
6984 self.advance();
6985 self.parse_plpgsql_exception_handlers()?
6986 } else {
6987 Vec::new()
6988 };
6989 Ok(PlPgSqlBlock {
6990 declarations,
6991 statements,
6992 exception_handlers,
6993 })
6994 }
6995
6996 /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
6997 /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
6998 fn parse_plpgsql_exception_handlers(
6999 &mut self,
7000 ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
7001 let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
7002 loop {
7003 // Stop at END — the block-level trailing END LOOP / END;
7004 // is handled by the caller.
7005 if matches!(
7006 self.peek(),
7007 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
7008 ) {
7009 return Ok(out);
7010 }
7011 // WHEN <cond> [OR <cond>]* THEN <body>
7012 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7013 {
7014 return Err(self.err(alloc::format!(
7015 "expected WHEN or END inside EXCEPTION clause, got {:?}",
7016 self.peek()
7017 )));
7018 }
7019 self.advance();
7020 let mut conditions: Vec<String> = Vec::new();
7021 conditions.push(self.expect_ident_like()?);
7022 while matches!(self.peek(), Token::Or) {
7023 self.advance();
7024 conditions.push(self.expect_ident_like()?);
7025 }
7026 let then_kw = self.expect_ident_like()?;
7027 if !then_kw.eq_ignore_ascii_case("then") {
7028 return Err(self.err(alloc::format!(
7029 "expected THEN after WHEN condition list, got {then_kw:?}"
7030 )));
7031 }
7032 let body = self.parse_plpgsql_stmt_list_until_end()?;
7033 out.push(crate::ast::ExceptionHandler { conditions, body });
7034 }
7035 }
7036
7037 /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
7038 /// prelude. Caller has already consumed `DECLARE`. We stop
7039 /// reading entries when we hit `BEGIN`.
7040 fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
7041 let mut out: Vec<PlPgSqlDeclare> = Vec::new();
7042 loop {
7043 if matches!(self.peek(), Token::Begin) {
7044 return Ok(out);
7045 }
7046 let name = self.expect_ident_like()?;
7047 // v7.37.20 (20.7) — type inference: if the next token is
7048 // `:=` or `=` (no explicit type), infer from the default
7049 // expression. Otherwise the ident that follows is the
7050 // declared type.
7051 //
7052 // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
7053 // (PG-standard). SPG parse-accepts and treats identically
7054 // to inference — the eventual runtime value determines
7055 // the local's type, which is faithful to how SPG handles
7056 // untyped locals today (see 20.7). Full compile-time
7057 // catalog lookup queues with v7.40 PL/pgSQL epic.
7058 let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
7059 // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
7060 // downstream declaration walker to type the local by
7061 // the runtime type of the default expression.
7062 FunctionArgType::Raw("_infer_".into())
7063 } else {
7064 let ty_token = self.expect_ident_like()?;
7065 // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
7066 // consume optional `.<ident>` qualifier + `%<KW>`
7067 // suffix. Both qualifier and suffix map to _infer_.
7068 if matches!(self.peek(), Token::Dot) {
7069 self.advance();
7070 let _ = self.expect_ident_like()?;
7071 }
7072 if matches!(self.peek(), Token::Percent) {
7073 self.advance();
7074 // Consume the trailing TYPE / ROWTYPE ident.
7075 let _ = self.expect_ident_like()?;
7076 FunctionArgType::Raw("_infer_".into())
7077 } else {
7078 match map_type_ident_to_column_type_name(&ty_token) {
7079 Some(t) => FunctionArgType::Typed(t),
7080 None => FunctionArgType::Raw(ty_token),
7081 }
7082 }
7083 };
7084 let default = match self.peek() {
7085 Token::ColonEq => {
7086 self.advance();
7087 Some(self.parse_expr(0)?)
7088 }
7089 Token::Eq => {
7090 // PL/pgSQL also accepts `=` for the
7091 // DECLARE default (PG treats them the same
7092 // in this position).
7093 self.advance();
7094 Some(self.parse_expr(0)?)
7095 }
7096 _ => None,
7097 };
7098 // Mandatory `;` between declarations.
7099 if !matches!(self.peek(), Token::Semicolon) {
7100 return Err(self.err(alloc::format!(
7101 "expected ; after DECLARE entry for {name:?}, got {:?}",
7102 self.peek()
7103 )));
7104 }
7105 self.advance();
7106 out.push(PlPgSqlDeclare { name, ty, default });
7107 }
7108 }
7109
7110 /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
7111 /// the terminating `END;` (or `END IF;` etc — handled by the
7112 /// per-construct sub-parsers). Used by both the outer block
7113 /// and the IF/ELSE branch bodies.
7114 fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
7115 let mut statements: Vec<PlPgSqlStmt> = Vec::new();
7116 loop {
7117 // Allow trailing semicolons + END.
7118 while matches!(self.peek(), Token::Semicolon) {
7119 self.advance();
7120 }
7121 // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7122 if matches!(
7123 self.peek(),
7124 Token::Ident(s) | Token::QuotedIdent(s)
7125 if s.eq_ignore_ascii_case("end")
7126 || s.eq_ignore_ascii_case("else")
7127 || s.eq_ignore_ascii_case("elsif")
7128 || s.eq_ignore_ascii_case("elseif")
7129 || s.eq_ignore_ascii_case("exception")
7130 || s.eq_ignore_ascii_case("when")
7131 ) {
7132 return Ok(statements);
7133 }
7134 // Otherwise: one statement, then expect `;` or
7135 // a block-terminator keyword.
7136 let stmt = self.parse_plpgsql_stmt()?;
7137 statements.push(stmt);
7138 match self.peek() {
7139 Token::Semicolon => {
7140 self.advance();
7141 }
7142 Token::Ident(s) | Token::QuotedIdent(s)
7143 if s.eq_ignore_ascii_case("end")
7144 || s.eq_ignore_ascii_case("else")
7145 || s.eq_ignore_ascii_case("elsif")
7146 || s.eq_ignore_ascii_case("elseif")
7147 || s.eq_ignore_ascii_case("exception")
7148 || s.eq_ignore_ascii_case("when") =>
7149 {
7150 // Final statement of the block without `;`.
7151 }
7152 other => {
7153 return Err(self.err(alloc::format!(
7154 "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7155 )));
7156 }
7157 }
7158 }
7159 }
7160
7161 fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7162 // RETURN keyword?
7163 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7164 {
7165 self.advance();
7166 return self.parse_plpgsql_return();
7167 }
7168 // v7.12.6 — IF block.
7169 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7170 {
7171 self.advance();
7172 return self.parse_plpgsql_if();
7173 }
7174 // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7175 // Detected by peeking that token pos+3 is Ident("execute").
7176 if matches!(self.peek(), Token::For)
7177 && matches!(
7178 self.tokens.get(self.pos + 1),
7179 Some(Token::Ident(_) | Token::QuotedIdent(_))
7180 )
7181 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7182 && matches!(
7183 self.tokens.get(self.pos + 3),
7184 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7185 )
7186 {
7187 self.advance(); // FOR
7188 let var = self.expect_ident_like()?;
7189 self.advance(); // IN
7190 self.advance(); // EXECUTE
7191 // Prescan for LOOP at paren depth 0 so parse_expr stops
7192 // before the LOOP keyword (same trick as the bare-SELECT
7193 // ForQuery arm).
7194 let mut depth: i32 = 0;
7195 let mut loop_pos: Option<usize> = None;
7196 let mut scan = self.pos;
7197 while scan < self.tokens.len() {
7198 match self.tokens.get(scan) {
7199 Some(Token::LParen) => depth += 1,
7200 Some(Token::RParen) => depth -= 1,
7201 Some(Token::Ident(s) | Token::QuotedIdent(s))
7202 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7203 {
7204 loop_pos = Some(scan);
7205 break;
7206 }
7207 _ => {}
7208 }
7209 scan += 1;
7210 }
7211 let loop_pos = loop_pos.ok_or_else(|| {
7212 self.err(alloc::format!(
7213 "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7214 ))
7215 })?;
7216 let saved_loop = self.tokens[loop_pos].clone();
7217 self.tokens[loop_pos] = Token::Semicolon;
7218 let expr_result = self.parse_expr(0);
7219 self.tokens[loop_pos] = saved_loop;
7220 let sql_expr = expr_result?;
7221 let loop_kw = self.expect_ident_like()?;
7222 if !loop_kw.eq_ignore_ascii_case("loop") {
7223 return Err(self.err(alloc::format!(
7224 "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7225 )));
7226 }
7227 let body = self.parse_plpgsql_stmt_list_until_end()?;
7228 let end_kw = self.expect_ident_like()?;
7229 if !end_kw.eq_ignore_ascii_case("end") {
7230 return Err(self.err(alloc::format!(
7231 "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7232 )));
7233 }
7234 let loop_kw2 = self.expect_ident_like()?;
7235 if !loop_kw2.eq_ignore_ascii_case("loop") {
7236 return Err(self.err(alloc::format!(
7237 "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7238 )));
7239 }
7240 return Ok(PlPgSqlStmt::ForExecute {
7241 var,
7242 sql_expr,
7243 body,
7244 });
7245 }
7246 // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7247 //
7248 // Two syntactic forms:
7249 // FOR var IN SELECT ... ORDER BY ... LOOP ...
7250 // FOR var IN (SELECT ...) LOOP ...
7251 //
7252 // Bare-SELECT form: to keep parse_select_stmt from swallowing
7253 // the trailing `LOOP` keyword as a table alias, we prescan
7254 // forward to find LOOP at paren depth 0, splice a fake
7255 // Semicolon at that position (so SELECT parses cleanly),
7256 // then re-splice LOOP back in.
7257 //
7258 // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7259 // LOOP directly — no scan required.
7260 if matches!(self.peek(), Token::For)
7261 && matches!(
7262 self.tokens.get(self.pos + 1),
7263 Some(Token::Ident(_) | Token::QuotedIdent(_))
7264 )
7265 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7266 && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7267 || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7268 {
7269 self.advance(); // FOR
7270 let var = self.expect_ident_like()?;
7271 // IN
7272 self.advance();
7273 let query = if matches!(self.peek(), Token::LParen) {
7274 // Paren-wrapped SELECT.
7275 self.advance();
7276 let inner = self.parse_select_stmt()?;
7277 let Statement::Select(q) = inner else {
7278 return Err(self.err(alloc::format!(
7279 "expected SELECT inside (…), got {:?}",
7280 self.peek()
7281 )));
7282 };
7283 if !matches!(self.peek(), Token::RParen) {
7284 return Err(self.err(alloc::format!(
7285 "expected ')' after FOR-IN-SELECT body, got {:?}",
7286 self.peek()
7287 )));
7288 }
7289 self.advance();
7290 q
7291 } else {
7292 // Bare SELECT: prescan to find the LOOP boundary.
7293 let mut depth: i32 = 0;
7294 let mut loop_pos: Option<usize> = None;
7295 let mut scan = self.pos;
7296 while scan < self.tokens.len() {
7297 match self.tokens.get(scan) {
7298 Some(Token::LParen) => depth += 1,
7299 Some(Token::RParen) => depth -= 1,
7300 Some(Token::Ident(s) | Token::QuotedIdent(s))
7301 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7302 {
7303 loop_pos = Some(scan);
7304 break;
7305 }
7306 _ => {}
7307 }
7308 scan += 1;
7309 }
7310 let loop_pos = loop_pos.ok_or_else(|| {
7311 self.err(alloc::format!(
7312 "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7313 ))
7314 })?;
7315 // Swap the LOOP token with a synthetic Semicolon so
7316 // parse_select_stmt stops there, then restore afterward.
7317 let saved_loop = self.tokens[loop_pos].clone();
7318 self.tokens[loop_pos] = Token::Semicolon;
7319 let parse_result = self.parse_select_stmt();
7320 self.tokens[loop_pos] = saved_loop;
7321 let inner = parse_result?;
7322 let Statement::Select(q) = inner else {
7323 return Err(self.err(alloc::format!(
7324 "expected SELECT after FOR <var> IN, got {:?}",
7325 self.peek()
7326 )));
7327 };
7328 q
7329 };
7330 let loop_kw = self.expect_ident_like()?;
7331 if !loop_kw.eq_ignore_ascii_case("loop") {
7332 return Err(self.err(alloc::format!(
7333 "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7334 )));
7335 }
7336 let body = self.parse_plpgsql_stmt_list_until_end()?;
7337 let end_kw = self.expect_ident_like()?;
7338 if !end_kw.eq_ignore_ascii_case("end") {
7339 return Err(self.err(alloc::format!(
7340 "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7341 )));
7342 }
7343 let loop_kw2 = self.expect_ident_like()?;
7344 if !loop_kw2.eq_ignore_ascii_case("loop") {
7345 return Err(self.err(alloc::format!(
7346 "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7347 )));
7348 }
7349 return Ok(PlPgSqlStmt::ForQuery {
7350 var,
7351 query: Box::new(query),
7352 body,
7353 });
7354 }
7355 // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7356 // FOR is a reserved keyword token (Token::For).
7357 if matches!(self.peek(), Token::For)
7358 && matches!(
7359 self.tokens.get(self.pos + 1),
7360 Some(Token::Ident(_) | Token::QuotedIdent(_))
7361 )
7362 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7363 {
7364 self.advance(); // FOR
7365 let var = self.expect_ident_like()?;
7366 if !matches!(self.peek(), Token::In) {
7367 return Err(self.err(alloc::format!(
7368 "expected IN after FOR <var>, got {:?}",
7369 self.peek()
7370 )));
7371 }
7372 self.advance();
7373 let reverse = matches!(
7374 self.peek(),
7375 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7376 );
7377 if reverse {
7378 self.advance();
7379 }
7380 let start = self.parse_expr(0)?;
7381 if !matches!(self.peek(), Token::DotDot) {
7382 return Err(self.err(alloc::format!(
7383 "expected '..' between FOR loop bounds, got {:?}",
7384 self.peek()
7385 )));
7386 }
7387 self.advance();
7388 let end = self.parse_expr(0)?;
7389 let loop_kw = self.expect_ident_like()?;
7390 if !loop_kw.eq_ignore_ascii_case("loop") {
7391 return Err(self.err(alloc::format!(
7392 "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7393 )));
7394 }
7395 let body = self.parse_plpgsql_stmt_list_until_end()?;
7396 let end_kw = self.expect_ident_like()?;
7397 if !end_kw.eq_ignore_ascii_case("end") {
7398 return Err(self.err(alloc::format!(
7399 "expected END LOOP after FOR body, got {end_kw:?}"
7400 )));
7401 }
7402 let loop_kw2 = self.expect_ident_like()?;
7403 if !loop_kw2.eq_ignore_ascii_case("loop") {
7404 return Err(self.err(alloc::format!(
7405 "expected END LOOP after FOR body, got END {loop_kw2:?}"
7406 )));
7407 }
7408 return Ok(PlPgSqlStmt::ForRange {
7409 var,
7410 start,
7411 end,
7412 reverse,
7413 body,
7414 });
7415 }
7416 // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7417 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7418 {
7419 self.advance();
7420 let body = self.parse_plpgsql_stmt_list_until_end()?;
7421 let end_kw = self.expect_ident_like()?;
7422 if !end_kw.eq_ignore_ascii_case("end") {
7423 return Err(self.err(alloc::format!(
7424 "expected END LOOP after LOOP body, got {end_kw:?}"
7425 )));
7426 }
7427 let loop_kw = self.expect_ident_like()?;
7428 if !loop_kw.eq_ignore_ascii_case("loop") {
7429 return Err(self.err(alloc::format!(
7430 "expected END LOOP after LOOP body, got END {loop_kw:?}"
7431 )));
7432 }
7433 return Ok(PlPgSqlStmt::Loop { body });
7434 }
7435 // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7436 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7437 {
7438 self.advance();
7439 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7440 {
7441 self.advance();
7442 Some(self.parse_expr(0)?)
7443 } else {
7444 None
7445 };
7446 return Ok(PlPgSqlStmt::Exit { when });
7447 }
7448 // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7449 // already-parsed Statement or a runtime-computed SQL string.
7450 // The disambiguator vs the extended-query-protocol `EXECUTE
7451 // <stmt_name>` (which is a top-level Statement, not a
7452 // plpgsql line) is that inside a DO block / trigger body the
7453 // EXECUTE keyword ALWAYS refers to dynamic SQL.
7454 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7455 {
7456 self.advance();
7457 let sql = self.parse_expr(0)?;
7458 return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7459 }
7460 // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7461 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7462 {
7463 self.advance();
7464 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7465 {
7466 self.advance();
7467 Some(self.parse_expr(0)?)
7468 } else {
7469 None
7470 };
7471 return Ok(PlPgSqlStmt::Continue { when });
7472 }
7473 // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7474 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7475 {
7476 self.advance();
7477 let condition = self.parse_expr(0)?;
7478 let loop_kw = self.expect_ident_like()?;
7479 if !loop_kw.eq_ignore_ascii_case("loop") {
7480 return Err(self.err(alloc::format!(
7481 "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7482 )));
7483 }
7484 let body = self.parse_plpgsql_stmt_list_until_end()?;
7485 // Expect END LOOP.
7486 let end_kw = self.expect_ident_like()?;
7487 if !end_kw.eq_ignore_ascii_case("end") {
7488 return Err(self.err(alloc::format!(
7489 "expected END LOOP after WHILE body, got {end_kw:?}"
7490 )));
7491 }
7492 let loop_kw2 = self.expect_ident_like()?;
7493 if !loop_kw2.eq_ignore_ascii_case("loop") {
7494 return Err(self.err(alloc::format!(
7495 "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7496 )));
7497 }
7498 return Ok(PlPgSqlStmt::While { condition, body });
7499 }
7500 // v7.12.6 — RAISE.
7501 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7502 {
7503 self.advance();
7504 return self.parse_plpgsql_raise();
7505 }
7506 // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7507 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7508 {
7509 self.advance();
7510 let condition = self.parse_expr(0)?;
7511 let message = if matches!(self.peek(), Token::Comma) {
7512 self.advance();
7513 Some(self.parse_expr(0)?)
7514 } else {
7515 None
7516 };
7517 return Ok(PlPgSqlStmt::Assert { condition, message });
7518 }
7519 // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7520 // "PERFORM is equivalent to SELECT but discards the
7521 // result." Side effects (function calls, RAISE inside
7522 // SQL functions, etc.) still execute. We desugar to
7523 // `SELECT <body>` and wrap in EmbeddedSql so the engine's
7524 // existing embedded-statement path handles execution +
7525 // result-discard cleanly. The result is naturally
7526 // discarded because EmbeddedSql doesn't propagate row
7527 // sets back to the plpgsql interpreter.
7528 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7529 {
7530 self.advance();
7531 // Splice a synthetic Token::Select into the stream at
7532 // the current position so parse_select_stmt parses the
7533 // remainder as a normal SELECT body. Token-stream
7534 // surgery mirrors the try_parse_plpgsql_select_into
7535 // pattern used for SELECT … INTO desugaring.
7536 self.tokens.insert(self.pos, Token::Select);
7537 let select = self.parse_select_stmt()?;
7538 let Statement::Select(s) = select else {
7539 return Err(self.err(alloc::format!(
7540 "expected SELECT body after PERFORM, got {:?}",
7541 self.peek()
7542 )));
7543 };
7544 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7545 }
7546 // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7547 // plpgsql-specific shape (mailrs round-10 migrate-042).
7548 // PG's SELECT INTO at top-level SQL would CREATE a new
7549 // table; inside plpgsql it ASSIGNS the query result to
7550 // a local variable. We detect the INTO at paren-depth
7551 // 0 between SELECT and the statement boundary; if
7552 // found, split the token stream into "pre-INTO
7553 // projection" + "var" + "post-INTO FROM/WHERE…" and
7554 // rebuild as a SelectInto with a regular SELECT body
7555 // (no INTO clause).
7556 if matches!(self.peek(), Token::Select)
7557 && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7558 {
7559 return Ok(PlPgSqlStmt::SelectInto {
7560 var: var_name,
7561 body: Box::new(select_body),
7562 });
7563 }
7564 // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7565 // SELECT can appear directly inside a trigger body; we
7566 // recurse into the regular Statement parser, which will
7567 // stop at the trailing `;` (which our caller then
7568 // consumes).
7569 // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7570 // also embed ALTER / CREATE / DROP statements; route
7571 // those through the same parser so the DO body parses
7572 // cleanly.
7573 if matches!(self.peek(), Token::Insert)
7574 || matches!(self.peek(), Token::Select)
7575 || matches!(self.peek(), Token::Create)
7576 || matches!(self.peek(), Token::Drop)
7577 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7578 if s.eq_ignore_ascii_case("update")
7579 || s.eq_ignore_ascii_case("delete")
7580 || s.eq_ignore_ascii_case("alter"))
7581 {
7582 let stmt = self.parse_one_statement()?;
7583 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7584 }
7585 // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7586 // followed by `:=` and an expression.
7587 let target = self.parse_plpgsql_assign_target()?;
7588 // PL/pgSQL assignment uses `:=`. The lexer represents
7589 // this as a colon followed by `=`; check both shapes.
7590 match self.peek() {
7591 Token::ColonEq => {
7592 self.advance();
7593 }
7594 Token::Colon => {
7595 self.advance();
7596 if !matches!(self.peek(), Token::Eq) {
7597 return Err(self.err(alloc::format!(
7598 "expected := after plpgsql assign target, got `:` then {:?}",
7599 self.peek()
7600 )));
7601 }
7602 self.advance();
7603 }
7604 other => {
7605 return Err(self.err(alloc::format!(
7606 "expected := after plpgsql assign target, got {other:?}"
7607 )));
7608 }
7609 }
7610 let value = self.parse_expr(0)?;
7611 Ok(PlPgSqlStmt::Assign { target, value })
7612 }
7613
7614 /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7615 /// [ELSE body] END IF`. `IF` keyword already consumed.
7616 fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7617 let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7618 let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7619 loop {
7620 // <expr> THEN
7621 let cond = self.parse_expr(0)?;
7622 let then_kw = self.expect_ident_like()?;
7623 if !then_kw.eq_ignore_ascii_case("then") {
7624 return Err(self.err(alloc::format!(
7625 "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7626 )));
7627 }
7628 let body = self.parse_plpgsql_stmt_list_until_end()?;
7629 branches.push((cond, body));
7630 // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7631 match self.peek() {
7632 Token::Ident(s) | Token::QuotedIdent(s)
7633 if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7634 {
7635 self.advance();
7636 continue;
7637 }
7638 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7639 self.advance();
7640 else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7641 break;
7642 }
7643 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7644 break;
7645 }
7646 other => {
7647 return Err(self.err(alloc::format!(
7648 "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7649 )));
7650 }
7651 }
7652 }
7653 // Expect `END IF` (the END keyword is the one we're
7654 // looking at right now).
7655 let end_kw = self.expect_ident_like()?;
7656 if !end_kw.eq_ignore_ascii_case("end") {
7657 return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7658 }
7659 let if_kw = self.expect_ident_like()?;
7660 if !if_kw.eq_ignore_ascii_case("if") {
7661 return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7662 }
7663 Ok(PlPgSqlStmt::If {
7664 branches,
7665 else_branch,
7666 })
7667 }
7668
7669 /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7670 /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7671 /// is already consumed.
7672 fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7673 let lvl_ident = self.expect_ident_like()?;
7674 let level = match lvl_ident.to_ascii_lowercase().as_str() {
7675 "notice" => RaiseLevel::Notice,
7676 "warning" => RaiseLevel::Warning,
7677 "info" => RaiseLevel::Info,
7678 "log" => RaiseLevel::Log,
7679 "debug" => RaiseLevel::Debug,
7680 "exception" => RaiseLevel::Exception,
7681 other => {
7682 return Err(self.err(alloc::format!(
7683 "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7684 )));
7685 }
7686 };
7687 // Message: required for v7.12.6. PG accepts a bare
7688 // RAISE-rethrow form (no message), reserved for future
7689 // RAISE-no-args support.
7690 let Token::String(msg) = self.peek() else {
7691 return Err(self.err(alloc::format!(
7692 "expected RAISE message string, got {:?}",
7693 self.peek()
7694 )));
7695 };
7696 let message = msg.clone();
7697 self.advance();
7698 // Optional comma-separated args (PG `%` format substitution).
7699 let mut args: Vec<Expr> = Vec::new();
7700 while matches!(self.peek(), Token::Comma) {
7701 self.advance();
7702 args.push(self.parse_expr(0)?);
7703 }
7704 Ok(PlPgSqlStmt::Raise {
7705 level,
7706 message,
7707 args,
7708 })
7709 }
7710
7711 /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7712 /// <projection> INTO <var> [FROM …]` (mailrs round-10
7713 /// migrate-042). Returns `(rebuilt_select_without_into,
7714 /// var_name)` when the pattern matches; `None` for
7715 /// regular SELECTs (those go through the embedded-SQL
7716 /// path). Token-stream surgery so the rebuilt SELECT
7717 /// parses through the regular `parse_select_stmt`.
7718 #[allow(clippy::too_many_lines)]
7719 fn try_parse_plpgsql_select_into(
7720 &mut self,
7721 ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7722 // Scan forward from `self.pos + 1` (past Token::Select)
7723 // for Token::Into at paren-depth 0, stopping at the
7724 // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7725 // end the plpgsql statement.
7726 let start = self.pos;
7727 let mut into_pos: Option<usize> = None;
7728 let mut depth: i32 = 0;
7729 let mut i = start + 1;
7730 while i < self.tokens.len() {
7731 match &self.tokens[i] {
7732 Token::LParen => depth += 1,
7733 Token::RParen => depth -= 1,
7734 Token::Semicolon if depth == 0 => break,
7735 Token::Ident(s)
7736 if depth == 0
7737 && (s.eq_ignore_ascii_case("end")
7738 || s.eq_ignore_ascii_case("else")
7739 || s.eq_ignore_ascii_case("elsif")) =>
7740 {
7741 break;
7742 }
7743 Token::Into if depth == 0 => {
7744 into_pos = Some(i);
7745 break;
7746 }
7747 _ => {}
7748 }
7749 i += 1;
7750 }
7751 let Some(into_at) = into_pos else {
7752 return Ok(None);
7753 };
7754 // The token immediately after INTO must be the target
7755 // var ident; anything else (e.g. INSERT INTO table)
7756 // ruled out by the depth-0 check above. Capture it.
7757 let var = match self.tokens.get(into_at + 1) {
7758 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7759 other => {
7760 return Err(self.err(alloc::format!(
7761 "expected variable name after SELECT … INTO, got {other:?}"
7762 )));
7763 }
7764 };
7765 // Find the end of the plpgsql SELECT INTO statement —
7766 // same boundary rules as the depth-0 scan above.
7767 let mut end = into_at + 2;
7768 let mut depth2: i32 = 0;
7769 while end < self.tokens.len() {
7770 match &self.tokens[end] {
7771 Token::LParen => depth2 += 1,
7772 Token::RParen => depth2 -= 1,
7773 Token::Semicolon if depth2 == 0 => break,
7774 Token::Ident(s)
7775 if depth2 == 0
7776 && (s.eq_ignore_ascii_case("end")
7777 || s.eq_ignore_ascii_case("else")
7778 || s.eq_ignore_ascii_case("elsif")) =>
7779 {
7780 break;
7781 }
7782 _ => {}
7783 }
7784 end += 1;
7785 }
7786 // Rebuild a token stream that represents the SELECT
7787 // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
7788 // post-var tokens up to statement end]. Run the
7789 // regular `parse_select_stmt` against it.
7790 let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
7791 for j in start..into_at {
7792 rebuilt.push(self.tokens[j].clone());
7793 }
7794 for j in (into_at + 2)..end {
7795 rebuilt.push(self.tokens[j].clone());
7796 }
7797 rebuilt.push(Token::Eof);
7798 let saved_pos = self.pos;
7799 let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
7800 self.pos = 0;
7801 // parse_select_stmt → parse_bare_select consumes Token::Select itself.
7802 if !matches!(self.peek(), Token::Select) {
7803 self.tokens = saved_tokens;
7804 self.pos = saved_pos;
7805 return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
7806 }
7807 let sel = self.parse_select_stmt();
7808 self.tokens = saved_tokens;
7809 self.pos = end;
7810 let sel = sel?;
7811 let Statement::Select(body) = sel else {
7812 return Err(self.err(alloc::format!(
7813 "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
7814 )));
7815 };
7816 Ok(Some((body, var)))
7817 }
7818
7819 fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
7820 // v7.16.1 — read the head token DIRECTLY rather than
7821 // via `expect_ident_like`. The v7.14.0 schema-qualifier
7822 // strip (`public.t` → `t`) inside `expect_ident_like`
7823 // greedily consumes any `ident . ident` pair, which
7824 // silently turned every `NEW.col := …` /
7825 // `OLD.col := …` plpgsql assignment into a Local("col")
7826 // assignment — the head "new"/"old" was eaten as if it
7827 // were a schema name and the Dot was consumed too, so
7828 // this function's own `peek() == Token::Dot` check
7829 // below never fired. Every BEFORE trigger that rewrote
7830 // a NEW cell was a silent no-op for two major releases
7831 // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
7832 // gate failures were investigated as v7.16.1 backlog.
7833 let head = match self.advance() {
7834 Token::Ident(s) | Token::QuotedIdent(s) => s,
7835 other => {
7836 return Err(self.err(alloc::format!(
7837 "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
7838 )));
7839 }
7840 };
7841 if matches!(self.peek(), Token::Dot) {
7842 self.advance();
7843 let col = self.expect_ident_like()?;
7844 if head.eq_ignore_ascii_case("new") {
7845 return Ok(AssignTarget::NewColumn(col));
7846 }
7847 if head.eq_ignore_ascii_case("old") {
7848 return Ok(AssignTarget::OldColumn(col));
7849 }
7850 return Err(self.err(alloc::format!(
7851 "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
7852 got {head:?}.<col>"
7853 )));
7854 }
7855 Ok(AssignTarget::Local(head))
7856 }
7857
7858 fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7859 // RETURN NEW / OLD / NULL — bare-ident forms.
7860 match self.peek() {
7861 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
7862 self.advance();
7863 return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
7864 }
7865 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
7866 self.advance();
7867 return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
7868 }
7869 Token::Null => {
7870 self.advance();
7871 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7872 }
7873 // Bare `RETURN;` (no value) — treated as `RETURN NULL`
7874 // per PL/pgSQL convention.
7875 Token::Semicolon => {
7876 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7877 }
7878 _ => {}
7879 }
7880 // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
7881 // EXECUTE <expr>. In a DO block context RETURN QUERY has no
7882 // caller-visible effect (blocks don't return sets), so we
7883 // desugar it identically to PERFORM: parse the SELECT (or
7884 // EXECUTE dynamic) as embedded SQL that runs for side
7885 // effects and discards the result. RETURN NEXT <expr>
7886 // (single-row accumulator) queues with v7.40 SETOF function
7887 // infrastructure.
7888 // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
7889 // and keep going.
7890 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
7891 {
7892 self.advance();
7893 let e = self.parse_expr(0)?;
7894 return Ok(PlPgSqlStmt::ReturnNext(e));
7895 }
7896 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
7897 {
7898 self.advance();
7899 // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
7900 // rows go to the set, like the static form. It used to desugar to a
7901 // bare ExecuteDynamic, whose result was DISCARDED.
7902 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7903 {
7904 self.advance();
7905 let sql = self.parse_expr(0)?;
7906 return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
7907 }
7908 // Bare RETURN QUERY <select>. If the current token is
7909 // not already SELECT (e.g., the user wrote `RETURN QUERY
7910 // <projection> FROM ...` in a shorthand — rare but PG
7911 // accepts a bare projection here), splice one in. Same
7912 // trick as PERFORM.
7913 if !matches!(self.peek(), Token::Select) {
7914 self.tokens.insert(self.pos, Token::Select);
7915 }
7916 let select = self.parse_select_stmt()?;
7917 let Statement::Select(s) = select else {
7918 return Err(self.err(alloc::format!(
7919 "expected SELECT body after RETURN QUERY, got {:?}",
7920 self.peek()
7921 )));
7922 };
7923 // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
7924 // to an embedded side-effect SELECT whose rows were DISCARDED, which
7925 // in a SETOF function is the entire answer thrown away.
7926 return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
7927 }
7928 // Fall through: parse a full expression.
7929 let e = self.parse_expr(0)?;
7930 Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
7931 }
7932
7933 fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
7934 // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
7935 // are ident-shaped (the parser keys off case-insensitive
7936 // match — same shape used by the top-level Update / Delete
7937 // dispatchers at parse_one_statement).
7938 if matches!(self.peek(), Token::Insert) {
7939 self.advance();
7940 return Ok(TriggerEvent::Insert);
7941 }
7942 match self.peek() {
7943 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7944 self.advance();
7945 Ok(TriggerEvent::Update)
7946 }
7947 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7948 self.advance();
7949 Ok(TriggerEvent::Delete)
7950 }
7951 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
7952 self.advance();
7953 Ok(TriggerEvent::Truncate)
7954 }
7955 other => Err(self.err(alloc::format!(
7956 "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
7957 ))),
7958 }
7959 }
7960
7961 /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
7962 /// - (no clause) → implicit `FOR ALL TABLES`
7963 /// - `FOR ALL TABLES`
7964 /// - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
7965 /// - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
7966 /// accepted as an SPG lenience. PG18-measured (round 753): PG
7967 /// REJECTS the bare plural (`invalid publication object list`,
7968 /// TABLES only pairs with IN SCHEMA); the old note claimed an
7969 /// unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
7970 fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
7971 let name = self.expect_ident_or_string()?;
7972 // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
7973 // shape so existing publications keep parsing identically.
7974 let scope = if matches!(self.peek(), Token::For) {
7975 self.advance();
7976 if matches!(self.peek(), Token::All) {
7977 self.advance();
7978 if !matches!(self.peek(), Token::Tables) {
7979 return Err(self.err(format!(
7980 "expected TABLES after FOR ALL, got {:?}",
7981 self.peek()
7982 )));
7983 }
7984 self.advance();
7985 if matches!(self.peek(), Token::Except) {
7986 self.advance();
7987 let tables = self.parse_publication_table_list()?;
7988 PublicationScope::AllTablesExcept(tables)
7989 } else {
7990 PublicationScope::AllTables
7991 }
7992 } else if matches!(self.peek(), Token::Table) {
7993 self.advance();
7994 let tables = self.parse_publication_table_list()?;
7995 PublicationScope::ForTables(tables)
7996 } else if matches!(self.peek(), Token::Tables) {
7997 // v7.39 (round 754, F31-B5) — PG18-measured: the bare
7998 // plural (`FOR TABLES t`) is REJECTED (`invalid
7999 // publication object list`); TABLES only pairs with
8000 // `IN SCHEMA`. The old arm accepted it on an
8001 // unverifiable "PG 19 accepts both" claim.
8002 self.advance();
8003 if !matches!(self.peek(), Token::In) {
8004 return Err(self.err(alloc::string::String::from(
8005 "invalid publication object list",
8006 )));
8007 }
8008 self.advance();
8009 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
8010 return Err(self.err(format!(
8011 "expected SCHEMA after FOR TABLES IN, got {:?}",
8012 self.peek()
8013 )));
8014 }
8015 self.advance();
8016 let schema = self.expect_ident_or_string()?;
8017 PublicationScope::TablesInSchema(schema)
8018 } else {
8019 return Err(self.err(format!(
8020 "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
8021 self.peek()
8022 )));
8023 }
8024 } else {
8025 PublicationScope::AllTables
8026 };
8027 Ok(Statement::CreatePublication(CreatePublicationStatement {
8028 name,
8029 scope,
8030 }))
8031 }
8032
8033 /// v6.1.3 — Comma-separated identifier list for the publication
8034 /// FOR-clause. Requires at least one entry; empty list is a
8035 /// parse error (PG behaviour). Quoted idents are accepted; the
8036 /// names round-trip through `Display` as `quote_ident(name)`.
8037 ///
8038 /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
8039 /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
8040 /// pg_dump output. SPG's publication state today is per-table
8041 /// only (matching the pre-PG-15 surface); the col list + WHERE
8042 /// are parsed so dumps load through and the table name reaches
8043 /// `PublicationScope::ForTables`, but the filter is not enforced
8044 /// at publish time. Re-open when a customer dogfood gate
8045 /// requires per-row-filter or column-subset publish semantics
8046 /// (which gates on persistent slot state landing first, 21.12).
8047 fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
8048 let first = self.parse_publication_table_entry()?;
8049 let mut out = alloc::vec![first];
8050 while matches!(self.peek(), Token::Comma) {
8051 self.advance();
8052 out.push(self.parse_publication_table_entry()?);
8053 }
8054 Ok(out)
8055 }
8056
8057 /// One table entry inside a FOR TABLE clause:
8058 /// tab_name [ (col, col, …) ] [ WHERE (predicate) ]
8059 /// Returns just the table name; the column list + WHERE predicate
8060 /// are consumed and discarded per the parse-accept-discard
8061 /// commitment above.
8062 fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
8063 let name = self.expect_ident_like()?;
8064 // Optional column list — `(col, col, …)`.
8065 if matches!(self.peek(), Token::LParen) {
8066 self.advance();
8067 // Empty parens are a PG error too; require ≥ 1 column.
8068 let _ = self.expect_ident_like()?;
8069 while matches!(self.peek(), Token::Comma) {
8070 self.advance();
8071 let _ = self.expect_ident_like()?;
8072 }
8073 if !matches!(self.peek(), Token::RParen) {
8074 return Err(self.err(alloc::format!(
8075 "expected ')' to close publication column list, got {:?}",
8076 self.peek()
8077 )));
8078 }
8079 self.advance();
8080 }
8081 // Optional row filter — `WHERE (predicate)`.
8082 if matches!(self.peek(), Token::Where) {
8083 self.advance();
8084 if !matches!(self.peek(), Token::LParen) {
8085 return Err(self.err(alloc::format!(
8086 "expected '(' after WHERE in publication row filter, got {:?}",
8087 self.peek()
8088 )));
8089 }
8090 self.advance();
8091 let _ = self.parse_expr(0)?;
8092 if !matches!(self.peek(), Token::RParen) {
8093 return Err(self.err(alloc::format!(
8094 "expected ')' to close publication WHERE filter, got {:?}",
8095 self.peek()
8096 )));
8097 }
8098 self.advance();
8099 }
8100 Ok(name)
8101 }
8102
8103 /// v6.1.4 — `CREATE SUBSCRIPTION <name>
8104 /// CONNECTION '<conn>'
8105 /// PUBLICATION <pub> [, <pub> ...]`.
8106 ///
8107 /// The clause order is fixed (CONNECTION first, then
8108 /// PUBLICATION) to match PG. No WITH-options accepted in
8109 /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
8110 fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
8111 let name = self.expect_ident_or_string()?;
8112 if !matches!(self.peek(), Token::Connection) {
8113 return Err(self.err(format!(
8114 "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
8115 self.peek()
8116 )));
8117 }
8118 self.advance();
8119 let conn_str = self.expect_string_literal()?;
8120 if !matches!(self.peek(), Token::Publication) {
8121 return Err(self.err(format!(
8122 "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8123 self.peek()
8124 )));
8125 }
8126 self.advance();
8127 // Reuse the publication FOR-list parser shape: at least one
8128 // identifier, comma-separated.
8129 let first = self.expect_ident_like()?;
8130 let mut publications = alloc::vec![first];
8131 while matches!(self.peek(), Token::Comma) {
8132 self.advance();
8133 publications.push(self.expect_ident_like()?);
8134 }
8135 Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8136 name,
8137 conn_str,
8138 publications,
8139 }))
8140 }
8141
8142 /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8143 /// All keywords after `WAIT` are bare idents in v6.1.x; no
8144 /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8145 /// that fit `u64`.
8146 /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8147 /// qualifier is a *namespace* the app owns (`app.user_id`,
8148 /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8149 /// to discard. So parse the raw segments here instead of
8150 /// `expect_ident_like`, which strips a leading `schema.` qualifier
8151 /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8152 /// a single segment and round-trip unchanged.
8153 fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8154 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8155 loop {
8156 let seg = match self.advance() {
8157 Token::Ident(s) | Token::QuotedIdent(s) => s,
8158 other if unreserved_keyword_text(&other).is_some() => {
8159 unreserved_keyword_text(&other).unwrap()
8160 }
8161 other => {
8162 return Err(ParseError {
8163 message: format!("expected parameter name, got {other:?}"),
8164 token_pos: self.consumed_pos(),
8165 });
8166 }
8167 };
8168 parts.push(seg);
8169 if matches!(self.peek(), Token::Dot) {
8170 self.advance();
8171 continue;
8172 }
8173 break;
8174 }
8175 Ok(parts.join(".").to_ascii_lowercase())
8176 }
8177
8178 fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8179 Self::parse_set_value_inner(self)
8180 }
8181
8182 fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8183 match self.advance() {
8184 Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8185 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8186 Ok(crate::ast::SetValue::Default)
8187 }
8188 Token::Ident(s) | Token::QuotedIdent(s) => {
8189 let mut accum = s;
8190 while matches!(self.peek(), Token::Dot) {
8191 self.advance();
8192 let next = self.expect_ident_like()?;
8193 accum.push('.');
8194 accum.push_str(&next);
8195 }
8196 Ok(crate::ast::SetValue::Ident(accum))
8197 }
8198 Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8199 Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8200 // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8201 // spellings that lex as keyword tokens, not idents:
8202 // `SET standard_conforming_strings = on` is in every
8203 // pg_dump preamble (`off` already lexes as an ident).
8204 // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8205 // DEFAULT lexes as its keyword token, so the ident arm above
8206 // never saw it and the everyday reset form was a syntax error.
8207 Token::Default => Ok(crate::ast::SetValue::Default),
8208 Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8209 Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8210 Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8211 // v7.14.0 — MySQL session/user variable RHS
8212 // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8213 // Wrap as Ident so the SET handler can record it; the
8214 // engine treats `@VAR` / `@@VAR` values as opaque
8215 // strings.
8216 Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8217 // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8218 // is the common MySQL preamble shape. Allow a `+` or
8219 // `-` prefix on negative numerics for parity with PG
8220 // (some param defaults are negative).
8221 Token::Minus => match self.advance() {
8222 Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8223 Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8224 other => Err(self.err(format!(
8225 "expected numeric after `-` in SET value, got {other:?}"
8226 ))),
8227 },
8228 other => Err(self.err(format!(
8229 "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8230 ))),
8231 }
8232 }
8233
8234 /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8235 /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8236 /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8237 /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8238 /// present). Modes are comma-separated per PG; SPG also
8239 /// accepts space-separated for tolerance. READ ONLY / WRITE
8240 /// / DEFERRABLE are parsed-and-ignored (recorded for future
8241 /// surface but not behaviorally honoured today).
8242 /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8243 /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8244 /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8245 /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8246 /// session default rather than forcing READ COMMITTED.
8247 fn parse_isolation_level_clauses(&mut self) -> Result<Option<IsolationLevel>, ParseError> {
8248 let mut level = IsolationLevel::default();
8249 let mut have_level = false;
8250 loop {
8251 // ISOLATION LEVEL …
8252 let saw_isolation =
8253 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8254 if saw_isolation {
8255 self.advance(); // ISOLATION
8256 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8257 return Err(self.err(alloc::format!(
8258 "expected LEVEL after ISOLATION, got {:?}",
8259 self.peek()
8260 )));
8261 }
8262 self.advance(); // LEVEL
8263 // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8264 let w1 = self
8265 .expect_ident_like()
8266 .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8267 let lc = w1.to_ascii_lowercase();
8268 level = match lc.as_str() {
8269 "serializable" => IsolationLevel::Serializable,
8270 "repeatable" => {
8271 // Expect READ
8272 let w2 = self
8273 .expect_ident_like()
8274 .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8275 if !w2.eq_ignore_ascii_case("read") {
8276 return Err(self.err(alloc::format!(
8277 "expected READ after REPEATABLE, got {w2:?}"
8278 )));
8279 }
8280 IsolationLevel::RepeatableRead
8281 }
8282 "read" => {
8283 let w2 = self
8284 .expect_ident_like()
8285 .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8286 match w2.to_ascii_lowercase().as_str() {
8287 "committed" => IsolationLevel::ReadCommitted,
8288 "uncommitted" => IsolationLevel::ReadUncommitted,
8289 other => {
8290 return Err(self.err(alloc::format!(
8291 "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8292 )));
8293 }
8294 }
8295 }
8296 other => {
8297 return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8298 }
8299 };
8300 have_level = true;
8301 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8302 // READ ONLY | READ WRITE — parsed, not behaviorally honoured.
8303 self.advance();
8304 match self.peek().clone() {
8305 Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8306 self.advance();
8307 }
8308 Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8309 self.advance();
8310 }
8311 other => {
8312 return Err(self.err(alloc::format!(
8313 "expected ONLY or WRITE after READ, got {other:?}"
8314 )));
8315 }
8316 }
8317 } else if matches!(self.peek(), Token::Not) {
8318 // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8319 self.advance();
8320 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8321 return Err(self.err(alloc::format!(
8322 "expected DEFERRABLE after NOT, got {:?}",
8323 self.peek()
8324 )));
8325 }
8326 self.advance();
8327 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8328 {
8329 self.advance();
8330 } else {
8331 break;
8332 }
8333 // Optional comma between modes.
8334 if matches!(self.peek(), Token::Comma) {
8335 self.advance();
8336 }
8337 }
8338 Ok(have_level.then_some(level))
8339 }
8340
8341 fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8342 // FOR is a v6.1.2-reserved keyword (Token::For). The
8343 // other two are bare idents — they've never needed lexer
8344 // support and we keep it that way.
8345 if !matches!(self.peek(), Token::For) {
8346 return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8347 }
8348 self.advance();
8349 self.expect_keyword_ident("wal")?;
8350 self.expect_keyword_ident("position")?;
8351 let pos = self.expect_u64_literal()?;
8352 let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8353 {
8354 self.advance();
8355 self.expect_keyword_ident("timeout")?;
8356 Some(self.expect_u64_literal()?)
8357 } else {
8358 None
8359 };
8360 Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8361 }
8362
8363 /// v6.1.7 helper — consume a `Token::Integer` and check it
8364 /// fits `u64`. WAL positions and millisecond timeouts are
8365 /// non-negative.
8366 fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8367 match self.advance() {
8368 Token::Integer(n) if n >= 0 => Ok(n as u64),
8369 Token::Integer(n) => Err(ParseError {
8370 message: format!("expected non-negative integer, got {n}"),
8371 token_pos: self.consumed_pos(),
8372 }),
8373 other => Err(ParseError {
8374 message: format!("expected integer literal, got {other:?}"),
8375 token_pos: self.consumed_pos(),
8376 }),
8377 }
8378 }
8379
8380 /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8381 /// ROLE '<role>' (defaults to readonly). All string slots accept
8382 /// either a quoted ident or a quoted string literal.
8383 /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8384 /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8385 ///
8386 /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8387 /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8388 /// wire role) still parses — it is a different axis from the PG attributes.
8389 /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8390 /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8391 /// or RESET, so the plain attribute forms keep their old path.
8392 fn peeks_db_role_setting(&self) -> bool {
8393 let mut i = self.pos + 1; // past the object's name
8394 let word = |p: usize| -> Option<String> {
8395 match self.tokens.get(p) {
8396 Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8397 Some(Token::In) => Some(String::from("in")),
8398 _ => None,
8399 }
8400 };
8401 if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8402 i += 3; // IN DATABASE <name>
8403 }
8404 matches!(word(i).as_deref(), Some("set" | "reset"))
8405 }
8406
8407 fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8408 use crate::ast::SetDbRoleSettingStatement;
8409 // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8410 // identifier, so the ordinary name reader refuses it. Same trap
8411 // as TABLE / INDEX / FULL / DEFAULT before it.
8412 let name = if matches!(self.peek(), Token::All) {
8413 self.advance();
8414 String::from("all")
8415 } else {
8416 self.expect_ident_or_string()?
8417 };
8418 // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8419 let all = name.eq_ignore_ascii_case("all");
8420 let (mut database, mut role) = if is_database {
8421 (Some(name), None)
8422 } else if all {
8423 (None, None)
8424 } else {
8425 (None, Some(name))
8426 };
8427 if matches!(self.peek(), Token::In) {
8428 self.advance();
8429 self.advance(); // DATABASE
8430 database = Some(self.expect_ident_or_string()?);
8431 }
8432 let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8433 self.advance(); // SET | RESET
8434 if resetting && matches!(self.peek(), Token::All) {
8435 self.advance();
8436 self.consume_until_statement_boundary();
8437 return Ok(Statement::SetDbRoleSetting(Box::new(
8438 SetDbRoleSettingStatement {
8439 database,
8440 role,
8441 param: None,
8442 value: None,
8443 },
8444 )));
8445 }
8446 let param = self.expect_ident_like()?;
8447 let value = if resetting {
8448 None
8449 } else {
8450 // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8451 // KEYWORD, so the ident-only check missed it and consumed
8452 // the word itself as the value — the same trap as ALL, one
8453 // clause over.
8454 if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8455 self.advance();
8456 }
8457 Some(self.take_guc_value())
8458 };
8459 self.consume_until_statement_boundary();
8460 Ok(Statement::SetDbRoleSetting(Box::new(
8461 SetDbRoleSettingStatement {
8462 database,
8463 role,
8464 param: Some(param),
8465 value,
8466 },
8467 )))
8468 }
8469
8470 /// The remainder of a `SET <p> = …` clause as PG renders it back:
8471 /// a quoted literal loses its quotes, a bare word or number does not.
8472 fn take_guc_value(&mut self) -> String {
8473 match self.advance() {
8474 Token::String(s) => s,
8475 Token::Integer(n) => format!("{n}"),
8476 Token::Float(f) => format!("{f}"),
8477 Token::Ident(s) | Token::QuotedIdent(s) => s,
8478 other => format!("{other:?}"),
8479 }
8480 }
8481
8482 fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8483 let name = self.expect_ident_or_string()?;
8484 if self.peek_keyword_ident("with") {
8485 self.advance();
8486 }
8487 let mut password = String::new();
8488 let mut role = String::new();
8489 let mut login: Option<bool> = None;
8490 let mut inherit: Option<bool> = None;
8491 let mut superuser: Option<bool> = None;
8492 // Not a `while let`: the pattern would borrow `self` across the
8493 // body, which calls `self.advance()` / `self.expect_*` (&mut).
8494 #[allow(clippy::while_let_loop)]
8495 loop {
8496 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8497 break;
8498 };
8499 match w.to_ascii_lowercase().as_str() {
8500 "password" => {
8501 self.advance();
8502 password = self.expect_string_literal()?;
8503 }
8504 // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8505 // is the same slot.
8506 "encrypted" => {
8507 self.advance();
8508 self.expect_keyword_ident("password")?;
8509 password = self.expect_string_literal()?;
8510 }
8511 "login" => {
8512 self.advance();
8513 login = Some(true);
8514 }
8515 "nologin" => {
8516 self.advance();
8517 login = Some(false);
8518 }
8519 "inherit" => {
8520 self.advance();
8521 inherit = Some(true);
8522 }
8523 "noinherit" => {
8524 self.advance();
8525 inherit = Some(false);
8526 }
8527 "superuser" => {
8528 self.advance();
8529 superuser = Some(true);
8530 }
8531 "nosuperuser" => {
8532 self.advance();
8533 superuser = Some(false);
8534 }
8535 // SPG's own coarse wire role: `ROLE 'readwrite'`.
8536 "role" => {
8537 self.advance();
8538 role = self.expect_string_literal()?;
8539 }
8540 // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8541 // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8542 // accepted and ignored so a pg_dump role block restores. They
8543 // gate capabilities SPG does not have.
8544 "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8545 | "noreplication" | "bypassrls" | "nobypassrls" => {
8546 self.advance();
8547 }
8548 "connection" => {
8549 self.advance();
8550 self.expect_keyword_ident("limit")?;
8551 self.advance(); // the number
8552 }
8553 "valid" => {
8554 self.advance();
8555 self.expect_keyword_ident("until")?;
8556 self.expect_string_literal()?;
8557 }
8558 _ => break,
8559 }
8560 }
8561 if role.is_empty() {
8562 role = "readonly".to_string();
8563 }
8564 Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8565 name,
8566 password,
8567 role,
8568 login,
8569 inherit,
8570 superuser,
8571 is_user,
8572 }))
8573 }
8574
8575 /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8576 /// consumed the USING / WITH CHECK keyword.
8577 fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8578 if !matches!(self.peek(), Token::LParen) {
8579 return Err(self.err(alloc::format!(
8580 "expected '(' after {clause}, got {:?}",
8581 self.peek()
8582 )));
8583 }
8584 self.advance();
8585 let e = self.parse_expr(0)?;
8586 if !matches!(self.peek(), Token::RParen) {
8587 return Err(self.err(alloc::format!(
8588 "expected ')' to close {clause}, got {:?}",
8589 self.peek()
8590 )));
8591 }
8592 self.advance();
8593 Ok(e)
8594 }
8595
8596 /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8597 fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8598 let mut roles = Vec::new();
8599 loop {
8600 roles.push(self.expect_ident_like()?);
8601 if matches!(self.peek(), Token::Comma) {
8602 self.advance();
8603 } else {
8604 break;
8605 }
8606 }
8607 Ok(roles)
8608 }
8609
8610 /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8611 /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8612 /// `CREATE POLICY`.
8613 fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8614 use crate::ast::PolicyCmd;
8615 let name = self.expect_ident_like()?;
8616 if !matches!(self.peek(), Token::On) {
8617 return Err(self.err(alloc::format!(
8618 "expected ON after CREATE POLICY name, got {:?}",
8619 self.peek()
8620 )));
8621 }
8622 self.advance();
8623 let table = self.expect_ident_like()?;
8624
8625 let mut permissive = true;
8626 if matches!(self.peek(), Token::As) {
8627 self.advance();
8628 let w = self.expect_ident_like()?;
8629 permissive = if w.eq_ignore_ascii_case("permissive") {
8630 true
8631 } else if w.eq_ignore_ascii_case("restrictive") {
8632 false
8633 } else {
8634 return Err(self.err(alloc::format!(
8635 "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8636 )));
8637 };
8638 }
8639
8640 let mut cmd = PolicyCmd::All;
8641 if matches!(self.peek(), Token::For) {
8642 self.advance();
8643 cmd = self.parse_policy_cmd()?;
8644 }
8645
8646 let mut roles = Vec::new();
8647 if matches!(self.peek(), Token::To) {
8648 self.advance();
8649 roles = self.parse_policy_roles()?;
8650 }
8651
8652 let mut using = None;
8653 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8654 {
8655 self.advance();
8656 using = Some(self.parse_paren_expr("USING")?);
8657 }
8658
8659 let mut with_check = None;
8660 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8661 {
8662 self.advance();
8663 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8664 {
8665 return Err(self.err(alloc::format!(
8666 "expected CHECK after WITH, got {:?}",
8667 self.peek()
8668 )));
8669 }
8670 self.advance();
8671 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8672 }
8673
8674 // Clause-per-command matrix (PG wording).
8675 match cmd {
8676 PolicyCmd::Insert => {
8677 if using.is_some() {
8678 return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8679 }
8680 }
8681 PolicyCmd::Select | PolicyCmd::Delete => {
8682 if with_check.is_some() {
8683 return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8684 }
8685 }
8686 PolicyCmd::Update | PolicyCmd::All => {}
8687 }
8688
8689 Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8690 name,
8691 table,
8692 permissive,
8693 cmd,
8694 roles,
8695 using,
8696 with_check,
8697 }))
8698 }
8699
8700 /// v7.39 (RLS) — the command word after `FOR`.
8701 fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8702 use crate::ast::PolicyCmd;
8703 match self.peek().clone() {
8704 Token::All => {
8705 self.advance();
8706 Ok(PolicyCmd::All)
8707 }
8708 Token::Select => {
8709 self.advance();
8710 Ok(PolicyCmd::Select)
8711 }
8712 Token::Insert => {
8713 self.advance();
8714 Ok(PolicyCmd::Insert)
8715 }
8716 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8717 self.advance();
8718 Ok(PolicyCmd::Update)
8719 }
8720 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8721 self.advance();
8722 Ok(PolicyCmd::Delete)
8723 }
8724 other => Err(self.err(alloc::format!(
8725 "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8726 ))),
8727 }
8728 }
8729
8730 /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8731 /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8732 fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8733 let name = self.expect_ident_like()?;
8734 if !matches!(self.peek(), Token::On) {
8735 return Err(self.err(alloc::format!(
8736 "expected ON after ALTER POLICY name, got {:?}",
8737 self.peek()
8738 )));
8739 }
8740 self.advance();
8741 let table = self.expect_ident_like()?;
8742
8743 // RENAME TO new
8744 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8745 {
8746 self.advance();
8747 if !matches!(self.peek(), Token::To) {
8748 return Err(self.err(alloc::format!(
8749 "expected TO after RENAME, got {:?}",
8750 self.peek()
8751 )));
8752 }
8753 self.advance();
8754 let new = self.expect_ident_like()?;
8755 return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8756 name,
8757 table,
8758 rename_to: Some(new),
8759 roles: None,
8760 using: None,
8761 with_check: None,
8762 }));
8763 }
8764
8765 let mut roles = None;
8766 if matches!(self.peek(), Token::To) {
8767 self.advance();
8768 roles = Some(self.parse_policy_roles()?);
8769 }
8770 let mut using = None;
8771 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8772 {
8773 self.advance();
8774 using = Some(self.parse_paren_expr("USING")?);
8775 }
8776 let mut with_check = None;
8777 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8778 {
8779 self.advance();
8780 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8781 {
8782 return Err(self.err(alloc::format!(
8783 "expected CHECK after WITH, got {:?}",
8784 self.peek()
8785 )));
8786 }
8787 self.advance();
8788 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8789 }
8790 Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8791 name,
8792 table,
8793 rename_to: None,
8794 roles,
8795 using,
8796 with_check,
8797 }))
8798 }
8799
8800 /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
8801 /// `DROP POLICY`.
8802 fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8803 let if_exists = self.consume_if_exists();
8804 let name = self.expect_ident_like()?;
8805 if !matches!(self.peek(), Token::On) {
8806 return Err(self.err(alloc::format!(
8807 "expected ON after DROP POLICY name, got {:?}",
8808 self.peek()
8809 )));
8810 }
8811 self.advance();
8812 let table = self.expect_ident_like()?;
8813 Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
8814 name,
8815 table,
8816 if_exists,
8817 }))
8818 }
8819}
8820fn wrap_from_leaves(
8821 e: &mut Expr,
8822 names: &[String],
8823 make: &dyn Fn(Expr) -> Expr,
8824 refs: &dyn Fn(&Expr) -> bool,
8825) {
8826 if let Expr::Column(c) = e {
8827 if c.qualifier
8828 .as_deref()
8829 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
8830 {
8831 let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
8832 *e = make(taken);
8833 }
8834 return;
8835 }
8836 match e {
8837 Expr::Binary { lhs, rhs, .. } => {
8838 wrap_from_leaves(lhs, names, make, refs);
8839 wrap_from_leaves(rhs, names, make, refs);
8840 }
8841 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
8842 wrap_from_leaves(expr, names, make, refs)
8843 }
8844 Expr::FunctionCall { args, .. } => {
8845 for a in args.iter_mut() {
8846 wrap_from_leaves(a, names, make, refs);
8847 }
8848 }
8849 Expr::Case {
8850 operand,
8851 branches,
8852 else_branch,
8853 } => {
8854 if let Some(o) = operand.as_deref_mut() {
8855 wrap_from_leaves(o, names, make, refs);
8856 }
8857 for (w, t) in branches.iter_mut() {
8858 wrap_from_leaves(w, names, make, refs);
8859 wrap_from_leaves(t, names, make, refs);
8860 }
8861 if let Some(el) = else_branch.as_deref_mut() {
8862 wrap_from_leaves(el, names, make, refs);
8863 }
8864 }
8865 // Compound variants the walk doesn't decompose: keep the
8866 // pre-D.30 behavior — wrap the whole sub-expr if it touches
8867 // a source table, so nothing regresses.
8868 other => {
8869 if refs(other) {
8870 let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
8871 *other = make(taken);
8872 }
8873 }
8874 }
8875}
8876
8877/// v7.39 (round 241) — does this expression reference any of the FROM /
8878/// USING table names (shared by the UPDATE…FROM and DELETE…USING
8879/// lowerings)?
8880fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
8881 match e {
8882 Expr::Column(c) => c
8883 .qualifier
8884 .as_deref()
8885 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
8886 Expr::Binary { lhs, rhs, .. } => {
8887 expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
8888 }
8889 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
8890 Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
8891 Expr::Case {
8892 operand,
8893 branches,
8894 else_branch,
8895 } => {
8896 operand
8897 .as_deref()
8898 .is_some_and(|o| expr_refs_tables(o, names))
8899 || branches
8900 .iter()
8901 .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
8902 || else_branch
8903 .as_deref()
8904 .is_some_and(|el| expr_refs_tables(el, names))
8905 }
8906 _ => false,
8907 }
8908}
8909
8910impl Parser {
8911 /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
8912 /// Caller already consumed the leading `UPDATE` ident.
8913 /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
8914 /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
8915 /// after the target name has been read. `JOIN` is a reserved token;
8916 /// the qualifiers are bare idents.
8917 fn peek_is_update_join_start(&self) -> bool {
8918 match self.peek() {
8919 // JOIN and its qualifiers are reserved lexer tokens (the grammar
8920 // dedicates arms to `LEFT [OUTER] JOIN` and friends).
8921 Token::Join
8922 | Token::Inner
8923 | Token::Left
8924 | Token::Right
8925 | Token::Cross
8926 | Token::Full => true,
8927 // NATURAL / STRAIGHT_JOIN arrive as bare idents.
8928 Token::Ident(s) | Token::QuotedIdent(s) => {
8929 matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
8930 }
8931 _ => false,
8932 }
8933 }
8934
8935 /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
8936 /// USER-variable assignment. Its own per-session namespace, an arbitrary
8937 /// expression on the right, and `:=` as a second spelling of `=`.
8938 ///
8939 /// Out-of-line (`inline(never)`): the statement-parse frame it is called
8940 /// from sits on the nesting recursion chain (a CTE body, a subquery),
8941 /// and holding this loop's `Vec` + `String` locals there overflowed the
8942 /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
8943 #[inline(never)]
8944 fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
8945 let mut assigns: Vec<(String, Expr)> = Vec::new();
8946 let mut settings: Vec<(String, Expr)> = Vec::new();
8947 loop {
8948 // v7.39 (round 554) — a plain NAME here is a session
8949 // setting, not a user variable. mysqldump writes the two in
8950 // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
8951 // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
8952 // changes it — and this refused the mixture outright, so no
8953 // dump could be restored past its preamble.
8954 if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
8955 self.advance();
8956 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8957 return Err(self.err(alloc::format!(
8958 "expected `=` after {name}, got {:?}",
8959 self.peek()
8960 )));
8961 }
8962 self.advance();
8963 let value = self.parse_expr(0)?;
8964 settings.push((name.to_ascii_lowercase(), value));
8965 if matches!(self.peek(), Token::Comma) {
8966 self.advance();
8967 continue;
8968 }
8969 break;
8970 }
8971 let Token::SessionVar(raw) = self.peek().clone() else {
8972 return Err(self.err(alloc::format!(
8973 "expected a user variable after SET, got {:?}",
8974 self.peek()
8975 )));
8976 };
8977 if raw.starts_with("@@") {
8978 return Err(self.err(alloc::string::String::from(
8979 "cannot mix `@@` settings with `@` user variables in one SET",
8980 )));
8981 }
8982 self.advance();
8983 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8984 return Err(self.err(alloc::format!(
8985 "expected `=` or `:=` after {raw}, got {:?}",
8986 self.peek()
8987 )));
8988 }
8989 self.advance();
8990 let value = self.parse_expr(0)?;
8991 assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
8992 if matches!(self.peek(), Token::Comma) {
8993 self.advance();
8994 continue;
8995 }
8996 break;
8997 }
8998 Ok(Statement::SetUserVars(assigns, settings))
8999 }
9000
9001 fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
9002 // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
9003 // NAMED `only` until now, which failed on `relation "only" does
9004 // not exist`. The lookahead is what keeps a table actually
9005 // called `only` working: the keyword is only a keyword when a
9006 // TABLE NAME follows it — and `SET` arrives as an identifier
9007 // here, so `UPDATE only SET a = 2` would otherwise take `SET`
9008 // for the table and die on the `=`. Measured by the pin.
9009 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9010 if s.eq_ignore_ascii_case("only"))
9011 && matches!(
9012 self.tokens.get(self.pos + 1),
9013 Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
9014 );
9015 if only {
9016 self.advance();
9017 }
9018 let table = self.expect_ident_like()?;
9019 // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
9020 // bare spelling; a bare identifier that is the SET keyword itself
9021 // is the clause, not an alias.
9022 // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
9023 // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
9024 // multi-table form, and swallowing `LEFT` as `a`'s alias made the
9025 // following JOIN a syntax error.
9026 let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
9027 let alias = if matches!(self.peek(), Token::As) {
9028 self.advance();
9029 Some(self.expect_ident_like()?)
9030 } else {
9031 match self.peek() {
9032 Token::Ident(s) | Token::QuotedIdent(s)
9033 if !s.eq_ignore_ascii_case("set") && !starts_join =>
9034 {
9035 let a = s.clone();
9036 self.advance();
9037 Some(a)
9038 }
9039 _ => None,
9040 }
9041 };
9042 // v7.39 (round 420) — MySQL's multi-table UPDATE:
9043 // UPDATE a, b SET a.v = b.v WHERE a.id = b.id
9044 // UPDATE a JOIN b ON a.id = b.id SET a.v = b.v + 1
9045 // UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
9046 // The FIRST table is the mutation target and the rest are sources —
9047 // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
9048 // SPG already lowers onto correlated subqueries. So rewind, let
9049 // `parse_from_clause` read the whole list (it handles aliases, comma
9050 // lists, and every JOIN form), then peel the target off the front.
9051 let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
9052 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9053 {
9054 // NOTE: `advance()` destroys the tokens it returns
9055 // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
9056 // is NOT possible — the tail is read forward, once, through the
9057 // same grammar `parse_from_clause` uses after its primary.
9058 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9059 let mut joins = self.parse_from_joins(&target_qual)?;
9060 if joins.is_empty() {
9061 return Err(self.err(alloc::string::String::from(
9062 "multi-table UPDATE needs at least one source table",
9063 )));
9064 }
9065 let head = joins.remove(0);
9066 // A LEFT join keeps every target row (the unmatched ones see NULL
9067 // on the source side), so it must NOT get the EXISTS row filter
9068 // the inner / comma forms use.
9069 let outer = matches!(head.kind, crate::ast::JoinKind::Left);
9070 let src = FromClause {
9071 primary: head.table,
9072 joins,
9073 };
9074 (Some(src), head.on, outer)
9075 } else {
9076 (None, None, false)
9077 };
9078 self.expect_keyword_ident("set")?;
9079 let mut assignments = Vec::new();
9080 loop {
9081 // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
9082 // …)` — the parenthesized multi-assignment. Expressions
9083 // assign positionally; a subquery RHS clones per column
9084 // keeping only the Nth projection item.
9085 if matches!(self.peek(), Token::LParen) {
9086 self.advance();
9087 let mut cols = alloc::vec![self.expect_ident_like()?];
9088 while matches!(self.peek(), Token::Comma) {
9089 self.advance();
9090 cols.push(self.expect_ident_like()?);
9091 }
9092 if !matches!(self.peek(), Token::RParen) {
9093 return Err(self.err(format!(
9094 "expected ')' after SET column list, got {:?}",
9095 self.peek()
9096 )));
9097 }
9098 self.advance();
9099 if !matches!(self.peek(), Token::Eq) {
9100 return Err(self.err(format!(
9101 "expected `=` after SET column list, got {:?}",
9102 self.peek()
9103 )));
9104 }
9105 self.advance();
9106 if !matches!(self.peek(), Token::LParen) {
9107 return Err(self.err(format!(
9108 "expected '(' after SET (…) =, got {:?}",
9109 self.peek()
9110 )));
9111 }
9112 self.advance();
9113 if matches!(self.peek(), Token::Select) {
9114 let inner = match self.parse_select_stmt()? {
9115 Statement::Select(s) => s,
9116 other => {
9117 return Err(self.err(alloc::format!(
9118 "expected SELECT in SET (…) = (SELECT …), got {other:?}"
9119 )));
9120 }
9121 };
9122 if !matches!(self.peek(), Token::RParen) {
9123 return Err(self.err(format!(
9124 "expected ')' after SET subquery, got {:?}",
9125 self.peek()
9126 )));
9127 }
9128 self.advance();
9129 if inner.items.len() != cols.len() {
9130 return Err(self.err(alloc::format!(
9131 "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9132 cols.len(),
9133 inner.items.len()
9134 )));
9135 }
9136 for (i, col) in cols.into_iter().enumerate() {
9137 let mut sub = inner.clone();
9138 sub.items = alloc::vec![sub.items[i].clone()];
9139 assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9140 }
9141 } else {
9142 let mut exprs = alloc::vec![self.parse_expr(0)?];
9143 while matches!(self.peek(), Token::Comma) {
9144 self.advance();
9145 exprs.push(self.parse_expr(0)?);
9146 }
9147 if !matches!(self.peek(), Token::RParen) {
9148 return Err(self.err(format!(
9149 "expected ')' after SET row values, got {:?}",
9150 self.peek()
9151 )));
9152 }
9153 self.advance();
9154 if exprs.len() != cols.len() {
9155 return Err(self.err(alloc::format!(
9156 "SET (…) = (…) arity mismatch: {} columns, {} values",
9157 cols.len(),
9158 exprs.len()
9159 )));
9160 }
9161 for (col, e) in cols.into_iter().zip(exprs) {
9162 assignments.push((col, e));
9163 }
9164 }
9165 if matches!(self.peek(), Token::Comma) {
9166 self.advance();
9167 continue;
9168 }
9169 break;
9170 }
9171 // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9172 // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9173 // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9174 // `public.` dump qualifiers), so the qualifier has to be read off
9175 // the token stream first — otherwise `SET b.v = 888` would write
9176 // to the TARGET table's `v` while naming a source table, a
9177 // silent-wrong. A qualifier naming a SOURCE table means a
9178 // multi-TARGET update — mutating two tables in one statement —
9179 // which SPG does not model, so it is refused loudly.
9180 let set_qual: Option<String> = if mysql_from.is_some()
9181 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9182 {
9183 match self.peek() {
9184 Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9185 _ => None,
9186 }
9187 } else {
9188 None
9189 };
9190 let col = self.expect_ident_like()?;
9191 if let Some(q) = set_qual {
9192 let names_target = q.eq_ignore_ascii_case(&table)
9193 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9194 if !names_target {
9195 return Err(self.err(alloc::format!(
9196 "multi-table UPDATE can only assign to its first table \
9197 ({table}); `{q}.{col}` targets another table"
9198 )));
9199 }
9200 }
9201 // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9202 // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9203 // `__column_default` marker lowering just below). PG assigns to the
9204 // i-th (1-based) element, NULL-padding when i exceeds the length.
9205 if matches!(self.peek(), Token::LBracket) {
9206 self.advance();
9207 let index = self.parse_expr(0)?;
9208 // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9209 // (and the open `arr[lo:]`), lowered to
9210 // `__array_assign_slice`. Only the single-subscript form
9211 // parsed before, so a slice assignment was a syntax error.
9212 let mut slice_hi: Option<Option<Expr>> = None;
9213 if matches!(self.peek(), Token::Colon) {
9214 self.advance();
9215 slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9216 None
9217 } else {
9218 Some(self.parse_expr(0)?)
9219 });
9220 }
9221 if !matches!(self.peek(), Token::RBracket) {
9222 return Err(self.err(format!(
9223 "expected `]` after array subscript in UPDATE SET, got {:?}",
9224 self.peek()
9225 )));
9226 }
9227 self.advance();
9228 if !matches!(self.peek(), Token::Eq) {
9229 return Err(self.err(format!(
9230 "expected `=` after array subscript in UPDATE SET, got {:?}",
9231 self.peek()
9232 )));
9233 }
9234 self.advance();
9235 let value = self.parse_expr(0)?;
9236 // PG merges several subscript writes to the same column into one
9237 // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9238 // assignment to `col` rather than each overwriting the original.
9239 let existing = assignments.iter().position(|(c, _)| c == &col);
9240 let base = match existing {
9241 Some(i) => assignments[i].1.clone(),
9242 None => Expr::Column(ColumnName {
9243 qualifier: None,
9244 name: col.clone(),
9245 }),
9246 };
9247 let assigned = match slice_hi {
9248 None => Expr::FunctionCall {
9249 name: "__array_assign".to_string(),
9250 args: alloc::vec![base, index, value],
9251 },
9252 Some(hi) => Expr::FunctionCall {
9253 name: "__array_assign_slice".to_string(),
9254 args: alloc::vec![
9255 base,
9256 index,
9257 hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9258 value,
9259 ],
9260 },
9261 };
9262 match existing {
9263 Some(i) => assignments[i].1 = assigned,
9264 None => assignments.push((col, assigned)),
9265 }
9266 if matches!(self.peek(), Token::Comma) {
9267 self.advance();
9268 continue;
9269 }
9270 break;
9271 }
9272 if !matches!(self.peek(), Token::Eq) {
9273 return Err(self.err(format!(
9274 "expected `=` after column name in UPDATE SET, got {:?}",
9275 self.peek()
9276 )));
9277 }
9278 self.advance();
9279 // `SET col = DEFAULT` — the column's declared default;
9280 // rides out as a marker call the update executor
9281 // resolves against the schema.
9282 let value = if matches!(self.peek(), Token::Default) {
9283 self.advance();
9284 Expr::FunctionCall {
9285 name: "__column_default".to_string(),
9286 args: Vec::new(),
9287 }
9288 } else {
9289 self.parse_expr(0)?
9290 };
9291 assignments.push((col, value));
9292 if matches!(self.peek(), Token::Comma) {
9293 self.advance();
9294 continue;
9295 }
9296 break;
9297 }
9298 // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9299 // update. Lowers onto the correlated-subquery machinery:
9300 // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9301 // and each assignment that references a FROM-list table
9302 // wraps into a correlated scalar subquery
9303 // (SELECT expr FROM src WHERE cond). Equivalent for the
9304 // unique-join shape (the overwhelmingly common one); a
9305 // multi-match, which PG resolves by arbitrary pick,
9306 // surfaces as a scalar-subquery cardinality error instead
9307 // of a silent arbitrary result.
9308 // v7.39 (round 420) — the MySQL multi-table form supplies the source
9309 // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9310 // the SAME lowering below. Both spellings together is not legal in
9311 // either dialect.
9312 let from_clause = if let Some(fc) = mysql_from {
9313 if matches!(self.peek(), Token::From) {
9314 return Err(self.err(alloc::string::String::from(
9315 "multi-table UPDATE already names its sources; drop the FROM clause",
9316 )));
9317 }
9318 Some(fc)
9319 } else if matches!(self.peek(), Token::From) {
9320 self.advance();
9321 Some(self.parse_from_clause()?)
9322 } else {
9323 None
9324 };
9325 let where_ = if matches!(self.peek(), Token::Where) {
9326 self.advance();
9327 Some(self.parse_expr(0)?)
9328 } else {
9329 None
9330 };
9331 // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9332 // and the TARGET-row filter are NOT the same predicate once a LEFT
9333 // join is involved:
9334 // * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9335 // one conjunction, and the whole thing filters target rows via
9336 // EXISTS.
9337 // * LEFT join: only the ON predicate belongs inside the source
9338 // subquery. The WHERE still filters TARGET rows (with source
9339 // columns read through the correlated subquery, which yields NULL
9340 // for an unmatched row — exactly LEFT-join semantics).
9341 // Round 420 folded ON into WHERE unconditionally and then dropped the
9342 // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9343 // WHERE a.id > 1` updated EVERY row.
9344 let sub_where = match (mysql_on.clone(), where_.clone()) {
9345 _ if mysql_outer => mysql_on.clone(),
9346 (Some(on), Some(w)) => Some(Expr::Binary {
9347 lhs: Box::new(on),
9348 op: crate::ast::BinOp::And,
9349 rhs: Box::new(w),
9350 }),
9351 (Some(on), None) => Some(on),
9352 (None, w) => w,
9353 };
9354 // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9355 // has no such clause on UPDATE, so this is accepted only under the
9356 // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9357 let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9358 let mut returning = self.parse_optional_returning()?;
9359 // v7.39 (round 533) — kept for the engine, which can resolve the
9360 // UNQUALIFIED leaves this lowering has to leave alone.
9361 let from_sources = from_clause.as_ref().map(|fc| {
9362 alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9363 from: fc.clone(),
9364 sub_where: sub_where.clone(),
9365 })
9366 });
9367 let (assignments, where_) = if let Some(fc) = from_clause {
9368 let names: Vec<String> = core::iter::once(&fc.primary)
9369 .chain(fc.joins.iter().map(|j| &j.table))
9370 .flat_map(|t| {
9371 t.alias
9372 .clone()
9373 .into_iter()
9374 .chain(core::iter::once(t.name.clone()))
9375 })
9376 .collect();
9377 let refs_list = |e: &Expr| -> bool {
9378 fn walk(e: &Expr, names: &[String]) -> bool {
9379 match e {
9380 Expr::Column(c) => c
9381 .qualifier
9382 .as_deref()
9383 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9384 Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9385 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9386 Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9387 Expr::Case {
9388 operand,
9389 branches,
9390 else_branch,
9391 } => {
9392 operand.as_deref().is_some_and(|o| walk(o, names))
9393 || branches
9394 .iter()
9395 .any(|(w, t)| walk(w, names) || walk(t, names))
9396 || else_branch.as_deref().is_some_and(|el| walk(el, names))
9397 }
9398 _ => false,
9399 }
9400 }
9401 walk(e, &names)
9402 };
9403 let sub_select = |items: Vec<SelectItem>| SelectStatement {
9404 locking: None,
9405 ctes: Vec::new(),
9406 distinct: false,
9407 distinct_on: Vec::new(),
9408 items,
9409 from: Some(fc.clone()),
9410 where_: sub_where.clone(),
9411 group_by: None,
9412 group_by_all: false,
9413 having: None,
9414 unions: Vec::new(),
9415 order_by: Vec::new(),
9416 limit: None,
9417 offset: None,
9418 limit_with_ties: false,
9419 window_check_exprs: Vec::new(),
9420 };
9421 // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9422 // assignment RHS with a correlated scalar subquery, instead of
9423 // wrapping the whole RHS. Wrapping the whole expr moved a target-
9424 // column reference (`SET v = v + u.bonus`, where `v` is the target
9425 // table's column) inside a subquery whose FROM only has the source
9426 // table, so the unqualified `v` resolved against the source and
9427 // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9428 // context — where they belong — fixes it; only the source columns
9429 // (`u.bonus`) become subqueries. A whole-expr fallback covers
9430 // compound variants the leaf-walk doesn't decompose.
9431 let make_subq = |inner: Expr| {
9432 Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9433 expr: inner,
9434 alias: None,
9435 }])))
9436 };
9437 let assignments = assignments
9438 .into_iter()
9439 .map(|(col, mut expr)| {
9440 wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9441 (col, expr)
9442 })
9443 .collect();
9444 let exists = Expr::Exists {
9445 subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9446 expr: Expr::Literal(Literal::Integer(1)),
9447 alias: None,
9448 }])),
9449 negated: false,
9450 };
9451 // v7.39 (round 241) — RETURNING may reference the FROM-list
9452 // tables too (`RETURNING emp.id, dept.name`); the same
9453 // leaf-to-correlated-subquery lowering the assignments get.
9454 // Without it the qualifier died at eval with "unknown table
9455 // qualifier". (RETURNING was parsed before this block — the
9456 // lowering is a pure AST transformation.)
9457 if let Some(items) = returning.as_mut() {
9458 for item in items.iter_mut() {
9459 if let SelectItem::Expr { expr, .. } = item {
9460 wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9461 }
9462 }
9463 }
9464 // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9465 // EVERY matching target row: it gets no EXISTS filter, but the
9466 // caller's WHERE still applies, with source columns read through
9467 // the correlated subquery (NULL when unmatched — LEFT-join
9468 // semantics). `sub_where` above already excluded the WHERE from
9469 // the source subquery for this case.
9470 if mysql_outer {
9471 let mut outer = where_;
9472 if let Some(w) = outer.as_mut() {
9473 wrap_from_leaves(w, &names, &make_subq, &refs_list);
9474 }
9475 (assignments, outer)
9476 } else {
9477 (assignments, Some(exists))
9478 }
9479 } else {
9480 (assignments, where_)
9481 };
9482 Ok(Statement::Update(crate::ast::UpdateStatement {
9483 ctes: Vec::new(),
9484 table,
9485 only,
9486 alias,
9487 assignments,
9488 from_sources,
9489 where_,
9490 order_limit: update_order_limit,
9491 returning,
9492 }))
9493 }
9494
9495 /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9496 /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9497 /// clause and its meaning are identical, so both call this rather than
9498 /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9499 /// legal. PG has no such clause on either statement, so it is read only
9500 /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9501 /// errors.
9502 ///
9503 /// `#[inline(never)]`: its locals would otherwise land on the statement-
9504 /// parsing recursion frame, which is what tipped the 512 KiB nesting
9505 /// stack in round 430.
9506 #[inline(never)]
9507 fn parse_mysql_dml_order_limit(
9508 &mut self,
9509 what: &str,
9510 ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9511 if !self.mysql_dialect {
9512 return Ok(None);
9513 }
9514 let order_by = self.parse_order_by_keys()?;
9515 let limit = if matches!(self.peek(), Token::Limit) {
9516 self.advance();
9517 let tok = self.advance();
9518 let Token::Integer(n) = tok else {
9519 return Err(self.err(alloc::format!(
9520 "expected integer after {what} LIMIT, got {tok:?}"
9521 )));
9522 };
9523 // MySQL rejects the `LIMIT offset, count` form here — only a
9524 // single row count is legal on a DML statement.
9525 if matches!(self.peek(), Token::Comma) {
9526 return Err(self.err(alloc::format!(
9527 "{what} LIMIT takes a row count, not an offset"
9528 )));
9529 }
9530 let n = u32::try_from(n)
9531 .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9532 Some(n)
9533 } else {
9534 None
9535 };
9536 if order_by.is_empty() && limit.is_none() {
9537 return Ok(None);
9538 }
9539 Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9540 order_by,
9541 limit,
9542 })))
9543 }
9544
9545 /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9546 /// the leading `DELETE` ident.
9547 fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9548 // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9549 // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9550 // USING a, b WHERE …` — the third MySQL spelling — needs no special
9551 // parse here; it reaches the existing USING path with the target
9552 // repeated in the list, which the source-list peel below handles.)
9553 // More than one name is a multi-TARGET delete, which SPG does not
9554 // model; it is refused rather than half-applied.
9555 let mysql_pre_target: Option<String> =
9556 if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9557 let first = self.expect_ident_like()?;
9558 if matches!(self.peek(), Token::Comma) {
9559 return Err(self.err(alloc::format!(
9560 "multi-table DELETE can only delete from one table; \
9561 `DELETE {first}, …` names several"
9562 )));
9563 }
9564 Some(first)
9565 } else {
9566 None
9567 };
9568 if !matches!(self.peek(), Token::From) {
9569 return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9570 }
9571 self.advance();
9572 // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9573 // lookahead as the UPDATE spelling.
9574 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9575 if s.eq_ignore_ascii_case("only"))
9576 && matches!(
9577 self.tokens.get(self.pos + 1),
9578 Some(Token::Ident(_) | Token::QuotedIdent(_))
9579 );
9580 if only {
9581 self.advance();
9582 }
9583 let table = self.expect_ident_like()?;
9584 // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9585 // spelling must not swallow the clause keywords that can follow
9586 // the target.
9587 let alias = if matches!(self.peek(), Token::As) {
9588 self.advance();
9589 Some(self.expect_ident_like()?)
9590 } else {
9591 match self.peek() {
9592 Token::Ident(s) | Token::QuotedIdent(s)
9593 if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9594 {
9595 let a = s.clone();
9596 self.advance();
9597 Some(a)
9598 }
9599 _ => None,
9600 }
9601 };
9602 // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9603 // through the SAME join grammar the FROM clause uses (see the
9604 // `advance()`-destroys-tokens note on `parse_from_joins`).
9605 let mut mysql_on: Option<Expr> = None;
9606 let mut mysql_outer = false;
9607 let mysql_using = if mysql_pre_target.is_some()
9608 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9609 {
9610 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9611 let mut joins = self.parse_from_joins(&target_qual)?;
9612 if joins.is_empty() {
9613 return Err(self.err(alloc::string::String::from(
9614 "multi-table DELETE needs at least one source table",
9615 )));
9616 }
9617 let head = joins.remove(0);
9618 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9619 mysql_on = head.on;
9620 Some(FromClause {
9621 primary: head.table,
9622 joins,
9623 })
9624 } else {
9625 None
9626 };
9627 // The pre-FROM target must be the table the FROM names (or its
9628 // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9629 // is not the scan target.
9630 if let Some(t) = &mysql_pre_target {
9631 let names_target = t.eq_ignore_ascii_case(&table)
9632 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9633 if !names_target {
9634 return Err(self.err(alloc::format!(
9635 "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9636 )));
9637 }
9638 }
9639 // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9640 // delete. Same lowering as UPDATE … FROM: the WHERE
9641 // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9642 // target row by the correlated machinery.
9643 let using_clause = if let Some(fc) = mysql_using {
9644 Some(fc)
9645 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9646 self.advance();
9647 let mut fc = self.parse_from_clause()?;
9648 // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9649 // repeats the TARGET as the first USING entry (PG's spelling
9650 // lists only the extra sources). Peel it so the source subquery
9651 // does not re-scan — and shadow — the target table.
9652 let primary_is_target =
9653 fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9654 if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9655 let head = fc.joins.remove(0);
9656 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9657 mysql_on = head.on;
9658 fc = FromClause {
9659 primary: head.table,
9660 joins: fc.joins,
9661 };
9662 }
9663 Some(fc)
9664 } else {
9665 None
9666 };
9667 let where_ = if matches!(self.peek(), Token::Where) {
9668 self.advance();
9669 Some(self.parse_expr(0)?)
9670 } else {
9671 None
9672 };
9673 // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9674 // read before RETURNING (MariaDB's own extension trails the LIMIT).
9675 let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9676 let mut returning = self.parse_optional_returning()?;
9677 let where_ = if let Some(fc) = using_clause {
9678 // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9679 // a USING-table reference in RETURNING becomes a correlated
9680 // scalar subquery over the USING list.
9681 let names: Vec<String> = core::iter::once(&fc.primary)
9682 .chain(fc.joins.iter().map(|j| &j.table))
9683 .flat_map(|t| {
9684 t.alias
9685 .clone()
9686 .into_iter()
9687 .chain(core::iter::once(t.name.clone()))
9688 })
9689 .collect();
9690 // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9691 // join filters the SOURCE subquery on the ON predicate alone and
9692 // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9693 // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9694 // rows); every other form folds ON and WHERE into one EXISTS.
9695 let sub_where = match (mysql_on.clone(), where_.clone()) {
9696 _ if mysql_outer => mysql_on.clone(),
9697 (Some(on), Some(w)) => Some(Expr::Binary {
9698 lhs: Box::new(on),
9699 op: crate::ast::BinOp::And,
9700 rhs: Box::new(w),
9701 }),
9702 (Some(on), None) => Some(on),
9703 (None, w) => w,
9704 };
9705 let exists_where = sub_where.clone();
9706 let sub_fc = fc.clone();
9707 let make_subq = move |leaf: Expr| -> Expr {
9708 Expr::ScalarSubquery(Box::new(SelectStatement {
9709 locking: None,
9710 ctes: Vec::new(),
9711 distinct: false,
9712 distinct_on: Vec::new(),
9713 items: alloc::vec![SelectItem::Expr {
9714 expr: leaf,
9715 alias: None,
9716 }],
9717 from: Some(sub_fc.clone()),
9718 where_: sub_where.clone(),
9719 group_by: None,
9720 group_by_all: false,
9721 having: None,
9722 unions: Vec::new(),
9723 order_by: Vec::new(),
9724 limit: None,
9725 offset: None,
9726 limit_with_ties: false,
9727 window_check_exprs: Vec::new(),
9728 }))
9729 };
9730 let refs = |e: &Expr| expr_refs_tables(e, &names);
9731 if let Some(items) = returning.as_mut() {
9732 for item in items.iter_mut() {
9733 if let SelectItem::Expr { expr, .. } = item {
9734 wrap_from_leaves(expr, &names, &make_subq, &refs);
9735 }
9736 }
9737 }
9738 // A LEFT join deletes the target rows the WHERE selects, reading
9739 // source columns through the correlated subquery (NULL when
9740 // unmatched); no EXISTS row filter.
9741 if mysql_outer {
9742 let mut outer = where_;
9743 if let Some(w) = outer.as_mut() {
9744 wrap_from_leaves(w, &names, &make_subq, &refs);
9745 }
9746 outer
9747 } else {
9748 Some(Expr::Exists {
9749 subquery: Box::new(SelectStatement {
9750 locking: None,
9751 ctes: Vec::new(),
9752 distinct: false,
9753 distinct_on: Vec::new(),
9754 items: alloc::vec![SelectItem::Expr {
9755 expr: Expr::Literal(Literal::Integer(1)),
9756 alias: None,
9757 }],
9758 from: Some(fc),
9759 where_: exists_where,
9760 group_by: None,
9761 group_by_all: false,
9762 having: None,
9763 unions: Vec::new(),
9764 order_by: Vec::new(),
9765 limit: None,
9766 offset: None,
9767 limit_with_ties: false,
9768 window_check_exprs: Vec::new(),
9769 }),
9770 negated: false,
9771 })
9772 }
9773 } else {
9774 where_
9775 };
9776 Ok(Statement::Delete(crate::ast::DeleteStatement {
9777 ctes: Vec::new(),
9778 table,
9779 only,
9780 alias,
9781 where_,
9782 order_limit: delete_order_limit,
9783 returning,
9784 }))
9785 }
9786
9787 /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
9788 /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
9789 /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
9790 /// keyword. v7.17 surface:
9791 /// * source: table reference (subquery source is a follow-up)
9792 /// * actions: UPDATE SET / DELETE / DO NOTHING (matched);
9793 /// INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
9794 /// * AND-conditioned WHEN clauses; clauses tried in declaration
9795 /// order
9796 fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
9797 // INTO
9798 let is_into_kw = matches!(self.peek(), Token::Into)
9799 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
9800 if !is_into_kw {
9801 return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
9802 }
9803 self.advance();
9804 let target = self.expect_ident_like()?;
9805 // Optional alias — bare ident before USING.
9806 let target_alias = match self.peek() {
9807 Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
9808 Some(self.expect_ident_like()?)
9809 }
9810 _ => None,
9811 };
9812 // USING
9813 let is_using_kw = matches!(
9814 self.peek(),
9815 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
9816 );
9817 if !is_using_kw {
9818 return Err(self.err(format!(
9819 "expected USING after MERGE INTO target, got {:?}",
9820 self.peek()
9821 )));
9822 }
9823 self.advance();
9824 // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
9825 // <table> [alias]`. PG requires an alias after a subquery source.
9826 let (source, source_select) = if matches!(self.peek(), Token::LParen) {
9827 self.advance(); // (
9828 // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
9829 // constant-SELECT lowering the derived-table parser uses
9830 // (PG deletes through this form; it was a parse error).
9831 let inner = if matches!(self.peek(), Token::Values) {
9832 self.advance(); // VALUES
9833 Statement::Select(self.parse_values_rows_body()?)
9834 } else {
9835 self.parse_select_stmt()?
9836 };
9837 match self.advance() {
9838 Token::RParen => {}
9839 other => {
9840 return Err(self.err(format!(
9841 "expected ')' after MERGE USING subquery, got {other:?}"
9842 )));
9843 }
9844 }
9845 let Statement::Select(sub) = inner else {
9846 return Err(self.err("MERGE USING subquery must be a SELECT".into()));
9847 };
9848 (String::new(), Some(Box::new(sub)))
9849 } else {
9850 (self.expect_ident_like()?, None)
9851 };
9852 let source_alias = match self.peek() {
9853 Token::Ident(s) | Token::QuotedIdent(s)
9854 if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
9855 {
9856 Some(self.expect_ident_like()?)
9857 }
9858 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
9859 self.advance(); // AS
9860 Some(self.expect_ident_like()?)
9861 }
9862 _ => None,
9863 };
9864 // v7.39 (round 768, F31-D5) — optional positional column-alias
9865 // list after the source alias (`s(id, v)`).
9866 let mut source_column_aliases: Vec<String> = Vec::new();
9867 if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
9868 self.advance();
9869 loop {
9870 source_column_aliases.push(self.expect_ident_like()?);
9871 match self.peek() {
9872 Token::Comma => {
9873 self.advance();
9874 }
9875 Token::RParen => {
9876 self.advance();
9877 break;
9878 }
9879 other => {
9880 return Err(self.err(format!(
9881 "expected ',' or ')' in MERGE source column list, got {other:?}"
9882 )));
9883 }
9884 }
9885 }
9886 }
9887 if source_select.is_some() && source_alias.is_none() {
9888 return Err(self.err("MERGE USING (subquery) requires an alias".into()));
9889 }
9890 // ON
9891 if !matches!(self.peek(), Token::On) {
9892 return Err(self.err(format!(
9893 "expected ON after MERGE … USING source, got {:?}",
9894 self.peek()
9895 )));
9896 }
9897 self.advance();
9898 let on = self.parse_expr(0)?;
9899 // One or more WHEN clauses.
9900 let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
9901 loop {
9902 let is_when_kw = matches!(
9903 self.peek(),
9904 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
9905 );
9906 if !is_when_kw {
9907 break;
9908 }
9909 self.advance(); // WHEN
9910 // [NOT] MATCHED
9911 let matched = if matches!(self.peek(), Token::Not) {
9912 self.advance();
9913 crate::ast::MergeMatched::NotMatched
9914 } else {
9915 crate::ast::MergeMatched::Matched
9916 };
9917 let is_matched_kw = matches!(
9918 self.peek(),
9919 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
9920 );
9921 if !is_matched_kw {
9922 return Err(self.err(format!(
9923 "expected MATCHED in WHEN clause, got {:?}",
9924 self.peek()
9925 )));
9926 }
9927 self.advance();
9928 // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
9929 // BY TARGET is the default (a synonym); BY SOURCE flips the clause
9930 // to fire for target rows no source row matches.
9931 let mut matched = matched;
9932 if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
9933 self.advance();
9934 match self.peek() {
9935 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
9936 self.advance();
9937 matched = crate::ast::MergeMatched::NotMatchedBySource;
9938 }
9939 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
9940 self.advance();
9941 }
9942 other => {
9943 return Err(self.err(format!(
9944 "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
9945 )));
9946 }
9947 }
9948 }
9949 // Optional AND <expr>
9950 let condition = if matches!(self.peek(), Token::And) {
9951 self.advance();
9952 Some(self.parse_expr(0)?)
9953 } else {
9954 None
9955 };
9956 // THEN
9957 let is_then_kw = matches!(
9958 self.peek(),
9959 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
9960 );
9961 if !is_then_kw {
9962 return Err(self.err(format!(
9963 "expected THEN in WHEN clause, got {:?}",
9964 self.peek()
9965 )));
9966 }
9967 self.advance();
9968 // Action: INSERT / UPDATE / DELETE / DO NOTHING
9969 let action = match self.peek().clone() {
9970 Token::Insert => {
9971 self.advance();
9972 // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
9973 // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
9974 // VALUES (…)` omits it and fills every column in declaration
9975 // order. PG accepts this; SPG used to require the list.
9976 let mut columns: Vec<String> = Vec::new();
9977 if matches!(self.peek(), Token::LParen) {
9978 self.advance();
9979 loop {
9980 columns.push(self.expect_ident_like()?);
9981 if matches!(self.peek(), Token::Comma) {
9982 self.advance();
9983 continue;
9984 }
9985 break;
9986 }
9987 if !matches!(self.peek(), Token::RParen) {
9988 return Err(self.err(format!(
9989 "expected ')' after INSERT column list, got {:?}",
9990 self.peek()
9991 )));
9992 }
9993 self.advance();
9994 }
9995 // VALUES (...)
9996 if !matches!(self.peek(), Token::Values) {
9997 return Err(self.err(format!(
9998 "expected VALUES in MERGE INSERT, got {:?}",
9999 self.peek()
10000 )));
10001 }
10002 self.advance();
10003 if !matches!(self.peek(), Token::LParen) {
10004 return Err(self.err(format!(
10005 "expected '(' after VALUES in MERGE INSERT, got {:?}",
10006 self.peek()
10007 )));
10008 }
10009 self.advance();
10010 let mut values: Vec<crate::ast::Expr> = Vec::new();
10011 loop {
10012 values.push(self.parse_expr(0)?);
10013 if matches!(self.peek(), Token::Comma) {
10014 self.advance();
10015 continue;
10016 }
10017 break;
10018 }
10019 if !matches!(self.peek(), Token::RParen) {
10020 return Err(self.err(format!(
10021 "expected ')' after MERGE INSERT values, got {:?}",
10022 self.peek()
10023 )));
10024 }
10025 self.advance();
10026 // Empty column list = positional into every column, so the
10027 // count is checked against the table arity at execution.
10028 if !columns.is_empty() && columns.len() != values.len() {
10029 return Err(self.err(format!(
10030 "MERGE INSERT column count ({}) ≠ value count ({})",
10031 columns.len(),
10032 values.len()
10033 )));
10034 }
10035 crate::ast::MergeAction::Insert { columns, values }
10036 }
10037 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
10038 self.advance();
10039 // SET
10040 let is_set_kw = matches!(
10041 self.peek(),
10042 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
10043 );
10044 if !is_set_kw {
10045 return Err(self.err(format!(
10046 "expected SET after UPDATE in MERGE, got {:?}",
10047 self.peek()
10048 )));
10049 }
10050 self.advance();
10051 let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
10052 loop {
10053 let col = self.expect_ident_like()?;
10054 if !matches!(self.peek(), Token::Eq) {
10055 return Err(self.err(format!(
10056 "expected '=' in MERGE UPDATE assignment, got {:?}",
10057 self.peek()
10058 )));
10059 }
10060 self.advance();
10061 let expr = self.parse_expr(0)?;
10062 assignments.push((col, expr));
10063 if matches!(self.peek(), Token::Comma) {
10064 self.advance();
10065 continue;
10066 }
10067 break;
10068 }
10069 crate::ast::MergeAction::Update { assignments }
10070 }
10071 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
10072 self.advance();
10073 crate::ast::MergeAction::Delete
10074 }
10075 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
10076 self.advance();
10077 let is_nothing_kw = matches!(
10078 self.peek(),
10079 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
10080 );
10081 if !is_nothing_kw {
10082 return Err(self.err(format!(
10083 "expected NOTHING after DO in MERGE clause, got {:?}",
10084 self.peek()
10085 )));
10086 }
10087 self.advance();
10088 crate::ast::MergeAction::DoNothing
10089 }
10090 other => {
10091 return Err(self.err(format!(
10092 "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
10093 )));
10094 }
10095 };
10096 // PG's grammar simply has no INSERT production under BY SOURCE
10097 // (a target row already exists there) — same syntax error.
10098 if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
10099 && matches!(action, crate::ast::MergeAction::Insert { .. })
10100 {
10101 return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
10102 }
10103 clauses.push(crate::ast::MergeWhenClause {
10104 matched,
10105 condition,
10106 action,
10107 });
10108 }
10109 if clauses.is_empty() {
10110 return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
10111 }
10112 // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
10113 // unconditional (no `AND`) WHEN of the same match kind: it could
10114 // never fire. Check per match kind in clause order.
10115 let mut seen_unconditional_matched = false;
10116 let mut seen_unconditional_not_matched = false;
10117 let mut seen_unconditional_by_source = false;
10118 for c in &clauses {
10119 let seen = match c.matched {
10120 crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10121 crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10122 crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10123 };
10124 if *seen {
10125 return Err(self.err(String::from(
10126 "unreachable WHEN clause specified after unconditional WHEN clause",
10127 )));
10128 }
10129 if c.condition.is_none() {
10130 *seen = true;
10131 }
10132 }
10133 // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10134 let returning = self.parse_optional_returning()?;
10135 Ok(Statement::Merge(crate::ast::MergeStatement {
10136 // Attached by `parse_with_cte_then_select` when the MERGE
10137 // heads a WITH clause (round 149).
10138 ctes: Vec::new(),
10139 target,
10140 target_alias,
10141 source,
10142 source_alias,
10143 source_select,
10144 source_column_aliases,
10145 on,
10146 clauses,
10147 returning,
10148 }))
10149 }
10150
10151 /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10152 /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10153 /// as SELECT, so `RETURNING *`, `RETURNING col`,
10154 /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10155 fn parse_optional_returning(
10156 &mut self,
10157 ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10158 let is_returning_kw = matches!(
10159 self.peek(),
10160 Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10161 );
10162 if !is_returning_kw {
10163 return Ok(None);
10164 }
10165 self.advance();
10166 let mut items = Vec::new();
10167 loop {
10168 items.push(self.parse_select_item()?);
10169 if matches!(self.peek(), Token::Comma) {
10170 self.advance();
10171 continue;
10172 }
10173 break;
10174 }
10175 Ok(Some(items))
10176 }
10177
10178 /// v6.0.4 — parse the tail of an ALTER statement after the
10179 /// leading `ALTER` keyword has been consumed. Only one form is
10180 /// supported in v6.0.4:
10181 ///
10182 /// ```text
10183 /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10184 /// ```
10185 fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10186 // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10187 // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10188 // exclusion) is accepted by stripping the `ONLY` keyword
10189 // before the table parse.
10190 // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10191 // and the long PG-dump tail are accepted as no-ops.
10192 match self.advance() {
10193 Token::Index => {}
10194 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10195 // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10196 // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10197 Token::Table => {
10198 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10199 self.advance();
10200 }
10201 return self.parse_alter_table_after_keyword();
10202 }
10203 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10204 return self.parse_alter_policy_after_keyword();
10205 }
10206 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10207 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10208 self.advance();
10209 }
10210 return self.parse_alter_table_after_keyword();
10211 }
10212 // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10213 // of the silent-noop tail.
10214 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10215 return self.parse_alter_sequence_after_keyword();
10216 }
10217 // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10218 // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10219 // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10220 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10221 // NB: the match arm consumed `TYPE` via self.advance(); the
10222 // cursor is now at the type name — do NOT advance again.
10223 let type_name = self.expect_ident_like()?;
10224 let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10225 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10226 if is_add_value {
10227 self.advance(); // ADD
10228 self.advance(); // VALUE
10229 // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10230 // IF/EXISTS as identifiers.
10231 let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10232 {
10233 let n1 = self.tokens.get(self.pos + 1);
10234 let n2 = self.tokens.get(self.pos + 2);
10235 if matches!(n1, Some(Token::Not))
10236 && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10237 {
10238 self.advance();
10239 self.advance();
10240 self.advance();
10241 true
10242 } else {
10243 false
10244 }
10245 } else {
10246 false
10247 };
10248 let label = self.expect_string_literal()?;
10249 let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10250 {
10251 let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10252 self.advance();
10253 let anchor = self.expect_string_literal()?;
10254 Some((is_before, anchor))
10255 } else {
10256 None
10257 };
10258 return Ok(Statement::AlterTypeAddValue {
10259 type_name,
10260 label,
10261 if_not_exists,
10262 position,
10263 });
10264 }
10265 // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10266 // Used to fall into the no-op tail below: accepted, silently
10267 // ignored. `RENAME TO <newtype>` keeps falling through.
10268 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10269 && matches!(
10270 self.tokens.get(self.pos + 1),
10271 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10272 )
10273 {
10274 self.advance(); // RENAME
10275 self.advance(); // VALUE
10276 let old = self.expect_string_literal()?;
10277 if matches!(self.peek(), Token::To) {
10278 self.advance();
10279 } else {
10280 self.expect_keyword_ident("to")?;
10281 }
10282 let new = self.expect_string_literal()?;
10283 return Ok(Statement::AlterTypeRenameValue {
10284 type_name,
10285 old,
10286 new,
10287 });
10288 }
10289 // Other ALTER TYPE forms — the ACTION stays a no-op
10290 // (pg_dump tail), but v7.39 (round 708) the NAME is
10291 // validated: `ALTER TYPE nosuch RENAME TO x` reported
10292 // success for a type that does not exist.
10293 self.consume_until_statement_boundary();
10294 return Ok(Statement::ValidateOnly {
10295 kind: crate::ast::ValidateOnlyKind::TypeName,
10296 names: alloc::vec![type_name],
10297 });
10298 }
10299 // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10300 // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10301 // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10302 // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10303 // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10304 // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10305 // pg_dump no-op list below: every form used to report success
10306 // and change nothing, which is worse than refusing outright
10307 // (a migration dropping a constraint kept rejecting data).
10308 // NOTE: the enclosing `match self.advance()` already consumed
10309 // the DOMAIN keyword, so the name is next.
10310 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10311 return self.parse_alter_domain_after_keyword();
10312 }
10313 // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10314 // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10315 // used to fall into the pg_dump no-op tail below, so a DBA
10316 // setting a per-role default was told it worked and nothing
10317 // happened. Intercepted here, BEFORE that tail.
10318 // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10319 // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10320 // interception below exists: swallowed with the no-op tail, an
10321 // unknown parameter name was ACCEPTED where PG18 answers
10322 // `unrecognized configuration parameter`. SPG applies nothing
10323 // either way — there is no postgresql.auto.conf — but it now
10324 // says so about a name it does not know.
10325 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10326 // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10327 // already consumed here. An extra advance eats the SET and
10328 // the parameter name is never seen — which is exactly the
10329 // bug a panic in this branch disproved: the branch WAS on
10330 // the path, the reading of it was wrong.
10331 let mut parameter = None;
10332 // SET <name> … | RESET <name> | RESET ALL
10333 if matches!(self.peek(), Token::Ident(k)
10334 if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10335 {
10336 self.advance();
10337 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10338 && !n.eq_ignore_ascii_case("all")
10339 {
10340 self.advance();
10341 // A dotted GUC (`plpgsql.check_asserts`) is two
10342 // tokens; keep the whole name.
10343 let mut full = n;
10344 while matches!(self.peek(), Token::Dot) {
10345 self.advance();
10346 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10347 full.push('.');
10348 full.push_str(&t);
10349 }
10350 }
10351 parameter = Some(full);
10352 }
10353 }
10354 self.consume_until_statement_boundary();
10355 return Ok(Statement::AlterSystem { parameter });
10356 }
10357 Token::Ident(s) | Token::QuotedIdent(s)
10358 if matches!(
10359 s.to_ascii_lowercase().as_str(),
10360 "role" | "user" | "database"
10361 ) && self.peeks_db_role_setting() =>
10362 {
10363 let is_database = s.eq_ignore_ascii_case("database");
10364 return self.parse_db_role_setting(is_database);
10365 }
10366 // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10367 // (the non-SET forms; SET/RESET took the branch above). The
10368 // attributes still no-op — recorded, and the ignored PASSWORD
10369 // is ledgered as its own follow-up — but the ROLE is validated:
10370 // any name was accepted for a role that does not exist.
10371 Token::Ident(s) | Token::QuotedIdent(s)
10372 if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10373 {
10374 // NB: the enclosing `match self.advance()` already consumed
10375 // ROLE/USER — the round-695 trap, hit again in this round's
10376 // first draft (the name was eaten and WITH parsed as the
10377 // role). The cursor is at the name.
10378 let name = self.expect_ident_or_string()?;
10379 // v7.39 (round 750) — scan the attribute tail for
10380 // PASSWORD. Everything else stays a recorded no-op, but
10381 // a dropped credential rotation is a SECURITY bug:
10382 // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10383 // changed nothing, so the old password kept working.
10384 // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10385 // NULL` clears the credential.
10386 let mut password: Option<Option<String>> = None;
10387 loop {
10388 match self.peek() {
10389 Token::Semicolon | Token::Eof => break,
10390 Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10391 self.advance();
10392 match self.advance() {
10393 Token::String(p) => password = Some(Some(p)),
10394 Token::Null => password = Some(None),
10395 Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10396 password = Some(None);
10397 }
10398 other => {
10399 return Err(self.err(alloc::format!(
10400 "expected password string or NULL after PASSWORD, got {other:?}"
10401 )));
10402 }
10403 }
10404 }
10405 _ => {
10406 self.advance();
10407 }
10408 }
10409 }
10410 if name.eq_ignore_ascii_case("all") {
10411 // `ALTER ROLE ALL …` names every role; nothing to check.
10412 return Ok(Statement::Empty);
10413 }
10414 if let Some(pw) = password {
10415 return Ok(Statement::AlterRolePassword { name, password: pw });
10416 }
10417 return Ok(Statement::ValidateOnly {
10418 kind: crate::ast::ValidateOnlyKind::RoleName,
10419 names: alloc::vec![name],
10420 });
10421 }
10422 // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10423 // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10424 // list far enough to validate the NAME; the actions still no-op.
10425 // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10426 // models none of them and their dumps are rare.)
10427 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10428 let name = self.expect_ident_or_string()?;
10429 self.consume_until_statement_boundary();
10430 return Ok(Statement::ValidateOnly {
10431 kind: crate::ast::ValidateOnlyKind::CollationName,
10432 names: alloc::vec![name],
10433 });
10434 }
10435 Token::Ident(s) | Token::QuotedIdent(s)
10436 if s.eq_ignore_ascii_case("text")
10437 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10438 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10439 {
10440 self.advance(); // SEARCH
10441 self.advance(); // CONFIGURATION
10442 let name = self.expect_ident_like()?;
10443 self.consume_until_statement_boundary();
10444 return Ok(Statement::ValidateOnly {
10445 kind: crate::ast::ValidateOnlyKind::TsConfigName,
10446 names: alloc::vec![name],
10447 });
10448 }
10449 Token::Ident(s) | Token::QuotedIdent(s)
10450 if s.eq_ignore_ascii_case("event")
10451 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10452 {
10453 self.advance(); // TRIGGER
10454 let name = self.expect_ident_like()?;
10455 self.consume_until_statement_boundary();
10456 return Ok(Statement::ValidateOnly {
10457 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10458 names: alloc::vec![name],
10459 });
10460 }
10461 Token::Ident(s) | Token::QuotedIdent(s)
10462 if s.eq_ignore_ascii_case("large")
10463 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10464 {
10465 self.advance(); // OBJECT
10466 let oid = match self.advance() {
10467 Token::Integer(n) => alloc::format!("{n}"),
10468 other => {
10469 return Err(
10470 self.err(alloc::format!("expected large object oid, got {other:?}"))
10471 );
10472 }
10473 };
10474 self.consume_until_statement_boundary();
10475 return Ok(Statement::ValidateOnly {
10476 kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10477 names: alloc::vec![oid],
10478 });
10479 }
10480 // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10481 // argument-list parse as DROP AGGREGATE (round 707); the
10482 // action no-ops, the existence check is real.
10483 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10484 // Same round-695 trap as above: AGGREGATE is already
10485 // consumed; the cursor is at the name.
10486 let name = self.expect_ident_like()?;
10487 let mut names = alloc::vec![name];
10488 if matches!(self.peek(), Token::LParen) {
10489 self.advance();
10490 loop {
10491 match self.peek().clone() {
10492 Token::RParen => {
10493 self.advance();
10494 break;
10495 }
10496 Token::Star => {
10497 self.advance();
10498 names.push(String::from("*"));
10499 }
10500 Token::Comma => {
10501 self.advance();
10502 }
10503 _ => {
10504 let mut t = self.expect_ident_like()?;
10505 while let Token::Ident(nx) = self.peek() {
10506 let nx = nx.clone();
10507 self.advance();
10508 t.push(' ');
10509 t.push_str(&nx);
10510 }
10511 names.push(t);
10512 }
10513 }
10514 }
10515 }
10516 self.consume_until_statement_boundary();
10517 return Ok(Statement::ValidateOnly {
10518 kind: crate::ast::ValidateOnlyKind::AggregateName,
10519 names,
10520 });
10521 }
10522 Token::Ident(s) | Token::QuotedIdent(s)
10523 if matches!(
10524 s.to_ascii_lowercase().as_str(),
10525 "view"
10526 | "function"
10527 | "database"
10528 | "schema"
10529 | "owner"
10530 | "default"
10531 | "extension"
10532 | "materialized"
10533 | "publication"
10534 | "subscription"
10535 // v7.37.17 (17.6 siblings) — additional ALTER
10536 // targets pg_dump / pg_dumpall / operator DB
10537 // migration scripts commonly emit. SPG has
10538 // no matching machinery for any of these; the
10539 // parser accepts + Empty-returns so pg_dump
10540 // tail statements don't stall.
10541 | "tablespace"
10542 | "language"
10543 | "operator"
10544 | "conversion"
10545 | "statistics"
10546 | "server"
10547 | "foreign"
10548 // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10549 // / TEMPLATE (CONFIGURATION intercepted above).
10550 | "text"
10551 ) =>
10552 {
10553 self.consume_until_statement_boundary();
10554 return Ok(Statement::Empty);
10555 }
10556 other => {
10557 return Err(self.err(format!(
10558 "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10559 after ALTER, got {other:?}"
10560 )));
10561 }
10562 }
10563 // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10564 // (mailrs migrate-042 ships these). The presence of an
10565 // IF EXISTS makes the subsequent name lookup tolerate
10566 // a missing index — engine returns CommandOk no-op.
10567 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10568 let next = self.tokens.get(self.pos + 1);
10569 if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10570 self.advance();
10571 self.advance();
10572 true
10573 } else {
10574 false
10575 }
10576 } else {
10577 false
10578 };
10579 let name = self.expect_ident_like()?;
10580 // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10581 // Detect BEFORE the REBUILD path so the existing REBUILD
10582 // arm stays untouched.
10583 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10584 self.advance();
10585 if matches!(self.peek(), Token::To) {
10586 self.advance();
10587 } else {
10588 self.expect_keyword_ident("to")?;
10589 }
10590 let new = self.expect_ident_like()?;
10591 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10592 name,
10593 target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10594 }));
10595 }
10596 // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10597 // A syntax error before; the index is validated, the params no-op.
10598 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10599 || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10600 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10601 {
10602 self.consume_until_statement_boundary();
10603 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10604 name,
10605 target: crate::ast::AlterIndexTarget::StorageParams,
10606 }));
10607 }
10608 // REBUILD
10609 self.expect_keyword_ident("rebuild")?;
10610 // Optional: WITH (encoding = <enc>)
10611 let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10612 self.advance();
10613 if !matches!(self.peek(), Token::LParen) {
10614 return Err(self.err(format!(
10615 "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10616 self.peek()
10617 )));
10618 }
10619 self.advance();
10620 self.expect_keyword_ident("encoding")?;
10621 if !matches!(self.peek(), Token::Eq) {
10622 return Err(self.err(format!(
10623 "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10624 self.peek()
10625 )));
10626 }
10627 self.advance();
10628 let enc_ident = match self.advance() {
10629 Token::Ident(s) | Token::QuotedIdent(s) => s,
10630 other => {
10631 return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10632 }
10633 };
10634 let enc = match enc_ident.to_ascii_lowercase().as_str() {
10635 "f32" => VecEncoding::F32,
10636 "sq8" => VecEncoding::Sq8,
10637 "half" => VecEncoding::F16,
10638 other => {
10639 return Err(self.err(format!(
10640 "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10641 )));
10642 }
10643 };
10644 if !matches!(self.peek(), Token::RParen) {
10645 return Err(self.err(format!(
10646 "expected ')' after encoding value, got {:?}",
10647 self.peek()
10648 )));
10649 }
10650 self.advance();
10651 Some(enc)
10652 } else {
10653 None
10654 };
10655 Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10656 name,
10657 target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10658 }))
10659 }
10660
10661 /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10662 /// only `SET` form currently supported; future v6.7.x can add
10663 /// more SET subjects without changing the dispatch shape.
10664 /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10665 /// subactions. Single-subaction shape stays a 1-element vec.
10666 fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10667 let table_name = self.expect_ident_like()?;
10668 let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10669 loop {
10670 let subaction = self.parse_alter_table_subaction()?;
10671 // ADD COLUMN with inline REFERENCES emits both an
10672 // AddColumn and an AddForeignKey subaction; the
10673 // helper returns 1 or 2 items.
10674 targets.extend(subaction);
10675 if matches!(self.peek(), Token::Comma) {
10676 self.advance();
10677 continue;
10678 }
10679 break;
10680 }
10681 Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10682 name: table_name,
10683 targets,
10684 }))
10685 }
10686
10687 /// Parse one ALTER TABLE subaction. Returns a Vec because
10688 /// inline `REFERENCES` on `ADD COLUMN` produces both an
10689 /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10690 fn parse_alter_table_subaction(
10691 &mut self,
10692 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10693 match self.peek() {
10694 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10695 self.advance();
10696 // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10697 // storage parameters: paren-prefixed; consume.
10698 if matches!(self.peek(), Token::LParen) {
10699 self.consume_until_statement_boundary();
10700 return Ok(Vec::new());
10701 }
10702 let setting = self.expect_ident_like()?;
10703 if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10704 if !matches!(self.peek(), Token::Eq) {
10705 return Err(self.err(alloc::format!(
10706 "expected '=' after hot_tier_bytes, got {:?}",
10707 self.peek()
10708 )));
10709 }
10710 self.advance();
10711 let n = self.expect_u64_literal()?;
10712 return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10713 }
10714 // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10715 // accept-and-no-op for ALTER TABLE SET <subject>
10716 // forms that pg_dump emits but SPG either treats
10717 // as N/A (single-tenant, single-owner, no shared
10718 // tablespaces) or accepts the dump-side declaration
10719 // without runtime effect:
10720 // SET SCHEMA <name> (18.11)
10721 // SET TABLESPACE <name> (18.8)
10722 // SET LOGGED / UNLOGGED (18.7 alt-form)
10723 // SET WITHOUT CLUSTER (18.13)
10724 // SET WITHOUT OIDS (PG legacy)
10725 // SET (option = value, …) (storage parameters)
10726 // SET REPLICA IDENTITY {…} (18.14)
10727 if setting.eq_ignore_ascii_case("schema")
10728 || setting.eq_ignore_ascii_case("tablespace")
10729 || setting.eq_ignore_ascii_case("logged")
10730 || setting.eq_ignore_ascii_case("unlogged")
10731 || setting.eq_ignore_ascii_case("without")
10732 {
10733 self.consume_until_statement_boundary();
10734 return Ok(Vec::new());
10735 }
10736 if setting.eq_ignore_ascii_case("replica") {
10737 // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10738 self.consume_until_statement_boundary();
10739 return Ok(Vec::new());
10740 }
10741 // SET (option=value, …) — storage parameters.
10742 if matches!(self.peek(), Token::LParen) {
10743 self.consume_until_statement_boundary();
10744 return Ok(Vec::new());
10745 }
10746 Err(self.err(alloc::format!(
10747 "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10748 hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10749 WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10750 )))
10751 }
10752 // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10753 // not ignored: round 645 gave SPG the inheritance the
10754 // v7.37.18 no-op said it did not have.
10755 Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10756 self.advance();
10757 let parent = self.expect_ident_like()?;
10758 self.consume_until_statement_boundary();
10759 Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10760 parent,
10761 detach: false
10762 }])
10763 }
10764 // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10765 // LEVEL SECURITY`, which has its own RLS arm below — without
10766 // the guard this swallowed NO FORCE as a no-op.
10767 Token::Ident(s)
10768 if s.eq_ignore_ascii_case("no")
10769 && !matches!(
10770 self.tokens.get(self.pos + 1),
10771 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10772 ) =>
10773 {
10774 self.advance();
10775 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
10776 if k.eq_ignore_ascii_case("inherit"))
10777 {
10778 self.advance();
10779 let parent = self.expect_ident_like()?;
10780 self.consume_until_statement_boundary();
10781 return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10782 parent,
10783 detach: true
10784 }]);
10785 }
10786 self.consume_until_statement_boundary();
10787 Ok(Vec::new())
10788 }
10789 // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
10790 // single-owner, so there is still nothing to record.
10791 //
10792 // v7.39 (round 652) — but the name now reaches the engine,
10793 // which refuses a role that does not exist as PG does. The
10794 // no-op was swallowing the whole statement, so a dump naming
10795 // a role this server never heard of restored clean and left
10796 // the table owned by whoever ran the restore.
10797 Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
10798 self.advance();
10799 if matches!(self.peek(), Token::To) {
10800 self.advance();
10801 }
10802 let role = self.expect_ident_like()?;
10803 Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
10804 role
10805 }])
10806 }
10807 // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
10808 // PG sets a hint; SPG doesn't have clustered storage, so the
10809 // hint itself stays a no-op.
10810 //
10811 // v7.39 (round 652) — the index name is checked now. PG
10812 // errors on one that does not exist, and swallowing that let
10813 // a typo'd CLUSTER ON pass silently.
10814 Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
10815 self.advance();
10816 // `ON` is a reserved token, not an ident.
10817 if !matches!(self.peek(), Token::On) {
10818 return Err(self.err(alloc::format!(
10819 "expected ON after CLUSTER, got {:?}",
10820 self.peek()
10821 )));
10822 }
10823 self.advance();
10824 let index = self.expect_ident_like()?;
10825 Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
10826 index: Some(index)
10827 }])
10828 }
10829 // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
10830 // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
10831 // what a logical decoder puts in the old-tuple image; SPG's
10832 // replication is SQL-text, so there is nothing to record.
10833 // Accept-and-no-op (it used to be a parse error).
10834 Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
10835 self.advance();
10836 // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
10837 // validates the index; DEFAULT / FULL / NOTHING stay no-op.
10838 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
10839 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
10840 {
10841 self.advance(); // IDENTITY
10842 self.advance(); // USING
10843 if matches!(self.peek(), Token::Index)
10844 || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
10845 {
10846 self.advance();
10847 }
10848 let index = self.expect_ident_like()?;
10849 self.consume_until_statement_boundary();
10850 return Ok(alloc::vec![
10851 crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
10852 ]);
10853 }
10854 self.consume_until_statement_boundary();
10855 Ok(Vec::new())
10856 }
10857 // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
10858 //
10859 // v7.39 (round 652) — it used to consume the statement and
10860 // return nothing, on the stated theory that SPG validated at
10861 // ADD CONSTRAINT time so there was never anything left to
10862 // validate. Measured against PG18, ADD CONSTRAINT did not
10863 // scan the existing rows at all — the comment described a
10864 // property SPG did not have, which is why nobody looked. Both
10865 // halves are real now: ADD scans unless told NOT VALID, and
10866 // this scans what NOT VALID skipped.
10867 Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
10868 self.advance();
10869 self.expect_keyword_ident("constraint")?;
10870 let name = self.expect_ident_like()?;
10871 Ok(alloc::vec![
10872 crate::ast::AlterTableTarget::ValidateConstraint { name }
10873 ])
10874 }
10875 // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
10876 // SET (option = value, …). PG uses it to clear per-table
10877 // storage params like fillfactor or autovacuum_*. SPG
10878 // engine-manages those parameters; accept-and-no-op.
10879 Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
10880 self.advance();
10881 self.consume_until_statement_boundary();
10882 Ok(Vec::new())
10883 }
10884 // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
10885 // type-of binding (PG 9.0+). SPG composite types
10886 // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
10887 // TABLE OF is rare and inverse of CREATE TABLE OF.
10888 // Accept-and-no-op until a customer dump round-trips it.
10889 Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
10890 self.advance();
10891 // v7.39 (round 710) — the type name is validated now.
10892 let type_name = self.expect_ident_like()?;
10893 self.consume_until_statement_boundary();
10894 Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
10895 type_name
10896 }])
10897 }
10898 // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
10899 // (reserved keyword) rather than Token::Ident("not"),
10900 // so it needs its own arm. Accept-and-no-op same as OF.
10901 Token::Not => {
10902 self.advance();
10903 self.consume_until_statement_boundary();
10904 Ok(Vec::new())
10905 }
10906 // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
10907 Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
10908 self.advance();
10909 self.expect_row_level_security()?;
10910 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10911 enabled: None,
10912 force: Some(true),
10913 }])
10914 }
10915 // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
10916 Token::Ident(s)
10917 if s.eq_ignore_ascii_case("no")
10918 && matches!(
10919 self.tokens.get(self.pos + 1),
10920 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10921 ) =>
10922 {
10923 self.advance(); // NO
10924 self.advance(); // FORCE
10925 self.expect_row_level_security()?;
10926 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10927 enabled: None,
10928 force: Some(false),
10929 }])
10930 }
10931 // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
10932 // (sets relrowsecurity). The guard requires the next token to be
10933 // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
10934 Token::Ident(s)
10935 if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
10936 && matches!(
10937 self.tokens.get(self.pos + 1),
10938 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
10939 ) =>
10940 {
10941 let enabled = s.eq_ignore_ascii_case("enable");
10942 self.advance(); // ENABLE/DISABLE
10943 self.expect_row_level_security()?;
10944 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10945 enabled: Some(enabled),
10946 force: None,
10947 }])
10948 }
10949 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
10950 self.advance();
10951 // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
10952 // {INDEX|KEY} [name] (cols)`, which every ORM migration
10953 // emits. The same grammar CREATE TABLE already accepts
10954 // inline (`KEY idx (a)`, prefix lengths and all), so it goes
10955 // through the SAME parser — an ALTER-only copy would be a
10956 // second place for the two to drift.
10957 if self.peek_mysql_inline_key_start() {
10958 return Ok(match self.parse_mysql_inline_key()? {
10959 Some(c) => {
10960 alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
10961 }
10962 // FULLTEXT / SPATIAL parse and are accepted as a
10963 // no-op here exactly as they are inline.
10964 None => Vec::new(),
10965 });
10966 }
10967 // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
10968 // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
10969 // PRIMARY KEY this way; mysqldump emits both.
10970 // Peek-only dispatch (no advance) — `advance()`
10971 // destructively replaces consumed tokens with Eof,
10972 // so saved-pos restore would land on Eofs.
10973 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
10974 {
10975 // The next-but-one ident is the constraint
10976 // name; the one after THAT is the kind.
10977 let kind_pos = self.pos + 2;
10978 let kind = self.tokens.get(kind_pos).cloned();
10979 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
10980 {
10981 let fk = self.parse_table_level_fk()?;
10982 return Ok(alloc::vec![
10983 crate::ast::AlterTableTarget::AddForeignKey(fk)
10984 ]);
10985 }
10986 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
10987 {
10988 self.advance(); // CONSTRAINT
10989 // v7.39 (read01 round 48) — keep the name; the engine
10990 // stores it now instead of dropping it on the floor.
10991 let con_name = self.expect_ident_like()?;
10992 self.advance(); // PRIMARY
10993 self.expect_keyword_ident("key")?;
10994 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10995 // v7.39 (round 711) — the ALTER form carries the
10996 // timing too (pg_dump writes it here).
10997 let (deferrable, initially_deferred) =
10998 self.consume_deferrable_clauses_timed()?;
10999 return Ok(alloc::vec![
11000 crate::ast::AlterTableTarget::AddTableConstraint(
11001 crate::ast::TableConstraint::PrimaryKey {
11002 name: Some(con_name),
11003 columns: cols,
11004 deferrable,
11005 initially_deferred,
11006 }
11007 )
11008 ]);
11009 }
11010 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
11011 {
11012 self.advance(); // CONSTRAINT
11013 // v7.39 (read01 round 48) — keep the name.
11014 let con_name = self.expect_ident_like()?;
11015 // v7.22 (mailrs round-13 gap 6) — delegate so
11016 // the optional `NULLS [NOT] DISTINCT` modifier
11017 // parses here too (pg_dump emits the ALTER
11018 // form; semantics enforced by the engine
11019 // since v7.13).
11020 let mut uc = self.parse_table_level_unique()?;
11021 if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
11022 *name = Some(con_name);
11023 }
11024 return Ok(alloc::vec![
11025 crate::ast::AlterTableTarget::AddTableConstraint(uc)
11026 ]);
11027 }
11028 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
11029 {
11030 self.advance(); // CONSTRAINT
11031 // v7.39 (read01 round 48) — keep the name.
11032 let con_name = self.expect_ident_like()?;
11033 self.advance(); // CHECK
11034 if !matches!(self.peek(), Token::LParen) {
11035 return Err(self.err(alloc::format!(
11036 "expected '(' after CHECK, got {:?}", self.peek()
11037 )));
11038 }
11039 self.advance();
11040 let expr = self.parse_expr(0)?;
11041 if matches!(self.peek(), Token::RParen) {
11042 self.advance();
11043 }
11044 let not_valid = self.parse_not_valid_suffix();
11045 return Ok(alloc::vec![
11046 crate::ast::AlterTableTarget::AddTableConstraint(
11047 crate::ast::TableConstraint::Check {
11048 name: Some(con_name),
11049 expr,
11050 not_valid,
11051 }
11052 )
11053 ]);
11054 }
11055 // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
11056 // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
11057 // exclusion constraints via this ALTER form.
11058 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
11059 {
11060 self.advance(); // CONSTRAINT
11061 let con_name = self.expect_ident_like()?;
11062 let mut ex = self.parse_table_level_exclude()?;
11063 if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
11064 *name = Some(con_name);
11065 }
11066 return Ok(alloc::vec![
11067 crate::ast::AlterTableTarget::AddTableConstraint(ex)
11068 ]);
11069 }
11070 // Unknown kind — fall through to FK path which
11071 // produces a descriptive parse error.
11072 }
11073 let is_fk = matches!(
11074 self.peek(),
11075 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
11076 || s.eq_ignore_ascii_case("foreign")
11077 );
11078 if is_fk {
11079 let fk = self.parse_table_level_fk()?;
11080 return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
11081 }
11082 // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
11083 // (no CONSTRAINT prefix) — same dispatch.
11084 match self.peek().clone() {
11085 Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
11086 self.advance();
11087 self.expect_keyword_ident("key")?;
11088 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11089 let (deferrable, initially_deferred) =
11090 self.consume_deferrable_clauses_timed()?;
11091 return Ok(alloc::vec![
11092 crate::ast::AlterTableTarget::AddTableConstraint(
11093 crate::ast::TableConstraint::PrimaryKey {
11094 name: None,
11095 columns: cols,
11096 deferrable,
11097 initially_deferred,
11098 }
11099 )
11100 ]);
11101 }
11102 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
11103 // v7.22 — delegate (NULLS [NOT] DISTINCT).
11104 let uc = self.parse_table_level_unique()?;
11105 return Ok(alloc::vec![
11106 crate::ast::AlterTableTarget::AddTableConstraint(uc)
11107 ]);
11108 }
11109 // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
11110 // prefix). The other three bare forms were here and
11111 // this one was not, so it fell through to the column
11112 // path and came back as "unexpected reserved keyword
11113 // 'check' at start of column definition".
11114 _ if self.peek_table_level_check_start() => {
11115 let chk = self.parse_table_level_check()?;
11116 let not_valid = self.parse_not_valid_suffix();
11117 let crate::ast::TableConstraint::Check { expr, .. } = chk else {
11118 unreachable!("parse_table_level_check returns Check")
11119 };
11120 return Ok(alloc::vec![
11121 crate::ast::AlterTableTarget::AddTableConstraint(
11122 crate::ast::TableConstraint::Check {
11123 name: None,
11124 expr,
11125 not_valid,
11126 }
11127 )
11128 ]);
11129 }
11130 // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11131 Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11132 let ex = self.parse_table_level_exclude()?;
11133 return Ok(alloc::vec![
11134 crate::ast::AlterTableTarget::AddTableConstraint(ex)
11135 ]);
11136 }
11137 _ => {}
11138 }
11139 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11140 self.advance();
11141 }
11142 let mut if_not_exists = false;
11143 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11144 self.advance();
11145 if !matches!(self.peek(), Token::Not) {
11146 return Err(self.err(alloc::format!(
11147 "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11148 self.peek()
11149 )));
11150 }
11151 self.advance();
11152 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11153 return Err(self.err(alloc::format!(
11154 "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11155 self.peek()
11156 )));
11157 }
11158 self.advance();
11159 if_not_exists = true;
11160 }
11161 // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11162 // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11163 // returns ColumnDef + an optional inline FK.
11164 let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11165 let col_name = column.name.clone();
11166 let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11167 column,
11168 if_not_exists,
11169 }];
11170 if let Some(mut fk) = col_level_fk {
11171 if fk.columns.is_empty() {
11172 fk.columns.push(col_name);
11173 }
11174 out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11175 }
11176 Ok(out)
11177 }
11178 Token::Drop => {
11179 self.advance();
11180 // v7.13.3 — dispatch on the next token. mailrs round-7
11181 // S8 closed DROP COLUMN; round-6 S7 closed
11182 // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11183 // RESTRICT modifiers.
11184 // DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11185 // DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11186 let subject = match self.peek() {
11187 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11188 self.advance();
11189 "constraint"
11190 }
11191 Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11192 self.advance();
11193 "column"
11194 }
11195 // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11196 // `INDEX` lexes as the reserved Token::Index, so it is
11197 // unambiguous. `KEY` is a plain ident, and PG allows a
11198 // column literally named "key", so only read it as the
11199 // keyword when a name follows it.
11200 Token::Index => {
11201 self.advance();
11202 "index"
11203 }
11204 Token::Ident(s)
11205 if s.eq_ignore_ascii_case("key")
11206 && matches!(
11207 self.tokens.get(self.pos + 1),
11208 Some(Token::Ident(_) | Token::QuotedIdent(_))
11209 ) =>
11210 {
11211 self.advance();
11212 "index"
11213 }
11214 // PG-canonical bare `DROP <col>` without COLUMN
11215 // keyword is also valid; treat any other ident
11216 // as the column name.
11217 Token::Ident(_) | Token::QuotedIdent(_) => "column",
11218 other => {
11219 return Err(self.err(alloc::format!(
11220 "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11221 )));
11222 }
11223 };
11224 let mut if_exists = false;
11225 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11226 let n1 = self.tokens.get(self.pos + 1);
11227 if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11228 self.advance();
11229 self.advance();
11230 if_exists = true;
11231 }
11232 }
11233 let name = self.expect_ident_like()?;
11234 let mut cascade = false;
11235 if matches!(
11236 self.peek(),
11237 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11238 || s.eq_ignore_ascii_case("restrict")
11239 ) {
11240 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11241 {
11242 cascade = true;
11243 }
11244 self.advance();
11245 }
11246 if subject == "index" {
11247 Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11248 name,
11249 if_exists,
11250 }])
11251 } else if subject == "constraint" {
11252 Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11253 name,
11254 if_exists,
11255 }])
11256 } else {
11257 Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11258 column: name,
11259 if_exists,
11260 cascade,
11261 }])
11262 }
11263 }
11264 Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11265 self.advance();
11266 // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11267 // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11268 // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11269 // immediately; accept-and-no-op.
11270 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11271 self.advance();
11272 self.consume_until_statement_boundary();
11273 return Ok(Vec::new());
11274 }
11275 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11276 self.advance();
11277 }
11278 let col_name = self.expect_ident_like()?;
11279 match self.peek() {
11280 Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11281 self.advance();
11282 }
11283 // v7.14.0 — pg_dump emits BIGSERIAL via
11284 // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11285 // nextval('seq')` (the sequence is created
11286 // separately). SPG's BIGSERIAL already uses
11287 // AUTO_INCREMENT; accept SET DEFAULT / DROP
11288 // DEFAULT / SET NOT NULL / DROP NOT NULL as
11289 // engine no-ops by consuming the tail.
11290 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11291 // v7.22 (round-13 T2) — `SET DEFAULT
11292 // nextval('…')` is how pg_dump spells a
11293 // SERIAL column (plain integer in CREATE
11294 // TABLE + this ALTER). It used to be
11295 // swallowed as a no-op, which silently
11296 // STRIPPED auto-increment from imported
11297 // schemas — the first post-import INSERT
11298 // without an explicit id then violated NOT
11299 // NULL. Lower it to the auto-increment
11300 // marker instead.
11301 let is_default_nextval =
11302 matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11303 && matches!(
11304 self.tokens.get(self.pos + 2),
11305 Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11306 );
11307 if is_default_nextval {
11308 let seq_name = self.scan_sequence_name_until_boundary();
11309 return Ok(alloc::vec![
11310 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11311 column: col_name,
11312 seq_name,
11313 }
11314 ]);
11315 }
11316 // v7.37.18 (18.1 + 18.2) — proper lowering.
11317 self.advance(); // consume "set"
11318 match self.peek().clone() {
11319 Token::Default => {
11320 self.advance();
11321 let default_expr = self.parse_expr(0)?;
11322 return Ok(alloc::vec![
11323 crate::ast::AlterTableTarget::AlterColumnSetDefault {
11324 column: col_name,
11325 default_expr,
11326 }
11327 ]);
11328 }
11329 Token::Not => {
11330 self.advance();
11331 if !matches!(self.peek(), Token::Null) {
11332 return Err(self.err(alloc::format!(
11333 "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11334 self.peek()
11335 )));
11336 }
11337 self.advance();
11338 return Ok(alloc::vec![
11339 crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11340 column: col_name,
11341 }
11342 ]);
11343 }
11344 // `SET EXPRESSION AS (expr)` (PG 17) — change a
11345 // stored generated column's expression and
11346 // recompute existing rows.
11347 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11348 self.advance(); // EXPRESSION
11349 if matches!(self.peek(), Token::As) {
11350 self.advance();
11351 }
11352 let expr = self.parse_expr(0)?;
11353 return Ok(alloc::vec![
11354 crate::ast::AlterTableTarget::AlterColumnSetExpression {
11355 column: col_name,
11356 expr,
11357 }
11358 ]);
11359 }
11360 other => {
11361 // Other SET subjects (STATISTICS,
11362 // STORAGE, COMPRESSION, …) stay no-ops —
11363 // storage hints with no SPG semantics.
11364 let _ = other;
11365 self.consume_until_statement_boundary();
11366 return Ok(Vec::new());
11367 }
11368 }
11369 }
11370 Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11371 self.advance(); // consume "drop"
11372 return self.parse_alter_column_drop_tail(col_name);
11373 }
11374 Token::Drop => {
11375 self.advance(); // consume Drop token
11376 return self.parse_alter_column_drop_tail(col_name);
11377 }
11378 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11379 // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11380 // GENERATED { ALWAYS | BY DEFAULT } AS
11381 // IDENTITY ( … )`: pg_dump's spelling for
11382 // identity columns. Same auto-increment
11383 // lowering as the nextval default; the
11384 // sequence options inside the parens are
11385 // no-ops under SPG's max+1 semantics.
11386 let is_generated = matches!(
11387 self.tokens.get(self.pos + 1),
11388 Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11389 );
11390 if !is_generated {
11391 return Err(self.err(alloc::format!(
11392 "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11393 self.tokens.get(self.pos + 1)
11394 )));
11395 }
11396 let seq_name = self.scan_sequence_name_until_boundary();
11397 return Ok(alloc::vec![
11398 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11399 column: col_name,
11400 seq_name,
11401 }
11402 ]);
11403 }
11404 // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11405 // column: floor the next allocated value at n (bare
11406 // RESTART = restart from the start value, 1).
11407 Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11408 self.advance();
11409 let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11410 {
11411 self.advance();
11412 let neg = if matches!(self.peek(), Token::Minus) {
11413 self.advance();
11414 true
11415 } else {
11416 false
11417 };
11418 match self.advance() {
11419 Token::Integer(v) => Some(if neg { -v } else { v }),
11420 other => {
11421 return Err(self.err(alloc::format!(
11422 "expected integer after RESTART WITH, got {other:?}"
11423 )));
11424 }
11425 }
11426 } else {
11427 None
11428 };
11429 return Ok(alloc::vec![
11430 crate::ast::AlterTableTarget::AlterColumnRestart {
11431 column: col_name,
11432 with,
11433 }
11434 ]);
11435 }
11436 other => {
11437 return Err(self.err(alloc::format!(
11438 "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11439 )));
11440 }
11441 }
11442 // v7.39 (round 713) — the type parser has consumed a
11443 // trailing `COLLATE <name>` since Phase 2.5, and
11444 // `parse_column_type_name` discarded it: `ALTER COLUMN t
11445 // TYPE text COLLATE "C"` parsed clean and changed
11446 // nothing. Keep the clause; the engine re-collates.
11447 let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _) =
11448 self.parse_type_with_implied_flags()?;
11449 let collation = if coll_explicit {
11450 coll_name.map(|n| (coll, n))
11451 } else {
11452 None
11453 };
11454 let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11455 {
11456 self.advance();
11457 Some(self.parse_expr(0)?)
11458 } else {
11459 None
11460 };
11461 Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11462 column: col_name,
11463 new_type,
11464 using,
11465 collation,
11466 }])
11467 }
11468 // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11469 // PG also supports `RENAME TO new_table` for table-name
11470 // rename; that surface is deferred (pg_dump never emits
11471 // it). If the first post-RENAME ident is `TO`, the user
11472 // is asking for table rename — error with a clear
11473 // message rather than misparsing `TO` as a column name.
11474 Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11475 self.advance();
11476 // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11477 // table-name rename (mailrs round-10 A.5 — used
11478 // by migrate-042's `RENAME TO email_contacts`).
11479 // `TO` lexes as Token::To.
11480 if matches!(self.peek(), Token::To)
11481 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11482 {
11483 self.advance();
11484 let new = self.expect_ident_like()?;
11485 return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11486 new,
11487 }]);
11488 }
11489 // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11490 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11491 self.advance();
11492 let old = self.expect_ident_like()?;
11493 if matches!(self.peek(), Token::To) {
11494 self.advance();
11495 } else {
11496 self.expect_keyword_ident("to")?;
11497 }
11498 let new = self.expect_ident_like()?;
11499 return Ok(alloc::vec![
11500 crate::ast::AlterTableTarget::RenameConstraint { old, new }
11501 ]);
11502 }
11503 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11504 self.advance();
11505 }
11506 let old = self.expect_ident_like()?;
11507 // `TO` is a reserved keyword token; accept both
11508 // Token::To and Token::Ident("to") for consistency.
11509 if matches!(self.peek(), Token::To) {
11510 self.advance();
11511 } else {
11512 self.expect_keyword_ident("to")?;
11513 }
11514 let new = self.expect_ident_like()?;
11515 Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11516 old,
11517 new,
11518 }])
11519 }
11520 // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11521 // { ALL | <name> }`. pg_dump --disable-triggers wraps
11522 // every data block with these. Real disable semantics —
11523 // not no-op — because reload correctness assumes the
11524 // triggers don't fire (rows already carry their
11525 // computed values from prod).
11526 Token::Ident(s)
11527 if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11528 {
11529 let enabled = s.eq_ignore_ascii_case("enable");
11530 self.advance();
11531 // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11532 // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11533 // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11534 // pg_dump output) — anything else falls through to
11535 // the catch-all error below.
11536 // v7.22 (round-13 T3) — mysqldump wraps every data
11537 // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11538 // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11539 // maintains indexes incrementally — engine no-op.
11540 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11541 self.advance();
11542 return Ok(Vec::new());
11543 }
11544 // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11545 // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11546 // to gate triggers on session_replication_role; SPG
11547 // has no replica role, so the prefix is consumed and
11548 // treated identically to the plain ENABLE/DISABLE
11549 // TRIGGER form.
11550 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11551 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11552 {
11553 self.advance();
11554 }
11555 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11556 return Err(self.err(alloc::format!(
11557 "expected TRIGGER after {}, got {:?}",
11558 if enabled { "ENABLE" } else { "DISABLE" },
11559 self.peek()
11560 )));
11561 }
11562 self.advance();
11563 // `ALL` lexes as Token::All (reserved); also
11564 // accept Token::Ident("all") for symmetry.
11565 // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11566 // TRIGGER selectors. USER (= all user triggers) is
11567 // semantically ALL here; REPLICA / ALWAYS gate on
11568 // session_replication_role which SPG doesn't track.
11569 // All map to TriggerSelector::All.
11570 let which = if matches!(self.peek(), Token::All)
11571 || matches!(self.peek(), Token::Ident(s)
11572 if s.eq_ignore_ascii_case("all")
11573 || s.eq_ignore_ascii_case("user")
11574 || s.eq_ignore_ascii_case("replica")
11575 || s.eq_ignore_ascii_case("always"))
11576 {
11577 self.advance();
11578 crate::ast::TriggerSelector::All
11579 } else {
11580 let name = self.expect_ident_like()?;
11581 crate::ast::TriggerSelector::Named(name)
11582 };
11583 Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11584 which,
11585 enabled,
11586 }])
11587 }
11588 // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11589 Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11590 self.advance();
11591 if !matches!(self.peek(), Token::Partition)
11592 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11593 if s.eq_ignore_ascii_case("partition"))
11594 {
11595 return Err(self.err(alloc::format!(
11596 "expected PARTITION after ATTACH, got {:?}",
11597 self.peek()
11598 )));
11599 }
11600 self.advance();
11601 let child = self.expect_ident_like()?;
11602 let bounds = self.parse_partition_bounds_tail()?;
11603 Ok(alloc::vec![
11604 crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11605 ])
11606 }
11607 // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11608 Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11609 self.advance();
11610 if !matches!(self.peek(), Token::Partition)
11611 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11612 if s.eq_ignore_ascii_case("partition"))
11613 {
11614 return Err(self.err(alloc::format!(
11615 "expected PARTITION after DETACH, got {:?}",
11616 self.peek()
11617 )));
11618 }
11619 self.advance();
11620 let child = self.expect_ident_like()?;
11621 let mut concurrently = false;
11622 let mut finalize = false;
11623 loop {
11624 match self.peek().clone() {
11625 Token::Ident(s) | Token::QuotedIdent(s)
11626 if s.eq_ignore_ascii_case("concurrently") =>
11627 {
11628 self.advance();
11629 concurrently = true;
11630 }
11631 Token::Ident(s) | Token::QuotedIdent(s)
11632 if s.eq_ignore_ascii_case("finalize") =>
11633 {
11634 self.advance();
11635 finalize = true;
11636 }
11637 _ => break,
11638 }
11639 }
11640 Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11641 child,
11642 concurrently,
11643 finalize,
11644 }])
11645 }
11646 other => Err(self.err(alloc::format!(
11647 "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11648 ))),
11649 }
11650 }
11651
11652 /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11653 /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11654 /// TABLE … ATTACH PARTITION. Shares the same grammar as
11655 /// `parse_partition_of_tail`'s bounds branch.
11656 /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11657 /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11658 /// lowering each to the respective AlterTableTarget. Any
11659 /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11660 /// no-op via consume_until_statement_boundary.
11661 fn parse_alter_column_drop_tail(
11662 &mut self,
11663 col_name: String,
11664 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11665 match self.peek().clone() {
11666 Token::Default => {
11667 self.advance();
11668 Ok(alloc::vec![
11669 crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11670 ])
11671 }
11672 Token::Not => {
11673 self.advance();
11674 if !matches!(self.peek(), Token::Null) {
11675 return Err(self.err(alloc::format!(
11676 "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11677 self.peek()
11678 )));
11679 }
11680 self.advance();
11681 Ok(alloc::vec![
11682 crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11683 ])
11684 }
11685 // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11686 // generated column into a plain column.
11687 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11688 self.advance();
11689 // v7.39 (round 187, U10) — IF EXISTS was consumed but
11690 // dropped, so the engine still errored on a plain
11691 // column; PG's semantics are NOTICE + skip.
11692 let mut if_exists = false;
11693 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11694 self.advance();
11695 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11696 self.advance();
11697 if_exists = true;
11698 }
11699 }
11700 Ok(alloc::vec![
11701 crate::ast::AlterTableTarget::AlterColumnDropExpression {
11702 column: col_name,
11703 if_exists,
11704 }
11705 ])
11706 }
11707 // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11708 // identity column into a plain column.
11709 Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11710 self.advance();
11711 let mut if_exists = false;
11712 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11713 self.advance();
11714 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11715 self.advance();
11716 if_exists = true;
11717 }
11718 }
11719 Ok(alloc::vec![
11720 crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11721 column: col_name,
11722 if_exists,
11723 }
11724 ])
11725 }
11726 _ => {
11727 self.consume_until_statement_boundary();
11728 Ok(Vec::new())
11729 }
11730 }
11731 }
11732
11733 /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11734 /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11735 /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11736 /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11737 fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11738 let mut opts = crate::ast::CopyOptions::default();
11739 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11740 return Ok(opts);
11741 }
11742 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11743 self.advance();
11744 }
11745 if matches!(self.peek(), Token::LParen) {
11746 self.advance();
11747 loop {
11748 self.parse_one_copy_option(&mut opts)?;
11749 match self.peek() {
11750 Token::Comma => {
11751 self.advance();
11752 }
11753 Token::RParen => {
11754 self.advance();
11755 break;
11756 }
11757 other => {
11758 return Err(self.err(alloc::format!(
11759 "expected ',' or ')' in COPY options, got {other:?}"
11760 )));
11761 }
11762 }
11763 }
11764 } else {
11765 while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11766 self.parse_one_copy_option(&mut opts)?;
11767 }
11768 }
11769 if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11770 return Err(self.err(alloc::format!(
11771 "unexpected token after COPY options: {:?}",
11772 self.peek()
11773 )));
11774 }
11775 Ok(opts)
11776 }
11777
11778 fn parse_one_copy_option(
11779 &mut self,
11780 opts: &mut crate::ast::CopyOptions,
11781 ) -> Result<(), ParseError> {
11782 use crate::ast::CopyFormat;
11783 // The option keyword. NULL lexes as its own token; the rest are
11784 // bare identifiers.
11785 let kw = match self.advance() {
11786 Token::Null => alloc::string::String::from("NULL"),
11787 Token::Ident(s) => s.to_uppercase(),
11788 other => {
11789 return Err(self.err(alloc::format!(
11790 "expected a COPY option keyword, got {other:?}"
11791 )));
11792 }
11793 };
11794 match kw.as_str() {
11795 "FORMAT" => {
11796 let fmt = self.expect_ident_like()?;
11797 match fmt.to_ascii_uppercase().as_str() {
11798 "CSV" => opts.format = CopyFormat::Csv,
11799 "TEXT" => opts.format = CopyFormat::Text,
11800 other => {
11801 return Err(self.err(alloc::format!(
11802 "COPY format \"{}\" not recognized",
11803 other.to_ascii_lowercase()
11804 )));
11805 }
11806 }
11807 }
11808 // Legacy bare format keywords.
11809 "CSV" => opts.format = CopyFormat::Csv,
11810 "TEXT" => opts.format = CopyFormat::Text,
11811 "HEADER" => {
11812 opts.header = match self.peek() {
11813 Token::True => {
11814 self.advance();
11815 true
11816 }
11817 Token::False => {
11818 self.advance();
11819 false
11820 }
11821 Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
11822 self.advance();
11823 true
11824 }
11825 Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
11826 self.advance();
11827 false
11828 }
11829 // Bare HEADER (no boolean) means HEADER true.
11830 _ => true,
11831 };
11832 }
11833 // r1066 (7.38 S5.1) — pgbench 14+ loads with
11834 // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
11835 // vacuum bookkeeping on a freshly created/truncated
11836 // table; SPG's per-statement visibility makes it a
11837 // faithful no-op, and rejecting it aborted `pgbench -i`
11838 // against the drop-in. Accept ON/OFF/bare, change nothing.
11839 "FREEZE" => match self.peek() {
11840 Token::True | Token::False => {
11841 self.advance();
11842 }
11843 Token::Ident(s)
11844 if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
11845 {
11846 self.advance();
11847 }
11848 _ => {}
11849 },
11850 "DELIMITER" | "QUOTE" | "ESCAPE" => {
11851 let s = match self.advance() {
11852 Token::String(s) => s,
11853 other => {
11854 return Err(self.err(alloc::format!(
11855 "COPY {kw} expects a single-character string, got {other:?}"
11856 )));
11857 }
11858 };
11859 // v7.39 (round 247) — PG's wording (0A000), keyword in
11860 // lowercase: "COPY delimiter must be a single one-byte
11861 // character".
11862 let one_byte_err = || {
11863 self.err(alloc::format!(
11864 "COPY {} must be a single one-byte character",
11865 kw.to_ascii_lowercase()
11866 ))
11867 };
11868 let mut chars = s.chars();
11869 let c = chars.next().ok_or_else(one_byte_err)?;
11870 if chars.next().is_some() || c.len_utf8() != 1 {
11871 return Err(one_byte_err());
11872 }
11873 match kw.as_str() {
11874 "DELIMITER" => opts.delimiter = Some(c),
11875 "QUOTE" => opts.quote = Some(c),
11876 _ => opts.escape = Some(c),
11877 }
11878 }
11879 // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
11880 "FORCE_QUOTE" => {
11881 if matches!(self.peek(), Token::Star) {
11882 self.advance();
11883 opts.force_quote = Some(Vec::new());
11884 } else {
11885 if !matches!(self.peek(), Token::LParen) {
11886 return Err(self.err(alloc::format!(
11887 "expected '(' or '*' after FORCE_QUOTE, got {:?}",
11888 self.peek()
11889 )));
11890 }
11891 self.advance();
11892 let mut cols = Vec::new();
11893 loop {
11894 cols.push(self.expect_ident_like()?);
11895 match self.peek() {
11896 Token::Comma => {
11897 self.advance();
11898 }
11899 Token::RParen => {
11900 self.advance();
11901 break;
11902 }
11903 other => {
11904 return Err(self.err(alloc::format!(
11905 "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
11906 )));
11907 }
11908 }
11909 }
11910 opts.force_quote = Some(cols);
11911 }
11912 }
11913 "NULL" => {
11914 opts.null_str = Some(match self.advance() {
11915 Token::String(s) => s,
11916 other => {
11917 return Err(self.err(alloc::format!(
11918 "COPY NULL expects a quoted string, got {other:?}"
11919 )));
11920 }
11921 });
11922 }
11923 // v7.39 (round 265) — the two CSV FROM-side column lists. Same
11924 // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
11925 // FORCE_NULL too.
11926 "FORCE_NOT_NULL" | "FORCE_NULL" => {
11927 let cols = self.parse_copy_column_list(&kw)?;
11928 if kw == "FORCE_NOT_NULL" {
11929 opts.force_not_null = Some(cols);
11930 } else {
11931 opts.force_null = Some(cols);
11932 }
11933 }
11934 other => {
11935 // PG's wording, lowercased option name.
11936 return Err(self.err(alloc::format!(
11937 "option \"{}\" not recognized",
11938 other.to_ascii_lowercase()
11939 )));
11940 }
11941 }
11942 Ok(())
11943 }
11944
11945 /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
11946 /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
11947 /// is the `*` spelling.
11948 fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
11949 if matches!(self.peek(), Token::Star) {
11950 self.advance();
11951 return Ok(Vec::new());
11952 }
11953 if !matches!(self.peek(), Token::LParen) {
11954 return Err(self.err(alloc::format!(
11955 "expected '(' or '*' after {kw}, got {:?}",
11956 self.peek()
11957 )));
11958 }
11959 self.advance();
11960 let mut cols = Vec::new();
11961 loop {
11962 cols.push(self.expect_ident_like()?);
11963 match self.peek() {
11964 Token::Comma => {
11965 self.advance();
11966 }
11967 Token::RParen => {
11968 self.advance();
11969 break;
11970 }
11971 other => {
11972 return Err(self.err(alloc::format!(
11973 "expected ',' or ')' in {kw} list, got {other:?}"
11974 )));
11975 }
11976 }
11977 }
11978 Ok(cols)
11979 }
11980
11981 fn parse_partition_bounds_tail(
11982 &mut self,
11983 ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
11984 use crate::ast::PartitionOfBoundsAst;
11985 match self.peek() {
11986 Token::Default => {
11987 self.advance();
11988 Ok(PartitionOfBoundsAst::Default)
11989 }
11990 Token::For => {
11991 self.advance();
11992 if !matches!(self.peek(), Token::Values) {
11993 return Err(
11994 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
11995 );
11996 }
11997 self.advance();
11998 let want_with = matches!(
11999 self.peek(),
12000 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
12001 );
12002 if want_with {
12003 self.advance();
12004 if !matches!(self.peek(), Token::LParen) {
12005 return Err(self.err(format!(
12006 "expected '(' after FOR VALUES WITH, got {:?}",
12007 self.peek()
12008 )));
12009 }
12010 self.advance();
12011 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
12012 loop {
12013 let key = self.expect_ident_like()?;
12014 let n = match self.peek().clone() {
12015 Token::Integer(v) if u32::try_from(v).is_ok() => {
12016 self.advance();
12017 v as u32
12018 }
12019 other => {
12020 return Err(self.err(format!(
12021 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
12022 )));
12023 }
12024 };
12025 match key.to_ascii_uppercase().as_str() {
12026 "MODULUS" => modulus = Some(n),
12027 "REMAINDER" => remainder = Some(n),
12028 other => {
12029 return Err(self.err(format!(
12030 "FOR VALUES WITH: unknown key {other:?}; \
12031 expected MODULUS or REMAINDER"
12032 )));
12033 }
12034 }
12035 match self.peek() {
12036 Token::Comma => {
12037 self.advance();
12038 }
12039 Token::RParen => {
12040 self.advance();
12041 break;
12042 }
12043 other => {
12044 return Err(self.err(format!(
12045 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
12046 )));
12047 }
12048 }
12049 }
12050 let modulus = modulus
12051 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
12052 let remainder = remainder.ok_or_else(|| {
12053 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
12054 })?;
12055 if modulus == 0 {
12056 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
12057 }
12058 if remainder >= modulus {
12059 return Err(self.err(format!(
12060 "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
12061 )));
12062 }
12063 return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
12064 }
12065 match self.peek() {
12066 Token::From => {
12067 self.advance();
12068 let lower = Box::new(self.parse_partition_bound_expr()?);
12069 if !matches!(self.peek(), Token::To) {
12070 return Err(self.err(format!(
12071 "expected TO after FROM (...), got {:?}",
12072 self.peek()
12073 )));
12074 }
12075 self.advance();
12076 let upper = Box::new(self.parse_partition_bound_expr()?);
12077 Ok(PartitionOfBoundsAst::Range { lower, upper })
12078 }
12079 Token::In => {
12080 self.advance();
12081 if !matches!(self.peek(), Token::LParen) {
12082 return Err(self.err(format!(
12083 "expected '(' after FOR VALUES IN, got {:?}",
12084 self.peek()
12085 )));
12086 }
12087 self.advance();
12088 let mut values = Vec::new();
12089 loop {
12090 values.push(self.parse_expr(0)?);
12091 match self.peek() {
12092 Token::Comma => {
12093 self.advance();
12094 }
12095 Token::RParen => {
12096 self.advance();
12097 break;
12098 }
12099 other => {
12100 return Err(self.err(format!(
12101 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
12102 )));
12103 }
12104 }
12105 }
12106 if values.is_empty() {
12107 return Err(
12108 self.err("FOR VALUES IN requires at least one literal".to_string())
12109 );
12110 }
12111 Ok(PartitionOfBoundsAst::List { values })
12112 }
12113 other => Err(self.err(format!(
12114 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
12115 ))),
12116 }
12117 }
12118 other => Err(self.err(format!(
12119 "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
12120 ))),
12121 }
12122 }
12123
12124 /// v7.16.2 — peek for `information_schema.<tbl>` /
12125 /// `pg_catalog.<tbl>` triples and, if matched, consume all
12126 /// three tokens + return a synthetic table name the engine's
12127 /// SELECT path recognises as a virtual view. Returns `None`
12128 /// when the head doesn't look like a meta-qualified name.
12129 /// Used by `parse_table_ref` to bypass the
12130 /// `expect_ident_like` schema-strip for these specific PG
12131 /// meta schemas (mailrs round-10 A.3).
12132 fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12133 // Extract the schema name. Must be a plain ident token.
12134 let schema = match self.tokens.get(self.pos) {
12135 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12136 _ => return None,
12137 };
12138 // Dot.
12139 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12140 return None;
12141 }
12142 // The table-side ident may lex as a reserved keyword
12143 // (e.g. `Token::Tables`). Tolerate the common ones via a
12144 // helper that reads the trailing token's underlying name.
12145 let tbl = match self.tokens.get(self.pos + 2)? {
12146 Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12147 Token::Tables => "tables".to_string(),
12148 // Other PG meta table names that may collide with
12149 // reserved keywords land here as needed.
12150 _ => return None,
12151 };
12152 // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12153 // names so the synthetic name doesn't double-prefix
12154 // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12155 let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12156 ("__spg_info_", tbl.to_ascii_lowercase())
12157 } else if schema.eq_ignore_ascii_case("pg_catalog") {
12158 // v7.39 (round 541) — only the catalogs SPG actually
12159 // synthesises are rewritten, which is what the BARE path
12160 // has always checked. Anything else keeps its own name and
12161 // takes the ordinary route: `pg_stat_activity` and friends
12162 // resolve through meta_view_result, and a name that is no
12163 // catalog at all gets PG's "relation does not exist"
12164 // instead of a message about a view SPG cannot materialise.
12165 let lowered = tbl.to_ascii_lowercase();
12166 if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12167 self.advance(); // schema
12168 self.advance(); // dot
12169 self.advance(); // tbl
12170 return Some((lowered.clone(), lowered));
12171 }
12172 let bare = lowered
12173 .strip_prefix("pg_")
12174 .map(alloc::string::String::from)
12175 .unwrap_or(lowered);
12176 ("__spg_pg_", bare)
12177 } else if schema.eq_ignore_ascii_case("mysql") {
12178 // v7.17.0 Phase 3.P0-65 — MySQL system schema
12179 // (`mysql.user`, `mysql.db`). Same synthetic-name
12180 // shape as pg_catalog.
12181 ("__spg_mysql_", tbl.to_ascii_lowercase())
12182 } else {
12183 return None;
12184 };
12185 self.advance(); // schema
12186 self.advance(); // dot
12187 self.advance(); // tbl
12188 Some((
12189 alloc::format!("{prefix}{normalised}"),
12190 tbl.to_ascii_lowercase(),
12191 ))
12192 }
12193
12194 /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12195 /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12196 /// implicit front of every search_path, so a bare reference to a
12197 /// known catalog table always means the catalog table. Only the
12198 /// names the engine actually synthesises are recognised — any
12199 /// other `pg_*` ident stays a user table (mailrs embed round-12).
12200 fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12201 // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12202 // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12203 // `pg_catalog` at the front of every search_path. (pg_stat_activity
12204 // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12205 // through the meta_view_result path instead, and already resolve
12206 // bare — they must NOT be listed here or the __spg_ rewrite would
12207 // mis-target them.)
12208 const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12209 let name = match self.tokens.get(self.pos) {
12210 Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12211 _ => return None,
12212 };
12213 // A following dot means this ident is a schema qualifier,
12214 // not a table name — let the qualified path handle it.
12215 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12216 return None;
12217 }
12218 if !PG_META_TABLES.contains(&name.as_str()) {
12219 return None;
12220 }
12221 self.advance();
12222 let bare = name.strip_prefix("pg_").unwrap_or(&name);
12223 Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12224 }
12225
12226 /// Consume a bare ident if its lowercase matches `kw`, else err.
12227 /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12228 /// Peeks only; the caller advances.
12229 fn peek_keyword_ident(&self, kw: &str) -> bool {
12230 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12231 }
12232
12233 fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12234 match self.advance() {
12235 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12236 other => Err(ParseError {
12237 message: format!("expected {kw:?}, got {other:?}"),
12238 token_pos: self.consumed_pos(),
12239 }),
12240 }
12241 }
12242
12243 /// Accept either a quoted identifier (`"foo"`) or a quoted string
12244 /// literal (`'foo'`) — same shape used by CREATE USER for the
12245 /// username slot.
12246 fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12247 match self.advance() {
12248 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12249 other => Err(ParseError {
12250 message: format!("expected identifier or string, got {other:?}"),
12251 token_pos: self.consumed_pos(),
12252 }),
12253 }
12254 }
12255
12256 fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12257 match self.advance() {
12258 Token::String(s) => Ok(s),
12259 other => Err(ParseError {
12260 message: format!("expected quoted string, got {other:?}"),
12261 token_pos: self.consumed_pos(),
12262 }),
12263 }
12264 }
12265
12266 fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12267 // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12268 // subqueries recurse through here without passing
12269 // parse_expr; share the same nesting budget.
12270 self.enter_nested()?;
12271 let r = self.parse_select_stmt_inner();
12272 self.nest_depth -= 1;
12273 r
12274 }
12275
12276 fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12277 // Caller dispatches on Token::Select; the inner helper handles
12278 // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12279 // get a fresh bare-select parse and may not have their own ORDER
12280 // BY / LIMIT.
12281 let mut head = self.parse_bare_select()?;
12282 self.parse_setop_chain_into(&mut head)?;
12283 self.parse_select_tail_into(&mut head)?;
12284 Ok(Statement::Select(head))
12285 }
12286
12287 /// v7.37.17 (17.6 siblings) — the three SQL set operations
12288 /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12289 /// token), and INTERSECT [ALL] (a bare ident — it was never
12290 /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12291 /// tighter than UNION / EXCEPT — the executor folds the chain
12292 /// left-to-right, which is already correct for LEADING
12293 /// intersects; an INTERSECT pair that FOLLOWS a union/except
12294 /// pair nests into that previous peer, so A UNION B INTERSECT C
12295 /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12296 /// groups.
12297 fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12298 // A parenthesized group arrives with its own (already
12299 // regrouped) unions on `head`; only the pairs THIS chain
12300 // appends participate in the precedence regroup below —
12301 // nesting an outer INTERSECT into a group-internal peer
12302 // would dissolve the explicit grouping.
12303 let boundary = head.unions.len();
12304 loop {
12305 let base = match self.peek() {
12306 Token::Union => UnionKind::Distinct,
12307 Token::Except => UnionKind::Except,
12308 Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12309 _ => break,
12310 };
12311 self.advance();
12312 let kind = if matches!(self.peek(), Token::All) {
12313 self.advance();
12314 match base {
12315 UnionKind::Distinct => UnionKind::All,
12316 UnionKind::Except => UnionKind::ExceptAll,
12317 _ => UnionKind::IntersectAll,
12318 }
12319 } else {
12320 base
12321 };
12322 let peer = self.parse_bare_select()?;
12323 head.unions.push((kind, peer));
12324 }
12325 let mut pairs = core::mem::take(&mut head.unions);
12326 let tail = pairs.split_off(boundary);
12327 let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12328 for (kind, peer) in tail {
12329 let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12330 // An intersect nests into the previous element of THIS
12331 // chain only; with no new previous element it stays at
12332 // the outer level (the left fold applies it to the
12333 // whole head, group included).
12334 match (
12335 is_intersect,
12336 regrouped.len() > boundary,
12337 regrouped.last_mut(),
12338 ) {
12339 (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12340 _ => regrouped.push((kind, peer)),
12341 }
12342 }
12343 head.unions = regrouped;
12344 Ok(())
12345 }
12346
12347 /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12348 /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12349 /// the top-level bare VALUES statement reuses it verbatim.
12350 /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12351 /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12352 /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12353 /// where the grouping-set universe is still in scope.
12354 fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12355 if !matches!(self.peek(), Token::Order) {
12356 return Ok(Vec::new());
12357 }
12358 self.advance();
12359 if !self.peek_is_by() {
12360 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12361 }
12362 self.advance();
12363 let mut keys = Vec::new();
12364 loop {
12365 // v7.39 (round 691) — save/restore, the discipline this parser
12366 // already uses around `pending_sample_preds`, so a subquery inside
12367 // a key neither inherits nor leaks the channel.
12368 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12369 let saved_coll = self.order_key_collation.take();
12370 let parsed = self.parse_expr(0);
12371 self.in_order_by_key = saved_flag;
12372 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12373 let expr = parsed?;
12374 let desc = if matches!(self.peek(), Token::Desc) {
12375 self.advance();
12376 true
12377 } else if matches!(self.peek(), Token::Asc) {
12378 self.advance();
12379 false
12380 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12381 // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12382 // one ordering per type, so the btree comparison operators map
12383 // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12384 // would need a custom operator class — honest error.
12385 self.advance();
12386 match self.advance() {
12387 Token::Lt | Token::LtEq => false,
12388 Token::Gt | Token::GtEq => true,
12389 other => {
12390 return Err(self.err(alloc::format!(
12391 "ORDER BY USING supports the btree comparison \
12392 operators (< <= > >=); got {other:?}"
12393 )));
12394 }
12395 }
12396 } else {
12397 false
12398 };
12399 // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12400 let nulls_first = self.parse_optional_nulls_placement()?;
12401 keys.push(OrderBy {
12402 expr,
12403 desc,
12404 nulls_first,
12405 collation,
12406 });
12407 if matches!(self.peek(), Token::Comma) {
12408 self.advance();
12409 } else {
12410 break;
12411 }
12412 }
12413 Ok(keys)
12414 }
12415
12416 fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12417 // v7.39 (round 135) — a grouping-set query may have already parsed +
12418 // rewritten its ORDER BY (to reference synthetic grouping columns); if
12419 // no ORDER BY token is present, keep that pre-set order_by rather than
12420 // clobbering it with an empty list.
12421 let parsed_keys = self.parse_order_by_keys()?;
12422 head.order_by = if parsed_keys.is_empty() {
12423 core::mem::take(&mut head.order_by)
12424 } else {
12425 parsed_keys
12426 };
12427 // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12428 // order. PG's grammar takes a limit clause and an offset clause
12429 // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12430 // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12431 // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12432 // spelling died on `expected end of input, got Limit`.
12433 //
12434 // Each may appear at most once, and LIMIT and FETCH FIRST are
12435 // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12436 // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12437 // A second one is left unconsumed here, which the caller reports
12438 // as trailing input rather than silently taking the last.
12439 let mut saw_limit = false;
12440 let mut saw_offset = false;
12441 loop {
12442 if !saw_limit && matches!(self.peek(), Token::Limit) {
12443 self.advance();
12444 // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12445 // PG synonyms for "no limit". Treat both as None
12446 // (no head.limit set) so the engine's existing
12447 // unlimited-result path takes over. Reject was the
12448 // pre-5.1 behaviour and broke pg_dump-flavoured
12449 // tooling that occasionally emits LIMIT NULL.
12450 if self.consume_limit_unbounded_sentinel() {
12451 head.limit = None;
12452 } else {
12453 let first = self.parse_limit_expr("LIMIT")?;
12454 // MySQL `LIMIT offset, count` — the first number is
12455 // the offset when a comma follows.
12456 if matches!(self.peek(), Token::Comma) {
12457 self.advance();
12458 let count = self.parse_limit_expr("LIMIT")?;
12459 head.offset = Some(first);
12460 saw_offset = true;
12461 head.limit = Some(count);
12462 } else {
12463 head.limit = Some(first);
12464 }
12465 }
12466 saw_limit = true;
12467 continue;
12468 }
12469 if !saw_offset && matches!(self.peek(), Token::Offset) {
12470 self.advance();
12471 // PG also accepts an optional `ROW` / `ROWS` trailer
12472 // after the offset value (`OFFSET 10 ROWS`). The
12473 // FETCH-FIRST branch below relies on the same.
12474 let off = self.parse_limit_expr("OFFSET")?;
12475 self.consume_optional_rows_keyword();
12476 head.offset = Some(off);
12477 saw_offset = true;
12478 continue;
12479 }
12480 // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12481 // the SQL-standard alias for LIMIT. PG accepts both
12482 // spellings interchangeably; pg_dump emits FETCH FIRST in
12483 // newer versions. We map it onto `head.limit` so the
12484 // engine path is unified.
12485 if !saw_limit
12486 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12487 if s.eq_ignore_ascii_case("fetch"))
12488 {
12489 self.advance(); // FETCH
12490 // `FIRST` or `NEXT` (both legal per SQL standard).
12491 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12492 if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12493 {
12494 self.advance();
12495 }
12496 // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12497 // implicit 1 — but we always consume one if present).
12498 let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12499 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12500 {
12501 // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12502 crate::ast::LimitExpr::Literal(1)
12503 } else {
12504 self.parse_limit_expr("FETCH FIRST")?
12505 };
12506 // Eat `ROW` / `ROWS` if not already consumed above.
12507 self.consume_optional_rows_keyword();
12508 // Optional `ONLY` (the spec form) — or the SQL:2008
12509 // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12510 // now honours WITH TIES by extending past the LIMIT
12511 // truncation point through every row that shares the
12512 // last-kept row's ORDER BY key.
12513 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12514 if s.eq_ignore_ascii_case("only"))
12515 {
12516 self.advance();
12517 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12518 if s.eq_ignore_ascii_case("with"))
12519 {
12520 self.advance(); // WITH
12521 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12522 if s.eq_ignore_ascii_case("ties"))
12523 {
12524 self.advance();
12525 head.limit_with_ties = true;
12526 }
12527 }
12528 head.limit = Some(count);
12529 saw_limit = true;
12530 continue;
12531 }
12532 break;
12533 }
12534 // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12535 // FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12536 // [ OF table_name [, …] ]
12537 // [ NOWAIT | SKIP LOCKED ]
12538 // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12539 // FOR SHARE OF t2`). SPG is a single-writer engine — every
12540 // SELECT already returns a consistent snapshot — so these
12541 // are accept-and-discard: the parser absorbs them so
12542 // mailrs / Rails / Django code paths that emit `SELECT
12543 // … FOR UPDATE` for advisory pessimistic locking load
12544 // without a parser error. The on-disk locking model is
12545 // unchanged; callers that rely on FOR UPDATE for read-
12546 // through-write ordering still get the right answer
12547 // because SPG serialises writes anyway.
12548 head.locking = self
12549 .consume_optional_for_lock_clauses()
12550 .map(alloc::boxed::Box::new);
12551 Ok(())
12552 }
12553
12554 /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12555 /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12556 /// LOCKED ]` trailers. Each clause is fully accepted and
12557 /// discarded — SPG's single-writer model already satisfies the
12558 /// callers' implicit ordering requirement. Stops at the first
12559 /// token that isn't `FOR`.
12560 fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12561 // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12562 // not discarded. PG keeps the strongest of several clauses; the
12563 // policy of the last one wins, which is what this loop records.
12564 let mut seen: Option<crate::ast::LockingClause> = None;
12565 while matches!(self.peek(), Token::For) {
12566 // v7.37.14 (A2.5-stub) — record that this query asked
12567 // for a row lock the parser is about to silently
12568 // discard. Operators surface the count via
12569 // `spg_sql::silent_for_update_count()` so they can
12570 // gauge how much of the workload depends on advisory
12571 // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12572 // before v7.37.15's per-row tuple locking lands.
12573 crate::record_silent_for_update_clause();
12574 self.advance(); // FOR
12575 // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12576 // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12577 let mut no_key = false;
12578 let mut key = false;
12579 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12580 if s.eq_ignore_ascii_case("no"))
12581 {
12582 self.advance(); // NO
12583 no_key = true;
12584 // The next ident should be KEY but be generous;
12585 // anything followed by UPDATE/SHARE is accepted.
12586 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12587 if s.eq_ignore_ascii_case("key"))
12588 {
12589 self.advance(); // KEY
12590 }
12591 }
12592 // `KEY` prefix (PG `FOR KEY SHARE`).
12593 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12594 if s.eq_ignore_ascii_case("key"))
12595 {
12596 self.advance(); // KEY
12597 key = true;
12598 }
12599 // Lock-strength keyword: UPDATE / SHARE. Required, but
12600 // we're lenient — an unexpected token here just bails
12601 // (we already consumed FOR; caller's downstream
12602 // dispatch will error if anything actually depends on
12603 // the trailing tokens).
12604 let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12605 if s.eq_ignore_ascii_case("update"));
12606 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12607 if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12608 {
12609 self.advance();
12610 use crate::ast::LockStrength as LS;
12611 let strength = match (is_update, no_key, key) {
12612 (true, true, _) => LS::NoKeyUpdate,
12613 (true, _, _) => LS::Update,
12614 (false, _, true) => LS::KeyShare,
12615 (false, _, _) => LS::Share,
12616 };
12617 seen = Some(crate::ast::LockingClause {
12618 strength,
12619 of_tables: alloc::vec::Vec::new(),
12620 policy: crate::ast::LockWait::Wait,
12621 });
12622 } else {
12623 // FOR by itself (or `FOR KEY` with nothing after) —
12624 // give up on the lock-clause path. We've already
12625 // advanced past FOR; further attempts to parse
12626 // here would clobber state.
12627 return seen;
12628 }
12629 // Optional `OF tbl[, tbl …]`. mailrs emits this when
12630 // joining and locking only a subset of tables.
12631 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12632 if s.eq_ignore_ascii_case("of"))
12633 {
12634 self.advance(); // OF
12635 #[allow(clippy::while_let_loop)]
12636 loop {
12637 match self.peek() {
12638 Token::Ident(_) | Token::QuotedIdent(_) => {
12639 // v7.39 (round 294) — the name is CAPTURED now: PG
12640 // validates it against the FROM clause, and an
12641 // uncaptured list silently means "lock everything".
12642 let mut nm = match self.advance() {
12643 Token::Ident(n) | Token::QuotedIdent(n) => n,
12644 _ => alloc::string::String::new(),
12645 };
12646 // Optional schema-qualified `schema.table`.
12647 if matches!(self.peek(), Token::Dot) {
12648 self.advance();
12649 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12650 {
12651 self.advance();
12652 nm = n;
12653 }
12654 }
12655 if let Some(c) = seen.as_mut() {
12656 c.of_tables.push(nm);
12657 }
12658 }
12659 _ => break,
12660 }
12661 if matches!(self.peek(), Token::Comma) {
12662 self.advance();
12663 } else {
12664 break;
12665 }
12666 }
12667 }
12668 // Optional `NOWAIT` | `SKIP LOCKED`.
12669 match self.peek().clone() {
12670 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12671 self.advance();
12672 if let Some(c) = seen.as_mut() {
12673 c.policy = crate::ast::LockWait::NoWait;
12674 }
12675 }
12676 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12677 self.advance(); // SKIP
12678 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12679 if s.eq_ignore_ascii_case("locked"))
12680 {
12681 self.advance(); // LOCKED
12682 if let Some(c) = seen.as_mut() {
12683 c.policy = crate::ast::LockWait::SkipLocked;
12684 }
12685 }
12686 }
12687 _ => {}
12688 }
12689 // Loop: PG allows multiple FOR clauses chained.
12690 }
12691 seen
12692 }
12693
12694 /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12695 /// Bind value gets resolved during prepared-statement Execute;
12696 /// the Pratt expression parser would over-accept here (e.g.
12697 /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12698 /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12699 /// sentinel tokens (PG synonyms for "no limit"). Returns true
12700 /// when one was consumed; caller skips the regular
12701 /// limit-value parse and leaves `head.limit` at None.
12702 fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12703 if matches!(self.peek(), Token::Null) {
12704 self.advance();
12705 return true;
12706 }
12707 if matches!(self.peek(), Token::All) {
12708 self.advance();
12709 return true;
12710 }
12711 false
12712 }
12713
12714 /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12715 /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12716 /// SQL-standard shape. No-op when missing.
12717 fn consume_optional_rows_keyword(&mut self) {
12718 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12719 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12720 {
12721 self.advance();
12722 }
12723 }
12724
12725 /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12726 ///
12727 /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12728 /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12729 /// constant, which is why that spelling keeps the token path below.
12730 ///
12731 /// Constants are folded here rather than carried into the tree: the
12732 /// 15+ execution paths that read the row count go through
12733 /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12734 /// means "no limit". A clause the engine could not resolve would
12735 /// therefore return the WHOLE table instead of failing. Folding at
12736 /// parse time keeps that impossible; a non-constant clause is still
12737 /// a clean error (recorded residual — closing it wants a resolution
12738 /// pre-pass on the simple-query path, where `substitute_placeholders`
12739 /// does not run).
12740 fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12741 // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12742 // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12743 // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12744 // ONLY` both work (its grammar takes a c_expr). Both measured
12745 // against PG 18.4 in round 305.
12746 if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12747 return self.parse_limit_constant(label);
12748 }
12749 // One pass, no rewind: `advance()` takes each token by
12750 // `mem::replace`, so a consumed token reads back as Eof and this
12751 // parser cannot backtrack. Everything — bare literal included —
12752 // is therefore folded from the parsed expression rather than
12753 // re-read from the token stream.
12754 let start = self.pos;
12755 let e = self.parse_expr(0)?;
12756 if let crate::ast::Expr::Placeholder(n) = e {
12757 return Ok(crate::ast::LimitExpr::Placeholder(n));
12758 }
12759 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12760 match fold_limit_constant(&e) {
12761 Some(Ok(v)) if v < 0 => Err(ParseError {
12762 message: alloc::format!("{neg_label} must not be negative"),
12763 token_pos: start,
12764 }),
12765 Some(Ok(v)) => u32::try_from(v)
12766 .map(crate::ast::LimitExpr::Literal)
12767 .map_err(|_| ParseError {
12768 message: alloc::format!("{label} value too large: {v}"),
12769 token_pos: start,
12770 }),
12771 Some(Err(message)) => Err(ParseError {
12772 message: message.replace("{L}", neg_label),
12773 token_pos: start,
12774 }),
12775 // v7.39 (round 305, V23) — not foldable at parse time
12776 // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
12777 // expression; the engine evaluates it once before dispatch.
12778 None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
12779 }
12780 }
12781
12782 fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12783 // v7.39 (round 239) — PG's row-count clause takes a bigint with its
12784 // coercion rules, not just an integer token: a NUMERIC rounds half
12785 // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
12786 // refused with PG's wording ("LIMIT must not be negative", 2201W /
12787 // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
12788 // content, failing as an input-syntax error on the value. General
12789 // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
12790 // they need an Expr-carrying LimitExpr variant.
12791 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12792 let err_at = |message: alloc::string::String, pos: usize| ParseError {
12793 message,
12794 token_pos: pos,
12795 };
12796 match self.advance() {
12797 Token::Integer(n) if n >= 0 => u32::try_from(n)
12798 .map(crate::ast::LimitExpr::Literal)
12799 .map_err(|_| ParseError {
12800 message: alloc::format!("{label} value too large: {n}"),
12801 token_pos: self.consumed_pos(),
12802 }),
12803 Token::Integer(_) => Err(err_at(
12804 alloc::format!("{neg_label} must not be negative"),
12805 self.pos.saturating_sub(1),
12806 )),
12807 Token::Numeric(t) => {
12808 let pos = self.pos.saturating_sub(1);
12809 let v: f64 = t.parse().map_err(|_| {
12810 err_at(
12811 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12812 pos,
12813 )
12814 })?;
12815 if v < 0.0 {
12816 return Err(err_at(
12817 alloc::format!("{neg_label} must not be negative"),
12818 pos,
12819 ));
12820 }
12821 // Round half away from zero — PG's numeric→bigint cast.
12822 // (no_std: no f64::round; v is non-negative, so truncating
12823 // v + 0.5 is the same thing.)
12824 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
12825 let rounded = (v + 0.5) as u64;
12826 u32::try_from(rounded)
12827 .map(crate::ast::LimitExpr::Literal)
12828 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
12829 }
12830 Token::Minus => {
12831 let pos = self.pos.saturating_sub(1);
12832 match self.peek() {
12833 Token::Integer(_) | Token::Numeric(_) => {
12834 self.advance();
12835 Err(err_at(
12836 alloc::format!("{neg_label} must not be negative"),
12837 pos,
12838 ))
12839 }
12840 other => Err(err_at(
12841 alloc::format!(
12842 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12843 ),
12844 pos,
12845 )),
12846 }
12847 }
12848 Token::String(t) => {
12849 let pos = self.pos.saturating_sub(1);
12850 match t.trim().parse::<i64>() {
12851 Ok(n) if n < 0 => Err(err_at(
12852 alloc::format!("{neg_label} must not be negative"),
12853 pos,
12854 )),
12855 Ok(n) => u32::try_from(n)
12856 .map(crate::ast::LimitExpr::Literal)
12857 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
12858 Err(_) => Err(err_at(
12859 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12860 pos,
12861 )),
12862 }
12863 }
12864 Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
12865 other => Err(ParseError {
12866 message: alloc::format!(
12867 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12868 ),
12869 token_pos: self.consumed_pos(),
12870 }),
12871 }
12872 }
12873
12874 /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
12875 /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
12876 /// `unions` empty and `order_by` / `limit` `None`; the top-level
12877 /// `parse_select_stmt` is responsible for filling those in.
12878 /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
12879 /// call in the expression tree to the per-set integer bitmask
12880 /// (PG semantics: one bit per argument, MSB first; 1 = the key
12881 /// is dropped in this grouping set). Runs during the ROLLUP /
12882 /// CUBE / GROUPING SETS expansion, where the set is known.
12883 /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
12884 /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
12885 fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
12886 if let Expr::FunctionCall { name, .. } = expr
12887 && name.eq_ignore_ascii_case("grouping")
12888 {
12889 if !out.iter().any(|e| e == expr) {
12890 out.push(expr.clone());
12891 }
12892 return;
12893 }
12894 match expr {
12895 Expr::Binary { lhs, rhs, .. } => {
12896 Self::collect_grouping_calls(lhs, out);
12897 Self::collect_grouping_calls(rhs, out);
12898 }
12899 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12900 Self::collect_grouping_calls(expr, out)
12901 }
12902 Expr::FunctionCall { args, .. } => {
12903 for a in args {
12904 Self::collect_grouping_calls(a, out);
12905 }
12906 }
12907 Expr::Case {
12908 operand,
12909 branches,
12910 else_branch,
12911 } => {
12912 if let Some(o) = operand {
12913 Self::collect_grouping_calls(o, out);
12914 }
12915 for (c, v) in branches {
12916 Self::collect_grouping_calls(c, out);
12917 Self::collect_grouping_calls(v, out);
12918 }
12919 if let Some(x) = else_branch {
12920 Self::collect_grouping_calls(x, out);
12921 }
12922 }
12923 _ => {}
12924 }
12925 }
12926
12927 /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
12928 /// `grp_exprs[k]` with a reference to the synthetic ordering column
12929 /// `__grp_ord_k` (injected per grouping-set branch).
12930 fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
12931 if let Expr::FunctionCall { name, .. } = expr
12932 && name.eq_ignore_ascii_case("grouping")
12933 {
12934 if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
12935 *expr = Expr::Column(crate::ast::ColumnName {
12936 qualifier: None,
12937 name: alloc::format!("__grp_ord_{k}"),
12938 });
12939 }
12940 return;
12941 }
12942 match expr {
12943 Expr::Binary { lhs, rhs, .. } => {
12944 Self::rewrite_grouping_to_col(lhs, grp_exprs);
12945 Self::rewrite_grouping_to_col(rhs, grp_exprs);
12946 }
12947 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12948 Self::rewrite_grouping_to_col(expr, grp_exprs)
12949 }
12950 Expr::FunctionCall { args, .. } => {
12951 for a in args {
12952 Self::rewrite_grouping_to_col(a, grp_exprs);
12953 }
12954 }
12955 Expr::Case {
12956 operand,
12957 branches,
12958 else_branch,
12959 } => {
12960 if let Some(o) = operand {
12961 Self::rewrite_grouping_to_col(o, grp_exprs);
12962 }
12963 for (c, v) in branches {
12964 Self::rewrite_grouping_to_col(c, grp_exprs);
12965 Self::rewrite_grouping_to_col(v, grp_exprs);
12966 }
12967 if let Some(x) = else_branch {
12968 Self::rewrite_grouping_to_col(x, grp_exprs);
12969 }
12970 }
12971 _ => {}
12972 }
12973 }
12974
12975 /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
12976 /// as the list of key sets it contributes. A bare expression is one
12977 /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
12978 /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
12979 /// the concatenation of its items' sets, where an item is itself an
12980 /// element, a parenthesized key list, or the empty set `()`. A
12981 /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
12982 /// move together.
12983 fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
12984 let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
12985 // ROLLUP ( … ) / CUBE ( … )
12986 if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
12987 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
12988 {
12989 let is_cube = is_kw(self.peek(), "cube");
12990 self.advance(); // ROLLUP / CUBE
12991 self.advance(); // (
12992 let mut units: Vec<Vec<Expr>> = Vec::new();
12993 loop {
12994 if matches!(self.peek(), Token::LParen) {
12995 // Composite unit: (a, b) rolls up as one.
12996 self.advance();
12997 let mut unit = Vec::new();
12998 if !matches!(self.peek(), Token::RParen) {
12999 loop {
13000 unit.push(self.parse_expr(0)?);
13001 match self.peek() {
13002 Token::Comma => {
13003 self.advance();
13004 }
13005 Token::RParen => break,
13006 other => {
13007 return Err(self.err(format!(
13008 "expected ',' or ')' in grouping unit, got {other:?}"
13009 )));
13010 }
13011 }
13012 }
13013 }
13014 self.advance(); // )
13015 units.push(unit);
13016 } else {
13017 units.push(alloc::vec![self.parse_expr(0)?]);
13018 }
13019 match self.peek() {
13020 Token::Comma => {
13021 self.advance();
13022 }
13023 Token::RParen => break,
13024 other => {
13025 return Err(self.err(format!(
13026 "expected ',' or ')' in grouping list, got {other:?}"
13027 )));
13028 }
13029 }
13030 }
13031 self.advance(); // )
13032 let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
13033 units
13034 .iter()
13035 .zip(unit_sel.iter())
13036 .filter(|(_, keep)| **keep)
13037 .flat_map(|(u, _)| u.iter().cloned())
13038 .collect()
13039 };
13040 let n = units.len();
13041 if is_cube {
13042 let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
13043 .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
13044 .collect();
13045 subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
13046 return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
13047 }
13048 return Ok((0..=n)
13049 .rev()
13050 .map(|keep| {
13051 let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
13052 flatten(&sel)
13053 })
13054 .collect());
13055 }
13056 // GROUPING SETS ( item [, item]* )
13057 if is_kw(self.peek(), "grouping")
13058 && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
13059 {
13060 self.advance(); // GROUPING
13061 self.advance(); // SETS
13062 if !matches!(self.peek(), Token::LParen) {
13063 return Err(self.err(format!(
13064 "expected '(' after GROUPING SETS, got {:?}",
13065 self.peek()
13066 )));
13067 }
13068 self.advance(); // outer (
13069 let mut sets: Vec<Vec<Expr>> = Vec::new();
13070 loop {
13071 if matches!(self.peek(), Token::LParen) {
13072 // A parenthesized key list (or the empty set).
13073 self.advance();
13074 let mut set = Vec::new();
13075 if !matches!(self.peek(), Token::RParen) {
13076 loop {
13077 set.push(self.parse_expr(0)?);
13078 match self.peek() {
13079 Token::Comma => {
13080 self.advance();
13081 }
13082 Token::RParen => break,
13083 other => {
13084 return Err(self.err(format!(
13085 "expected ',' or ')' in grouping set, got {other:?}"
13086 )));
13087 }
13088 }
13089 }
13090 }
13091 self.advance(); // )
13092 sets.push(set);
13093 } else {
13094 // A nested element: ROLLUP/CUBE/GROUPING SETS or a
13095 // bare expression.
13096 sets.extend(self.parse_grouping_element()?);
13097 }
13098 match self.peek() {
13099 Token::Comma => {
13100 self.advance();
13101 }
13102 Token::RParen => break,
13103 other => {
13104 return Err(self.err(format!(
13105 "expected ',' or ')' after a grouping set, got {other:?}"
13106 )));
13107 }
13108 }
13109 }
13110 self.advance(); // outer )
13111 return Ok(sets);
13112 }
13113 Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
13114 }
13115
13116 fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
13117 // v7.38 (read01) — a reference to a key that is dropped in this grouping
13118 // set evaluates to NULL, at any depth. Previously only a *top-level*
13119 // select item equal to a dropped key was nullified, so a key nested in
13120 // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13121 // column and failed to resolve against the set's synthetic schema.
13122 if dropped.iter().any(|d| d == expr) {
13123 *expr = Expr::Literal(Literal::Null);
13124 return;
13125 }
13126 if let Expr::FunctionCall { name, args } = expr
13127 && name.eq_ignore_ascii_case("grouping")
13128 {
13129 let mut mask: i64 = 0;
13130 for a in args.iter() {
13131 mask <<= 1;
13132 if dropped.iter().any(|d| d == a) {
13133 mask |= 1;
13134 }
13135 }
13136 // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13137 // literal: a bare integer in a select item is indistinguishable
13138 // from a positional reference once `ORDER BY 1` substitutes the
13139 // item back in, and the round-232 position check then read the
13140 // mask value as an out-of-range position. The cast changes
13141 // nothing semantically (grouping() is integer).
13142 *expr = Expr::Cast {
13143 expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13144 target: crate::ast::CastTarget::Int,
13145 };
13146 return;
13147 }
13148 // Generic recursion over the common expression shapes the
13149 // SELECT list uses; anything without child expressions is
13150 // left alone.
13151 match expr {
13152 Expr::FunctionCall { args, .. } => {
13153 for a in args {
13154 Self::substitute_grouping_calls(a, dropped);
13155 }
13156 }
13157 Expr::Binary { lhs, rhs, .. } => {
13158 Self::substitute_grouping_calls(lhs, dropped);
13159 Self::substitute_grouping_calls(rhs, dropped);
13160 }
13161 Expr::Unary { expr: inner, .. } => {
13162 Self::substitute_grouping_calls(inner, dropped);
13163 }
13164 Expr::Cast { expr: inner, .. } => {
13165 Self::substitute_grouping_calls(inner, dropped);
13166 }
13167 Expr::Case {
13168 operand,
13169 branches,
13170 else_branch,
13171 } => {
13172 if let Some(op) = operand {
13173 Self::substitute_grouping_calls(op, dropped);
13174 }
13175 for (w, t) in branches {
13176 Self::substitute_grouping_calls(w, dropped);
13177 Self::substitute_grouping_calls(t, dropped);
13178 }
13179 if let Some(e) = else_branch {
13180 Self::substitute_grouping_calls(e, dropped);
13181 }
13182 }
13183 // v7.38 (read01) — recurse into the remaining child-bearing shapes
13184 // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13185 // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13186 // …` is the canonical rollup-total label idiom).
13187 Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13188 Expr::Like { expr, pattern, .. } => {
13189 Self::substitute_grouping_calls(expr, dropped);
13190 Self::substitute_grouping_calls(pattern, dropped);
13191 }
13192 Expr::InList { expr, list, .. } => {
13193 Self::substitute_grouping_calls(expr, dropped);
13194 for item in list {
13195 Self::substitute_grouping_calls(item, dropped);
13196 }
13197 }
13198 Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13199 Expr::Array(items) => {
13200 for item in items {
13201 Self::substitute_grouping_calls(item, dropped);
13202 }
13203 }
13204 Expr::ArraySubscript { target, index } => {
13205 Self::substitute_grouping_calls(target, dropped);
13206 Self::substitute_grouping_calls(index, dropped);
13207 }
13208 Expr::ArraySlice { target, lo, hi } => {
13209 Self::substitute_grouping_calls(target, dropped);
13210 if let Some(lo) = lo {
13211 Self::substitute_grouping_calls(lo, dropped);
13212 }
13213 if let Some(hi) = hi {
13214 Self::substitute_grouping_calls(hi, dropped);
13215 }
13216 }
13217 Expr::AnyAll { expr, array, .. } => {
13218 Self::substitute_grouping_calls(expr, dropped);
13219 Self::substitute_grouping_calls(array, dropped);
13220 }
13221 _ => {}
13222 }
13223 }
13224
13225 fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13226 // v7.37.17 (17.6 siblings) — parenthesized set-operation
13227 // group: `( <select chain> )` usable anywhere a query block
13228 // is (head or peer of an outer chain). The group's own
13229 // unions ride the returned SelectStatement; the executor's
13230 // nested-peer recursion runs them.
13231 if matches!(self.peek(), Token::LParen)
13232 && matches!(
13233 self.tokens.get(self.pos + 1),
13234 Some(Token::Select | Token::LParen | Token::Values)
13235 )
13236 {
13237 self.advance(); // (
13238 self.enter_nested()?;
13239 // v7.37 D.20 — a group whose head is a VALUES list:
13240 // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13241 // otherwise recurse into a nested SELECT/group head.
13242 let mut head = (if matches!(self.peek(), Token::Values) {
13243 self.advance(); // VALUES
13244 self.parse_values_rows_body()
13245 } else {
13246 self.parse_bare_select()
13247 })
13248 .and_then(|mut h| {
13249 self.parse_setop_chain_into(&mut h)?;
13250 Ok(h)
13251 });
13252 self.nest_depth -= 1;
13253 let mut head = match &mut head {
13254 Ok(h) => core::mem::take(h),
13255 Err(_) => return head,
13256 };
13257 // v7.37.17 (17.6 siblings) — group-internal tail:
13258 // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13259 // group head, then wrap the group as a derived table
13260 // (SELECT * FROM (group)) so the outer chain / outer
13261 // tail can't clobber the group's own ordering or limit.
13262 let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13263 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13264 if s.eq_ignore_ascii_case("fetch"));
13265 if has_tail {
13266 self.parse_select_tail_into(&mut head)?;
13267 head = SelectStatement {
13268 locking: None,
13269 ctes: Vec::new(),
13270 distinct: false,
13271 distinct_on: Vec::new(),
13272 items: alloc::vec![SelectItem::Wildcard],
13273 from: Some(FromClause {
13274 primary: TableRef {
13275 name: "subquery".to_string(),
13276 alias: None,
13277 only: false,
13278 as_of_segment: None,
13279 unnest_expr: None,
13280 unnest_column_aliases: Vec::new(),
13281 with_ordinality: false,
13282 generate_series_args: None,
13283 lateral_subquery: Some(Box::new(head)),
13284 jsonb_each_text_arg: None,
13285 table_fn_call: None,
13286 rows_from: None,
13287 json_table: None,
13288 scalar_fn_item: false,
13289 },
13290 joins: Vec::new(),
13291 }),
13292 where_: None,
13293 group_by: None,
13294 group_by_all: false,
13295 having: None,
13296 unions: Vec::new(),
13297 order_by: Vec::new(),
13298 limit: None,
13299 offset: None,
13300 limit_with_ties: false,
13301 window_check_exprs: Vec::new(),
13302 };
13303 }
13304 if !matches!(self.peek(), Token::RParen) {
13305 return Err(self.err(format!(
13306 "expected ')' after parenthesized query group, got {:?}",
13307 self.peek()
13308 )));
13309 }
13310 self.advance();
13311 return Ok(head);
13312 }
13313 // `TABLE name` shorthand as a query block — valid anywhere
13314 // a SELECT head is (set-op peers included).
13315 if matches!(self.peek(), Token::Table)
13316 && matches!(
13317 self.tokens.get(self.pos + 1),
13318 Some(Token::Ident(_) | Token::QuotedIdent(_))
13319 )
13320 {
13321 return self.parse_table_shorthand();
13322 }
13323 if !matches!(self.peek(), Token::Select) {
13324 return Err(self.err(format!(
13325 "expected SELECT to start a query block, got {:?}",
13326 self.peek()
13327 )));
13328 }
13329 self.advance();
13330 let distinct = if matches!(self.peek(), Token::Distinct) {
13331 self.advance();
13332 true
13333 } else {
13334 false
13335 };
13336 // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13337 // keep the first row (per ORDER BY) of each group the
13338 // expressions define. Django's .distinct('field') shape.
13339 let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13340 self.advance(); // ON
13341 if !matches!(self.peek(), Token::LParen) {
13342 return Err(self.err(format!(
13343 "expected '(' after DISTINCT ON, got {:?}",
13344 self.peek()
13345 )));
13346 }
13347 self.advance();
13348 let mut exprs = Vec::new();
13349 loop {
13350 exprs.push(self.parse_expr(0)?);
13351 match self.peek() {
13352 Token::Comma => {
13353 self.advance();
13354 }
13355 Token::RParen => break,
13356 other => {
13357 return Err(self.err(format!(
13358 "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13359 )));
13360 }
13361 }
13362 }
13363 self.advance(); // )
13364 exprs
13365 } else {
13366 Vec::new()
13367 };
13368 let mut items = self.parse_select_list()?;
13369 // Scope the TABLESAMPLE lowering channel to this SELECT:
13370 // stash whatever an enclosing select accumulated, collect
13371 // our own FROM's predicates, restore after the combine.
13372 let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13373 let mut from = if matches!(self.peek(), Token::From) {
13374 self.advance();
13375 Some(self.parse_from_clause()?)
13376 } else {
13377 None
13378 };
13379 // v7.37 D.22 — a set-returning function in the projection with no FROM
13380 // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13381 // rows. Move the first SRF projection item to a FROM-position derived
13382 // table and replace it in the projection with a reference to its output
13383 // column; sibling scalar columns repeat per SRF row. PG names the output
13384 // column after the function (or its AS alias). Reuses the FROM-SRF
13385 // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13386 // works via the targetlist-SRF path.
13387 // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13388 // `SELECT * FROM f(args)` — the record's fields become the columns, which
13389 // is exactly what the function's own row shape already is. Anywhere else
13390 // (per outer row, or beside other items) it would need a real record-typed
13391 // projection, so it says so rather than answering something else.
13392 if let [
13393 SelectItem::Expr {
13394 expr: Expr::FunctionCall { name, args },
13395 ..
13396 },
13397 ] = items.as_slice()
13398 && name == "__record_expand"
13399 {
13400 let Some(Expr::FunctionCall {
13401 name: inner_name,
13402 args: inner_args,
13403 }) = args.first()
13404 else {
13405 return Err(self.err(
13406 "(<expr>).* expands a function's record — it needs a function call".into(),
13407 ));
13408 };
13409 if from.is_some() {
13410 return Err(self.err(
13411 "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13412 .into(),
13413 ));
13414 }
13415 let fn_ref = TableRef {
13416 name: inner_name.clone(),
13417 alias: None,
13418 only: false,
13419 as_of_segment: None,
13420 unnest_expr: None,
13421 unnest_column_aliases: Vec::new(),
13422 with_ordinality: false,
13423 generate_series_args: None,
13424 lateral_subquery: None,
13425 jsonb_each_text_arg: None,
13426 table_fn_call: Some(Box::new((
13427 inner_name.to_ascii_lowercase(),
13428 inner_args.clone(),
13429 ))),
13430 rows_from: None,
13431 json_table: None,
13432 scalar_fn_item: false,
13433 };
13434 items = alloc::vec![SelectItem::Wildcard];
13435 from = Some(FromClause {
13436 primary: fn_ref,
13437 joins: Vec::new(),
13438 });
13439 }
13440 // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13441 // FROM, keeps its marker: the ENGINE lowers it, because naming the
13442 // record's fields takes the catalog. It becomes a LATERAL of the same
13443 // function plus one item per declared column — the machinery rounds 65
13444 // and 69 already built.
13445 // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13446 // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13447 // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13448 // express, since the lifted one becomes a scan and the other would
13449 // expand per its rows (a cross product, not a zip). So when the
13450 // projection holds more than one top-level function call, the lift steps
13451 // aside and the engine's target-list expansion takes the whole list.
13452 let fn_call_items = items
13453 .iter()
13454 .filter(|it| {
13455 matches!(
13456 it,
13457 SelectItem::Expr {
13458 expr: Expr::FunctionCall { .. },
13459 ..
13460 }
13461 )
13462 })
13463 .count();
13464 if from.is_none() && fn_call_items <= 1 {
13465 let mut found: Option<(usize, TableRef, String)> = None;
13466 for (i, item) in items.iter().enumerate() {
13467 if let SelectItem::Expr {
13468 expr: Expr::FunctionCall { name, args },
13469 alias,
13470 } = item
13471 {
13472 let lname = name.to_ascii_lowercase();
13473 let colname = alias.clone().unwrap_or_else(|| lname.clone());
13474 let (unnest, gs) = match lname.as_str() {
13475 "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13476 "generate_series" if (2..=3).contains(&args.len()) => {
13477 (None, Some(args.clone()))
13478 }
13479 // v7.38 (read01) — generate_subscripts(arr, dim) in a
13480 // no-FROM projection yields the 1-based subscripts, i.e.
13481 // generate_series(1, array_length(arr, dim)); an invalid
13482 // dimension makes array_length NULL → 0 rows, as in PG.
13483 "generate_subscripts" if args.len() == 2 => (
13484 None,
13485 Some(alloc::vec![
13486 Expr::Literal(Literal::Integer(1)),
13487 Expr::FunctionCall {
13488 name: "array_length".to_string(),
13489 args: args.clone(),
13490 },
13491 ]),
13492 ),
13493 // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13494 // in a no-FROM projection unnest their *_to_array form.
13495 "string_to_table" | "regexp_split_to_table" => {
13496 let array_fn = if lname == "string_to_table" {
13497 "string_to_array"
13498 } else {
13499 "regexp_split_to_array"
13500 };
13501 (
13502 Some(Box::new(Expr::FunctionCall {
13503 name: array_fn.to_string(),
13504 args: args.clone(),
13505 })),
13506 None,
13507 )
13508 }
13509 // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13510 // a no-FROM projection expand per element. The scalar form
13511 // returns the elements as a TEXT array, so unnest over the
13512 // same call materialises one row each (same rewrite the
13513 // FROM-clause form uses).
13514 "jsonb_array_elements"
13515 | "json_array_elements"
13516 | "jsonb_array_elements_text"
13517 | "json_array_elements_text"
13518 if args.len() == 1 =>
13519 {
13520 (
13521 Some(Box::new(Expr::FunctionCall {
13522 name: lname.clone(),
13523 args: args.clone(),
13524 })),
13525 None,
13526 )
13527 }
13528 // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13529 // in a no-FROM projection expands per match (scalar form
13530 // returns the matches as a TEXT array → unnest).
13531 "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13532 Some(Box::new(Expr::FunctionCall {
13533 name: lname.clone(),
13534 args: args.clone(),
13535 })),
13536 None,
13537 ),
13538 _ => continue,
13539 };
13540 found = Some((
13541 i,
13542 TableRef {
13543 name: colname.clone(),
13544 alias: Some(colname.clone()),
13545 only: false,
13546 as_of_segment: None,
13547 unnest_expr: unnest,
13548 unnest_column_aliases: alloc::vec![colname.clone()],
13549 with_ordinality: false,
13550 generate_series_args: gs,
13551 lateral_subquery: None,
13552 jsonb_each_text_arg: None,
13553 table_fn_call: None,
13554 rows_from: None,
13555 json_table: None,
13556 scalar_fn_item: false,
13557 },
13558 colname,
13559 ));
13560 break;
13561 }
13562 }
13563 if let Some((idx, tref, colname)) = found {
13564 from = Some(FromClause {
13565 primary: tref,
13566 joins: Vec::new(),
13567 });
13568 items[idx] = SelectItem::Expr {
13569 expr: Expr::Column(ColumnName {
13570 qualifier: None,
13571 name: colname.clone(),
13572 }),
13573 alias: Some(colname),
13574 };
13575 }
13576 }
13577 let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13578 let where_ = if matches!(self.peek(), Token::Where) {
13579 self.advance();
13580 Some(self.parse_expr(0)?)
13581 } else {
13582 None
13583 };
13584 let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13585 Some(match acc {
13586 Some(w) => Expr::Binary {
13587 lhs: Box::new(pred),
13588 op: crate::ast::BinOp::And,
13589 rhs: Box::new(w),
13590 },
13591 None => pred,
13592 })
13593 });
13594 self.pending_sample_preds = enclosing_sample_preds;
13595 let mut group_by_all = false;
13596 // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13597 // share one expansion: `grouping_sets` lists the key subsets
13598 // (first = primary, assigned to stmt.group_by; the rest
13599 // become UNION ALL peers), `grouping_universe` is the full
13600 // key list used to compute each peer's dropped keys.
13601 let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13602 let mut grouping_universe: Vec<Expr> = Vec::new();
13603 // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13604 // A BOOL, not the key list: this frame is the statement parser's, and
13605 // round 430 measured that a `Vec` local here is enough on its own to
13606 // tip the 512 KiB nesting guard. The keys are recoverable from
13607 // `grouping_universe`, which a rollup fills with exactly them.
13608 let mut mysql_rollup = false;
13609 let group_by = if matches!(self.peek(), Token::Group) {
13610 self.advance();
13611 if !self.peek_is_by() {
13612 return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13613 }
13614 self.advance();
13615 // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13616 // every non-aggregate SELECT-list item later.
13617 if matches!(self.peek(), Token::All) {
13618 self.advance();
13619 group_by_all = true;
13620 None
13621 } else {
13622 // v7.39 (round 242) — PG's general grouping-element grammar:
13623 // GROUP BY [DISTINCT] element [, element]*, where an element
13624 // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13625 // SETS (…) — mixed freely. Each element yields a list of
13626 // key sets; the query's grouping sets are the CARTESIAN
13627 // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13628 // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13629 // content. ROLLUP/CUBE members may be composite
13630 // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13631 // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13632 // parser handled only a lone ROLLUP/CUBE/GS as the whole
13633 // clause.
13634 let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13635 self.advance();
13636 true
13637 } else {
13638 false
13639 };
13640 let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13641 loop {
13642 element_sets.push(self.parse_grouping_element()?);
13643 if matches!(self.peek(), Token::Comma) {
13644 self.advance();
13645 } else {
13646 break;
13647 }
13648 }
13649 let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13650 for el in &element_sets {
13651 let mut next: Vec<Vec<Expr>> = Vec::new();
13652 for base in &total {
13653 for set in el {
13654 let mut merged = base.clone();
13655 for k in set {
13656 if !merged.iter().any(|m| m == k) {
13657 merged.push(k.clone());
13658 }
13659 }
13660 next.push(merged);
13661 }
13662 }
13663 total = next;
13664 }
13665 // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13666 // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13667 // The keys and the aggregates come out identical; the ROW
13668 // ORDER does not, and that is the part a report depends on.
13669 // MySQL interleaves each group's subtotal right after its
13670 // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13671 // where the union-of-grouping-sets expansion emits every
13672 // leaf first and then every subtotal. MariaDB REFUSES an
13673 // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13674 // order itself — measured on MariaDB 11 and MySQL 9.7, which
13675 // agree on the order and disagree only on whether ORDER BY
13676 // is allowed (MySQL allows it; SPG allows it too, since
13677 // refusing would break the clients that can write it).
13678 if self.mysql_dialect
13679 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13680 && matches!(
13681 self.tokens.get(self.pos + 1),
13682 Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13683 )
13684 {
13685 self.advance(); // WITH
13686 self.advance(); // ROLLUP
13687 let keys = total.into_iter().next().unwrap_or_default();
13688 mysql_rollup = true;
13689 // n+1 prefixes, largest first — the same expansion
13690 // `ROLLUP (…)` produces.
13691 total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13692 }
13693 if distinct_sets {
13694 let mut seen: Vec<Vec<String>> = Vec::new();
13695 total.retain(|set| {
13696 let mut key: Vec<String> =
13697 set.iter().map(|e| alloc::format!("{e}")).collect();
13698 key.sort();
13699 if seen.contains(&key) {
13700 false
13701 } else {
13702 seen.push(key);
13703 true
13704 }
13705 });
13706 }
13707 if total.len() > 1 {
13708 let mut universe: Vec<Expr> = Vec::new();
13709 for set in &total {
13710 for k in set {
13711 if !universe.iter().any(|u| u == k) {
13712 universe.push(k.clone());
13713 }
13714 }
13715 }
13716 grouping_universe = universe;
13717 let primary = total[0].clone();
13718 grouping_sets = total;
13719 Some(primary)
13720 } else {
13721 // One set (a plain GROUP BY list, or a single-set
13722 // spelling like GROUPING SETS ((a, b))). An EMPTY
13723 // single set — GROUPING SETS (()) — stays
13724 // `Some(vec![])`: the grand-total group, which must
13725 // run the aggregate path.
13726 Some(total.into_iter().next().unwrap_or_default())
13727 }
13728 }
13729 } else {
13730 None
13731 };
13732 let having = if matches!(self.peek(), Token::Having) {
13733 self.advance();
13734 Some(self.parse_expr(0)?)
13735 } else {
13736 None
13737 };
13738 // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
13739 // OVER w parsed to a marker above; inline each definition
13740 // into the referencing WindowFunction nodes.
13741 let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
13742 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
13743 self.advance();
13744 loop {
13745 let wname = self.expect_ident_like()?;
13746 if !matches!(self.peek(), Token::As) {
13747 return Err(self.err(format!(
13748 "expected AS after WINDOW {wname}, got {:?}",
13749 self.peek()
13750 )));
13751 }
13752 self.advance();
13753 // v7.39 (round 229) — PG rejects a redefinition outright.
13754 if window_defs
13755 .iter()
13756 .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
13757 {
13758 return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
13759 }
13760 let def = self.parse_over_clause()?;
13761 // A definition may itself copy an earlier one
13762 // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
13763 // so resolve it against the defs already in scope. Same
13764 // copy rules as an `OVER (w1 …)` in the select list.
13765 let mut probe = Expr::WindowFunction {
13766 name: String::new(),
13767 args: Vec::new(),
13768 partition_by: def.0,
13769 order_by: def.1,
13770 frame: def.2,
13771 null_treatment: crate::ast::NullTreatment::Respect,
13772 filter: None,
13773 };
13774 Self::substitute_named_windows(&mut probe, &window_defs)
13775 .map_err(|m| self.err(m))?;
13776 let Expr::WindowFunction {
13777 partition_by,
13778 order_by,
13779 frame,
13780 ..
13781 } = probe
13782 else {
13783 unreachable!("probe is a WindowFunction")
13784 };
13785 window_defs.push((wname, (partition_by, order_by, frame)));
13786 if matches!(self.peek(), Token::Comma) {
13787 self.advance();
13788 continue;
13789 }
13790 break;
13791 }
13792 }
13793 // v7.39 (round 705) — which definitions did anything reference?
13794 // The ones nothing did used to be dropped here, unexamined, so
13795 // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
13796 // definition whether referenced or not. Their key expressions ride
13797 // out on the statement for the engine to resolve.
13798 let mut window_refs: Vec<String> = Vec::new();
13799 if !window_defs.is_empty() {
13800 for it in &items {
13801 if let SelectItem::Expr { expr, .. } = it {
13802 Self::collect_named_window_refs(expr, &mut window_refs);
13803 }
13804 }
13805 }
13806 let window_check_exprs: Vec<Expr> = window_defs
13807 .iter()
13808 .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
13809 .flat_map(|(_, (partition, order, _))| {
13810 partition
13811 .iter()
13812 .cloned()
13813 .chain(order.iter().map(|(e, _, _)| e.clone()))
13814 })
13815 .collect();
13816 if !window_defs.is_empty()
13817 || items
13818 .iter()
13819 .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
13820 {
13821 for it in &mut items {
13822 if let SelectItem::Expr { expr, .. } = it {
13823 Self::substitute_named_windows(expr, &window_defs)
13824 .map_err(|m| self.err(m))?;
13825 }
13826 }
13827 }
13828 // `GROUP BY 1` — positional keys substitute with the Nth
13829 // select item's expression (same contract ORDER BY has had
13830 // since v6.x). Out-of-range positions error.
13831 let group_by = match group_by {
13832 Some(mut keys) => {
13833 for k in &mut keys {
13834 if let Expr::Literal(Literal::Integer(n)) = k {
13835 let idx = *n;
13836 if idx < 1 || idx as usize > items.len() {
13837 return Err(self.err(alloc::format!(
13838 "GROUP BY position {idx} is not in select list"
13839 )));
13840 }
13841 match &items[(idx - 1) as usize] {
13842 SelectItem::Expr { expr, .. } => *k = expr.clone(),
13843 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
13844 return Err(self.err(alloc::format!(
13845 "GROUP BY position {idx} references a wildcard item"
13846 )));
13847 }
13848 }
13849 }
13850 }
13851 Some(keys)
13852 }
13853 None => None,
13854 };
13855 let mut stmt = SelectStatement {
13856 locking: None,
13857 ctes: Vec::new(),
13858 distinct,
13859 distinct_on,
13860 items,
13861 from,
13862 where_,
13863 group_by,
13864 group_by_all,
13865 having,
13866 unions: Vec::new(),
13867 order_by: Vec::new(),
13868 limit: None,
13869 offset: None,
13870 limit_with_ties: false,
13871 window_check_exprs,
13872 };
13873 // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
13874 // first set is the primary (already on stmt.group_by); each
13875 // further set becomes a UNION ALL peer with its dropped
13876 // keys (universe minus the set) replaced by NULL literals
13877 // in the peer's items and group_by. PG-legal: non-grouped
13878 // select items must be group keys or aggregates, so a
13879 // dropped key's occurrences in the projection are exactly
13880 // the ones to nullify.
13881 // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
13882 // over a plain GROUP BY (every argument must be a group key; the
13883 // mask is then 0) and rejects anything else with 42803. SPG's
13884 // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
13885 // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
13886 // function `grouping`".
13887 if grouping_sets.len() <= 1 {
13888 let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
13889 let mut calls: Vec<Expr> = Vec::new();
13890 for item in &stmt.items {
13891 if let SelectItem::Expr { expr, .. } = item {
13892 Self::collect_grouping_calls(expr, &mut calls);
13893 }
13894 }
13895 if let Some(h) = &stmt.having {
13896 Self::collect_grouping_calls(h, &mut calls);
13897 }
13898 for call in &calls {
13899 let Expr::FunctionCall { args, .. } = call else {
13900 continue;
13901 };
13902 for a in args {
13903 if !keys.iter().any(|k| k == a) {
13904 return Err(self.err(
13905 "arguments to GROUPING must be grouping expressions of the associated query level"
13906 .to_string(),
13907 ));
13908 }
13909 }
13910 }
13911 if !calls.is_empty() {
13912 for item in &mut stmt.items {
13913 if let SelectItem::Expr { expr, .. } = item {
13914 Self::substitute_grouping_calls(expr, &[]);
13915 }
13916 }
13917 if let Some(h) = &mut stmt.having {
13918 Self::substitute_grouping_calls(h, &[]);
13919 }
13920 }
13921 }
13922 if grouping_sets.len() > 1 {
13923 // The primary set's own dropped keys nullify in the
13924 // HEAD's projection too (GROUPING SETS's first set may
13925 // omit keys other sets use).
13926 let primary = grouping_sets[0].clone();
13927 let head_dropped: Vec<Expr> = grouping_universe
13928 .iter()
13929 .filter(|u| !primary.iter().any(|k| k == *u))
13930 .cloned()
13931 .collect();
13932 for set in grouping_sets.iter().skip(1) {
13933 let mut peer = stmt.clone();
13934 peer.unions = Vec::new();
13935 let dropped: Vec<&Expr> = grouping_universe
13936 .iter()
13937 .filter(|u| !set.iter().any(|k| k == *u))
13938 .collect();
13939 // Empty set = grand-total group: `Some(vec![])` forces
13940 // the aggregate path (one collapsed row) instead of a
13941 // per-row passthrough. See the primary-set note above.
13942 peer.group_by = Some(set.clone());
13943 let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
13944 for item in &mut peer.items {
13945 if let SelectItem::Expr { expr, alias } = item {
13946 if dropped.iter().any(|d| *d == expr) {
13947 // v7.39 — keep the dropped key's name on the
13948 // NULL literal so the UNION output column
13949 // (and any top-level ORDER BY on it) still
13950 // resolves.
13951 if alias.is_none()
13952 && let Expr::Column(c) = &expr
13953 {
13954 *alias = Some(c.name.clone());
13955 }
13956 *expr = Expr::Literal(Literal::Null);
13957 } else {
13958 Self::substitute_grouping_calls(expr, &dropped_owned);
13959 }
13960 }
13961 }
13962 if let Some(h) = &mut peer.having {
13963 Self::substitute_grouping_calls(h, &dropped_owned);
13964 }
13965 stmt.unions.push((UnionKind::All, peer));
13966 }
13967 for item in &mut stmt.items {
13968 if let SelectItem::Expr { expr, alias } = item {
13969 if head_dropped.iter().any(|d| d == expr) {
13970 if alias.is_none()
13971 && let Expr::Column(c) = &expr
13972 {
13973 *alias = Some(c.name.clone());
13974 }
13975 *expr = Expr::Literal(Literal::Null);
13976 } else {
13977 Self::substitute_grouping_calls(expr, &head_dropped);
13978 }
13979 }
13980 }
13981 if let Some(h) = &mut stmt.having {
13982 Self::substitute_grouping_calls(h, &head_dropped);
13983 }
13984 // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
13985 // (while `grouping_universe` / the per-branch sets are in scope). For
13986 // each grouping() call in it, inject a per-branch hidden column
13987 // `__grp_ord_K` carrying that branch's mask into the head + every
13988 // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
13989 // preserves this pre-set order_by; the engine strips `__grp_ord_*`
13990 // from the final output. A standalone grouping-set query has ORDER BY
13991 // (not an explicit set-op) next, so consuming it here is safe.
13992 // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
13993 // rollup carries the hierarchical order: sort by the grouping
13994 // keys with the rolled-up NULLs last, which is exactly the
13995 // interleaving both oracles emit. A client's own ORDER BY wins,
13996 // which is what MySQL does (MariaDB refuses to let one be
13997 // written at all).
13998 // The synthesised keys have to travel the SAME path a written
13999 // ORDER BY does: the block below is what turns a `grouping()`
14000 // call into the per-branch `__grp_ord_K` column the engine can
14001 // actually sort on. Bypassing it left a bare `grouping(text)`
14002 // for the evaluator to reject.
14003 let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
14004 self.parse_order_by_keys()?
14005 } else if mysql_rollup {
14006 Self::mysql_rollup_order(&grouping_universe)
14007 } else {
14008 Vec::new()
14009 };
14010 if !synthesised_or_parsed.is_empty() {
14011 let mut order_keys = synthesised_or_parsed;
14012 let mut grp_exprs: Vec<Expr> = Vec::new();
14013 for ob in &order_keys {
14014 Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
14015 }
14016 for (k, gexpr) in grp_exprs.iter().enumerate() {
14017 let colname = alloc::format!("__grp_ord_{k}");
14018 // Head branch (primary set) uses `head_dropped`.
14019 let mut he = gexpr.clone();
14020 Self::substitute_grouping_calls(&mut he, &head_dropped);
14021 stmt.items.push(SelectItem::Expr {
14022 expr: he,
14023 alias: Some(colname.clone()),
14024 });
14025 // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
14026 for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14027 let set = &grouping_sets[i + 1];
14028 let dropped: Vec<Expr> = grouping_universe
14029 .iter()
14030 .filter(|u| !set.iter().any(|k| k == *u))
14031 .cloned()
14032 .collect();
14033 let mut pe = gexpr.clone();
14034 Self::substitute_grouping_calls(&mut pe, &dropped);
14035 peer.items.push(SelectItem::Expr {
14036 expr: pe,
14037 alias: Some(colname.clone()),
14038 });
14039 }
14040 }
14041 for ob in &mut order_keys {
14042 Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
14043 }
14044 stmt.order_by = order_keys;
14045 }
14046 }
14047 Ok(stmt)
14048 }
14049
14050 /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
14051 /// as ORDER BY keys.
14052 ///
14053 /// Per key: the rollup marker, then the key. Sorting on the key alone
14054 /// is not enough, and a table with a NULL in it says why — MariaDB puts
14055 /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
14056 /// the ROLLUP-introduced NULL last, and both print as NULL.
14057 /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
14058 /// real group including the data-NULL one, 1 only for the row the
14059 /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
14060 /// rolls up to NULL|2, a|1, b|3, NULL|6.
14061 ///
14062 /// `#[inline(never)]`: its locals must not join the statement parser's
14063 /// frame, which round 430 measured sitting against the nesting guard.
14064 #[inline(never)]
14065 fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
14066 let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
14067 for e in keys {
14068 out.push(OrderBy {
14069 expr: Expr::FunctionCall {
14070 name: "grouping".into(),
14071 args: alloc::vec![e.clone()],
14072 },
14073 desc: false,
14074 nulls_first: None,
14075 collation: None,
14076 });
14077 out.push(OrderBy {
14078 expr: e.clone(),
14079 desc: false,
14080 // MySQL orders NULL first on an ascending key.
14081 nulls_first: Some(true),
14082 collation: None,
14083 });
14084 }
14085 out
14086 }
14087
14088 /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
14089 /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
14090 #[inline(never)]
14091 fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
14092 use crate::ast::MaintainKind;
14093 self.skip_paren_option_list();
14094 let kind = match self.peek() {
14095 // `TABLE` and `INDEX` lex as keywords, not identifiers.
14096 Token::Table | Token::Index => {
14097 self.advance();
14098 MaintainKind::ReindexRelation
14099 }
14100 Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
14101 "index" | "table" => {
14102 self.advance();
14103 MaintainKind::ReindexRelation
14104 }
14105 "schema" => {
14106 self.advance();
14107 MaintainKind::ReindexSchema
14108 }
14109 "system" | "database" => {
14110 self.advance();
14111 MaintainKind::Whole
14112 }
14113 // PG requires the object type; anything else is the
14114 // caller's problem, not something to swallow.
14115 _ => MaintainKind::ReindexRelation,
14116 },
14117 _ => MaintainKind::Whole,
14118 };
14119 // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
14120 // allows the plain form, so the modifier is recorded rather than
14121 // skipped. It still has no effect on how the reindex runs.
14122 let mut concurrently = false;
14123 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14124 self.advance();
14125 concurrently = true;
14126 }
14127 let target = self.take_optional_maintain_name();
14128 self.consume_until_statement_boundary();
14129 Ok(Statement::Maintain {
14130 kind,
14131 concurrently,
14132 target,
14133 })
14134 }
14135
14136 /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14137 /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14138 #[inline(never)]
14139 fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14140 use crate::ast::MaintainKind;
14141 self.skip_paren_option_list();
14142 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14143 self.advance();
14144 }
14145 let target = self.take_optional_maintain_name();
14146 self.consume_until_statement_boundary();
14147 Ok(Statement::Maintain {
14148 kind: if target.is_some() {
14149 MaintainKind::ClusterRelation
14150 } else {
14151 MaintainKind::Whole
14152 },
14153 // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14154 // transaction block quite happily (measured).
14155 concurrently: false,
14156 target,
14157 })
14158 }
14159
14160 /// The next token as a relation / schema name, when there is one.
14161 fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14162 match self.peek() {
14163 Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14164 Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14165 _ => None,
14166 },
14167 _ => None,
14168 }
14169 }
14170
14171 /// A parenthesised option list, absorbed.
14172 fn skip_paren_option_list(&mut self) {
14173 if !matches!(self.peek(), Token::LParen) {
14174 return;
14175 }
14176 let mut depth = 0usize;
14177 loop {
14178 match self.advance() {
14179 Token::LParen => depth += 1,
14180 Token::RParen => {
14181 depth -= 1;
14182 if depth == 0 {
14183 return;
14184 }
14185 }
14186 Token::Eof => return,
14187 _ => {}
14188 }
14189 }
14190 }
14191
14192 /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14193 /// column list.
14194 ///
14195 /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14196 /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14197 /// / ALL. The three that describe physical storage have no meaning
14198 /// here, so they parse and change nothing rather than making a
14199 /// dump that mentions them fail to load.
14200 ///
14201 /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14202 /// parse chain the nesting sentinel is tuned against.
14203 #[inline(never)]
14204 fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14205 self.advance(); // LIKE
14206 let source = self.expect_ident_like()?;
14207 let mut options = crate::ast::LikeOptions::default();
14208 loop {
14209 let including = match self.peek() {
14210 Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14211 Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14212 _ => break,
14213 };
14214 self.advance();
14215 // `ALL` lexes as its own keyword, not an identifier.
14216 let opt = if matches!(self.peek(), Token::All) {
14217 self.advance();
14218 alloc::string::String::from("all")
14219 } else {
14220 self.expect_ident_like()?
14221 };
14222 let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14223 o.defaults = on;
14224 o.constraints = on;
14225 o.identity = on;
14226 o.generated = on;
14227 o.indexes = on;
14228 o.comments = on;
14229 };
14230 match opt.to_ascii_lowercase().as_str() {
14231 "all" => set(&mut options, including),
14232 "defaults" => options.defaults = including,
14233 "constraints" => options.constraints = including,
14234 "identity" => options.identity = including,
14235 "generated" => options.generated = including,
14236 "indexes" => options.indexes = including,
14237 "comments" => options.comments = including,
14238 // No storage model to copy into.
14239 "storage" | "statistics" | "compression" => {}
14240 other => {
14241 return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14242 }
14243 }
14244 }
14245 Ok(crate::ast::LikeSpec {
14246 source,
14247 at,
14248 options,
14249 })
14250 }
14251
14252 fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14253 // Caller already consumed CREATE; we're sitting on TABLE.
14254 debug_assert!(matches!(self.peek(), Token::Table));
14255 self.advance();
14256 let if_not_exists = self.consume_if_not_exists();
14257 let name = self.expect_ident_like()?;
14258 // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14259 // child shape has no column list; the child inherits its
14260 // columns from the parent at engine-DDL time. Detect it
14261 // before the `(` requirement below.
14262 if matches!(self.peek(), Token::Partition)
14263 && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14264 {
14265 self.advance(); // PARTITION
14266 self.advance(); // of
14267 let partition_of = self.parse_partition_of_tail()?;
14268 return Ok(Statement::CreateTable(CreateTableStatement {
14269 temporary: false,
14270 name,
14271 columns: Vec::new(),
14272 like_specs: Vec::new(),
14273 inherits: Vec::new(),
14274 if_not_exists,
14275 foreign_keys: Vec::new(),
14276 table_constraints: Vec::new(),
14277 partition_by: None,
14278 partition_of: Some(partition_of),
14279 }));
14280 }
14281 // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14282 // the materialized-view materialisation path (run the SELECT, infer the
14283 // column types, create + populate the table) but marks the node so the
14284 // executor creates a plain table without a mat-view registry entry.
14285 if matches!(self.peek(), Token::As) {
14286 self.advance();
14287 let body_stmt = self.parse_select_stmt()?;
14288 let Statement::Select(body) = body_stmt else {
14289 return Err(self.err(format!(
14290 "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14291 )));
14292 };
14293 let with_data = self.parse_optional_with_data(true)?;
14294 return Ok(Statement::CreateMaterializedView(
14295 crate::ast::CreateMaterializedViewStatement {
14296 temporary: false,
14297 name,
14298 if_not_exists,
14299 columns: Vec::new(),
14300 body,
14301 with_data,
14302 as_plain_table: true,
14303 },
14304 ));
14305 }
14306 if !matches!(self.peek(), Token::LParen) {
14307 return Err(self.err(format!(
14308 "expected '(' after table name, got {:?}",
14309 self.peek()
14310 )));
14311 }
14312 self.advance();
14313 let mut columns = Vec::new();
14314 let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14315 let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14316 let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14317 loop {
14318 // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14319 // column list. It is how a child that adds nothing of its own is
14320 // written, and this loop demanded at least one entry: `syntax
14321 // error at or near ")"`. The child takes the parent's columns,
14322 // which the INHERITS clause already arranges.
14323 if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14324 self.advance();
14325 break;
14326 }
14327 // v7.6.0 / v7.9.18 — distinguish table-level constraint
14328 // clauses from column definitions. Constraints start
14329 // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14330 // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14331 // a column.
14332 if self.peek_table_level_pk_start() {
14333 table_constraints.push(self.parse_table_level_primary_key()?);
14334 } else if matches!(self.peek(), Token::Like) {
14335 // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14336 // <opt> ]*`. The source table's shape lives in the catalog,
14337 // so this records the clause and the engine expands it.
14338 like_specs.push(self.parse_create_table_like(columns.len())?);
14339 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14340 // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14341 table_constraints.push(self.parse_table_level_exclude()?);
14342 } else if self.peek_table_level_unique_start() {
14343 table_constraints.push(self.parse_table_level_unique()?);
14344 } else if self.peek_table_level_check_start() {
14345 // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14346 table_constraints.push(self.parse_table_level_check()?);
14347 } else if self.peek_mysql_inline_key_start() {
14348 // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14349 // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14350 // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14351 // inside the column list. Skip name + paren list;
14352 // for UNIQUE KEY, register as a UC.
14353 if let Some(uc) = self.parse_mysql_inline_key()? {
14354 table_constraints.push(uc);
14355 }
14356 } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14357 // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14358 // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14359 // CHECK is named, and the named-CONSTRAINT arm used
14360 // to accept FOREIGN KEY only. The name is accepted
14361 // and discarded — same handling as every other SPG
14362 // constraint name.
14363 self.advance(); // CONSTRAINT
14364 // v7.39 (read01 round 48) — the name is kept now: the schema
14365 // stores it, so DROP / RENAME CONSTRAINT can find it.
14366 let con_name = self.expect_ident_like()?;
14367 let mut tc = match kind {
14368 NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14369 NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14370 NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14371 NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14372 };
14373 match &mut tc {
14374 crate::ast::TableConstraint::Check { name, .. }
14375 | crate::ast::TableConstraint::Unique { name, .. }
14376 | crate::ast::TableConstraint::PrimaryKey { name, .. }
14377 | crate::ast::TableConstraint::Exclude { name, .. } => {
14378 *name = Some(con_name);
14379 }
14380 _ => {}
14381 }
14382 table_constraints.push(tc);
14383 } else if self.peek_constraint_or_fk_start() {
14384 foreign_keys.push(self.parse_table_level_fk()?);
14385 } else {
14386 let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14387 // v7.13.0 — fold inline UNIQUE / CHECK column
14388 // constraints into table-level entries so the
14389 // engine path stays uniform.
14390 if col.is_unique {
14391 table_constraints.push(crate::ast::TableConstraint::Unique {
14392 name: None,
14393 columns: alloc::vec![col.name.clone()],
14394 nulls_not_distinct: col.unique_nulls_not_distinct,
14395 deferrable: col.constraint_deferrable,
14396 initially_deferred: col.constraint_initially_deferred,
14397 });
14398 }
14399 if let Some(check_expr) = col.check.clone() {
14400 table_constraints.push(crate::ast::TableConstraint::Check {
14401 name: None,
14402 expr: check_expr,
14403 not_valid: false,
14404 });
14405 }
14406 columns.push(col);
14407 if let Some(fk) = col_level_fk {
14408 foreign_keys.push(fk);
14409 }
14410 }
14411 match self.peek() {
14412 Token::Comma => {
14413 self.advance();
14414 }
14415 Token::RParen => {
14416 self.advance();
14417 break;
14418 }
14419 other => {
14420 return Err(
14421 self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14422 );
14423 }
14424 }
14425 }
14426 // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14427 // `CREATE TABLE k (LIKE t)` is a complete definition even though
14428 // nothing is written between the parentheses.
14429 // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14430 // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14431 // empty parentheses were a parse error in their own right — quite apart
14432 // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14433 // SPG does not have (filed separately).
14434 let _ = &like_specs;
14435 // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14436 // It sits between the column list and the MySQL table options,
14437 // and it was a syntax error until this round.
14438 let mut inherits: Vec<String> = Vec::new();
14439 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14440 if k.eq_ignore_ascii_case("inherits"))
14441 {
14442 self.advance();
14443 if !matches!(self.peek(), Token::LParen) {
14444 return Err(self.err(alloc::format!(
14445 "expected ( after INHERITS, got {:?}",
14446 self.peek()
14447 )));
14448 }
14449 self.advance();
14450 loop {
14451 inherits.push(self.expect_ident_like()?);
14452 if matches!(self.peek(), Token::Comma) {
14453 self.advance();
14454 continue;
14455 }
14456 break;
14457 }
14458 if !matches!(self.peek(), Token::RParen) {
14459 return Err(self.err(alloc::format!(
14460 "expected ) closing INHERITS, got {:?}",
14461 self.peek()
14462 )));
14463 }
14464 self.advance();
14465 }
14466 // v7.14.0 — consume MySQL/MariaDB table options after the
14467 // closing `)`. mysqldump emits things like
14468 // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14469 // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14470 // SPG accepts all forms as no-ops (each option is
14471 // `<ident> [=] <ident-or-string>` separated by whitespace).
14472 self.consume_mysql_table_options();
14473 // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14474 // SPG has no per-table reloptions, so accept and ignore them so a
14475 // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14476 self.consume_with_reloptions();
14477 // v7.37.6-B — declarative-partition-parent suffix
14478 // (`PARTITION BY RANGE (key_col)`) sits after the column
14479 // list + MySQL table-options. v7.37.6-B only accepts RANGE
14480 // and locks the key column at one ident; the engine then
14481 // verifies the column type is TIMESTAMPTZ.
14482 let partition_by = if matches!(self.peek(), Token::Partition) {
14483 self.advance(); // PARTITION
14484 if !self.peek_is_by() {
14485 return Err(self.err(format!(
14486 "expected BY after PARTITION, got {:?}",
14487 self.peek()
14488 )));
14489 }
14490 self.advance();
14491 Some(self.parse_partition_by_tail()?)
14492 } else {
14493 None
14494 };
14495 Ok(Statement::CreateTable(CreateTableStatement {
14496 temporary: false,
14497 name,
14498 columns,
14499 like_specs,
14500 inherits,
14501 if_not_exists,
14502 foreign_keys,
14503 table_constraints,
14504 partition_by,
14505 partition_of: None,
14506 }))
14507 }
14508
14509 /// v7.37.6-B — case-insensitive ident match helper for the
14510 /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14511 /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14512 /// didn't burn a global keyword slot for each (see the
14513 /// `Token::Partition` doc-comment in `lexer.rs`).
14514 fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14515 matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14516 }
14517
14518 /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14519 /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14520 fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14521 use crate::ast::{PartitionBySpec, PartitionKindAst};
14522 let kind = match self.peek() {
14523 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14524 self.advance();
14525 PartitionKindAst::Range
14526 }
14527 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14528 self.advance();
14529 PartitionKindAst::List
14530 }
14531 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14532 self.advance();
14533 PartitionKindAst::Hash
14534 }
14535 other => {
14536 return Err(self.err(format!(
14537 "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14538 )));
14539 }
14540 };
14541 if !matches!(self.peek(), Token::LParen) {
14542 return Err(self.err(format!(
14543 "expected '(' after PARTITION BY <strategy>, got {:?}",
14544 self.peek()
14545 )));
14546 }
14547 self.advance();
14548 let mut key_columns = Vec::new();
14549 loop {
14550 key_columns.push(self.expect_ident_like()?);
14551 match self.peek() {
14552 Token::Comma => {
14553 self.advance();
14554 }
14555 Token::RParen => {
14556 self.advance();
14557 break;
14558 }
14559 other => {
14560 return Err(self.err(format!(
14561 "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14562 )));
14563 }
14564 }
14565 }
14566 if key_columns.is_empty() {
14567 return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14568 }
14569 Ok(PartitionBySpec { kind, key_columns })
14570 }
14571
14572 /// v7.37.6-B — after `PARTITION OF`, expect
14573 /// <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14574 /// or
14575 /// <parent> DEFAULT
14576 fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14577 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14578 let parent_name = self.expect_ident_like()?;
14579 // v7.37.6-B rejects an explicit column list — the child
14580 // inherits from the parent. mailrs round-7 taught us that
14581 // CREATE TABLE-side schema reconciliation hides drift, so
14582 // we surface this as a parse error rather than silently
14583 // ignoring user columns.
14584 if matches!(self.peek(), Token::LParen) {
14585 return Err(self.err(
14586 "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14587 at v7.37.6-B; the child inherits its columns from the parent"
14588 .to_string(),
14589 ));
14590 }
14591 let bounds = match self.peek() {
14592 Token::Default => {
14593 self.advance();
14594 PartitionOfBoundsAst::Default
14595 }
14596 Token::For => {
14597 self.advance();
14598 if !matches!(self.peek(), Token::Values) {
14599 return Err(
14600 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14601 );
14602 }
14603 self.advance();
14604 // WITH is not a reserved Token in the lexer — it lexes
14605 // as Token::Ident("with"). Disambiguate manually.
14606 let want_with = matches!(
14607 self.peek(),
14608 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14609 );
14610 if want_with {
14611 self.advance();
14612 if !matches!(self.peek(), Token::LParen) {
14613 return Err(self.err(format!(
14614 "expected '(' after FOR VALUES WITH, got {:?}",
14615 self.peek()
14616 )));
14617 }
14618 self.advance();
14619 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14620 loop {
14621 let key = self.expect_ident_like()?;
14622 let n = match self.peek().clone() {
14623 Token::Integer(v) if u32::try_from(v).is_ok() => {
14624 self.advance();
14625 v as u32
14626 }
14627 other => {
14628 return Err(self.err(format!(
14629 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14630 )));
14631 }
14632 };
14633 match key.to_ascii_uppercase().as_str() {
14634 "MODULUS" => modulus = Some(n),
14635 "REMAINDER" => remainder = Some(n),
14636 other => {
14637 return Err(self.err(format!(
14638 "FOR VALUES WITH: unknown key {other:?}; \
14639 expected MODULUS or REMAINDER"
14640 )));
14641 }
14642 }
14643 match self.peek() {
14644 Token::Comma => {
14645 self.advance();
14646 }
14647 Token::RParen => {
14648 self.advance();
14649 break;
14650 }
14651 other => {
14652 return Err(self.err(format!(
14653 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14654 )));
14655 }
14656 }
14657 }
14658 let modulus = modulus
14659 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14660 let remainder = remainder.ok_or_else(|| {
14661 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14662 })?;
14663 if modulus == 0 {
14664 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14665 }
14666 if remainder >= modulus {
14667 return Err(self.err(format!(
14668 "FOR VALUES WITH: REMAINDER ({remainder}) \
14669 must be < MODULUS ({modulus})"
14670 )));
14671 }
14672 PartitionOfBoundsAst::Hash { modulus, remainder }
14673 } else {
14674 match self.peek() {
14675 Token::From => {
14676 self.advance();
14677 let lower = Box::new(self.parse_partition_bound_expr()?);
14678 if !matches!(self.peek(), Token::To) {
14679 return Err(self.err(format!(
14680 "expected TO after FROM (...), got {:?}",
14681 self.peek()
14682 )));
14683 }
14684 self.advance();
14685 let upper = Box::new(self.parse_partition_bound_expr()?);
14686 PartitionOfBoundsAst::Range { lower, upper }
14687 }
14688 // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14689 Token::In => {
14690 self.advance();
14691 if !matches!(self.peek(), Token::LParen) {
14692 return Err(self.err(format!(
14693 "expected '(' after FOR VALUES IN, got {:?}",
14694 self.peek()
14695 )));
14696 }
14697 self.advance();
14698 let mut values = Vec::new();
14699 loop {
14700 values.push(self.parse_expr(0)?);
14701 match self.peek() {
14702 Token::Comma => {
14703 self.advance();
14704 }
14705 Token::RParen => {
14706 self.advance();
14707 break;
14708 }
14709 other => {
14710 return Err(self.err(format!(
14711 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
14712 )));
14713 }
14714 }
14715 }
14716 if values.is_empty() {
14717 return Err(self.err(
14718 "FOR VALUES IN requires at least one literal".to_string(),
14719 ));
14720 }
14721 PartitionOfBoundsAst::List { values }
14722 }
14723 other => {
14724 return Err(self.err(format!(
14725 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
14726 )));
14727 }
14728 }
14729 }
14730 }
14731 other => {
14732 return Err(self.err(format!(
14733 "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
14734 )));
14735 }
14736 };
14737 Ok(PartitionOfSpec {
14738 parent_name,
14739 bounds,
14740 })
14741 }
14742
14743 /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
14744 /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
14745 /// markers (no-arg builtins) so the engine resolves them
14746 /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
14747 fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
14748 if !matches!(self.peek(), Token::LParen) {
14749 return Err(self.err(format!(
14750 "expected '(' before partition bound, got {:?}",
14751 self.peek()
14752 )));
14753 }
14754 self.advance();
14755 let expr = match self.peek() {
14756 Token::Ident(s) | Token::QuotedIdent(s)
14757 if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
14758 {
14759 let name = s.to_ascii_uppercase();
14760 self.advance();
14761 crate::ast::Expr::FunctionCall {
14762 name,
14763 args: Vec::new(),
14764 }
14765 }
14766 _ => self.parse_expr(0)?,
14767 };
14768 if !matches!(self.peek(), Token::RParen) {
14769 return Err(self.err(format!(
14770 "expected ')' after partition bound, got {:?}",
14771 self.peek()
14772 )));
14773 }
14774 self.advance();
14775 Ok(expr)
14776 }
14777
14778 /// v7.14.0 — true when the next tokens look like an inline
14779 /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
14780 /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
14781 /// — each followed by an optional name + `(...)`. Critical:
14782 /// a column NAMED `key` / `index` (PG accepts as ident) must
14783 /// NOT be mistaken for the KEY constraint shape. We disambig
14784 /// by requiring the keyword to be followed by either `(` or
14785 /// `<ident> (`.
14786 fn peek_mysql_inline_key_start(&self) -> bool {
14787 let cur = self.peek();
14788 // Shapes:
14789 // KEY (cols)
14790 // KEY name (cols)
14791 // INDEX (cols)
14792 // INDEX name (cols)
14793 // UNIQUE KEY [name] (cols)
14794 // UNIQUE INDEX [name] (cols)
14795 // FULLTEXT [KEY|INDEX] [name] (cols)
14796 // SPATIAL [KEY|INDEX] [name] (cols)
14797 let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
14798 // tokens at skip = the position AFTER the index-form
14799 // keywords (KEY/INDEX) have been consumed.
14800 match self.tokens.get(skip) {
14801 Some(Token::LParen) => true,
14802 Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
14803 matches!(self.tokens.get(skip + 1), Some(Token::LParen))
14804 }
14805 _ => false,
14806 }
14807 };
14808 // `INDEX` lexes as Token::Index (reserved), not as
14809 // Token::Ident("index"). Both shapes count as a KEY/INDEX
14810 // start; the peek helper below handles either.
14811 let is_key_or_index_tok = |t: &Token| -> bool {
14812 matches!(t, Token::Index)
14813 || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
14814 };
14815 match cur {
14816 Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
14817 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14818 after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
14819 }
14820 Token::Ident(s)
14821 if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
14822 {
14823 let nxt = self.tokens.get(self.pos + 1);
14824 let after_after = if nxt.is_some_and(is_key_or_index_tok) {
14825 self.pos + 2
14826 } else {
14827 self.pos + 1
14828 };
14829 after_keyword_followed_by_paren_or_ident_paren(after_after)
14830 }
14831 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
14832 let nxt = self.tokens.get(self.pos + 1);
14833 if !nxt.is_some_and(is_key_or_index_tok) {
14834 return false;
14835 }
14836 after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
14837 }
14838 _ => false,
14839 }
14840 }
14841
14842 /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
14843 /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
14844 /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
14845 /// returns Some(TableConstraint::Index) so the engine builds
14846 /// a real BTree index on the leading column (mysqldump
14847 /// `KEY idx_posts_author (author_id)` shape).
14848 /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
14849 /// (the storage layer has no matching AM).
14850 fn parse_mysql_inline_key(
14851 &mut self,
14852 ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
14853 // Detect UNIQUE prefix.
14854 let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
14855 {
14856 self.advance();
14857 true
14858 } else {
14859 false
14860 };
14861 // Consume FULLTEXT / SPATIAL prefix and record which one
14862 // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
14863 // dedicated TableConstraint variant so the engine can
14864 // build a tsvector-GIN; SPATIAL still has no matching
14865 // AM, so it falls back to accept-as-no-op.
14866 let mut is_fulltext = false;
14867 let mut is_spatial = false;
14868 if let Token::Ident(s) = self.peek().clone() {
14869 if s.eq_ignore_ascii_case("fulltext") {
14870 self.advance();
14871 is_fulltext = true;
14872 } else if s.eq_ignore_ascii_case("spatial") {
14873 self.advance();
14874 is_spatial = true;
14875 }
14876 }
14877 // KEY / INDEX keyword. `INDEX` lexes as Token::Index
14878 // (reserved); accept either token shape.
14879 match self.peek() {
14880 Token::Index => {
14881 self.advance();
14882 }
14883 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14884 self.advance();
14885 }
14886 other => {
14887 return Err(self.err(alloc::format!(
14888 "expected KEY/INDEX in inline index declaration, got {other:?}"
14889 )));
14890 }
14891 }
14892 // Optional index name (an ident before the `(`).
14893 // v7.15.0 — capture the name when present so the engine
14894 // builds the secondary index under the user's chosen
14895 // name (matches mysqldump's `KEY idx_x (col)` shape).
14896 let mut idx_name: Option<String> = None;
14897 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
14898 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
14899 {
14900 if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
14901 idx_name = Some(s);
14902 }
14903 }
14904 // Optional `USING BTREE` / `USING HASH` (MySQL).
14905 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
14906 self.advance();
14907 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14908 self.advance();
14909 }
14910 }
14911 // Required column list `(col [, col]*)`.
14912 if !matches!(self.peek(), Token::LParen) {
14913 return Err(self.err(alloc::format!(
14914 "expected '(' in inline KEY/INDEX, got {:?}",
14915 self.peek()
14916 )));
14917 }
14918 self.advance();
14919 let mut cols: Vec<String> = Vec::new();
14920 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
14921 self.advance();
14922 cols.push(s);
14923 // Skip optional `(length)` per-column prefix.
14924 if matches!(self.peek(), Token::LParen) {
14925 let mut depth = 1usize;
14926 self.advance();
14927 while depth > 0 {
14928 match self.peek() {
14929 Token::LParen => depth += 1,
14930 Token::RParen => depth -= 1,
14931 Token::Eof => break,
14932 _ => {}
14933 }
14934 self.advance();
14935 }
14936 }
14937 // Skip optional ASC / DESC.
14938 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
14939 || matches!(self.peek(), Token::Asc | Token::Desc)
14940 {
14941 self.advance();
14942 }
14943 if matches!(self.peek(), Token::Comma) {
14944 self.advance();
14945 continue;
14946 }
14947 break;
14948 }
14949 if matches!(self.peek(), Token::RParen) {
14950 self.advance();
14951 }
14952 // Trailing options on the inline index — comment / etc.
14953 // Skip until comma or `)`.
14954 while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
14955 self.advance();
14956 }
14957 if cols.is_empty() {
14958 return Ok(None);
14959 }
14960 if is_unique {
14961 // Carry the captured idx_name on UNIQUE too so future
14962 // engine work can name the underlying BTree
14963 // accordingly; today the unique-constraint installer
14964 // synthesises the name itself, but Display round-trip
14965 // benefits from preserving it.
14966 Ok(Some(crate::ast::TableConstraint::Unique {
14967 name: idx_name,
14968 columns: cols,
14969 nulls_not_distinct: false,
14970 // MySQL inline UNIQUE KEY has no deferral vocabulary.
14971 deferrable: false,
14972 initially_deferred: false,
14973 }))
14974 } else if is_fulltext {
14975 // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
14976 // routes through `TableConstraint::FulltextIndex`;
14977 // the engine builds a tsvector-GIN over each named
14978 // column so MATCH AGAINST gets a real inverted
14979 // index instead of a silently-dropped declaration.
14980 Ok(Some(crate::ast::TableConstraint::FulltextIndex {
14981 name: idx_name,
14982 columns: cols,
14983 }))
14984 } else if is_spatial {
14985 // SPG has no native SPATIAL AM. Accept-as-no-op
14986 // (declaration is parsed, but no index is built).
14987 Ok(None)
14988 } else {
14989 // v7.15.0 — plain KEY / INDEX builds a real BTree
14990 // secondary index.
14991 Ok(Some(crate::ast::TableConstraint::Index {
14992 name: idx_name,
14993 columns: cols,
14994 }))
14995 }
14996 }
14997
14998 /// v7.14.0 — consume MySQL/MariaDB table-options tail after
14999 /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
15000 /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
15001 /// (in any order, separated by whitespace).
15002 /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
15003 /// storage-parameter clause on CREATE TABLE. SPG has no per-table
15004 /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
15005 /// bare ident here, and only the parenthesised form is reloptions (so this
15006 /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
15007 fn consume_with_reloptions(&mut self) {
15008 let is_with = matches!(
15009 self.peek(),
15010 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15011 );
15012 if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
15013 return;
15014 }
15015 self.advance(); // WITH
15016 self.advance(); // (
15017 let mut depth = 1u32;
15018 while depth > 0 && !matches!(self.peek(), Token::Eof) {
15019 match self.peek() {
15020 Token::LParen => depth += 1,
15021 Token::RParen => depth -= 1,
15022 _ => {}
15023 }
15024 self.advance();
15025 }
15026 }
15027
15028 fn consume_mysql_table_options(&mut self) {
15029 loop {
15030 // Heuristic: a table option is an ident (or `DEFAULT`
15031 // reserved keyword) followed by `=` and an
15032 // ident / string / integer.
15033 let name_lc = match self.peek().clone() {
15034 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15035 Token::Default => alloc::string::String::from("default"),
15036 _ => break,
15037 };
15038 let known = matches!(
15039 name_lc.as_str(),
15040 "engine"
15041 | "default"
15042 | "charset"
15043 | "collate"
15044 | "auto_increment"
15045 | "row_format"
15046 | "comment"
15047 | "pack_keys"
15048 | "stats_persistent"
15049 | "stats_auto_recalc"
15050 | "stats_sample_pages"
15051 | "key_block_size"
15052 | "tablespace"
15053 | "min_rows"
15054 | "max_rows"
15055 | "checksum"
15056 | "delay_key_write"
15057 | "insert_method"
15058 | "data"
15059 | "index"
15060 | "encryption"
15061 | "compression"
15062 );
15063 if !known {
15064 break;
15065 }
15066 self.advance(); // option name
15067 // `DEFAULT` optional prefix is followed by `CHARSET` /
15068 // `COLLATE`; consume the next ident too.
15069 if name_lc == "default" {
15070 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15071 self.advance();
15072 }
15073 }
15074 if matches!(self.peek(), Token::Eq) {
15075 self.advance();
15076 }
15077 match self.peek() {
15078 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_) | Token::Integer(_) => {
15079 self.advance();
15080 }
15081 _ => {}
15082 }
15083 }
15084 }
15085
15086 /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
15087 /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
15088 /// sure (otherwise a column literally named `primary` would
15089 /// be mistaken).
15090 fn peek_table_level_pk_start(&self) -> bool {
15091 let cur = self.peek();
15092 let nxt = self.tokens.get(self.pos + 1);
15093 let nxt2 = self.tokens.get(self.pos + 2);
15094 let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
15095 let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
15096 let is_lparen = matches!(nxt2, Some(Token::LParen));
15097 is_primary && is_key && is_lparen
15098 }
15099
15100 /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
15101 /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
15102 /// (mailrs round-5 G10).
15103 fn peek_table_level_unique_start(&self) -> bool {
15104 let cur = self.peek();
15105 let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
15106 if !is_unique {
15107 return false;
15108 }
15109 let n1 = self.tokens.get(self.pos + 1);
15110 // Plain `UNIQUE (…)`.
15111 if matches!(n1, Some(Token::LParen)) {
15112 return true;
15113 }
15114 // `UNIQUE NULLS [NOT] DISTINCT (…)`.
15115 let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
15116 if !is_nulls {
15117 return false;
15118 }
15119 let n2 = self.tokens.get(self.pos + 2);
15120 let n3 = self.tokens.get(self.pos + 3);
15121 let n4 = self.tokens.get(self.pos + 4);
15122 // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15123 if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15124 return true;
15125 }
15126 // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15127 if matches!(n2, Some(Token::Not))
15128 && matches!(n3, Some(Token::Distinct))
15129 && matches!(n4, Some(Token::LParen))
15130 {
15131 return true;
15132 }
15133 false
15134 }
15135
15136 fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15137 self.advance(); // PRIMARY
15138 self.advance(); // KEY
15139 let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15140 // v7.39 (round 711) — the trailer's values are CARRIED now; round
15141 // 621 consumed and dropped them (the storing half of F08).
15142 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15143 Ok(crate::ast::TableConstraint::PrimaryKey {
15144 name: None,
15145 columns,
15146 deferrable,
15147 initially_deferred,
15148 })
15149 }
15150
15151 fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15152 self.advance(); // UNIQUE
15153 // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15154 // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15155 // is `NULLS DISTINCT` per the SQL standard.
15156 let mut nulls_not_distinct = false;
15157 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15158 let n1 = self.tokens.get(self.pos + 1);
15159 let n2 = self.tokens.get(self.pos + 2);
15160 let is_not = matches!(n1, Some(Token::Not));
15161 let is_distinct = matches!(n2, Some(Token::Distinct));
15162 if is_not && is_distinct {
15163 self.advance(); // NULLS
15164 self.advance(); // NOT
15165 self.advance(); // DISTINCT
15166 nulls_not_distinct = true;
15167 } else if matches!(n1, Some(Token::Distinct)) {
15168 self.advance(); // NULLS
15169 self.advance(); // DISTINCT
15170 }
15171 }
15172 let columns = self.parse_paren_ident_list("UNIQUE")?;
15173 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15174 Ok(crate::ast::TableConstraint::Unique {
15175 name: None,
15176 columns,
15177 nulls_not_distinct,
15178 deferrable,
15179 initially_deferred,
15180 })
15181 }
15182
15183 /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15184 /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15185 /// expression.
15186 /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15187 /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15188 /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15189 /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15190 /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15191 /// commit: `NOT` starts no other suffix here, but reading both
15192 /// tokens before advancing keeps the caller's error message intact
15193 /// if someone writes `NOT NULL` by mistake.
15194 fn parse_not_valid_suffix(&mut self) -> bool {
15195 if !matches!(self.peek(), Token::Not) {
15196 return false;
15197 }
15198 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15199 {
15200 return false;
15201 }
15202 self.advance();
15203 self.advance();
15204 true
15205 }
15206
15207 fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15208 self.advance(); // EXCLUDE
15209 // Optional `USING <method>`.
15210 let mut method = None;
15211 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15212 self.advance();
15213 method = Some(match self.advance() {
15214 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15215 other => {
15216 return Err(self.err(alloc::format!(
15217 "expected index method after USING, got {other:?}"
15218 )));
15219 }
15220 });
15221 }
15222 if !matches!(self.peek(), Token::LParen) {
15223 return Err(self.err(alloc::format!(
15224 "expected '(' after EXCLUDE, got {:?}",
15225 self.peek()
15226 )));
15227 }
15228 self.advance();
15229 let mut elements: Vec<(String, String)> = Vec::new();
15230 loop {
15231 let col = match self.advance() {
15232 Token::Ident(s) | Token::QuotedIdent(s) => s,
15233 other => {
15234 return Err(self.err(alloc::format!(
15235 "expected column name in EXCLUDE, got {other:?}"
15236 )));
15237 }
15238 };
15239 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15240 return Err(self.err(alloc::format!(
15241 "expected WITH after EXCLUDE column, got {:?}",
15242 self.peek()
15243 )));
15244 }
15245 self.advance();
15246 let op = match self.advance() {
15247 Token::InetOverlap => String::from("&&"),
15248 Token::Intersects => String::from("?#"),
15249 Token::IsBelow => String::from("<^"),
15250 Token::IsAbove => String::from(">^"),
15251 Token::PatternLt => String::from("~<~"),
15252 Token::PatternLtEq => String::from("~<=~"),
15253 Token::PatternGt => String::from("~>~"),
15254 Token::PatternGtEq => String::from("~>=~"),
15255 Token::TsMatchOld => String::from("@@@"),
15256 Token::Eq => String::from("="),
15257 Token::JsonContains => String::from("@>"),
15258 Token::JsonContainedBy => String::from("<@"),
15259 Token::OverLeft => String::from("&<"),
15260 Token::OverRight => String::from("&>"),
15261 other => {
15262 return Err(self.err(alloc::format!(
15263 "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15264 )));
15265 }
15266 };
15267 elements.push((col, op));
15268 if matches!(self.peek(), Token::Comma) {
15269 self.advance();
15270 continue;
15271 }
15272 break;
15273 }
15274 if !matches!(self.peek(), Token::RParen) {
15275 return Err(self.err(alloc::format!(
15276 "expected ')' to close EXCLUDE, got {:?}",
15277 self.peek()
15278 )));
15279 }
15280 self.advance();
15281 Ok(crate::ast::TableConstraint::Exclude {
15282 name: None,
15283 method,
15284 elements,
15285 })
15286 }
15287
15288 fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15289 self.advance(); // CHECK
15290 if !matches!(self.peek(), Token::LParen) {
15291 return Err(self.err(alloc::format!(
15292 "expected '(' after CHECK, got {:?}",
15293 self.peek()
15294 )));
15295 }
15296 self.advance();
15297 let expr = self.parse_expr(0)?;
15298 if !matches!(self.peek(), Token::RParen) {
15299 return Err(self.err(alloc::format!(
15300 "expected ')' to close CHECK predicate, got {:?}",
15301 self.peek()
15302 )));
15303 }
15304 self.advance();
15305 // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15306 // are no existing rows for PG to skip, so it rejects the suffix.
15307 Ok(crate::ast::TableConstraint::Check {
15308 name: None,
15309 expr,
15310 not_valid: false,
15311 })
15312 }
15313
15314 /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15315 fn peek_table_level_check_start(&self) -> bool {
15316 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15317 }
15318
15319 /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15320 /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15321 /// on the dedicated FK path (`parse_table_level_fk` consumes its
15322 /// own CONSTRAINT prefix).
15323 fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15324 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15325 return None;
15326 }
15327 // tokens[pos+1] is the constraint name (any ident-like);
15328 // tokens[pos+2] is the kind keyword.
15329 match self.tokens.get(self.pos + 2) {
15330 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15331 Some(NamedTableConstraintKind::Check)
15332 }
15333 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15334 Some(NamedTableConstraintKind::Unique)
15335 }
15336 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15337 Some(NamedTableConstraintKind::PrimaryKey)
15338 }
15339 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15340 Some(NamedTableConstraintKind::Exclude)
15341 }
15342 _ => None,
15343 }
15344 }
15345
15346 fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15347 if !matches!(self.peek(), Token::LParen) {
15348 return Err(self.err(alloc::format!(
15349 "expected '(' after {ctx}, got {:?}",
15350 self.peek()
15351 )));
15352 }
15353 self.advance();
15354 let mut out = Vec::new();
15355 loop {
15356 out.push(self.expect_ident_like()?);
15357 match self.peek() {
15358 Token::Comma => {
15359 self.advance();
15360 }
15361 Token::RParen => {
15362 self.advance();
15363 break;
15364 }
15365 other => {
15366 return Err(self.err(alloc::format!(
15367 "expected ',' or ')' in {ctx} list, got {other:?}"
15368 )));
15369 }
15370 }
15371 }
15372 if out.is_empty() {
15373 return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15374 }
15375 Ok(out)
15376 }
15377
15378 /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15379 /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15380 /// table-level FK; a column def never starts with either keyword
15381 /// (column names are not in this reserved set).
15382 fn peek_constraint_or_fk_start(&self) -> bool {
15383 let is_constraint_kw = matches!(
15384 self.peek(),
15385 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15386 );
15387 let is_foreign_kw = matches!(
15388 self.peek(),
15389 Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15390 );
15391 is_constraint_kw || is_foreign_kw
15392 }
15393
15394 /// v7.6.0 — parse a table-level FK clause:
15395 /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15396 /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15397 fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15398 let mut name: Option<String> = None;
15399 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15400 self.advance();
15401 name = Some(self.expect_ident_like()?);
15402 }
15403 // `FOREIGN`
15404 match self.advance() {
15405 Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15406 other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15407 }
15408 // `KEY`
15409 match self.advance() {
15410 Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15411 other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15412 }
15413 // `(col, col, ...)`
15414 if !matches!(self.peek(), Token::LParen) {
15415 return Err(self.err(format!(
15416 "expected '(' after FOREIGN KEY, got {:?}",
15417 self.peek()
15418 )));
15419 }
15420 self.advance();
15421 let mut columns = Vec::new();
15422 loop {
15423 columns.push(self.expect_ident_like()?);
15424 match self.peek() {
15425 Token::Comma => {
15426 self.advance();
15427 }
15428 Token::RParen => {
15429 self.advance();
15430 break;
15431 }
15432 other => {
15433 return Err(self.err(format!(
15434 "expected ',' or ')' in FK column list, got {other:?}"
15435 )));
15436 }
15437 }
15438 }
15439 if columns.is_empty() {
15440 return Err(self.err("FOREIGN KEY requires at least one column".into()));
15441 }
15442 let (
15443 parent_table,
15444 parent_columns,
15445 on_delete,
15446 on_update,
15447 match_type,
15448 deferrable,
15449 initially_deferred,
15450 ) = self.parse_references_tail(columns.len())?;
15451 Ok(ForeignKeyConstraint {
15452 name,
15453 columns,
15454 parent_table,
15455 parent_columns,
15456 on_delete,
15457 on_update,
15458 match_type,
15459 deferrable,
15460 initially_deferred,
15461 })
15462 }
15463
15464 /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15465 /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15466 /// the local column count, used to default the parent column
15467 /// list when omitted (SQL spec: parent's PK is implied).
15468 fn parse_references_tail(
15469 &mut self,
15470 expected_arity: usize,
15471 ) -> Result<
15472 (
15473 String,
15474 Vec<String>,
15475 FkAction,
15476 FkAction,
15477 crate::ast::MatchType,
15478 // v7.39 (round 288) — deferrable, initially_deferred.
15479 bool,
15480 bool,
15481 ),
15482 ParseError,
15483 > {
15484 match self.advance() {
15485 Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15486 other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15487 }
15488 let parent_table = self.expect_ident_like()?;
15489 let mut parent_columns: Vec<String> = Vec::new();
15490 if matches!(self.peek(), Token::LParen) {
15491 self.advance();
15492 loop {
15493 parent_columns.push(self.expect_ident_like()?);
15494 match self.peek() {
15495 Token::Comma => {
15496 self.advance();
15497 }
15498 Token::RParen => {
15499 self.advance();
15500 break;
15501 }
15502 other => {
15503 return Err(self.err(format!(
15504 "expected ',' or ')' in REFERENCES column list, got {other:?}"
15505 )));
15506 }
15507 }
15508 }
15509 }
15510 if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15511 return Err(self.err(format!(
15512 "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15513 expected_arity,
15514 parent_columns.len()
15515 )));
15516 }
15517 // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15518 // it between the referenced column list and the ON / DEFERRABLE
15519 // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15520 // is skipped when any referencing column is NULL), so SIMPLE —
15521 // the default, and the only spelling pg_dump emits — is accepted
15522 // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15523 // mixed-NULL rule, which is not wired yet; reject them honestly
15524 // rather than silently applying SIMPLE (PG itself errors on
15525 // MATCH PARTIAL as "not yet implemented").
15526 let mut match_type = crate::ast::MatchType::Simple;
15527 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15528 self.advance();
15529 // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15530 // SIMPLE / PARTIAL arrive as bare identifiers.
15531 let kind = match self.advance() {
15532 Token::Full => "FULL".to_string(),
15533 Token::Ident(s) => s.to_uppercase(),
15534 other => {
15535 return Err(self.err(format!(
15536 "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15537 )));
15538 }
15539 };
15540 match kind.as_str() {
15541 "SIMPLE" => {} // Default — match_type stays Simple.
15542 // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15543 // when ALL referencing columns are NULL; a mixed-NULL key errors.
15544 "FULL" => match_type = crate::ast::MatchType::Full,
15545 "PARTIAL" => {
15546 return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15547 }
15548 _ => {
15549 return Err(self.err(format!(
15550 "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15551 )));
15552 }
15553 }
15554 }
15555 // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15556 // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15557 // <action>` / `ON UPDATE <action>` in either order. PG /
15558 // pg_dump emits the timing clause AFTER the ON clauses
15559 // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15560 // but the SQL spec allows either order. We loop over
15561 // every possible trailer and dispatch on the next token,
15562 // stopping when nothing matches. Phase 3.1 changes the
15563 // bare DEFERRABLE form from hard-error to accept-as-
15564 // immediate; SPG is single-writer with no deferred-
15565 // constraint window so the runtime semantics are always
15566 // immediate even when INITIALLY DEFERRED is requested.
15567 // PG's default referential action (no ON DELETE / ON UPDATE
15568 // clause) is NO ACTION, not RESTRICT — the two enforce
15569 // identically in SPG (single-writer, no deferred window; see the
15570 // shared match arm in constraints.rs) but information_schema.
15571 // referential_constraints must report NO ACTION to match PG.
15572 let mut on_delete = FkAction::NoAction;
15573 let mut on_update = FkAction::NoAction;
15574 let mut seen_on_delete = false;
15575 let mut seen_on_update = false;
15576 let mut deferrable = false;
15577 let mut initially_deferred = false;
15578 loop {
15579 // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15580 let before = self.pos;
15581 let (d, idef) = self.consume_deferrable_clauses_timed()?;
15582 if self.pos != before {
15583 deferrable = d;
15584 initially_deferred = idef;
15585 continue;
15586 }
15587 // ON DELETE / ON UPDATE.
15588 if !matches!(self.peek(), Token::On) {
15589 break;
15590 }
15591 self.advance();
15592 let which = self.advance();
15593 let action = self.parse_fk_action()?;
15594 match which {
15595 Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15596 if seen_on_delete {
15597 return Err(self.err("ON DELETE specified twice".into()));
15598 }
15599 seen_on_delete = true;
15600 on_delete = action;
15601 }
15602 Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15603 if seen_on_update {
15604 return Err(self.err("ON UPDATE specified twice".into()));
15605 }
15606 seen_on_update = true;
15607 on_update = action;
15608 }
15609 other => {
15610 return Err(
15611 self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15612 );
15613 }
15614 }
15615 }
15616 Ok((
15617 parent_table,
15618 parent_columns,
15619 on_delete,
15620 on_update,
15621 match_type,
15622 deferrable,
15623 initially_deferred,
15624 ))
15625 }
15626
15627 /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15628 /// NO ACTION`.
15629 fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15630 match self.advance() {
15631 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15632 Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15633 Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15634 Token::Null => Ok(FkAction::SetNull),
15635 Token::Default => Ok(FkAction::SetDefault),
15636 other => Err(self.err(format!(
15637 "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15638 ))),
15639 },
15640 Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15641 Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15642 other => Err(self.err(format!(
15643 "expected ACTION after NO in FK action, got {other:?}"
15644 ))),
15645 },
15646 other => Err(self.err(format!(
15647 "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15648 ))),
15649 }
15650 }
15651
15652 /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15653 /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15654 fn consume_if_not_exists(&mut self) -> bool {
15655 // `IF` arrives as a bare Ident (we don't reserve it because it
15656 // also appears mid-expression in PG, though we don't support
15657 // those forms yet).
15658 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15659 if !looks_like_if {
15660 return false;
15661 }
15662 // Peek one ahead before committing: only consume IF when it's
15663 // actually `IF NOT EXISTS`.
15664 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
15665 return false;
15666 }
15667 if !matches!(
15668 self.tokens.get(self.pos + 2),
15669 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15670 ) {
15671 return false;
15672 }
15673 self.advance(); // IF
15674 self.advance(); // NOT
15675 self.advance(); // EXISTS
15676 true
15677 }
15678
15679 /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
15680 /// Consumes IF EXISTS as a pair; returns false otherwise
15681 /// without consuming any tokens.
15682 /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
15683 /// ENABLE/DISABLE/FORCE/NO FORCE.
15684 fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
15685 for kw in ["row", "level", "security"] {
15686 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
15687 {
15688 return Err(self.err(alloc::format!(
15689 "expected {} in ROW LEVEL SECURITY, got {:?}",
15690 kw.to_ascii_uppercase(),
15691 self.peek()
15692 )));
15693 }
15694 self.advance();
15695 }
15696 Ok(())
15697 }
15698
15699 fn consume_if_exists(&mut self) -> bool {
15700 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15701 if !looks_like_if {
15702 return false;
15703 }
15704 if !matches!(
15705 self.tokens.get(self.pos + 1),
15706 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15707 ) {
15708 return false;
15709 }
15710 self.advance(); // IF
15711 self.advance(); // EXISTS
15712 true
15713 }
15714
15715 /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
15716 /// qualifiers after an index column ref. ASC / DESC are
15717 /// reserved tokens; NULLS / FIRST / LAST are bare idents.
15718 /// We accept and discard them since single-column BTree
15719 /// stores rows in natural key order today.
15720 /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
15721 /// ORDER BY key. Returns None when absent.
15722 fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
15723 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15724 return Ok(None);
15725 }
15726 self.advance();
15727 match self.advance() {
15728 Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
15729 Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
15730 other => Err(self.err(alloc::format!(
15731 "expected FIRST or LAST after NULLS, got {other:?}"
15732 ))),
15733 }
15734 }
15735
15736 /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
15737 /// rather than discarded.
15738 ///
15739 /// SPG's index does not scan in a direction — column ordering is
15740 /// intrinsic to the storage — but `pg_indexes.indexdef` is a
15741 /// reproduction of the DDL, and dropping the clause meant
15742 /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
15743 /// dump lost it, and a schema diff saw drift on every run.
15744 fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
15745 let mut order = crate::ast::IndexColumnOrder::default();
15746 loop {
15747 match self.peek() {
15748 Token::Asc => {
15749 self.advance();
15750 }
15751 Token::Desc => {
15752 order.descending = true;
15753 self.advance();
15754 }
15755 Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
15756 let look = self.tokens.get(self.pos + 1);
15757 if matches!(
15758 look,
15759 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
15760 || k.eq_ignore_ascii_case("last")
15761 ) {
15762 self.advance();
15763 order.nulls_first = Some(matches!(
15764 self.advance(),
15765 Token::Ident(k) if k.eq_ignore_ascii_case("first")
15766 ));
15767 } else {
15768 break;
15769 }
15770 }
15771 _ => break,
15772 }
15773 }
15774 order
15775 }
15776
15777 fn parse_create_index_stmt_after_create(
15778 &mut self,
15779 is_unique: bool,
15780 ) -> Result<Statement, ParseError> {
15781 // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
15782 debug_assert!(matches!(self.peek(), Token::Index));
15783 self.advance();
15784 // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
15785 // SPG's CREATE INDEX is synchronous end-to-end today (real
15786 // CONCURRENTLY variant with restartable scans queues with
15787 // v7.39 indexes epic), so the modifier has no runtime effect
15788 // — same accept-and-no-op shape as v7.37.16.5 DETACH
15789 // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
15790 // VIEW CONCURRENTLY.
15791 let mut concurrently = false;
15792 if matches!(
15793 self.peek(),
15794 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
15795 ) {
15796 self.advance();
15797 concurrently = true;
15798 }
15799 let if_not_exists = self.consume_if_not_exists();
15800 // v7.39 (read01 round 93) — the index name is optional (PG since
15801 // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
15802 // When the token after `[IF NOT EXISTS]` is already `ON`, no name
15803 // was given; leave it empty and the engine derives a PG-style
15804 // `<table>_<cols>_idx` name at CREATE time (with collision counter).
15805 let name = if matches!(self.peek(), Token::On) {
15806 String::new()
15807 } else {
15808 self.expect_ident_like()?
15809 };
15810 if !matches!(self.peek(), Token::On) {
15811 return Err(self.err(format!(
15812 "expected ON after CREATE INDEX <name>, got {:?}",
15813 self.peek()
15814 )));
15815 }
15816 self.advance();
15817 let table = self.expect_ident_like()?;
15818 // Optional `USING <method>` — only recognised method in v2.0 is
15819 // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
15820 // ident `using` (we don't promote it to a reserved keyword
15821 // because it isn't reserved anywhere else in our SQL surface).
15822 let mut method_name: Option<String> = None;
15823 let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15824 self.advance();
15825 let m = self.expect_ident_like()?;
15826 method_name = Some(m.to_ascii_lowercase());
15827 match m.to_ascii_lowercase().as_str() {
15828 "hnsw" => IndexMethod::Hnsw,
15829 "btree" => IndexMethod::BTree,
15830 "brin" => IndexMethod::Brin,
15831 // v7.12.3 — real GIN inverted index over `tsvector`.
15832 // v7.9.26b's `USING gin` → BTree silent fallback is
15833 // gone; the engine validates that the indexed column
15834 // is `tsvector` at CREATE INDEX time.
15835 "gin" => IndexMethod::Gin,
15836 // v7.9.26b — PG `pg_dump` emits `USING gist` /
15837 // `USING spgist` / `USING hash` for their built-in
15838 // AMs that SPG doesn't have a matching
15839 // implementation for; degrade to BTree on the
15840 // leading column so the schema loads + the index
15841 // catalogue stays consistent. Operator pays the
15842 // planner cost only for the queries that would have
15843 // used the specialised AM.
15844 "gist" | "spgist" | "hash" => IndexMethod::BTree,
15845 // v7.11.3 — pgvector ships both `ivfflat` and
15846 // `hnsw`. Customers shouldn't have to choose
15847 // their on-disk index method based on what SPG
15848 // implements; accept `ivfflat` as a synonym for
15849 // `hnsw` so PG schemas using either method drop
15850 // in. The vector distance op (`<->` / `<#>` /
15851 // `<=>`) at query time still picks the metric.
15852 "ivfflat" => IndexMethod::Hnsw,
15853 other => {
15854 return Err(self.err(alloc::format!(
15855 "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
15856 )));
15857 }
15858 }
15859 } else {
15860 IndexMethod::BTree
15861 };
15862 if !matches!(self.peek(), Token::LParen) {
15863 return Err(self.err(format!(
15864 "expected '(' before indexed column, got {:?}",
15865 self.peek()
15866 )));
15867 }
15868 self.advance();
15869 // v6.8.2 — accept either a bare column ident (legacy) or
15870 // an expression `fn(col, …)` for expression indexes.
15871 // Distinguish by peeking the token *after* the current
15872 // ident: `ident )` is the legacy column-only path;
15873 // anything else triggers the Pratt expression parser.
15874 // (`advance()` uses `mem::replace` to nil out the current
15875 // slot, so we can't save+rewind cleanly — peek-ahead via
15876 // direct index avoids the mutation.)
15877 let mut opclass: Option<String> = None;
15878 let mut key_collation: Option<String> = None;
15879 let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
15880 // Single column with `)` immediately after — fast path.
15881 // v7.9.29 — also: bare column followed by `,` (the
15882 // multi-column form `(a, b, c)`). Without this branch
15883 // the leading ident gets pulled into `parse_expr`
15884 // which then sets `expression = Some(Column(a))` and
15885 // breaks Display round-trip on the multi-column shape.
15886 Token::Ident(s) | Token::QuotedIdent(s)
15887 if matches!(
15888 self.tokens.get(self.pos + 1),
15889 Some(Token::RParen | Token::Comma)
15890 ) =>
15891 {
15892 self.advance();
15893 (s, None)
15894 }
15895 // v7.9.22 — single column followed by a pgvector
15896 // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
15897 // v7.15.0 — capture the opclass instead of discarding
15898 // it so the engine can dispatch (e.g. `gin_trgm_ops`
15899 // → real trigram-shingle GIN over a TEXT column).
15900 // Vector/HNSW opclasses still take their distance
15901 // metric from the query operator (`<->` / `<#>` /
15902 // `<=>`), so for those callers the opclass stays
15903 // informational.
15904 // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
15905 // opclass: `(embedding public.vector_cosine_ops)`. Strip
15906 // the schema and dispatch on the bare opclass, the same
15907 // treatment table/type names get.
15908 Token::Ident(s) | Token::QuotedIdent(s)
15909 if matches!(
15910 self.tokens.get(self.pos + 1),
15911 Some(Token::Ident(_) | Token::QuotedIdent(_))
15912 ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
15913 && matches!(
15914 self.tokens.get(self.pos + 3),
15915 Some(Token::Ident(op) | Token::QuotedIdent(op))
15916 if is_vector_opclass_name(op)
15917 ) =>
15918 {
15919 self.advance(); // column name
15920 self.advance(); // schema qualifier
15921 self.advance(); // dot
15922 let op_tok = self.advance();
15923 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15924 opclass = Some(op.to_ascii_lowercase());
15925 }
15926 (s, None)
15927 }
15928 // r1038 — an operator class is recognised by its POSITION, not
15929 // by a list of names. It used to be `is_vector_opclass_name`,
15930 // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
15931 // sentori's migration wrote — was a syntax error while
15932 // `USING gin (doc)` parsed. Anything sitting between a column
15933 // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
15934 // two bare identifiers in a row are not valid there otherwise.
15935 Token::Ident(s) | Token::QuotedIdent(s)
15936 if matches!(
15937 self.tokens.get(self.pos + 1),
15938 Some(Token::Ident(op) | Token::QuotedIdent(op))
15939 if is_vector_opclass_name(op) || Self::opclass_position_follows(
15940 self.tokens.get(self.pos + 2)
15941 )
15942 ) =>
15943 {
15944 self.advance(); // column name
15945 // Capture the opclass token, lower-cased for
15946 // case-insensitive engine dispatch.
15947 let op_tok = self.advance();
15948 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15949 opclass = Some(op.to_ascii_lowercase());
15950 }
15951 (s, None)
15952 }
15953 Token::Ident(_) | Token::QuotedIdent(_) => {
15954 // v7.39 (round 538) — an explicit COLLATE on the key,
15955 // read by LOOKAHEAD because `parse_expr` absorbs the
15956 // clause as a no-op (SPG orders text by bytes, which is
15957 // the C collation, so it changes nothing to honour). PG
15958 // still PRINTS it: an explicitly written `"C"` and the
15959 // collation a column inherits are different collation
15960 // OBJECTS even where they sort identically, which is why
15961 // `(a COLLATE "C")` shows on a C-collation database too.
15962 if matches!(
15963 self.tokens.get(self.pos + 1),
15964 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
15965 ) {
15966 key_collation = match self.tokens.get(self.pos + 2) {
15967 Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
15968 Some(n.clone())
15969 }
15970 _ => None,
15971 };
15972 }
15973 let key_expr = self.parse_expr(0)?;
15974 let primary = extract_first_column(&key_expr).ok_or_else(|| {
15975 self.err("expression index key must reference at least one column".into())
15976 })?;
15977 (primary, Some(key_expr))
15978 }
15979 // v7.37.43-T4 — parenthesised expression index key
15980 // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
15981 // PG's CREATE INDEX requires the expression to be in
15982 // its own parens to disambiguate function calls from
15983 // column lists, so this `LParen` is the inner open-paren
15984 // of an expression key. parse_expr handles the recursive
15985 // descent and consumes the matching `RParen`.
15986 Token::LParen => {
15987 let key_expr = self.parse_expr(0)?;
15988 let primary = extract_first_column(&key_expr).ok_or_else(|| {
15989 self.err("expression index key must reference at least one column".into())
15990 })?;
15991 (primary, Some(key_expr))
15992 }
15993 other => {
15994 return Err(self.err(format!(
15995 "expected column ident or expression, got {other:?}"
15996 )));
15997 }
15998 };
15999 // v7.9.14 — accept extra comma-separated columns inside
16000 // the index key parens (`CREATE INDEX … (a, b, c)`).
16001 // mailrs F2. Each extra column may carry an optional
16002 // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
16003 // — parsed and discarded; SPG doesn't honour direction
16004 // on a BTree index today (column ordering is intrinsic
16005 // to the storage). v7.10 will widen to genuine composite
16006 // index keys.
16007 let mut extra_columns: Vec<String> = Vec::new();
16008 // The leading column may also have ASC/DESC after it — and that
16009 // one is the column SPG indexes, so its clause is kept.
16010 let key_order = self.consume_optional_index_column_qualifiers();
16011 while matches!(self.peek(), Token::Comma) {
16012 self.advance();
16013 let extra = self.expect_ident_like()?;
16014 let _ = self.consume_optional_index_column_qualifiers();
16015 extra_columns.push(extra);
16016 }
16017 if !matches!(self.peek(), Token::RParen) {
16018 return Err(self.err(format!(
16019 "expected ')' after indexed column / expression, got {:?}",
16020 self.peek()
16021 )));
16022 }
16023 self.advance();
16024 // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
16025 // index-only-scan annotation. Bare ident (not a reserved
16026 // keyword) so we test by case-insensitive string match.
16027 let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
16028 {
16029 self.advance();
16030 if !matches!(self.peek(), Token::LParen) {
16031 return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
16032 }
16033 self.advance();
16034 let mut cols = Vec::new();
16035 loop {
16036 cols.push(self.expect_ident_like()?);
16037 match self.peek() {
16038 Token::Comma => {
16039 self.advance();
16040 }
16041 Token::RParen => {
16042 self.advance();
16043 break;
16044 }
16045 other => {
16046 return Err(self.err(format!(
16047 "expected ',' or ')' in INCLUDE list, got {other:?}"
16048 )));
16049 }
16050 }
16051 }
16052 cols
16053 } else {
16054 Vec::new()
16055 };
16056 // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
16057 // storage parameters. pgvector emits `WITH (lists = N)` for
16058 // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
16059 // SPG's HNSW picks its own parameters today (tunable via
16060 // env vars), so the WITH clause is informational and dropped.
16061 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
16062 self.advance();
16063 if !matches!(self.peek(), Token::LParen) {
16064 return Err(self.err(format!(
16065 "expected '(' after WITH in CREATE INDEX, got {:?}",
16066 self.peek()
16067 )));
16068 }
16069 self.advance();
16070 loop {
16071 if matches!(self.peek(), Token::RParen) {
16072 self.advance();
16073 break;
16074 }
16075 // Drain `key = value` or bare `key` tokens.
16076 let _ = self.advance(); // key
16077 if matches!(self.peek(), Token::Eq) {
16078 self.advance();
16079 let _ = self.advance(); // value (int / string / ident)
16080 }
16081 match self.peek() {
16082 Token::Comma => {
16083 self.advance();
16084 }
16085 Token::RParen => {
16086 self.advance();
16087 break;
16088 }
16089 other => {
16090 return Err(self.err(format!(
16091 "expected ',' or ')' in WITH (…) clause, got {other:?}"
16092 )));
16093 }
16094 }
16095 }
16096 }
16097 // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
16098 // which sits between the key list and the WHERE clause.
16099 let mut nulls_not_distinct = false;
16100 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16101 let n1 = self.tokens.get(self.pos + 1);
16102 let n2 = self.tokens.get(self.pos + 2);
16103 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
16104 self.advance(); // NULLS
16105 self.advance(); // NOT
16106 self.advance(); // DISTINCT
16107 nulls_not_distinct = true;
16108 } else if matches!(n1, Some(Token::Distinct)) {
16109 self.advance(); // NULLS
16110 self.advance(); // DISTINCT
16111 }
16112 }
16113 // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
16114 let partial_predicate = if matches!(self.peek(), Token::Where) {
16115 self.advance();
16116 Some(self.parse_expr(0)?)
16117 } else {
16118 None
16119 };
16120 // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16121 // sense: uniqueness over an ANN structure has no clean
16122 // semantics. Reject early. (BRIN UNIQUE is similarly
16123 // meaningless — block both.)
16124 if is_unique && !matches!(method, IndexMethod::BTree) {
16125 return Err(self.err(alloc::format!(
16126 "UNIQUE is only supported on BTree indexes, got USING {:?}",
16127 method
16128 )));
16129 }
16130 Ok(Statement::CreateIndex(CreateIndexStatement {
16131 concurrently,
16132 name,
16133 key_order,
16134 key_collation,
16135 table,
16136 column,
16137 nulls_not_distinct,
16138 method,
16139 if_not_exists,
16140 included_columns,
16141 partial_predicate,
16142 extra_columns: extra_columns.clone(),
16143 expression,
16144 is_unique,
16145 opclass,
16146 method_name,
16147 }))
16148 }
16149
16150 /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16151 /// column-level `REFERENCES ...` clause. The trailing FK is
16152 /// normalised into table-level shape (single-element columns +
16153 /// parent_columns) so the engine sees one uniform constraint list.
16154 fn parse_column_def_with_fk(
16155 &mut self,
16156 ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16157 let col = self.parse_column_def()?;
16158 // v7.39 (round 308, V29) — an explicitly named inline FK:
16159 // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16160 // loop leaves this spelling intact precisely so the name can be
16161 // kept here; PG reports it in violation messages and matches it
16162 // in `SET CONSTRAINTS`.
16163 let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16164 {
16165 self.advance();
16166 Some(self.expect_ident_like()?)
16167 } else {
16168 None
16169 };
16170 // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16171 let inline_references = matches!(
16172 self.peek(),
16173 Token::Ident(s) if s.eq_ignore_ascii_case("references")
16174 );
16175 if !inline_references {
16176 return Ok((col, None));
16177 }
16178 let (
16179 parent_table,
16180 parent_columns,
16181 on_delete,
16182 on_update,
16183 match_type,
16184 deferrable,
16185 initially_deferred,
16186 ) = self.parse_references_tail(1)?;
16187 let fk = ForeignKeyConstraint {
16188 name: declared_name,
16189 columns: vec![col.name.clone()],
16190 parent_table,
16191 parent_columns,
16192 on_delete,
16193 on_update,
16194 match_type,
16195 deferrable,
16196 initially_deferred,
16197 };
16198 Ok((col, Some(fk)))
16199 }
16200
16201 /// v7.13.0 — parse a column type (consuming the type ident and
16202 /// any trailing parameters / `[]`), without surrounding column
16203 /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16204 /// Returns the resolved `ColumnTypeName` plus implied
16205 /// `(auto_increment, not_null)` flags from PG SERIAL family
16206 /// shorthands — callers that don't expect those (ALTER COLUMN
16207 /// TYPE) can discard them.
16208 fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16209 let (ty, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16210 Ok(ty)
16211 }
16212
16213 #[allow(clippy::type_complexity)]
16214 fn parse_type_with_implied_flags(
16215 &mut self,
16216 ) -> Result<
16217 (
16218 ColumnTypeName,
16219 bool,
16220 bool,
16221 Option<String>,
16222 Collation,
16223 // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16224 bool,
16225 // v7.39 (round 676) — the collation NAME as written, which the
16226 // `Collation` enum above cannot carry.
16227 Option<String>,
16228 bool,
16229 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16230 // list captured at type-parse time. None for all
16231 // non-ENUM types.
16232 Option<Vec<String>>,
16233 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16234 // list. Distinct from ENUM (subset semantics).
16235 Option<Vec<String>>,
16236 // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16237 // width, lost when the type collapses to SmallInt / Int.
16238 Option<MysqlIntWidth>,
16239 // v7.39 (round 424) — declared fractional-seconds precision of a
16240 // MySQL temporal column (bare spelling = 0). None under PG.
16241 Option<u8>,
16242 ),
16243 ParseError,
16244 > {
16245 let mut ty_ident = match self.advance() {
16246 Token::Ident(s) => s,
16247 // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16248 // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16249 // '<span>'` literal grammar. As a column type it lands
16250 // here directly; downstream resolution still uses the
16251 // canonical lowercase string.
16252 Token::Interval => "interval".to_string(),
16253 other => {
16254 return Err(ParseError {
16255 message: format!("expected column type, got {other:?}"),
16256 token_pos: self.consumed_pos(),
16257 });
16258 }
16259 };
16260 // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16261 // pg_dump qualifies extension types (`public.vector(1024)`).
16262 // SPG is single-namespace; drop the schema and resolve the
16263 // bare type — same treatment table names already get.
16264 while matches!(self.peek(), Token::Dot) {
16265 self.advance();
16266 ty_ident = self.expect_ident_like()?;
16267 }
16268 let mut implied_auto_increment = false;
16269 let mut implied_not_null = false;
16270 let mut user_type_ref: Option<String> = None;
16271 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16272 // value list, captured here and bubbled up through the
16273 // ColumnDef so the engine can attach it to the column
16274 // schema (and validate INSERT cells against it).
16275 let mut inline_enum_variants: Option<Vec<String>> = None;
16276 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16277 let mut inline_set_variants: Option<Vec<String>> = None;
16278 // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16279 // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16280 // collapses to SmallInt / Int. Only under the MySQL dialect.
16281 let mut mysql_int_width: Option<MysqlIntWidth> = None;
16282 // v7.39 (round 424) — the declared fractional-seconds precision of a
16283 // MySQL temporal column. Set by the temporal arms below; stays None
16284 // for PG (whose temporal columns keep full microseconds).
16285 let mut mysql_fsp: Option<u8> = None;
16286 let mut ty = match ty_ident.as_str() {
16287 // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16288 "smallserial" | "serial2" => {
16289 implied_auto_increment = true;
16290 implied_not_null = true;
16291 ColumnTypeName::SmallInt
16292 }
16293 "serial" | "serial4" => {
16294 implied_auto_increment = true;
16295 implied_not_null = true;
16296 ColumnTypeName::Int
16297 }
16298 "bigserial" | "serial8" => {
16299 implied_auto_increment = true;
16300 implied_not_null = true;
16301 ColumnTypeName::BigInt
16302 }
16303 // MySQL flavours we accept by aliasing to the closest SPG
16304 // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16305 // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16306 // 24-bit) → INT. UNSIGNED modifiers are consumed below
16307 // without semantic effect.
16308 // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16309 // PG's internal type names; pg_dump and hand-written PG schemas
16310 // use them interchangeably with smallint / int / bigint (the cast
16311 // path already accepted them, only the column grammar didn't).
16312 "smallint" | "int2" => {
16313 // v7.14.0 — MySQL display-width on integers
16314 // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16315 // parenthesised number is purely cosmetic — it
16316 // doesn't change storage. Accept + discard.
16317 self.consume_optional_paren_size();
16318 ColumnTypeName::SmallInt
16319 }
16320 // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16321 // canonical encoding for BOOLEAN. Every MySQL driver
16322 // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16323 // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16324 // 4.3 SPG classified TINYINT(1) as SmallInt, which
16325 // gave the customer i16-shaped values where the app
16326 // expected bool — a Tier-A silent type drift on
16327 // mysqldump restores. Now: `TINYINT(1)` → Bool;
16328 // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16329 // stay SmallInt (the legacy width-agnostic path).
16330 "tinyint" => {
16331 let width = self.peek_optional_paren_size_value();
16332 self.consume_optional_paren_size();
16333 if width == Some(1) {
16334 ColumnTypeName::Bool
16335 } else {
16336 // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16337 // lost width so the write path can enforce -128..127.
16338 if self.mysql_dialect {
16339 mysql_int_width = Some(MysqlIntWidth::Tiny);
16340 }
16341 ColumnTypeName::SmallInt
16342 }
16343 }
16344 "mediumint" => {
16345 self.consume_optional_paren_size();
16346 // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16347 if self.mysql_dialect {
16348 mysql_int_width = Some(MysqlIntWidth::Medium);
16349 }
16350 ColumnTypeName::Int
16351 }
16352 "int" | "integer" | "int4" => {
16353 self.consume_optional_paren_size();
16354 ColumnTypeName::Int
16355 }
16356 "bigint" | "int8" => {
16357 self.consume_optional_paren_size();
16358 ColumnTypeName::BigInt
16359 }
16360 // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16361 // (mailrs round-5 G6). Consume the optional `PRECISION`
16362 // tail when the type keyword was `double` / `DOUBLE`.
16363 //
16364 // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16365 // FLOAT". `FLOAT(p)` picks the width the way PG does:
16366 // p in 1..=24 is real, 25..=53 is double precision, and
16367 // anything else is an error.
16368 "float" | "double" | "real" => {
16369 if ty_ident.eq_ignore_ascii_case("double")
16370 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16371 {
16372 self.advance();
16373 }
16374 if ty_ident.eq_ignore_ascii_case("real") {
16375 // v7.39 (round 274) — the two dialects genuinely
16376 // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16377 // synonym for DOUBLE (8-byte). Round 269 made REAL
16378 // 32-bit globally and thereby narrowed the stored
16379 // precision of every MySQL REAL column.
16380 if self.mysql_dialect {
16381 ColumnTypeName::Float
16382 } else {
16383 ColumnTypeName::Real
16384 }
16385 } else if ty_ident.eq_ignore_ascii_case("float")
16386 && self.mysql_dialect
16387 && matches!(self.peek(), Token::LParen)
16388 && self.peek_paren_has_comma()
16389 {
16390 // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16391 // display form (`FLOAT(10,2)`), which PG has no
16392 // equivalent of. It was `syntax error at or near ","`,
16393 // so the whole CREATE failed. The digits are a display
16394 // hint only; SPG stores the full double.
16395 self.consume_optional_paren_size();
16396 ColumnTypeName::Float
16397 } else if ty_ident.eq_ignore_ascii_case("float")
16398 && matches!(self.peek(), Token::LParen)
16399 {
16400 // PG words the two bounds differently, and
16401 // parse_paren_size already rejects a zero.
16402 let p = self.parse_paren_size("FLOAT")?;
16403 if p > 53 {
16404 return Err(self.err(String::from(
16405 "precision for type float must be less than 54 bits",
16406 )));
16407 }
16408 if p <= 24 {
16409 ColumnTypeName::Real
16410 } else {
16411 ColumnTypeName::Float
16412 }
16413 } else {
16414 ColumnTypeName::Float
16415 }
16416 }
16417 // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16418 "float4" => ColumnTypeName::Real,
16419 "float8" => ColumnTypeName::Float,
16420 "text" => ColumnTypeName::Text,
16421 // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16422 // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16423 // real MySQL schema and NONE of them existed: the CREATE
16424 // failed outright with `type "blob" does not exist`, so the
16425 // table was never made. The sizes differ only in MySQL's
16426 // maximum length, which SPG does not cap, so they collapse
16427 // onto TEXT and BYTEA the way the unsized spellings do.
16428 "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16429 "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16430 // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16431 // enforce, consumed so the declaration parses.
16432 "varbinary" | "binary" => {
16433 self.consume_optional_paren_size();
16434 ColumnTypeName::Bytes
16435 }
16436 "name" => ColumnTypeName::Name,
16437 "xid" => ColumnTypeName::Xid,
16438 "oid" => ColumnTypeName::Oid,
16439 "xid8" => ColumnTypeName::Xid8,
16440 "bool" | "boolean" => ColumnTypeName::Bool,
16441 // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16442 // an unbounded `character varying`, which the arm below has always
16443 // read as text. Only the short spelling demanded a length, so
16444 // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16445 // there is — failed on `VARCHAR type requires (N)` while the long
16446 // spelling of the same thing was accepted. The same asymmetry
16447 // round 613 closed on the CAST side, here on the DDL side.
16448 "varchar" => {
16449 if matches!(self.peek(), Token::LParen) {
16450 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16451 } else {
16452 ColumnTypeName::Text
16453 }
16454 }
16455 // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16456 // `character` below (SQL standard).
16457 "char" => {
16458 if matches!(self.peek(), Token::LParen) {
16459 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16460 } else {
16461 ColumnTypeName::Char(1)
16462 }
16463 }
16464 // pg_dump's canonical spellings: `character varying(n)` = varchar,
16465 // `character(n)` = char, bare `character` = char(1). Unbounded
16466 // `character varying` maps to text.
16467 "character" => {
16468 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16469 self.advance();
16470 if matches!(self.peek(), Token::LParen) {
16471 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16472 } else {
16473 ColumnTypeName::Text
16474 }
16475 } else if matches!(self.peek(), Token::LParen) {
16476 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16477 } else {
16478 ColumnTypeName::Char(1)
16479 }
16480 }
16481 "vector" => {
16482 let dim = self.parse_paren_size("VECTOR")?;
16483 let encoding = self.parse_optional_vector_encoding()?;
16484 ColumnTypeName::Vector { dim, encoding }
16485 }
16486 // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16487 // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16488 // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16489 // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16490 // DECIMAL(10,2))` — how nearly every money column is written,
16491 // in either dialect — was a syntax error and the table was
16492 // never created. `FIXED` is MySQL's alias alone, so it is
16493 // taken only in that dialect.
16494 "numeric" | "decimal" | "dec" => {
16495 let (precision, scale) = self.parse_optional_numeric_params()?;
16496 ColumnTypeName::Numeric(precision, scale)
16497 }
16498 "fixed" if self.mysql_dialect => {
16499 let (precision, scale) = self.parse_optional_numeric_params()?;
16500 ColumnTypeName::Numeric(precision, scale)
16501 }
16502 "date" => ColumnTypeName::Date,
16503 // MySQL's `DATETIME` is the same domain as standard
16504 // `TIMESTAMP` — accept both spellings.
16505 "timestamp" | "datetime" => {
16506 // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16507 // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16508 // TIME ZONE` clause, so consume it first.
16509 // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16510 // (it truncates on write and pads on render), so capture it;
16511 // a bare spelling means precision 0 there. PG stores µs always
16512 // and keeps `None`.
16513 let n = self.take_optional_paren_size();
16514 if self.mysql_dialect {
16515 mysql_fsp = Some(n.unwrap_or(0).min(6));
16516 }
16517 // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16518 // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16519 // the full form. SPG canonicalises:
16520 // - WITH TIME ZONE → Timestamptz
16521 // - WITHOUT TIME ZONE → Timestamp
16522 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16523 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16524 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16525 {
16526 self.advance(); // WITH
16527 self.advance(); // TIME
16528 self.advance(); // ZONE
16529 ColumnTypeName::Timestamptz
16530 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16531 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16532 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16533 {
16534 self.advance(); // WITHOUT
16535 self.advance(); // TIME
16536 self.advance(); // ZONE
16537 ColumnTypeName::Timestamp
16538 } else {
16539 // A second `(precision)` cannot legally follow, but the
16540 // old grammar tolerated it; keep that tolerance.
16541 self.consume_optional_paren_size();
16542 ColumnTypeName::Timestamp
16543 }
16544 }
16545 // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16546 // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16547 // only PG-wire OID differs.
16548 "timestamptz" => {
16549 self.consume_optional_paren_size();
16550 ColumnTypeName::Timestamptz
16551 }
16552 // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16553 // validation. We accept the JSONB spelling too because
16554 // most PG clients default to it; SPG doesn't distinguish
16555 // the two (no path-operator perf advantage to model).
16556 "json" => ColumnTypeName::Json,
16557 "jsonb" => ColumnTypeName::Jsonb,
16558 // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16559 // surface here. Same storage shape; mapping happens at
16560 // the engine side via the ColumnTypeName → DataType
16561 // resolver. Literal forms are handled at coerce_value
16562 // time so the lexer stays untouched.
16563 "bytea" | "bytes" => ColumnTypeName::Bytes,
16564 // v7.17.0 Phase 7 — PG network address types
16565 // v7.17.0 had a Text-backed fallback here for
16566 // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16567 // each to a first-class type; the keywords are
16568 // bound below in the ζ-A block.
16569 // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16570 // The actual `to_tsvector` / `@@` / `ts_rank` surface
16571 // arrives in v7.12.1+; the type itself loads here so
16572 // mailrs's `scripts/init-schema.sql` runs unmodified.
16573 "tsvector" => ColumnTypeName::TsVector,
16574 "tsquery" => ColumnTypeName::TsQuery,
16575 // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
16576 // surface for Django / Rails / Hibernate's default
16577 // PK pattern.
16578 "uuid" => ColumnTypeName::Uuid,
16579 // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
16580 // Storage = three-field {months, days, micros}, catalog
16581 // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
16582 // line `INTERVAL` was parser-rejected at CREATE TABLE.
16583 "interval" => {
16584 // pg_dump emits field-qualified forms like `INTERVAL DAY TO
16585 // SECOND` and an optional `(p)` precision. SPG stores the full
16586 // {months,days,micros}; consume + ignore the qualifier/precision.
16587 while matches!(self.peek(), Token::To)
16588 || matches!(self.peek(), Token::Ident(s) if matches!(
16589 s.to_ascii_lowercase().as_str(),
16590 "year" | "month" | "day" | "hour" | "minute" | "second"
16591 ))
16592 {
16593 self.advance();
16594 }
16595 self.consume_optional_paren_size();
16596 ColumnTypeName::Interval
16597 }
16598 // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
16599 // i64 microseconds since 00:00:00. Wire OID 1083.
16600 // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
16601 "time" => {
16602 // v7.39 (round 424) — MySQL TIME carries a semantic
16603 // fractional-seconds precision, bare meaning 0.
16604 let n = self.take_optional_paren_size();
16605 if self.mysql_dialect {
16606 mysql_fsp = Some(n.unwrap_or(0).min(6));
16607 }
16608 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16609 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16610 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16611 {
16612 self.advance();
16613 self.advance();
16614 self.advance();
16615 ColumnTypeName::TimeTz
16616 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16617 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16618 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16619 {
16620 self.advance();
16621 self.advance();
16622 self.advance();
16623 ColumnTypeName::Time
16624 } else {
16625 ColumnTypeName::Time
16626 }
16627 }
16628 // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
16629 // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
16630 "year" => ColumnTypeName::Year,
16631 // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
16632 // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
16633 "timetz" => ColumnTypeName::TimeTz,
16634 // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
16635 // Wire OID 790.
16636 "money" => ColumnTypeName::Money,
16637 // v7.17.0 Phase 3.P0-38 — PG range types.
16638 "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
16639 "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
16640 "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
16641 "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
16642 "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
16643 "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
16644 // v7.37.5 δ — PG 14+ multirange keywords.
16645 "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
16646 "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
16647 "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
16648 "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
16649 "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
16650 "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
16651 // v7.37.5 ε — PG geometry scalar keywords.
16652 "point" => ColumnTypeName::Point,
16653 "lseg" => ColumnTypeName::Lseg,
16654 "path" => ColumnTypeName::Path,
16655 "box" => ColumnTypeName::PgBox,
16656 "polygon" => ColumnTypeName::Polygon,
16657 "line" => ColumnTypeName::Line,
16658 "circle" => ColumnTypeName::Circle,
16659 // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
16660 "inet" => ColumnTypeName::Inet,
16661 "cidr" => ColumnTypeName::Cidr,
16662 "macaddr" => ColumnTypeName::Macaddr,
16663 "macaddr8" => ColumnTypeName::Macaddr8,
16664 // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
16665 // width in the value, so the optional `(N)` typmod is accepted and
16666 // ignored (the column stores whatever width it's given).
16667 "bit" => {
16668 let varying = matches!(
16669 self.peek(),
16670 Token::Ident(k) if k.eq_ignore_ascii_case("varying")
16671 );
16672 if varying {
16673 self.advance();
16674 }
16675 // v7.39 (round 281) — the length used to be parsed and
16676 // dropped, so `bit(3)` accepted a five-bit string.
16677 let n = if matches!(self.peek(), Token::LParen) {
16678 self.parse_paren_size("BIT")?
16679 } else {
16680 0
16681 };
16682 if varying {
16683 ColumnTypeName::BitVarying(n)
16684 } else {
16685 ColumnTypeName::Bit(n)
16686 }
16687 }
16688 "varbit" => {
16689 let n = if matches!(self.peek(), Token::LParen) {
16690 self.parse_paren_size("VARBIT")?
16691 } else {
16692 0
16693 };
16694 ColumnTypeName::BitVarying(n)
16695 }
16696 "xml" => ColumnTypeName::Xml,
16697 // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
16698 "hstore" => ColumnTypeName::Hstore,
16699 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
16700 // `ENUM('a','b','c')`. Storage is TEXT; the value
16701 // list lands on `inline_enum_variants` for the
16702 // engine to validate INSERT cells against. Empty
16703 // value list is a parse error (matches MySQL).
16704 "enum" => {
16705 // Expect the opening `(`.
16706 if !matches!(self.peek(), Token::LParen) {
16707 return Err(self.err(alloc::format!(
16708 "expected '(' after ENUM, got {:?}",
16709 self.peek()
16710 )));
16711 }
16712 self.advance();
16713 let mut variants: Vec<String> = Vec::new();
16714 loop {
16715 match self.advance() {
16716 Token::String(s) => variants.push(s),
16717 other => {
16718 return Err(self.err(alloc::format!(
16719 "ENUM(...) expects string literal variants, got {other:?}"
16720 )));
16721 }
16722 }
16723 match self.peek() {
16724 Token::Comma => {
16725 self.advance();
16726 continue;
16727 }
16728 Token::RParen => {
16729 self.advance();
16730 break;
16731 }
16732 other => {
16733 return Err(self.err(alloc::format!(
16734 "expected ',' or ')' in ENUM(...), got {other:?}"
16735 )));
16736 }
16737 }
16738 }
16739 if variants.is_empty() {
16740 return Err(self.err("ENUM(...) must declare at least one variant".into()));
16741 }
16742 inline_enum_variants = Some(variants);
16743 // Storage is plain TEXT; the variant list lives on
16744 // the ColumnSchema side.
16745 ColumnTypeName::Text
16746 }
16747 // v7.17.0 Phase 3.P0-37 — MySQL inline SET
16748 // `SET('a','b','c')`. Same parse shape as ENUM;
16749 // semantics differ (subset rather than pick-one).
16750 "set" => {
16751 if !matches!(self.peek(), Token::LParen) {
16752 return Err(self.err(alloc::format!(
16753 "expected '(' after SET, got {:?}",
16754 self.peek()
16755 )));
16756 }
16757 self.advance();
16758 let mut variants: Vec<String> = Vec::new();
16759 loop {
16760 match self.advance() {
16761 Token::String(s) => variants.push(s),
16762 other => {
16763 return Err(self.err(alloc::format!(
16764 "SET(...) expects string literal variants, got {other:?}"
16765 )));
16766 }
16767 }
16768 match self.peek() {
16769 Token::Comma => {
16770 self.advance();
16771 continue;
16772 }
16773 Token::RParen => {
16774 self.advance();
16775 break;
16776 }
16777 other => {
16778 return Err(self.err(alloc::format!(
16779 "expected ',' or ')' in SET(...), got {other:?}"
16780 )));
16781 }
16782 }
16783 }
16784 if variants.is_empty() {
16785 return Err(self.err("SET(...) must declare at least one variant".into()));
16786 }
16787 inline_set_variants = Some(variants);
16788 ColumnTypeName::Text
16789 }
16790 _other => {
16791 // v7.17.0 Phase 1.4 — unknown ident → defer
16792 // resolution to the engine. Stored as Text in
16793 // ColumnTypeName + the original name carried as
16794 // `user_type_ref` so CREATE TABLE can look up
16795 // user-defined enum / domain types.
16796 user_type_ref = Some(ty_ident.clone());
16797 ColumnTypeName::Text
16798 }
16799 };
16800 // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
16801 // right after the type keyword. Pre-4.4 SPG consumed +
16802 // discarded the keyword, leaving a customer column
16803 // declared `id INT UNSIGNED NOT NULL` silently accepting
16804 // negative values — a Tier-A correctness drift where
16805 // application invariants (auto-increment-IDs never
16806 // negative) silently broke on cutover. Now: capture as
16807 // a column flag, persist on the schema, enforce at
16808 // INSERT / UPDATE time.
16809 let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
16810 {
16811 self.advance();
16812 true
16813 } else {
16814 false
16815 };
16816 // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
16817 // `<type> COLLATE <name>` post-fixes on text columns. SPG
16818 // stores text as UTF-8 always so CHARACTER SET is still a
16819 // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
16820 // name: it gets classified into a `Collation` variant the
16821 // engine consults at WHERE-eval time. PG `default` /
16822 // `pg_catalog.default` / `C` / `POSIX` collations all
16823 // resolve to `Binary` (the prior behaviour); `_ci` /
16824 // `case_insensitive` / `nocase` shift to CaseInsensitive.
16825 // The schema-qualifier form (`pg_catalog.default`) lexes
16826 // as `Ident '.' Ident` — peek for the `.` and consume both
16827 // halves so it's treated as one collation name. PG's
16828 // `IDENT.IDENT` collation form (which can appear here) is
16829 // resolved by Collation::from_collation_name on the bare
16830 // identifier after the dot.
16831 let mut collation = Collation::Binary;
16832 // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
16833 // clause was written. The engine needs this to tell an explicit
16834 // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
16835 // clause at all: both resolve to `Collation::Binary`, but under the
16836 // MySQL dialect the latter takes the folding default collation.
16837 let mut collation_explicit = false;
16838 let mut collation_name: Option<alloc::string::String> = None;
16839 loop {
16840 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
16841 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
16842 {
16843 self.advance(); // CHARACTER
16844 self.advance(); // SET
16845 if matches!(
16846 self.peek(),
16847 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
16848 ) {
16849 self.advance();
16850 }
16851 continue;
16852 }
16853 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
16854 self.advance(); // COLLATE
16855 // Accept Ident / QuotedIdent / String AND the
16856 // keyword-tokenised `Default` (PG `pg_catalog.default`
16857 // and bare `DEFAULT` collation names — `default` is a
16858 // reserved word so the lexer hands back Token::Default
16859 // not Token::Ident).
16860 let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
16861 match this.peek().clone() {
16862 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
16863 this.advance();
16864 Some(s)
16865 }
16866 Token::Default => {
16867 this.advance();
16868 Some(alloc::string::String::from("default"))
16869 }
16870 _ => None,
16871 }
16872 };
16873 let raw = if let Some(head) = read_collation_atom(self) {
16874 // Schema-qualified PG form: `pg_catalog.default`.
16875 if matches!(self.peek(), Token::Dot) {
16876 self.advance();
16877 let tail = read_collation_atom(self).unwrap_or_default();
16878 alloc::format!("{head}.{tail}")
16879 } else {
16880 head
16881 }
16882 } else {
16883 alloc::string::String::new()
16884 };
16885 if !raw.is_empty() {
16886 collation_explicit = true;
16887 // v7.39 (round 676) — keep the name too. The enum below
16888 // folds C / POSIX / en_US / default into one value, and
16889 // `pg_attribute.attcollation` has to tell them apart.
16890 // The schema qualifier goes: PG's `pg_catalog.default`
16891 // and a bare `default` name the same collation.
16892 // v7.39 (round 679) — strip a SCHEMA qualifier, not an
16893 // encoding suffix. Round 676 used `rsplit('.')` for
16894 // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
16895 // PG writes `pg_catalog.default` (qualifier) and
16896 // `en_US.utf8` (locale + encoding) with the same
16897 // separator. Only `pg_catalog.` is a qualifier, and it
16898 // is the only one PG's own dumps emit.
16899 let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
16900 let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
16901 collation_name = Some(alloc::string::String::from(bare));
16902 let parsed = Collation::from_collation_name(&raw);
16903 // Last COLLATE clause wins, but `Binary` from a
16904 // bare keyword like `default` should not
16905 // silently downgrade a stronger one set earlier
16906 // on the same column. v7.17 only ships one
16907 // non-Binary variant so a simple OR is enough.
16908 if parsed != Collation::Binary {
16909 collation = parsed;
16910 }
16911 }
16912 continue;
16913 }
16914 break;
16915 }
16916 // v7.10.10 — postfix `[]` widens the base type to its array
16917 // type. PG accepts `TYPE[]` after any base type and so does
16918 // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
16919 // all through; the old "only TEXT[]" note was stale).
16920 if matches!(self.peek(), Token::LBracket) {
16921 self.advance();
16922 if !matches!(self.peek(), Token::RBracket) {
16923 return Err(self.err(alloc::format!(
16924 "TEXT[] takes no dimension; got {:?}",
16925 self.peek()
16926 )));
16927 }
16928 self.advance();
16929 // v7.11.13 — widened to INT[] and BIGINT[] in addition
16930 // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
16931 // still error here.
16932 ty = match ty {
16933 ColumnTypeName::Text => ColumnTypeName::TextArray,
16934 ColumnTypeName::Int => ColumnTypeName::IntArray,
16935 ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
16936 // v7.37.5 β-P4 — INTERVAL[] via the same postfix
16937 // `[]` grammar. Wire OID 1187.
16938 ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
16939 // v7.37.5 γ — full PG array-of-scalar family.
16940 ColumnTypeName::Bool => ColumnTypeName::BoolArray,
16941 ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
16942 ColumnTypeName::Float => ColumnTypeName::FloatArray,
16943 // NUMERIC(p, s) loses its precision params at the
16944 // array level (matches PG: `NUMERIC[]` is untyped,
16945 // per-element precision flows through values).
16946 ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
16947 ColumnTypeName::Date => ColumnTypeName::DateArray,
16948 ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
16949 ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
16950 ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
16951 ColumnTypeName::Json => ColumnTypeName::JsonArray,
16952 ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
16953 ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
16954 // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
16955 // the array level (matches PG semantics where the
16956 // element precision is per-row, not column-wide).
16957 ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
16958 ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
16959 // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
16960 // follow-up.
16961 ColumnTypeName::Money => ColumnTypeName::MoneyArray,
16962 other => {
16963 return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
16964 }
16965 };
16966 // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
16967 // for INT/TEXT/BIGINT. Anything else is an error.
16968 if matches!(self.peek(), Token::LBracket) {
16969 self.advance();
16970 if !matches!(self.peek(), Token::RBracket) {
16971 return Err(self.err(alloc::format!(
16972 "TYPE[][] second dimension takes no size; got {:?}",
16973 self.peek()
16974 )));
16975 }
16976 self.advance();
16977 ty = match ty {
16978 ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
16979 ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
16980 ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
16981 // v7.39 (read01 round 75) — bool[][].
16982 ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
16983 other => {
16984 return Err(self.err(alloc::format!(
16985 "v7.17 2D arrays support INT[][] / BIGINT[][] / \
16986 TEXT[][] only; got {other:?}"
16987 )));
16988 }
16989 };
16990 }
16991 }
16992 Ok((
16993 ty,
16994 implied_auto_increment,
16995 implied_not_null,
16996 user_type_ref,
16997 collation,
16998 collation_explicit,
16999 collation_name,
17000 is_unsigned,
17001 inline_enum_variants,
17002 inline_set_variants,
17003 mysql_int_width,
17004 mysql_fsp,
17005 ))
17006 }
17007
17008 fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
17009 // v7.20 — PG reserves the table-constraint keywords, so a
17010 // BARE `UNIQUE` / `PRIMARY` / … in column position is a
17011 // malformed constraint clause (e.g. `UNIQUE a` missing its
17012 // parens), not a column named "unique". Since v7.17's
17013 // unknown-type leniency (`user_type_ref`) such a clause
17014 // would otherwise parse as a column with a user-defined
17015 // type — silently accepting invalid DDL. Quoted
17016 // identifiers ("unique" / `unique`) remain valid names.
17017 if let Token::Ident(s) = self.peek()
17018 && [
17019 "unique",
17020 "primary",
17021 "foreign",
17022 "constraint",
17023 "check",
17024 "references",
17025 "exclude",
17026 ]
17027 .iter()
17028 .any(|kw| s.eq_ignore_ascii_case(kw))
17029 {
17030 return Err(self.err(alloc::format!(
17031 "unexpected reserved keyword '{s}' at start of column definition \
17032 (malformed table constraint?)"
17033 )));
17034 }
17035 let name = self.expect_ident_like()?;
17036 let (
17037 ty,
17038 implied_auto_increment,
17039 implied_not_null,
17040 user_type_ref,
17041 collation,
17042 collation_explicit,
17043 collation_name,
17044 is_unsigned,
17045 inline_enum_variants,
17046 inline_set_variants,
17047 mysql_int_width,
17048 mysql_fsp,
17049 ) = self.parse_type_with_implied_flags()?;
17050 // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
17051 // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
17052 // each at most once.
17053 let mut default: Option<Expr> = None;
17054 let mut nullable = !implied_not_null;
17055 let mut nullability_seen = implied_not_null;
17056 let mut auto_increment = implied_auto_increment;
17057 let mut is_primary_key = false;
17058 let mut is_unique = false;
17059 let mut unique_nulls_not_distinct = false;
17060 let mut constraint_deferrable = false;
17061 let mut constraint_initially_deferred = false;
17062 let mut check: Option<Expr> = None;
17063 let mut on_update_runtime: Option<Expr> = None;
17064 let mut generated_stored_expr: Option<Box<Expr>> = None;
17065 let mut identity_always = false;
17066 loop {
17067 // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
17068 // not-null constraints by name and pg_dump emits them
17069 // inline: `id bigint CONSTRAINT contacts_id_not_null1
17070 // NOT NULL`. Accept and discard the name; whatever
17071 // constraint follows is parsed by the arms below.
17072 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
17073 // v7.39 (round 308, V29) — a name on an inline
17074 // REFERENCES belongs to the FOREIGN KEY, and the caller
17075 // (`parse_column_def_with_fk`) is what builds it, so
17076 // leave the whole clause for it. Dropping the name here
17077 // is what made `CONSTRAINT fk_a REFERENCES …` come back
17078 // as the synthesised `c_pid_fkey` — which then could
17079 // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
17080 // `advance()` takes tokens by `mem::replace`, so there
17081 // is no rewinding once consumed.
17082 if matches!(
17083 self.tokens.get(self.pos + 2),
17084 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
17085 ) {
17086 break;
17087 }
17088 self.advance();
17089 let _name = self.expect_ident_like()?;
17090 continue;
17091 }
17092 // v7.39 (round 379) — MySQL's SHORT generated-column form
17093 // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
17094 // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
17095 // below), but hand-written schemas and app migrations use this.
17096 // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
17097 // SPG computes-and-stores either way, like the long form.
17098 if matches!(self.peek(), Token::As) {
17099 self.advance();
17100 if !matches!(self.peek(), Token::LParen) {
17101 return Err(self.err(alloc::format!(
17102 "expected '(' after AS in a generated column, got {:?}",
17103 self.peek()
17104 )));
17105 }
17106 self.advance();
17107 let expr = self.parse_expr(0)?;
17108 if !matches!(self.peek(), Token::RParen) {
17109 return Err(self.err(alloc::format!(
17110 "expected ')' after AS (<expr>), got {:?}",
17111 self.peek()
17112 )));
17113 }
17114 self.advance();
17115 if matches!(self.peek(), Token::Ident(s)
17116 if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
17117 {
17118 self.advance();
17119 }
17120 generated_stored_expr = Some(alloc::boxed::Box::new(expr));
17121 continue;
17122 }
17123 // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
17124 // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
17125 // the modern replacement for SERIAL in hand-written
17126 // schemas). Both flavours map onto the auto-increment
17127 // machinery — SPG's serial semantics ≈ BY DEFAULT;
17128 // ALWAYS's reject-explicit-values nuance is documented
17129 // leniency. Generated EXPRESSION columns
17130 // (`AS (expr) STORED`) are not supported: error loudly
17131 // instead of silently storing NULLs.
17132 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
17133 self.advance();
17134 let mut saw_generated_always = false;
17135 match self.peek().clone() {
17136 Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
17137 self.advance();
17138 saw_generated_always = true;
17139 }
17140 Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
17141 self.advance();
17142 if !matches!(self.peek(), Token::Default) {
17143 return Err(self.err(alloc::format!(
17144 "expected DEFAULT after GENERATED BY, got {:?}",
17145 self.peek()
17146 )));
17147 }
17148 self.advance();
17149 }
17150 other => {
17151 return Err(self.err(alloc::format!(
17152 "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
17153 )));
17154 }
17155 }
17156 if !matches!(self.peek(), Token::As) {
17157 return Err(self.err(alloc::format!(
17158 "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
17159 self.peek()
17160 )));
17161 }
17162 self.advance();
17163 // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
17164 // ( <expr> ) STORED` stored computed-column. The
17165 // expression is captured for the engine to recompute
17166 // on every INSERT / UPDATE. v7.37.7 accepts the
17167 // STORED keyword only; PG also has VIRTUAL, which
17168 // v7.37.7 carves out (sentori only uses STORED).
17169 if matches!(self.peek(), Token::LParen) {
17170 self.advance();
17171 let expr = self.parse_expr(0)?;
17172 if !matches!(self.peek(), Token::RParen) {
17173 return Err(self.err(alloc::format!(
17174 "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
17175 self.peek()
17176 )));
17177 }
17178 self.advance();
17179 let stored = match self.peek() {
17180 Token::Ident(s) | Token::QuotedIdent(s)
17181 if s.eq_ignore_ascii_case("stored") =>
17182 {
17183 self.advance();
17184 true
17185 }
17186 // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
17187 // generated columns. SPG computes them on write and
17188 // persists like STORED; the two are observably
17189 // identical for query results (the value, recompute
17190 // on base-column change, and NOT NULL enforcement all
17191 // match), so a PG 18 schema/dump using VIRTUAL loads
17192 // and behaves correctly. The compute-on-read storage
17193 // saving is an invisible internal difference.
17194 Token::Ident(s) | Token::QuotedIdent(s)
17195 if s.eq_ignore_ascii_case("virtual") =>
17196 {
17197 self.advance();
17198 false
17199 }
17200 other => {
17201 return Err(self.err(alloc::format!(
17202 "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
17203 got {other:?}"
17204 )));
17205 }
17206 };
17207 let _ = stored; // STORED / VIRTUAL both compute-and-store.
17208 generated_stored_expr = Some(Box::new(expr));
17209 continue;
17210 }
17211 self.expect_keyword_ident("identity")?;
17212 // Optional `(START WITH 1 INCREMENT BY 1 …)` —
17213 // consume the balanced parens and discard (SPG's
17214 // auto-increment is max+1-scan based).
17215 if matches!(self.peek(), Token::LParen) {
17216 let mut depth = 0usize;
17217 loop {
17218 match self.advance() {
17219 Token::LParen => depth += 1,
17220 Token::RParen => {
17221 depth -= 1;
17222 if depth == 0 {
17223 break;
17224 }
17225 }
17226 Token::Eof => {
17227 return Err(self.err(
17228 "unterminated sequence-options parens after IDENTITY".into(),
17229 ));
17230 }
17231 _ => {}
17232 }
17233 }
17234 }
17235 auto_increment = true;
17236 // v7.38 (read01) — remember the ALWAYS flavour so the engine
17237 // can reject explicit non-DEFAULT INSERT values (unless
17238 // OVERRIDING SYSTEM VALUE) the way PG does.
17239 identity_always = saw_generated_always;
17240 // PG identity columns are implicitly NOT NULL.
17241 nullable = false;
17242 continue;
17243 }
17244 // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
17245 // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
17246 // is accepted today. The "ON" token is an Ident
17247 // (not reserved) — peek before consuming.
17248 if matches!(self.peek(), Token::On)
17249 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17250 {
17251 self.advance(); // ON
17252 self.advance(); // update
17253 // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17254 let next = self.peek().clone();
17255 match next {
17256 Token::Ident(s) | Token::QuotedIdent(s)
17257 if s.eq_ignore_ascii_case("current_timestamp") =>
17258 {
17259 self.advance();
17260 // Optional `(N)` precision.
17261 if matches!(self.peek(), Token::LParen) {
17262 self.advance();
17263 if !matches!(self.peek(), Token::Integer(_)) {
17264 return Err(self.err(alloc::format!(
17265 "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17266 self.peek()
17267 )));
17268 }
17269 self.advance();
17270 if !matches!(self.peek(), Token::RParen) {
17271 return Err(self.err(alloc::format!(
17272 "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17273 self.peek()
17274 )));
17275 }
17276 self.advance();
17277 }
17278 on_update_runtime = Some(Expr::FunctionCall {
17279 name: "now".into(),
17280 args: Vec::new(),
17281 });
17282 continue;
17283 }
17284 other => {
17285 return Err(self.err(alloc::format!(
17286 "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17287 )));
17288 }
17289 }
17290 }
17291 if matches!(self.peek(), Token::Default) {
17292 if default.is_some() {
17293 return Err(self.err("DEFAULT specified twice".into()));
17294 }
17295 self.advance();
17296 default = Some(self.parse_expr(0)?);
17297 continue;
17298 }
17299 // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17300 // token with NOT NULL and sits EARLIER in the loop than the
17301 // deferrability arm, so without the lookahead it was reported as
17302 // "NOT NULL specified twice" (or "expected NULL after NOT").
17303 if matches!(self.peek(), Token::Not)
17304 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17305 {
17306 // NOT DEFERRABLE — explicit immediate; nothing to carry.
17307 self.consume_optional_deferrable_clauses()?;
17308 continue;
17309 }
17310 if matches!(self.peek(), Token::Not) {
17311 if nullability_seen {
17312 return Err(self.err("NOT NULL specified twice".into()));
17313 }
17314 self.advance();
17315 if !matches!(self.peek(), Token::Null) {
17316 return Err(self.err(format!(
17317 "expected NULL after NOT in column def, got {:?}",
17318 self.peek()
17319 )));
17320 }
17321 self.advance();
17322 nullable = false;
17323 nullability_seen = true;
17324 continue;
17325 }
17326 // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17327 // "this column is nullable" marker (the default in
17328 // standard SQL anyway). mysqldump emits it routinely
17329 // (`col TYPE NULL DEFAULT NULL` for nullable
17330 // timestamps etc). Accept + no-op.
17331 if matches!(self.peek(), Token::Null) {
17332 if nullability_seen && !nullable {
17333 // v7.39 (round 761, F31 tranche 2 #31) — PG's
17334 // sentence, PG18-measured (the table name is the
17335 // caller's; the column half is exact).
17336 return Err(self.err(alloc::format!(
17337 "conflicting NULL/NOT NULL declarations for column \"{name}\""
17338 )));
17339 }
17340 self.advance();
17341 nullable = true;
17342 nullability_seen = true;
17343 continue;
17344 }
17345 // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17346 // arrives as a bare Ident. Match either, case-insensitive.
17347 if let Token::Ident(s) = self.peek()
17348 && (s.eq_ignore_ascii_case("auto_increment")
17349 || s.eq_ignore_ascii_case("autoincrement"))
17350 {
17351 if auto_increment {
17352 return Err(self.err("AUTO_INCREMENT specified twice".into()));
17353 }
17354 self.advance();
17355 auto_increment = true;
17356 continue;
17357 }
17358 // v7.9.13 — inline `PRIMARY KEY` column constraint
17359 // (mailrs F1). Implies `NOT NULL`. The engine creates
17360 // a BTree index for the PK column at CREATE TABLE time
17361 // so FK parent-side index lookups resolve.
17362 // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17363 // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17364 // spelling was a parse error, so a pg_dump carrying one stopped
17365 // mid-restore. The clauses are consumed by the same helper the FK
17366 // path has used since round 288 and recorded nowhere: SPG enforces
17367 // the constraint IMMEDIATELY either way, which fails earlier than
17368 // PG inside a transaction that violates-then-repairs — a refusal,
17369 // not a wrong answer. True deferral is the open remainder of F08.
17370 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17371 || (matches!(self.peek(), Token::Not)
17372 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
17373 {
17374 // v7.39 (round 711) — CARRIED now (the storing half of
17375 // F08); round 621 only consumed.
17376 let (d, idef) = self.consume_deferrable_clauses_timed()?;
17377 constraint_deferrable |= d;
17378 constraint_initially_deferred |= idef;
17379 continue;
17380 }
17381 if let Token::Ident(s) = self.peek()
17382 && s.eq_ignore_ascii_case("primary")
17383 {
17384 if is_primary_key {
17385 return Err(self.err("PRIMARY KEY specified twice".into()));
17386 }
17387 // Peek-ahead for the required `KEY` token.
17388 let next = self.tokens.get(self.pos + 1);
17389 let next_is_key = matches!(
17390 next,
17391 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17392 );
17393 if !next_is_key {
17394 return Err(self.err(format!(
17395 "expected KEY after PRIMARY in column def, got {:?}",
17396 next
17397 )));
17398 }
17399 self.advance(); // PRIMARY
17400 self.advance(); // KEY
17401 is_primary_key = true;
17402 if nullability_seen && nullable {
17403 return Err(self.err(
17404 "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17405 ));
17406 }
17407 nullable = false;
17408 nullability_seen = true;
17409 continue;
17410 }
17411 // v7.13.0 — inline `UNIQUE` column constraint
17412 // (mailrs round-5 G2). Fold into a single-column
17413 // table-level UNIQUE at CREATE TABLE post-process time.
17414 if let Token::Ident(s) = self.peek()
17415 && s.eq_ignore_ascii_case("unique")
17416 {
17417 if is_unique {
17418 return Err(self.err("UNIQUE specified twice".into()));
17419 }
17420 self.advance();
17421 is_unique = true;
17422 // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17423 // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17424 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17425 let n1 = self.tokens.get(self.pos + 1);
17426 let n2 = self.tokens.get(self.pos + 2);
17427 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17428 self.advance(); // NULLS
17429 self.advance(); // NOT
17430 self.advance(); // DISTINCT
17431 unique_nulls_not_distinct = true;
17432 } else if matches!(n1, Some(Token::Distinct)) {
17433 self.advance(); // NULLS
17434 self.advance(); // DISTINCT
17435 }
17436 }
17437 continue;
17438 }
17439 // v7.13.0 — inline `CHECK (<expr>)` column constraint
17440 // (mailrs round-5 G3). PG semantics: column-level
17441 // CHECK is equivalent to a table-level CHECK. Multiple
17442 // inline CHECKs on the same column AND together.
17443 if let Token::Ident(s) = self.peek()
17444 && s.eq_ignore_ascii_case("check")
17445 {
17446 self.advance();
17447 if !matches!(self.peek(), Token::LParen) {
17448 return Err(self.err(alloc::format!(
17449 "expected '(' after CHECK in column def, got {:?}",
17450 self.peek()
17451 )));
17452 }
17453 self.advance();
17454 let pred = self.parse_expr(0)?;
17455 if !matches!(self.peek(), Token::RParen) {
17456 return Err(self.err(alloc::format!(
17457 "expected ')' to close CHECK predicate, got {:?}",
17458 self.peek()
17459 )));
17460 }
17461 self.advance();
17462 check = Some(match check.take() {
17463 Some(prev) => Expr::Binary {
17464 op: BinOp::And,
17465 lhs: Box::new(prev),
17466 rhs: Box::new(pred),
17467 },
17468 None => pred,
17469 });
17470 continue;
17471 }
17472 break;
17473 }
17474 Ok(ColumnDef {
17475 name,
17476 ty,
17477 nullable,
17478 default,
17479 auto_increment,
17480 is_primary_key,
17481 is_unique,
17482 unique_nulls_not_distinct,
17483 constraint_deferrable,
17484 constraint_initially_deferred,
17485 check,
17486 user_type_ref,
17487 on_update_runtime,
17488 collation,
17489 collation_explicit,
17490 collation_name,
17491 is_unsigned,
17492 inline_enum_variants,
17493 inline_set_variants,
17494 generated_stored_expr,
17495 identity_always,
17496 mysql_int_width,
17497 mysql_fsp,
17498 })
17499 }
17500
17501 /// `NUMERIC` may appear without parameters, with one (precision
17502 /// only, scale=0), or with both. Returns `(precision, scale)` with
17503 /// 0 = unspecified for the bare form.
17504 fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17505 if !matches!(self.peek(), Token::LParen) {
17506 // Bare `NUMERIC` — PG treats this as "unlimited precision";
17507 // we surface it as precision=0 to mean "unconstrained" so
17508 // the engine doesn't need a separate variant.
17509 return Ok((0, 0));
17510 }
17511 self.advance();
17512 // v7.39 (round 272) — PG's declared precision runs to 1000, and
17513 // it words the out-of-range case with the value it saw. SPG
17514 // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17515 // accepts failed to parse at all; values wider than i128 are
17516 // carried by the arbitrary-precision form.
17517 let precision = match self.advance() {
17518 Token::Integer(n) if (1..=1000).contains(&n) => {
17519 u16::try_from(n).expect("range-checked")
17520 }
17521 Token::Integer(n) => {
17522 return Err(ParseError {
17523 message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17524 token_pos: self.consumed_pos(),
17525 });
17526 }
17527 other => {
17528 return Err(ParseError {
17529 message: format!(
17530 "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17531 ),
17532 token_pos: self.consumed_pos(),
17533 });
17534 }
17535 };
17536 // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17537 // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17538 // then overflows). A negative scale rounds to tens / hundreds / …
17539 let scale = if matches!(self.peek(), Token::Comma) {
17540 self.advance();
17541 let neg = if matches!(self.peek(), Token::Minus) {
17542 self.advance();
17543 true
17544 } else {
17545 false
17546 };
17547 match self.advance() {
17548 Token::Integer(n) => {
17549 let signed = if neg { -n } else { n };
17550 if !(-1000..=1000).contains(&signed) {
17551 return Err(ParseError {
17552 message: format!(
17553 "NUMERIC scale {signed} must be between -1000 and 1000"
17554 ),
17555 token_pos: self.consumed_pos(),
17556 });
17557 }
17558 i16::try_from(signed).expect("range-checked")
17559 }
17560 other => {
17561 return Err(ParseError {
17562 message: format!("NUMERIC scale must be an integer, got {other:?}"),
17563 token_pos: self.consumed_pos(),
17564 });
17565 }
17566 }
17567 } else {
17568 0
17569 };
17570 if !matches!(self.peek(), Token::RParen) {
17571 return Err(self.err(format!(
17572 "expected ')' to close NUMERIC params, got {:?}",
17573 self.peek()
17574 )));
17575 }
17576 self.advance();
17577 Ok((precision, scale))
17578 }
17579
17580 /// Parse `(N)` where `N` is a positive integer literal — used by the
17581 /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
17582 /// for the error message.
17583 /// v6.0.1: parse the optional `USING <encoding>` clause that
17584 /// follows `VECTOR(N)` in a column definition. Missing clause
17585 /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
17586 /// ident → `ParseError` listing the encodings recognised today.
17587 fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
17588 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
17589 return Ok(VecEncoding::F32);
17590 }
17591 // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
17592 // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
17593 // consume the token when the very next token is a known
17594 // vector-encoding keyword (SQ8 / HALF). Otherwise leave
17595 // `USING` for the caller — it's the rewrite-expression form.
17596 let n1 = self.tokens.get(self.pos + 1);
17597 let next_is_encoding = matches!(
17598 n1,
17599 Some(Token::Ident(s))
17600 if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
17601 );
17602 if !next_is_encoding {
17603 return Ok(VecEncoding::F32);
17604 }
17605 self.advance();
17606 let enc_ident = match self.advance() {
17607 Token::Ident(s) => s,
17608 other => {
17609 return Err(self.err(format!(
17610 "expected vector encoding after USING, got {other:?}"
17611 )));
17612 }
17613 };
17614 match enc_ident.to_ascii_lowercase().as_str() {
17615 "sq8" => Ok(VecEncoding::Sq8),
17616 // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
17617 // binary16 per-element storage.
17618 "half" => Ok(VecEncoding::F16),
17619 other => Err(self.err(format!(
17620 "unknown vector encoding {other:?}; supported: SQ8, HALF"
17621 ))),
17622 }
17623 }
17624
17625 /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
17626 /// without consuming it. Returns `Some(N)` when the next
17627 /// tokens are `( <int> )`; None otherwise. Used by the
17628 /// TINYINT classifier to decide whether to map to Bool or
17629 /// SmallInt.
17630 fn peek_optional_paren_size_value(&self) -> Option<i64> {
17631 if !matches!(self.peek(), Token::LParen) {
17632 return None;
17633 }
17634 let next = self.tokens.get(self.pos + 1)?;
17635 let n = match next {
17636 Token::Integer(n) => *n,
17637 _ => return None,
17638 };
17639 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17640 return None;
17641 }
17642 Some(n)
17643 }
17644
17645 /// v7.14.0 — consume an optional MySQL display-width
17646 /// parenthesised number after an integer type, returning
17647 /// nothing. `TINYINT(1)` etc.
17648 /// v7.39 (round 360) — does the parenthesised group ahead contain a
17649 /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
17650 fn peek_paren_has_comma(&self) -> bool {
17651 let mut i = self.pos + 1;
17652 let mut depth = 1usize;
17653 while depth > 0 {
17654 match self.tokens.get(i) {
17655 Some(Token::LParen) => depth += 1,
17656 Some(Token::RParen) => depth -= 1,
17657 Some(Token::Comma) if depth == 1 => return true,
17658 None | Some(Token::Eof) => return false,
17659 _ => {}
17660 }
17661 i += 1;
17662 }
17663 false
17664 }
17665
17666 /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
17667 /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
17668 /// fractional-seconds precision that drives write truncation and render
17669 /// padding, where `consume_optional_paren_size` throws it away.
17670 /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
17671 fn take_optional_paren_size(&mut self) -> Option<u8> {
17672 let Some(Token::Integer(n)) = self
17673 .tokens
17674 .get(self.pos + 1)
17675 .filter(|_| matches!(self.peek(), Token::LParen))
17676 .cloned()
17677 else {
17678 self.consume_optional_paren_size();
17679 return None;
17680 };
17681 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17682 self.consume_optional_paren_size();
17683 return None;
17684 }
17685 self.consume_optional_paren_size();
17686 u8::try_from(n).ok()
17687 }
17688
17689 fn consume_optional_paren_size(&mut self) {
17690 if !matches!(self.peek(), Token::LParen) {
17691 return;
17692 }
17693 self.advance();
17694 // Skip until matching RParen (allow nested or any tokens).
17695 let mut depth = 1usize;
17696 while depth > 0 {
17697 match self.peek() {
17698 Token::LParen => depth += 1,
17699 Token::RParen => depth -= 1,
17700 Token::Eof => return,
17701 _ => {}
17702 }
17703 self.advance();
17704 }
17705 }
17706
17707 fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
17708 if !matches!(self.peek(), Token::LParen) {
17709 return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
17710 }
17711 self.advance();
17712 let n = match self.advance() {
17713 Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
17714 message: format!("{label} size too large: {n}"),
17715 token_pos: self.consumed_pos(),
17716 })?,
17717 other => {
17718 return Err(ParseError {
17719 message: format!("expected positive integer {label} size, got {other:?}"),
17720 token_pos: self.consumed_pos(),
17721 });
17722 }
17723 };
17724 if !matches!(self.peek(), Token::RParen) {
17725 return Err(self.err(format!(
17726 "expected ')' after {label} size, got {:?}",
17727 self.peek()
17728 )));
17729 }
17730 self.advance();
17731 Ok(n)
17732 }
17733
17734 /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
17735 /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
17736 /// key, like MySQL) whose action skips conflicting rows.
17737 /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
17738 /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
17739 /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
17740 /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
17741 /// common bulk-upsert spellings —
17742 /// INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
17743 /// REPLACE INTO t SELECT …
17744 /// — were a parse error / a duplicate-key failure respectively.
17745 ///
17746 /// Precedence: an explicitly written clause beats a statement-level flag.
17747 /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
17748 /// implicit `REPLACE` and `IGNORE` lowerings.
17749 fn parse_insert_conflict_clause(
17750 &mut self,
17751 replace: bool,
17752 ignore: bool,
17753 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17754 if let Some(c) = self.parse_optional_on_duplicate_key()? {
17755 return Ok(Some(c));
17756 }
17757 if let Some(c) = self.parse_optional_on_conflict()? {
17758 return Ok(Some(c));
17759 }
17760 if replace {
17761 // REPLACE INTO = delete-then-insert, which PG spells as
17762 // `ON CONFLICT DO UPDATE SET` over every column; the engine
17763 // reads an empty assignment list as "take the incoming row".
17764 return Ok(Some(crate::ast::OnConflictClause {
17765 target_columns: Vec::new(),
17766 index_where: None,
17767 constraint_name: None,
17768 mysql_lowered: true,
17769 action: crate::ast::OnConflictAction::Update {
17770 assignments: Vec::new(),
17771 where_: None,
17772 },
17773 }));
17774 }
17775 if ignore {
17776 return Ok(Some(Self::insert_ignore_clause()));
17777 }
17778 Ok(None)
17779 }
17780
17781 /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
17782 /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
17783 /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
17784 /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
17785 fn parse_optional_on_duplicate_key(
17786 &mut self,
17787 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17788 if !(matches!(self.peek(), Token::On)
17789 && matches!(self.tokens.get(self.pos + 1),
17790 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
17791 {
17792 return Ok(None);
17793 }
17794 self.advance(); // ON
17795 self.advance(); // DUPLICATE
17796 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
17797 return Err(self.err(format!(
17798 "expected KEY after ON DUPLICATE, got {:?}",
17799 self.peek()
17800 )));
17801 }
17802 self.advance();
17803 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
17804 return Err(self.err(format!(
17805 "expected UPDATE after ON DUPLICATE KEY, got {:?}",
17806 self.peek()
17807 )));
17808 }
17809 self.advance();
17810 let mut assignments: Vec<(String, Expr)> = Vec::new();
17811 loop {
17812 let col = self.expect_ident_like()?;
17813 if !matches!(self.peek(), Token::Eq) {
17814 return Err(self.err(format!(
17815 "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
17816 self.peek()
17817 )));
17818 }
17819 self.advance();
17820 let mut expr = self.parse_expr(0)?;
17821 Self::rewrite_mysql_values_refs(&mut expr);
17822 assignments.push((col, expr));
17823 if matches!(self.peek(), Token::Comma) {
17824 self.advance();
17825 continue;
17826 }
17827 break;
17828 }
17829 Ok(Some(crate::ast::OnConflictClause {
17830 target_columns: Vec::new(),
17831 index_where: None,
17832 constraint_name: None,
17833 mysql_lowered: true,
17834 action: crate::ast::OnConflictAction::Update {
17835 assignments,
17836 where_: None,
17837 },
17838 }))
17839 }
17840
17841 fn insert_ignore_clause() -> crate::ast::OnConflictClause {
17842 crate::ast::OnConflictClause {
17843 target_columns: Vec::new(),
17844 index_where: None,
17845 constraint_name: None,
17846 mysql_lowered: true,
17847 action: crate::ast::OnConflictAction::Nothing,
17848 }
17849 }
17850
17851 fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
17852 debug_assert!(
17853 matches!(self.peek(), Token::Insert)
17854 || (replace
17855 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
17856 );
17857 self.advance();
17858 // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
17859 // would raise a duplicate-key error instead of failing the statement,
17860 // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
17861 // plain ident to the lexer; only the MySQL dialect accepts it here.
17862 let ignore = self.mysql_dialect
17863 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
17864 if ignore {
17865 self.advance();
17866 }
17867 if !matches!(self.peek(), Token::Into) {
17868 return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
17869 }
17870 self.advance();
17871 let table = self.expect_ident_like()?;
17872 // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
17873 // grammar requires the AS keyword here (a bare identifier would be
17874 // ambiguous with a column list). The alias is what the ON CONFLICT
17875 // DO UPDATE expressions refer to the target row by.
17876 let alias = if matches!(self.peek(), Token::As) {
17877 self.advance();
17878 Some(self.expect_ident_like()?)
17879 } else {
17880 None
17881 };
17882 // v7.39 (round 428) — MySQL's SET-form INSERT:
17883 // INSERT INTO t SET a = 1, b = 'x'
17884 // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
17885 // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
17886 // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
17887 // measured). So it lowers to the column list + one VALUES row and
17888 // rejoins the ordinary path, which already handles every one of
17889 // those. PG has no such spelling, hence the dialect gate.
17890 if self.mysql_dialect
17891 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
17892 {
17893 self.advance(); // SET
17894 let mut names = Vec::new();
17895 let mut values = Vec::new();
17896 loop {
17897 names.push(self.expect_ident_like()?);
17898 if !matches!(self.peek(), Token::Eq) {
17899 return Err(self.err(alloc::format!(
17900 "expected '=' in INSERT … SET, got {:?}",
17901 self.peek()
17902 )));
17903 }
17904 self.advance();
17905 // `SET a = DEFAULT` rides the same `__column_default` marker
17906 // the VALUES-row and UPDATE-SET paths use; the INSERT
17907 // executor resolves it against the target column.
17908 if matches!(self.peek(), Token::Default) {
17909 self.advance();
17910 values.push(Expr::FunctionCall {
17911 name: "__column_default".to_string(),
17912 args: Vec::new(),
17913 });
17914 } else {
17915 values.push(self.parse_expr(0)?);
17916 }
17917 if matches!(self.peek(), Token::Comma) {
17918 self.advance();
17919 continue;
17920 }
17921 break;
17922 }
17923 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17924 let returning = self.parse_optional_returning()?;
17925 return Ok(Statement::Insert(InsertStatement {
17926 ctes: Vec::new(),
17927 table,
17928 alias,
17929 columns: Some(names),
17930 rows: alloc::vec![values],
17931 select_source: None,
17932 // MySQL's SET form has no `OVERRIDING …` clause (that is
17933 // PG's identity-column spelling).
17934 overriding: Overriding::None,
17935 mysql_ignore: ignore,
17936 on_conflict,
17937 returning,
17938 }));
17939 }
17940 // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
17941 // v7.39 (round 151) — a SELECT or WITH right after the paren is
17942 // a parenthesized query source instead (PG select_with_parens:
17943 // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
17944 // both keywords are reserved in PG, so no column list can start
17945 // with them.
17946 let columns = if matches!(self.peek(), Token::LParen) {
17947 self.advance();
17948 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17949 let select_stmt = if self.peek_is_with_kw() {
17950 self.advance();
17951 self.parse_nested_with_select()?
17952 } else {
17953 match self.parse_select_stmt()? {
17954 Statement::Select(s) => s,
17955 other => {
17956 return Err(self.err(alloc::format!(
17957 "expected SELECT in parenthesized INSERT source, got {other:?}"
17958 )));
17959 }
17960 }
17961 };
17962 if !matches!(self.peek(), Token::RParen) {
17963 return Err(self.err(format!(
17964 "expected ')' after parenthesized INSERT source, got {:?}",
17965 self.peek()
17966 )));
17967 }
17968 self.advance();
17969 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17970 let returning = self.parse_optional_returning()?;
17971 return Ok(Statement::Insert(InsertStatement {
17972 ctes: Vec::new(),
17973 table,
17974 alias: alias.clone(),
17975 columns: None,
17976 rows: Vec::new(),
17977 select_source: Some(Box::new(select_stmt)),
17978 on_conflict,
17979 returning,
17980 overriding: Overriding::None,
17981 mysql_ignore: ignore,
17982 }));
17983 }
17984 let mut names = Vec::new();
17985 loop {
17986 names.push(self.expect_ident_like()?);
17987 match self.peek() {
17988 Token::Comma => {
17989 self.advance();
17990 }
17991 Token::RParen => {
17992 self.advance();
17993 break;
17994 }
17995 other => {
17996 return Err(self.err(format!(
17997 "expected ',' or ')' in INSERT column list, got {other:?}"
17998 )));
17999 }
18000 }
18001 }
18002 Some(names)
18003 } else {
18004 None
18005 };
18006 // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
18007 // OVERRIDING SYSTEM VALUE for its identity columns. The clause
18008 // is captured on the statement so the engine can apply PG's
18009 // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
18010 let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
18011 {
18012 self.advance();
18013 let which = self.expect_ident_like()?;
18014 let ov = if which.eq_ignore_ascii_case("system") {
18015 Overriding::System
18016 } else if which.eq_ignore_ascii_case("user") {
18017 Overriding::User
18018 } else {
18019 return Err(self.err(format!(
18020 "expected SYSTEM or USER after OVERRIDING, got {which:?}"
18021 )));
18022 };
18023 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
18024 return Err(self.err(format!(
18025 "expected VALUE after OVERRIDING {}, got {:?}",
18026 which.to_ascii_uppercase(),
18027 self.peek()
18028 )));
18029 }
18030 self.advance();
18031 ov
18032 } else {
18033 Overriding::None
18034 };
18035 // `INSERT INTO t DEFAULT VALUES` — a single row made
18036 // entirely of column defaults. Lower to the permuted
18037 // column-list path with an empty list: every schema column
18038 // is unmapped, so the engine fills each from its default
18039 // (serials advance, plain defaults evaluate, the rest NULL).
18040 if matches!(self.peek(), Token::Default) {
18041 self.advance();
18042 if !matches!(self.peek(), Token::Values) {
18043 return Err(self.err(format!(
18044 "expected VALUES after DEFAULT in INSERT, got {:?}",
18045 self.peek()
18046 )));
18047 }
18048 self.advance();
18049 if columns.is_some() {
18050 return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
18051 }
18052 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18053 let returning = self.parse_optional_returning()?;
18054 return Ok(Statement::Insert(InsertStatement {
18055 ctes: Vec::new(),
18056 table,
18057 alias: alias.clone(),
18058 columns: Some(Vec::new()),
18059 rows: alloc::vec![Vec::new()],
18060 select_source: None,
18061 on_conflict,
18062 returning,
18063 overriding,
18064 mysql_ignore: ignore,
18065 }));
18066 }
18067 // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
18068 // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
18069 // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
18070 // SELECT …`) heads the SOURCE select, as in PG (the statement's
18071 // own WITH comes before INSERT).
18072 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18073 let select_stmt = if self.peek_is_with_kw() {
18074 self.advance();
18075 self.parse_nested_with_select()?
18076 } else {
18077 match self.parse_select_stmt()? {
18078 Statement::Select(s) => s,
18079 other => {
18080 return Err(self.err(alloc::format!(
18081 "expected SELECT after INSERT INTO ... target, got {other:?}"
18082 )));
18083 }
18084 }
18085 };
18086 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18087 let returning = self.parse_optional_returning()?;
18088 return Ok(Statement::Insert(InsertStatement {
18089 ctes: Vec::new(),
18090 table,
18091 alias: alias.clone(),
18092 columns,
18093 rows: Vec::new(),
18094 select_source: Some(Box::new(select_stmt)),
18095 on_conflict,
18096 returning,
18097 overriding,
18098 mysql_ignore: ignore,
18099 }));
18100 }
18101 if !matches!(self.peek(), Token::Values) {
18102 return Err(self.err(format!(
18103 "expected VALUES or SELECT after table name, got {:?}",
18104 self.peek()
18105 )));
18106 }
18107 self.advance();
18108 if !matches!(self.peek(), Token::LParen) {
18109 return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
18110 }
18111 let mut rows = Vec::new();
18112 loop {
18113 // Each iteration consumes one `(expr, expr, …)` tuple.
18114 if !matches!(self.peek(), Token::LParen) {
18115 return Err(self.err(format!(
18116 "expected '(' for next VALUES tuple, got {:?}",
18117 self.peek()
18118 )));
18119 }
18120 self.advance();
18121 let mut tuple = Vec::new();
18122 loop {
18123 // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
18124 // the column's declared default for that slot. Rides out as the
18125 // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
18126 // path uses; the INSERT executor resolves it per target column.
18127 if matches!(self.peek(), Token::Default) {
18128 self.advance();
18129 tuple.push(Expr::FunctionCall {
18130 name: "__column_default".to_string(),
18131 args: Vec::new(),
18132 });
18133 } else {
18134 tuple.push(self.parse_expr(0)?);
18135 }
18136 match self.peek() {
18137 Token::Comma => {
18138 self.advance();
18139 }
18140 Token::RParen => {
18141 self.advance();
18142 break;
18143 }
18144 other => {
18145 return Err(self.err(format!(
18146 "expected ',' or ')' in VALUES tuple, got {other:?}"
18147 )));
18148 }
18149 }
18150 }
18151 if tuple.is_empty() {
18152 return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
18153 }
18154 rows.push(tuple);
18155 // Continue with comma-separated tuples.
18156 if matches!(self.peek(), Token::Comma) {
18157 self.advance();
18158 } else {
18159 break;
18160 }
18161 }
18162 // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
18163 // to ON CONFLICT DO UPDATE with an empty conflict target
18164 // (the engine picks the table's first unique index, which
18165 // matches MySQL's any-unique-key behaviour for the common
18166 // single-key case). `VALUES(col)` in the assignments is
18167 // MySQL's spelling of EXCLUDED.col.
18168 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18169 let returning = self.parse_optional_returning()?;
18170 Ok(Statement::Insert(InsertStatement {
18171 ctes: Vec::new(),
18172 table,
18173 alias,
18174 columns,
18175 rows,
18176 select_source: None,
18177 on_conflict,
18178 returning,
18179 overriding,
18180 mysql_ignore: ignore,
18181 }))
18182 }
18183
18184 /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
18185 /// the incoming row's value — exactly PG's EXCLUDED.col.
18186 fn rewrite_mysql_values_refs(e: &mut Expr) {
18187 match e {
18188 Expr::FunctionCall { name, args }
18189 if name.eq_ignore_ascii_case("values")
18190 && args.len() == 1
18191 && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
18192 {
18193 let Expr::Column(c) = &args[0] else {
18194 unreachable!("guarded above");
18195 };
18196 *e = Expr::Column(crate::ast::ColumnName {
18197 qualifier: Some("EXCLUDED".to_string()),
18198 name: c.name.clone(),
18199 });
18200 }
18201 Expr::FunctionCall { args, .. } => {
18202 for a in args {
18203 Self::rewrite_mysql_values_refs(a);
18204 }
18205 }
18206 Expr::Binary { lhs, rhs, .. } => {
18207 Self::rewrite_mysql_values_refs(lhs);
18208 Self::rewrite_mysql_values_refs(rhs);
18209 }
18210 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
18211 Self::rewrite_mysql_values_refs(expr);
18212 }
18213 Expr::Case {
18214 operand,
18215 branches,
18216 else_branch,
18217 } => {
18218 if let Some(op) = operand {
18219 Self::rewrite_mysql_values_refs(op);
18220 }
18221 for (w, t) in branches {
18222 Self::rewrite_mysql_values_refs(w);
18223 Self::rewrite_mysql_values_refs(t);
18224 }
18225 if let Some(el) = else_branch {
18226 Self::rewrite_mysql_values_refs(el);
18227 }
18228 }
18229 _ => {}
18230 }
18231 }
18232
18233 /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
18234 /// clause sitting between the INSERT body and the trailing
18235 /// RETURNING. All keywords come in as bare idents; `ON` is
18236 /// a reserved Token though.
18237 fn parse_optional_on_conflict(
18238 &mut self,
18239 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18240 if !matches!(self.peek(), Token::On) {
18241 return Ok(None);
18242 }
18243 // Peek further: we want exactly "ON CONFLICT ...". If the
18244 // next ident isn't "conflict", let some other parser handle.
18245 let next_is_conflict = matches!(
18246 self.tokens.get(self.pos + 1),
18247 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18248 );
18249 if !next_is_conflict {
18250 return Ok(None);
18251 }
18252 self.advance(); // ON
18253 self.advance(); // CONFLICT
18254 // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18255 // the constraint instead of listing columns (the pg_dump
18256 // form); the engine resolves it.
18257 let mut constraint_name: Option<String> = None;
18258 if matches!(self.peek(), Token::On) {
18259 self.advance(); // ON
18260 match self.advance() {
18261 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18262 }
18263 other => {
18264 return Err(self.err(alloc::format!(
18265 "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18266 )));
18267 }
18268 }
18269 constraint_name = Some(self.expect_ident_like()?);
18270 }
18271 // Optional `(col [, col]*)` target list.
18272 let mut target_columns: Vec<String> = Vec::new();
18273 if matches!(self.peek(), Token::LParen) {
18274 self.advance();
18275 loop {
18276 target_columns.push(self.expect_ident_like()?);
18277 match self.peek() {
18278 Token::Comma => {
18279 self.advance();
18280 }
18281 Token::RParen => {
18282 self.advance();
18283 break;
18284 }
18285 other => {
18286 return Err(self.err(alloc::format!(
18287 "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18288 )));
18289 }
18290 }
18291 }
18292 }
18293 // v7.39 (round 240) — optional index predicate after the target
18294 // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18295 // PARTIAL unique index; SPG's arbiters are full indexes, which
18296 // satisfy any predicate, so it is parsed and carried but not
18297 // consulted (recorded residual: partial-unique-index arbiters).
18298 let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18299 self.advance();
18300 Some(self.parse_expr(0)?)
18301 } else {
18302 None
18303 };
18304 // Required `DO`.
18305 match self.advance() {
18306 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18307 other => {
18308 return Err(self.err(alloc::format!(
18309 "expected DO after ON CONFLICT [(…)], got {other:?}"
18310 )));
18311 }
18312 }
18313 // Action: NOTHING | UPDATE SET …
18314 let action = match self.advance() {
18315 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18316 crate::ast::OnConflictAction::Nothing
18317 }
18318 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18319 self.parse_on_conflict_update_action()?
18320 }
18321 other => {
18322 return Err(self.err(alloc::format!(
18323 "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18324 )));
18325 }
18326 };
18327 Ok(Some(crate::ast::OnConflictClause {
18328 target_columns,
18329 index_where,
18330 constraint_name,
18331 mysql_lowered: false,
18332 action,
18333 }))
18334 }
18335
18336 /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18337 /// `SET col = expr [, …] [WHERE cond]`. Caller already
18338 /// consumed `UPDATE`.
18339 fn parse_on_conflict_update_action(
18340 &mut self,
18341 ) -> Result<crate::ast::OnConflictAction, ParseError> {
18342 // `SET`
18343 match self.advance() {
18344 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18345 other => {
18346 return Err(self.err(alloc::format!(
18347 "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18348 )));
18349 }
18350 }
18351 let mut assignments: Vec<(String, Expr)> = Vec::new();
18352 loop {
18353 let col = self.expect_ident_like()?;
18354 if !matches!(self.peek(), Token::Eq) {
18355 return Err(self.err(alloc::format!(
18356 "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18357 self.peek()
18358 )));
18359 }
18360 self.advance();
18361 let value = self.parse_expr(0)?;
18362 assignments.push((col, value));
18363 if matches!(self.peek(), Token::Comma) {
18364 self.advance();
18365 continue;
18366 }
18367 break;
18368 }
18369 let where_ = if matches!(self.peek(), Token::Where) {
18370 self.advance();
18371 Some(self.parse_expr(0)?)
18372 } else {
18373 None
18374 };
18375 Ok(crate::ast::OnConflictAction::Update {
18376 assignments,
18377 where_,
18378 })
18379 }
18380
18381 fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18382 let mut items = Vec::new();
18383 // v7.39 (round 341, V66) — PG's target list may be EMPTY
18384 // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18385 // answers one zero-column row per row of t, and a bare `SELECT`
18386 // answers a single zero-column row. SPG required at least one
18387 // item, so both were syntax errors. Recognised by the token that
18388 // follows — nothing that can start an expression appears here.
18389 if self.select_list_is_empty_here() {
18390 return Ok(items);
18391 }
18392 loop {
18393 items.push(self.parse_select_item()?);
18394 if matches!(self.peek(), Token::Comma) {
18395 self.advance();
18396 } else {
18397 break;
18398 }
18399 }
18400 Ok(items)
18401 }
18402
18403 /// Is the target list empty at this point — i.e. does the next token
18404 /// end the SELECT's item list rather than start an item?
18405 fn select_list_is_empty_here(&self) -> bool {
18406 match self.peek() {
18407 Token::From
18408 | Token::Where
18409 | Token::Group
18410 | Token::Having
18411 | Token::Order
18412 | Token::Limit
18413 | Token::Offset
18414 | Token::Semicolon
18415 | Token::RParen
18416 | Token::Union
18417 | Token::Except
18418 | Token::Eof => true,
18419 // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18420 // with unreserved keywords, so they arrive as plain idents.
18421 Token::Ident(s) => {
18422 s.eq_ignore_ascii_case("fetch")
18423 || s.eq_ignore_ascii_case("window")
18424 || s.eq_ignore_ascii_case("intersect")
18425 }
18426 _ => false,
18427 }
18428 }
18429
18430 fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18431 if matches!(self.peek(), Token::Star) {
18432 self.advance();
18433 return Ok(SelectItem::Wildcard);
18434 }
18435 // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18436 // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18437 // choke on the `*` ("expected identifier, got Star"). The lookahead is
18438 // `<ident> . *` with nothing binding tighter.
18439 if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18440 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18441 && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18442 {
18443 self.advance(); // qualifier
18444 self.advance(); // .
18445 self.advance(); // *
18446 return Ok(SelectItem::QualifiedWildcard(q));
18447 }
18448 }
18449 let start_tok = self.pos;
18450 let expr = self.parse_expr(0)?;
18451 let end_tok = self.consumed_pos();
18452 // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18453 // multi-column function returns into columns. Marked here and lowered in
18454 // `parse_bare_select`, where the FROM clause is in hand.
18455 if matches!(self.peek(), Token::Dot)
18456 && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18457 {
18458 self.advance(); // .
18459 self.advance(); // *
18460 return Ok(SelectItem::Expr {
18461 expr: Expr::FunctionCall {
18462 name: "__record_expand".to_string(),
18463 args: alloc::vec![expr],
18464 },
18465 alias: None,
18466 });
18467 }
18468 let alias = match self.parse_optional_alias()? {
18469 Some(a) => Some(a),
18470 None => self.mysql_item_label(&expr, start_tok, end_tok),
18471 };
18472 Ok(SelectItem::Expr { expr, alias })
18473 }
18474
18475 /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18476 /// carries no `AS`, filled in here so every downstream path reports it
18477 /// without knowing the rule. `None` leaves the item un-aliased, which is
18478 /// what a PG session always gets.
18479 ///
18480 /// Measured against MariaDB 11, three rules and no more:
18481 ///
18482 /// | item | label | why |
18483 /// |------------------|------------|------------------------------|
18484 /// | `lbl.a` | `a` | a column reports its name |
18485 /// | `'it''s'` | `it's` | a string reports its VALUE |
18486 /// | `a + b` | `a + b` | anything else, source text |
18487 ///
18488 /// The third is why this lives in the parser at all: the label is the
18489 /// text the client WROTE, down to the spacing, so it cannot be printed
18490 /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18491 ///
18492 /// Comments survive, and that is right: through a `mariadb` CLI both
18493 /// servers answer `a + b` for `SELECT a /* c */ + b`, but that is the
18494 /// CLIENT stripping the comment before it sends. Asked over the raw
18495 /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18496 /// produces.
18497 fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18498 if !self.mysql_dialect {
18499 return None;
18500 }
18501 match expr {
18502 // A column already reports its own name downstream; naming it
18503 // again here would only re-state the qualifier the label drops.
18504 Expr::Column(_) => None,
18505 Expr::Literal(Literal::String(v)) => Some(v.clone()),
18506 _ => self.source_span(start_tok, end_tok).map(str::to_string),
18507 }
18508 }
18509
18510 /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18511 /// consumed VALUES keyword. Each row lowers to a constant SELECT
18512 /// with PG's default column1..columnN names; subsequent rows
18513 /// chain as UNION ALL peers. Shared by the FROM-position
18514 /// `( VALUES … )` arm and the top-level bare VALUES statement.
18515 fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
18516 let mut row_selects: Vec<SelectStatement> = Vec::new();
18517 loop {
18518 if !matches!(self.peek(), Token::LParen) {
18519 return Err(self.err(alloc::format!(
18520 "expected '(' to start a VALUES row, got {:?}",
18521 self.peek()
18522 )));
18523 }
18524 self.advance(); // (
18525 let mut items: Vec<SelectItem> = Vec::new();
18526 loop {
18527 let expr = self.parse_expr(0)?;
18528 items.push(SelectItem::Expr {
18529 expr,
18530 alias: Some(alloc::format!("column{}", items.len() + 1)),
18531 });
18532 match self.peek() {
18533 Token::Comma => {
18534 self.advance();
18535 }
18536 Token::RParen => break,
18537 other => {
18538 return Err(self.err(alloc::format!(
18539 "expected ',' or ')' in VALUES row, got {other:?}"
18540 )));
18541 }
18542 }
18543 }
18544 self.advance(); // )
18545 row_selects.push(SelectStatement {
18546 locking: None,
18547 ctes: Vec::new(),
18548 distinct: false,
18549 distinct_on: Vec::new(),
18550 items,
18551 from: None,
18552 where_: None,
18553 group_by: None,
18554 group_by_all: false,
18555 having: None,
18556 unions: Vec::new(),
18557 order_by: Vec::new(),
18558 limit: None,
18559 offset: None,
18560 limit_with_ties: false,
18561 window_check_exprs: Vec::new(),
18562 });
18563 if matches!(self.peek(), Token::Comma) {
18564 self.advance();
18565 continue;
18566 }
18567 break;
18568 }
18569 let mut head = row_selects.remove(0);
18570 head.unions = row_selects
18571 .into_iter()
18572 .map(|s| (UnionKind::All, s))
18573 .collect();
18574 Ok(head)
18575 }
18576
18577 fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
18578 // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
18579 // children. It was read as a table NAMED `only`, so the query
18580 // failed on `relation "only" does not exist`.
18581 //
18582 // v7.39 (round 644) — and it is no longer a no-op. Round 621
18583 // absorbed the keyword, reasoning that SPG's children are
18584 // separate relations a plain scan does not descend into, so ONLY
18585 // already described the scan. That stopped being true when a
18586 // partition parent started unioning its children: measured,
18587 // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
18588 // where PG answers 0. The flag is carried now.
18589 let mut only = false;
18590 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
18591 && matches!(
18592 self.tokens.get(self.pos + 1),
18593 Some(Token::Ident(_) | Token::QuotedIdent(_))
18594 )
18595 {
18596 only = true;
18597 self.advance();
18598 }
18599 // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
18600 // for these SRFs the keyword is noise at parse time: the
18601 // join executor already substitutes outer-column references
18602 // into unnest_expr / generate_series_args per outer row
18603 // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
18604 // licences the correlation even without the keyword. Absorb
18605 // it and fall through to the SRF arms below.
18606 // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
18607 // just the four builtin SRFs: a user set-returning function on a JOIN's
18608 // right side is the whole point of LATERAL. The keyword stays noise at
18609 // parse time — the join executor substitutes the outer row into the
18610 // call's arguments per outer row.
18611 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18612 && matches!(
18613 self.tokens.get(self.pos + 1),
18614 // The json_each family has its OWN `LATERAL …` arm below, which
18615 // needs to see the keyword — absorbing it here would send those
18616 // calls down the generic table-function channel instead.
18617 Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
18618 )
18619 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18620 {
18621 self.advance(); // LATERAL
18622 }
18623 // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
18624 // set-returning function whose argument may reference a
18625 // preceding FROM item. We rewrite this to
18626 // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
18627 // AS __srf__) AS <alias>` so the existing LATERAL subquery
18628 // executor handles per-outer-row evaluation and the
18629 // SRF-primary jsonb_each_text path handles the inner
18630 // materialisation. Sentori 0067 backfill is the dogfood
18631 // shape.
18632 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18633 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
18634 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18635 {
18636 self.advance(); // LATERAL
18637 let each_fn = match self.peek() {
18638 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18639 _ => unreachable!(),
18640 };
18641 self.advance(); // jsonb_each[_text] / json_each[_text]
18642 self.advance(); // (
18643 let arg = self.parse_expr(0)?;
18644 if !matches!(self.peek(), Token::RParen) {
18645 return Err(self.err(alloc::format!(
18646 "expected ')' after LATERAL {each_fn}() argument, got {:?}",
18647 self.peek()
18648 )));
18649 }
18650 self.advance();
18651 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18652 let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18653 // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
18654 // FROM jsonb_each_text(<arg>) AS __srf__
18655 // PG's `AS kv(key, value)` column-alias list maps
18656 // positions to names; default to (key, value) when
18657 // omitted (matching the SRF's natural column names).
18658 let srf_alias = "__srf__".to_string();
18659 let key_alias = column_aliases
18660 .first()
18661 .cloned()
18662 .unwrap_or_else(|| "key".to_string());
18663 let value_alias = column_aliases
18664 .get(1)
18665 .cloned()
18666 .unwrap_or_else(|| "value".to_string());
18667 let inner_select = crate::ast::SelectStatement {
18668 locking: None,
18669 ctes: Vec::new(),
18670 distinct: false,
18671 distinct_on: Vec::new(),
18672 items: alloc::vec![
18673 crate::ast::SelectItem::Expr {
18674 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18675 qualifier: Some(srf_alias.clone()),
18676 name: "key".to_string(),
18677 }),
18678 alias: Some(key_alias),
18679 },
18680 crate::ast::SelectItem::Expr {
18681 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18682 qualifier: Some(srf_alias.clone()),
18683 name: "value".to_string(),
18684 }),
18685 alias: Some(value_alias),
18686 },
18687 ],
18688 from: Some(crate::ast::FromClause {
18689 primary: TableRef {
18690 name: srf_alias.clone(),
18691 alias: Some(srf_alias.clone()),
18692 only: false,
18693 as_of_segment: None,
18694 unnest_expr: None,
18695 unnest_column_aliases: Vec::new(),
18696 with_ordinality: false,
18697 generate_series_args: None,
18698 lateral_subquery: None,
18699 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18700 table_fn_call: None,
18701 rows_from: None,
18702 json_table: None,
18703 scalar_fn_item: false,
18704 },
18705 joins: Vec::new(),
18706 }),
18707 where_: None,
18708 group_by: None,
18709 group_by_all: false,
18710 having: None,
18711 unions: Vec::new(),
18712 order_by: Vec::new(),
18713 limit: None,
18714 offset: None,
18715 limit_with_ties: false,
18716 window_check_exprs: Vec::new(),
18717 };
18718 return Ok(TableRef {
18719 name: alias.clone(),
18720 alias: Some(alias),
18721 only: false,
18722 as_of_segment: None,
18723 unnest_expr: None,
18724 unnest_column_aliases: Vec::new(),
18725 with_ordinality: false,
18726 generate_series_args: None,
18727 lateral_subquery: Some(Box::new(inner_select)),
18728 jsonb_each_text_arg: None,
18729 table_fn_call: None,
18730 rows_from: None,
18731 json_table: None,
18732 scalar_fn_item: false,
18733 });
18734 }
18735 // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
18736 // without an explicit `LATERAL` keyword is the same shape
18737 // PG accepts (SRF naturally licences lateral correlation).
18738 // We mirror the LATERAL rewrite when the argument syntactic-
18739 // ally references an outer column (Column { qualifier:
18740 // Some(_), … }). For simplicity we apply the rewrite
18741 // whenever the SRF directly follows JOIN/CROSS JOIN/comma
18742 // in the FROM-list — caller-side join parsing positions
18743 // this peek correctly.
18744 // (Implementation note: detection lives below; the LATERAL
18745 // branch above already covers the explicit form; the bare
18746 // form falls through to the plain SRF arm and the engine
18747 // treats it as a constant-arg SRF if no outer reference is
18748 // present.)
18749 // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
18750 // table. Detect at the head so it claims precedence over
18751 // every other table-ref shape (unnest / generate_series /
18752 // bare ident); the lateral subquery itself follows the
18753 // regular SELECT grammar.
18754 // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
18755 // t(cols)`. Each row lowers to a constant SELECT with PG's
18756 // default column1..columnN names; subsequent rows chain as
18757 // UNION ALL peers. The result rides the derived-table
18758 // lateral_subquery channel — zero executor work.
18759 if matches!(self.peek(), Token::LParen)
18760 && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
18761 {
18762 self.advance(); // (
18763 self.advance(); // VALUES
18764 let head = self.parse_values_rows_body()?;
18765 if !matches!(self.peek(), Token::RParen) {
18766 return Err(self.err(alloc::format!(
18767 "expected ')' after VALUES list, got {:?}",
18768 self.peek()
18769 )));
18770 }
18771 self.advance();
18772 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18773 let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
18774 return Ok(TableRef {
18775 name,
18776 alias: alias_ident,
18777 only: false,
18778 as_of_segment: None,
18779 unnest_expr: None,
18780 unnest_column_aliases: column_aliases,
18781 with_ordinality: false,
18782 generate_series_args: None,
18783 lateral_subquery: Some(Box::new(head)),
18784 jsonb_each_text_arg: None,
18785 table_fn_call: None,
18786 rows_from: None,
18787 json_table: None,
18788 scalar_fn_item: false,
18789 });
18790 }
18791 // v7.37.17 (17.6 siblings) — plain derived table:
18792 // `FROM ( SELECT … ) [AS] alias`. Rides the same
18793 // lateral_subquery channel the explicit LATERAL form uses —
18794 // an uncorrelated inner SELECT executes identically. The
18795 // inner parse carries UNION tails (they live on
18796 // SelectStatement.unions).
18797 // v7.37 D.20 — the derived-table inner may itself be a
18798 // parenthesized set-operation group (`FROM ((SELECT…) UNION
18799 // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
18800 // bare `(SELECT …)`. parse_one_statement already routes a leading
18801 // `(` set-op group (its LParen arm) and a leading WITH
18802 // (parse_with_cte_then_select), so widen the second-token gate to
18803 // Select | LParen | WITH.
18804 // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
18805 // PG's spelling of `SELECT * FROM t` and is accepted wherever a
18806 // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
18807 // has existed since the shorthand landed and `parse_bare_select`
18808 // already routes it ("valid anywhere a SELECT head is"); what was
18809 // missing is this second-token gate, and the CTE body's dispatch
18810 // below. Round 868 found both by putting the shorthand in a
18811 // subquery — the top-level forms had been the only ones tested.
18812 if matches!(self.peek(), Token::LParen)
18813 && (matches!(
18814 self.tokens.get(self.pos + 1),
18815 Some(Token::Select | Token::LParen | Token::Table)
18816 ) || matches!(self.tokens.get(self.pos + 1),
18817 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
18818 {
18819 self.advance(); // (
18820 let inner = match self.parse_one_statement()? {
18821 Statement::Select(s) => s,
18822 other => {
18823 return Err(self.err(alloc::format!(
18824 "expected SELECT inside derived table ( … ), got {other:?}"
18825 )));
18826 }
18827 };
18828 if !matches!(self.peek(), Token::RParen) {
18829 return Err(self.err(alloc::format!(
18830 "expected ')' after derived-table subquery, got {:?}",
18831 self.peek()
18832 )));
18833 }
18834 self.advance();
18835 // `AS t(a, b)` column-alias list rides the
18836 // unnest_column_aliases field (same positional-rename
18837 // contract the unnest SRFs use).
18838 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18839 let name = alias_ident
18840 .clone()
18841 .unwrap_or_else(|| "subquery".to_string());
18842 return Ok(TableRef {
18843 name,
18844 alias: alias_ident,
18845 only: false,
18846 as_of_segment: None,
18847 unnest_expr: None,
18848 unnest_column_aliases: column_aliases,
18849 with_ordinality: false,
18850 generate_series_args: None,
18851 lateral_subquery: Some(Box::new(inner)),
18852 jsonb_each_text_arg: None,
18853 table_fn_call: None,
18854 rows_from: None,
18855 json_table: None,
18856 scalar_fn_item: false,
18857 });
18858 }
18859 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18860 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18861 {
18862 self.advance(); // LATERAL
18863 self.advance(); // (
18864 // Parse the inner SELECT.
18865 let inner = match self.parse_one_statement()? {
18866 Statement::Select(s) => s,
18867 other => {
18868 return Err(self.err(alloc::format!(
18869 "expected SELECT inside LATERAL ( … ), got {other:?}"
18870 )));
18871 }
18872 };
18873 if !matches!(self.peek(), Token::RParen) {
18874 return Err(self.err(alloc::format!(
18875 "expected ')' after LATERAL subquery, got {:?}",
18876 self.peek()
18877 )));
18878 }
18879 self.advance();
18880 // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
18881 // `(VALUES …) t(g)` derived table round-trips through view-body
18882 // Display, which renders on the lateral_subquery channel).
18883 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18884 let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
18885 return Ok(TableRef {
18886 name,
18887 alias: alias_ident,
18888 only: false,
18889 as_of_segment: None,
18890 unnest_expr: None,
18891 unnest_column_aliases: column_aliases,
18892 with_ordinality: false,
18893 generate_series_args: None,
18894 lateral_subquery: Some(Box::new(inner)),
18895 jsonb_each_text_arg: None,
18896 table_fn_call: None,
18897 rows_from: None,
18898 json_table: None,
18899 scalar_fn_item: false,
18900 });
18901 }
18902 // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
18903 // function as a FROM item. Emits one row per (key, value)
18904 // pair in the JSONB object argument as TEXT columns. May
18905 // be wrapped in CROSS JOIN LATERAL when the argument
18906 // references a preceding FROM item (sentori migration
18907 // 0067 backfill shape: `CROSS JOIN LATERAL
18908 // jsonb_each_text(t.json_col) AS kv(key, value)`).
18909 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
18910 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18911 {
18912 let each_fn = match self.peek() {
18913 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18914 _ => unreachable!(),
18915 };
18916 self.advance(); // jsonb_each[_text] / json_each[_text]
18917 self.advance(); // (
18918 let arg = self.parse_expr(0)?;
18919 if !matches!(self.peek(), Token::RParen) {
18920 return Err(self.err(alloc::format!(
18921 "expected ')' after {each_fn}() argument, got {:?}",
18922 self.peek()
18923 )));
18924 }
18925 self.advance();
18926 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18927 let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18928 return Ok(TableRef {
18929 name,
18930 alias: alias_ident,
18931 only: false,
18932 as_of_segment: None,
18933 unnest_expr: None,
18934 // `AS t(k, v)` renames key/value positionally, same as the
18935 // LATERAL-position form already does.
18936 unnest_column_aliases: column_aliases,
18937 with_ordinality: false,
18938 generate_series_args: None,
18939 lateral_subquery: None,
18940 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18941 table_fn_call: None,
18942 rows_from: None,
18943 json_table: None,
18944 scalar_fn_item: false,
18945 });
18946 }
18947 // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
18948 // (+ json_ variants) — record-returning JSON functions with a
18949 // column-definition list. Desugar to a derived table that
18950 // projects each declared column from the JSON via `->>` + a cast,
18951 // over `jsonb_array_elements(J)` for the *set (per-element) form.
18952 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
18953 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18954 {
18955 return self.parse_json_to_record_from();
18956 }
18957 // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
18958 // row is a text[] of capture groups, so it cannot desugar to unnest
18959 // (that would flatten the array). Wrap it as a derived table
18960 // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
18961 // SRF path already emits one text[] row per match. PG names the column
18962 // `regexp_matches`; an `AS a(col)` alias overrides it.
18963 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18964 if s.eq_ignore_ascii_case("regexp_matches"))
18965 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18966 {
18967 self.advance(); // fn name
18968 self.advance(); // (
18969 let mut fn_args: Vec<Expr> = Vec::new();
18970 loop {
18971 fn_args.push(self.parse_expr(0)?);
18972 if matches!(self.peek(), Token::Comma) {
18973 self.advance();
18974 continue;
18975 }
18976 break;
18977 }
18978 if !matches!(self.peek(), Token::RParen) {
18979 return Err(self.err(alloc::format!(
18980 "expected ')' after regexp_matches() arguments, got {:?}",
18981 self.peek()
18982 )));
18983 }
18984 self.advance();
18985 // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
18986 // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
18987 // it, so it died on the `with` token while every other table function
18988 // accepted it.
18989 let with_ordinality = self.absorb_with_ordinality();
18990 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18991 let table_alias = alias_ident
18992 .clone()
18993 .unwrap_or_else(|| "regexp_matches".to_string());
18994 // PG names a single-column function's output column after the ALIAS
18995 // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
18996 // `m` reads as that column and not as a whole-row composite. Naming
18997 // it after the function regardless made `SELECT m[1] FROM … AS m`
18998 // subscript a record.
18999 let col_name = column_aliases
19000 .first()
19001 .cloned()
19002 .or_else(|| alias_ident.clone())
19003 .unwrap_or_else(|| "regexp_matches".to_string());
19004 let inner = crate::ast::SelectStatement {
19005 locking: None,
19006 ctes: Vec::new(),
19007 distinct: false,
19008 distinct_on: Vec::new(),
19009 items: alloc::vec![SelectItem::Expr {
19010 expr: Expr::FunctionCall {
19011 name: "regexp_matches".to_string(),
19012 args: fn_args,
19013 },
19014 alias: Some(col_name),
19015 }],
19016 from: None,
19017 where_: None,
19018 group_by: None,
19019 group_by_all: false,
19020 having: None,
19021 unions: Vec::new(),
19022 order_by: Vec::new(),
19023 limit: None,
19024 offset: None,
19025 limit_with_ties: false,
19026 window_check_exprs: Vec::new(),
19027 };
19028 return Ok(TableRef {
19029 name: table_alias.clone(),
19030 alias: Some(table_alias),
19031 only: false,
19032 as_of_segment: None,
19033 unnest_expr: None,
19034 unnest_column_aliases: column_aliases,
19035 with_ordinality,
19036 generate_series_args: None,
19037 lateral_subquery: Some(Box::new(inner)),
19038 jsonb_each_text_arg: None,
19039 table_fn_call: None,
19040 rows_from: None,
19041 json_table: None,
19042 // regexp_matches returns text[], a base type: `SELECT m FROM
19043 // regexp_matches(…) AS m` is the array, not a composite wrapping it.
19044 scalar_fn_item: true,
19045 });
19046 }
19047 // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
19048 // / json_ variants as a FROM item. Rewritten into
19049 // `unnest(<same fn>(<expr>))`: the scalar form returns the
19050 // elements as a TEXT array, and the existing unnest SRF path
19051 // materialises one row per element. PG's natural column name
19052 // is `value`; an `AS a(col)` column-alias list overrides it.
19053 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19054 if s.eq_ignore_ascii_case("jsonb_array_elements")
19055 || s.eq_ignore_ascii_case("json_array_elements")
19056 || s.eq_ignore_ascii_case("jsonb_array_elements_text")
19057 || s.eq_ignore_ascii_case("json_array_elements_text")
19058 || s.eq_ignore_ascii_case("jsonb_object_keys")
19059 || s.eq_ignore_ascii_case("json_object_keys")
19060 || s.eq_ignore_ascii_case("jsonb_path_query")
19061 || s.eq_ignore_ascii_case("json_path_query")
19062 || s.eq_ignore_ascii_case("generate_subscripts")
19063 || s.eq_ignore_ascii_case("string_to_table")
19064 || s.eq_ignore_ascii_case("regexp_split_to_table"))
19065 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19066 {
19067 let fn_name = match self.peek() {
19068 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19069 _ => unreachable!(),
19070 };
19071 self.advance(); // fn name
19072 self.advance(); // (
19073 let mut fn_args: Vec<Expr> = Vec::new();
19074 loop {
19075 fn_args.push(self.parse_expr(0)?);
19076 if matches!(self.peek(), Token::Comma) {
19077 self.advance();
19078 continue;
19079 }
19080 break;
19081 }
19082 if !matches!(self.peek(), Token::RParen) {
19083 return Err(self.err(alloc::format!(
19084 "expected ')' after {fn_name}() arguments, got {:?}",
19085 self.peek()
19086 )));
19087 }
19088 self.advance();
19089 let with_ordinality = self.absorb_with_ordinality();
19090 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19091 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
19092 // PG's natural column name: the array-elements SRFs
19093 // declare an OUT parameter `value`; jsonb_object_keys
19094 // and generate_subscripts have none, so the column is
19095 // named after the function. A bare table alias on a
19096 // single-column SRF renames the column too (PG: `FROM
19097 // generate_subscripts(a, 1) AS s` projects column s) —
19098 // except for the OUT-parameter SRFs, whose column stays
19099 // `value` under a bare alias.
19100 let natural_col = if fn_name.ends_with("_array_elements")
19101 || fn_name.ends_with("_array_elements_text")
19102 {
19103 "value".to_string()
19104 } else {
19105 alias_ident.clone().unwrap_or_else(|| fn_name.clone())
19106 };
19107 let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
19108 // Keep any further entries — the second names the
19109 // ordinality column under WITH ORDINALITY.
19110 srf_cols.extend(column_aliases.into_iter().skip(1));
19111 // The *_to_table SRFs are row-streams over the existing
19112 // *_to_array scalars — map the call target; the display
19113 // name (alias / column defaults) keeps the SRF spelling.
19114 let call_name = match fn_name.as_str() {
19115 "string_to_table" => "string_to_array".to_string(),
19116 "regexp_split_to_table" => "regexp_split_to_array".to_string(),
19117 _ => fn_name,
19118 };
19119 // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
19120 // preceding FROM item (bare or qualified column) is correlated;
19121 // route it through the per-outer-row lateral channel.
19122 let expr = crate::ast::Expr::FunctionCall {
19123 name: call_name,
19124 args: fn_args,
19125 };
19126 let correlated = Self::expr_has_any_column(&expr);
19127 let tref = TableRef {
19128 name,
19129 alias: alias_ident,
19130 only: false,
19131 as_of_segment: None,
19132 unnest_expr: Some(Box::new(expr)),
19133 unnest_column_aliases: srf_cols,
19134 with_ordinality,
19135 generate_series_args: None,
19136 lateral_subquery: None,
19137 jsonb_each_text_arg: None,
19138 table_fn_call: None,
19139 rows_from: None,
19140 json_table: None,
19141 // Each of these returns a BASE type (jsonb / text / int), so the item's
19142 // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
19143 // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
19144 scalar_fn_item: !with_ordinality,
19145 };
19146 return Ok(if correlated {
19147 Self::wrap_correlated_srf(tref)
19148 } else {
19149 tref
19150 });
19151 }
19152 // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
19153 // explicit parallel-zip syntax. Each entry lowers to its
19154 // array-returning scalar form (unnest(x) → x itself; the
19155 // FROM-SRF rewrite family → their scalar array calls) and
19156 // the list rides the multi-arg unnest zip channel:
19157 // NULL-padded to the longest, WITH ORDINALITY appends the
19158 // counter. generate_series has no scalar array form and
19159 // errors honestly.
19160 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
19161 && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
19162 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19163 {
19164 self.advance(); // ROWS
19165 self.advance(); // FROM
19166 self.advance(); // (
19167 let mut entries: Vec<Expr> = Vec::new();
19168 // v7.39 (read01 round 74) — the generic channel, filled in parallel.
19169 // Used only when some entry has no array form.
19170 let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
19171 loop {
19172 let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
19173 if !matches!(self.peek(), Token::LParen) {
19174 return Err(self.err(alloc::format!(
19175 "expected '(' after {fn_name} in ROWS FROM, got {:?}",
19176 self.peek()
19177 )));
19178 }
19179 self.advance();
19180 let mut fn_args: Vec<Expr> = Vec::new();
19181 if !matches!(self.peek(), Token::RParen) {
19182 loop {
19183 fn_args.push(self.parse_expr(0)?);
19184 if matches!(self.peek(), Token::Comma) {
19185 self.advance();
19186 continue;
19187 }
19188 break;
19189 }
19190 }
19191 if !matches!(self.peek(), Token::RParen) {
19192 return Err(self.err(alloc::format!(
19193 "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
19194 self.peek()
19195 )));
19196 }
19197 self.advance();
19198 let entry = match fn_name.as_str() {
19199 "unnest" => {
19200 if fn_args.len() != 1 {
19201 return Err(
19202 self.err("unnest inside ROWS FROM takes exactly one array".into())
19203 );
19204 }
19205 fn_args.pop().expect("len checked")
19206 }
19207 "jsonb_array_elements"
19208 | "json_array_elements"
19209 | "jsonb_array_elements_text"
19210 | "json_array_elements_text"
19211 | "jsonb_object_keys"
19212 | "json_object_keys"
19213 | "generate_subscripts" => crate::ast::Expr::FunctionCall {
19214 name: fn_name,
19215 args: fn_args,
19216 },
19217 "string_to_table" => crate::ast::Expr::FunctionCall {
19218 name: "string_to_array".to_string(),
19219 args: fn_args,
19220 },
19221 "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
19222 name: "regexp_split_to_array".to_string(),
19223 args: fn_args,
19224 },
19225 // v7.39 (read01 round 74) — an SRF with no array form
19226 // (`generate_series`, a user `RETURNS SETOF` function) has no
19227 // scalar expression to zip, so the WHOLE list switches to the
19228 // rows_from channel, which runs each function and zips the
19229 // rows themselves. The all-array case keeps the old lowering:
19230 // it is well-trodden and this must not disturb it.
19231 _ => {
19232 generic.push((fn_name, fn_args));
19233 if matches!(self.peek(), Token::Comma) {
19234 self.advance();
19235 continue;
19236 }
19237 break;
19238 }
19239 };
19240 generic.push((
19241 // The array-able entries carry their lowered expr along, so a
19242 // MIXED list still works: the engine sees the scalar array
19243 // form and unnests it.
19244 "__array".to_string(),
19245 alloc::vec![entry.clone()],
19246 ));
19247 entries.push(entry);
19248 if matches!(self.peek(), Token::Comma) {
19249 self.advance();
19250 continue;
19251 }
19252 break;
19253 }
19254 if !matches!(self.peek(), Token::RParen) {
19255 return Err(self.err(alloc::format!(
19256 "expected ')' to close ROWS FROM, got {:?}",
19257 self.peek()
19258 )));
19259 }
19260 self.advance();
19261 let with_ordinality = self.absorb_with_ordinality();
19262 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19263 let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19264 // v7.39 (read01 round 74) — some entry had no array form, so the whole
19265 // list rides the generic channel.
19266 if generic.iter().any(|(n, _)| n != "__array") {
19267 let correlated = generic
19268 .iter()
19269 .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19270 let tref = TableRef {
19271 name,
19272 alias: alias_ident,
19273 only: false,
19274 as_of_segment: None,
19275 unnest_expr: None,
19276 unnest_column_aliases,
19277 with_ordinality,
19278 generate_series_args: None,
19279 lateral_subquery: None,
19280 jsonb_each_text_arg: None,
19281 table_fn_call: None,
19282 rows_from: Some(generic),
19283 json_table: None,
19284 scalar_fn_item: false,
19285 };
19286 return Ok(if correlated {
19287 Self::wrap_correlated_srf(tref)
19288 } else {
19289 tref
19290 });
19291 }
19292 let correlated = entries.iter().any(Self::expr_has_any_column);
19293 let expr = if entries.len() == 1 {
19294 entries.pop().expect("len checked")
19295 } else {
19296 crate::ast::Expr::FunctionCall {
19297 name: "__unnest_zip".to_string(),
19298 args: entries,
19299 }
19300 };
19301 let tref = TableRef {
19302 name,
19303 alias: alias_ident,
19304 only: false,
19305 as_of_segment: None,
19306 unnest_expr: Some(Box::new(expr)),
19307 unnest_column_aliases,
19308 with_ordinality,
19309 generate_series_args: None,
19310 lateral_subquery: None,
19311 jsonb_each_text_arg: None,
19312 table_fn_call: None,
19313 rows_from: None,
19314 json_table: None,
19315 scalar_fn_item: false,
19316 };
19317 return Ok(if correlated {
19318 Self::wrap_correlated_srf(tref)
19319 } else {
19320 tref
19321 });
19322 }
19323 // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19324 // source. Detect at the head before the bare-ident fallback;
19325 // unnest is not a reserved token.
19326 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19327 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19328 {
19329 self.advance(); // unnest
19330 self.advance(); // (
19331 let mut srf_args = alloc::vec![self.parse_expr(0)?];
19332 while matches!(self.peek(), Token::Comma) {
19333 self.advance();
19334 srf_args.push(self.parse_expr(0)?);
19335 }
19336 if !matches!(self.peek(), Token::RParen) {
19337 return Err(self.err(alloc::format!(
19338 "expected ')' after unnest() argument, got {:?}",
19339 self.peek()
19340 )));
19341 }
19342 self.advance();
19343 // Multi-arg unnest(a, b, …) zips the arrays in
19344 // parallel, NULL-padding to the longest (PG's ROWS
19345 // FROM shorthand). Lower onto the unnest channel as an
19346 // internal marker call the executors unpack.
19347 let expr = if srf_args.len() == 1 {
19348 srf_args.pop().expect("len checked")
19349 } else {
19350 crate::ast::Expr::FunctionCall {
19351 name: "__unnest_zip".to_string(),
19352 args: srf_args,
19353 }
19354 };
19355 let with_ordinality = self.absorb_with_ordinality();
19356 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19357 let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19358 let correlated = Self::expr_has_any_column(&expr);
19359 let tref = TableRef {
19360 name,
19361 alias: alias_ident,
19362 only: false,
19363 as_of_segment: None,
19364 unnest_expr: Some(Box::new(expr)),
19365 unnest_column_aliases,
19366 with_ordinality,
19367 generate_series_args: None,
19368 lateral_subquery: None,
19369 jsonb_each_text_arg: None,
19370 table_fn_call: None,
19371 rows_from: None,
19372 json_table: None,
19373 scalar_fn_item: false,
19374 };
19375 return Ok(if correlated {
19376 Self::wrap_correlated_srf(tref)
19377 } else {
19378 tref
19379 });
19380 }
19381 // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19382 // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19383 // generic table-fn arg parser can't read), so it is intercepted
19384 // here BEFORE the generic dispatch. The doc expr may reference
19385 // outer columns (implicit LATERAL) — same correlated-wrap rule.
19386 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19387 if s.eq_ignore_ascii_case("json_table"))
19388 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19389 {
19390 let tref = self.parse_json_table_ref()?;
19391 let correlated = tref
19392 .json_table
19393 .as_deref()
19394 .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19395 return Ok(if correlated {
19396 Self::wrap_correlated_srf(tref)
19397 } else {
19398 tref
19399 });
19400 }
19401 // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19402 // functions dispatched by name (`pg_partition_tree('t')`,
19403 // `pg_partition_ancestors('t')`). Same head-detection shape as
19404 // unnest; the engine executor owns the row shape per function.
19405 // v7.39 (read01 round 65) — and a USER function in FROM position
19406 // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19407 // (generate_series / unnest / the json_each family) keep it — their arms
19408 // sit further down, so they are excluded here by name rather than by
19409 // ordering. Anything else that is an ident followed by `(` is a table
19410 // function; the engine executor decides whether it is a builtin, a
19411 // set-returning user function, or an error.
19412 // 7.38.1 S5.1 — pg_dump spells its table functions
19413 // schema-qualified (`pg_catalog.pg_options_to_table(...)`);
19414 // strip the pg_catalog prefix here so the same head-detection
19415 // fires. Only pg_catalog: a user schema's `s.f(x)` keeps its
19416 // meaning.
19417 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
19418 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
19419 && matches!(
19420 self.tokens.get(self.pos + 2),
19421 Some(Token::Ident(_) | Token::QuotedIdent(_))
19422 )
19423 && matches!(self.tokens.get(self.pos + 3), Some(Token::LParen))
19424 {
19425 self.advance(); // pg_catalog
19426 self.advance(); // .
19427 }
19428 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19429 if !s.eq_ignore_ascii_case("generate_series")
19430 && !s.eq_ignore_ascii_case("unnest")
19431 && !is_json_each_name(s))
19432 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19433 {
19434 // Body out-of-line — this parse sits on the FROM/subquery
19435 // recursion chain (debug frame-cliff discipline).
19436 // v7.39 (read01 round 69) — a call whose arguments reference an outer
19437 // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19438 // outer row, so it rides the lateral channel. Same rule the unnest
19439 // arm uses.
19440 let tref = self.parse_table_fn_ref()?;
19441 let correlated = tref
19442 .table_fn_call
19443 .as_deref()
19444 .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19445 return Ok(if correlated {
19446 Self::wrap_correlated_srf(tref)
19447 } else {
19448 tref
19449 });
19450 }
19451 // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19452 // [, step])` set-returning source. Same shape as unnest:
19453 // detect at the head, parse the comma-separated arg list,
19454 // dispatch downstream through the engine's set-returning
19455 // path. Supports integer triplets (mailrs's `WITH row_no AS
19456 // (SELECT * FROM generate_series(1, N))` pattern) and
19457 // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19458 // date-range iteration pattern, which pre-3.10 had no
19459 // direct equivalent in SPG).
19460 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19461 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19462 {
19463 self.advance(); // generate_series
19464 self.advance(); // (
19465 let mut args: Vec<Expr> = Vec::new();
19466 loop {
19467 args.push(self.parse_expr(0)?);
19468 if matches!(self.peek(), Token::Comma) {
19469 self.advance();
19470 continue;
19471 }
19472 break;
19473 }
19474 if !matches!(self.peek(), Token::RParen) {
19475 return Err(self.err(alloc::format!(
19476 "expected ')' after generate_series() arguments, got {:?}",
19477 self.peek()
19478 )));
19479 }
19480 self.advance();
19481 if args.len() < 2 || args.len() > 3 {
19482 return Err(self.err(alloc::format!(
19483 "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19484 args.len()
19485 )));
19486 }
19487 let with_ordinality = self.absorb_with_ordinality();
19488 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19489 let name = alias_ident
19490 .clone()
19491 .unwrap_or_else(|| "generate_series".to_string());
19492 let correlated = args.iter().any(Self::expr_has_any_column);
19493 let tref = TableRef {
19494 name,
19495 alias: alias_ident,
19496 only: false,
19497 as_of_segment: None,
19498 unnest_expr: None,
19499 unnest_column_aliases: column_aliases,
19500 with_ordinality,
19501 generate_series_args: Some(args),
19502 lateral_subquery: None,
19503 jsonb_each_text_arg: None,
19504 table_fn_call: None,
19505 rows_from: None,
19506 json_table: None,
19507 scalar_fn_item: false,
19508 };
19509 return Ok(if correlated {
19510 Self::wrap_correlated_srf(tref)
19511 } else {
19512 tref
19513 });
19514 }
19515 // v7.16.2 — preserve information_schema / pg_catalog
19516 // qualifiers (mailrs round-10 A.3). The generic
19517 // `expect_ident_like` strip silently drops the schema;
19518 // we want the engine to recognise these PG meta tables
19519 // and synthesise rows from the live catalog. Produce a
19520 // synthetic name (`__spg_info_columns` etc.) so the
19521 // engine's SELECT-side router can dispatch without
19522 // clashing with any user-defined `columns` table.
19523 let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
19524 (synth, Some(orig))
19525 } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
19526 (synth, Some(orig))
19527 } else {
19528 (self.expect_ident_like()?, None)
19529 };
19530 // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
19531 // time-travel clause. Parse BEFORE the alias so the
19532 // alias can still ride at the tail (`tbl AS OF SEGMENT
19533 // '5' alias`). `AS` is a reserved keyword token, while
19534 // `OF` and `SEGMENT` are bare idents.
19535 let as_of_segment = if matches!(self.peek(), Token::As)
19536 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
19537 {
19538 self.advance(); // AS
19539 self.advance(); // OF
19540 let kw = match self.peek().clone() {
19541 Token::Ident(s) | Token::QuotedIdent(s) => s,
19542 other => {
19543 return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
19544 }
19545 };
19546 if !kw.eq_ignore_ascii_case("segment") {
19547 return Err(self.err(format!(
19548 "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
19549 )));
19550 }
19551 self.advance();
19552 // Segment id literal — accept either a string or
19553 // integer for operator ergonomics.
19554 let id = match self.advance() {
19555 Token::String(s) => s
19556 .parse::<u32>()
19557 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19558 Token::Integer(n) => u32::try_from(n)
19559 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19560 other => {
19561 return Err(self.err(format!(
19562 "expected segment id literal after AS OF SEGMENT, got {other:?}"
19563 )));
19564 }
19565 };
19566 Some(id)
19567 } else {
19568 None
19569 };
19570 // TABLESAMPLE is not a reserved token — keep the bare-ident
19571 // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
19572 let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
19573 {
19574 None
19575 } else {
19576 self.parse_optional_alias()?
19577 };
19578 // r1052 — a catalog name rewritten to its synthetic form keeps
19579 // the WRITTEN name as the relation's alias, so `pg_cast.oid`
19580 // still binds after `pg_cast` became `__spg_pg_cast`. PG
19581 // semantics: the visible name of `pg_catalog.pg_cast` IS
19582 // `pg_cast`. Without this, every table-name-qualified column
19583 // on a synthesised catalog answered "missing FROM-clause
19584 // entry" — which is the wall pg_dump hit on its first
19585 // pg_proc/pg_cast query.
19586 let alias = match (&alias, &meta_original) {
19587 (None, Some(orig)) if *orig != name => Some(orig.clone()),
19588 _ => alias,
19589 };
19590 // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
19591 // (PG grammar). BERNOULLI lowers to a per-row
19592 // `random() < p/100` conjunct on the enclosing SELECT's
19593 // WHERE — exact row-level Bernoulli semantics. SYSTEM
19594 // shares the lowering: SPG has no page structure to
19595 // sample, and the row-level form returns the same expected
19596 // fraction. REPEATABLE(seed) promises a deterministic
19597 // sample SPG cannot honour yet — honest error rather than
19598 // a silently ignored seed.
19599 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
19600 self.advance();
19601 let method = self.expect_ident_like()?;
19602 if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
19603 return Err(self.err(alloc::format!(
19604 "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
19605 )));
19606 }
19607 if !matches!(self.peek(), Token::LParen) {
19608 return Err(self.err(alloc::format!(
19609 "expected '(' after TABLESAMPLE {}, got {:?}",
19610 method.to_ascii_uppercase(),
19611 self.peek()
19612 )));
19613 }
19614 self.advance();
19615 let percent = self.parse_expr(0)?;
19616 if !matches!(self.peek(), Token::RParen) {
19617 return Err(self.err(alloc::format!(
19618 "expected ')' after TABLESAMPLE percentage, got {:?}",
19619 self.peek()
19620 )));
19621 }
19622 self.advance();
19623 // REPEATABLE(seed) → a deterministic per-row draw seeded by
19624 // `seed`, so the sample is stable across repeats and rescans.
19625 // Non-REPEATABLE keeps the non-deterministic `random()` draw.
19626 let mut sample_seed: Option<Expr> = None;
19627 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
19628 self.advance();
19629 if !matches!(self.peek(), Token::LParen) {
19630 return Err(self.err(alloc::format!(
19631 "expected '(' after REPEATABLE, got {:?}",
19632 self.peek()
19633 )));
19634 }
19635 self.advance();
19636 let seed = self.parse_expr(0)?;
19637 if !matches!(self.peek(), Token::RParen) {
19638 return Err(self.err(alloc::format!(
19639 "expected ')' after REPEATABLE seed, got {:?}",
19640 self.peek()
19641 )));
19642 }
19643 self.advance();
19644 sample_seed = Some(seed);
19645 }
19646 let draw = match sample_seed {
19647 Some(seed) => Expr::FunctionCall {
19648 name: "__tsm_fract".to_string(),
19649 args: alloc::vec![seed],
19650 },
19651 None => Expr::FunctionCall {
19652 name: "random".to_string(),
19653 args: Vec::new(),
19654 },
19655 };
19656 self.pending_sample_preds.push(Expr::Binary {
19657 lhs: Box::new(draw),
19658 op: crate::ast::BinOp::Lt,
19659 rhs: Box::new(Expr::Binary {
19660 lhs: Box::new(percent),
19661 op: crate::ast::BinOp::Div,
19662 rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
19663 }),
19664 });
19665 }
19666 Ok(TableRef {
19667 name,
19668 alias,
19669 only,
19670 as_of_segment,
19671 unnest_expr: None,
19672 unnest_column_aliases: Vec::new(),
19673 with_ordinality: false,
19674 generate_series_args: None,
19675 lateral_subquery: None,
19676 jsonb_each_text_arg: None,
19677 table_fn_call: None,
19678 rows_from: None,
19679 json_table: None,
19680 scalar_fn_item: false,
19681 })
19682 }
19683
19684 /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
19685 /// but also accepts `AS alias(col [, col, …])` — the
19686 /// PG-standard table-function column-list form. The column
19687 /// list is only honoured when paired with `UNNEST(...)` in
19688 /// the parent; other call sites currently discard it.
19689 /// True when the expression tree contains a qualified column
19690 /// reference (`t.col`) — the syntactic marker that an SRF
19691 /// argument correlates with a preceding FROM item.
19692 fn expr_has_qualified_column(e: &Expr) -> bool {
19693 match e {
19694 Expr::Column(c) => c.qualifier.is_some(),
19695 Expr::Binary { lhs, rhs, .. } => {
19696 Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
19697 }
19698 Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
19699 Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
19700 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
19701 Expr::Case {
19702 operand,
19703 branches,
19704 else_branch,
19705 } => {
19706 operand
19707 .as_deref()
19708 .is_some_and(Self::expr_has_qualified_column)
19709 || branches.iter().any(|(w, t)| {
19710 Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
19711 })
19712 || else_branch
19713 .as_deref()
19714 .is_some_and(Self::expr_has_qualified_column)
19715 }
19716 _ => false,
19717 }
19718 }
19719
19720 /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
19721 /// counts a bare (unqualified) column. A set-returning function has no
19722 /// input columns of its own, so ANY column in its arguments is an outer
19723 /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
19724 fn expr_has_any_column(e: &Expr) -> bool {
19725 match e {
19726 Expr::Column(_) => true,
19727 Expr::Binary { lhs, rhs, .. } => {
19728 Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
19729 }
19730 Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
19731 Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
19732 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
19733 // v7.39 (round 759, F31-B8b) — a column INSIDE an array
19734 // constructor or subscript fell to the `_ => false` arm, so
19735 // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
19736 // channel and the eager peer eval answered `column "x" does
19737 // not exist` (the substitution walker already recurses both
19738 // shapes; only this detector was blind to them).
19739 Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
19740 Expr::ArraySubscript { target, index } => {
19741 Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
19742 }
19743 Expr::Case {
19744 operand,
19745 branches,
19746 else_branch,
19747 } => {
19748 operand.as_deref().is_some_and(Self::expr_has_any_column)
19749 || branches
19750 .iter()
19751 .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
19752 || else_branch
19753 .as_deref()
19754 .is_some_and(Self::expr_has_any_column)
19755 }
19756 _ => false,
19757 }
19758 }
19759
19760 /// Wrap a correlated SRF table ref (`unnest(t.col)` /
19761 /// `generate_series(1, t.n)`) into the lateral_subquery
19762 /// channel: `SELECT * FROM <srf>` executes per outer row with
19763 /// outer references substituted (v7.37.43-T4.5 machinery).
19764 /// Uncorrelated SRFs stay on their plain channels.
19765 fn wrap_correlated_srf(srf: TableRef) -> TableRef {
19766 let name = srf.name.clone();
19767 let alias = srf.alias.clone();
19768 let inner = crate::ast::SelectStatement {
19769 locking: None,
19770 ctes: Vec::new(),
19771 distinct: false,
19772 distinct_on: Vec::new(),
19773 items: alloc::vec![crate::ast::SelectItem::Wildcard],
19774 from: Some(crate::ast::FromClause {
19775 primary: srf,
19776 joins: Vec::new(),
19777 }),
19778 where_: None,
19779 group_by: None,
19780 group_by_all: false,
19781 having: None,
19782 unions: Vec::new(),
19783 order_by: Vec::new(),
19784 limit: None,
19785 offset: None,
19786 limit_with_ties: false,
19787 window_check_exprs: Vec::new(),
19788 };
19789 TableRef {
19790 name,
19791 alias,
19792 only: false,
19793 as_of_segment: None,
19794 unnest_expr: None,
19795 unnest_column_aliases: Vec::new(),
19796 with_ordinality: false,
19797 generate_series_args: None,
19798 lateral_subquery: Some(Box::new(inner)),
19799 jsonb_each_text_arg: None,
19800 table_fn_call: None,
19801 rows_from: None,
19802 json_table: None,
19803 scalar_fn_item: false,
19804 }
19805 }
19806
19807 /// True when the expression tree contains an unresolved
19808 /// `OVER w` marker (see parse_over_clause).
19809 fn expr_has_named_window(e: &Expr) -> bool {
19810 match e {
19811 Expr::WindowFunction { partition_by, .. } => matches!(
19812 partition_by.as_slice(),
19813 [Expr::Column(c)] if matches!(
19814 c.qualifier.as_deref(),
19815 Some("__named_window__") | Some("__named_window_ref__")
19816 )
19817 ),
19818 Expr::Binary { lhs, rhs, .. } => {
19819 Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
19820 }
19821 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
19822 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
19823 Expr::Case {
19824 operand,
19825 branches,
19826 else_branch,
19827 } => {
19828 operand.as_deref().is_some_and(Self::expr_has_named_window)
19829 || branches.iter().any(|(w, t)| {
19830 Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
19831 })
19832 || else_branch
19833 .as_deref()
19834 .is_some_and(Self::expr_has_named_window)
19835 }
19836 _ => false,
19837 }
19838 }
19839
19840 /// v7.39 (round 705) — the NAMES the expression references through the
19841 /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
19842 /// definitions nothing referenced. Traversal mirrors
19843 /// `expr_has_named_window` above.
19844 fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
19845 match e {
19846 Expr::WindowFunction { partition_by, .. } => {
19847 if let [Expr::Column(c)] = partition_by.as_slice()
19848 && matches!(
19849 c.qualifier.as_deref(),
19850 Some("__named_window__") | Some("__named_window_ref__")
19851 )
19852 {
19853 into.push(c.name.clone());
19854 }
19855 }
19856 Expr::Binary { lhs, rhs, .. } => {
19857 Self::collect_named_window_refs(lhs, into);
19858 Self::collect_named_window_refs(rhs, into);
19859 }
19860 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19861 Self::collect_named_window_refs(expr, into);
19862 }
19863 Expr::FunctionCall { args, .. } => {
19864 for a in args {
19865 Self::collect_named_window_refs(a, into);
19866 }
19867 }
19868 Expr::Case {
19869 operand,
19870 branches,
19871 else_branch,
19872 } => {
19873 if let Some(o) = operand.as_deref() {
19874 Self::collect_named_window_refs(o, into);
19875 }
19876 for (w, t) in branches {
19877 Self::collect_named_window_refs(w, into);
19878 Self::collect_named_window_refs(t, into);
19879 }
19880 if let Some(eb) = else_branch.as_deref() {
19881 Self::collect_named_window_refs(eb, into);
19882 }
19883 }
19884 _ => {}
19885 }
19886 }
19887
19888 /// Inline named-window definitions into the `OVER w` markers.
19889 /// An unknown name errors (PG: window "w" does not exist).
19890 #[allow(clippy::type_complexity)]
19891 fn substitute_named_windows(
19892 e: &mut Expr,
19893 defs: &[(
19894 String,
19895 (
19896 Vec<Expr>,
19897 Vec<(Expr, bool, Option<bool>)>,
19898 Option<WindowFrame>,
19899 ),
19900 )],
19901 ) -> Result<(), String> {
19902 match e {
19903 Expr::WindowFunction {
19904 partition_by,
19905 order_by,
19906 frame,
19907 ..
19908 } => {
19909 // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
19910 // from the bare `OVER w1` (a plain reference).
19911 let named = match partition_by.as_slice() {
19912 [Expr::Column(c)] => match c.qualifier.as_deref() {
19913 Some("__named_window__") => Some((c.name.clone(), false)),
19914 Some("__named_window_ref__") => Some((c.name.clone(), true)),
19915 _ => None,
19916 },
19917 _ => None,
19918 };
19919 if let Some((wname, is_copy)) = named {
19920 let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
19921 else {
19922 return Err(alloc::format!("window {wname:?} does not exist"));
19923 };
19924 if !is_copy {
19925 *partition_by = def.0.clone();
19926 *order_by = def.1.clone();
19927 *frame = def.2.clone();
19928 return Ok(());
19929 }
19930 // v7.39 (round 229) — PG's copy rules, probed against
19931 // 18.4: a copy inherits the partitioning, may supply an
19932 // ordering only when the base has none, and may not copy
19933 // a base that already carries a frame (its own frame
19934 // would be ambiguous with the inherited one).
19935 if !def.1.is_empty() && !order_by.is_empty() {
19936 return Err(alloc::format!(
19937 "cannot override ORDER BY clause of window \"{wname}\""
19938 ));
19939 }
19940 if def.2.is_some() {
19941 return Err(alloc::format!(
19942 "cannot copy window \"{wname}\" because it has a frame clause"
19943 ));
19944 }
19945 *partition_by = def.0.clone();
19946 if order_by.is_empty() {
19947 *order_by = def.1.clone();
19948 }
19949 }
19950 Ok(())
19951 }
19952 Expr::Binary { lhs, rhs, .. } => {
19953 Self::substitute_named_windows(lhs, defs)?;
19954 Self::substitute_named_windows(rhs, defs)
19955 }
19956 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19957 Self::substitute_named_windows(expr, defs)
19958 }
19959 Expr::FunctionCall { args, .. } => {
19960 for a in args {
19961 Self::substitute_named_windows(a, defs)?;
19962 }
19963 Ok(())
19964 }
19965 Expr::Case {
19966 operand,
19967 branches,
19968 else_branch,
19969 } => {
19970 if let Some(op) = operand {
19971 Self::substitute_named_windows(op, defs)?;
19972 }
19973 for (w, t) in branches {
19974 Self::substitute_named_windows(w, defs)?;
19975 Self::substitute_named_windows(t, defs)?;
19976 }
19977 if let Some(el) = else_branch {
19978 Self::substitute_named_windows(el, defs)?;
19979 }
19980 Ok(())
19981 }
19982 _ => Ok(()),
19983 }
19984 }
19985
19986 /// SQL-standard `TABLE name` shorthand — builds the equivalent
19987 /// `SELECT * FROM name` head. Callers own set-op chain / tail
19988 /// composition.
19989 fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
19990 debug_assert!(matches!(self.peek(), Token::Table));
19991 self.advance(); // TABLE
19992 let tname = self.expect_ident_like()?;
19993 Ok(SelectStatement {
19994 locking: None,
19995 ctes: Vec::new(),
19996 distinct: false,
19997 distinct_on: Vec::new(),
19998 items: alloc::vec![SelectItem::Wildcard],
19999 from: Some(FromClause {
20000 primary: TableRef {
20001 name: tname,
20002 alias: None,
20003 only: false,
20004 as_of_segment: None,
20005 unnest_expr: None,
20006 unnest_column_aliases: Vec::new(),
20007 with_ordinality: false,
20008 generate_series_args: None,
20009 lateral_subquery: None,
20010 jsonb_each_text_arg: None,
20011 table_fn_call: None,
20012 rows_from: None,
20013 json_table: None,
20014 scalar_fn_item: false,
20015 },
20016 joins: Vec::new(),
20017 }),
20018 where_: None,
20019 group_by: None,
20020 group_by_all: false,
20021 having: None,
20022 unions: Vec::new(),
20023 order_by: Vec::new(),
20024 limit: None,
20025 offset: None,
20026 limit_with_ties: false,
20027 window_check_exprs: Vec::new(),
20028 })
20029 }
20030
20031 /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
20032 /// variants) → a derived table that reads each declared column out of
20033 /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
20034 /// `jsonb_array_elements(J)` (one row per element, column `value`);
20035 /// the scalar *record form projects a single row straight off `J`.
20036 /// Rides the existing lateral-subquery channel, so no new executor or
20037 /// AST is needed.
20038 fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
20039 use crate::ast::{
20040 BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
20041 };
20042 let fn_name = match self.peek() {
20043 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20044 _ => unreachable!("caller guarded is_json_to_record_name"),
20045 };
20046 self.advance(); // fn name
20047 self.advance(); // (
20048 let mut arg = self.parse_expr(0)?;
20049 // populate_record(base, json): the base only carries the record
20050 // type here — the JSON argument is the second expression.
20051 let mut base: Option<Expr> = None;
20052 if matches!(self.peek(), Token::Comma) {
20053 self.advance();
20054 base = Some(arg);
20055 arg = self.parse_expr(0)?;
20056 }
20057 if !matches!(self.peek(), Token::RParen) {
20058 return Err(self.err(alloc::format!(
20059 "expected ')' after {fn_name}() argument, got {:?}",
20060 self.peek()
20061 )));
20062 }
20063 self.advance(); // )
20064 let is_set = fn_name.ends_with("recordset");
20065 // `[AS] alias ( col type [, …] )` column-definition list.
20066 if matches!(self.peek(), Token::As) {
20067 self.advance();
20068 }
20069 let alias_opt = match self.peek() {
20070 Token::Ident(s) | Token::QuotedIdent(s) => {
20071 let a = s.clone();
20072 self.advance();
20073 Some(a)
20074 }
20075 _ => None,
20076 };
20077 // v7.39 (read01 round 76) — the populate family's canonical PG
20078 // spelling carries no column list at all: the row shape comes from
20079 // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
20080 // j)`). The parser has no catalog, so hand the two arguments to the
20081 // engine's table-function channel, which does. Only `*_to_record*`
20082 // (whose base is bare `record`) genuinely requires the list.
20083 if !matches!(self.peek(), Token::LParen) {
20084 if let Some(base_expr) = base {
20085 let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
20086 return Ok(TableRef {
20087 name: alias.clone(),
20088 alias: Some(alias),
20089 only: false,
20090 as_of_segment: None,
20091 unnest_expr: None,
20092 unnest_column_aliases: Vec::new(),
20093 with_ordinality: false,
20094 generate_series_args: None,
20095 lateral_subquery: None,
20096 jsonb_each_text_arg: None,
20097 table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
20098 rows_from: None,
20099 json_table: None,
20100 scalar_fn_item: false,
20101 });
20102 }
20103 return Err(self.err(alloc::format!(
20104 "expected '(' to start the {fn_name} column-definition list, got {:?}",
20105 self.peek()
20106 )));
20107 }
20108 let Some(alias) = alias_opt else {
20109 return Err(self.err(alloc::format!(
20110 "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
20111 )));
20112 };
20113 self.advance(); // (
20114 let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
20115 loop {
20116 let col = self.expect_ident_like()?;
20117 let ty = self.parse_cast_target()?;
20118 coldefs.push((col, ty));
20119 if matches!(self.peek(), Token::Comma) {
20120 self.advance();
20121 continue;
20122 }
20123 if matches!(self.peek(), Token::RParen) {
20124 self.advance();
20125 break;
20126 }
20127 return Err(self.err(alloc::format!(
20128 "expected ',' or ')' in {fn_name} column list, got {:?}",
20129 self.peek()
20130 )));
20131 }
20132 if coldefs.is_empty() {
20133 return Err(self.err(alloc::format!(
20134 "{fn_name} column-definition list must declare at least one column"
20135 )));
20136 }
20137 // Per column: (base ->> 'col')::type AS col. The base is the
20138 // per-element `value` column for the *set form, or the argument
20139 // itself for the scalar record form.
20140 let items: Vec<SelectItem> = coldefs
20141 .into_iter()
20142 .map(|(col, ty)| {
20143 let base = if is_set {
20144 Expr::Column(ColumnName {
20145 qualifier: None,
20146 name: "value".to_string(),
20147 })
20148 } else {
20149 arg.clone()
20150 };
20151 SelectItem::Expr {
20152 expr: Expr::Cast {
20153 expr: Box::new(Expr::Binary {
20154 lhs: Box::new(base),
20155 op: BinOp::JsonGetText,
20156 rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
20157 }),
20158 target: ty,
20159 },
20160 alias: Some(col),
20161 }
20162 })
20163 .collect();
20164 let from = if is_set {
20165 let elem_fn = if fn_name.starts_with("jsonb") {
20166 "jsonb_array_elements"
20167 } else {
20168 "json_array_elements"
20169 };
20170 Some(FromClause {
20171 primary: TableRef {
20172 name: "value".to_string(),
20173 alias: None,
20174 only: false,
20175 as_of_segment: None,
20176 unnest_expr: Some(Box::new(Expr::FunctionCall {
20177 name: elem_fn.to_string(),
20178 args: alloc::vec![arg],
20179 })),
20180 unnest_column_aliases: alloc::vec!["value".to_string()],
20181 with_ordinality: false,
20182 generate_series_args: None,
20183 lateral_subquery: None,
20184 jsonb_each_text_arg: None,
20185 table_fn_call: None,
20186 rows_from: None,
20187 json_table: None,
20188 scalar_fn_item: false,
20189 },
20190 joins: Vec::new(),
20191 })
20192 } else {
20193 None
20194 };
20195 let inner = SelectStatement {
20196 locking: None,
20197 ctes: Vec::new(),
20198 distinct: false,
20199 distinct_on: Vec::new(),
20200 items,
20201 from,
20202 where_: None,
20203 group_by: None,
20204 group_by_all: false,
20205 having: None,
20206 unions: Vec::new(),
20207 order_by: Vec::new(),
20208 limit: None,
20209 offset: None,
20210 limit_with_ties: false,
20211 window_check_exprs: Vec::new(),
20212 };
20213 Ok(TableRef {
20214 name: alias.clone(),
20215 alias: Some(alias),
20216 only: false,
20217 as_of_segment: None,
20218 unnest_expr: None,
20219 unnest_column_aliases: Vec::new(),
20220 with_ordinality: false,
20221 generate_series_args: None,
20222 lateral_subquery: Some(Box::new(inner)),
20223 jsonb_each_text_arg: None,
20224 table_fn_call: None,
20225 rows_from: None,
20226 json_table: None,
20227 scalar_fn_item: false,
20228 })
20229 }
20230
20231 /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
20232 /// Returns true when the clause was present. `WITH` alone (a
20233 /// CTE can never start here) is not enough — the ORDINALITY
20234 /// ident must follow, so a stray WITH still errors downstream.
20235 fn absorb_with_ordinality(&mut self) -> bool {
20236 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
20237 && matches!(self.tokens.get(self.pos + 1),
20238 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
20239 {
20240 self.advance();
20241 self.advance();
20242 true
20243 } else {
20244 false
20245 }
20246 }
20247
20248 /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
20249 /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
20250 /// Out-of-line: the caller sits on the FROM recursion chain.
20251 #[inline(never)]
20252 fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
20253 let fn_name = match self.advance() {
20254 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20255 _ => unreachable!("caller peeked an ident"),
20256 };
20257 self.advance(); // (
20258 let mut args: Vec<Expr> = Vec::new();
20259 if !matches!(self.peek(), Token::RParen) {
20260 loop {
20261 args.push(self.parse_expr(0)?);
20262 if matches!(self.peek(), Token::Comma) {
20263 self.advance();
20264 continue;
20265 }
20266 break;
20267 }
20268 }
20269 if !matches!(self.peek(), Token::RParen) {
20270 return Err(self.err(alloc::format!(
20271 "expected ')' after {fn_name}() arguments, got {:?}",
20272 self.peek()
20273 )));
20274 }
20275 self.advance();
20276 // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20277 // counter column rides after the function's own, and the alias list
20278 // names it.
20279 let with_ordinality = self.absorb_with_ordinality();
20280 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20281 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20282 Ok(TableRef {
20283 name,
20284 alias: alias_ident,
20285 only: false,
20286 as_of_segment: None,
20287 unnest_expr: None,
20288 unnest_column_aliases,
20289 with_ordinality,
20290 generate_series_args: None,
20291 lateral_subquery: None,
20292 jsonb_each_text_arg: None,
20293 table_fn_call: Some(Box::new((fn_name, args))),
20294 rows_from: None,
20295 json_table: None,
20296 scalar_fn_item: false,
20297 })
20298 }
20299
20300 /// v7.39 (round 205, JSON_TABLE) — parse
20301 /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20302 /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20303 /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20304 #[inline(never)]
20305 fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20306 self.advance(); // json_table
20307 self.advance(); // (
20308 let doc = Box::new(self.parse_expr(0)?);
20309 self.expect_comma_json_table()?;
20310 let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20311 // Optional `PASSING <expr> AS <name> [, …]`.
20312 let mut passing: Vec<(String, Expr)> = Vec::new();
20313 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20314 self.advance();
20315 loop {
20316 let e = self.parse_expr(0)?;
20317 if !matches!(self.peek(), Token::As) {
20318 return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20319 }
20320 self.advance();
20321 let vname = match self.advance() {
20322 Token::Ident(s) | Token::QuotedIdent(s) => s,
20323 other => {
20324 return Err(self.err(alloc::format!(
20325 "expected PASSING variable name, got {other:?}"
20326 )));
20327 }
20328 };
20329 passing.push((vname, e));
20330 if matches!(self.peek(), Token::Comma) {
20331 self.advance();
20332 continue;
20333 }
20334 break;
20335 }
20336 }
20337 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20338 return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20339 }
20340 self.advance();
20341 let columns = self.parse_json_table_columns()?;
20342 if !matches!(self.peek(), Token::RParen) {
20343 return Err(self.err(alloc::format!(
20344 "expected ')' to close JSON_TABLE, got {:?}",
20345 self.peek()
20346 )));
20347 }
20348 self.advance();
20349 let alias_ident = self.parse_optional_alias()?;
20350 let name = alias_ident
20351 .clone()
20352 .unwrap_or_else(|| String::from("json_table"));
20353 Ok(TableRef {
20354 name,
20355 alias: alias_ident,
20356 only: false,
20357 as_of_segment: None,
20358 unnest_expr: None,
20359 unnest_column_aliases: Vec::new(),
20360 with_ordinality: false,
20361 generate_series_args: None,
20362 lateral_subquery: None,
20363 jsonb_each_text_arg: None,
20364 table_fn_call: None,
20365 rows_from: None,
20366 json_table: Some(Box::new(crate::ast::JsonTable {
20367 doc,
20368 row_path,
20369 columns,
20370 passing,
20371 })),
20372 scalar_fn_item: false,
20373 })
20374 }
20375
20376 fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20377 if !matches!(self.peek(), Token::Comma) {
20378 return Err(self.err(alloc::format!(
20379 "expected ',' after JSON_TABLE document, got {:?}",
20380 self.peek()
20381 )));
20382 }
20383 self.advance();
20384 Ok(())
20385 }
20386
20387 fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20388 match self.advance() {
20389 Token::String(s) => Ok(s),
20390 other => Err(self.err(alloc::format!(
20391 "expected {what} string literal, got {other:?}"
20392 ))),
20393 }
20394 }
20395
20396 /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20397 #[inline(never)]
20398 fn parse_json_table_columns(
20399 &mut self,
20400 ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20401 if !matches!(self.peek(), Token::LParen) {
20402 return Err(self.err("expected '(' after COLUMNS".into()));
20403 }
20404 self.advance();
20405 let mut cols = Vec::new();
20406 loop {
20407 cols.push(self.parse_json_table_one_column()?);
20408 if matches!(self.peek(), Token::Comma) {
20409 self.advance();
20410 continue;
20411 }
20412 break;
20413 }
20414 if !matches!(self.peek(), Token::RParen) {
20415 return Err(self.err(alloc::format!(
20416 "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20417 self.peek()
20418 )));
20419 }
20420 self.advance();
20421 Ok(cols)
20422 }
20423
20424 fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20425 use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20426 // NESTED [PATH] '<p>' COLUMNS (...)
20427 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20428 self.advance();
20429 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20430 self.advance();
20431 }
20432 let path = self.parse_json_string_literal("NESTED PATH")?;
20433 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20434 return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20435 }
20436 self.advance();
20437 let columns = self.parse_json_table_columns()?;
20438 return Ok(JsonTableColumn::Nested { path, columns });
20439 }
20440 // <name> ...
20441 let name = match self.advance() {
20442 Token::Ident(s) | Token::QuotedIdent(s) => s,
20443 other => {
20444 return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20445 }
20446 };
20447 // <name> FOR ORDINALITY
20448 if matches!(self.peek(), Token::For) {
20449 self.advance();
20450 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20451 return Err(self.err("expected ORDINALITY after FOR".into()));
20452 }
20453 self.advance();
20454 return Ok(JsonTableColumn::Ordinality { name });
20455 }
20456 // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20457 let ty = self.parse_column_type_name()?;
20458 let mut format_json = false;
20459 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20460 self.advance();
20461 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20462 return Err(self.err("expected JSON after FORMAT".into()));
20463 }
20464 self.advance();
20465 format_json = true;
20466 }
20467 let mut exists = false;
20468 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20469 self.advance();
20470 exists = true;
20471 }
20472 let mut path = alloc::format!("$.{name}");
20473 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20474 self.advance();
20475 path = self.parse_json_string_literal("column PATH")?;
20476 }
20477 if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20478 // `FORMAT JSON` after PATH (alternate placement).
20479 self.advance();
20480 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20481 self.advance();
20482 }
20483 format_json = true;
20484 }
20485 let mut wrapper = false;
20486 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20487 self.advance();
20488 // optional CONDITIONAL/UNCONDITIONAL
20489 if matches!(self.peek(), Token::Ident(s)
20490 if s.eq_ignore_ascii_case("unconditional")
20491 || s.eq_ignore_ascii_case("conditional"))
20492 {
20493 self.advance();
20494 }
20495 if !matches!(self.peek(), Token::Ident(s)
20496 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20497 {
20498 return Err(self.err("expected WRAPPER after WITH".into()));
20499 }
20500 self.advance();
20501 // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20502 if matches!(self.peek(), Token::Ident(s)
20503 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20504 {
20505 self.advance();
20506 }
20507 wrapper = true;
20508 }
20509 // ON EMPTY / ON ERROR clauses (two, in any order).
20510 let mut on_empty = JsonTableOnBehavior::Null;
20511 let mut on_error = JsonTableOnBehavior::Null;
20512 for _ in 0..2 {
20513 let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20514 {
20515 self.advance();
20516 Some(JsonTableOnBehavior::Error)
20517 } else if matches!(self.peek(), Token::Null) {
20518 self.advance();
20519 Some(JsonTableOnBehavior::Null)
20520 } else if matches!(self.peek(), Token::Default) {
20521 self.advance();
20522 Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
20523 } else {
20524 None
20525 };
20526 let Some(behavior) = behavior else { break };
20527 // `ON {EMPTY|ERROR}`
20528 if !matches!(self.peek(), Token::On) {
20529 return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
20530 }
20531 self.advance();
20532 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
20533 self.advance();
20534 on_empty = behavior;
20535 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
20536 self.advance();
20537 on_error = behavior;
20538 } else {
20539 return Err(self.err("expected EMPTY or ERROR after ON".into()));
20540 }
20541 }
20542 Ok(JsonTableColumn::Regular {
20543 name,
20544 ty,
20545 path,
20546 exists,
20547 format_json,
20548 wrapper,
20549 on_empty,
20550 on_error,
20551 })
20552 }
20553
20554 fn parse_optional_alias_with_columns(
20555 &mut self,
20556 ) -> Result<(Option<String>, Vec<String>), ParseError> {
20557 let alias = self.parse_optional_alias()?;
20558 if alias.is_none() {
20559 return Ok((None, Vec::new()));
20560 }
20561 let mut cols: Vec<String> = Vec::new();
20562 if matches!(self.peek(), Token::LParen) {
20563 self.advance();
20564 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
20565 self.advance();
20566 cols.push(s);
20567 if matches!(self.peek(), Token::Comma) {
20568 self.advance();
20569 continue;
20570 }
20571 break;
20572 }
20573 if matches!(self.peek(), Token::RParen) {
20574 self.advance();
20575 }
20576 }
20577 Ok((alias, cols))
20578 }
20579
20580 /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
20581 /// whose keyword token was already consumed and whose `(` is the
20582 /// current token. Factored out of `parse_atom` (and marked
20583 /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
20584 /// recursive `parse_atom` frame — inlining them there enlarges the
20585 /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
20586 /// against, risking an overflow before the budget triggers.
20587 #[inline(never)]
20588 fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
20589 self.advance(); // (
20590 let mut args = Vec::new();
20591 if !matches!(self.peek(), Token::RParen) {
20592 loop {
20593 args.push(self.parse_expr(0)?);
20594 match self.peek() {
20595 Token::Comma => {
20596 self.advance();
20597 }
20598 Token::RParen => break,
20599 other => {
20600 return Err(self.err(alloc::format!(
20601 "expected ',' or ')' in {name}() args, got {other:?}"
20602 )));
20603 }
20604 }
20605 }
20606 }
20607 self.advance(); // )
20608 Ok(Expr::FunctionCall {
20609 name: name.into(),
20610 args,
20611 })
20612 }
20613
20614 /// FROM-clause: a primary table reference plus zero-or-more joined
20615 /// peers expressed via either `, <table>` (cross-product, no ON) or
20616 /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
20617 /// v1.10 keeps the join list flat (left-associative nested-loop
20618 /// semantics).
20619 fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
20620 let primary = self.parse_table_ref()?;
20621 let primary_qual = primary
20622 .alias
20623 .clone()
20624 .unwrap_or_else(|| primary.name.clone());
20625 let joins = self.parse_from_joins(&primary_qual)?;
20626 Ok(FromClause { primary, joins })
20627 }
20628
20629 /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
20630 /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
20631 /// SAME grammar after its target table has already been consumed.
20632 /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
20633 /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
20634 /// be parsed forward, once.)
20635 /// `left_primary_qual` is the qualifier (alias, else name) of whatever
20636 /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
20637 /// target in the MySQL multi-table form. It only feeds the `USING (…)`
20638 /// desugaring, which needs a name for the left side of each equality.
20639 fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
20640 let mut joins = Vec::new();
20641 loop {
20642 // `, <table>` — cross-product with no ON.
20643 if matches!(self.peek(), Token::Comma) {
20644 self.advance();
20645 let table = self.parse_table_ref()?;
20646 joins.push(FromJoin {
20647 kind: JoinKind::Cross,
20648 table,
20649 on: None,
20650 using_cols: None,
20651 natural: false,
20652 });
20653 continue;
20654 }
20655 // v7.37.16 — optional leading `NATURAL` before the join
20656 // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
20657 // not a lexer keyword (it arrives as a bare Ident), so match
20658 // it case-insensitively here. When present, no ON/USING
20659 // clause is allowed — the common columns are resolved at
20660 // execution time.
20661 let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
20662 if natural {
20663 self.advance();
20664 }
20665 // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
20666 // CROSS JOIN, and bare JOIN (defaults to INNER).
20667 let kind =
20668 match self.peek() {
20669 Token::Inner => {
20670 self.advance();
20671 if !matches!(self.peek(), Token::Join) {
20672 return Err(self
20673 .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
20674 }
20675 self.advance();
20676 JoinKind::Inner
20677 }
20678 Token::Left => {
20679 self.advance();
20680 if matches!(self.peek(), Token::Outer) {
20681 self.advance();
20682 }
20683 if !matches!(self.peek(), Token::Join) {
20684 return Err(self.err(format!(
20685 "expected JOIN after LEFT [OUTER], got {:?}",
20686 self.peek()
20687 )));
20688 }
20689 self.advance();
20690 JoinKind::Left
20691 }
20692 Token::Cross => {
20693 self.advance();
20694 if !matches!(self.peek(), Token::Join) {
20695 return Err(self
20696 .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
20697 }
20698 self.advance();
20699 JoinKind::Cross
20700 }
20701 // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
20702 Token::Right => {
20703 self.advance();
20704 if matches!(self.peek(), Token::Outer) {
20705 self.advance();
20706 }
20707 if !matches!(self.peek(), Token::Join) {
20708 return Err(self.err(format!(
20709 "expected JOIN after RIGHT [OUTER], got {:?}",
20710 self.peek()
20711 )));
20712 }
20713 self.advance();
20714 JoinKind::Right
20715 }
20716 // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
20717 Token::Full => {
20718 self.advance();
20719 if matches!(self.peek(), Token::Outer) {
20720 self.advance();
20721 }
20722 if !matches!(self.peek(), Token::Join) {
20723 return Err(self.err(format!(
20724 "expected JOIN after FULL [OUTER], got {:?}",
20725 self.peek()
20726 )));
20727 }
20728 self.advance();
20729 JoinKind::FullOuter
20730 }
20731 Token::Join => {
20732 self.advance();
20733 JoinKind::Inner
20734 }
20735 _ => break,
20736 };
20737 let table = self.parse_table_ref()?;
20738 // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
20739 // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
20740 // where prev_table is the most-recent left-side table
20741 // (the previous join's table if any, else the FROM primary).
20742 // PG semantics around column merging are richer (USING'd
20743 // cols become deduplicated single output columns); for
20744 // sugar purposes the predicate-only form covers the
20745 // baseline corpus shape and chained `… JOIN x USING (k)
20746 // JOIN y USING (k)` calls.
20747 // v7.37.16 — NATURAL joins carry no ON/USING clause; the
20748 // common columns resolve at execution time.
20749 if natural {
20750 joins.push(FromJoin {
20751 kind,
20752 table,
20753 on: None,
20754 using_cols: None,
20755 natural: true,
20756 });
20757 continue;
20758 }
20759 let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
20760 // v7.37.16 — capture the USING column list (in addition to
20761 // the ON desugar below) so the executor can perform PG's
20762 // column-merge on the output side.
20763 let mut using_cols: Option<Vec<String>> = None;
20764 let on = if matches!(self.peek(), Token::On) {
20765 self.advance();
20766 Some(self.parse_expr(0)?)
20767 } else if using_match {
20768 self.advance();
20769 if !matches!(self.peek(), Token::LParen) {
20770 return Err(
20771 self.err(format!("expected '(' after USING, got {:?}", self.peek()))
20772 );
20773 }
20774 self.advance();
20775 let mut cols: Vec<String> = Vec::new();
20776 loop {
20777 match self.peek().clone() {
20778 Token::Ident(s) | Token::QuotedIdent(s) => {
20779 self.advance();
20780 cols.push(s);
20781 }
20782 other => {
20783 return Err(self.err(format!(
20784 "expected column name inside USING (…), got {other:?}"
20785 )));
20786 }
20787 }
20788 match self.peek() {
20789 Token::Comma => {
20790 self.advance();
20791 continue;
20792 }
20793 Token::RParen => {
20794 self.advance();
20795 break;
20796 }
20797 other => {
20798 return Err(self.err(format!(
20799 "expected ',' or ')' inside USING (…), got {other:?}"
20800 )));
20801 }
20802 }
20803 }
20804 if cols.is_empty() {
20805 return Err(self.err("USING (…) requires at least one column".to_string()));
20806 }
20807 using_cols = Some(cols.clone());
20808 // Pick the left-side alias: prev join's table if any,
20809 // else FROM primary. Use alias when present, else
20810 // table name (PG-equivalent qualifier).
20811 let left_qual: String = joins
20812 .last()
20813 .map(|j| {
20814 j.table
20815 .alias
20816 .clone()
20817 .unwrap_or_else(|| j.table.name.clone())
20818 })
20819 .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
20820 let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
20821 let mut iter = cols.into_iter().map(|c| Expr::Binary {
20822 lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20823 qualifier: Some(left_qual.clone()),
20824 name: c.clone(),
20825 })),
20826 op: crate::ast::BinOp::Eq,
20827 rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20828 qualifier: Some(right_qual.clone()),
20829 name: c,
20830 })),
20831 });
20832 let first = iter.next().expect("at least one col");
20833 Some(iter.fold(first, |acc, pred| Expr::Binary {
20834 lhs: alloc::boxed::Box::new(acc),
20835 op: crate::ast::BinOp::And,
20836 rhs: alloc::boxed::Box::new(pred),
20837 }))
20838 } else if kind == JoinKind::Cross {
20839 None
20840 } else {
20841 return Err(self.err(format!(
20842 "expected ON or USING after {:?} JOIN, got {:?}",
20843 kind,
20844 self.peek()
20845 )));
20846 };
20847 joins.push(FromJoin {
20848 kind,
20849 table,
20850 on,
20851 using_cols,
20852 natural: false,
20853 });
20854 }
20855 Ok(joins)
20856 }
20857
20858 /// Optional alias after an expression or table:
20859 /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
20860 /// accepted (PG-style implicit alias). Returns `None` if the next token
20861 /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
20862 fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
20863 if matches!(self.peek(), Token::As) {
20864 self.advance();
20865 // v7.39 (round 340, V56) — after AS the next token MUST be an
20866 // identifier. This used to return None and "let the caller
20867 // surface the error on the next expectation", but when AS is
20868 // the LAST token there is no next expectation: `SELECT 1 AS`
20869 // parsed clean and silently dropped the alias. PG rejects it.
20870 if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
20871 return self.expect_ident_like().map(Some);
20872 }
20873 return Err(self.err(alloc::format!(
20874 "expected an alias after AS, got {:?}",
20875 self.peek()
20876 )));
20877 }
20878 // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
20879 // grammar reserves a long list of follow-keywords from the
20880 // alias slot. SPG's bareword approximation: skip a small
20881 // set of idents that would otherwise be swallowed as the
20882 // table alias and break trailing clauses like CREATE
20883 // MATERIALIZED VIEW … WITH [NO] DATA or future ON
20884 // CONFLICT WHERE shapes.
20885 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
20886 if is_alias_stopword(s) {
20887 return Ok(None);
20888 }
20889 return Ok(self.expect_ident_like().ok());
20890 }
20891 Ok(None)
20892 }
20893
20894 /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
20895 fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
20896 // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
20897 // error beats a stack overflow (an overflow aborts the
20898 // embedding host process).
20899 self.enter_nested()?;
20900 let r = self.parse_expr_inner(min_prec);
20901 self.nest_depth -= 1;
20902 r
20903 }
20904
20905 /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
20906 /// When the upcoming tokens form one, return the underlying
20907 /// operator token and the position just past the closing paren
20908 /// so the binary loop can dispatch on the plain operator.
20909 fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
20910 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
20911 return None;
20912 }
20913 if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
20914 return None;
20915 }
20916 let mut i = self.pos + 2;
20917 // Optional schema qualifier (pg_catalog.<op> etc.).
20918 if matches!(self.tokens.get(i), Some(Token::Ident(_)))
20919 && matches!(self.tokens.get(i + 1), Some(Token::Dot))
20920 {
20921 i += 2;
20922 }
20923 let op_tok = self.tokens.get(i)?.clone();
20924 if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
20925 return None;
20926 }
20927 Some((i + 2, op_tok))
20928 }
20929
20930 /// PG operator symbols that lower onto function calls in
20931 /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
20932 /// family → regexp_like, comparison rung), `^@` (starts_with,
20933 /// comparison rung), `^` (power, tighter than `*`), `#`
20934 /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
20935 /// subset of the OR bits so the subtraction never borrows).
20936 fn try_symbol_operator(
20937 &mut self,
20938 lhs: &Expr,
20939 min_prec: u8,
20940 ) -> Result<Option<Expr>, ParseError> {
20941 enum Sym {
20942 Regex { ci: bool, negated: bool },
20943 Like { ci: bool, negated: bool },
20944 StartsWith,
20945 Power,
20946 Xor,
20947 RangeAdjacent,
20948 }
20949 // v7.39 (IS-precedence knife) — the low-precedence postfix
20950 // predicates ride this existing leaf call (zero new frame slots
20951 // on the nesting chain).
20952 if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
20953 return Ok(Some(e));
20954 }
20955 let (sym, prec): (Sym, u8) = match self.peek() {
20956 Token::Tilde => (
20957 Sym::Regex {
20958 ci: false,
20959 negated: false,
20960 },
20961 5,
20962 ),
20963 Token::TildeStar => (
20964 Sym::Regex {
20965 ci: true,
20966 negated: false,
20967 },
20968 5,
20969 ),
20970 Token::NotTilde => (
20971 Sym::Regex {
20972 ci: false,
20973 negated: true,
20974 },
20975 5,
20976 ),
20977 Token::NotTildeStar => (
20978 Sym::Regex {
20979 ci: true,
20980 negated: true,
20981 },
20982 5,
20983 ),
20984 // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
20985 Token::DoubleTilde => (
20986 Sym::Like {
20987 ci: false,
20988 negated: false,
20989 },
20990 5,
20991 ),
20992 Token::DoubleTildeStar => (
20993 Sym::Like {
20994 ci: true,
20995 negated: false,
20996 },
20997 5,
20998 ),
20999 Token::NotDoubleTilde => (
21000 Sym::Like {
21001 ci: false,
21002 negated: true,
21003 },
21004 5,
21005 ),
21006 Token::NotDoubleTildeStar => (
21007 Sym::Like {
21008 ci: true,
21009 negated: true,
21010 },
21011 5,
21012 ),
21013 Token::CaretAt => (Sym::StartsWith, 5),
21014 // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
21015 // tighter than `* / & |`, which the prec-9 rung preserves —
21016 // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
21017 Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
21018 Token::Caret => (Sym::Power, 9),
21019 // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
21020 // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
21021 Token::Hash => (Sym::Xor, 6),
21022 Token::Adjacent => (Sym::RangeAdjacent, 5),
21023 _ => return Ok(None),
21024 };
21025 if prec < min_prec {
21026 return Ok(None);
21027 }
21028 self.advance();
21029 let rhs = self.parse_expr(prec + 1)?;
21030 let out = match sym {
21031 Sym::Regex { ci, negated } => {
21032 let mut args = alloc::vec![lhs.clone(), rhs];
21033 if ci {
21034 args.push(Expr::Literal(Literal::String(String::from("i"))));
21035 }
21036 maybe_not(
21037 Expr::FunctionCall {
21038 name: String::from("regexp_like"),
21039 args,
21040 },
21041 negated,
21042 )
21043 }
21044 Sym::Like { ci, negated } => Expr::Like {
21045 expr: alloc::boxed::Box::new(lhs.clone()),
21046 pattern: alloc::boxed::Box::new(rhs),
21047 negated,
21048 case_insensitive: ci,
21049 },
21050 Sym::StartsWith => Expr::FunctionCall {
21051 name: String::from("starts_with"),
21052 args: alloc::vec![lhs.clone(), rhs],
21053 },
21054 Sym::Power => Expr::FunctionCall {
21055 name: String::from("power"),
21056 args: alloc::vec![lhs.clone(), rhs],
21057 },
21058 // `#` bitwise XOR — a real operator now (was desugared to
21059 // `(a|b)-(a&b)`, algebraically identical for integers but
21060 // undefined for bit strings; the direct op handles both).
21061 Sym::Xor => Expr::Binary {
21062 lhs: Box::new(lhs.clone()),
21063 op: BinOp::BitXor,
21064 rhs: Box::new(rhs),
21065 },
21066 // range `-|-` "is adjacent to" — lowered to a catalog function.
21067 Sym::RangeAdjacent => Expr::FunctionCall {
21068 name: String::from("range_adjacent"),
21069 args: alloc::vec![lhs.clone(), rhs],
21070 },
21071 };
21072 Ok(Some(out))
21073 }
21074
21075 /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
21076 /// predicates, moved out of the tight postfix-cast loop: PG binds
21077 /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
21078 /// looser than EVERY binary operator (only NOT/AND/OR are looser),
21079 /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
21080 /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
21081 /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
21082 /// when nothing at this position belongs to the family. Out-of-line
21083 /// (`inline(never)`): the caller sits on the per-nesting-level frame
21084 /// chain that MAX_NEST_DEPTH is tuned against.
21085 #[inline(never)]
21086 fn parse_postfix_predicate(
21087 &mut self,
21088 lhs: &Expr,
21089 min_prec: u8,
21090 ) -> Result<Option<Expr>, ParseError> {
21091 // Reached through try_symbol_operator (an existing leaf call of
21092 // the binary loop) so NO new stack slots land on the per-nesting
21093 // frame chain; the lhs clones only when a predicate actually
21094 // consumes it.
21095 match self.peek() {
21096 // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
21097 // comparison family rung 5 (each +1 from the pre-XOR ladder).
21098 Token::Is if min_prec <= 4 => {}
21099 Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
21100 Token::Not
21101 if min_prec <= 5
21102 && matches!(
21103 self.tokens.get(self.pos + 1),
21104 Some(Token::Between | Token::In | Token::Like)
21105 ) => {}
21106 Token::Not | Token::Ident(_)
21107 if min_prec <= 5
21108 && (matches!(self.peek(), Token::Ident(s)
21109 if s.eq_ignore_ascii_case("ilike")
21110 || (self.mysql_dialect
21111 && (s.eq_ignore_ascii_case("regexp")
21112 || s.eq_ignore_ascii_case("rlike")))
21113 || (s.eq_ignore_ascii_case("similar")
21114 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
21115 || (matches!(self.peek(), Token::Not)
21116 && matches!(self.tokens.get(self.pos + 1),
21117 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21118 || (self.mysql_dialect
21119 && (s.eq_ignore_ascii_case("regexp")
21120 || s.eq_ignore_ascii_case("rlike")))
21121 || s.eq_ignore_ascii_case("similar")))) => {}
21122 _ => return Ok(None),
21123 }
21124 let mut expr = lhs.clone();
21125 // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
21126 // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
21127 if min_prec <= 4 {
21128 if matches!(self.peek(), Token::Is) {
21129 self.advance();
21130 let negated = if matches!(self.peek(), Token::Not) {
21131 self.advance();
21132 true
21133 } else {
21134 false
21135 };
21136 // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
21137 // mailrs pg_dump.
21138 if matches!(self.peek(), Token::Distinct) {
21139 self.advance();
21140 if !matches!(self.peek(), Token::From) {
21141 return Err(self.err(format!(
21142 "expected FROM after IS{} DISTINCT, got {:?}",
21143 if negated { " NOT" } else { "" },
21144 self.peek()
21145 )));
21146 }
21147 self.advance();
21148 // Right-hand side: parse at the same precedence
21149 // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
21150 // groups as `x IS DISTINCT FROM (a + b)`.
21151 let rhs = self.parse_expr(5)?;
21152 let op = if negated {
21153 BinOp::IsNotDistinctFrom
21154 } else {
21155 BinOp::IsDistinctFrom
21156 };
21157 expr = Expr::Binary {
21158 op,
21159 lhs: Box::new(expr),
21160 rhs: Box::new(rhs),
21161 };
21162 {
21163 return Ok(Some(expr));
21164 }
21165 }
21166 // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
21167 // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
21168 // Lowers onto pg_is_json(x, kind); NOT wraps the
21169 // call in a logical negation.
21170 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21171 if s.eq_ignore_ascii_case("json"))
21172 {
21173 self.advance(); // JSON
21174 let kind = match self.peek() {
21175 Token::Ident(s) | Token::QuotedIdent(s)
21176 if matches!(
21177 s.to_ascii_lowercase().as_str(),
21178 "value" | "object" | "array" | "scalar"
21179 ) =>
21180 {
21181 let k = s.to_ascii_lowercase();
21182 self.advance();
21183 k
21184 }
21185 _ => "value".to_string(),
21186 };
21187 let call = Expr::FunctionCall {
21188 name: "pg_is_json".to_string(),
21189 args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
21190 };
21191 expr = if negated {
21192 Expr::Unary {
21193 op: UnOp::Not,
21194 expr: Box::new(call),
21195 }
21196 } else {
21197 call
21198 };
21199 {
21200 return Ok(Some(expr));
21201 }
21202 }
21203 // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
21204 // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
21205 // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
21206 {
21207 let form_kw = match self.peek() {
21208 Token::Ident(s) | Token::QuotedIdent(s)
21209 if matches!(
21210 s.to_ascii_uppercase().as_str(),
21211 "NFC" | "NFD" | "NFKC" | "NFKD"
21212 ) && matches!(
21213 self.tokens.get(self.pos + 1),
21214 Some(Token::Ident(n) | Token::QuotedIdent(n))
21215 if n.eq_ignore_ascii_case("normalized")
21216 ) =>
21217 {
21218 Some(s.to_ascii_uppercase())
21219 }
21220 _ => None,
21221 };
21222 let bare_normalized = form_kw.is_none()
21223 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21224 if s.eq_ignore_ascii_case("normalized"));
21225 if form_kw.is_some() || bare_normalized {
21226 if form_kw.is_some() {
21227 self.advance(); // form keyword
21228 }
21229 self.advance(); // NORMALIZED
21230 let mut args = alloc::vec![expr];
21231 if let Some(f) = form_kw {
21232 args.push(Expr::Literal(Literal::String(f)));
21233 }
21234 let call = Expr::FunctionCall {
21235 name: "is_normalized".to_string(),
21236 args,
21237 };
21238 expr = if negated {
21239 Expr::Unary {
21240 op: UnOp::Not,
21241 expr: Box::new(call),
21242 }
21243 } else {
21244 call
21245 };
21246 {
21247 return Ok(Some(expr));
21248 }
21249 }
21250 }
21251 // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
21252 // three-valued boolean tests. IS TRUE/FALSE never
21253 // return NULL, so they lower to CASE forms whose
21254 // ELSE catches the NULL branch; IS UNKNOWN on a
21255 // boolean is exactly IS NULL.
21256 if matches!(self.peek(), Token::True | Token::False)
21257 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
21258 {
21259 let tok = self.advance();
21260 let test = match tok {
21261 Token::True => Some(true),
21262 Token::False => Some(false),
21263 _ => None, // UNKNOWN
21264 };
21265 // v7.39 (round 328, V45) — kept as what the user
21266 // wrote. These used to be lowered here into `CASE` /
21267 // `IS NULL`; the semantics were right but the AST no
21268 // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
21269 // was echoed back as
21270 // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
21271 expr = Expr::BoolTest {
21272 expr: Box::new(expr),
21273 value: test,
21274 negated,
21275 };
21276 {
21277 return Ok(Some(expr));
21278 }
21279 }
21280 if !matches!(self.peek(), Token::Null) {
21281 return Err(self.err(format!(
21282 "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21283 if negated { " NOT" } else { "" },
21284 self.peek()
21285 )));
21286 }
21287 self.advance();
21288 expr = Expr::IsNull {
21289 expr: Box::new(expr),
21290 negated,
21291 };
21292 {
21293 return Ok(Some(expr));
21294 }
21295 }
21296 }
21297 // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21298 if min_prec <= 5 {
21299 // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21300 // Look one token ahead so a stray `NOT` not followed by any of
21301 // these flows through to the early return below untouched.
21302 let negated = if matches!(self.peek(), Token::Not) {
21303 let next = self.tokens.get(self.pos + 1);
21304 matches!(next, Some(Token::Between | Token::In | Token::Like))
21305 || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21306 || (self.mysql_dialect
21307 && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21308 || s.eq_ignore_ascii_case("similar"))
21309 } else {
21310 false
21311 };
21312 if negated {
21313 self.advance();
21314 }
21315 if matches!(self.peek(), Token::Between) {
21316 expr = self.parse_between_tail(expr, negated)?;
21317 {
21318 return Ok(Some(expr));
21319 }
21320 }
21321 if matches!(self.peek(), Token::In) {
21322 if self.suppress_in_tail && !negated {
21323 // POSITION(sub IN str) — IN belongs to the
21324 // enclosing function syntax; stop here.
21325 {
21326 return Ok(None);
21327 }
21328 }
21329 expr = self.parse_in_tail(expr, negated)?;
21330 {
21331 return Ok(Some(expr));
21332 }
21333 }
21334 // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21335 // lowers onto the internal __similar_to(expr, pat[, esc]) call
21336 // (the SQL→regex transform runs inside, in the backtracking-
21337 // friendly shape SPG's matcher needs).
21338 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21339 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21340 {
21341 self.advance(); // SIMILAR
21342 self.advance(); // TO
21343 let pattern = self.parse_expr(6)?;
21344 let mut args = alloc::vec![expr, pattern];
21345 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21346 self.advance();
21347 args.push(self.parse_expr(6)?);
21348 }
21349 let call = Expr::FunctionCall {
21350 name: "__similar_to".to_string(),
21351 args,
21352 };
21353 expr = maybe_not(call, negated);
21354 {
21355 return Ok(Some(expr));
21356 }
21357 }
21358 if matches!(self.peek(), Token::Like) {
21359 self.advance();
21360 // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21361 if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21362 expr = q;
21363 {
21364 return Ok(Some(expr));
21365 }
21366 }
21367 // Pattern at the same precedence as other comparison RHSes —
21368 // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21369 let mut pattern = self.parse_expr(6)?;
21370 // `ESCAPE 'c'` — rewrite a literal pattern to the
21371 // default backslash escape at parse time. Custom
21372 // escapes on non-literal patterns would need
21373 // matcher support; error honestly.
21374 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21375 self.advance();
21376 let esc = self.parse_expr(6)?;
21377 pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21378 }
21379 expr = Expr::Like {
21380 expr: Box::new(expr),
21381 pattern: Box::new(pattern),
21382 negated,
21383 case_insensitive: false,
21384 };
21385 {
21386 return Ok(Some(expr));
21387 }
21388 }
21389 // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21390 // keyword reaches us as a plain identifier.
21391 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21392 self.advance();
21393 if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21394 expr = q;
21395 {
21396 return Ok(Some(expr));
21397 }
21398 }
21399 let pattern = self.parse_expr(6)?;
21400 expr = Expr::Like {
21401 expr: Box::new(expr),
21402 pattern: Box::new(pattern),
21403 negated,
21404 case_insensitive: true,
21405 };
21406 {
21407 return Ok(Some(expr));
21408 }
21409 }
21410 // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21411 // operator (RLIKE is the alias). It is a keyword, not `~`, and
21412 // matches case-insensitively under the default collation, so it
21413 // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21414 // `~*` operator uses, wrapped in NOT when negated.
21415 if self.mysql_dialect
21416 && matches!(self.peek(), Token::Ident(s)
21417 if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21418 {
21419 self.advance();
21420 let pattern = self.parse_expr(6)?;
21421 let call = Expr::FunctionCall {
21422 name: String::from("regexp_like"),
21423 args: alloc::vec![
21424 expr,
21425 pattern,
21426 Expr::Literal(Literal::String(String::from("i"))),
21427 ],
21428 };
21429 return Ok(Some(maybe_not(call, negated)));
21430 }
21431 }
21432 let _ = expr;
21433 Ok(None)
21434 }
21435
21436 fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21437 let mut lhs = self.parse_unary()?;
21438 let mut chain_len = 0usize;
21439 loop {
21440 // OPERATOR([schema.]op) reduces to its underlying
21441 // operator token before the normal dispatch.
21442 let explicit = self.peek_explicit_operator();
21443 let dispatch = match &explicit {
21444 Some((_, tok)) => self.binop_here(tok),
21445 None => self.binop_here(self.peek()),
21446 };
21447 let Some((op, prec)) = dispatch else {
21448 // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21449 // of the symbol family. `binop_here` answers None for them
21450 // because they lower onto function calls rather than a
21451 // BinOp, and the fallback below reads `self.peek()` — the
21452 // word OPERATOR, not the operator. `pg_dump` writes every
21453 // catalog predicate this way, so its first query failed
21454 // and no dump ran:
21455 //
21456 // AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21457 //
21458 // Collapsing the wrapper to the operator it names puts the
21459 // token where the fallback already looks.
21460 if let Some((next, op_tok)) = explicit {
21461 self.tokens.splice(self.pos..next, [op_tok]);
21462 }
21463 if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21464 lhs = e;
21465 chain_len += 1;
21466 if chain_len > MAX_BINARY_CHAIN {
21467 return Err(self.err(alloc::format!(
21468 "more than {MAX_BINARY_CHAIN} chained binary operators"
21469 )));
21470 }
21471 continue;
21472 }
21473 break;
21474 };
21475 if prec < min_prec {
21476 break;
21477 }
21478 // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21479 // iteratively but evaluates and drops recursively;
21480 // depth beyond the budget overflows worker stacks.
21481 chain_len += 1;
21482 if chain_len > MAX_BINARY_CHAIN {
21483 return Err(self.err(alloc::format!(
21484 "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21485 )));
21486 }
21487 match explicit {
21488 Some((end_pos, _)) => self.pos = end_pos,
21489 None => {
21490 self.advance();
21491 }
21492 }
21493 // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21494 // ANY is a bare ident; ALL is a reserved Token. Both
21495 // require an immediate `(` to disambiguate from
21496 // identifier columns named `any` / `all`.
21497 let any_kind = match self.peek() {
21498 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21499 Some(false)
21500 }
21501 Token::Ident(s) | Token::QuotedIdent(s)
21502 if (s.eq_ignore_ascii_case("any")
21503 || s.eq_ignore_ascii_case("some")
21504 || s.eq_ignore_ascii_case("all"))
21505 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21506 {
21507 Some(!s.eq_ignore_ascii_case("all"))
21508 }
21509 _ => None,
21510 };
21511 if let Some(is_any) = any_kind {
21512 lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21513 continue;
21514 }
21515 let rhs = self.parse_expr(prec + 1)?;
21516 lhs = Expr::Binary {
21517 lhs: Box::new(lhs),
21518 op,
21519 rhs: Box::new(rhs),
21520 };
21521 }
21522 Ok(lhs)
21523 }
21524
21525 /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
21526 /// and the array form.
21527 ///
21528 /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
21529 /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
21530 /// this block's `Expr` temporaries and four `format!` sites slots in
21531 /// that frame on every level of `((((1))))`, which never reaches it.
21532 #[inline(never)]
21533 fn parse_any_all_rhs(
21534 &mut self,
21535 lhs: Expr,
21536 op: BinOp,
21537 is_any: bool,
21538 ) -> Result<Expr, ParseError> {
21539 self.advance(); // ident
21540 self.advance(); // (
21541 // `x op ANY (SELECT …)` — the quantified-subquery
21542 // form. `= ANY` is exactly IN; the other operators
21543 // lower onto EXISTS over the subquery as a derived
21544 // table, comparing against its single projection
21545 // aliased __v (x's columns resolve correlated).
21546 // ALL is the negated-EXISTS complement; a NULL
21547 // element makes PG return NULL where this lowering
21548 // returns true — the NOT NULL column case (the
21549 // practical one) is exact.
21550 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
21551 // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
21552 // legal PG too (round-151 sibling). Out-of-line
21553 // (#[inline(never)] helper) — this sits on
21554 // parse_expr's recursive frame and the two-armed
21555 // SELECT temporary blew the nesting-budget stack.
21556 let mut sub = self.parse_any_all_select_body()?;
21557 if !matches!(self.peek(), Token::RParen) {
21558 return Err(self.err(alloc::format!(
21559 "expected ')' after ANY/ALL subquery, got {:?}",
21560 self.peek()
21561 )));
21562 }
21563 self.advance();
21564 if sub.items.len() != 1 {
21565 return Err(self.err(alloc::format!(
21566 "ANY/ALL subquery must return one column, got {}",
21567 sub.items.len()
21568 )));
21569 }
21570 if is_any && matches!(op, BinOp::Eq) {
21571 return Ok(Expr::InSubquery {
21572 expr: Box::new(lhs),
21573 subquery: Box::new(sub),
21574 negated: false,
21575 });
21576 }
21577 // The engine's subquery resolvers materialise
21578 // the single-column result into an ARRAY the
21579 // existing AnyAll three-valued eval consumes.
21580 return Ok(Expr::AnyAll {
21581 expr: Box::new(lhs),
21582 op,
21583 array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
21584 is_any,
21585 });
21586 }
21587 let arr = self.parse_expr(0)?;
21588 if !matches!(self.peek(), Token::RParen) {
21589 return Err(self.err(alloc::format!(
21590 "expected ')' after ANY/ALL argument, got {:?}",
21591 self.peek()
21592 )));
21593 }
21594 self.advance();
21595 Ok(Expr::AnyAll {
21596 expr: Box::new(lhs),
21597 op,
21598 array: Box::new(arr),
21599 is_any,
21600 })
21601 }
21602
21603 /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
21604 /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
21605 #[inline(never)]
21606 fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
21607 self.advance();
21608 let e = self.parse_expr(9)?;
21609 Ok(build_center_call(e))
21610 }
21611
21612 /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
21613 /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
21614 /// unary minus.
21615 ///
21616 /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
21617 /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
21618 /// the Expr-sized local stays out of that frame.
21619 #[inline(never)]
21620 fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21621 self.advance();
21622 let e = self.parse_expr(9)?;
21623 Ok(Expr::FunctionCall {
21624 name: alloc::string::String::from(name),
21625 args: alloc::vec![e],
21626 })
21627 }
21628
21629 /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
21630 /// (horizontal). Out-of-line from `parse_unary` (frame budget).
21631 #[inline(never)]
21632 fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
21633 self.advance();
21634 let e = self.parse_expr(9)?;
21635 Ok(Expr::FunctionCall {
21636 name: alloc::string::String::from(if vertical {
21637 "isvertical"
21638 } else {
21639 "ishorizontal"
21640 }),
21641 args: alloc::vec![e],
21642 })
21643 }
21644
21645 /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
21646 /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
21647 /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
21648 #[inline(never)]
21649 fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
21650 self.advance();
21651 let e = self.parse_expr(9)?;
21652 Ok(Expr::Cast {
21653 expr: Box::new(e),
21654 target: CastTarget::Named("binary".to_string()),
21655 })
21656 }
21657
21658 /// The prefix operators that share one shape: take the token, parse
21659 /// an operand at `prec`, wrap it.
21660 ///
21661 /// `#[inline(never)]`, and one function instead of five arms, for the
21662 /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
21663 /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
21664 /// debug build gives EVERY arm's locals a slot in the frame, whichever
21665 /// arm runs. `((((1))))` reaches none of these arms and was carrying
21666 /// five `Expr`-sized locals per level for them anyway.
21667 #[inline(never)]
21668 fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
21669 self.advance();
21670 let e = self.parse_expr(prec)?;
21671 Ok(Expr::Unary {
21672 op,
21673 expr: Box::new(e),
21674 })
21675 }
21676
21677 /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
21678 /// and separate from it because of the literal folding below and the
21679 /// `format!` temporaries that folding needs.
21680 #[inline(never)]
21681 fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
21682 self.advance();
21683 // v7.39 (round 549) — fold the sign into an integer literal that
21684 // only fits once it is negative.
21685 //
21686 // `9223372036854775808` is one past i64::MAX, so the lexer hands
21687 // it over as a NUMERIC and `-` on a numeric stays numeric. PG
21688 // folds the sign first, so `-9223372036854775808` is a bigint
21689 // there — and `-9223372036854775808 - 1` raises "bigint out of
21690 // range" where SPG quietly answered -9223372036854775809, a value
21691 // no bigint can hold. The arithmetic itself was already checked;
21692 // only the literal's type was wrong.
21693 if let Token::Numeric(lit) = self.peek()
21694 && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
21695 {
21696 self.advance();
21697 return Ok(Expr::Literal(Literal::Integer(folded)));
21698 }
21699 // Unary minus binds tighter than `*`/`/` (now at prec 7 after
21700 // `<->` slotted into 5 and arithmetic shifted up).
21701 let e = self.parse_expr(9)?;
21702 Ok(Expr::Unary {
21703 op: UnOp::Neg,
21704 expr: Box::new(e),
21705 })
21706 }
21707
21708 /// tsquery `!!` prefix negation, lowered to the catalog function.
21709 /// Binds like unary minus. Out-of-line for the frame reason on
21710 /// `parse_unary_op`.
21711 #[inline(never)]
21712 fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
21713 self.advance();
21714 let e = self.parse_expr(9)?;
21715 Ok(Expr::FunctionCall {
21716 name: String::from("tsquery_not"),
21717 args: alloc::vec![e],
21718 })
21719 }
21720
21721 fn parse_unary(&mut self) -> Result<Expr, ParseError> {
21722 match self.peek() {
21723 // NOT binds tighter than AND / XOR / OR but looser than
21724 // comparisons — its operand takes everything ≥ the comparison
21725 // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
21726 // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
21727 // was rung 3, behaviour-identical when 3 was unused; AND now
21728 // occupies 3, so this must be 4 to keep NOT tighter than AND.)
21729 Token::Not => self.parse_unary_op(UnOp::Not, 4),
21730 // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
21731 // The body is out-of-line: `parse_unary` is one of the three
21732 // frames the parser's MAX_NEST_DEPTH is tuned against, and an
21733 // inline arm here overflowed the native stack in
21734 // `nesting_budget_errors_cleanly` — the guard test caught it,
21735 // exactly as the eval-side cliff did in rounds 346 and 351.
21736 Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
21737 self.parse_binary_prefix()
21738 }
21739 // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
21740 // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
21741 // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
21742 Token::Bang => self.parse_unary_op(UnOp::Not, 9),
21743 Token::Minus => self.parse_prefix_minus(),
21744 // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
21745 // worked only because the lexer reads it as one signed literal;
21746 // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
21747 // PG18 and MariaDB take all of them. Binds like unary minus.
21748 Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
21749 // Bitwise NOT binds like unary minus.
21750 Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
21751 // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
21752 // "center of" operator; desugars to center(x). The whole arm
21753 // is out-of-line: parse_unary sits on the per-nesting-level
21754 // frame chain that MAX_NEST_DEPTH is tuned against, so no
21755 // Expr-sized local may live in this frame.
21756 Token::TsMatch => self.parse_prefix_center(),
21757 // v7.39 (round 508) — the prefix operators that are named
21758 // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
21759 // is length. Out-of-line for the same nesting-frame reason as
21760 // parse_prefix_center — parse_unary sits on the recursive cycle
21761 // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
21762 // live in this frame.
21763 Token::At => self.parse_prefix_call("abs"),
21764 Token::Hash => self.parse_prefix_call("npoints"),
21765 Token::AtMinusAt => self.parse_prefix_call("length"),
21766 // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
21767 // "is horizontal" (lseg / line); desugars to the existing
21768 // isvertical()/ishorizontal() functions. Out-of-line for the
21769 // same nesting-frame reason as parse_prefix_center.
21770 Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
21771 Token::GeomHoriz => self.parse_prefix_geom_axis(false),
21772 Token::DoubleBang => self.parse_prefix_tsquery_not(),
21773 _ => self.parse_atom(),
21774 }
21775 }
21776
21777 /// Parse a parenthesised scalar subquery body after the caller has consumed
21778 /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
21779 /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
21780 /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
21781 /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
21782 /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
21783 /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
21784 /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
21785 /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
21786 /// which sits on the recursive nesting-budget cycle (a few extra bytes there
21787 /// tips the deep-nesting test into a stack overflow).
21788 #[inline(never)]
21789 fn array_subquery_ahead(&self) -> bool {
21790 if !matches!(self.peek(), Token::LParen) {
21791 return false;
21792 }
21793 matches!(
21794 self.tokens.get(self.pos + 1),
21795 Some(Token::Select | Token::Values)
21796 ) || matches!(
21797 self.tokens.get(self.pos + 1),
21798 Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
21799 )
21800 }
21801
21802 /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
21803 /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
21804 /// locals stay off parse_atom's recursive frame (round 105).
21805 #[inline(never)]
21806 fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
21807 self.advance(); // consume `[`
21808 let mut items: Vec<Expr> = Vec::new();
21809 if !matches!(self.peek(), Token::RBracket) {
21810 loop {
21811 // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
21812 // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
21813 if matches!(self.peek(), Token::LBracket) {
21814 items.push(self.parse_array_bracket_body()?);
21815 } else {
21816 items.push(self.parse_expr(0)?);
21817 }
21818 match self.peek() {
21819 Token::Comma => {
21820 self.advance();
21821 }
21822 Token::RBracket => break,
21823 other => {
21824 return Err(self.err(alloc::format!(
21825 "expected ',' or ']' in ARRAY literal, got {other:?}"
21826 )));
21827 }
21828 }
21829 }
21830 }
21831 self.advance(); // consume `]`
21832 Ok(Expr::Array(items))
21833 }
21834
21835 /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
21836 /// is already consumed; the current token is `(`. Desugars to a scalar
21837 /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
21838 /// the subquery's single-column rows in order — reusing the existing
21839 /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
21840 /// keeps the large `Statement` local off parse_atom's recursive frame.
21841 #[inline(never)]
21842 fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
21843 self.advance(); // consume `(`
21844 let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
21845 if w.eq_ignore_ascii_case("with"));
21846 let sub = if is_with {
21847 self.advance(); // WITH
21848 self.parse_with_cte_then_select()?
21849 } else {
21850 self.parse_select_stmt()?
21851 };
21852 if !matches!(self.peek(), Token::RParen) {
21853 return Err(self.err(alloc::format!(
21854 "expected ')' to close ARRAY(subquery), got {:?}",
21855 self.peek()
21856 )));
21857 }
21858 self.advance(); // consume `)`
21859 // Reuse the parser to build the array_agg wrapper from the subquery's
21860 // canonical text — avoids hand-constructing the derived-table AST.
21861 let wrapper = alloc::format!(
21862 "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
21863 );
21864 let stmt = parse_statement(&wrapper)
21865 .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
21866 let Statement::Select(sel) = stmt else {
21867 return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
21868 };
21869 Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
21870 }
21871
21872 #[inline(never)]
21873 fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
21874 let inner = if is_with {
21875 self.advance(); // WITH
21876 self.parse_with_cte_then_select()?
21877 } else {
21878 self.parse_select_stmt()?
21879 };
21880 match self.advance() {
21881 Token::RParen => {
21882 let Statement::Select(s) = inner else {
21883 return Err(ParseError {
21884 message: "scalar subquery body must be a SELECT".into(),
21885 token_pos: self.consumed_pos(),
21886 });
21887 };
21888 Ok(Expr::ScalarSubquery(Box::new(s)))
21889 }
21890 other => Err(ParseError {
21891 message: format!("expected ')' after scalar subquery, got {other:?}"),
21892 token_pos: self.consumed_pos(),
21893 }),
21894 }
21895 }
21896
21897 /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
21898 /// literals. The lexer splits them into an ident + string; recombine
21899 /// here. Out-of-line and returning `Option` so `parse_atom` — the
21900 /// recursive frame the 768 KiB stack budget is tuned against — pays no
21901 /// frame for the `body` / `bits` strings and their char loops (the
21902 /// round-367 frame cliff, M20).
21903 #[inline(never)]
21904 fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
21905 let is_hex = match self.peek() {
21906 Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
21907 Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
21908 _ => return None,
21909 };
21910 if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
21911 return None;
21912 }
21913 self.advance();
21914 let Token::String(body) = self.advance() else {
21915 unreachable!("guarded above");
21916 };
21917 // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
21918 // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
21919 // (hex pairs, even count required — MariaDB errors on an odd
21920 // count); `b'1010'` packs its bits big-endian, left-padded to a
21921 // byte. Lower both onto the bytea cast.
21922 if self.mysql_dialect {
21923 if is_hex {
21924 if body.len() % 2 == 1 {
21925 return Some(Err(self.err(alloc::format!(
21926 "invalid hex string literal X'{body}': odd digit count"
21927 ))));
21928 }
21929 for c in body.chars() {
21930 if !c.is_ascii_hexdigit() {
21931 return Some(Err(
21932 self.err(alloc::format!("invalid hexadecimal digit {c:?} in X'…'"))
21933 ));
21934 }
21935 }
21936 return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
21937 }
21938 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21939 return Some(Err(
21940 self.err(alloc::format!("invalid binary digit {bad:?} in b'…'"))
21941 ));
21942 }
21943 return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
21944 }
21945 let bits = if is_hex {
21946 let mut out = String::with_capacity(body.len() * 4);
21947 for c in body.chars() {
21948 let Some(d) = c.to_digit(16) else {
21949 return Some(Err(self.err(alloc::format!(
21950 "invalid hexadecimal digit {c:?} in X'…' bit string"
21951 ))));
21952 };
21953 out.push_str(&alloc::format!("{d:04b}"));
21954 }
21955 out
21956 } else {
21957 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21958 return Some(Err(self.err(alloc::format!(
21959 "invalid binary digit {bad:?} in B'…' bit string"
21960 ))));
21961 }
21962 body
21963 };
21964 // Route through the postfix-cast loop so a chained cast like
21965 // `B'1010'::int` attaches onto the implicit `::bit` cast instead
21966 // of erroring at the `::`.
21967 // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
21968 // literal keeps its exact length, while an explicit `::bit` cast is
21969 // bit(1) with pad/truncate semantics (PG).
21970 Some(self.finish_postfix_casts(Expr::Cast {
21971 expr: Box::new(Expr::Literal(Literal::String(bits))),
21972 target: CastTarget::Named("__bit_literal".to_string()),
21973 }))
21974 }
21975
21976 fn parse_atom(&mut self) -> Result<Expr, ParseError> {
21977 if let Some(res) = self.try_parse_bit_string_literal() {
21978 return res;
21979 }
21980 let tok_pos = self.pos;
21981 match self.advance() {
21982 Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
21983 Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
21984 // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
21985 // carrying the source mantissa + scale so no precision is lost. A
21986 // literal too wide for i128 falls back to double precision.
21987 // Out-of-line (#[inline(never)]) — this arm sits on the
21988 // parse_expr recursion chain; its expansion locals must not
21989 // widen the recursive frame (debug frame-cliff discipline).
21990 Token::Numeric(s) => match numeric_token_to_literal(s) {
21991 Ok(lit) => Ok(Expr::Literal(lit)),
21992 Err(msg) => Err(self.err(msg)),
21993 },
21994 Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
21995 // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
21996 // (the lexer only emits this token in the MySQL dialect). Lower
21997 // onto the existing bytea cast; out-of-line to keep this arm off
21998 // the parse recursion frame.
21999 Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
22000 Token::True => Ok(Expr::Literal(Literal::Bool(true))),
22001 Token::False => Ok(Expr::Literal(Literal::Bool(false))),
22002 Token::Null => Ok(Expr::Literal(Literal::Null)),
22003 // v6.1.1 — `$N` placeholder. The actual Value lookup
22004 // happens in the engine eval path against the prepared-
22005 // statement bind buffer.
22006 Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
22007 Token::LParen => {
22008 // v4.10: `(SELECT ...)` in expression position is a
22009 // scalar subquery; otherwise it's a parenthesised
22010 // expression. Peek for SELECT keyword to dispatch.
22011 // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
22012 // lexes as Ident("with") (not a reserved token). The subquery body
22013 // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
22014 // so its large `Statement` local stays out of parse_atom's stack
22015 // frame — parse_atom is on the recursive `((…))` cycle and the
22016 // nesting budget is tuned to its frame size).
22017 let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22018 if s.eq_ignore_ascii_case("with"));
22019 if matches!(self.peek(), Token::Select) || is_with {
22020 self.parse_paren_scalar_subquery(is_with)
22021 } else {
22022 let e = self.parse_expr(0)?;
22023 // `(a, b, …)` — a row constructor. Valid only
22024 // in front of a comparison operator or [NOT]
22025 // IN; both expand at parse time (lexicographic
22026 // comparison / OR'd row equalities).
22027 if matches!(self.peek(), Token::Comma) {
22028 let mut row = alloc::vec![e];
22029 while matches!(self.peek(), Token::Comma) {
22030 self.advance();
22031 row.push(self.parse_expr(0)?);
22032 }
22033 if !matches!(self.peek(), Token::RParen) {
22034 return Err(self.err(alloc::format!(
22035 "expected ')' after row constructor, got {:?}",
22036 self.peek()
22037 )));
22038 }
22039 self.advance();
22040 // A bare `(a, b, …)` row constructor can carry postfix
22041 // (`::text`, `.field`) just like `ROW(a, b, …)`; the
22042 // early return here skips parse_atom's tail postfix
22043 // pass, so fold casts in explicitly. For the
22044 // comparison / predicate forms nothing postfix follows,
22045 // so this is a no-op.
22046 return self
22047 .parse_row_comparison_tail(row)
22048 .and_then(|e| self.finish_postfix_casts(e));
22049 }
22050 match self.advance() {
22051 Token::RParen => Ok(e),
22052 other => Err(ParseError {
22053 message: format!("expected ')', got {other:?}"),
22054 token_pos: self.consumed_pos(),
22055 }),
22056 }
22057 }
22058 }
22059 Token::LBracket => self.parse_vector_literal_body(),
22060 Token::Extract => self.parse_extract_atom(),
22061 Token::Interval => self.parse_interval_atom(),
22062 // `LEFT` / `RIGHT` are reserved-keyword tokens because the
22063 // grammar dedicates arms for `LEFT [OUTER] JOIN` /
22064 // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
22065 // expression position calling the PG `left(string, n)` /
22066 // `right(string, n)` function; rebuild the AST as a regular
22067 // function call so the engine's apply_function dispatch picks
22068 // it up. Delegated to a #[inline(never)] helper so its locals
22069 // don't bloat this recursive `parse_atom` frame (the nesting
22070 // budget in `enter_nested` is tuned to parse_atom's size).
22071 Token::Left if matches!(self.peek(), Token::LParen) => {
22072 self.parse_lr_string_function_call("left")
22073 }
22074 Token::Right if matches!(self.peek(), Token::LParen) => {
22075 self.parse_lr_string_function_call("right")
22076 }
22077 // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
22078 // token; we match on the bare ident. NOT is a token
22079 // (consumed in the comparison rung), but `EXISTS (...)`
22080 // at the top of an expression starts here.
22081 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
22082 self.parse_exists_atom(false)
22083 }
22084 // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
22085 // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
22086 // CASE is a bare ident; we dispatch on lowercase match.
22087 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
22088 self.parse_case_atom()
22089 }
22090 // v7.37.17 (17.6 siblings) — PG typed datetime literals:
22091 // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
22092 // '…'`. Lower onto the ::cast node so the existing
22093 // runtime text→date/timestamp paths do the parsing. The
22094 // string must follow immediately, else the ident stays a
22095 // plain column reference.
22096 Token::Ident(s)
22097 if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
22098 && matches!(self.peek(), Token::String(_)) =>
22099 {
22100 let target =
22101 typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
22102 let Token::String(lit) = self.advance() else {
22103 unreachable!("peek guaranteed a string token");
22104 };
22105 Ok(Expr::Cast {
22106 expr: Box::new(Expr::Literal(Literal::String(lit))),
22107 target,
22108 })
22109 }
22110 // v7.39 (round 221) — the SQL-standard long spellings:
22111 // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
22112 // TIME ZONE '…'`. Consume the modifier and lower to the same
22113 // typed-literal cast (`timetz` / `timestamptz` for WITH).
22114 Token::Ident(s)
22115 if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
22116 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
22117 || w.eq_ignore_ascii_case("without"))
22118 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
22119 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
22120 && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
22121 {
22122 let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
22123 self.advance(); // WITH / WITHOUT
22124 self.advance(); // TIME
22125 self.advance(); // ZONE
22126 let Token::String(lit) = self.advance() else {
22127 unreachable!("guard checked a string token");
22128 };
22129 let base = s.to_ascii_lowercase();
22130 let target = match (base.as_str(), with_tz) {
22131 ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
22132 ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
22133 (_, true) => CastTarget::Timestamptz,
22134 (_, false) => CastTarget::Timestamp,
22135 };
22136 Ok(Expr::Cast {
22137 expr: Box::new(Expr::Literal(Literal::String(lit))),
22138 target,
22139 })
22140 }
22141 // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
22142 // gathers the subquery's single-column rows (in its row order)
22143 // into an array. Desugared to `array_agg` over the subquery as a
22144 // derived table; out-of-line to keep parse_atom's frame small (it
22145 // sits on the recursive nesting-budget cycle).
22146 Token::Ident(s) | Token::QuotedIdent(s)
22147 if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
22148 {
22149 self.parse_array_subquery()
22150 }
22151 // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
22152 // is not a reserved token; we match by case-insensitive
22153 // ident. The opening `[` must follow immediately. v7.39 (read01
22154 // round 105) — the body moved out-of-line so its `Vec`/loop locals
22155 // leave parse_atom's frame (which sits on the nesting-budget cycle).
22156 Token::Ident(s) | Token::QuotedIdent(s)
22157 if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
22158 {
22159 self.parse_array_literal_body()
22160 }
22161 // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
22162 // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
22163 // We special-case before the generic ident dispatch so
22164 // the AGAINST clause never reaches the function-call
22165 // loop (which would mis-read `(cols) AGAINST` as a
22166 // call with no trailing modifier). The shape is
22167 // rewritten to a Boolean OR over per-column
22168 // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
22169 // term)` so the existing FTS evaluator handles
22170 // semantics — the fulltext-GIN built at CREATE TABLE
22171 // time is currently a "real index that survives dump
22172 // round-trip"; the planner hook that actually uses
22173 // it for posting-list intersection lands in a later
22174 // sub-phase (Phase 2.2b) without touching this surface.
22175 Token::Ident(s) | Token::QuotedIdent(s)
22176 if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
22177 {
22178 self.parse_match_against_atom()
22179 }
22180 Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
22181 // v7.37.43-T4 — PG-unreserved keywords are legal column /
22182 // alias names in expression context too. `release` appears
22183 // in sentori `0003_partition_events.sql` as both a column
22184 // reference (SELECT … release …) and an INSERT column list
22185 // entry. Mirrors `expect_ident_like`'s expansion of the
22186 // identifier set.
22187 other if unreserved_keyword_text(&other).is_some() => {
22188 let s = unreserved_keyword_text(&other).unwrap();
22189 self.finish_ident_atom(s)
22190 }
22191 // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
22192 // only inside `SET` before, so `SELECT @@autocommit` — which
22193 // every MySQL connector asks at handshake — was a parse error.
22194 // MariaDB accepts the bare, `@@session.` and `@@global.`
22195 // spellings alike and answers from the session's own value.
22196 Token::SessionVar(v) => {
22197 // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
22198 // has nothing to do with a `@@` engine setting: its own
22199 // per-session namespace, and an unset one reads NULL instead
22200 // of raising. Stripping every `@` (as this did) made `@x` and
22201 // `@@x` the same node, so `SELECT @x` answered "Unknown
22202 // system variable".
22203 Ok(variable_ref_atom(&v))
22204 }
22205 other => Err(ParseError {
22206 message: format!("unexpected token {other:?} in expression"),
22207 token_pos: tok_pos,
22208 }),
22209 }
22210 // After parsing the atom, fold any postfix `::vector` casts.
22211 .and_then(|atom| self.finish_postfix_casts(atom))
22212 }
22213
22214 /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
22215 /// Both bind tighter than any binary op.
22216 /// Shared cast-target parser for postfix `::TYPE` and the
22217 /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
22218 /// If the next tokens are `( N )`, consume them and return the canonical
22219 /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
22220 /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
22221 fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
22222 if !matches!(self.peek(), Token::LParen) {
22223 return None;
22224 }
22225 self.advance(); // (
22226 let n = match self.advance() {
22227 Token::Integer(n) => n,
22228 _ => return Some(base.to_string()), // malformed → drop precision
22229 };
22230 if matches!(self.peek(), Token::RParen) {
22231 self.advance();
22232 }
22233 Some(alloc::format!("{base}({n})"))
22234 }
22235
22236 fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
22237 // r1052 — `::pg_catalog.regproc` and friends: pg_dump
22238 // schema-qualifies every cast target, and `pg_catalog.X` names
22239 // exactly the builtin type X. Consume the qualifier and let
22240 // the ordinary target parse decide.
22241 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
22242 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
22243 {
22244 self.advance();
22245 self.advance();
22246 }
22247 let target = match self.advance() {
22248 Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
22249 "int" | "integer" | "int4" => {
22250 if matches!(self.peek(), Token::LBracket)
22251 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22252 {
22253 self.advance();
22254 self.advance();
22255 CastTarget::IntArray
22256 } else {
22257 CastTarget::Int
22258 }
22259 }
22260 "bigint" | "int8" => {
22261 if matches!(self.peek(), Token::LBracket)
22262 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22263 {
22264 self.advance();
22265 self.advance();
22266 CastTarget::BigIntArray
22267 } else {
22268 CastTarget::BigInt
22269 }
22270 }
22271 "float" | "double" => CastTarget::Float,
22272 "text" => {
22273 // v7.10.11 — `::TEXT[]` widens to TextArray.
22274 if matches!(self.peek(), Token::LBracket)
22275 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22276 {
22277 self.advance();
22278 self.advance();
22279 CastTarget::TextArray
22280 } else {
22281 CastTarget::Text
22282 }
22283 }
22284 "bool" | "boolean" => CastTarget::Bool,
22285 "vector" => CastTarget::Vector,
22286 "date" => CastTarget::Date,
22287 // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22288 // seconds precision through the Named path (the engine rounds
22289 // the sub-second field); bare `::timestamp` keeps the fast arm.
22290 "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22291 Some(named) => CastTarget::Named(named),
22292 None => CastTarget::Timestamp,
22293 },
22294 "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22295 Some(named) => CastTarget::Named(named),
22296 None => CastTarget::Timestamptz,
22297 },
22298 "interval" => CastTarget::Interval,
22299 "json" => CastTarget::Json,
22300 "jsonb" => CastTarget::Jsonb,
22301 // v7.39 (round 694) — these have dedicated CastTarget
22302 // variants, so they never reached the postfix `[]` handling
22303 // further down and `::regtype[]` was a SYNTAX error at the
22304 // `]`. PG has an array type for every scalar; take the
22305 // suffix here and hand the canonical `<ty>_array` name to
22306 // the engine, the same shape every other array cast uses.
22307 "regtype" if self.peek_postfix_array_brackets() => {
22308 self.advance();
22309 self.advance();
22310 CastTarget::Named(alloc::string::String::from("regtype_array"))
22311 }
22312 "regclass" if self.peek_postfix_array_brackets() => {
22313 self.advance();
22314 self.advance();
22315 CastTarget::Named(alloc::string::String::from("regclass_array"))
22316 }
22317 "regtype" => CastTarget::RegType,
22318 "regclass" => CastTarget::RegClass,
22319 // v7.12.0 — `::tsvector` / `::tsquery`.
22320 // Engine decodes the LHS text via the PG
22321 // external form parser.
22322 // v7.39 (round 352, M8) — MySQL's own cast targets.
22323 // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22324 // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22325 // such type, so they are taken only in that dialect and
22326 // fall through to the "type does not exist" arm otherwise.
22327 "signed" | "unsigned" if self.mysql_dialect => {
22328 if matches!(self.peek(), Token::Ident(k)
22329 if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22330 {
22331 self.advance();
22332 }
22333 CastTarget::Named(s.to_ascii_lowercase())
22334 }
22335 // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22336 // in MySQL: MariaDB answers '123' where the SQL-standard
22337 // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22338 // Truncating a number to its first digit is a wrong answer
22339 // with no error, so the MySQL session gets MySQL's reading.
22340 "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22341 CastTarget::Text
22342 }
22343 "tsvector" => CastTarget::TsVector,
22344 "tsquery" => CastTarget::TsQuery,
22345 // v7.17.0 — `::uuid`. Engine decodes the LHS
22346 // text via `spg_storage::parse_uuid_str`.
22347 "uuid" => CastTarget::Uuid,
22348 // v7.18 — `::bytea`. Engine decodes the LHS
22349 // text via the PG hex form (`'\xdeadbeef'`)
22350 // or escape form (`'\\x05\\x00'`). Closes
22351 // mailrs D-pre #3 reverse-acceptance gap.
22352 "bytea" => CastTarget::Bytea,
22353 // v7.37.5 ship triage — generic typed-cast escape.
22354 // Anything the long-tail PG type ident table knows
22355 // about(network/bit/geometry/multirange/etc.)flows
22356 // through `CastTarget::Named(canonical)`; the engine
22357 // resolves via `column_type_to_data_type` and dispatches
22358 // through the typed `coerce_value` path. Truly
22359 // unrecognised idents still hit the error arm below
22360 // because the engine rejects them.
22361 other => {
22362 // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22363 // `::varchar(255)`, etc. Capture into the canonical
22364 // `name(p,s)` form so `type_name_to_data_type` can
22365 // reconstruct the `DataType::Numeric { precision,
22366 // scale }` (and similar param-carrying types).
22367 let mut name = other.to_string();
22368 // v7.39 (round 281) — `::bit varying(3)` is two
22369 // words; fold the tail in so the typmod reaches the
22370 // type resolver instead of tripping the parser.
22371 if name.eq_ignore_ascii_case("bit")
22372 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22373 {
22374 self.advance();
22375 name = alloc::string::String::from("varbit");
22376 }
22377 // v7.39 (round 613) — `::character varying` is the same
22378 // two-word shape and had no fold, so the `varying` was
22379 // left behind and the cast became a bare `character`,
22380 // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22381 // `a` where PG answers `ab`. Silently, and for a spelling
22382 // pg_dump writes.
22383 if name.eq_ignore_ascii_case("character")
22384 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22385 {
22386 self.advance();
22387 name = alloc::string::String::from("varchar");
22388 }
22389 if matches!(self.peek(), Token::LParen) {
22390 let mut buf = alloc::string::String::from("(");
22391 let mut depth = 0usize;
22392 loop {
22393 match self.advance() {
22394 Token::LParen => {
22395 depth += 1;
22396 if depth > 1 {
22397 buf.push('(');
22398 }
22399 }
22400 Token::RParen => {
22401 depth -= 1;
22402 if depth == 0 {
22403 buf.push(')');
22404 break;
22405 }
22406 buf.push(')');
22407 }
22408 Token::Comma => buf.push(','),
22409 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22410 // v7.39 (round 273) — a minus used to fall
22411 // into the catch-all below and vanish, so
22412 // `::numeric(10,-2)` reached the engine as
22413 // the text `numeric(10,2)` and silently
22414 // rounded to two DECIMALS instead of to
22415 // hundreds. A dropped token is not a
22416 // no-op when it carries a sign.
22417 Token::Minus => buf.push('-'),
22418 Token::Eof => break,
22419 _ => {}
22420 }
22421 }
22422 name.push_str(&buf);
22423 }
22424 // Optional postfix `[]` widens to the array form —
22425 // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22426 // The engine's `type_name_to_data_type` recognises
22427 // the canonical `<ty>_array` form.
22428 if matches!(self.peek(), Token::LBracket)
22429 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22430 {
22431 self.advance();
22432 self.advance();
22433 name.push_str("_array");
22434 }
22435 CastTarget::Named(name)
22436 }
22437 },
22438 Token::Interval => CastTarget::Interval,
22439 // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22440 // "char" (oid 18, SPG Char1 — distinct from bare `char`
22441 // = char(1)); other quoted names resolve like idents.
22442 Token::QuotedIdent(q) => {
22443 if q.eq_ignore_ascii_case("char") {
22444 CastTarget::Named("char1".into())
22445 } else {
22446 CastTarget::Named(q.to_ascii_lowercase())
22447 }
22448 }
22449 other => {
22450 return Err(ParseError {
22451 message: format!("expected type ident after `::`, got {other:?}"),
22452 token_pos: self.consumed_pos(),
22453 });
22454 }
22455 };
22456 // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22457 // target to its array sibling. Closed-enum arms (Bool /
22458 // SmallInt / Numeric / Float / Date / …) didn't carry the
22459 // explicit widening that Text / Int / BigInt did, so
22460 // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22461 // error. The widening here mirrors the per-arm Text /
22462 // Int / BigInt logic above + folds the new ζ-A first-class
22463 // types through `CastTarget::Named("<ty>_array")`.
22464 if matches!(self.peek(), Token::LBracket)
22465 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22466 {
22467 let widened = match &target {
22468 CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22469 CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22470 // v7.39 (round 326, V43) — the two temporal types stay
22471 // distinct. Both used to widen to `timestamptz_array`, so
22472 // `::timestamp[]` named the wrong target in its own error
22473 // message and lost the zone-less identity on the way.
22474 CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22475 CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22476 CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22477 CastTarget::Json | CastTarget::Jsonb => {
22478 Some(CastTarget::Named("jsonb_array".to_string()))
22479 }
22480 CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22481 CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22482 CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22483 CastTarget::Named(name) => {
22484 let mut a = name.clone();
22485 a.push_str("_array");
22486 Some(CastTarget::Named(a))
22487 }
22488 // Int / BigInt / Text / Vector / TsVector / TsQuery /
22489 // RegType / RegClass / TextArray / IntArray /
22490 // BigIntArray already finalised — leave as is.
22491 _ => None,
22492 };
22493 if let Some(w) = widened {
22494 self.advance();
22495 self.advance();
22496 return Ok(w);
22497 }
22498 }
22499 Ok(target)
22500 }
22501
22502 fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
22503 loop {
22504 // v7.38 (read01, T9) — composite field access `(expr).field`.
22505 // A bare `a.b` is consumed as a qualified column inside the ident
22506 // atom, so a Dot only survives to this postfix position when the
22507 // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
22508 // `.*` whole-row expansion is not handled here (projection-level).
22509 if matches!(self.peek(), Token::Dot)
22510 && matches!(
22511 self.tokens.get(self.pos + 1),
22512 Some(Token::Ident(_) | Token::QuotedIdent(_))
22513 )
22514 {
22515 self.advance(); // .
22516 let field = match self.advance() {
22517 Token::Ident(s) | Token::QuotedIdent(s) => s,
22518 other => {
22519 return Err(
22520 self.err(format!("expected a field name after '.', got {other:?}"))
22521 );
22522 }
22523 };
22524 expr = Expr::FieldAccess {
22525 base: Box::new(expr),
22526 field,
22527 };
22528 continue;
22529 }
22530 if matches!(self.peek(), Token::DoubleColon) {
22531 self.advance();
22532 // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
22533 // target set to include INTERVAL (reserved Token),
22534 // TIMESTAMPTZ, and PG catalog regtype / regclass.
22535 // mailrs follow-up H3a + H3b.
22536 let target = self.parse_cast_target()?;
22537 expr = Expr::Cast {
22538 expr: Box::new(expr),
22539 target,
22540 };
22541 continue;
22542 }
22543 // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
22544 // returns NULL for out-of-range. Multiple subscripts
22545 // chain: `a[i][j]` parses left-to-right.
22546 if matches!(self.peek(), Token::LBracket) {
22547 self.advance();
22548 // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
22549 // bare index stays a subscript.
22550 let lo = if matches!(self.peek(), Token::Colon) {
22551 None
22552 } else {
22553 Some(self.parse_expr(0)?)
22554 };
22555 if matches!(self.peek(), Token::Colon) {
22556 self.advance();
22557 let hi = if matches!(self.peek(), Token::RBracket) {
22558 None
22559 } else {
22560 Some(Box::new(self.parse_expr(0)?))
22561 };
22562 if !matches!(self.peek(), Token::RBracket) {
22563 return Err(self.err(alloc::format!(
22564 "expected ']' after array slice, got {:?}",
22565 self.peek()
22566 )));
22567 }
22568 self.advance();
22569 expr = Expr::ArraySlice {
22570 target: Box::new(expr),
22571 lo: lo.map(Box::new),
22572 hi,
22573 };
22574 continue;
22575 }
22576 let index = lo.expect("non-colon branch parsed an index");
22577 if !matches!(self.peek(), Token::RBracket) {
22578 return Err(self.err(alloc::format!(
22579 "expected ']' after array index, got {:?}",
22580 self.peek()
22581 )));
22582 }
22583 self.advance();
22584 expr = Expr::ArraySubscript {
22585 target: Box::new(expr),
22586 index: Box::new(index),
22587 };
22588 continue;
22589 }
22590 // `expr AT TIME ZONE zone` — lowers to PG's own function
22591 // form timezone(zone, expr); the scalar implements the
22592 // offset shift (named zones error there — no tzdata).
22593 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
22594 && matches!(self.tokens.get(self.pos + 1),
22595 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
22596 && matches!(self.tokens.get(self.pos + 2),
22597 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
22598 {
22599 self.advance(); // AT
22600 self.advance(); // TIME
22601 self.advance(); // ZONE
22602 // Zone at comparison precedence so AND/OR stay out.
22603 let zone = self.parse_expr(6)?;
22604 expr = Expr::FunctionCall {
22605 name: "timezone".to_string(),
22606 args: alloc::vec![zone, expr],
22607 };
22608 continue;
22609 }
22610 // `expr COLLATE "name"` — SPG's single text ordering IS
22611 // byte order, i.e. the C collation. The byte-order
22612 // spellings absorb as no-ops; a locale collation would
22613 // silently sort differently from PG, so it errors
22614 // honestly instead.
22615 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
22616 self.advance();
22617 let mut cname = match self.advance() {
22618 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22619 other => {
22620 return Err(self.err(alloc::format!(
22621 "expected collation name after COLLATE, got {other:?}"
22622 )));
22623 }
22624 };
22625 // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
22626 // is how `pg_dump` writes the default one:
22627 // `… COLLATE pg_catalog.default`. Reading a single token
22628 // left the SCHEMA as the name, so the clause was refused
22629 // as an unsupported locale collation and no dump ran.
22630 if matches!(self.peek(), Token::Dot) {
22631 self.advance();
22632 cname = match self.advance() {
22633 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22634 // `default` lexes as a KEYWORD, and it is the name
22635 // pg_dump writes — the same trap round 535 hit with
22636 // TABLE / INDEX / FULL.
22637 Token::Default => alloc::string::String::from("default"),
22638 other => {
22639 return Err(self.err(alloc::format!(
22640 "expected collation name after COLLATE, got {other:?}"
22641 )));
22642 }
22643 };
22644 }
22645 let lc = cname.to_ascii_lowercase();
22646 // v7.39 (round 371, M4 P4b) — a per-expression MySQL
22647 // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
22648 // family / `binary`) forces byte-wise, which is exactly
22649 // what `BINARY expr` does — lower onto that so every fold
22650 // site (comparison, LIKE, ORDER BY) suppresses via
22651 // `is_binary_coerced`. A `_ci` family override folds, and
22652 // under the MySQL dialect the default already folds, so it
22653 // absorbs as a no-op; likewise the C / byte-order spellings.
22654 if self.mysql_dialect && (lc.ends_with("_bin") || lc == "binary") {
22655 expr = Expr::Cast {
22656 expr: alloc::boxed::Box::new(expr),
22657 target: CastTarget::Named("binary".to_string()),
22658 };
22659 continue;
22660 }
22661 let mysql_ci = self.mysql_dialect
22662 && (lc.ends_with("_ci")
22663 || matches!(lc.as_str(), "case_insensitive" | "nocase"));
22664 // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
22665 // goes to the lowering channel, the byte-order spellings
22666 // included. Round 691 recorded only the names the old
22667 // allow-list rejected, which left `ORDER BY a COLLATE "C"`
22668 // absorbed as a no-op — and once a column could declare a
22669 // collation, absorbing the clause meant the COLUMN's
22670 // collation won where the query had asked for bytes.
22671 if self.in_order_by_key && !mysql_ci {
22672 self.order_key_collation = Some(cname);
22673 continue;
22674 }
22675 if !matches!(
22676 lc.as_str(),
22677 "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
22678 ) && !mysql_ci
22679 {
22680 // v7.38.18 — the old message read "SPG orders text
22681 // by bytes (the C collation); locale collations are
22682 // not supported yet", and both halves were false by
22683 // the time it was read. This build performs locale
22684 // collations: declared on a column or written in an
22685 // ORDER BY key, `en_US.utf8` orders `apple, client,
22686 // DateStyle, Zebra` exactly as PG 18.4 does. What it
22687 // cannot do is carry a collation on an arbitrary
22688 // expression, because there is no `Expr::Collate` to
22689 // carry it — so say that, and say where the clause
22690 // does work rather than telling the reader to drop it.
22691 return Err(self.err(alloc::format!(
22692 "COLLATE {cname:?} is not supported in this position: \
22693 SPG carries a collation on a column declaration and \
22694 on an ORDER BY key, not on an arbitrary expression. \
22695 Declare it on the column (`x text COLLATE \
22696 {cname:?}`) or move it into the ORDER BY key"
22697 )));
22698 }
22699 continue;
22700 }
22701 return Ok(expr);
22702 }
22703 }
22704
22705 /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
22706 /// the first token that is not one. Schema qualifiers collapse to the
22707 /// last part, which is what every other name path here does (SPG is
22708 /// single-schema).
22709 fn take_comma_separated_names(&mut self) -> Vec<String> {
22710 let mut out = Vec::new();
22711 while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
22712 self.advance();
22713 let mut last = n;
22714 while matches!(self.peek(), Token::Dot) {
22715 self.advance();
22716 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
22717 last = t;
22718 }
22719 }
22720 out.push(last);
22721 if matches!(self.peek(), Token::Comma) {
22722 self.advance();
22723 } else {
22724 break;
22725 }
22726 }
22727 out
22728 }
22729
22730 /// v7.39 (round 694) — is the next token pair a postfix `[]`?
22731 ///
22732 /// The general cast-target path tests this inline; the types with their
22733 /// own `CastTarget` variant need it as a guard on their match arm,
22734 /// which is what this exists for.
22735 fn peek_postfix_array_brackets(&self) -> bool {
22736 matches!(self.peek(), Token::LBracket)
22737 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22738 }
22739
22740 /// Parse the operator tail after a `(a, b, …)` row constructor
22741 /// and expand at parse time. `=` is the conjunction of element
22742 /// equalities; `<>` its negation; the order operators expand
22743 /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
22744 /// equalities. Anything else (a bare row value, a subquery
22745 /// RHS) errors honestly — SPG has no composite runtime value.
22746 fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
22747 fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
22748 let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
22749 lhs: Box::new(l.clone()),
22750 op: BinOp::Eq,
22751 rhs: Box::new(r.clone()),
22752 });
22753 let first = it.next().expect("row has at least two elements");
22754 it.fold(first, |acc, e| Expr::Binary {
22755 lhs: Box::new(acc),
22756 op: BinOp::And,
22757 rhs: Box::new(e),
22758 })
22759 }
22760 // Lexicographic (a,b) OP (c,d):
22761 // a STRICT c OR (a = c AND (b OP d)) — recursing right.
22762 fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
22763 if lhs.len() == 1 {
22764 return Expr::Binary {
22765 lhs: Box::new(lhs[0].clone()),
22766 op: last,
22767 rhs: Box::new(rhs[0].clone()),
22768 };
22769 }
22770 let head_strict = Expr::Binary {
22771 lhs: Box::new(lhs[0].clone()),
22772 op: strict,
22773 rhs: Box::new(rhs[0].clone()),
22774 };
22775 let head_eq = Expr::Binary {
22776 lhs: Box::new(lhs[0].clone()),
22777 op: BinOp::Eq,
22778 rhs: Box::new(rhs[0].clone()),
22779 };
22780 Expr::Binary {
22781 lhs: Box::new(head_strict),
22782 op: BinOp::Or,
22783 rhs: Box::new(Expr::Binary {
22784 lhs: Box::new(head_eq),
22785 op: BinOp::And,
22786 rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
22787 }),
22788 }
22789 }
22790 let negated_in = if matches!(self.peek(), Token::Not)
22791 && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
22792 {
22793 self.advance();
22794 true
22795 } else {
22796 false
22797 };
22798 if matches!(self.peek(), Token::In) {
22799 self.advance();
22800 if !matches!(self.peek(), Token::LParen) {
22801 return Err(self.err(alloc::format!(
22802 "expected '(' after row IN, got {:?}",
22803 self.peek()
22804 )));
22805 }
22806 self.advance();
22807 // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
22808 // not a list of literal rows. Row-vs-list decomposes to
22809 // OR-of-AND above, but the subquery's rows are only known at
22810 // runtime, so keep it as a RowInSubquery node.
22811 if matches!(self.peek(), Token::Select) {
22812 let inner = self.parse_select_stmt()?;
22813 if !matches!(self.peek(), Token::RParen) {
22814 return Err(self.err(alloc::format!(
22815 "expected ')' after row IN-subquery, got {:?}",
22816 self.peek()
22817 )));
22818 }
22819 self.advance();
22820 let Statement::Select(s) = inner else {
22821 unreachable!("parse_select_stmt always returns Statement::Select")
22822 };
22823 return Ok(Expr::RowInSubquery {
22824 row,
22825 subquery: Box::new(s),
22826 negated: negated_in,
22827 });
22828 }
22829 let mut alternatives: Vec<Expr> = Vec::new();
22830 loop {
22831 // Optional ROW keyword before the paren row.
22832 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22833 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22834 {
22835 self.advance();
22836 }
22837 if !matches!(self.peek(), Token::LParen) {
22838 return Err(self.err(alloc::format!(
22839 "expected '(' to open a row inside IN, got {:?}",
22840 self.peek()
22841 )));
22842 }
22843 self.advance();
22844 let mut rhs = alloc::vec![self.parse_expr(0)?];
22845 while matches!(self.peek(), Token::Comma) {
22846 self.advance();
22847 rhs.push(self.parse_expr(0)?);
22848 }
22849 if !matches!(self.peek(), Token::RParen) {
22850 return Err(self.err(alloc::format!(
22851 "expected ')' after row inside IN, got {:?}",
22852 self.peek()
22853 )));
22854 }
22855 self.advance();
22856 if rhs.len() != row.len() {
22857 return Err(self.err(alloc::format!(
22858 "row IN arity mismatch: left has {}, right has {}",
22859 row.len(),
22860 rhs.len()
22861 )));
22862 }
22863 alternatives.push(row_eq(&row, &rhs));
22864 if matches!(self.peek(), Token::Comma) {
22865 self.advance();
22866 continue;
22867 }
22868 break;
22869 }
22870 if !matches!(self.peek(), Token::RParen) {
22871 return Err(self.err(alloc::format!(
22872 "expected ')' to close row IN list, got {:?}",
22873 self.peek()
22874 )));
22875 }
22876 self.advance();
22877 let mut it = alternatives.into_iter();
22878 let first = it.next().expect("IN list has at least one row");
22879 let combined = it.fold(first, |acc, e| Expr::Binary {
22880 lhs: Box::new(acc),
22881 op: BinOp::Or,
22882 rhs: Box::new(e),
22883 });
22884 return Ok(maybe_not(combined, negated_in));
22885 }
22886 // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
22887 // two periods share at least one time point. Each pair is
22888 // normalised with least/greatest (PG accepts the endpoints
22889 // in either order), then lowered to the standard
22890 // `start1 < end2 AND start2 < end1` form.
22891 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
22892 if row.len() != 2 {
22893 return Err(self.err(alloc::format!(
22894 "OVERLAPS needs (start, end) pairs; left side has {} elements",
22895 row.len()
22896 )));
22897 }
22898 self.advance();
22899 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22900 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22901 {
22902 self.advance();
22903 }
22904 if !matches!(self.peek(), Token::LParen) {
22905 return Err(self.err(alloc::format!(
22906 "expected '(' after OVERLAPS, got {:?}",
22907 self.peek()
22908 )));
22909 }
22910 self.advance();
22911 let r0 = self.parse_expr(0)?;
22912 if !matches!(self.peek(), Token::Comma) {
22913 return Err(self.err(alloc::format!(
22914 "OVERLAPS needs (start, end) on the right, got {:?}",
22915 self.peek()
22916 )));
22917 }
22918 self.advance();
22919 let r1 = self.parse_expr(0)?;
22920 if !matches!(self.peek(), Token::RParen) {
22921 return Err(self.err(alloc::format!(
22922 "expected ')' after OVERLAPS pair, got {:?}",
22923 self.peek()
22924 )));
22925 }
22926 self.advance();
22927 let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
22928 name: String::from(name),
22929 args: alloc::vec![a.clone(), b.clone()],
22930 };
22931 let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
22932 lhs: Box::new(lhs),
22933 op: BinOp::Lt,
22934 rhs: Box::new(rhs),
22935 };
22936 return Ok(Expr::Binary {
22937 lhs: Box::new(lt(
22938 pair_fn("least", &row[0], &row[1]),
22939 pair_fn("greatest", &r0, &r1),
22940 )),
22941 op: BinOp::And,
22942 rhs: Box::new(lt(
22943 pair_fn("least", &r0, &r1),
22944 pair_fn("greatest", &row[0], &row[1]),
22945 )),
22946 });
22947 }
22948 // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
22949 // PG, `IS NULL` is true only when EVERY field is NULL, and
22950 // `IS NOT NULL` is true only when every field is non-NULL — the
22951 // latter is NOT the negation of the former (a mixed row is
22952 // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
22953 // which reproduces exactly that all-fields semantics.
22954 if matches!(self.peek(), Token::Is) {
22955 self.advance();
22956 let negated = if matches!(self.peek(), Token::Not) {
22957 self.advance();
22958 true
22959 } else {
22960 false
22961 };
22962 if !matches!(self.peek(), Token::Null) {
22963 return Err(self.err(alloc::format!(
22964 "expected NULL after row IS [NOT], got {:?}",
22965 self.peek()
22966 )));
22967 }
22968 self.advance();
22969 let mut it = row.iter().map(|e| Expr::IsNull {
22970 expr: Box::new(e.clone()),
22971 negated,
22972 });
22973 let first = it.next().expect("row has at least two elements");
22974 return Ok(it.fold(first, |acc, e| Expr::Binary {
22975 lhs: Box::new(acc),
22976 op: BinOp::And,
22977 rhs: Box::new(e),
22978 }));
22979 }
22980 let op = match self.peek() {
22981 Token::Eq => BinOp::Eq,
22982 Token::NotEq => BinOp::NotEq,
22983 Token::Lt => BinOp::Lt,
22984 Token::LtEq => BinOp::LtEq,
22985 Token::Gt => BinOp::Gt,
22986 Token::GtEq => BinOp::GtEq,
22987 // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
22988 // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
22989 // constructor value, identical to the `ROW(a, b, …)` keyword form:
22990 // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
22991 // (`::text`, `.field`) applies at the caller just as it does for the
22992 // ROW(...) node. All the comparison / predicate forms returned above.
22993 _ => {
22994 return Ok(Expr::FunctionCall {
22995 name: String::from("row"),
22996 args: row,
22997 });
22998 }
22999 };
23000 self.advance();
23001 // Optional ROW keyword before the paren row.
23002 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23003 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23004 {
23005 self.advance();
23006 }
23007 if !matches!(self.peek(), Token::LParen) {
23008 return Err(self.err(alloc::format!(
23009 "expected '(' to open the right-hand row, got {:?}",
23010 self.peek()
23011 )));
23012 }
23013 self.advance();
23014 // `(a, b) <op> (SELECT x, y)` — compare against a single-row
23015 // subquery. Kept as a node (the subquery's row is a runtime value);
23016 // the literal-RHS form below still decomposes at parse time.
23017 if matches!(self.peek(), Token::Select) {
23018 let inner = self.parse_select_stmt()?;
23019 if !matches!(self.peek(), Token::RParen) {
23020 return Err(self.err(alloc::format!(
23021 "expected ')' after row comparison subquery, got {:?}",
23022 self.peek()
23023 )));
23024 }
23025 self.advance();
23026 let Statement::Select(s) = inner else {
23027 unreachable!("parse_select_stmt always returns Statement::Select")
23028 };
23029 return Ok(Expr::RowCmpSubquery {
23030 row,
23031 op,
23032 subquery: Box::new(s),
23033 });
23034 }
23035 let mut rhs = alloc::vec![self.parse_expr(0)?];
23036 while matches!(self.peek(), Token::Comma) {
23037 self.advance();
23038 rhs.push(self.parse_expr(0)?);
23039 }
23040 if !matches!(self.peek(), Token::RParen) {
23041 return Err(self.err(alloc::format!(
23042 "expected ')' after right-hand row, got {:?}",
23043 self.peek()
23044 )));
23045 }
23046 self.advance();
23047 if rhs.len() != row.len() {
23048 // v7.39 (round 239) — PG's wording (42601).
23049 return Err(self.err("unequal number of entries in row expressions".to_string()));
23050 }
23051 Ok(match op {
23052 BinOp::Eq => row_eq(&row, &rhs),
23053 BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
23054 BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
23055 BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
23056 BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
23057 BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
23058 _ => unreachable!("op restricted above"),
23059 })
23060 }
23061
23062 /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
23063 /// escape character becomes the matcher's default backslash:
23064 /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
23065 /// → the char itself, and any pre-existing backslash escapes
23066 /// itself so it stays literal. Both operands must be string
23067 /// literals — a runtime pattern would need matcher support.
23068 fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
23069 let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
23070 (&pattern, &esc)
23071 else {
23072 return Err(
23073 "LIKE ... ESCAPE requires string-literal pattern and escape \
23074 (runtime escape characters are not supported yet)"
23075 .into(),
23076 );
23077 };
23078 // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
23079 // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
23080 // multi-character escape is an error.
23081 let esc_ch: Option<char> = {
23082 let mut ch_iter = e.chars();
23083 match (ch_iter.next(), ch_iter.next()) {
23084 (Some(c), None) => Some(c),
23085 (None, _) => None,
23086 (Some(_), Some(_)) => {
23087 return Err(alloc::format!(
23088 "ESCAPE must be a single character, got {e:?}"
23089 ));
23090 }
23091 }
23092 };
23093 let mut out = String::with_capacity(p.len() + 4);
23094 let mut chars = p.chars();
23095 while let Some(c) = chars.next() {
23096 if Some(c) == esc_ch {
23097 match chars.next() {
23098 // Escaped wildcard / escaped escape → keep the
23099 // next char literal via backslash.
23100 Some(next) => {
23101 out.push('\\');
23102 out.push(next);
23103 }
23104 None => {
23105 return Err("LIKE pattern ends with the escape character".into());
23106 }
23107 }
23108 } else if c == '\\' && esc_ch != Some('\\') {
23109 // A raw backslash is literal under a custom (or absent) escape
23110 // — escape it for the backslash-based matcher.
23111 out.push('\\');
23112 out.push('\\');
23113 } else {
23114 out.push(c);
23115 }
23116 }
23117 Ok(Expr::Literal(Literal::String(out)))
23118 }
23119
23120 /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
23121 /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
23122 /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
23123 /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
23124 /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
23125 /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
23126 /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
23127 /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
23128 /// array expression errors honestly rather than silently mismatching.
23129 fn try_like_any_all(
23130 &mut self,
23131 base: &Expr,
23132 negated: bool,
23133 case_insensitive: bool,
23134 ) -> Result<Option<Expr>, ParseError> {
23135 let is_any = match self.peek() {
23136 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
23137 Token::Ident(s)
23138 if s.eq_ignore_ascii_case("any")
23139 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
23140 {
23141 true
23142 }
23143 _ => return Ok(None),
23144 };
23145 self.advance(); // ANY / ALL
23146 self.advance(); // '('
23147 let arr = self.parse_expr(0)?;
23148 if !matches!(self.peek(), Token::RParen) {
23149 return Err(self.err(format!(
23150 "expected ')' after LIKE {} argument, got {:?}",
23151 if is_any { "ANY" } else { "ALL" },
23152 self.peek()
23153 )));
23154 }
23155 self.advance(); // ')'
23156 let Expr::Array(items) = arr else {
23157 return Err(self.err(
23158 "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
23159 ));
23160 };
23161 let mut clauses = items.into_iter().map(|p| Expr::Like {
23162 expr: Box::new(base.clone()),
23163 pattern: Box::new(p),
23164 negated,
23165 case_insensitive,
23166 });
23167 let Some(first) = clauses.next() else {
23168 // ANY(empty) = FALSE, ALL(empty) = TRUE.
23169 return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
23170 };
23171 let op = if is_any { BinOp::Or } else { BinOp::And };
23172 let combined = clauses.fold(first, |acc, c| Expr::Binary {
23173 lhs: Box::new(acc),
23174 op,
23175 rhs: Box::new(c),
23176 });
23177 Ok(Some(combined))
23178 }
23179
23180 /// `x BETWEEN low AND high` → `(x >= low) AND (x <= high)`, wrapped in
23181 /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
23182 /// `AND` is not swallowed.
23183 fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23184 self.advance(); // BETWEEN
23185 // SYMMETRIC — the bounds may arrive in either order; both
23186 // orientations OR together. ASYMMETRIC is the default and
23187 // absorbs as noise.
23188 let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
23189 {
23190 self.advance();
23191 true
23192 } else {
23193 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
23194 self.advance();
23195 }
23196 false
23197 };
23198 let low = self.parse_expr(6)?;
23199 if !matches!(self.peek(), Token::And) {
23200 return Err(self.err(format!(
23201 "expected AND after BETWEEN low bound, got {:?}",
23202 self.peek()
23203 )));
23204 }
23205 self.advance();
23206 let high = self.parse_expr(6)?;
23207 let target = Box::new(expr);
23208 let range = |lo: Expr, hi: Expr| Expr::Binary {
23209 lhs: Box::new(Expr::Binary {
23210 lhs: target.clone(),
23211 op: BinOp::GtEq,
23212 rhs: Box::new(lo),
23213 }),
23214 op: BinOp::And,
23215 rhs: Box::new(Expr::Binary {
23216 lhs: target.clone(),
23217 op: BinOp::LtEq,
23218 rhs: Box::new(hi),
23219 }),
23220 };
23221 let combined = if symmetric {
23222 Expr::Binary {
23223 lhs: Box::new(range(low.clone(), high.clone())),
23224 op: BinOp::Or,
23225 rhs: Box::new(range(high, low)),
23226 }
23227 } else {
23228 range(low, high)
23229 };
23230 Ok(maybe_not(combined, negated))
23231 }
23232
23233 /// `x IN (a, b, c)` → chained OR of equalities. Empty list collapses
23234 /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
23235 /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
23236 /// Caller already consumed the leading `WITH` ident.
23237 /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
23238 /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
23239 /// self-reference that appears more than once in a single term.
23240 fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
23241 use crate::ast::{CteBody, SelectStatement};
23242 if !cte.recursive {
23243 return Ok(());
23244 }
23245 let CteBody::Select(body) = &cte.body else {
23246 return Ok(());
23247 };
23248 // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
23249 // check the anchor and every peer term.
23250 let has_order = |s: &SelectStatement| !s.order_by.is_empty();
23251 let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
23252 if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
23253 return Err(self.err(String::from(
23254 "ORDER BY in a recursive query is not implemented",
23255 )));
23256 }
23257 if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
23258 return Err(self.err(String::from(
23259 "LIMIT in a recursive query is not implemented",
23260 )));
23261 }
23262 let self_refs = |s: &SelectStatement| -> usize {
23263 let Some(from) = &s.from else {
23264 return 0;
23265 };
23266 let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
23267 for j in &from.joins {
23268 if j.table.name.eq_ignore_ascii_case(&cte.name) {
23269 n += 1;
23270 }
23271 }
23272 n
23273 };
23274 if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
23275 return Err(self.err(alloc::format!(
23276 "recursive reference to query \"{}\" must not appear more than once",
23277 cte.name
23278 )));
23279 }
23280 // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
23281 // apply only when the body actually references itself (a non-self-
23282 // referencing CTE under WITH RECURSIVE may use any set-op shape).
23283 let anchor_refs = self_refs(body);
23284 let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
23285 if anchor_refs > 0 || union_refs {
23286 // Shape: the top level must be UNION [ALL] arms only. A self-ref
23287 // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
23288 // "does not have the form" error — SPG used to compute a value.
23289 if body.unions.is_empty()
23290 || body.unions.iter().any(|(k, _)| {
23291 !matches!(
23292 k,
23293 crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
23294 )
23295 })
23296 {
23297 return Err(self.err(alloc::format!(
23298 "recursive query \"{}\" does not have the form non-recursive-term \
23299 UNION [ALL] recursive-term",
23300 cte.name
23301 )));
23302 }
23303 if anchor_refs > 0 {
23304 return Err(self.err(alloc::format!(
23305 "recursive reference to query \"{}\" must not appear within its non-recursive term",
23306 cte.name
23307 )));
23308 }
23309 }
23310 let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23311 for (_, u) in &body.unions {
23312 if self_refs(u) == 0 {
23313 continue;
23314 }
23315 // The self-reference must not sit on the nullable side of an outer
23316 // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23317 if let Some(from) = &u.from {
23318 for (i, j) in from.joins.iter().enumerate() {
23319 let left_has_self = is_self(&from.primary)
23320 || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23321 let violated = match j.kind {
23322 crate::ast::JoinKind::Left => is_self(&j.table),
23323 crate::ast::JoinKind::Right => left_has_self,
23324 crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23325 _ => false,
23326 };
23327 if violated {
23328 return Err(self.err(alloc::format!(
23329 "recursive reference to query \"{}\" must not appear within an outer join",
23330 cte.name
23331 )));
23332 }
23333 }
23334 }
23335 // No aggregates at the top level of the recursive term (SPG used
23336 // to run them and surface a misleading downstream error).
23337 let mut items_and_having: Vec<&Expr> = Vec::new();
23338 for it in &u.items {
23339 if let crate::ast::SelectItem::Expr { expr, .. } = it {
23340 items_and_having.push(expr);
23341 }
23342 }
23343 if let Some(h) = &u.having {
23344 items_and_having.push(h);
23345 }
23346 for e in items_and_having {
23347 if expr_has_toplevel_aggregate(e) {
23348 return Err(self.err(String::from(
23349 "aggregate functions are not allowed in a recursive query's recursive term",
23350 )));
23351 }
23352 }
23353 }
23354 // A self-reference inside a sublink expression (EXISTS / IN / scalar
23355 // subquery) anywhere in the body is rejected; a plain FROM derived
23356 // table is legal in PG and untouched here.
23357 let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23358 all_terms.extend(body.unions.iter().map(|(_, u)| u));
23359 for term in all_terms {
23360 if select_has_self_ref_in_sublink(term, &cte.name) {
23361 return Err(self.err(alloc::format!(
23362 "recursive reference to query \"{}\" must not appear within a subquery",
23363 cte.name
23364 )));
23365 }
23366 }
23367 Ok(())
23368 }
23369
23370 /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23371 /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23372 /// right after parse so the engine sees a plain recursive CTE with the
23373 /// tracking columns already projected. DEPTH FIRST and CYCLE are
23374 /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23375 /// text-rendered rows can't provide, and errors honestly.
23376 fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23377 use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23378 if cte.search.is_none() && cte.cycle.is_none() {
23379 return Ok(());
23380 }
23381 let cte_name = cte.name.clone();
23382 let col_names = cte.column_overrides.clone();
23383 if col_names.is_empty() {
23384 return Err(
23385 self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23386 );
23387 }
23388 let search = cte.search.take();
23389 let cycle = cte.cycle.take();
23390 let mut extra_cols: Vec<String> = Vec::new();
23391 let col_ref = |name: &str| {
23392 Expr::Column(ColumnName {
23393 qualifier: Some(cte_name.clone()),
23394 name: name.to_string(),
23395 })
23396 };
23397 // Position of a SEARCH/CYCLE column within the CTE's column list.
23398 let pos_of = |name: &str| -> Result<usize, ParseError> {
23399 col_names
23400 .iter()
23401 .position(|c| c.eq_ignore_ascii_case(name))
23402 .ok_or_else(|| {
23403 self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23404 })
23405 };
23406 let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23407 let mut args = Vec::with_capacity(positions.len());
23408 for &p in positions {
23409 match items.get(p) {
23410 Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23411 _ => {
23412 return Err(self.err(
23413 "SEARCH/CYCLE column maps to a non-expression select item".into(),
23414 ));
23415 }
23416 }
23417 }
23418 Ok(Expr::FunctionCall {
23419 name: "row".into(),
23420 args,
23421 })
23422 };
23423 let CteBody::Select(body) = &mut cte.body else {
23424 return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23425 };
23426 if body.unions.is_empty() {
23427 return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23428 }
23429 let rec = body.unions.len() - 1; // recursive term = last UNION peer
23430
23431 if let Some(srch) = search {
23432 // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23433 // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23434 // no typed `record[]`, but element-wise array ORDER BY is correct
23435 // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23436 // exactly onto a typed array: DEPTH is the root→node path
23437 // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23438 // orders numerically (multi-digit keys included), matching PG.
23439 //
23440 // A multi-column BY would need a record[] to keep the per-node key
23441 // tuple orderable, which SPG can't express — error honestly there
23442 // rather than mis-order.
23443 if srch.by_columns.len() != 1 {
23444 return Err(self.err(
23445 "SEARCH … BY with multiple columns needs typed record[] ordering \
23446 SPG doesn't have yet; a single BY column is supported"
23447 .into(),
23448 ));
23449 }
23450 let key_pos = pos_of(&srch.by_columns[0])?;
23451 let base_key = match body.items.get(key_pos) {
23452 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23453 _ => {
23454 return Err(
23455 self.err("SEARCH BY column maps to a non-expression select item".into())
23456 );
23457 }
23458 };
23459 let rec_key = match body.unions[rec].1.items.get(key_pos) {
23460 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23461 _ => {
23462 return Err(
23463 self.err("SEARCH BY column maps to a non-expression select item".into())
23464 );
23465 }
23466 };
23467 if srch.depth_first {
23468 // base: ARRAY[key]; rec: array_append(cte.set, key).
23469 body.items.push(SelectItem::Expr {
23470 expr: Expr::Array(alloc::vec![base_key]),
23471 alias: Some(srch.set_column.clone()),
23472 });
23473 body.unions[rec].1.items.push(SelectItem::Expr {
23474 expr: Expr::FunctionCall {
23475 name: "array_append".into(),
23476 args: alloc::vec![col_ref(&srch.set_column), rec_key],
23477 },
23478 alias: Some(srch.set_column.clone()),
23479 });
23480 } else {
23481 // BREADTH: [depth, key]; depth starts at 0 and increments. The
23482 // leading depth element dominates the element-wise comparison,
23483 // so shallower rows sort first, then by key — PG's (depth, key).
23484 body.items.push(SelectItem::Expr {
23485 expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
23486 alias: Some(srch.set_column.clone()),
23487 });
23488 // rec depth = cte.set[1] + 1.
23489 let parent_depth = Expr::ArraySubscript {
23490 target: Box::new(col_ref(&srch.set_column)),
23491 index: Box::new(Expr::Literal(Literal::Integer(1))),
23492 };
23493 body.unions[rec].1.items.push(SelectItem::Expr {
23494 expr: Expr::Array(alloc::vec![
23495 Expr::Binary {
23496 lhs: Box::new(parent_depth),
23497 op: BinOp::Add,
23498 rhs: Box::new(Expr::Literal(Literal::Integer(1))),
23499 },
23500 rec_key,
23501 ]),
23502 alias: Some(srch.set_column.clone()),
23503 });
23504 }
23505 extra_cols.push(srch.set_column);
23506 }
23507
23508 if let Some(cyc) = cycle {
23509 let positions: Vec<usize> = cyc
23510 .columns
23511 .iter()
23512 .map(|c| pos_of(c))
23513 .collect::<Result<_, _>>()?;
23514 // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
23515 // cast it to text for the cycle path: membership only needs equality,
23516 // and the record text form gives SPG a TextArray path (SPG has no
23517 // typed record[] array). Cycle detection is unaffected.
23518 let base_row = Expr::Cast {
23519 expr: Box::new(row_of(&body.items, &positions)?),
23520 target: CastTarget::Text,
23521 };
23522 let rec_row = Expr::Cast {
23523 expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
23524 target: CastTarget::Text,
23525 };
23526 let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
23527 let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
23528 // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
23529 body.items.push(SelectItem::Expr {
23530 expr: Expr::Literal(dflt.clone()),
23531 alias: Some(cyc.mark_column.clone()),
23532 });
23533 body.items.push(SelectItem::Expr {
23534 expr: Expr::Array(alloc::vec![base_row]),
23535 alias: Some(cyc.path_column.clone()),
23536 });
23537 // rec mark: ROW(cols) already in the path → cycle.
23538 let hit = Expr::AnyAll {
23539 expr: Box::new(rec_row.clone()),
23540 op: BinOp::Eq,
23541 array: Box::new(col_ref(&cyc.path_column)),
23542 is_any: true,
23543 };
23544 let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
23545 Expr::Case {
23546 operand: None,
23547 branches: alloc::vec![(hit, Expr::Literal(mark))],
23548 else_branch: Some(Box::new(Expr::Literal(dflt))),
23549 }
23550 } else {
23551 hit
23552 };
23553 body.unions[rec].1.items.push(SelectItem::Expr {
23554 expr: mark_expr,
23555 alias: Some(cyc.mark_column.clone()),
23556 });
23557 // rec path: array_append(cte.path, ROW(cols)).
23558 body.unions[rec].1.items.push(SelectItem::Expr {
23559 expr: Expr::FunctionCall {
23560 name: "array_append".into(),
23561 args: alloc::vec![col_ref(&cyc.path_column), rec_row],
23562 },
23563 alias: Some(cyc.path_column.clone()),
23564 });
23565 // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
23566 let stop = Expr::Unary {
23567 op: UnOp::Not,
23568 expr: Box::new(col_ref(&cyc.mark_column)),
23569 };
23570 let w = &mut body.unions[rec].1.where_;
23571 *w = Some(match w.take() {
23572 Some(prev) => Expr::Binary {
23573 lhs: Box::new(prev),
23574 op: BinOp::And,
23575 rhs: Box::new(stop),
23576 },
23577 None => stop,
23578 });
23579 extra_cols.push(cyc.mark_column);
23580 extra_cols.push(cyc.path_column);
23581 }
23582 cte.column_overrides.extend(extra_cols);
23583 Ok(())
23584 }
23585
23586 /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
23587 /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
23588 fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
23589 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
23590 return Ok(None);
23591 }
23592 self.advance(); // SEARCH
23593 let depth_first = match self.peek() {
23594 Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
23595 Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
23596 other => {
23597 return Err(self.err(format!(
23598 "expected DEPTH or BREADTH after SEARCH, got {other:?}"
23599 )));
23600 }
23601 };
23602 self.advance();
23603 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
23604 return Err(self.err(format!(
23605 "expected FIRST after SEARCH mode, got {:?}",
23606 self.peek()
23607 )));
23608 }
23609 self.advance();
23610 if !self.peek_is_by() {
23611 return Err(self.err(format!(
23612 "expected BY after SEARCH … FIRST, got {:?}",
23613 self.peek()
23614 )));
23615 }
23616 self.advance();
23617 let mut by_columns = alloc::vec![self.expect_ident_like()?];
23618 while matches!(self.peek(), Token::Comma) {
23619 self.advance();
23620 by_columns.push(self.expect_ident_like()?);
23621 }
23622 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23623 return Err(self.err(format!(
23624 "expected SET in SEARCH clause, got {:?}",
23625 self.peek()
23626 )));
23627 }
23628 self.advance();
23629 let set_column = self.expect_ident_like()?;
23630 Ok(Some(crate::ast::SearchClause {
23631 depth_first,
23632 by_columns,
23633 set_column,
23634 }))
23635 }
23636
23637 /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
23638 /// USING pathcol`. Returns None when the next token isn't CYCLE.
23639 fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
23640 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
23641 return Ok(None);
23642 }
23643 self.advance(); // CYCLE
23644 let mut columns = alloc::vec![self.expect_ident_like()?];
23645 while matches!(self.peek(), Token::Comma) {
23646 self.advance();
23647 columns.push(self.expect_ident_like()?);
23648 }
23649 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23650 return Err(self.err(format!(
23651 "expected SET in CYCLE clause, got {:?}",
23652 self.peek()
23653 )));
23654 }
23655 self.advance();
23656 let mark_column = self.expect_ident_like()?;
23657 let mut mark_value = None;
23658 let mut default_value = None;
23659 if matches!(self.peek(), Token::To) {
23660 self.advance();
23661 mark_value = Some(self.parse_cycle_literal()?);
23662 if !matches!(self.peek(), Token::Default) {
23663 return Err(self.err(format!(
23664 "expected DEFAULT after CYCLE … TO, got {:?}",
23665 self.peek()
23666 )));
23667 }
23668 self.advance();
23669 default_value = Some(self.parse_cycle_literal()?);
23670 }
23671 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
23672 return Err(self.err(format!(
23673 "expected USING in CYCLE clause, got {:?}",
23674 self.peek()
23675 )));
23676 }
23677 self.advance();
23678 let path_column = self.expect_ident_like()?;
23679 Ok(Some(crate::ast::CycleClause {
23680 columns,
23681 mark_column,
23682 mark_value,
23683 default_value,
23684 path_column,
23685 }))
23686 }
23687
23688 /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
23689 /// literal (string / bool / number) in PG.
23690 fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
23691 match self.parse_expr(0)? {
23692 Expr::Literal(l) => Ok(l),
23693 other => Err(self.err(format!(
23694 "CYCLE mark/default value must be a literal, got {other:?}"
23695 ))),
23696 }
23697 }
23698
23699 fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
23700 // v4.22: WITH RECURSIVE — optional keyword right after WITH.
23701 // Comes through as an identifier; consume it if present and
23702 // mark every CTE in the clause as recursive (PG semantics —
23703 // the flag is per-WITH, not per-CTE).
23704 let mut recursive = false;
23705 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
23706 && s.eq_ignore_ascii_case("recursive")
23707 {
23708 self.advance();
23709 recursive = true;
23710 }
23711 let mut ctes = Vec::new();
23712 loop {
23713 let name = self.expect_ident_like()?;
23714 // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
23715 // PG uses these to rename the body's output columns; we
23716 // do the same below by overriding `columns[i].name`.
23717 let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
23718 self.advance();
23719 let mut names = Vec::new();
23720 loop {
23721 names.push(self.expect_ident_like()?);
23722 if matches!(self.peek(), Token::Comma) {
23723 self.advance();
23724 continue;
23725 }
23726 break;
23727 }
23728 if !matches!(self.peek(), Token::RParen) {
23729 return Err(self.err(format!(
23730 "expected ')' to close CTE column list, got {:?}",
23731 self.peek()
23732 )));
23733 }
23734 self.advance();
23735 names
23736 } else {
23737 Vec::new()
23738 };
23739 // AS is a reserved Token::As (used by SELECT-item / FROM
23740 // aliasing) — handle it specially rather than as a bare
23741 // ident.
23742 if !matches!(self.peek(), Token::As) {
23743 return Err(self.err(format!(
23744 "expected AS after CTE name {name:?}, got {:?}",
23745 self.peek()
23746 )));
23747 }
23748 self.advance();
23749 // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
23750 // MATERIALIZED` optimizer hints. SPG materialises every
23751 // CTE, so both spellings are accepted and absorbed.
23752 if matches!(self.peek(), Token::Not) {
23753 self.advance(); // NOT
23754 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23755 if s.eq_ignore_ascii_case("materialized"))
23756 {
23757 self.advance();
23758 } else {
23759 return Err(self.err(format!(
23760 "expected MATERIALIZED after AS NOT, got {:?}",
23761 self.peek()
23762 )));
23763 }
23764 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23765 if s.eq_ignore_ascii_case("materialized"))
23766 {
23767 self.advance();
23768 }
23769 if !matches!(self.peek(), Token::LParen) {
23770 return Err(self.err(format!(
23771 "expected '(' after AS in WITH clause, got {:?}",
23772 self.peek()
23773 )));
23774 }
23775 self.advance();
23776 // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
23777 // RETURNING) as the CTE body in addition to SELECT.
23778 // PG writable CTE semantics. UPDATE / DELETE come in as
23779 // bare Idents (lexer keeps SELECT / INSERT as reserved
23780 // tokens but treats the rest of DML as case-insensitive
23781 // idents).
23782 let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23783 let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23784 let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23785 let body = match self.peek() {
23786 Token::Select => {
23787 let inner = self.parse_select_stmt()?;
23788 let Statement::Select(s) = inner else {
23789 unreachable!("parse_select_stmt returns Select");
23790 };
23791 crate::ast::CteBody::Select(s)
23792 }
23793 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23794 // `SELECT * FROM t` this way and accepts it wherever a
23795 // SELECT goes, so the CTE body dispatch needs its own
23796 // arm: this match is keyed on the FIRST token, and
23797 // `Token::Table` fell through to a tail that then
23798 // rejected what it got. `parse_table_shorthand` has
23799 // returned a desugared SelectStatement since the
23800 // shorthand landed — only the routing was missing.
23801 // Round 868 found this by putting the shorthand in a
23802 // subquery; every earlier check used a top-level form.
23803 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23804 // `SELECT * FROM t` this way and accepts it wherever a
23805 // SELECT goes, so the CTE body dispatch needs its own
23806 // arm: this match is keyed on the FIRST token, and
23807 // `Token::Table` fell through to a tail that rejected
23808 // what it got. `parse_table_shorthand` has returned a
23809 // desugared SelectStatement since the shorthand landed —
23810 // only the routing was missing, here and in the derived
23811 // table's second-token gate. Round 868 found both by
23812 // putting the shorthand in a subquery; every earlier
23813 // check had used a top-level form.
23814 Token::Table
23815 if matches!(
23816 self.tokens.get(self.pos + 1),
23817 Some(Token::Ident(_) | Token::QuotedIdent(_))
23818 ) =>
23819 {
23820 let mut head = self.parse_table_shorthand()?;
23821 self.parse_setop_chain_into(&mut head)?;
23822 self.parse_select_tail_into(&mut head)?;
23823 crate::ast::CteBody::Select(head)
23824 }
23825 // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
23826 // WITH t(a) AS (VALUES (1), (2)) … lowers through
23827 // the shared rows helper onto a Select body.
23828 Token::Values => {
23829 self.advance(); // VALUES
23830 let mut head = self.parse_values_rows_body()?;
23831 // A VALUES seed can head a set-operation chain —
23832 // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
23833 // SELECT n+1 FROM t …). Attach any trailing
23834 // UNION / INTERSECT / EXCEPT peers so the
23835 // recursive-CTE body parses like the SELECT seed.
23836 self.parse_setop_chain_into(&mut head)?;
23837 crate::ast::CteBody::Select(head)
23838 }
23839 Token::Insert => {
23840 let inner = self.parse_one_statement()?;
23841 let Statement::Insert(s) = inner else {
23842 unreachable!("Token::Insert routes to Insert");
23843 };
23844 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23845 }
23846 _ if is_update_kw => {
23847 let inner = self.parse_one_statement()?;
23848 let Statement::Update(s) = inner else {
23849 return Err(
23850 self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
23851 );
23852 };
23853 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23854 }
23855 _ if is_delete_kw => {
23856 let inner = self.parse_one_statement()?;
23857 let Statement::Delete(s) = inner else {
23858 return Err(
23859 self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
23860 );
23861 };
23862 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23863 }
23864 // v7.39 (round 149) — PG 17 allows MERGE as a
23865 // data-modifying CTE body.
23866 _ if is_merge_kw => {
23867 let inner = self.parse_one_statement()?;
23868 let Statement::Merge(s) = inner else {
23869 return Err(
23870 self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
23871 );
23872 };
23873 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23874 }
23875 // v7.39 (round 151) — a CTE body may itself be
23876 // WITH-headed (PG grammar: PreparableStmt carries its
23877 // own with_clause). The nested statement keeps its own
23878 // ctes; the modifying-CTE-at-top-level rule is enforced
23879 // at execution.
23880 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
23881 self.advance(); // WITH
23882 match self.parse_with_cte_then_select()? {
23883 Statement::Select(s) => crate::ast::CteBody::Select(s),
23884 Statement::Insert(s) => {
23885 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23886 }
23887 Statement::Update(s) => {
23888 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23889 }
23890 Statement::Delete(s) => {
23891 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23892 }
23893 Statement::Merge(s) => {
23894 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23895 }
23896
23897 other => {
23898 return Err(self.err(format!(
23899 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23900 )));
23901 }
23902 }
23903 }
23904 other => {
23905 return Err(self.err(format!(
23906 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23907 )));
23908 }
23909 };
23910 if !matches!(self.peek(), Token::RParen) {
23911 return Err(self.err(format!(
23912 "expected ')' after CTE body, got {:?}",
23913 self.peek()
23914 )));
23915 }
23916 self.advance();
23917 // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
23918 // CTE, desugared into extra body columns by the engine.
23919 let search = self.parse_cte_search_clause()?;
23920 let cycle = self.parse_cte_cycle_clause()?;
23921 let mut cte = crate::ast::Cte {
23922 name,
23923 body,
23924 recursive,
23925 column_overrides,
23926 search,
23927 cycle,
23928 };
23929 self.validate_recursive_cte(&cte)?;
23930 self.desugar_cte_search_cycle(&mut cte)?;
23931 ctes.push(cte);
23932 if matches!(self.peek(), Token::Comma) {
23933 self.advance();
23934 continue;
23935 }
23936 break;
23937 }
23938 // v7.37.43-T4.4 — the outer body may be SELECT (classical),
23939 // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
23940 // the parsed CTEs to whichever statement the body produces.
23941 let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23942 let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23943 let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23944 match self.peek() {
23945 Token::Select => {
23946 let body_stmt = self.parse_select_stmt()?;
23947 let Statement::Select(mut body) = body_stmt else {
23948 unreachable!()
23949 };
23950 body.ctes = ctes;
23951 Ok(Statement::Select(body))
23952 }
23953 Token::Insert => {
23954 let body_stmt = self.parse_one_statement()?;
23955 let Statement::Insert(mut body) = body_stmt else {
23956 unreachable!()
23957 };
23958 body.ctes = ctes;
23959 Ok(Statement::Insert(body))
23960 }
23961 _ if outer_is_update => {
23962 let body_stmt = self.parse_one_statement()?;
23963 let Statement::Update(mut body) = body_stmt else {
23964 return Err(self.err(format!("expected UPDATE after WITH clause")));
23965 };
23966 body.ctes = ctes;
23967 Ok(Statement::Update(body))
23968 }
23969 _ if outer_is_delete => {
23970 let body_stmt = self.parse_one_statement()?;
23971 let Statement::Delete(mut body) = body_stmt else {
23972 return Err(self.err(format!("expected DELETE after WITH clause")));
23973 };
23974 body.ctes = ctes;
23975 Ok(Statement::Delete(body))
23976 }
23977 // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
23978 // WITH RECURSIVE is rejected with PG's exact message
23979 // (parse analysis, transformWithClause).
23980 _ if outer_is_merge => {
23981 if recursive {
23982 return Err(self.err(String::from(
23983 "WITH RECURSIVE is not supported for MERGE statement",
23984 )));
23985 }
23986 let body_stmt = self.parse_one_statement()?;
23987 let Statement::Merge(mut body) = body_stmt else {
23988 return Err(self.err(format!("expected MERGE after WITH clause")));
23989 };
23990 body.ctes = ctes;
23991 Ok(Statement::Merge(body))
23992 }
23993 other => Err(self.err(format!(
23994 "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
23995 ))),
23996 }
23997 }
23998
23999 /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
24000 /// already consumed the leading `EXISTS` ident via
24001 /// `self.advance()`.
24002 /// v7.13.0 — parse the rest of a `CASE … END` expression after
24003 /// the leading `CASE` ident has been consumed (mailrs round-5
24004 /// G9). Supports both the searched form
24005 /// (`CASE WHEN cond THEN val …`) and the simple form
24006 /// (`CASE operand WHEN val THEN val …`).
24007 fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
24008 // Disambiguate searched vs simple form: if the next token
24009 // is `WHEN`, we're in the searched form. Otherwise the
24010 // intervening expression is the operand.
24011 let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
24012 None
24013 } else {
24014 Some(Box::new(self.parse_expr(0)?))
24015 };
24016 let mut branches: Vec<(Expr, Expr)> = Vec::new();
24017 loop {
24018 match self.peek() {
24019 Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
24020 self.advance();
24021 let cond = self.parse_expr(0)?;
24022 match self.peek() {
24023 Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
24024 self.advance();
24025 }
24026 other => {
24027 return Err(self.err(alloc::format!(
24028 "expected THEN after CASE WHEN <expr>, got {other:?}"
24029 )));
24030 }
24031 }
24032 let value = self.parse_expr(0)?;
24033 branches.push((cond, value));
24034 }
24035 _ => break,
24036 }
24037 }
24038 if branches.is_empty() {
24039 return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
24040 }
24041 let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
24042 {
24043 self.advance();
24044 Some(Box::new(self.parse_expr(0)?))
24045 } else {
24046 None
24047 };
24048 match self.peek() {
24049 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
24050 self.advance();
24051 }
24052 other => {
24053 return Err(self.err(alloc::format!(
24054 "expected END to close CASE expression, got {other:?}"
24055 )));
24056 }
24057 }
24058 Ok(Expr::Case {
24059 operand,
24060 branches,
24061 else_branch,
24062 })
24063 }
24064
24065 /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
24066 /// query-source position (EXISTS / IN / INSERT source / CTE body /
24067 /// view body). Caller consumed the WITH keyword. Only a SELECT
24068 /// outer is grammatical here; the data-modifying-CTE-at-top-level
24069 /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
24070 /// maps correctly.
24071 fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24072 let inner = self.parse_with_cte_then_select()?;
24073 match inner {
24074 Statement::Select(s) => Ok(s),
24075 other => Err(self.err(format!(
24076 "expected SELECT after WITH in a subquery, got {other:?}"
24077 ))),
24078 }
24079 }
24080
24081 /// True when the next token is the (unquoted) WITH keyword. WITH is
24082 /// reserved in PG, so a bare `with` can never be a column reference
24083 /// in these positions; a quoted `"with"` stays an identifier.
24084 fn peek_is_with_kw(&self) -> bool {
24085 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
24086 }
24087
24088 /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
24089 /// `#[inline(never)]` keeps the large SelectStatement temporaries
24090 /// off parse_expr's recursive frame (the nesting-budget stack
24091 /// cliff — see the round-153 gate regression).
24092 #[inline(never)]
24093 fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24094 if self.peek_is_with_kw() {
24095 self.advance();
24096 self.parse_nested_with_select()
24097 } else {
24098 match self.parse_select_stmt()? {
24099 Statement::Select(s) => Ok(s),
24100 other => Err(self.err(alloc::format!(
24101 "expected SELECT inside ANY/ALL, got {other:?}"
24102 ))),
24103 }
24104 }
24105 }
24106
24107 fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
24108 if !matches!(self.peek(), Token::LParen) {
24109 return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
24110 }
24111 self.advance();
24112 // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
24113 let s = if self.peek_is_with_kw() {
24114 self.advance();
24115 self.parse_nested_with_select()?
24116 } else {
24117 let inner = self.parse_select_stmt()?;
24118 let Statement::Select(s) = inner else {
24119 unreachable!("parse_select_stmt returns Select")
24120 };
24121 s
24122 };
24123 if !matches!(self.peek(), Token::RParen) {
24124 return Err(self.err(format!(
24125 "expected ')' after EXISTS-subquery, got {:?}",
24126 self.peek()
24127 )));
24128 }
24129 self.advance();
24130 Ok(Expr::Exists {
24131 subquery: Box::new(s),
24132 negated,
24133 })
24134 }
24135
24136 fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
24137 self.advance(); // IN
24138 if !matches!(self.peek(), Token::LParen) {
24139 return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
24140 }
24141 self.advance();
24142 // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
24143 // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
24144 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
24145 let s = if self.peek_is_with_kw() {
24146 self.advance();
24147 self.parse_nested_with_select()?
24148 } else {
24149 let inner = self.parse_select_stmt()?;
24150 let Statement::Select(s) = inner else {
24151 unreachable!("parse_select_stmt always returns Statement::Select")
24152 };
24153 s
24154 };
24155 if !matches!(self.peek(), Token::RParen) {
24156 return Err(self.err(format!(
24157 "expected ')' after IN-subquery, got {:?}",
24158 self.peek()
24159 )));
24160 }
24161 self.advance();
24162 return Ok(Expr::InSubquery {
24163 expr: Box::new(expr),
24164 subquery: Box::new(s),
24165 negated,
24166 });
24167 }
24168 let mut elements = Vec::new();
24169 if !matches!(self.peek(), Token::RParen) {
24170 loop {
24171 elements.push(self.parse_expr(0)?);
24172 match self.peek() {
24173 Token::Comma => {
24174 self.advance();
24175 }
24176 Token::RParen => break,
24177 other => {
24178 return Err(
24179 self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
24180 );
24181 }
24182 }
24183 }
24184 }
24185 self.advance(); // ')'
24186 // v7.30.2 (mailrs round-25) — flat InList node instead of a
24187 // left-deep OR-Eq chain: chain depth scaled with the element
24188 // count and overflowed the stack (eval + drop are recursive).
24189 if elements.is_empty() {
24190 return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
24191 }
24192 Ok(Expr::InList {
24193 expr: Box::new(expr),
24194 list: elements,
24195 negated,
24196 })
24197 }
24198
24199 /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
24200 /// already consumed by the caller. Elements must be numeric literals
24201 /// (with optional unary `-`); any compound expression is rejected at
24202 /// parse time so the runtime never needs to evaluate inside a vector.
24203 /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
24204 /// has already consumed the `EXTRACT` token before calling us —
24205 /// we pick up at the opening `(`.
24206 /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
24207 /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
24208 /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
24209 /// per-column OR-fold of
24210 /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
24211 /// term)` so the existing FTS evaluator handles semantics.
24212 ///
24213 /// The mode modifier is accepted-and-ignored at v7.17 — all
24214 /// modes map to the same `plainto_tsquery` rewrite. Boolean-
24215 /// mode operators (`+foo -bar`) would need their own parser
24216 /// (Phase 2.2c); customers who hit them today already get a
24217 /// correct lexeme-match against the bare term, only without
24218 /// the +/- precedence the customer asked for.
24219 fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
24220 // Already at `MATCH`-consumed position; the dispatcher
24221 // confirmed the next token is `(`.
24222 if !matches!(self.peek(), Token::LParen) {
24223 return Err(self.err(alloc::format!(
24224 "expected '(' after MATCH, got {:?}",
24225 self.peek()
24226 )));
24227 }
24228 self.advance();
24229 let mut cols: Vec<Expr> = Vec::new();
24230 loop {
24231 cols.push(self.parse_expr(0)?);
24232 match self.peek() {
24233 Token::Comma => {
24234 self.advance();
24235 }
24236 Token::RParen => break,
24237 other => {
24238 return Err(self.err(alloc::format!(
24239 "expected ',' or ')' in MATCH column list, got {other:?}"
24240 )));
24241 }
24242 }
24243 }
24244 self.advance(); // ')'
24245 // Expect AGAINST.
24246 match self.peek() {
24247 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
24248 self.advance();
24249 }
24250 other => {
24251 return Err(self.err(alloc::format!(
24252 "expected AGAINST after MATCH column list, got {other:?}"
24253 )));
24254 }
24255 }
24256 if !matches!(self.peek(), Token::LParen) {
24257 return Err(self.err(alloc::format!(
24258 "expected '(' after AGAINST, got {:?}",
24259 self.peek()
24260 )));
24261 }
24262 self.advance();
24263 // Read AGAINST's argument as a single primary token —
24264 // string literal, placeholder, or column-ref ident. We
24265 // can't call `parse_expr` / `parse_unary` here because
24266 // the postfix chain inside `parse_atom` would greedily
24267 // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
24268 // and fail at "expected '(' after IN". Customers always
24269 // write a literal or bound parameter in AGAINST, so this
24270 // restriction is non-blocking; the error path explains
24271 // the limit if a more complex expression shows up.
24272 let term = match self.advance() {
24273 Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
24274 Token::Placeholder(n) => Expr::Placeholder(n),
24275 Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
24276 qualifier: None,
24277 name: s,
24278 }),
24279 other => {
24280 return Err(self.err(alloc::format!(
24281 "MATCH ... AGAINST(<term>) expects a string literal, \
24282 bound parameter, or column ref, got {other:?}"
24283 )));
24284 }
24285 };
24286 // Optional mode tail — accept-and-ignore at v7.17:
24287 // IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
24288 // IN BOOLEAN MODE
24289 // WITH QUERY EXPANSION
24290 loop {
24291 match self.peek() {
24292 // IN lexes as a reserved Token::In, not an ident,
24293 // so it gets its own arm.
24294 Token::In => {
24295 self.advance();
24296 }
24297 Token::Ident(s) | Token::QuotedIdent(s)
24298 if s.eq_ignore_ascii_case("natural")
24299 || s.eq_ignore_ascii_case("language")
24300 || s.eq_ignore_ascii_case("boolean")
24301 || s.eq_ignore_ascii_case("mode")
24302 || s.eq_ignore_ascii_case("with")
24303 || s.eq_ignore_ascii_case("query")
24304 || s.eq_ignore_ascii_case("expansion") =>
24305 {
24306 self.advance();
24307 }
24308 _ => break,
24309 }
24310 }
24311 if !matches!(self.peek(), Token::RParen) {
24312 return Err(self.err(alloc::format!(
24313 "expected ')' to close AGAINST, got {:?}",
24314 self.peek()
24315 )));
24316 }
24317 self.advance();
24318 // Build per-column `to_tsvector('simple', col) @@
24319 // plainto_tsquery('simple', term)` and OR-fold.
24320 let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24321 let plainto = Expr::FunctionCall {
24322 name: String::from("plainto_tsquery"),
24323 args: alloc::vec![simple_lit(), term.clone()],
24324 };
24325 let mut folded: Option<Expr> = None;
24326 for col in cols {
24327 let to_tsv = Expr::FunctionCall {
24328 name: String::from("to_tsvector"),
24329 args: alloc::vec![simple_lit(), col],
24330 };
24331 let leaf = Expr::Binary {
24332 lhs: Box::new(to_tsv),
24333 op: crate::ast::BinOp::TsMatch,
24334 rhs: Box::new(plainto.clone()),
24335 };
24336 folded = Some(match folded {
24337 None => leaf,
24338 Some(prev) => Expr::Binary {
24339 lhs: Box::new(prev),
24340 op: crate::ast::BinOp::Or,
24341 rhs: Box::new(leaf),
24342 },
24343 });
24344 }
24345 match folded {
24346 Some(e) => Ok(e),
24347 None => Err(self.err(String::from(
24348 "MATCH(...) AGAINST(...) requires at least one column",
24349 ))),
24350 }
24351 }
24352
24353 fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24354 if !matches!(self.peek(), Token::LParen) {
24355 return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24356 }
24357 self.advance();
24358 let field_name = self.expect_ident_like()?;
24359 let field = match field_name.to_ascii_lowercase().as_str() {
24360 // PG accepts the plural spellings (years/months/…/millenniums) as
24361 // aliases for the singular fields — its datetime unit table has both.
24362 // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24363 "year" | "years" => ExtractField::Year,
24364 "month" | "months" => ExtractField::Month,
24365 "day" | "days" => ExtractField::Day,
24366 "hour" | "hours" => ExtractField::Hour,
24367 "minute" | "minutes" => ExtractField::Minute,
24368 "second" | "seconds" => ExtractField::Second,
24369 "microsecond" | "microseconds" => ExtractField::Microsecond,
24370 "epoch" => ExtractField::Epoch,
24371 "dow" => ExtractField::Dow,
24372 "isodow" => ExtractField::Isodow,
24373 "doy" => ExtractField::Doy,
24374 "week" | "weeks" => ExtractField::Week,
24375 "isoyear" => ExtractField::Isoyear,
24376 "quarter" => ExtractField::Quarter,
24377 "decade" | "decades" => ExtractField::Decade,
24378 "century" | "centuries" => ExtractField::Century,
24379 "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24380 "julian" => ExtractField::Julian,
24381 "millisecond" | "milliseconds" => ExtractField::Millisecond,
24382 "timezone" => ExtractField::Timezone,
24383 "timezone_hour" => ExtractField::TimezoneHour,
24384 "timezone_minute" => ExtractField::TimezoneMinute,
24385 // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24386 // reports an unknown one with the source type (22023); carry the
24387 // raw name so eval can word it.
24388 other => ExtractField::Other(alloc::string::String::from(other)),
24389 };
24390 if !matches!(self.peek(), Token::From) {
24391 return Err(self.err(format!(
24392 "expected FROM after EXTRACT field, got {:?}",
24393 self.peek()
24394 )));
24395 }
24396 self.advance();
24397 let source = self.parse_expr(0)?;
24398 if !matches!(self.peek(), Token::RParen) {
24399 return Err(self.err(format!(
24400 "expected ')' to close EXTRACT, got {:?}",
24401 self.peek()
24402 )));
24403 }
24404 self.advance();
24405 Ok(Expr::Extract {
24406 field,
24407 source: Box::new(source),
24408 })
24409 }
24410
24411 /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24412 /// is already consumed; we expect a single string literal next and
24413 /// resolve it into `Literal::Interval` at parse time so the engine
24414 /// never has to re-tokenise inside the string.
24415 /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24416 /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24417 /// is the SQL-standard form and is left to the path below.
24418 fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24419 // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24420 let (offset, sign) = match self.peek() {
24421 Token::Minus => (1, "-"),
24422 _ => (0, ""),
24423 };
24424 let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24425 return None;
24426 };
24427 self.tokens
24428 .get(self.pos + offset + 1)
24429 .filter(|t| mysql_interval_unit(t).is_some())?;
24430 Some((alloc::format!("{sign}{n}"), offset + 1))
24431 }
24432
24433 /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24434 /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24435 /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24436 ///
24437 /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24438 /// this by parsing the group and then restoring `self.pos` — which could
24439 /// never have worked, because `advance()` DESTROYS the token it returns
24440 /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24441 /// inert only because both branches errored back then.
24442 fn interval_paren_is_quantity(&self) -> bool {
24443 let mut depth = 0usize;
24444 let mut saw_top_level_comma = false;
24445 let mut i = self.pos;
24446 while let Some(tok) = self.tokens.get(i) {
24447 match tok {
24448 Token::LParen => depth += 1,
24449 Token::RParen => {
24450 depth = depth.saturating_sub(1);
24451 if depth == 0 {
24452 return !saw_top_level_comma
24453 && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
24454 .is_some();
24455 }
24456 }
24457 // A comma directly inside the outermost parens means the
24458 // argument list of the INTERVAL() function.
24459 Token::Comma if depth == 1 => saw_top_level_comma = true,
24460 Token::Eof => return false,
24461 _ => {}
24462 }
24463 i += 1;
24464 }
24465 false
24466 }
24467
24468 fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
24469 // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
24470 // (the index of the last Ni ≤ N), distinct from the interval literal.
24471 // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
24472 // is decided by a non-destructive lookahead (round 422) before either
24473 // branch consumes anything. MySQL only.
24474 if self.mysql_dialect
24475 && matches!(self.peek(), Token::LParen)
24476 && !self.interval_paren_is_quantity()
24477 {
24478 self.advance(); // (
24479 let mut args = Vec::new();
24480 if !matches!(self.peek(), Token::RParen) {
24481 loop {
24482 args.push(self.parse_expr(0)?);
24483 if matches!(self.peek(), Token::Comma) {
24484 self.advance();
24485 continue;
24486 }
24487 break;
24488 }
24489 }
24490 if !matches!(self.peek(), Token::RParen) {
24491 return Err(self.err(alloc::format!(
24492 "expected ')' after INTERVAL() arguments, got {:?}",
24493 self.peek()
24494 )));
24495 }
24496 self.advance(); // )
24497 return Ok(Expr::FunctionCall {
24498 name: alloc::string::String::from("interval"),
24499 args,
24500 });
24501 }
24502 // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
24503 // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
24504 // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
24505 // writes every date arithmetic there is, and it did not parse at
24506 // all. PG rejects the unquoted form outright (`syntax error at or
24507 // near "1"`, measured), so it is taken only in the MySQL dialect —
24508 // PG's own `INTERVAL '1' DAY` is untouched below.
24509 if self.mysql_dialect
24510 && let Some((text, consume)) = self.peek_unquoted_interval_count()
24511 {
24512 for _ in 0..consume {
24513 self.advance(); // the optional `-` and the number
24514 }
24515 let Some(unit) = mysql_interval_unit(self.peek()) else {
24516 return Err(self.err(alloc::format!(
24517 "expected an interval unit after INTERVAL {text}, got {:?}",
24518 self.peek()
24519 )));
24520 };
24521 self.advance(); // the unit
24522 let (months, days, micros) = scale_mysql_interval(&text, unit)
24523 .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
24524 return Ok(Expr::Literal(Literal::Interval {
24525 months,
24526 days,
24527 micros,
24528 // The canonical rendering, so Display round-trips into a
24529 // form both dialects read back.
24530 text: alloc::format!("{text} {unit}"),
24531 }));
24532 }
24533 // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
24534 // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
24535 // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
24536 // Those cannot fold into a compile-time `Literal::Interval`, so they
24537 // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
24538 // builtin, which builds the value at run time (and yields NULL for a
24539 // NULL quantity, as MariaDB does). The literal path above still folds
24540 // the constant case — it is cheaper and round-trips through Display.
24541 //
24542 // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
24543 // MySQL's quoted spelling) keep the qualifier path below.
24544 if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
24545 let qty = self.parse_expr(0)?;
24546 let Some(unit) = mysql_interval_unit(self.peek()) else {
24547 return Err(self.err(alloc::format!(
24548 "expected an interval unit after INTERVAL <expr>, got {:?}",
24549 self.peek()
24550 )));
24551 };
24552 self.advance(); // the unit
24553 return Ok(make_interval_call(qty, unit));
24554 }
24555 let tok = self.advance();
24556 let Token::String(text) = tok else {
24557 return Err(self.err(format!(
24558 "expected string literal after INTERVAL, got {tok:?}"
24559 )));
24560 };
24561 // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
24562 // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
24563 // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
24564 // bare number means and the leading/trailing precision.
24565 let field1 = interval_field_of(self.peek());
24566 let qualifier = if let Some(f1) = field1 {
24567 self.advance();
24568 let f2 = if matches!(self.peek(), Token::To) {
24569 self.advance();
24570 let Some(f) = interval_field_of(self.peek()) else {
24571 return Err(self.err(format!(
24572 "expected an interval field after TO, got {:?}",
24573 self.peek()
24574 )));
24575 };
24576 self.advance();
24577 Some(f)
24578 } else {
24579 None
24580 };
24581 Some((f1, f2))
24582 } else {
24583 None
24584 };
24585 let (months, days, micros) = match qualifier {
24586 Some(q) => interpret_qualified_interval(&text, q),
24587 None => parse_interval_text(&text),
24588 }
24589 .ok_or_else(|| ParseError {
24590 message: format!(
24591 "cannot parse INTERVAL {text:?}; \
24592 expected `<n> <unit> [<n> <unit> ...]` with units \
24593 microsecond[s], millisecond[s], second[s], minute[s], \
24594 hour[s], day[s], week[s], month[s], year[s]"
24595 ),
24596 token_pos: self.consumed_pos(),
24597 })?;
24598 Ok(Expr::Literal(Literal::Interval {
24599 months,
24600 days,
24601 micros,
24602 text,
24603 }))
24604 }
24605
24606 /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
24607 /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
24608 /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
24609 /// than a pgvector literal.
24610 fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
24611 self.advance(); // consume `[`
24612 let mut items: Vec<Expr> = Vec::new();
24613 if !matches!(self.peek(), Token::RBracket) {
24614 loop {
24615 if matches!(self.peek(), Token::LBracket) {
24616 items.push(self.parse_array_bracket_body()?);
24617 } else {
24618 items.push(self.parse_expr(0)?);
24619 }
24620 match self.peek() {
24621 Token::Comma => {
24622 self.advance();
24623 }
24624 Token::RBracket => break,
24625 other => {
24626 return Err(self.err(alloc::format!(
24627 "expected ',' or ']' in array literal, got {other:?}"
24628 )));
24629 }
24630 }
24631 }
24632 }
24633 self.advance(); // consume `]`
24634 Ok(Expr::Array(items))
24635 }
24636
24637 fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
24638 let mut elems = Vec::new();
24639 if matches!(self.peek(), Token::RBracket) {
24640 self.advance();
24641 return Ok(Expr::Literal(Literal::Vector(elems)));
24642 }
24643 loop {
24644 let e = self.parse_expr(0)?;
24645 let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
24646 message: format!("vector element must be a numeric literal, got {e:?}"),
24647 token_pos: self.pos,
24648 })?;
24649 elems.push(x);
24650 match self.peek() {
24651 Token::Comma => {
24652 self.advance();
24653 }
24654 Token::RBracket => {
24655 self.advance();
24656 break;
24657 }
24658 other => {
24659 return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
24660 }
24661 }
24662 }
24663 Ok(Expr::Literal(Literal::Vector(elems)))
24664 }
24665
24666 /// Atom that started with an identifier: could be `t.col`, `col`, or
24667 /// `func(arg, ...)`. Detect each shape by looking at the next token.
24668 /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
24669 /// [, ...])`. Caller has already consumed `OVER`. Either clause
24670 /// is optional; an empty `()` is also legal (PG semantics).
24671 /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
24672 /// modifier between `name(args)` and `OVER (...)`. Default is
24673 /// `Respect`. Unrecognised idents leave the stream unchanged.
24674 fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
24675 let Token::Ident(s) = self.peek().clone() else {
24676 return NullTreatment::Respect;
24677 };
24678 let is_ignore = s.eq_ignore_ascii_case("ignore");
24679 let is_respect = s.eq_ignore_ascii_case("respect");
24680 if !is_ignore && !is_respect {
24681 return NullTreatment::Respect;
24682 }
24683 // Lookahead for NULLS — only consume both tokens together.
24684 // pos+1 must hold a "nulls" ident.
24685 if self.pos + 1 < self.tokens.len()
24686 && let Token::Ident(s2) = &self.tokens[self.pos + 1]
24687 && s2.eq_ignore_ascii_case("nulls")
24688 {
24689 self.advance();
24690 self.advance();
24691 return if is_ignore {
24692 NullTreatment::Ignore
24693 } else {
24694 NullTreatment::Respect
24695 };
24696 }
24697 NullTreatment::Respect
24698 }
24699
24700 /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
24701 /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
24702 /// (same shape as the `OVER` tail). Consumes the whole clause and
24703 /// returns the predicate; returns `None` when no `FILTER` follows.
24704 fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
24705 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24706 return Ok(None);
24707 };
24708 if !s.eq_ignore_ascii_case("filter") {
24709 return Ok(None);
24710 }
24711 self.advance(); // FILTER
24712 if !matches!(self.peek(), Token::LParen) {
24713 return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
24714 }
24715 self.advance(); // (
24716 if !matches!(self.peek(), Token::Where) {
24717 return Err(self.err(format!(
24718 "expected WHERE inside FILTER (...), got {:?}",
24719 self.peek()
24720 )));
24721 }
24722 self.advance(); // WHERE
24723 let cond = self.parse_expr(0)?;
24724 if !matches!(self.peek(), Token::RParen) {
24725 return Err(self.err(format!(
24726 "expected ')' to close FILTER (WHERE ...), got {:?}",
24727 self.peek()
24728 )));
24729 }
24730 self.advance(); // )
24731 Ok(Some(Box::new(cond)))
24732 }
24733
24734 /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
24735 /// the separator as the aggregate's second argument, which is the
24736 /// shape `string_agg` already takes. Returns whether one was there.
24737 fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
24738 if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
24739 return Ok(false);
24740 }
24741 self.advance();
24742 let Token::String(sep) = self.peek().clone() else {
24743 return Err(self.err(alloc::format!(
24744 "expected a string literal after SEPARATOR, got {:?}",
24745 self.peek()
24746 )));
24747 };
24748 self.advance();
24749 args.push(Expr::Literal(Literal::String(sep)));
24750 Ok(true)
24751 }
24752
24753 /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
24754 /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
24755 /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
24756 /// keys, or an empty vec when no `WITHIN GROUP` follows.
24757 fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
24758 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24759 return Ok(Vec::new());
24760 };
24761 if !s.eq_ignore_ascii_case("within") {
24762 return Ok(Vec::new());
24763 }
24764 self.advance(); // WITHIN
24765 if !matches!(self.peek(), Token::Group) {
24766 return Err(self.err(format!(
24767 "expected GROUP after WITHIN, got {:?}",
24768 self.peek()
24769 )));
24770 }
24771 self.advance(); // GROUP
24772 if !matches!(self.peek(), Token::LParen) {
24773 return Err(self.err(format!(
24774 "expected '(' after WITHIN GROUP, got {:?}",
24775 self.peek()
24776 )));
24777 }
24778 self.advance(); // (
24779 if !matches!(self.peek(), Token::Order) {
24780 return Err(self.err(format!(
24781 "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
24782 self.peek()
24783 )));
24784 }
24785 self.advance(); // ORDER
24786 if !self.peek_is_by() {
24787 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24788 }
24789 self.advance(); // BY
24790 let mut keys: Vec<OrderBy> = Vec::new();
24791 loop {
24792 // v7.39 (round 691) — save/restore, the discipline this parser
24793 // already uses around `pending_sample_preds`, so a subquery inside
24794 // a key neither inherits nor leaks the channel.
24795 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
24796 let saved_coll = self.order_key_collation.take();
24797 let parsed = self.parse_expr(0);
24798 self.in_order_by_key = saved_flag;
24799 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
24800 let expr = parsed?;
24801 let desc = if matches!(self.peek(), Token::Desc) {
24802 self.advance();
24803 true
24804 } else if matches!(self.peek(), Token::Asc) {
24805 self.advance();
24806 false
24807 } else {
24808 false
24809 };
24810 let nulls_first = self.parse_optional_nulls_placement()?;
24811 keys.push(OrderBy {
24812 expr,
24813 desc,
24814 nulls_first,
24815 collation,
24816 });
24817 if matches!(self.peek(), Token::Comma) {
24818 self.advance();
24819 } else {
24820 break;
24821 }
24822 }
24823 if !matches!(self.peek(), Token::RParen) {
24824 return Err(self.err(format!(
24825 "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
24826 self.peek()
24827 )));
24828 }
24829 self.advance(); // )
24830 Ok(keys)
24831 }
24832
24833 /// No frame clause is supported.
24834 #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
24835 fn parse_over_clause(
24836 &mut self,
24837 ) -> Result<
24838 (
24839 Vec<Expr>,
24840 Vec<(Expr, bool, Option<bool>)>,
24841 Option<WindowFrame>,
24842 ),
24843 ParseError,
24844 > {
24845 // `OVER w` — a named-window reference. The WINDOW clause
24846 // parses after the select list, so the name rides out as a
24847 // marker in partition_by; parse_bare_select substitutes the
24848 // definition once the clause is known.
24849 if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
24850 let name = w.clone();
24851 self.advance();
24852 return Ok((
24853 alloc::vec![Expr::Column(crate::ast::ColumnName {
24854 qualifier: Some("__named_window__".to_string()),
24855 name,
24856 })],
24857 Vec::new(),
24858 None,
24859 ));
24860 }
24861 if !matches!(self.peek(), Token::LParen) {
24862 return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
24863 }
24864 self.advance();
24865 let mut partition_by = Vec::new();
24866 let mut order_by = Vec::new();
24867 // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
24868 // window, refined in place. PG's rules (probed against 18.4) differ
24869 // from the bare `OVER w1` form, so the reference rides out under its
24870 // own marker and `substitute_named_windows` applies them. The base
24871 // name is any leading identifier that isn't a window-spec keyword.
24872 let base_window = match self.peek() {
24873 Token::Ident(s) | Token::QuotedIdent(s)
24874 if !s.eq_ignore_ascii_case("partition")
24875 && !s.eq_ignore_ascii_case("rows")
24876 && !s.eq_ignore_ascii_case("range")
24877 && !s.eq_ignore_ascii_case("groups") =>
24878 {
24879 let n = s.clone();
24880 self.advance();
24881 Some(n)
24882 }
24883 _ => None,
24884 };
24885 // PARTITION BY ?
24886 // v7.37.6-B promoted PARTITION to a reserved keyword
24887 // (Token::Partition); pre-7.37.6-B catalogs lexed it as
24888 // `Token::Ident("partition")`. Accept both so older sources
24889 // and the new lexer surface land on the same path.
24890 let is_partition_kw = match self.peek() {
24891 Token::Partition => true,
24892 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
24893 _ => false,
24894 };
24895 if is_partition_kw {
24896 self.advance();
24897 if !self.peek_is_by() {
24898 return Err(self.err(format!(
24899 "expected BY after PARTITION, got {:?}",
24900 self.peek()
24901 )));
24902 }
24903 self.advance();
24904 loop {
24905 partition_by.push(self.parse_expr(0)?);
24906 if matches!(self.peek(), Token::Comma) {
24907 self.advance();
24908 continue;
24909 }
24910 break;
24911 }
24912 }
24913 // ORDER BY ?
24914 if matches!(self.peek(), Token::Order) {
24915 self.advance();
24916 if !self.peek_is_by() {
24917 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24918 }
24919 self.advance();
24920 loop {
24921 let e = self.parse_expr(0)?;
24922 let desc = if matches!(self.peek(), Token::Desc) {
24923 self.advance();
24924 true
24925 } else if matches!(self.peek(), Token::Asc) {
24926 self.advance();
24927 false
24928 } else {
24929 false
24930 };
24931 // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
24932 let nulls_first = self.parse_optional_nulls_placement()?;
24933 order_by.push((e, desc, nulls_first));
24934 if matches!(self.peek(), Token::Comma) {
24935 self.advance();
24936 continue;
24937 }
24938 break;
24939 }
24940 }
24941 // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
24942 // Both keywords come through the lexer as identifiers; match
24943 // case-insensitively.
24944 let mut frame: Option<WindowFrame> = None;
24945 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
24946 let kind = if s.eq_ignore_ascii_case("rows") {
24947 Some(FrameKind::Rows)
24948 } else if s.eq_ignore_ascii_case("range") {
24949 Some(FrameKind::Range)
24950 } else if s.eq_ignore_ascii_case("groups") {
24951 // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
24952 Some(FrameKind::Groups)
24953 } else {
24954 None
24955 };
24956 if let Some(kind) = kind {
24957 self.advance();
24958 frame = Some(self.parse_frame_tail(kind)?);
24959 }
24960 }
24961 if !matches!(self.peek(), Token::RParen) {
24962 return Err(self.err(format!(
24963 "expected ')' to close OVER clause, got {:?}",
24964 self.peek()
24965 )));
24966 }
24967 self.advance();
24968 if let Some(base) = base_window {
24969 // A copy may refine but never override the base's partitioning
24970 // (PG rejects it outright, before looking the name up).
24971 if !partition_by.is_empty() {
24972 return Err(self.err(alloc::format!(
24973 "cannot override PARTITION BY clause of window \"{base}\""
24974 )));
24975 }
24976 partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
24977 qualifier: Some("__named_window_ref__".to_string()),
24978 name: base,
24979 })];
24980 }
24981 Ok((partition_by, order_by, frame))
24982 }
24983
24984 /// v4.20: parse the tail of an explicit frame, given the `ROWS`
24985 /// or `RANGE` keyword was just consumed. Accepts both
24986 /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
24987 /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
24988 /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
24989 fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
24990 let (start, end) = if matches!(self.peek(), Token::Between) {
24991 self.advance();
24992 let start = self.parse_frame_bound()?;
24993 if !matches!(self.peek(), Token::And) {
24994 return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
24995 }
24996 self.advance();
24997 let end = self.parse_frame_bound()?;
24998 (start, Some(end))
24999 } else {
25000 (self.parse_frame_bound()?, None)
25001 };
25002 let exclude = self.parse_frame_exclusion()?;
25003 Ok(WindowFrame {
25004 kind,
25005 start,
25006 end,
25007 exclude,
25008 })
25009 }
25010
25011 /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
25012 /// after a frame spec. NO OTHERS is the default no-op.
25013 fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
25014 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
25015 return Ok(FrameExclusion::NoOthers);
25016 }
25017 self.advance(); // EXCLUDE
25018 match self.peek() {
25019 Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
25020 self.advance();
25021 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
25022 return Err(self.err(format!(
25023 "expected ROW after EXCLUDE CURRENT, got {:?}",
25024 self.peek()
25025 )));
25026 }
25027 self.advance();
25028 Ok(FrameExclusion::CurrentRow)
25029 }
25030 // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
25031 // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
25032 // Without this arm it fell to the catch-all, whose message
25033 // self-contradictingly listed GROUP as expected.
25034 Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
25035 self.advance();
25036 Ok(FrameExclusion::Group)
25037 }
25038 Token::Group => {
25039 self.advance();
25040 Ok(FrameExclusion::Group)
25041 }
25042 Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
25043 self.advance();
25044 Ok(FrameExclusion::Ties)
25045 }
25046 Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
25047 self.advance();
25048 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
25049 return Err(self.err(format!(
25050 "expected OTHERS after EXCLUDE NO, got {:?}",
25051 self.peek()
25052 )));
25053 }
25054 self.advance();
25055 Ok(FrameExclusion::NoOthers)
25056 }
25057 other => Err(self.err(format!(
25058 "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
25059 ))),
25060 }
25061 }
25062
25063 /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
25064 /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
25065 /// `UNBOUNDED FOLLOWING`.
25066 fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
25067 // Interval-typed offset for a value-based RANGE frame over a
25068 // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
25069 // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
25070 // PRECEDING`.
25071 if let Some((months, days, micros)) = self.try_take_interval_offset()? {
25072 let dir = self.expect_ident_like()?;
25073 return if dir.eq_ignore_ascii_case("preceding") {
25074 Ok(FrameBound::IntervalPreceding {
25075 months,
25076 days,
25077 micros,
25078 })
25079 } else if dir.eq_ignore_ascii_case("following") {
25080 Ok(FrameBound::IntervalFollowing {
25081 months,
25082 days,
25083 micros,
25084 })
25085 } else {
25086 Err(self.err(format!(
25087 "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
25088 )))
25089 };
25090 }
25091 // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
25092 if let Token::Integer(n) = *self.peek() {
25093 self.advance();
25094 let n: u64 = u64::try_from(n).map_err(|_| {
25095 self.err(format!(
25096 "invalid frame offset {n} — expected non-negative integer"
25097 ))
25098 })?;
25099 let dir = self.expect_ident_like()?;
25100 return if dir.eq_ignore_ascii_case("preceding") {
25101 Ok(FrameBound::OffsetPreceding(n))
25102 } else if dir.eq_ignore_ascii_case("following") {
25103 Ok(FrameBound::OffsetFollowing(n))
25104 } else {
25105 Err(self.err(format!(
25106 "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
25107 )))
25108 };
25109 }
25110 let first = self.expect_ident_like()?;
25111 if first.eq_ignore_ascii_case("unbounded") {
25112 let dir = self.expect_ident_like()?;
25113 return if dir.eq_ignore_ascii_case("preceding") {
25114 Ok(FrameBound::UnboundedPreceding)
25115 } else if dir.eq_ignore_ascii_case("following") {
25116 Ok(FrameBound::UnboundedFollowing)
25117 } else {
25118 Err(self.err(format!(
25119 "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
25120 )))
25121 };
25122 }
25123 if first.eq_ignore_ascii_case("current") {
25124 let row = self.expect_ident_like()?;
25125 if !row.eq_ignore_ascii_case("row") {
25126 return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
25127 }
25128 return Ok(FrameBound::CurrentRow);
25129 }
25130 Err(self.err(format!(
25131 "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
25132 )))
25133 }
25134
25135 /// Detect and consume a leading interval offset in a frame bound —
25136 /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
25137 /// `(months, days, micros)`. Leaves the cursor on the trailing
25138 /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
25139 /// when the next tokens are not an interval offset.
25140 fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
25141 // Shape A — `INTERVAL '1 day'`.
25142 if matches!(self.peek(), Token::Interval) {
25143 self.advance(); // INTERVAL
25144 let atom = self.parse_interval_atom()?;
25145 if let Expr::Literal(Literal::Interval {
25146 months,
25147 days,
25148 micros,
25149 ..
25150 }) = atom
25151 {
25152 return Ok(Some((months, days, micros)));
25153 }
25154 return Err(self.err("expected an interval literal in frame offset".to_string()));
25155 }
25156 // Shape B — `'1 day'::interval`. Look ahead for the exact
25157 // string / `::` / interval-target triple before committing.
25158 if let Token::String(text) = self.peek() {
25159 let target_is_interval = match self.tokens.get(self.pos + 2) {
25160 Some(Token::Interval) => true,
25161 Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
25162 _ => false,
25163 };
25164 let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
25165 && target_is_interval;
25166 if is_cast {
25167 let text = text.clone();
25168 self.advance(); // string
25169 self.advance(); // ::
25170 self.advance(); // interval
25171 let parts = parse_interval_text(&text).ok_or_else(|| {
25172 self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
25173 })?;
25174 return Ok(Some(parts));
25175 }
25176 }
25177 Ok(None)
25178 }
25179
25180 fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
25181 if matches!(self.peek(), Token::Dot) {
25182 self.advance();
25183 let name = self.expect_ident_like()?;
25184 // v7.14.0 — schema-qualified function call
25185 // `<schema>.<fn>(args)`. PG dumps emit
25186 // `pg_catalog.set_config(...)` in the preamble. SPG
25187 // is single-namespace: drop the schema prefix and
25188 // route the dispatch on the bare function name.
25189 if matches!(self.peek(), Token::LParen) {
25190 return self.finish_ident_atom(name);
25191 }
25192 return Ok(Expr::Column(ColumnName {
25193 qualifier: Some(first),
25194 name,
25195 }));
25196 }
25197 if matches!(self.peek(), Token::LParen) {
25198 self.advance();
25199 // `COUNT(*)` — special-cased here because `*` isn't a normal
25200 // expression token. Lower-case match on `first` since the lexer
25201 // folds identifiers.
25202 if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
25203 self.advance();
25204 if !matches!(self.peek(), Token::RParen) {
25205 return Err(self.err(format!(
25206 "expected ')' after COUNT(*), got {:?}",
25207 self.peek()
25208 )));
25209 }
25210 self.advance();
25211 // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
25212 let filter = self.parse_filter_clause()?;
25213 // v4.12: COUNT(*) OVER (...) — same window tail.
25214 let null_treatment = self.parse_null_treatment_modifier();
25215 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25216 && s.eq_ignore_ascii_case("over")
25217 {
25218 self.advance();
25219 let (partition_by, order_by, frame) = self.parse_over_clause()?;
25220 return Ok(Expr::WindowFunction {
25221 name: "count_star".into(),
25222 args: Vec::new(),
25223 partition_by,
25224 order_by,
25225 frame,
25226 null_treatment,
25227 filter,
25228 });
25229 }
25230 if let Some(filter) = filter {
25231 return Ok(Expr::AggregateOrdered {
25232 call: Box::new(Expr::FunctionCall {
25233 name: "count_star".into(),
25234 args: Vec::new(),
25235 }),
25236 order_by: Vec::new(),
25237 distinct: false,
25238 filter: Some(filter),
25239 });
25240 }
25241 return Ok(Expr::FunctionCall {
25242 name: "count_star".into(),
25243 args: Vec::new(),
25244 });
25245 }
25246 // Function call. PG-style: zero-or-more comma-separated args.
25247 let mut args = Vec::new();
25248 // v7.38 (read01, T14) — named-argument notation `argname => value`.
25249 // Names are collected in lock-step with `args` and resolved to
25250 // positional order after the loop (the AST stays positional).
25251 let mut arg_names: Vec<Option<String>> = Vec::new();
25252 let mut agg_order_by: Vec<OrderBy> = Vec::new();
25253 // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
25254 // seen, so the value arguments before it can be folded.
25255 let mut saw_separator = false;
25256 // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
25257 // v7.32 (round-29) — accept the dual `ALL` quantifier too
25258 // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
25259 let agg_distinct = if matches!(self.peek(), Token::Distinct) {
25260 self.advance();
25261 true
25262 } else if matches!(self.peek(), Token::All) {
25263 self.advance();
25264 false
25265 } else {
25266 false
25267 };
25268 // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
25269 // TIMESTAMPDIFF take a bare unit keyword as the first
25270 // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
25271 // bare type keyword (DATE / TIME / DATETIME); lower them
25272 // onto string literals so the evaluator sees plain text.
25273 if ((first.eq_ignore_ascii_case("timestampadd")
25274 || first.eq_ignore_ascii_case("timestampdiff"))
25275 && matches!(self.peek(), Token::Ident(u) if matches!(
25276 u.to_ascii_lowercase().as_str(),
25277 "microsecond" | "second" | "minute" | "hour" | "day"
25278 | "week" | "month" | "quarter" | "year"
25279 )))
25280 || (first.eq_ignore_ascii_case("get_format")
25281 && matches!(self.peek(), Token::Ident(u) if matches!(
25282 u.to_ascii_lowercase().as_str(),
25283 "date" | "time" | "datetime" | "timestamp"
25284 )))
25285 {
25286 if let Token::Ident(u) = self.peek() {
25287 args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
25288 }
25289 self.advance();
25290 if matches!(self.peek(), Token::Comma) {
25291 self.advance();
25292 }
25293 }
25294 // `ROW(a, b, …)` keyword constructor. Followed by a
25295 // comparison operator or [NOT] IN it joins the paren
25296 // row-constructor machinery (fieldwise parse-time
25297 // expansion); bare, it stays a `row` call the evaluator
25298 // renders as PG record text.
25299 if first.eq_ignore_ascii_case("row") {
25300 let mut row_items = Vec::new();
25301 if !matches!(self.peek(), Token::RParen) {
25302 loop {
25303 row_items.push(self.parse_expr(0)?);
25304 match self.peek() {
25305 Token::Comma => {
25306 self.advance();
25307 }
25308 Token::RParen => break,
25309 other => {
25310 return Err(self.err(format!(
25311 "expected ',' or ')' in ROW(...), got {other:?}"
25312 )));
25313 }
25314 }
25315 }
25316 }
25317 self.advance(); // ')'
25318 let comparison_follows = matches!(
25319 self.peek(),
25320 Token::Eq
25321 | Token::NotEq
25322 | Token::Lt
25323 | Token::LtEq
25324 | Token::Gt
25325 | Token::GtEq
25326 | Token::In
25327 ) || (matches!(self.peek(), Token::Not)
25328 && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25329 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25330 if comparison_follows && !row_items.is_empty() {
25331 return self.parse_row_comparison_tail(row_items);
25332 }
25333 return Ok(Expr::FunctionCall {
25334 name: String::from("row"),
25335 args: row_items,
25336 });
25337 }
25338 // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25339 // the parse-mode keyword introduces the source text. SPG
25340 // carries XML as text, so both modes lower to __xmlparse(expr)
25341 // which validates well-formedness and returns Value::Xml.
25342 if first.eq_ignore_ascii_case("xmlparse")
25343 && matches!(self.peek(), Token::Ident(kw)
25344 if kw.eq_ignore_ascii_case("document")
25345 || kw.eq_ignore_ascii_case("content"))
25346 {
25347 let mode = match self.advance() {
25348 Token::Ident(kw) => kw.to_ascii_lowercase(),
25349 _ => unreachable!("peeked an ident"),
25350 };
25351 let src = self.parse_expr(0)?;
25352 if !matches!(self.peek(), Token::RParen) {
25353 return Err(self.err(format!(
25354 "expected ')' to close XMLPARSE, got {:?}",
25355 self.peek()
25356 )));
25357 }
25358 self.advance();
25359 return Ok(Expr::FunctionCall {
25360 name: String::from("__xmlparse"),
25361 args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25362 });
25363 }
25364 // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25365 // keyword introduces the element name (a bare or quoted
25366 // identifier), then optional content expressions. Lower to a
25367 // plain `xmlelement(name_text, content …)` call.
25368 if first.eq_ignore_ascii_case("xmlelement")
25369 && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25370 {
25371 self.advance(); // consume NAME
25372 let elem_name = match self.peek().clone() {
25373 Token::Ident(n) | Token::QuotedIdent(n) => {
25374 self.advance();
25375 n
25376 }
25377 other => {
25378 return Err(self.err(format!(
25379 "expected element name after XMLELEMENT NAME, got {other:?}"
25380 )));
25381 }
25382 };
25383 let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25384 while matches!(self.peek(), Token::Comma) {
25385 self.advance();
25386 args.push(self.parse_expr(0)?);
25387 }
25388 if !matches!(self.peek(), Token::RParen) {
25389 return Err(self.err(format!(
25390 "expected ')' to close XMLELEMENT, got {:?}",
25391 self.peek()
25392 )));
25393 }
25394 self.advance();
25395 return Ok(Expr::FunctionCall {
25396 name: String::from("xmlelement"),
25397 args,
25398 });
25399 }
25400 // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
25401 // becomes a `<name>value</name>` element; a bare column infers its
25402 // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
25403 if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
25404 let mut args: Vec<Expr> = Vec::new();
25405 loop {
25406 let val = self.parse_expr(0)?;
25407 let name = if matches!(self.peek(), Token::As) {
25408 self.advance();
25409 match self.peek().clone() {
25410 Token::Ident(n) | Token::QuotedIdent(n) => {
25411 self.advance();
25412 n
25413 }
25414 other => {
25415 return Err(self.err(format!(
25416 "expected name after AS in XMLFOREST, got {other:?}"
25417 )));
25418 }
25419 }
25420 } else if let Expr::Column(c) = &val {
25421 c.name.clone()
25422 } else {
25423 return Err(
25424 self.err("XMLFOREST element without a column name needs AS".into())
25425 );
25426 };
25427 args.push(Expr::Literal(Literal::String(name)));
25428 args.push(val);
25429 if matches!(self.peek(), Token::Comma) {
25430 self.advance();
25431 } else {
25432 break;
25433 }
25434 }
25435 if !matches!(self.peek(), Token::RParen) {
25436 return Err(self.err(format!(
25437 "expected ')' to close XMLFOREST, got {:?}",
25438 self.peek()
25439 )));
25440 }
25441 self.advance();
25442 return Ok(Expr::FunctionCall {
25443 name: String::from("xmlforest"),
25444 args,
25445 });
25446 }
25447 // SQL-standard `POSITION(sub IN str)` — lowers onto
25448 // strpos(str, sub). IN is the argument separator here,
25449 // so the needle parses with the IN-tail suppressed.
25450 if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
25451 let saved = self.suppress_in_tail;
25452 self.suppress_in_tail = true;
25453 let needle = self.parse_expr(0);
25454 self.suppress_in_tail = saved;
25455 let needle = needle?;
25456 if matches!(self.peek(), Token::In) {
25457 self.advance();
25458 let haystack = self.parse_expr(0)?;
25459 if !matches!(self.peek(), Token::RParen) {
25460 return Err(self.err(format!(
25461 "expected ')' to close POSITION, got {:?}",
25462 self.peek()
25463 )));
25464 }
25465 self.advance();
25466 return Ok(Expr::FunctionCall {
25467 name: String::from("strpos"),
25468 args: alloc::vec![haystack, needle],
25469 });
25470 }
25471 // position(sub, str) comma form (incl. bytea) —
25472 // hand the parsed first arg to the generic list.
25473 args.push(needle);
25474 if matches!(self.peek(), Token::Comma) {
25475 self.advance();
25476 }
25477 }
25478 // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
25479 // FROM str)` — lowers onto btrim / ltrim / rtrim. The
25480 // plain comma forms TRIM(str) / TRIM(str, chars) keep
25481 // riding the generic argument list below.
25482 if first.eq_ignore_ascii_case("trim") {
25483 let mode = match self.peek() {
25484 Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
25485 self.advance();
25486 Some("btrim")
25487 }
25488 Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
25489 self.advance();
25490 Some("ltrim")
25491 }
25492 Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
25493 self.advance();
25494 Some("rtrim")
25495 }
25496 _ => None,
25497 };
25498 if mode.is_some() || matches!(self.peek(), Token::From) {
25499 // TRIM([mode] FROM str) — no strip-chars.
25500 let chars = if matches!(self.peek(), Token::From) {
25501 None
25502 } else {
25503 Some(self.parse_expr(0)?)
25504 };
25505 if !matches!(self.peek(), Token::From) {
25506 return Err(self.err(format!(
25507 "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
25508 self.peek()
25509 )));
25510 }
25511 self.advance();
25512 let target = self.parse_expr(0)?;
25513 if !matches!(self.peek(), Token::RParen) {
25514 return Err(
25515 self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
25516 );
25517 }
25518 self.advance();
25519 let mut trim_args = alloc::vec![target];
25520 if let Some(c) = chars {
25521 trim_args.push(c);
25522 }
25523 return Ok(Expr::FunctionCall {
25524 name: String::from(mode.unwrap_or("btrim")),
25525 args: trim_args,
25526 });
25527 }
25528 }
25529 if !matches!(self.peek(), Token::RParen) {
25530 loop {
25531 // v7.38 (read01, T14) — `argname => value` names this arg.
25532 // v7.39 (read01 round 77) — `argname := value` is the same
25533 // thing, and it is the spelling PG's own docs lead with. It
25534 // was simply never lexed here, so every `f(x := 1)` died in
25535 // the parser regardless of what `f` was.
25536 let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
25537 (
25538 Token::Ident(n) | Token::QuotedIdent(n),
25539 Some(Token::FatArrow | Token::ColonEq),
25540 ) => {
25541 let name = n.clone();
25542 self.advance(); // name
25543 self.advance(); // => / :=
25544 Some(name)
25545 }
25546 _ => None,
25547 };
25548 // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
25549 // array's elements into a variadic call's trailing args
25550 // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
25551 // reserved, so it arrives as a bare ident before the arg.
25552 let is_variadic = this_name.is_none()
25553 && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
25554 if is_variadic {
25555 self.advance();
25556 }
25557 let arg = self.parse_expr(0)?;
25558 args.push(match &this_name {
25559 // The callee's parameter names decide the slot, and a
25560 // user function's live in the catalog. Carry the name
25561 // to eval rather than guessing here.
25562 Some(n) => Expr::NamedArg {
25563 name: n.clone(),
25564 expr: Box::new(arg),
25565 },
25566 None if is_variadic => Expr::Variadic(Box::new(arg)),
25567 None => arg,
25568 });
25569 arg_names.push(this_name);
25570 // v7.25 (round-17) — standard `CAST(expr AS type)`.
25571 // The `::` cast already worked; this lowers the
25572 // function form onto the same Expr::Cast node.
25573 if first.eq_ignore_ascii_case("cast")
25574 && args.len() == 1
25575 && matches!(self.peek(), Token::As)
25576 {
25577 self.advance();
25578 let target = self.parse_cast_target()?;
25579 if !matches!(self.peek(), Token::RParen) {
25580 return Err(self.err(format!(
25581 "expected ')' to close CAST, got {:?}",
25582 self.peek()
25583 )));
25584 }
25585 self.advance();
25586 return Ok(Expr::Cast {
25587 expr: Box::new(args.pop().expect("one arg")),
25588 target,
25589 });
25590 }
25591 // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
25592 // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
25593 // keywords; SPG's lexer makes them plain idents (so they'd be
25594 // read as column refs). Lower the keyword to the string form
25595 // the evaluator already accepts.
25596 if first.eq_ignore_ascii_case("normalize")
25597 && args.len() == 1
25598 && matches!(self.peek(), Token::Comma)
25599 {
25600 let form = match self.tokens.get(self.pos + 1) {
25601 Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
25602 let up = f.to_ascii_uppercase();
25603 matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
25604 }
25605 _ => None,
25606 };
25607 if let Some(up) = form {
25608 self.advance(); // comma
25609 self.advance(); // form keyword
25610 args.push(Expr::Literal(Literal::String(up)));
25611 }
25612 }
25613 // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
25614 // form. Desugars to the comma-list shape evaluator already
25615 // handles. Triggered after the first arg when the function
25616 // name is substring / substr and the next token is FROM
25617 // (a reserved keyword in PG; SPG also reserves it).
25618 // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
25619 // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
25620 // internal __substring_similar(str, pat, esc) call.
25621 if (first.eq_ignore_ascii_case("substring")
25622 || first.eq_ignore_ascii_case("substr"))
25623 && args.len() == 1
25624 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
25625 {
25626 self.advance(); // SIMILAR
25627 let pattern = self.parse_expr(0)?;
25628 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
25629 {
25630 return Err(self.err(format!(
25631 "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
25632 self.peek()
25633 )));
25634 }
25635 self.advance(); // ESCAPE
25636 let esc = self.parse_expr(0)?;
25637 if !matches!(self.peek(), Token::RParen) {
25638 return Err(self.err(format!(
25639 "expected ')' to close substring(... SIMILAR ...), got {:?}",
25640 self.peek()
25641 )));
25642 }
25643 self.advance();
25644 args.push(pattern);
25645 args.push(esc);
25646 return Ok(Expr::FunctionCall {
25647 name: "__substring_similar".to_string(),
25648 args,
25649 });
25650 }
25651 if (first.eq_ignore_ascii_case("substring")
25652 || first.eq_ignore_ascii_case("substr"))
25653 && args.len() == 1
25654 && matches!(self.peek(), Token::From | Token::For)
25655 {
25656 // `substring(str FROM pos [FOR len])`, or the FOR-only
25657 // `substring(str FOR len)` which PG treats as FROM 1.
25658 if matches!(self.peek(), Token::From) {
25659 self.advance();
25660 let start = self.parse_expr(0)?;
25661 args.push(start);
25662 } else {
25663 args.push(Expr::Literal(Literal::Integer(1)));
25664 }
25665 if matches!(self.peek(), Token::For) {
25666 self.advance();
25667 let length = self.parse_expr(0)?;
25668 args.push(length);
25669 }
25670 if !matches!(self.peek(), Token::RParen) {
25671 return Err(self.err(format!(
25672 "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
25673 self.peek()
25674 )));
25675 }
25676 self.advance();
25677 return Ok(Expr::FunctionCall {
25678 name: first.to_ascii_lowercase(),
25679 args,
25680 });
25681 }
25682 // PG `overlay(str PLACING repl FROM n [FOR len])`
25683 // syntactic form. Desugars to the `overlay(str,
25684 // repl, n[, len])` comma-list shape the evaluator
25685 // already implements. `PLACING` is not a reserved
25686 // token in SPG, so it arrives as a bare Ident.
25687 if first.eq_ignore_ascii_case("overlay")
25688 && args.len() == 1
25689 && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
25690 {
25691 self.advance(); // consume PLACING
25692 args.push(self.parse_expr(0)?); // replacement
25693 if !matches!(self.peek(), Token::From) {
25694 return Err(self.err(format!(
25695 "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
25696 self.peek()
25697 )));
25698 }
25699 self.advance();
25700 args.push(self.parse_expr(0)?); // start position
25701 if matches!(self.peek(), Token::For) {
25702 self.advance();
25703 args.push(self.parse_expr(0)?); // length
25704 }
25705 if !matches!(self.peek(), Token::RParen) {
25706 return Err(self.err(format!(
25707 "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
25708 self.peek()
25709 )));
25710 }
25711 self.advance();
25712 return Ok(Expr::FunctionCall {
25713 name: String::from("overlay"),
25714 args,
25715 });
25716 }
25717 // `TRIM(chars FROM str)` — the keyword-less
25718 // spelling lands here after the chars parse
25719 // (the keyword forms return earlier).
25720 if first.eq_ignore_ascii_case("trim")
25721 && args.len() == 1
25722 && matches!(self.peek(), Token::From)
25723 {
25724 self.advance();
25725 let target = self.parse_expr(0)?;
25726 if !matches!(self.peek(), Token::RParen) {
25727 return Err(self.err(format!(
25728 "expected ')' to close TRIM(chars FROM str), got {:?}",
25729 self.peek()
25730 )));
25731 }
25732 self.advance();
25733 let chars = args.pop().expect("one arg");
25734 return Ok(Expr::FunctionCall {
25735 name: String::from("btrim"),
25736 args: alloc::vec![target, chars],
25737 });
25738 }
25739 // v7.24 (round-16 A) — aggregate-internal
25740 // ordering: `array_agg(x ORDER BY y DESC NULLS
25741 // LAST)`. Keys close the argument list.
25742 if matches!(self.peek(), Token::Order) {
25743 self.advance();
25744 if !self.peek_is_by() {
25745 return Err(self.err(format!(
25746 "expected BY after ORDER in aggregate args, got {:?}",
25747 self.peek()
25748 )));
25749 }
25750 self.advance();
25751 loop {
25752 // v7.39 (round 691) — save/restore, the discipline this parser
25753 // already uses around `pending_sample_preds`, so a subquery inside
25754 // a key neither inherits nor leaks the channel.
25755 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25756 let saved_coll = self.order_key_collation.take();
25757 let parsed = self.parse_expr(0);
25758 self.in_order_by_key = saved_flag;
25759 let collation =
25760 core::mem::replace(&mut self.order_key_collation, saved_coll);
25761 let expr = parsed?;
25762 let desc = if matches!(self.peek(), Token::Desc) {
25763 self.advance();
25764 true
25765 } else if matches!(self.peek(), Token::Asc) {
25766 self.advance();
25767 false
25768 } else {
25769 false
25770 };
25771 let nulls_first = self.parse_optional_nulls_placement()?;
25772 agg_order_by.push(OrderBy {
25773 expr,
25774 desc,
25775 nulls_first,
25776 collation,
25777 });
25778 if matches!(self.peek(), Token::Comma) {
25779 self.advance();
25780 } else {
25781 break;
25782 }
25783 }
25784 // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
25785 // follow the ORDER BY inside GROUP_CONCAT.
25786 if self.consume_group_concat_separator(&mut args)? {
25787 saw_separator = true;
25788 }
25789 if !matches!(self.peek(), Token::RParen) {
25790 return Err(self.err(format!(
25791 "expected ')' after aggregate ORDER BY, got {:?}",
25792 self.peek()
25793 )));
25794 }
25795 break;
25796 }
25797 // v7.39 (round 354, M12) — …or directly after the
25798 // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
25799 // own spelling of what PG passes as string_agg's second
25800 // argument; it was a parse error, so every MySQL query
25801 // that names its own separator failed outright.
25802 if self.consume_group_concat_separator(&mut args)? {
25803 saw_separator = true;
25804 break;
25805 }
25806 match self.peek() {
25807 Token::Comma => {
25808 self.advance();
25809 }
25810 Token::RParen => break,
25811 other => {
25812 return Err(self.err(format!(
25813 "expected ',' or ')' in function args, got {other:?}"
25814 )));
25815 }
25816 }
25817 }
25818 }
25819 // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
25820 // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
25821 // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
25822 // meaning a separator — that is what the explicit SEPARATOR
25823 // tail is for. Fold them into one `concat(...)` so the
25824 // aggregate keeps its single value argument.
25825 if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
25826 let values = args.len() - usize::from(saw_separator);
25827 if values > 1 {
25828 let sep_arg = if saw_separator { args.pop() } else { None };
25829 let folded = Expr::FunctionCall {
25830 name: "concat".to_string(),
25831 args: core::mem::take(&mut args),
25832 };
25833 args.push(folded);
25834 if let Some(sep) = sep_arg {
25835 args.push(sep);
25836 }
25837 }
25838 }
25839 self.advance(); // consume ')'
25840 // v7.39 (read01 round 77) — named arguments are NOT reordered here
25841 // any more. The parser has no catalog, so it could only ever resolve
25842 // the handful of `make_*` builtins whose parameter names were baked
25843 // into a table right here — every user function got
25844 // "does not support named arguments", though the catalog has been
25845 // storing its parameter names all along. Reordering happens in eval,
25846 // in one place, for builtins and user functions alike.
25847 // v7.32 (round-29) — ordered-set aggregate tail
25848 // `name(direct_args) WITHIN GROUP (ORDER BY …)`
25849 // (percentile_cont / percentile_disc / mode). The sort spec
25850 // lands in the same `order_by` slot a decorated aggregate
25851 // uses; the executor dispatches on the function name. WITHIN
25852 // GROUP and an intra-argument ORDER BY are mutually
25853 // exclusive (PG rejects both).
25854 let within_group_order = self.parse_within_group_clause()?;
25855 if !within_group_order.is_empty() && !agg_order_by.is_empty() {
25856 return Err(self.err(
25857 "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
25858 .into(),
25859 ));
25860 }
25861 let within_group_seen = !within_group_order.is_empty();
25862 let agg_order_by = if within_group_order.is_empty() {
25863 agg_order_by
25864 } else {
25865 within_group_order
25866 };
25867 // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
25868 let filter = self.parse_filter_clause()?;
25869 // v4.12: window-function tail — `name(args) OVER (...)`.
25870 // Promotes the just-parsed FunctionCall into a
25871 // WindowFunction node carrying partition + order.
25872 // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
25873 // / `RESPECT NULLS OVER (...)` between the closing paren
25874 // and `OVER`.
25875 let null_treatment = self.parse_null_treatment_modifier();
25876 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25877 && s.eq_ignore_ascii_case("over")
25878 {
25879 self.advance();
25880 // v7.39 (round 230) — PG implements neither modifier for a
25881 // windowed call and says so (0A000). Both used to be parsed
25882 // and then silently dropped here, so `count(DISTINCT v)
25883 // OVER (…)` quietly answered the non-distinct count.
25884 if agg_distinct {
25885 return Err(
25886 self.err("DISTINCT is not implemented for window functions".to_string())
25887 );
25888 }
25889 if !agg_order_by.is_empty() {
25890 // PG separates the two shapes that land here: a
25891 // WITHIN GROUP call is an ordered-set aggregate and gets
25892 // its own message naming the aggregate; a plain
25893 // `agg(x ORDER BY y)` gets the generic one.
25894 let msg = if within_group_seen {
25895 alloc::format!("OVER is not supported for ordered-set aggregate {first}")
25896 } else {
25897 "aggregate ORDER BY is not implemented for window functions".to_string()
25898 };
25899 return Err(self.err(msg));
25900 }
25901 let (partition_by, order_by, frame) = self.parse_over_clause()?;
25902 return Ok(Expr::WindowFunction {
25903 name: first,
25904 args,
25905 partition_by,
25906 order_by,
25907 frame,
25908 null_treatment,
25909 filter,
25910 });
25911 }
25912 if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
25913 return Ok(Expr::AggregateOrdered {
25914 call: Box::new(Expr::FunctionCall { name: first, args }),
25915 order_by: agg_order_by,
25916 distinct: agg_distinct,
25917 filter,
25918 });
25919 }
25920 // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
25921 // over TIMESTAMPTZ and has no timestamp overload, so a
25922 // timestamp argument is coerced on the way in and the answer
25923 // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
25924 // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
25925 // zone`. SPG answered `timestamp without time zone`, dropping
25926 // the offset from every rendering.
25927 //
25928 // Writing the coercion PG performs makes the existing
25929 // argument-driven typing (the one `date_trunc` uses) reach the
25930 // right answer, rather than teaching the type layer a second
25931 // rule. MySQL's DATE_ADD is a different function that returns
25932 // DATE or DATETIME, so this is PG-dialect only.
25933 //
25934 // Out-of-line because this sits on the RECURSIVE descent
25935 // frame: an inline block with locals here costs every nesting
25936 // level, and the suite's deep-nesting sentinel overflowed the
25937 // 512 KiB parser stack the moment one was added (round 430's
25938 // lesson, in the same shape).
25939 if !self.mysql_dialect {
25940 lift_date_add_arg_to_timestamptz(&first, &mut args);
25941 }
25942 return Ok(Expr::FunctionCall { name: first, args });
25943 }
25944 // v7.9.20 — SQL-standard parenless keyword expressions
25945 // (PG treats these as functions called without parens).
25946 // Resolve to a synthetic FunctionCall so the engine's
25947 // eval path reuses the existing function-call routing.
25948 // mailrs G3.
25949 let lc = first.to_ascii_lowercase();
25950 if matches!(
25951 lc.as_str(),
25952 "current_date"
25953 | "current_time"
25954 | "current_timestamp"
25955 | "localtimestamp"
25956 | "localtime"
25957 // v7.37.17 (17.6 siblings) — session-identity SQL-
25958 // standard parenless keywords. current_user /
25959 // session_user / user were already caught by the
25960 // pgwire canned-response shortcut but bare-select
25961 // in the embedded engine went through Expr::Column
25962 // and errored. Adding them here so the parser
25963 // resolves to a synthetic FunctionCall that reuses
25964 // the existing eval/functions.rs dispatch.
25965 | "current_user"
25966 | "session_user"
25967 | "current_role"
25968 | "current_catalog"
25969 | "current_schema"
25970 | "current_database"
25971 // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
25972 | "system_user"
25973 ) {
25974 return Ok(Expr::FunctionCall {
25975 name: lc,
25976 args: Vec::new(),
25977 });
25978 }
25979 Ok(Expr::Column(ColumnName {
25980 qualifier: None,
25981 name: first,
25982 }))
25983 }
25984}
25985
25986/// v7.39 (round 522) — write the coercion PG's `date_add` /
25987/// `date_subtract` signature performs.
25988///
25989/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
25990/// timestamp argument is cast on the way in and the answer is
25991/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
25992/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
25993/// `timestamp without time zone`, dropping the offset from every
25994/// rendering of the result.
25995///
25996/// Writing the cast the signature implies lets the existing
25997/// argument-driven typing (the one `date_trunc` uses) reach the right
25998/// answer instead of teaching the type layer a second rule. MySQL's
25999/// DATE_ADD is a different function returning DATE or DATETIME, so the
26000/// caller applies this in PG dialect only.
26001///
26002/// A free function, and not a block at the call site, because the caller
26003/// is on the recursive-descent frame chain.
26004#[inline(never)]
26005fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
26006 if args.len() != 2
26007 || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
26008 {
26009 return;
26010 }
26011 let base = args.remove(0);
26012 args.insert(
26013 0,
26014 Expr::Cast {
26015 expr: Box::new(base),
26016 target: CastTarget::Timestamptz,
26017 },
26018 );
26019}
26020
26021/// v6.8.2 — walk an expression tree and return the first column
26022/// reference's bare name. Used by `parse_create_index_stmt_after_create`
26023/// to derive `CreateIndexStatement.column` from an expression
26024/// key (so downstream planner code resolving a primary column
26025/// position keeps working with expression indexes). Returns
26026/// `None` when the expression has no column ref at all — caller
26027/// surfaces that as a parse error.
26028fn extract_first_column(expr: &Expr) -> Option<String> {
26029 match expr {
26030 Expr::Column(cn) => Some(cn.name.clone()),
26031 Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
26032 Expr::Binary { lhs, rhs, .. } => {
26033 extract_first_column(lhs).or_else(|| extract_first_column(rhs))
26034 }
26035 Expr::Unary { expr: e, .. } => extract_first_column(e),
26036 // v7.39 (read01 round 93) — a cast wraps its operand: a common
26037 // expression-index key is `lower(col::text)`, where the column
26038 // sits under the `::text` cast inside the function arg. Without
26039 // descending here the key was rejected as "references no column".
26040 Expr::Cast { expr: e, .. } => extract_first_column(e),
26041 _ => None,
26042 }
26043}
26044
26045fn maybe_not(expr: Expr, negated: bool) -> Expr {
26046 if negated {
26047 Expr::Unary {
26048 op: UnOp::Not,
26049 expr: Box::new(expr),
26050 }
26051 } else {
26052 expr
26053 }
26054}
26055
26056/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
26057/// things in the two dialects, and SPG read all three PG's way:
26058///
26059/// | token | PG (and SPG) | MySQL, measured |
26060/// |---|---|---|
26061/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
26062/// | `&&` | inet / array overlap | **AND** |
26063/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
26064///
26065/// `1 || 0` answering the string '10' on a MySQL session is a wrong
26066/// answer with no error, which is why they are routed here rather than
26067/// left to the shared table.
26068impl Parser {
26069 fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
26070 if self.mysql_dialect {
26071 // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
26072 // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
26073 // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
26074 if let Token::Ident(w) = tok
26075 && w.eq_ignore_ascii_case("div")
26076 {
26077 return Some((BinOp::IntDiv, 8));
26078 }
26079 // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
26080 // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
26081 // the lexer; the `MOD(x, y)` function form is unaffected (MOD
26082 // there sits in operand position, not infix).
26083 if let Token::Ident(w) = tok
26084 && w.eq_ignore_ascii_case("mod")
26085 {
26086 return Some((BinOp::Mod, 8));
26087 }
26088 // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
26089 // plain ident to the lexer. Its precedence sits between OR (1)
26090 // and AND (3) — hence rung 2, the slot freed by moving AND up.
26091 if let Token::Ident(w) = tok
26092 && w.eq_ignore_ascii_case("xor")
26093 {
26094 return Some((BinOp::LogicalXor, 2));
26095 }
26096 match tok {
26097 Token::Concat => return Some((BinOp::Or, 1)),
26098 // MySQL's `&&` is logical AND, sharing AND's rung (3).
26099 Token::InetOverlap => return Some((BinOp::And, 3)),
26100 // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
26101 Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
26102 _ => {}
26103 }
26104 }
26105 binop_from(tok)
26106 }
26107}
26108
26109// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
26110// (which sits strictly between OR and AND), every level from AND upward was
26111// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
26112// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
26113// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
26114// the *relative* order of every PG operator is unchanged by the shift.
26115fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
26116 let pair = match tok {
26117 Token::Or => (BinOp::Or, 1),
26118 Token::And => (BinOp::And, 3),
26119 Token::Eq => (BinOp::Eq, 5),
26120 Token::NotEq => (BinOp::NotEq, 5),
26121 Token::Lt => (BinOp::Lt, 5),
26122 Token::LtEq => (BinOp::LtEq, 5),
26123 Token::Gt => (BinOp::Gt, 5),
26124 Token::GtEq => (BinOp::GtEq, 5),
26125 // pgvector distance ops all sit on the same rung — tighter than
26126 // comparisons (5) so `col <-> v < threshold` parses correctly.
26127 Token::L2Distance => (BinOp::L2Distance, 6),
26128 // v7.39 (read01 geo_ops.c) — geometric predicates ride the
26129 // comparison rung.
26130 Token::GeomParallel => (BinOp::GeomParallel, 5),
26131 // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
26132 // comparison rung.
26133 Token::OverLeft => (BinOp::OverLeft, 5),
26134 Token::OverRight => (BinOp::OverRight, 5),
26135 Token::GeomPerp => (BinOp::GeomPerp, 5),
26136 Token::GeomSameAs => (BinOp::GeomSameAs, 5),
26137 Token::ClosestPoint => (BinOp::ClosestPoint, 6),
26138 Token::GeomHoriz => (BinOp::GeomHoriz, 5),
26139 Token::InnerProduct => (BinOp::InnerProduct, 6),
26140 Token::CosineDistance => (BinOp::CosineDistance, 6),
26141 Token::Plus => (BinOp::Add, 7),
26142 Token::Minus => (BinOp::Sub, 7),
26143 // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
26144 // binds every "other" operator (`||`, `|`, `&`, `#`, the
26145 // pgvector distances above) BETWEEN additive (7) and the
26146 // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
26147 // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
26148 // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
26149 // ("matches PG conceptually" — the round-753 audit measured it
26150 // false; the old rung errored on `'a' || 1 + 1` with
26151 // `text + integer`). Same-level chains left-fold, as PG does.
26152 Token::Concat => (BinOp::Concat, 6),
26153 Token::Pipe => (BinOp::BitOr, 6),
26154 Token::Amp => (BinOp::BitAnd, 6),
26155 Token::Star => (BinOp::Mul, 8),
26156 Token::Slash => (BinOp::Div, 8),
26157 Token::Percent => (BinOp::Mod, 8),
26158 // v4.14: JSON path ops bind tighter than comparisons (5)
26159 // and additive (7) so `doc->'k' = 'v'` parses correctly.
26160 // Same rung as the multiplicative ops.
26161 Token::JsonGet => (BinOp::JsonGet, 8),
26162 Token::JsonGetText => (BinOp::JsonGetText, 8),
26163 Token::JsonGetPath => (BinOp::JsonGetPath, 8),
26164 Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
26165 Token::JsonContains => (BinOp::JsonContains, 8),
26166 Token::JsonPathExists => (BinOp::JsonPathExists, 8),
26167 Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
26168 Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
26169 Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
26170 Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
26171 Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
26172 // v7.12.2 — `@@` binds at the comparison rung (looser than
26173 // arithmetic, tighter than AND / OR). PG places `@@` at
26174 // the same precedence as `=` / `<`, so we follow.
26175 Token::TsMatch => (BinOp::TsMatch, 5),
26176 // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
26177 // PG places these at the comparison rung (same level as `=`),
26178 // so we follow.
26179 Token::InetContainedBy => (BinOp::InetContainedBy, 5),
26180 Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
26181 Token::InetContains => (BinOp::InetContains, 5),
26182 Token::InetContainsEq => (BinOp::InetContainsEq, 5),
26183 Token::InetOverlap => (BinOp::InetOverlap, 5),
26184 // v7.39 (round 508) — the geometric and pattern-order predicates
26185 // ride the comparison rung, as every other predicate does.
26186 Token::Intersects => (BinOp::Intersects, 5),
26187 Token::IsBelow => (BinOp::IsBelow, 5),
26188 Token::IsAbove => (BinOp::IsAbove, 5),
26189 Token::PatternLt => (BinOp::PatternLt, 5),
26190 Token::PatternLtEq => (BinOp::PatternLtEq, 5),
26191 Token::PatternGt => (BinOp::PatternGt, 5),
26192 Token::PatternGtEq => (BinOp::PatternGtEq, 5),
26193 // `@@@` is the old spelling of `@@` and means exactly it.
26194 Token::TsMatchOld => (BinOp::TsMatch, 5),
26195 _ => return None,
26196 };
26197 Some(pair)
26198}
26199
26200#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26201// `as f32` here is intentional: vector elements widen / narrow into f32 on
26202// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
26203// past ~15 decimal digits — both are acceptable for a fixed-precision
26204// pgvector column.
26205/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
26206/// implicit table alias and break trailing clauses. WITH lands
26207/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
26208/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
26209/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
26210/// / VALUES / FOR / LATERAL — all of which would otherwise be
26211/// silently swallowed by `parse_optional_alias`.
26212fn is_alias_stopword(s: &str) -> bool {
26213 matches!(
26214 s.to_ascii_lowercase().as_str(),
26215 "with"
26216 | "on"
26217 | "where"
26218 | "having"
26219 | "group"
26220 | "order"
26221 | "limit"
26222 | "offset"
26223 | "union"
26224 | "except"
26225 | "intersect"
26226 | "returning"
26227 | "set"
26228 | "values"
26229 | "for"
26230 | "window"
26231 | "tablesample"
26232 | "lateral"
26233 | "left"
26234 | "right"
26235 | "inner"
26236 | "outer"
26237 | "full"
26238 | "cross"
26239 | "join"
26240 | "natural"
26241 | "using"
26242 | "fetch"
26243 )
26244}
26245
26246fn extract_numeric_literal(e: &Expr) -> Option<f32> {
26247 match e {
26248 Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
26249 Expr::Literal(Literal::Float(x)) => Some(*x as f32),
26250 // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
26251 // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
26252 // so scale the divisor by hand instead of `f32::powi`.)
26253 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26254 let mut div = 1.0f32;
26255 for _ in 0..*scale {
26256 div *= 10.0;
26257 }
26258 Some(*unscaled as f32 / div)
26259 }
26260 Expr::Unary {
26261 op: UnOp::Neg,
26262 expr,
26263 } => extract_numeric_literal(expr).map(|x| -x),
26264 _ => None,
26265 }
26266}
26267
26268/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
26269/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
26270/// negative. Returns `None` if any pair fails to parse or no pair is found.
26271///
26272/// Recognised units (case-insensitive, optional trailing `s`):
26273/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
26274/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
26275/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
26276/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
26277/// (PG-canonical: DST and month-boundary semantics depend on this).
26278/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
26279/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
26280/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
26281#[allow(clippy::cast_possible_truncation)]
26282fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
26283 let mut months: i64 = 0;
26284 let mut days: i64 = 0;
26285 let mut micros: i64 = 0;
26286 let mut in_time = false;
26287 let mut num = alloc::string::String::new();
26288 for ch in rest.chars() {
26289 if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
26290 num.push(ch);
26291 continue;
26292 }
26293 if ch == 'T' || ch == 't' {
26294 if !num.is_empty() {
26295 return None;
26296 }
26297 in_time = true;
26298 continue;
26299 }
26300 let n: f64 = num.parse().ok()?;
26301 num.clear();
26302 match (ch, in_time) {
26303 ('Y' | 'y', false) => months += (n * 12.0) as i64,
26304 ('M', false) => months += n as i64,
26305 ('W' | 'w', false) => days += (n * 7.0) as i64,
26306 ('D' | 'd', false) => days += n as i64,
26307 ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26308 ('M', true) => micros += (n * 60_000_000.0) as i64,
26309 ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26310 _ => return None,
26311 }
26312 }
26313 if !num.is_empty() {
26314 return None;
26315 }
26316 Some((
26317 i32::try_from(months).ok()?,
26318 i32::try_from(days).ok()?,
26319 micros,
26320 ))
26321}
26322
26323/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
26324/// leading `-` negates the whole value). Rejects date-like strings.
26325fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
26326 let (neg, body) = match s.strip_prefix('-') {
26327 Some(b) => (true, b),
26328 None => (false, s),
26329 };
26330 let (y, m) = body.split_once('-')?;
26331 let years: i32 = y.parse().ok()?;
26332 let mons: i32 = m.parse().ok()?;
26333 if years < 0 || mons < 0 {
26334 return None;
26335 }
26336 let total = years.checked_mul(12)?.checked_add(mons)?;
26337 Some((if neg { -total } else { total }, 0, 0))
26338}
26339
26340/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
26341/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
26342fn parse_interval_clock(tok: &str) -> Option<i64> {
26343 let (neg, body) = match tok.strip_prefix('-') {
26344 Some(r) => (true, r),
26345 None => (false, tok.strip_prefix('+').unwrap_or(tok)),
26346 };
26347 let mut it = body.split(':');
26348 let h: i64 = it.next()?.parse().ok()?;
26349 let m: i64 = it.next()?.parse().ok()?;
26350 let s_tok = it.next().unwrap_or("0");
26351 if it.next().is_some() {
26352 return None;
26353 }
26354 let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
26355 let sec: i64 = sec.parse().ok()?;
26356 let mut f = alloc::string::String::from(frac);
26357 while f.len() < 6 {
26358 f.push('0');
26359 }
26360 f.truncate(6);
26361 let fus: i64 = f.parse().ok()?;
26362 sec.checked_mul(1_000_000)?.checked_add(fus)?
26363 } else {
26364 s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
26365 };
26366 let total = h
26367 .checked_mul(3_600_000_000)?
26368 .checked_add(m.checked_mul(60_000_000)?)?
26369 .checked_add(sec_us)?;
26370 Some(if neg { -total } else { total })
26371}
26372
26373/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
26374/// every spelling PG accepts (measured against live PG18.4, not guessed):
26375/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
26376/// Before this, the unit table matched long names only, with an ad-hoc
26377/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
26378/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
26379/// INTERVAL", and it had also grown arms for the debris that stripping leaves
26380/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
26381/// fractional) both read from this one table now.
26382fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
26383 let u = raw.to_ascii_lowercase();
26384 Some(match u.as_str() {
26385 "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
26386 "microsecond"
26387 }
26388 "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
26389 "millisecond"
26390 }
26391 "second" | "seconds" | "sec" | "secs" | "s" => "second",
26392 "minute" | "minutes" | "min" | "mins" | "m" => "minute",
26393 "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
26394 "day" | "days" | "d" => "day",
26395 "week" | "weeks" | "w" => "week",
26396 "month" | "months" | "mon" | "mons" => "month",
26397 "year" | "years" | "yr" | "yrs" | "y" => "year",
26398 "decade" | "decades" | "dec" | "decs" => "decade",
26399 "century" | "centuries" | "cent" | "c" => "century",
26400 "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
26401 _ => return None,
26402 })
26403}
26404
26405/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
26406/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
26407#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26408pub(crate) enum IntervalField {
26409 Year,
26410 Month,
26411 Day,
26412 Hour,
26413 Minute,
26414 Second,
26415}
26416
26417/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
26418/// spellings aren't standard for the qualifier position, so only the singular
26419/// forms are accepted.
26420/// v7.39 (round 350, M7) — MySQL's interval units, measured against
26421/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
26422/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
26423/// take a `'1 2'` style literal — are not read here; they stay a parse
26424/// error rather than being silently misread.)
26425/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
26426///
26427/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
26428/// to do with a `@@` engine setting, and an unset one reads NULL rather
26429/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
26430/// were the same node and `SELECT @x` answered "Unknown system variable".)
26431/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
26432/// not see a session override — measured, after `SET autocommit=0`,
26433/// `@@global.autocommit` is still 1.
26434///
26435/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
26436/// the parser's nesting budget is tuned against, and building these
26437/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
26438/// wall `parse_left_right_atom` and friends were factored out for).
26439#[inline(never)]
26440fn variable_ref_atom(raw: &str) -> Expr {
26441 let user_var = !raw.starts_with("@@");
26442 let bare = raw.trim_start_matches('@').to_ascii_lowercase();
26443 Expr::FunctionCall {
26444 name: String::from(if user_var {
26445 "__spg_user_var"
26446 } else {
26447 "__spg_session_var"
26448 }),
26449 args: alloc::vec![Expr::Literal(Literal::String(bare))],
26450 }
26451}
26452
26453fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
26454 let Token::Ident(s) = tok else { return None };
26455 Some(match () {
26456 () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
26457 () if s.eq_ignore_ascii_case("second") => "second",
26458 () if s.eq_ignore_ascii_case("minute") => "minute",
26459 () if s.eq_ignore_ascii_case("hour") => "hour",
26460 () if s.eq_ignore_ascii_case("day") => "day",
26461 () if s.eq_ignore_ascii_case("week") => "week",
26462 () if s.eq_ignore_ascii_case("month") => "month",
26463 () if s.eq_ignore_ascii_case("quarter") => "quarter",
26464 () if s.eq_ignore_ascii_case("year") => "year",
26465 () => return None,
26466 })
26467}
26468
26469/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
26470/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
26471/// which constructs the value at run time. Only the slot the unit names
26472/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
26473/// slot the builtin has (months and fractional seconds respectively).
26474fn make_interval_call(qty: Expr, unit: &str) -> Expr {
26475 let zero = || Expr::Literal(Literal::Integer(0));
26476 let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
26477 lhs: alloc::boxed::Box::new(qty.clone()),
26478 op,
26479 rhs: alloc::boxed::Box::new(by),
26480 };
26481 // (years, months, weeks, days, hours, mins, secs)
26482 let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
26483 match unit {
26484 "year" => args[0] = qty,
26485 "quarter" => {
26486 args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
26487 }
26488 "month" => args[1] = qty,
26489 "week" => args[2] = qty,
26490 "day" => args[3] = qty,
26491 "hour" => args[4] = qty,
26492 "minute" => args[5] = qty,
26493 "second" => args[6] = qty,
26494 // The builtin's seconds slot takes a fraction, so microseconds ride
26495 // it scaled down; the divisor is a NUMERIC literal so the division
26496 // stays exact rather than going through a float.
26497 "microsecond" => {
26498 args[6] = scaled(
26499 crate::ast::BinOp::Div,
26500 Expr::Literal(Literal::Numeric {
26501 unscaled: 1_000_000,
26502 scale: 0,
26503 }),
26504 );
26505 }
26506 _ => args[3] = qty,
26507 }
26508 Expr::FunctionCall {
26509 name: alloc::string::String::from("make_interval"),
26510 args,
26511 }
26512}
26513
26514/// `(count, unit)` → `(months, days, micros)`.
26515fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
26516 let n: i64 = count.trim().parse().ok()?;
26517 Some(match unit {
26518 "microsecond" => (0, 0, n),
26519 "second" => (0, 0, n.checked_mul(1_000_000)?),
26520 "minute" => (0, 0, n.checked_mul(60_000_000)?),
26521 "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
26522 "day" => (0, i32::try_from(n).ok()?, 0),
26523 "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
26524 "month" => (i32::try_from(n).ok()?, 0, 0),
26525 "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
26526 "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
26527 _ => return None,
26528 })
26529}
26530
26531fn interval_field_of(tok: &Token) -> Option<IntervalField> {
26532 let Token::Ident(s) = tok else { return None };
26533 Some(match () {
26534 () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
26535 () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
26536 () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
26537 () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
26538 () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
26539 () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
26540 () => return None,
26541 })
26542}
26543
26544/// v7.39 (read01 round 102) — interpret an interval literal under a field
26545/// qualifier. Returns `(months, days, micros)`.
26546///
26547/// * A single field applied to a bare number sets which unit the number means,
26548/// truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
26549/// SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
26550/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
26551/// * Every other range, and any literal a single field can't read as a plain
26552/// number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
26553/// interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
26554/// like PG, and the qualifier there only bounds precision.
26555fn interpret_qualified_interval(
26556 text: &str,
26557 (f1, f2): (IntervalField, Option<IntervalField>),
26558) -> Option<(i32, i32, i64)> {
26559 if let Some(f2) = f2 {
26560 if f1 == IntervalField::Year && f2 == IntervalField::Month {
26561 if let Some(m) = parse_year_month_literal(text) {
26562 return Some((m, 0, 0));
26563 }
26564 }
26565 return parse_interval_text(text);
26566 }
26567 // Single field: reinterpret a bare number; otherwise the default parse.
26568 let trimmed = text.trim();
26569 if let Ok(val) = trimmed.parse::<f64>() {
26570 // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
26571 #[allow(clippy::cast_possible_truncation)]
26572 let whole = val as i64;
26573 #[allow(clippy::cast_possible_truncation)]
26574 let secs_micros = {
26575 let m = val * 1_000_000.0;
26576 if m >= 0.0 {
26577 (m + 0.5) as i64
26578 } else {
26579 (m - 0.5) as i64
26580 }
26581 };
26582 return Some(match f1 {
26583 IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
26584 IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
26585 IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
26586 IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
26587 IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
26588 IntervalField::Second => (0, 0, secs_micros),
26589 });
26590 }
26591 parse_interval_text(text)
26592}
26593
26594/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
26595fn parse_year_month_literal(text: &str) -> Option<i32> {
26596 let t = text.trim();
26597 let (neg, body) = match t.strip_prefix('-') {
26598 Some(r) => (true, r),
26599 None => (false, t.strip_prefix('+').unwrap_or(t)),
26600 };
26601 let mut it = body.split('-');
26602 let years: i32 = it.next()?.trim().parse().ok()?;
26603 let months: i32 = match it.next() {
26604 Some(m) => m.trim().parse().ok()?,
26605 None => 0,
26606 };
26607 if it.next().is_some() {
26608 return None;
26609 }
26610 let total = years.checked_mul(12)?.checked_add(months)?;
26611 Some(if neg { -total } else { total })
26612}
26613
26614pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
26615 // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
26616 // `@` is decorative; a trailing `ago` negates the whole interval.
26617 let mut trimmed = s.trim();
26618 trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
26619 let mut negate = false;
26620 if let Some(rest) = trimmed
26621 .strip_suffix("ago")
26622 .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
26623 {
26624 negate = true;
26625 trimmed = rest.trim();
26626 }
26627 let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
26628 let (mo, d, us) = v?;
26629 if negate {
26630 Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
26631 } else {
26632 Some((mo, d, us))
26633 }
26634 };
26635 let s = trimmed;
26636 // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
26637 // are single tokens, not the `<n> <unit>` pair form handled below.
26638 if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
26639 return finish(parse_iso8601_interval(rest));
26640 }
26641 if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
26642 if let Some(iv) = parse_year_month_interval(trimmed) {
26643 return finish(Some(iv));
26644 }
26645 }
26646 // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
26647 // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
26648 // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
26649 if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
26650 if let Ok(n) = trimmed.parse::<i64>() {
26651 return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
26652 }
26653 if let Ok(f) = trimmed.parse::<f64>() {
26654 if f.is_finite() {
26655 #[allow(clippy::cast_possible_truncation)]
26656 return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
26657 }
26658 }
26659 }
26660 // v7.39 (round 243) — PG accepts the number and unit run together
26661 // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
26662 // the `<n> <unit>` pair loop below sees them as two.
26663 let raw_parts: Vec<&str> = s.split_whitespace().collect();
26664 let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
26665 for p in raw_parts {
26666 let boundary = p
26667 .char_indices()
26668 .find(|(i, c)| {
26669 *i > 0
26670 && c.is_ascii_alphabetic()
26671 && p[..*i]
26672 .chars()
26673 .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
26674 && p[..*i].chars().any(|d| d.is_ascii_digit())
26675 })
26676 .map(|(i, _)| i);
26677 match boundary {
26678 Some(i) => {
26679 parts.push(&p[..i]);
26680 parts.push(&p[i..]);
26681 }
26682 None => parts.push(p),
26683 }
26684 }
26685 // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
26686 // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
26687 // remains is the `<n> <unit>` pair form handled below.
26688 let mut clock_us: i64 = 0;
26689 let mut had_clock = false;
26690 if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
26691 clock_us = parse_interval_clock(parts[pos])?;
26692 parts.remove(pos);
26693 had_clock = true;
26694 }
26695 // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
26696 // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
26697 let mut lone_days: i32 = 0;
26698 if had_clock && parts.len() == 1 {
26699 if let Ok(n) = parts[0].parse::<i64>() {
26700 lone_days = i32::try_from(n).ok()?;
26701 parts.clear();
26702 }
26703 }
26704 if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
26705 return None;
26706 }
26707 let mut months: i32 = 0;
26708 let mut days: i32 = lone_days;
26709 let mut micros: i64 = clock_us;
26710 let mut i = 0;
26711 while i < parts.len() {
26712 let unit_stripped = canonical_interval_unit(parts[i + 1])?;
26713 if let Ok(n) = parts[i].parse::<i64>() {
26714 match unit_stripped {
26715 "microsecond" => micros = micros.checked_add(n)?,
26716 "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
26717 "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
26718 "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
26719 "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
26720 "day" => {
26721 let n32 = i32::try_from(n).ok()?;
26722 days = days.checked_add(n32)?;
26723 }
26724 "week" => {
26725 let n32 = i32::try_from(n).ok()?;
26726 days = days.checked_add(n32.checked_mul(7)?)?;
26727 }
26728 "month" => {
26729 let n32 = i32::try_from(n).ok()?;
26730 months = months.checked_add(n32)?;
26731 }
26732 "year" => {
26733 let n32 = i32::try_from(n).ok()?;
26734 months = months.checked_add(n32.checked_mul(12)?)?;
26735 }
26736 // v7.39 (read01 timestamp.c) — the larger calendar units.
26737 "decade" => {
26738 let n32 = i32::try_from(n).ok()?;
26739 months = months.checked_add(n32.checked_mul(120)?)?;
26740 }
26741 "century" => {
26742 let n32 = i32::try_from(n).ok()?;
26743 months = months.checked_add(n32.checked_mul(1200)?)?;
26744 }
26745 "millennium" => {
26746 let n32 = i32::try_from(n).ok()?;
26747 months = months.checked_add(n32.checked_mul(12000)?)?;
26748 }
26749 _ => return None,
26750 }
26751 } else if let Ok(f) = parts[i].parse::<f64>() {
26752 // Fractional units cascade down to the next-finer field the way
26753 // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
26754 // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
26755 // no_std: f64 has no trunc/fract/round methods, so do them with
26756 // casts (toward-zero) + explicit round-half-away-from-zero.
26757 #[allow(clippy::cast_possible_truncation)]
26758 fn round_i64(x: f64) -> i64 {
26759 if x >= 0.0 {
26760 (x + 0.5) as i64
26761 } else {
26762 (x - 0.5) as i64
26763 }
26764 }
26765 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26766 fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
26767 const DAY_US: f64 = 86_400_000_000.0;
26768 let whole = d as i64; // truncates toward zero
26769 let frac = d - whole as f64;
26770 *days = days.checked_add(i32::try_from(whole).ok()?)?;
26771 *micros = micros.checked_add(round_i64(frac * DAY_US))?;
26772 Some(())
26773 }
26774 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26775 match unit_stripped {
26776 "microsecond" => micros = micros.checked_add(round_i64(f))?,
26777 "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
26778 "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
26779 "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
26780 "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
26781 "day" => add_days_frac(&mut days, &mut micros, f)?,
26782 "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
26783 "month" => {
26784 let whole = f as i64;
26785 months = months.checked_add(i32::try_from(whole).ok()?)?;
26786 add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
26787 }
26788 "year" => {
26789 let m = f * 12.0;
26790 let whole = m as i64;
26791 months = months.checked_add(i32::try_from(whole).ok()?)?;
26792 add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
26793 }
26794 _ => return None,
26795 }
26796 } else {
26797 return None;
26798 }
26799 i += 2;
26800 }
26801 finish(Some((months, days, micros)))
26802}
26803
26804/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
26805/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
26806/// `interval` is intentionally absent (handled by its own parser arm).
26807/// Returns `None` for names that aren't sensible as a bare typed literal, so
26808/// the caller falls back to treating the ident as a column reference.
26809fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
26810 Some(match ident {
26811 "date" => CastTarget::Date,
26812 "timestamp" | "datetime" => CastTarget::Timestamp,
26813 "timestamptz" => CastTarget::Timestamptz,
26814 "bool" | "boolean" => CastTarget::Bool,
26815 "int" | "integer" | "int4" => CastTarget::Int,
26816 "bigint" | "int8" => CastTarget::BigInt,
26817 "float8" | "double precision" => CastTarget::Float,
26818 "uuid" => CastTarget::Uuid,
26819 "bytea" => CastTarget::Bytea,
26820 "json" => CastTarget::Json,
26821 "jsonb" => CastTarget::Jsonb,
26822 // Types without a dedicated CastTarget variant flow through the
26823 // generic Named path (engine resolves via column_type_to_data_type).
26824 "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
26825 | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
26826 | "money" | "bit" | "varbit"
26827 // Geometric types accept the `TYPE 'literal'` prefix spelling too.
26828 | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
26829 // Range / multirange types likewise.
26830 | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
26831 | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
26832 | "datemultirange" | "tsmultirange" | "tstzmultirange"
26833 // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
26834 | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
26835 CastTarget::Named(alloc::string::String::from(ident))
26836 }
26837 _ => return None,
26838 })
26839}
26840
26841/// v7.12.4 — map a bare type-name identifier (the form that
26842/// appears in a function arg list or RETURNS clause) to a
26843/// [`ColumnTypeName`]. Returns `None` for unknown / extension
26844/// types so the caller can preserve them as
26845/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
26846///
26847/// Subset of the full column-type grammar — we deliberately
26848/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
26849/// here because function-arg types in v7.12.4 are mostly the
26850/// bare form (`text`, `int`, `bytea`, …).
26851/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
26852/// than being `name TYPE`?
26853///
26854/// The multi-word spellings SQL allows for a bare argument type, each
26855/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
26856///
26857/// NOTE this list also exists in `spg-storage`, which computes the
26858/// signature key from the rendered argument text and has to reach the
26859/// same verdict. The two crates are siblings — neither depends on the
26860/// other — and each already carries its own table of type spellings
26861/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
26862/// there), so this follows the structure rather than inventing new
26863/// duplication. Recorded as V49.
26864pub fn is_multiword_type_phrase(phrase: &str) -> bool {
26865 let t = phrase.trim().to_ascii_lowercase();
26866 let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
26867 matches!(
26868 base,
26869 "double precision"
26870 | "character varying"
26871 | "bit varying"
26872 | "timestamp with time zone"
26873 | "timestamp without time zone"
26874 | "time with time zone"
26875 | "time without time zone"
26876 | "national character"
26877 | "national character varying"
26878 )
26879}
26880
26881fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
26882 Some(match ident.to_ascii_lowercase().as_str() {
26883 "smallint" | "tinyint" => ColumnTypeName::SmallInt,
26884 "int" | "integer" | "mediumint" => ColumnTypeName::Int,
26885 "bigint" => ColumnTypeName::BigInt,
26886 "float" | "double" => ColumnTypeName::Float,
26887 // v7.39 (round 269) — real is 32-bit.
26888 "real" | "float4" => ColumnTypeName::Real,
26889 "text" => ColumnTypeName::Text,
26890 "bool" | "boolean" => ColumnTypeName::Bool,
26891 "date" => ColumnTypeName::Date,
26892 "timestamp" | "datetime" => ColumnTypeName::Timestamp,
26893 "timestamptz" => ColumnTypeName::Timestamptz,
26894 "json" => ColumnTypeName::Json,
26895 "jsonb" => ColumnTypeName::Jsonb,
26896 "bytea" | "bytes" => ColumnTypeName::Bytes,
26897 "tsvector" => ColumnTypeName::TsVector,
26898 "tsquery" => ColumnTypeName::TsQuery,
26899 "uuid" => ColumnTypeName::Uuid,
26900 "interval" => ColumnTypeName::Interval,
26901 "time" => ColumnTypeName::Time,
26902 "year" => ColumnTypeName::Year,
26903 "timetz" => ColumnTypeName::TimeTz,
26904 "money" => ColumnTypeName::Money,
26905 _ => return None,
26906 })
26907}
26908
26909/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
26910/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
26911///
26912/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
26913/// / embedded SQL land in v7.12.5+):
26914///
26915/// ```text
26916/// body := [ws] block [ws]
26917/// block := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
26918/// stmt := assign | return
26919/// assign := assign_target := expr
26920/// assign_target := ( NEW | OLD ) . ident | ident
26921/// return := RETURN ( NEW | OLD | NULL | expr )
26922/// ```
26923///
26924/// `expr` is parsed by recursing into the regular `Parser` — so a
26925/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
26926/// NEW.subject || ' ' || NEW.sender)` body shape works without
26927/// the body parser knowing what `to_tsvector` is.
26928///
26929/// Errors here cause the caller to fall back to
26930/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
26931/// successful, but the executor will refuse to invoke the
26932/// function with an "unparseable body" error.
26933/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
26934/// from the crate root as `spg_sql::parse_function_body`.
26935pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26936 parse_plpgsql_body(body)
26937}
26938
26939fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26940 // Use the regular lexer on the body text. The trailing
26941 // `END;` may or may not have a semicolon; the lexer treats
26942 // both forms identically.
26943 let tokens = lexer::tokenize(body).map_err(|e| ParseError {
26944 message: alloc::format!("plpgsql body lex error: {e}"),
26945 token_pos: 0,
26946 })?;
26947 let mut parser = Parser::new(tokens);
26948 parser.parse_plpgsql_block()
26949}
26950
26951/// v7.39 (GUC) — the textual body of a SET value, for list joining.
26952fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
26953 match v {
26954 crate::ast::SetValue::String(s)
26955 | crate::ast::SetValue::Ident(s)
26956 | crate::ast::SetValue::Number(s) => s.clone(),
26957 crate::ast::SetValue::Default => "DEFAULT".into(),
26958 }
26959}
26960
26961/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
26962/// contains an aggregate call at ITS OWN query level (recursion stops at
26963/// sublink boundaries — a sublink's aggregates belong to the sublink).
26964/// Backs the "aggregate functions are not allowed in a recursive query's
26965/// recursive term" well-formedness check.
26966fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
26967 const AGG_NAMES: &[&str] = &[
26968 "count",
26969 "sum",
26970 "min",
26971 "max",
26972 "avg",
26973 "string_agg",
26974 "array_agg",
26975 "bool_and",
26976 "bool_or",
26977 "every",
26978 "any_value",
26979 "json_agg",
26980 "jsonb_agg",
26981 "json_object_agg",
26982 "jsonb_object_agg",
26983 "bit_and",
26984 "bit_or",
26985 "bit_xor",
26986 "var_pop",
26987 "var_samp",
26988 "variance",
26989 "stddev",
26990 "stddev_pop",
26991 "stddev_samp",
26992 "range_agg",
26993 "range_intersect_agg",
26994 "percentile_cont",
26995 "percentile_disc",
26996 "mode",
26997 "corr",
26998 "covar_pop",
26999 "covar_samp",
27000 ];
27001 match e {
27002 Expr::AggregateOrdered { .. } => true,
27003 Expr::FunctionCall { name, args } => {
27004 AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
27005 || args.iter().any(expr_has_toplevel_aggregate)
27006 }
27007 Expr::NamedArg { expr, .. }
27008 | Expr::Variadic(expr)
27009 | Expr::Unary { expr, .. }
27010 | Expr::Cast { expr, .. }
27011 | Expr::IsNull { expr, .. }
27012 | Expr::FieldAccess { base: expr, .. }
27013 | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
27014 Expr::Binary { lhs, rhs, .. } => {
27015 expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
27016 }
27017 Expr::Like { expr, pattern, .. } => {
27018 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
27019 }
27020 Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
27021 Expr::InList { expr, list, .. } => {
27022 expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
27023 }
27024 Expr::ArraySubscript { target, index } => {
27025 expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
27026 }
27027 Expr::ArraySlice { target, lo, hi } => {
27028 expr_has_toplevel_aggregate(target)
27029 || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
27030 || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
27031 }
27032 Expr::AnyAll { expr, array, .. } => {
27033 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
27034 }
27035 Expr::Case {
27036 operand,
27037 branches,
27038 else_branch,
27039 } => {
27040 operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
27041 || branches
27042 .iter()
27043 .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
27044 || else_branch
27045 .as_deref()
27046 .is_some_and(expr_has_toplevel_aggregate)
27047 }
27048 // The outer-level operands of a sublink can aggregate; the sublink's
27049 // own body cannot leak its aggregates up here.
27050 Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
27051 Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
27052 row.iter().any(expr_has_toplevel_aggregate)
27053 }
27054 _ => false,
27055 }
27056}
27057
27058/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
27059/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
27060/// named table anywhere in its subtree. A plain FROM derived table is NOT a
27061/// sublink and is legal in a recursive term, so it is not walked here.
27062fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
27063 let mut exprs: Vec<&Expr> = Vec::new();
27064 for it in &s.items {
27065 if let crate::ast::SelectItem::Expr { expr, .. } = it {
27066 exprs.push(expr);
27067 }
27068 }
27069 if let Some(w) = &s.where_ {
27070 exprs.push(w);
27071 }
27072 if let Some(h) = &s.having {
27073 exprs.push(h);
27074 }
27075 if let Some(g) = &s.group_by {
27076 exprs.extend(g.iter());
27077 }
27078 if let Some(from) = &s.from {
27079 for j in &from.joins {
27080 if let Some(on) = &j.on {
27081 exprs.push(on);
27082 }
27083 }
27084 }
27085 exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
27086}
27087
27088/// Does this expression contain a sublink whose subquery mentions `name`?
27089fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
27090 match e {
27091 Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
27092 Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
27093 Expr::InSubquery { expr, subquery, .. } => {
27094 expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
27095 }
27096 Expr::RowInSubquery { row, subquery, .. } => {
27097 row.iter().any(|x| expr_sublink_mentions(x, name))
27098 || select_mentions_table(subquery, name)
27099 }
27100 Expr::RowCmpSubquery { row, subquery, .. } => {
27101 row.iter().any(|x| expr_sublink_mentions(x, name))
27102 || select_mentions_table(subquery, name)
27103 }
27104 Expr::NamedArg { expr, .. }
27105 | Expr::Variadic(expr)
27106 | Expr::Unary { expr, .. }
27107 | Expr::Cast { expr, .. }
27108 | Expr::IsNull { expr, .. }
27109 | Expr::FieldAccess { base: expr, .. }
27110 | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
27111 Expr::Binary { lhs, rhs, .. } => {
27112 expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
27113 }
27114 Expr::Like { expr, pattern, .. } => {
27115 expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
27116 }
27117 Expr::FunctionCall { args, .. } | Expr::Array(args) => {
27118 args.iter().any(|x| expr_sublink_mentions(x, name))
27119 }
27120 Expr::InList { expr, list, .. } => {
27121 expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
27122 }
27123 Expr::ArraySubscript { target, index } => {
27124 expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
27125 }
27126 Expr::ArraySlice { target, lo, hi } => {
27127 expr_sublink_mentions(target, name)
27128 || lo
27129 .as_deref()
27130 .is_some_and(|x| expr_sublink_mentions(x, name))
27131 || hi
27132 .as_deref()
27133 .is_some_and(|x| expr_sublink_mentions(x, name))
27134 }
27135 Expr::AnyAll { expr, array, .. } => {
27136 expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
27137 }
27138 Expr::Case {
27139 operand,
27140 branches,
27141 else_branch,
27142 } => {
27143 operand
27144 .as_deref()
27145 .is_some_and(|x| expr_sublink_mentions(x, name))
27146 || branches
27147 .iter()
27148 .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
27149 || else_branch
27150 .as_deref()
27151 .is_some_and(|x| expr_sublink_mentions(x, name))
27152 }
27153 _ => false,
27154 }
27155}
27156
27157/// Does this SELECT (in full — FROM tables, derived tables, its own
27158/// sublinks, and union arms) mention the named table?
27159fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
27160 if let Some(from) = &s.from {
27161 if from.primary.name.eq_ignore_ascii_case(name) {
27162 return true;
27163 }
27164 if let Some(sub) = &from.primary.lateral_subquery
27165 && select_mentions_table(sub, name)
27166 {
27167 return true;
27168 }
27169 for j in &from.joins {
27170 if j.table.name.eq_ignore_ascii_case(name) {
27171 return true;
27172 }
27173 if let Some(sub) = &j.table.lateral_subquery
27174 && select_mentions_table(sub, name)
27175 {
27176 return true;
27177 }
27178 }
27179 }
27180 if select_has_self_ref_in_sublink(s, name) {
27181 return true;
27182 }
27183 s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
27184}
27185
27186/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
27187/// row count, the way PG evaluates one before applying it.
27188///
27189/// `None` = not a constant (a column, a subquery, a function call).
27190/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
27191/// message stands in for LIMIT / OFFSET, which the caller substitutes.
27192/// All wordings were read off live PG 18.4.
27193fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
27194 use crate::ast::{BinOp, Expr, Literal, UnOp};
27195 match e {
27196 Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
27197 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27198 Some(Ok(round_scaled_half_away(*unscaled, *scale)))
27199 }
27200 // PG coerces a string by its CONTENT, and fails on the value.
27201 Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
27202 |_| {
27203 Err(alloc::format!(
27204 "invalid input syntax for type bigint: \"{t}\""
27205 ))
27206 },
27207 |n| Ok(i128::from(n)),
27208 )),
27209 Expr::Literal(Literal::Bool(_)) => Some(Err(
27210 "argument of {L} must be type bigint, not type boolean".into(),
27211 )),
27212 Expr::Unary {
27213 op: UnOp::Neg,
27214 expr,
27215 } => match fold_limit_constant(expr)? {
27216 Ok(v) => Some(Ok(-v)),
27217 e @ Err(_) => Some(e),
27218 },
27219 Expr::Binary { lhs, op, rhs } => {
27220 let a = match fold_limit_constant(lhs)? {
27221 Ok(v) => v,
27222 e @ Err(_) => return Some(e),
27223 };
27224 let b = match fold_limit_constant(rhs)? {
27225 Ok(v) => v,
27226 e @ Err(_) => return Some(e),
27227 };
27228 let out = match op {
27229 BinOp::Add => a.checked_add(b),
27230 BinOp::Sub => a.checked_sub(b),
27231 BinOp::Mul => a.checked_mul(b),
27232 BinOp::Div if b != 0 => a.checked_div(b),
27233 BinOp::Div => return Some(Err("division by zero".into())),
27234 BinOp::Mod if b != 0 => a.checked_rem(b),
27235 BinOp::Mod => return Some(Err("division by zero".into())),
27236 _ => return None,
27237 };
27238 // PG evaluates the arithmetic in the operand's own type, so an
27239 // int-by-int product that leaves int range fails there — before
27240 // the row count is ever looked at.
27241 match out {
27242 Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
27243 Some(Err("integer out of range".into()))
27244 }
27245 Some(v) => Some(Ok(v)),
27246 None => Some(Err("integer out of range".into())),
27247 }
27248 }
27249 _ => None,
27250 }
27251}
27252
27253/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
27254/// cast, which is what makes `LIMIT 2.5` keep three rows.
27255fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
27256 if scale == 0 {
27257 return unscaled;
27258 }
27259 let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
27260 return 0;
27261 };
27262 let neg = unscaled < 0;
27263 let mag = unscaled.unsigned_abs() as i128;
27264 let rounded = (mag + div / 2) / div;
27265 if neg { -rounded } else { rounded }
27266}
27267
27268#[cfg(test)]
27269mod tests {
27270 use super::*;
27271 use alloc::string::ToString;
27272
27273 fn parse(s: &str) -> Statement {
27274 parse_statement(s).expect("parse ok")
27275 }
27276
27277 // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
27278 // `tables`, `partition`, etc. are unreserved keywords per PG's
27279 // `pg_get_keywords()` and MUST be usable as column / table /
27280 // alias names. Pre-T4 every drop-in user whose schema had one
27281 // of these as a column name (sentori events.release, mailrs
27282 // messages.index in some forks) blew the parser up at CREATE
27283 // TABLE time with "expected identifier, got Release". The
27284 // generalisation lives in `unreserved_keyword_text` + the
27285 // `expect_ident_like` and `parse_atom` arms that consult it.
27286 #[test]
27287 fn release_usable_as_column_name_in_create_table() {
27288 let stmt =
27289 parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
27290 if let Statement::CreateTable(t) = stmt {
27291 let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
27292 assert_eq!(names, alloc::vec!["id", "release", "payload"]);
27293 } else {
27294 panic!("expected CreateTable");
27295 }
27296 }
27297
27298 #[test]
27299 fn release_usable_as_column_ref_in_select_projection() {
27300 // The sentori `0003_partition_events.sql` INSERT-SELECT
27301 // walk references `release` in both column lists; the
27302 // projection-side use exercises `parse_atom`'s relaxed
27303 // identifier set.
27304 parse("SELECT id, release, payload FROM events WHERE id = 1");
27305 }
27306
27307 #[test]
27308 fn release_usable_as_column_ref_in_insert_column_list() {
27309 // INSERT INTO t (id, release, payload) VALUES (…)
27310 parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
27311 }
27312
27313 #[test]
27314 fn alter_column_drop_not_null_uses_keyword_drop_token() {
27315 // Sentori `0013_audit_tombstone.sql` issues
27316 // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
27317 // emits Token::Drop (not Ident("drop")); the parser must
27318 // accept both in the ALTER COLUMN sub-dispatch.
27319 parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
27320 }
27321
27322 #[test]
27323 fn create_index_accepts_parenthesised_expression_key() {
27324 // sentori `0040_events_bundle_idx.sql` shape — JSONB
27325 // expression index. Pre-T4 the parser bailed at the
27326 // inner `(` with "expected column ident or expression,
27327 // got LParen". The Token::LParen arm in CREATE INDEX
27328 // routes through the expression parser instead.
27329 parse(
27330 "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
27331 ON events ((payload->'bundle'->>'id'))",
27332 );
27333 }
27334
27335 // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
27336 // surface as parse errors, never stack overflows (embed hosts
27337 // abort on overflow).
27338 /// The nesting budget is a COUNT; what it has to fit inside is a
27339 /// number of BYTES, and only one of those two is stable across
27340 /// compiler versions. Round 847 measured 30,336 bytes per level
27341 /// after a toolchain move, which puts 64 levels at 1.94 MB and
27342 /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
27343 /// aborted instead of erroring, which is precisely the outcome it
27344 /// exists to rule out.
27345 ///
27346 /// So the budget is metered rather than assumed. The ceiling leaves
27347 /// the depth SPG advertises fitting in a default 2 MiB thread with
27348 /// room to spare, in the debug build, where frames are widest.
27349 #[test]
27350 fn nesting_frame_cost_stays_under_ceiling() {
27351 // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
27352 // thread keeps a margin for whatever called the parser.
27353 const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
27354
27355 frame_meter::reset();
27356 let depth = frame_meter::SAMPLE_HI + 8;
27357 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27358 parse(&sql);
27359
27360 let per_level = frame_meter::bytes_per_level();
27361 {
27362 extern crate std;
27363 std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
27364 }
27365 assert!(
27366 per_level <= CEILING,
27367 "{per_level} bytes per nesting level exceeds {CEILING}; \
27368 {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
27369 in parse_expr_inner / parse_unary rather than lowering the \
27370 depth or widening the stack.",
27371 per_level * MAX_NEST_DEPTH
27372 );
27373 }
27374
27375 #[test]
27376 fn nesting_budget_errors_cleanly() {
27377 let depth = MAX_NEST_DEPTH + 50;
27378 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27379 let err = parse_statement(&sql).expect_err("must reject");
27380 assert!(err.message.contains("nests deeper"), "{err:?}");
27381 // Within budget still parses.
27382 let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
27383 parse(&sql);
27384 }
27385
27386 #[test]
27387 fn binary_chain_budget_errors_cleanly() {
27388 let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
27389 let err = parse_statement(&sql).expect_err("must reject");
27390 assert!(err.message.contains("chained binary"), "{err:?}");
27391 // Within budget still parses (chain depth ≤ budget is safe
27392 // for recursive eval/drop on 2 MiB stacks).
27393 let sql = format!("SELECT 1{}", " + 1".repeat(200));
27394 parse(&sql);
27395 }
27396
27397 #[test]
27398 fn in_list_unaffected_by_chain_budget() {
27399 // Flat InList: 20k elements parse fine and stay flat.
27400 let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
27401 let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
27402 let Statement::Select(s) = parse(&sql) else {
27403 panic!("expected select")
27404 };
27405 let Some(Expr::InList { list, negated, .. }) = s.where_ else {
27406 panic!("expected flat InList, got {:?}", s.where_)
27407 };
27408 assert_eq!(list.len(), 20_000);
27409 assert!(!negated);
27410 }
27411
27412 fn lit_int(n: i64) -> Expr {
27413 Expr::Literal(Literal::Integer(n))
27414 }
27415
27416 fn col(name: &str) -> Expr {
27417 Expr::Column(ColumnName {
27418 qualifier: None,
27419 name: name.into(),
27420 })
27421 }
27422
27423 #[test]
27424 fn select_single_integer() {
27425 let s = parse("SELECT 1");
27426 let Statement::Select(s) = s else {
27427 panic!("expected SELECT")
27428 };
27429 assert_eq!(s.items.len(), 1);
27430 assert!(s.from.is_none());
27431 assert!(s.where_.is_none());
27432 }
27433
27434 #[test]
27435 fn select_multiple_literal_kinds() {
27436 let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
27437 let Statement::Select(s) = s else {
27438 panic!("expected SELECT")
27439 };
27440 assert_eq!(s.items.len(), 5);
27441 }
27442
27443 #[test]
27444 fn select_wildcard_from_table() {
27445 let s = parse("SELECT * FROM users");
27446 let Statement::Select(s) = s else {
27447 panic!("expected SELECT")
27448 };
27449 assert!(matches!(s.items[..], [SelectItem::Wildcard]));
27450 assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
27451 }
27452
27453 #[test]
27454 fn select_with_table_alias() {
27455 let s = parse("SELECT * FROM users AS u");
27456 let Statement::Select(s) = s else {
27457 panic!("expected SELECT")
27458 };
27459 let t = &s.from.as_ref().unwrap().primary;
27460 assert_eq!(t.name, "users");
27461 assert_eq!(t.alias.as_deref(), Some("u"));
27462 }
27463
27464 #[test]
27465 fn select_with_where_eq() {
27466 let s = parse("SELECT a FROM t WHERE a = 1");
27467 let Statement::Select(s) = s else {
27468 panic!("expected SELECT")
27469 };
27470 let w = s.where_.unwrap();
27471 assert_eq!(
27472 w,
27473 Expr::Binary {
27474 lhs: Box::new(col("a")),
27475 op: BinOp::Eq,
27476 rhs: Box::new(lit_int(1)),
27477 }
27478 );
27479 }
27480
27481 #[test]
27482 fn arithmetic_precedence() {
27483 let s = parse("SELECT 1 + 2 * 3");
27484 let Statement::Select(s) = s else {
27485 panic!("expected SELECT")
27486 };
27487 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27488 panic!("wildcard?")
27489 };
27490 assert_eq!(
27491 expr,
27492 &Expr::Binary {
27493 lhs: Box::new(lit_int(1)),
27494 op: BinOp::Add,
27495 rhs: Box::new(Expr::Binary {
27496 lhs: Box::new(lit_int(2)),
27497 op: BinOp::Mul,
27498 rhs: Box::new(lit_int(3)),
27499 }),
27500 }
27501 );
27502 }
27503
27504 #[test]
27505 fn parentheses_override_precedence() {
27506 let s = parse("SELECT (1 + 2) * 3");
27507 let Statement::Select(s) = s else {
27508 panic!("expected SELECT")
27509 };
27510 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27511 panic!()
27512 };
27513 assert_eq!(
27514 expr,
27515 &Expr::Binary {
27516 lhs: Box::new(Expr::Binary {
27517 lhs: Box::new(lit_int(1)),
27518 op: BinOp::Add,
27519 rhs: Box::new(lit_int(2)),
27520 }),
27521 op: BinOp::Mul,
27522 rhs: Box::new(lit_int(3)),
27523 }
27524 );
27525 }
27526
27527 #[test]
27528 fn not_binds_below_comparison() {
27529 // `NOT a = 1` should parse as `NOT (a = 1)`.
27530 let s = parse("SELECT NOT a = 1 FROM t");
27531 let Statement::Select(s) = s else {
27532 panic!("expected SELECT")
27533 };
27534 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27535 panic!()
27536 };
27537 assert_eq!(
27538 expr,
27539 &Expr::Unary {
27540 op: UnOp::Not,
27541 expr: Box::new(Expr::Binary {
27542 lhs: Box::new(col("a")),
27543 op: BinOp::Eq,
27544 rhs: Box::new(lit_int(1)),
27545 }),
27546 }
27547 );
27548 }
27549
27550 #[test]
27551 fn unary_minus_binds_above_multiplication() {
27552 // `-a * 2` should be `(-a) * 2`.
27553 let s = parse("SELECT -a * 2 FROM t");
27554 let Statement::Select(s) = s else {
27555 panic!("expected SELECT")
27556 };
27557 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27558 panic!()
27559 };
27560 assert_eq!(
27561 expr,
27562 &Expr::Binary {
27563 lhs: Box::new(Expr::Unary {
27564 op: UnOp::Neg,
27565 expr: Box::new(col("a")),
27566 }),
27567 op: BinOp::Mul,
27568 rhs: Box::new(lit_int(2)),
27569 }
27570 );
27571 }
27572
27573 #[test]
27574 fn qualified_column() {
27575 let s = parse("SELECT t.col FROM t");
27576 let Statement::Select(s) = s else {
27577 panic!("expected SELECT")
27578 };
27579 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27580 panic!()
27581 };
27582 assert_eq!(
27583 expr,
27584 &Expr::Column(ColumnName {
27585 qualifier: Some("t".into()),
27586 name: "col".into()
27587 })
27588 );
27589 }
27590
27591 #[test]
27592 fn select_item_alias_with_as() {
27593 let s = parse("SELECT a AS y FROM t");
27594 let Statement::Select(s) = s else {
27595 panic!("expected SELECT")
27596 };
27597 let SelectItem::Expr { alias, .. } = &s.items[0] else {
27598 panic!()
27599 };
27600 assert_eq!(alias.as_deref(), Some("y"));
27601 }
27602
27603 #[test]
27604 fn trailing_semicolon_accepted() {
27605 let s = parse("SELECT 1;");
27606 let Statement::Select(s) = s else {
27607 panic!("expected SELECT")
27608 };
27609 assert_eq!(s.items.len(), 1);
27610 }
27611
27612 #[test]
27613 fn boolean_chain_with_and_or_not() {
27614 // (NOT a) OR (b AND (NOT c))
27615 let s = parse("SELECT NOT a OR b AND NOT c FROM t");
27616 let Statement::Select(s) = s else {
27617 panic!("expected SELECT")
27618 };
27619 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27620 panic!()
27621 };
27622 let expected = Expr::Binary {
27623 lhs: Box::new(Expr::Unary {
27624 op: UnOp::Not,
27625 expr: Box::new(col("a")),
27626 }),
27627 op: BinOp::Or,
27628 rhs: Box::new(Expr::Binary {
27629 lhs: Box::new(col("b")),
27630 op: BinOp::And,
27631 rhs: Box::new(Expr::Unary {
27632 op: UnOp::Not,
27633 expr: Box::new(col("c")),
27634 }),
27635 }),
27636 };
27637 assert_eq!(expr, &expected);
27638 }
27639
27640 #[test]
27641 fn empty_input_errors() {
27642 // v7.14.0 — pg_dump preambles emit several comment-only
27643 // / blank-line statements that collapse to Statement::
27644 // Empty rather than a parse error. The old "SELECT in
27645 // message" assertion is stale; verify the new contract:
27646 // empty / whitespace / comment-only input parses to
27647 // Statement::Empty.
27648 assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
27649 assert!(matches!(
27650 parse_statement(" \n\t ").unwrap(),
27651 Statement::Empty
27652 ));
27653 // Sanity: malformed-but-non-empty still errors.
27654 assert!(parse_statement("SELECT FROM WHERE").is_err());
27655 }
27656
27657 #[test]
27658 fn unmatched_paren_errors() {
27659 assert!(parse_statement("SELECT (1 + 2").is_err());
27660 }
27661
27662 #[test]
27663 fn display_round_trip_simple_select() {
27664 let original = parse("SELECT a + 1 FROM t WHERE a > 0");
27665 let text = original.to_string();
27666 let again = parse_statement(&text).expect("re-parse");
27667 assert_eq!(original, again);
27668 }
27669
27670 // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
27671
27672 #[test]
27673 fn create_table_single_column() {
27674 let s = parse("CREATE TABLE foo (a INT)");
27675 let Statement::CreateTable(c) = s else {
27676 panic!("expected CreateTable")
27677 };
27678 assert_eq!(c.name, "foo");
27679 assert_eq!(c.columns.len(), 1);
27680 assert_eq!(c.columns[0].name, "a");
27681 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27682 assert!(c.columns[0].nullable);
27683 }
27684
27685 #[test]
27686 fn create_table_multi_column_with_not_null_mix() {
27687 let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
27688 let Statement::CreateTable(c) = s else {
27689 panic!()
27690 };
27691 assert_eq!(c.columns.len(), 4);
27692 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27693 assert!(!c.columns[0].nullable);
27694 assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
27695 assert!(c.columns[1].nullable);
27696 assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
27697 assert!(!c.columns[2].nullable);
27698 assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
27699 }
27700
27701 #[test]
27702 fn create_table_bigint_supported() {
27703 let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
27704 let Statement::CreateTable(c) = s else {
27705 panic!()
27706 };
27707 assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
27708 }
27709
27710 #[test]
27711 fn create_table_vector_default_is_f32() {
27712 let s = parse("CREATE TABLE t (v VECTOR(128))");
27713 let Statement::CreateTable(c) = s else {
27714 panic!()
27715 };
27716 assert_eq!(
27717 c.columns[0].ty,
27718 ColumnTypeName::Vector {
27719 dim: 128,
27720 encoding: VecEncoding::F32,
27721 },
27722 );
27723 }
27724
27725 #[test]
27726 fn create_table_vector_using_sq8() {
27727 // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
27728 // Case-insensitive on both `USING` and the encoding name.
27729 for sql in [
27730 "CREATE TABLE t (v VECTOR(128) USING SQ8)",
27731 "CREATE TABLE t (v VECTOR(128) using sq8)",
27732 ] {
27733 let s = parse(sql);
27734 let Statement::CreateTable(c) = s else {
27735 panic!()
27736 };
27737 assert_eq!(
27738 c.columns[0].ty,
27739 ColumnTypeName::Vector {
27740 dim: 128,
27741 encoding: VecEncoding::Sq8,
27742 },
27743 "{sql}",
27744 );
27745 }
27746 }
27747
27748 #[test]
27749 fn create_table_vector_using_unknown_errors() {
27750 // v7.16.1 — the inline `USING <encoding>` shape on
27751 // CREATE TABLE column defs was withdrawn before
27752 // v7.14.0 in favour of `CREATE INDEX … USING hnsw
27753 // (col vector_<metric>_ops)`; the parser now rejects
27754 // USING at column-list position with a clearer
27755 // "expected ',' or ')'" message. Test asserts the
27756 // current rejection, not the old "unknown vector
27757 // encoding" string.
27758 let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
27759 assert!(
27760 err.message.contains("USING")
27761 || err.message.contains("using")
27762 || err.message.contains("')'")
27763 || err.message.contains("','"),
27764 "expected USING/column-list rejection, got: {}",
27765 err.message
27766 );
27767 }
27768
27769 #[test]
27770 fn vector_using_sq8_display_roundtrips() {
27771 // The Display impl must produce text that re-parses to the
27772 // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
27773 let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
27774 let Statement::CreateTable(c) = s else {
27775 panic!()
27776 };
27777 assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
27778 }
27779
27780 #[test]
27781 fn parser_recognises_placeholders() {
27782 use crate::ast::{Expr, SelectItem, Statement};
27783 // $N in expression position parses as Expr::Placeholder(N).
27784 let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
27785 let Statement::Select(sel) = s else { panic!() };
27786 assert!(matches!(
27787 sel.items[0],
27788 SelectItem::Expr {
27789 expr: Expr::Placeholder(1),
27790 alias: None
27791 }
27792 ));
27793 // $2 + 1
27794 let SelectItem::Expr {
27795 expr: Expr::Binary { lhs, rhs, .. },
27796 ..
27797 } = &sel.items[1]
27798 else {
27799 panic!()
27800 };
27801 assert!(matches!(**lhs, Expr::Placeholder(2)));
27802 assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
27803 // WHERE x = $3
27804 let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
27805 panic!()
27806 };
27807 assert!(matches!(**rhs, Expr::Placeholder(3)));
27808 }
27809
27810 #[test]
27811 fn parser_rejects_dollar_zero() {
27812 // $0 is not valid in PG; the lexer rejects it.
27813 assert!(parse_statement("SELECT $0").is_err());
27814 }
27815
27816 #[test]
27817 fn placeholder_display_roundtrips() {
27818 // The Display impl must produce text that re-lexes to the
27819 // same Placeholder token.
27820 let s = parse("SELECT $42 FROM t");
27821 let printed = s.to_string();
27822 assert!(printed.contains("$42"));
27823 let again = parse(&printed);
27824 assert_eq!(s, again);
27825 }
27826
27827 #[test]
27828 fn alter_index_rebuild_bare() {
27829 use crate::ast::{AlterIndexTarget, Statement};
27830 let s = parse("ALTER INDEX my_idx REBUILD");
27831 let Statement::AlterIndex(a) = s else {
27832 panic!("expected AlterIndex, got {s:?}")
27833 };
27834 assert_eq!(a.name, "my_idx");
27835 assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
27836 }
27837
27838 #[test]
27839 fn alter_index_rebuild_with_encoding() {
27840 use crate::ast::{AlterIndexTarget, Statement};
27841 for (sql, want) in [
27842 (
27843 "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
27844 VecEncoding::F32,
27845 ),
27846 (
27847 "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
27848 VecEncoding::Sq8,
27849 ),
27850 (
27851 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27852 VecEncoding::F16,
27853 ),
27854 ] {
27855 let s = parse(sql);
27856 let Statement::AlterIndex(a) = s else {
27857 panic!("{sql}: expected AlterIndex")
27858 };
27859 assert_eq!(a.name, "my_idx");
27860 assert_eq!(
27861 a.target,
27862 AlterIndexTarget::Rebuild {
27863 encoding: Some(want)
27864 },
27865 "{sql}"
27866 );
27867 }
27868 }
27869
27870 #[test]
27871 fn alter_index_rebuild_unknown_encoding_errors() {
27872 let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
27873 assert!(
27874 err.message.contains("unknown vector encoding"),
27875 "got: {}",
27876 err.message
27877 );
27878 }
27879
27880 #[test]
27881 fn alter_index_rebuild_display_roundtrips() {
27882 for (input, want) in [
27883 ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
27884 (
27885 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27886 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27887 ),
27888 (
27889 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27890 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27891 ),
27892 ] {
27893 let s = parse(input);
27894 assert_eq!(s.to_string(), want);
27895 }
27896 }
27897
27898 #[test]
27899 fn create_table_unknown_type_defers_to_engine() {
27900 // v4.9 picked XML as a parse-time "unsupported column
27901 // type" probe. v7.17.0 Phase 1.4 changed the contract:
27902 // an unknown type ident parses as Text + `user_type_ref`
27903 // so CREATE TABLE can resolve user-defined enum / domain
27904 // types — rejection of truly-unknown types moved to the
27905 // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
27906 // to a first-class built-in, so this probe switched to a
27907 // synthetic name nothing in the lexer will ever recognise.
27908 let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
27909 let Statement::CreateTable(t) = stmt else {
27910 panic!("expected CreateTable");
27911 };
27912 assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
27913 }
27914
27915 #[test]
27916 fn create_table_missing_table_keyword_errors() {
27917 assert!(parse_statement("CREATE x (a INT)").is_err());
27918 }
27919
27920 // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
27921 // `PARTITION OF parent <bounds>` child parse + Display round-trip.
27922
27923 #[test]
27924 fn parse_create_table_partition_by_range() {
27925 use crate::ast::{PartitionBySpec, PartitionKindAst};
27926 let stmt = parse_statement(
27927 "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
27928 payload JSONB) PARTITION BY RANGE (ts)",
27929 )
27930 .unwrap();
27931 let Statement::CreateTable(t) = stmt else {
27932 panic!("expected CreateTable");
27933 };
27934 assert!(t.partition_of.is_none(), "parent has no partition_of");
27935 assert_eq!(t.columns.len(), 3);
27936 let by = t.partition_by.as_ref().expect("expected PARTITION BY");
27937 assert_eq!(
27938 by,
27939 &PartitionBySpec {
27940 kind: PartitionKindAst::Range,
27941 key_columns: alloc::vec!["ts".to_string()],
27942 }
27943 );
27944 // Display round-trip preserves the suffix. `quote_ident`
27945 // only adds double quotes when the ident needs escaping, so
27946 // a plain `ts` survives bare here.
27947 assert!(
27948 t.to_string().contains("PARTITION BY RANGE (ts)"),
27949 "Display lost PARTITION BY suffix: {t}"
27950 );
27951 }
27952
27953 #[test]
27954 fn parse_create_table_partition_of_range() {
27955 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
27956 let stmt = parse_statement(
27957 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
27958 FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
27959 )
27960 .unwrap();
27961 let Statement::CreateTable(t) = stmt else {
27962 panic!("expected CreateTable");
27963 };
27964 assert!(t.columns.is_empty(), "child inherits columns from parent");
27965 assert!(t.partition_by.is_none());
27966 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27967 assert_eq!(of.parent_name, "events_partitioned");
27968 let PartitionOfSpec { bounds, .. } = of.clone();
27969 match bounds {
27970 PartitionOfBoundsAst::Range { lower, upper } => {
27971 assert!(lower.to_string().contains("2026-06-01"));
27972 assert!(upper.to_string().contains("2026-07-01"));
27973 }
27974 other => panic!("expected Range, got {other:?}"),
27975 }
27976 // Display round-trip emits the FOR VALUES tail. `quote_ident`
27977 // skips quotes when not required, so the parent name appears
27978 // bare here.
27979 let s = t.to_string();
27980 assert!(
27981 s.contains("PARTITION OF events_partitioned"),
27982 "Display lost PARTITION OF: {s}"
27983 );
27984 assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
27985 assert!(s.contains(") TO ("), "Display lost TO: {s}");
27986 }
27987
27988 #[test]
27989 fn parse_create_table_partition_of_default() {
27990 use crate::ast::PartitionOfBoundsAst;
27991 let stmt =
27992 parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
27993 .unwrap();
27994 let Statement::CreateTable(t) = stmt else {
27995 panic!("expected CreateTable");
27996 };
27997 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27998 assert_eq!(of.parent_name, "events_partitioned");
27999 assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
28000 assert!(
28001 t.to_string()
28002 .contains("PARTITION OF events_partitioned DEFAULT"),
28003 "Display lost DEFAULT: {t}"
28004 );
28005 }
28006
28007 #[test]
28008 fn parse_create_table_partition_by_list() {
28009 // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
28010 // child with `FOR VALUES IN (lit, lit, …)`.
28011 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28012 let parent =
28013 parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
28014 .unwrap();
28015 let Statement::CreateTable(t) = parent else {
28016 panic!("expected CreateTable");
28017 };
28018 let Some(PartitionBySpec {
28019 kind,
28020 ref key_columns,
28021 }) = t.partition_by
28022 else {
28023 panic!("expected PARTITION BY");
28024 };
28025 assert_eq!(kind, PartitionKindAst::List);
28026 assert_eq!(*key_columns, vec!["region".to_string()]);
28027 assert!(t.to_string().contains("PARTITION BY LIST (region)"));
28028
28029 let child = parse_statement(
28030 "CREATE TABLE events_apac PARTITION OF events_listed \
28031 FOR VALUES IN ('jp', 'kr', 'tw')",
28032 )
28033 .unwrap();
28034 let Statement::CreateTable(c) = child else {
28035 panic!("expected CreateTable");
28036 };
28037 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28038 let PartitionOfBoundsAst::List { values } = &of.bounds else {
28039 panic!("expected List bounds, got {:?}", of.bounds);
28040 };
28041 assert_eq!(values.len(), 3);
28042 let disp = c.to_string();
28043 assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
28044 }
28045
28046 #[test]
28047 fn parse_create_table_partition_by_hash() {
28048 // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
28049 // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
28050 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28051 let parent =
28052 parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
28053 let Statement::CreateTable(t) = parent else {
28054 panic!("expected CreateTable");
28055 };
28056 let Some(PartitionBySpec {
28057 kind,
28058 ref key_columns,
28059 }) = t.partition_by
28060 else {
28061 panic!("expected PARTITION BY");
28062 };
28063 assert_eq!(kind, PartitionKindAst::Hash);
28064 assert_eq!(*key_columns, vec!["id".to_string()]);
28065 assert!(t.to_string().contains("PARTITION BY HASH (id)"));
28066
28067 let child = parse_statement(
28068 "CREATE TABLE orders_h_0 PARTITION OF orders_h \
28069 FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
28070 )
28071 .unwrap();
28072 let Statement::CreateTable(c) = child else {
28073 panic!("expected CreateTable");
28074 };
28075 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28076 let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
28077 panic!("expected Hash bounds");
28078 };
28079 assert_eq!(modulus, 4);
28080 assert_eq!(remainder, 0);
28081 let disp = c.to_string();
28082 assert!(
28083 disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
28084 "Display lost HASH bounds: {disp}"
28085 );
28086
28087 // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
28088 let bad = parse_statement(
28089 "CREATE TABLE orders_h_bad PARTITION OF orders_h \
28090 FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
28091 );
28092 let msg = format!("{}", bad.unwrap_err());
28093 assert!(
28094 msg.contains("REMAINDER") && msg.contains("MODULUS"),
28095 "expected REMAINDER/MODULUS validation error: {msg}"
28096 );
28097 }
28098
28099 #[test]
28100 fn parse_create_table_partition_of_rejects_columns() {
28101 // v7.37.6-B contract: PARTITION OF children inherit columns
28102 // from the parent; an explicit list MUST surface as a parse
28103 // error rather than getting silently ignored.
28104 let err = parse_statement(
28105 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
28106 FOR VALUES FROM ('a') TO ('b')",
28107 );
28108 assert!(err.is_err(), "expected parse error for explicit columns");
28109 let msg = format!("{}", err.unwrap_err());
28110 assert!(
28111 msg.contains("PARTITION OF") && msg.contains("column"),
28112 "error should mention PARTITION OF + columns: {msg}"
28113 );
28114 }
28115
28116 #[test]
28117 fn insert_single_value() {
28118 let s = parse("INSERT INTO foo VALUES (42)");
28119 let Statement::Insert(i) = s else {
28120 panic!("expected Insert")
28121 };
28122 assert_eq!(i.table, "foo");
28123 assert_eq!(i.rows.len(), 1);
28124 assert_eq!(i.rows[0].len(), 1);
28125 assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
28126 }
28127
28128 #[test]
28129 fn insert_multi_value_with_mixed_literals() {
28130 let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
28131 let Statement::Insert(i) = s else { panic!() };
28132 assert_eq!(i.rows.len(), 1);
28133 assert_eq!(i.rows[0].len(), 5);
28134 }
28135
28136 #[test]
28137 fn insert_missing_into_errors() {
28138 assert!(parse_statement("INSERT foo VALUES (1)").is_err());
28139 }
28140
28141 #[test]
28142 fn create_table_round_trip() {
28143 let original =
28144 parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
28145 let text = original.to_string();
28146 let again = parse_statement(&text).expect("re-parse");
28147 assert_eq!(original, again);
28148 }
28149
28150 #[test]
28151 fn insert_round_trip_with_negation_and_string() {
28152 let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
28153 let text = original.to_string();
28154 let again = parse_statement(&text).expect("re-parse");
28155 assert_eq!(original, again);
28156 }
28157
28158 #[test]
28159 fn unknown_keyword_at_statement_start_errors() {
28160 // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
28161 // the top-level dispatch still has no branch to take.
28162 let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
28163 assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
28164 }
28165
28166 // --- v0.8 CREATE INDEX --------------------------------------------------
28167
28168 #[test]
28169 fn create_index_basic() {
28170 let s = parse("CREATE INDEX idx_id ON users (id)");
28171 let Statement::CreateIndex(c) = s else {
28172 panic!("expected CreateIndex")
28173 };
28174 assert_eq!(c.name, "idx_id");
28175 assert_eq!(c.table, "users");
28176 assert_eq!(c.column, "id");
28177 }
28178
28179 #[test]
28180 fn create_index_missing_on_errors() {
28181 assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
28182 }
28183
28184 #[test]
28185 fn create_index_missing_paren_errors() {
28186 assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
28187 }
28188
28189 #[test]
28190 fn create_index_round_trip() {
28191 let original = parse("CREATE INDEX by_name ON users (name)");
28192 let again = parse_statement(&original.to_string()).unwrap();
28193 assert_eq!(original, again);
28194 }
28195
28196 // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
28197
28198 #[test]
28199 fn create_unique_index_basic() {
28200 let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
28201 let Statement::CreateIndex(c) = s else {
28202 panic!("expected CreateIndex");
28203 };
28204 assert!(c.is_unique);
28205 assert_eq!(c.column, "a");
28206 assert!(c.partial_predicate.is_none());
28207 }
28208
28209 #[test]
28210 fn create_unique_index_partial() {
28211 // mailrs's email_templates "one default per user" shape.
28212 let s = parse(
28213 "CREATE UNIQUE INDEX idx_email_templates_user_default \
28214 ON email_templates (user_address) WHERE is_default = true",
28215 );
28216 let Statement::CreateIndex(c) = s else {
28217 panic!("expected CreateIndex");
28218 };
28219 assert!(c.is_unique);
28220 assert_eq!(c.table, "email_templates");
28221 assert_eq!(c.column, "user_address");
28222 assert!(c.partial_predicate.is_some());
28223 }
28224
28225 #[test]
28226 fn create_unique_index_composite_with_predicate() {
28227 // mailrs's calendar_events instance: composite columns.
28228 let s = parse(
28229 "CREATE UNIQUE INDEX uq_calendar_events_instance \
28230 ON calendar_events (calendar_id, uid, recurrence_id) \
28231 WHERE recurrence_id IS NOT NULL",
28232 );
28233 let Statement::CreateIndex(c) = s else {
28234 panic!("expected CreateIndex");
28235 };
28236 assert!(c.is_unique);
28237 assert_eq!(c.column, "calendar_id");
28238 assert_eq!(
28239 c.extra_columns,
28240 vec!["uid".to_string(), "recurrence_id".to_string()]
28241 );
28242 assert!(c.partial_predicate.is_some());
28243 }
28244
28245 #[test]
28246 fn create_unique_index_using_btree_ok() {
28247 let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
28248 assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
28249 }
28250
28251 #[test]
28252 fn create_unique_index_using_hnsw_rejected() {
28253 let err =
28254 parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
28255 assert!(err.message.contains("UNIQUE"), "{}", err.message);
28256 }
28257
28258 #[test]
28259 fn create_unique_index_round_trip() {
28260 let original = parse(
28261 "CREATE UNIQUE INDEX uq_calendar_events_master \
28262 ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
28263 );
28264 let again = parse_statement(&original.to_string()).unwrap();
28265 assert_eq!(original, again);
28266 }
28267
28268 #[test]
28269 fn create_unique_without_index_errors() {
28270 let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
28271 // v7.39 (round 340, V56) — PG 18.4, verbatim.
28272 assert_eq!(err.message, "syntax error at or near \"TABLE\"");
28273 }
28274
28275 // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
28276
28277 #[test]
28278 fn create_table_bytea_column() {
28279 let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
28280 let Statement::CreateTable(c) = s else {
28281 panic!("expected CreateTable");
28282 };
28283 assert_eq!(c.columns.len(), 2);
28284 assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
28285 assert!(!c.columns[1].nullable);
28286 }
28287
28288 #[test]
28289 fn create_table_bytes_alias_column() {
28290 let s = parse("CREATE TABLE t (blob BYTES)");
28291 let Statement::CreateTable(c) = s else {
28292 panic!("expected CreateTable");
28293 };
28294 assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
28295 }
28296
28297 #[test]
28298 fn bytea_round_trip_display() {
28299 let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
28300 let again = parse_statement(&original.to_string()).unwrap();
28301 assert_eq!(original, again);
28302 }
28303
28304 // --- v0.9 transactions -------------------------------------------------
28305
28306 #[test]
28307 fn begin_commit_rollback_parse_as_unit_variants() {
28308 assert_eq!(parse("BEGIN"), Statement::Begin(None));
28309 assert_eq!(parse("COMMIT"), Statement::Commit);
28310 // r1066 — PG synonyms pgbench's tpcb script relies on.
28311 assert_eq!(parse("END"), Statement::Commit);
28312 assert_eq!(parse("END TRANSACTION"), Statement::Commit);
28313 assert_eq!(parse("COMMIT WORK"), Statement::Commit);
28314 assert_eq!(parse("ROLLBACK"), Statement::Rollback);
28315 // Trailing semicolons accepted too.
28316 assert_eq!(parse("BEGIN;"), Statement::Begin(None));
28317 // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
28318 // statement (with or without the WORK/TRANSACTION noise word).
28319 assert_eq!(
28320 parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
28321 Statement::Begin(Some(IsolationLevel::RepeatableRead))
28322 );
28323 assert_eq!(
28324 parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
28325 Statement::Begin(Some(IsolationLevel::Serializable))
28326 );
28327 // A non-isolation mode keeps the session default (None).
28328 assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
28329 }
28330
28331 // --- v1.2: pgvector distance ops + ::vector cast --------------------
28332
28333 #[test]
28334 fn inner_product_binop_parses() {
28335 let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
28336 let Statement::Select(s) = s else { panic!() };
28337 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28338 panic!()
28339 };
28340 assert!(matches!(
28341 expr,
28342 Expr::Binary {
28343 op: BinOp::InnerProduct,
28344 ..
28345 }
28346 ));
28347 }
28348
28349 #[test]
28350 fn cosine_distance_binop_parses() {
28351 let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
28352 let Statement::Select(s) = s else { panic!() };
28353 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28354 panic!()
28355 };
28356 assert!(matches!(
28357 expr,
28358 Expr::Binary {
28359 op: BinOp::CosineDistance,
28360 ..
28361 }
28362 ));
28363 }
28364
28365 #[test]
28366 fn vector_cast_postfix_wraps_string_literal() {
28367 let s = parse("SELECT '[1,2,3]'::vector FROM t");
28368 let Statement::Select(s) = s else { panic!() };
28369 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28370 panic!()
28371 };
28372 assert!(matches!(
28373 expr,
28374 Expr::Cast {
28375 target: CastTarget::Vector,
28376 ..
28377 }
28378 ));
28379 }
28380
28381 #[test]
28382 fn unsupported_cast_target_errors() {
28383 // v7.37.5 ship triage promoted the parser to accept every
28384 // ident as a `CastTarget::Named(canonical)`; the engine
28385 // surfaces the "unsupported cast target" error at eval
28386 // time when `type_name_to_data_type` can't resolve it.
28387 // Parser-side error now requires a NON-ident after `::`
28388 // (e.g. a punctuation token).
28389 let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
28390 assert_eq!(err.message, "syntax error at or near \",\"");
28391 }
28392
28393 #[test]
28394 fn tx_statements_round_trip() {
28395 for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
28396 let original = parse(q);
28397 let again = parse_statement(&original.to_string()).unwrap();
28398 assert_eq!(original, again);
28399 }
28400 }
28401
28402 #[test]
28403 fn interval_text_parsing_units() {
28404 // v7.37.5 β — three-field shape `(months, days, micros)` so
28405 // `'1 day'` and `'24 hours'` no longer collide (PG parity).
28406 // Single unit.
28407 assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
28408 assert_eq!(
28409 parse_interval_text("24 hours"),
28410 Some((0, 0, 86_400_000_000))
28411 );
28412 assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
28413 assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
28414 assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
28415 assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
28416 // Compound spans accumulate per-dimension.
28417 assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
28418 assert_eq!(
28419 parse_interval_text("1 day 2 hours"),
28420 Some((0, 1, 7_200_000_000))
28421 );
28422 // Negative numbers carry through per-dimension.
28423 assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
28424 // Bad shapes return None.
28425 assert_eq!(parse_interval_text(""), None);
28426 assert_eq!(parse_interval_text("garbage"), None);
28427 assert_eq!(parse_interval_text("1 fortnight"), None);
28428 // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
28429 // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
28430 assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
28431 assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
28432 assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
28433 }
28434
28435 #[test]
28436 fn interval_literal_roundtrips_via_display() {
28437 let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
28438 let s = parsed.to_string();
28439 // Display preserves the original text verbatim.
28440 assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
28441 // And re-parsing yields a structurally equal statement.
28442 let again = parse_statement(&s).unwrap();
28443 assert_eq!(parsed, again);
28444 }
28445
28446 // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
28447
28448 #[test]
28449 fn parser_recognises_create_publication_bare() {
28450 let s = parse("CREATE PUBLICATION pub_a");
28451 let Statement::CreatePublication(p) = s else {
28452 panic!("expected CreatePublication, got {s:?}")
28453 };
28454 assert_eq!(p.name, "pub_a");
28455 assert_eq!(p.scope, PublicationScope::AllTables);
28456 }
28457
28458 #[test]
28459 fn parser_recognises_create_publication_for_all_tables() {
28460 let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
28461 let Statement::CreatePublication(p) = s else {
28462 panic!("expected CreatePublication, got {s:?}")
28463 };
28464 assert_eq!(p.name, "pub_a");
28465 assert_eq!(p.scope, PublicationScope::AllTables);
28466 }
28467
28468 #[test]
28469 fn parser_recognises_drop_publication() {
28470 let s = parse("DROP PUBLICATION pub_a");
28471 let Statement::DropPublication { name, .. } = s else {
28472 panic!("expected DropPublication, got {s:?}")
28473 };
28474 assert_eq!(name, "pub_a");
28475 }
28476
28477 #[test]
28478 fn parser_recognises_for_table_list() {
28479 let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
28480 let Statement::CreatePublication(p) = s else {
28481 panic!("expected CreatePublication, got {s:?}")
28482 };
28483 assert_eq!(p.name, "pub_a");
28484 let PublicationScope::ForTables(ts) = p.scope else {
28485 panic!("expected ForTables scope")
28486 };
28487 assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
28488 }
28489
28490 #[test]
28491 fn parser_rejects_bare_for_tables_and_takes_in_schema() {
28492 // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
28493 // is rejected (`invalid publication object list`; the old
28494 // test pinned an unverifiable "PG 19 accepts both" claim);
28495 // TABLES pairs with IN SCHEMA.
28496 let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
28497 .expect_err("bare FOR TABLES must reject");
28498 assert!(
28499 alloc::format!("{err}").contains("invalid publication object list"),
28500 "got: {err}"
28501 );
28502 let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
28503 let Statement::CreatePublication(p) = s else {
28504 panic!("expected CreatePublication, got {s:?}")
28505 };
28506 let PublicationScope::TablesInSchema(schema) = p.scope else {
28507 panic!("expected TablesInSchema")
28508 };
28509 assert_eq!(schema, "public");
28510 }
28511
28512 #[test]
28513 fn parser_recognises_for_all_tables_except_list() {
28514 let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
28515 let Statement::CreatePublication(p) = s else {
28516 panic!()
28517 };
28518 let PublicationScope::AllTablesExcept(ts) = p.scope else {
28519 panic!("expected AllTablesExcept")
28520 };
28521 assert_eq!(ts, alloc::vec!["t1", "t2"]);
28522 }
28523
28524 #[test]
28525 fn parser_rejects_for_table_with_empty_list() {
28526 // `FOR TABLE` with nothing after is a parse error.
28527 let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
28528 .expect_err("must error on empty list");
28529 // No specific message asserted — the call falls through to
28530 // expect_ident_like which yields "expected identifier, got …".
28531 assert!(!err.message.is_empty());
28532 }
28533
28534 #[test]
28535 fn parser_recognises_show_publications() {
28536 // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
28537 // bare ident in this position, NOT a reserved keyword.
28538 let s = parse("SHOW PUBLICATIONS");
28539 assert!(matches!(s, Statement::ShowPublications));
28540 }
28541
28542 // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
28543
28544 #[test]
28545 fn parser_recognises_create_subscription_single_publication() {
28546 let s = parse(
28547 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
28548 );
28549 let Statement::CreateSubscription(c) = s else {
28550 panic!("expected CreateSubscription, got {s:?}")
28551 };
28552 assert_eq!(c.name, "sub_a");
28553 assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
28554 assert_eq!(c.publications, alloc::vec!["pub_a"]);
28555 }
28556
28557 #[test]
28558 fn parser_recognises_create_subscription_multi_publication() {
28559 let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
28560 let Statement::CreateSubscription(c) = s else {
28561 panic!()
28562 };
28563 assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
28564 }
28565
28566 #[test]
28567 fn parser_rejects_create_subscription_missing_connection() {
28568 let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
28569 .expect_err("must error on missing CONNECTION");
28570 assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
28571 }
28572
28573 #[test]
28574 fn parser_rejects_create_subscription_missing_publication() {
28575 let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
28576 .expect_err("must error on missing PUBLICATION");
28577 assert_eq!(err.message, "syntax error at end of input");
28578 }
28579
28580 #[test]
28581 fn parser_recognises_drop_subscription() {
28582 let s = parse("DROP SUBSCRIPTION sub_a");
28583 let Statement::DropSubscription { name, .. } = s else {
28584 panic!("expected DropSubscription, got {s:?}")
28585 };
28586 assert_eq!(name, "sub_a");
28587 }
28588
28589 #[test]
28590 fn parser_recognises_show_subscriptions() {
28591 let s = parse("SHOW SUBSCRIPTIONS");
28592 assert!(matches!(s, Statement::ShowSubscriptions));
28593 }
28594
28595 #[test]
28596 fn parser_recognises_wait_for_wal_position_no_timeout() {
28597 let s = parse("WAIT FOR WAL POSITION 12345");
28598 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28599 panic!("expected WaitForWalPosition, got {s:?}")
28600 };
28601 assert_eq!(pos, 12345);
28602 assert!(timeout_ms.is_none());
28603 }
28604
28605 #[test]
28606 fn parser_recognises_wait_for_wal_position_with_timeout() {
28607 let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
28608 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28609 panic!()
28610 };
28611 assert_eq!(pos, 67890);
28612 assert_eq!(timeout_ms, Some(5000));
28613 }
28614
28615 #[test]
28616 fn parser_rejects_wait_with_negative_position() {
28617 // The lexer treats `-` as a token; `expect_u64_literal`
28618 // only sees the Integer that follows, so the negative
28619 // arrives as a unary-minus expression at higher levels.
28620 // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
28621 // parse error one way or another.
28622 let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
28623 assert!(!err.message.is_empty());
28624 }
28625
28626 #[test]
28627 fn parser_recognises_bare_analyze() {
28628 let s = parse("ANALYZE");
28629 assert!(matches!(s, Statement::Analyze(None)));
28630 }
28631
28632 #[test]
28633 fn parser_recognises_analyze_with_table() {
28634 let s = parse("ANALYZE users");
28635 let Statement::Analyze(Some(name)) = s else {
28636 panic!("expected Analyze, got {s:?}")
28637 };
28638 assert_eq!(name, "users");
28639 }
28640
28641 #[test]
28642 fn parser_recognises_analyze_with_quoted_table() {
28643 let s = parse("ANALYZE \"Mixed Case\"");
28644 let Statement::Analyze(Some(name)) = s else {
28645 panic!()
28646 };
28647 assert_eq!(name, "Mixed Case");
28648 }
28649
28650 #[test]
28651 fn parser_rejects_analyze_with_garbage_token() {
28652 let err = parse_statement("ANALYZE 42").expect_err("must error");
28653 assert!(!err.message.is_empty());
28654 }
28655
28656 #[test]
28657 fn analyze_display_roundtrips() {
28658 for sql in ["ANALYZE", "ANALYZE users"] {
28659 let s = parse(sql);
28660 let printed = s.to_string();
28661 let again = parse_statement(&printed)
28662 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28663 assert_eq!(s, again);
28664 }
28665 }
28666
28667 #[test]
28668 fn wait_for_display_roundtrips() {
28669 for sql in [
28670 "WAIT FOR WAL POSITION 12345",
28671 "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
28672 ] {
28673 let s = parse(sql);
28674 let printed = s.to_string();
28675 let again = parse_statement(&printed)
28676 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28677 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28678 }
28679 }
28680
28681 #[test]
28682 fn subscription_ddl_display_roundtrips() {
28683 for sql in [
28684 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
28685 "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
28686 "DROP SUBSCRIPTION sub_a",
28687 "SHOW SUBSCRIPTIONS",
28688 ] {
28689 let s = parse(sql);
28690 let printed = s.to_string();
28691 let again = parse_statement(&printed)
28692 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28693 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28694 }
28695 }
28696
28697 #[test]
28698 fn parser_drop_dispatches_user_vs_publication() {
28699 // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
28700 // tokenises DROP. Both targets must still parse.
28701 let s = parse("DROP USER 'alice'");
28702 let Statement::DropUser { name, .. } = s else {
28703 panic!("expected DropUser, got {s:?}")
28704 };
28705 assert_eq!(name, "alice");
28706 // And DROP PUBLICATION lands the new variant.
28707 let s = parse("DROP PUBLICATION p1");
28708 assert!(matches!(s, Statement::DropPublication { .. }));
28709 }
28710
28711 #[test]
28712 fn publication_ddl_display_roundtrips() {
28713 // Every CREATE PUBLICATION variant must Display → parse →
28714 // same AST. v6.1.3 covers all three scope shapes.
28715 for sql in [
28716 "CREATE PUBLICATION pub_a",
28717 "CREATE PUBLICATION pub_a FOR ALL TABLES",
28718 "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
28719 "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
28720 "DROP PUBLICATION pub_a",
28721 "SHOW PUBLICATIONS",
28722 ] {
28723 let s = parse(sql);
28724 let printed = s.to_string();
28725 let again = parse_statement(&printed)
28726 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28727 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28728 }
28729 }
28730
28731 // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
28732
28733 #[test]
28734 fn create_function_returns_trigger_plpgsql_minimal() {
28735 let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
28736 let s = parse(sql);
28737 let Statement::CreateFunction(f) = s else {
28738 panic!("expected CreateFunction");
28739 };
28740 assert_eq!(f.name, "noop");
28741 assert!(!f.or_replace);
28742 assert!(f.args.is_empty());
28743 assert!(matches!(f.returns, FunctionReturn::Trigger));
28744 assert_eq!(f.language, "plpgsql");
28745 let FunctionBody::PlPgSql(block) = f.body else {
28746 panic!("expected PlPgSql body");
28747 };
28748 assert_eq!(block.statements.len(), 1);
28749 assert!(matches!(
28750 block.statements[0],
28751 PlPgSqlStmt::Return(ReturnTarget::New)
28752 ));
28753 }
28754
28755 #[test]
28756 fn create_function_or_replace_with_assignment() {
28757 // mailrs-shape trigger function: NEW.col := to_tsvector(...);
28758 // RETURN NEW.
28759 let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
28760BEGIN
28761 NEW.search_vector := to_tsvector('english', NEW.subject);
28762 RETURN NEW;
28763END;
28764$$";
28765 let s = parse(sql);
28766 let Statement::CreateFunction(f) = s else {
28767 panic!("expected CreateFunction");
28768 };
28769 assert!(f.or_replace);
28770 let FunctionBody::PlPgSql(block) = &f.body else {
28771 panic!("expected PlPgSql body");
28772 };
28773 assert_eq!(block.statements.len(), 2);
28774 // First statement: NEW.search_vector := to_tsvector(...)
28775 let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
28776 panic!("expected Assign as first stmt");
28777 };
28778 match target {
28779 AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
28780 other => panic!("expected NEW.col, got {other:?}"),
28781 }
28782 // Second statement: RETURN NEW
28783 assert!(matches!(
28784 block.statements[1],
28785 PlPgSqlStmt::Return(ReturnTarget::New)
28786 ));
28787 }
28788
28789 #[test]
28790 fn create_trigger_after_insert_or_update() {
28791 let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
28792 let s = parse(sql);
28793 let Statement::CreateTrigger(t) = s else {
28794 panic!("expected CreateTrigger");
28795 };
28796 assert_eq!(t.name, "tg");
28797 assert_eq!(t.table, "messages");
28798 assert_eq!(t.timing, TriggerTiming::After);
28799 assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
28800 assert_eq!(t.for_each, TriggerForEach::Row);
28801 assert_eq!(t.function, "update_sv");
28802 }
28803
28804 #[test]
28805 fn create_trigger_before_delete_execute_procedure_alias() {
28806 // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
28807 let sql =
28808 "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
28809 let s = parse(sql);
28810 let Statement::CreateTrigger(t) = s else {
28811 panic!("expected CreateTrigger");
28812 };
28813 assert_eq!(t.timing, TriggerTiming::Before);
28814 assert_eq!(t.events, vec![TriggerEvent::Delete]);
28815 }
28816
28817 #[test]
28818 fn drop_trigger_if_exists_round_trips() {
28819 // No parser support for DROP TRIGGER yet — added in v7.12.5
28820 // alongside the broader DROP …{IF EXISTS} cleanup. The
28821 // AST + Display impls are in place so we round-trip via
28822 // construction:
28823 let s = Statement::DropTrigger {
28824 name: "tg".into(),
28825 table: "messages".into(),
28826 if_exists: true,
28827 };
28828 assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
28829 }
28830
28831 #[test]
28832 fn trigger_ddl_display_roundtrips_through_parser() {
28833 // CREATE TRIGGER + its referenced CREATE FUNCTION must
28834 // Display → parse → same AST (modulo PL/pgSQL body
28835 // formatting which is parser-canonicalised).
28836 for sql in [
28837 "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
28838 "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
28839 ] {
28840 let s = parse(sql);
28841 let printed = s.to_string();
28842 let again = parse_statement(&printed)
28843 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28844 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28845 }
28846 }
28847}