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.
362///
363/// v7.38.19 — that sentence used to end "(recorded delta)". Measured
364/// against PG 18.4: a literal with 300 fractional digits round-trips
365/// identically on both engines, so whatever the note described is gone.
366/// It is RD-7 in `docs/RECORDED_DELTAS.md`, under "corrected by
367/// measurement" rather than under "open".
368/// Kept out of the parse_expr recursion frame — see the call site.
369#[inline(never)]
370fn numeric_token_to_literal(s: String) -> Result<Literal, String> {
371 match parse_decimal_literal(&s) {
372 Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
373 // v7.38 (read01, T3.C3) — a plain decimal too wide for i128 keeps
374 // its exact value as a NumericBig.
375 None if !s.contains(['e', 'E']) => Ok(Literal::NumericBig(s)),
376 // v7.39 (read01 numeric.c) — expand the exponent form.
377 None => match expand_scientific_literal(&s) {
378 SciExpanded::Expanded(plain) => match parse_decimal_literal(&plain) {
379 Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
380 None if plain
381 .split_once('.')
382 .is_none_or(|(_, f)| u8::try_from(f.len()).is_ok()) =>
383 {
384 Ok(Literal::NumericBig(plain))
385 }
386 None => s
387 .parse::<f64>()
388 .map(Literal::Float)
389 .map_err(|_| format!("invalid numeric literal {s:?}")),
390 },
391 SciExpanded::Overflow => Err("value overflows numeric format".to_string()),
392 SciExpanded::NotScientific => s
393 .parse::<f64>()
394 .map(Literal::Float)
395 .map_err(|_| format!("invalid numeric literal {s:?}")),
396 },
397 }
398}
399
400fn parse_decimal_literal(s: &str) -> Option<(i128, u16)> {
401 let (int_part, frac_part) = match s.split_once('.') {
402 Some((i, f)) => (i, f),
403 None => (s, ""),
404 };
405 // v7.39 (round 271) — was u8::MAX. A literal with 256 decimal
406 // places fell out of the numeric path here, which is why
407 // `pg_typeof(1e-256)` answered double precision and a plain
408 // 256-place decimal aborted the query in the big-decimal converter.
409 if frac_part.len() > u16::MAX as usize {
410 return None;
411 }
412 let mut digits = String::with_capacity(int_part.len() + frac_part.len());
413 digits.push_str(int_part);
414 digits.push_str(frac_part);
415 let mantissa: i128 = digits.parse().ok()?;
416 #[allow(clippy::cast_possible_truncation)]
417 Some((mantissa, frac_part.len() as u16))
418}
419
420/// `jsonb_to_record` / `jsonb_to_recordset` (+ `json_` variants) — the
421/// record-returning JSON functions that take a `AS alias(col type, …)`
422/// column-definition list in FROM position.
423fn is_json_to_record_name(s: &str) -> bool {
424 s.eq_ignore_ascii_case("jsonb_to_recordset")
425 || s.eq_ignore_ascii_case("jsonb_to_record")
426 // v7.39 (read01 jsonfuncs.c) — the populate family with an AS
427 // column-definition list desugars identically (the record base
428 // argument only carries the type; a non-NULL base's field
429 // defaults are a recorded delta, RD-6).
430 || s.eq_ignore_ascii_case("json_populate_record")
431 || s.eq_ignore_ascii_case("jsonb_populate_record")
432 || s.eq_ignore_ascii_case("json_populate_recordset")
433 || s.eq_ignore_ascii_case("jsonb_populate_recordset")
434 || s.eq_ignore_ascii_case("json_to_recordset")
435 || s.eq_ignore_ascii_case("json_to_record")
436}
437
438impl Parser {
439 /// Whether what follows an identifier ends an index key, which is how
440 /// an operator class is told from anything else in that position.
441 fn opclass_position_follows(next: Option<&Token>) -> bool {
442 match next {
443 // `ASC` / `DESC` have their own tokens; matching them as
444 // identifiers named "asc" / "desc" — which the first version of
445 // this did — never fires, and `(c text_pattern_ops DESC)` (which
446 // PG18.4 accepts, verified) went on failing to parse.
447 Some(Token::Comma | Token::RParen | Token::Asc | Token::Desc) => true,
448 Some(Token::Ident(w)) => {
449 w.eq_ignore_ascii_case("nulls") || w.eq_ignore_ascii_case("collate")
450 }
451 _ => false,
452 }
453 }
454}
455
456fn is_vector_opclass_name(name: &str) -> bool {
457 let lc = name.to_ascii_lowercase();
458 matches!(
459 lc.as_str(),
460 "vector_cosine_ops"
461 | "vector_l2_ops"
462 | "vector_ip_ops"
463 | "halfvec_cosine_ops"
464 | "halfvec_l2_ops"
465 | "halfvec_ip_ops"
466 | "sq8_cosine_ops"
467 | "sq8_l2_ops"
468 | "sq8_ip_ops"
469 // pg_trgm — trigram operator class. SPG's GIN index
470 // already uses tsvector tokens; trigram-style LIKE
471 // pattern matching still routes through a sequential
472 // scan, but the opclass name is accepted so PG schemas
473 // load.
474 | "gin_trgm_ops"
475 | "gist_trgm_ops"
476 // PG built-in btree opclasses occasionally appear in
477 // pg_dump output for column types with multiple
478 // sort orders (text_pattern_ops, varchar_pattern_ops,
479 // bpchar_pattern_ops).
480 | "text_pattern_ops"
481 | "varchar_pattern_ops"
482 | "bpchar_pattern_ops"
483 | "int4_ops"
484 | "int8_ops"
485 | "text_ops"
486 )
487}
488
489#[derive(Debug, Clone, PartialEq, Eq)]
490pub struct ParseError {
491 pub message: String,
492 /// Index into the token stream where parsing tripped. Not a byte offset.
493 /// v7.39 (read01 round 95) — the byte/char position is NOT stored here: a
494 /// field would grow every `Result<_, ParseError>` slot on the deeply
495 /// recursive parse stack and tip the nesting-budget frame cliff. PG's
496 /// 1-based char position is recovered on the cold error path by
497 /// [`syntax_error_position`], which re-tokenizes to map this token index.
498 pub token_pos: usize,
499}
500
501impl fmt::Display for ParseError {
502 /// v7.39 (round 322/V24) — the message ALONE. It used to be prefixed
503 /// with `parse error at token #N: `, which PG has no equivalent of:
504 /// the message bodies are already PG's verbatim (`LIMIT must not be
505 /// negative`, `invalid input syntax for type bigint: "abc"`), and the
506 /// prefix was SPG's internal token index leaking into every one of
507 /// them. `token_pos` stays a field — the wire recovers PG's 1-based
508 /// character position from it for the ErrorResponse `P`.
509 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510 f.write_str(&self.message)
511 }
512}
513
514impl From<LexError> for ParseError {
515 fn from(e: LexError) -> Self {
516 Self {
517 message: format!("lex: {e}"),
518 token_pos: 0,
519 }
520 }
521}
522
523/// v7.9.30 — parse a single expression (no trailing junk). Used by
524/// the engine to re-hydrate stored partial-index / unique-index
525/// predicates from their canonical Display form. The same Pratt
526/// parser the statement path uses; this entry point just skips the
527/// statement dispatch.
528pub fn parse_expression(input: &str) -> Result<Expr, ParseError> {
529 let (tokens, offsets) =
530 lexer::tokenize_with_offsets(input, false).map_err(|e| shape_lex_error(&e, input))?;
531 let mut p = Parser::new(tokens);
532 let expr = p
533 .parse_expr(0)
534 .and_then(|e| p.expect_eof().map(|()| e))
535 .map_err(|e| shape_syntax_error(e, input, &offsets))?;
536 Ok(expr)
537}
538
539/// Parse exactly one statement, swallow an optional trailing `;`, and require
540/// the token stream to end there. PG string semantics.
541pub fn parse_statement(input: &str) -> Result<Statement, ParseError> {
542 parse_statement_with(input, false)
543}
544
545/// v7.22 (round-13 T3) — dialect-aware entry: `backslash_escapes`
546/// selects MySQL-style string lexing (see `lexer::tokenize_with`).
547/// The engine threads its session flag through here.
548pub fn parse_statement_with(input: &str, backslash_escapes: bool) -> Result<Statement, ParseError> {
549 let (tokens, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes)
550 .map_err(|e| shape_lex_error(&e, input))?;
551 // The same session flag names the dialect for both the lexer and
552 // the type mapping.
553 let mut p = Parser::new_with_dialect(tokens, backslash_escapes).with_source(input, &offsets);
554 let stmt = (|| {
555 let stmt = p.parse_one_statement()?;
556 if matches!(p.peek(), Token::Semicolon) {
557 p.advance();
558 }
559 p.expect_eof()?;
560 Ok(stmt)
561 })()
562 .map_err(|e: ParseError| shape_syntax_error(e, input, &offsets))?;
563 Ok(stmt)
564}
565
566/// v7.39 (round 340, V56) — PG has exactly two syntax-error wordings:
567/// `syntax error at or near "<token>"` and `syntax error at end of input`
568/// (measured on 18.4 across a dozen shapes). SPG wrote its own per-site
569/// prose — `expected identifier, got Eof`, `unexpected token From in
570/// expression`, `expected end of input, got Ident("with")` — which named
571/// internal token types and, in the Debug forms, leaked the parser's own
572/// enum into a message clients read.
573///
574/// Applied once on the way out, so every construction site is covered and
575/// the token named is the one the error itself points at. Messages whose
576/// bodies are already PG's verbatim (`LIMIT must not be negative`,
577/// `invalid input syntax for type bigint: "abc"`) are left alone — those
578/// are PG's own errors, not its syntax error.
579fn shape_syntax_error(e: ParseError, input: &str, offsets: &[usize]) -> ParseError {
580 if !(e.message.starts_with("expected ") || e.message.starts_with("unexpected token ")) {
581 return e;
582 }
583 let message = match offending_lexeme(input, offsets, e.token_pos) {
584 Some(tok) => alloc::format!("syntax error at or near \"{tok}\""),
585 None => "syntax error at end of input".into(),
586 };
587 ParseError {
588 message,
589 token_pos: e.token_pos,
590 }
591}
592
593/// v7.39 (round 340, V56) — a lexer-level failure the way PG words it.
594/// Measured on 18.4: `unterminated quoted string at or near "'abc"`,
595/// `unterminated quoted identifier at or near ""abc"`, `unterminated /*
596/// comment at or near "/* x"` — the quoted part runs from the opening
597/// delimiter to the end of the input. SPG reported its own internal
598/// shape instead (`unterminated string literal at byte 7`), which named
599/// a byte offset no client can use.
600fn shape_lex_error(e: &lexer::LexError, input: &str) -> ParseError {
601 use lexer::LexErrorKind as K;
602 let from_here = input.get(e.pos..).map(str::trim_end).unwrap_or("");
603 let message = match &e.kind {
604 K::UnterminatedString => {
605 alloc::format!("unterminated quoted string at or near \"{from_here}\"")
606 }
607 K::UnterminatedQuotedIdent => {
608 alloc::format!("unterminated quoted identifier at or near \"{from_here}\"")
609 }
610 K::UnterminatedBlockComment => {
611 alloc::format!("unterminated /* comment at or near \"{from_here}\"")
612 }
613 // PG has no "unknown character" error of its own — the character
614 // is skipped and the parser reports the next token. SPG stops at
615 // the character itself and names it, which is the same shape.
616 K::UnknownChar(c) => alloc::format!("syntax error at or near \"{c}\""),
617 // The number-literal kinds already carry PG's `at or near` form.
618 other => alloc::format!(
619 "{}",
620 lexer::LexError {
621 kind: other.clone(),
622 pos: e.pos,
623 }
624 ),
625 };
626 ParseError {
627 message,
628 token_pos: 0,
629 }
630}
631
632/// The offending token exactly as it appears in the input, or `None` at
633/// end of input. PG echoes the source spelling — a lower-case `frm`
634/// reports as `frm`, not as a canonicalised keyword.
635fn offending_lexeme<'a>(input: &'a str, offsets: &[usize], token_pos: usize) -> Option<&'a str> {
636 let start = *offsets.get(token_pos)?;
637 if start >= input.len() {
638 return None;
639 }
640 let end = offsets
641 .get(token_pos + 1)
642 .copied()
643 .unwrap_or(input.len())
644 .min(input.len());
645 let seg = input.get(start..end)?.trim();
646 if seg.is_empty() {
647 return None;
648 }
649 // A quoted literal / identifier keeps its inner spaces; anything else
650 // ends at the first whitespace (the segment runs to the NEXT token's
651 // start, which may swallow a comment).
652 if seg.starts_with('\'') || seg.starts_with('"') || seg.starts_with('`') {
653 Some(seg)
654 } else {
655 seg.split_whitespace().next()
656 }
657}
658
659/// v7.39 (read01 round 95) — recover PG's 1-based CHARACTER error position for
660/// a [`ParseError::token_pos`]. Kept off the `ParseError` struct (and so off
661/// every recursive `Result` slot) to protect the nesting-budget frame cliff:
662/// this re-tokenizes `input` on the cold error path to map the failing token
663/// index to its start byte, then to a character offset. `backslash_escapes`
664/// must match the parse that produced `token_pos` (it barely shifts offsets,
665/// but stay consistent). Returns `None` when the index has no offset or the
666/// byte isn't a char boundary. The wire attaches it as the ErrorResponse `P`.
667#[must_use]
668pub fn syntax_error_position(
669 input: &str,
670 backslash_escapes: bool,
671 token_pos: usize,
672) -> Option<usize> {
673 let (_, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes).ok()?;
674 let byte_off = *offsets.get(token_pos)?;
675 if byte_off > input.len() || !input.is_char_boundary(byte_off) {
676 return None;
677 }
678 Some(input[..byte_off].chars().count() + 1)
679}
680
681struct Parser {
682 tokens: Vec<Token>,
683 pos: usize,
684 /// v7.39 (round 274) — the session's dialect, carried by the same
685 /// signal that drives string-literal escaping: `SET sql_mode` (only
686 /// MySQL clients and mysqldump preambles emit it) turns it on,
687 /// `SET standard_conforming_strings` (every pg_dump preamble) turns
688 /// it off. Needed here because the two dialects disagree about what
689 /// `REAL` means — see the type mapping below.
690 mysql_dialect: bool,
691 /// v7.30.2 (mailrs round-25 ask 2) — live nesting depth of the
692 /// mutually recursive expr/select parsers. Bounded so a deeply
693 /// nested input returns a parse error instead of overflowing
694 /// the stack (embed hosts die on overflow — it is an abort,
695 /// not a catchable error).
696 nest_depth: usize,
697 /// TABLESAMPLE lowering channel: the table-ref parser pushes a
698 /// `random() < p/100` predicate here; the enclosing SELECT
699 /// drains the list after its WHERE parses and ANDs the
700 /// predicates in. parse_bare_select save/restores around its
701 /// FROM+WHERE so nested selects only drain their own.
702 pending_sample_preds: Vec<Expr>,
703 /// v7.38.19 — the target of a `SELECT … INTO <table>`, carried out
704 /// of `parse_bare_select` (which returns a `SelectStatement` and has
705 /// nowhere to put it) to the caller that lowers the pair to the CTAS
706 /// node. `bool` is `TEMP`.
707 pending_select_into: Option<(String, bool)>,
708 /// v7.39 (round 691) — collation lowering channel, the same shape as
709 /// `pending_sample_preds` above. `expr COLLATE "name"` is ORDERING
710 /// information, and `ast::OrderBy` is where this parser keeps ordering
711 /// information (`desc`, `nulls_first`); the alternative — a new `Expr`
712 /// variant — puts a new arm on `eval_expr`, which this repo has
713 /// measured to overflow the debug stack. So while an ORDER BY KEY is
714 /// being parsed the postfix loop drops the name here instead of
715 /// refusing it, and the key's parser takes it.
716 ///
717 /// Only inside an ORDER BY key: everywhere else an unperformable
718 /// collation still errors, because accepting one at a COMPARISON and
719 /// ignoring it is the defect F36 exists to close.
720 in_order_by_key: bool,
721 order_key_collation: Option<String>,
722 /// POSITION(sub IN str) — while parsing the needle, the IN
723 /// keyword is the argument separator, not a membership test.
724 /// The postfix loop leaves IN unconsumed when this is set.
725 suppress_in_tail: bool,
726 /// Index of the token the last `advance()` returned — see
727 /// [`Parser::consumed_pos`].
728 last_consumed: usize,
729 /// v7.39 (round 506) — the statement's own text and the byte each token
730 /// starts at, so a MySQL projection item can report the SOURCE TEXT
731 /// MariaDB reports: `SELECT a + b` names its column `a + b`,
732 /// spacing and all. Only filled for a MySQL session — a PG one names
733 /// columns from the parsed shape and pays nothing for this.
734 src: Option<(String, Vec<usize>)>,
735}
736
737/// Max expr/select parser nesting (parens, subqueries, CASE, …).
738/// Real SQL nests a few dozen levels at the extreme. Each nesting level
739/// costs a parse_expr→parse_unary→parse_atom frame chain, so the budget
740/// exists to turn a deep statement into a catchable parse ERROR: a stack
741/// overflow is an abort, and in the server it does not fail one query, it
742/// takes the process down and every other connection with it.
743///
744/// v7.39 (round 507) — measured, because the figure here used to be a
745/// guess ("over 10 KiB in debug … comfortably inside a 2 MiB worker stack
746/// in BOTH debug and release"), and the debug half of that is wrong by
747/// more than an order of magnitude:
748///
749/// * RELEASE, on a 2 MiB worker stack: every recursive shape reaches
750/// this budget and errors. Verified against a live server for nested
751/// derived tables, parens, calls, CASE, IN-subqueries, scalar
752/// subqueries, NOT and unary minus — the server stayed up through all
753/// of them. This is the contract that matters, and it holds.
754/// * DEBUG: nested derived tables cost roughly 235 KiB of stack PER
755/// LEVEL, so parsing alone aborts around 35 levels on an 8 MiB stack
756/// and executing aborts around 8 inside a test thread. The budget is
757/// simply unreachable there, which is why a deep-nesting test has to
758/// ask for a large stack of its own — see `nesting_budget_errors_at`
759/// in the parser tests.
760/// v7.39 (round 541) — the pg_catalog relations SPG synthesises, in
761/// one place.
762///
763/// There were two copies of this fact: a curated list, used for BARE
764/// names, and — in `try_peek_meta_qualified` — no list at all, which
765/// rewrote `pg_catalog.<anything>` to `__spg_pg_<anything>` and left
766/// the engine to complain about a view it could not materialise. So
767/// writing the schema qualifier CHANGED THE ANSWER: `pg_stat_activity`
768/// had rows, `pg_catalog.pg_stat_activity` was an error.
769///
770/// PG puts `pg_catalog` at the implicit front of every search_path, so
771/// the two spellings name the same relation and must resolve the same
772/// way. Names NOT here (`pg_stat_activity`, `pg_locks`,
773/// `pg_stat_statements`, `pg_statio_user_tables`) route through the
774/// meta_view_result path under their own names and must not be
775/// rewritten; a name that is neither reaches the ordinary resolver,
776/// which reports that the relation does not exist — PG's answer.
777const SYNTHESISED_PG_CATALOGS: &[&str] = &[
778 "pg_am",
779 "pg_attrdef",
780 "pg_attribute",
781 "pg_cast",
782 "pg_db_role_setting",
783 "pg_conversion",
784 "pg_default_acl",
785 "pg_shadow",
786 "pg_sequences",
787 "pg_range",
788 "pg_partitioned_table",
789 "pg_language",
790 "pg_group",
791 "pg_authid",
792 "pg_class",
793 "pg_collation",
794 "pg_constraint",
795 "pg_database",
796 "pg_depend",
797 "pg_amop",
798 "pg_amproc",
799 "pg_opclass",
800 "pg_opfamily",
801 // v7.39 (read01 round 50) — COMMENT ON store, PG's pg_description.
802 "pg_description",
803 "pg_enum",
804 "pg_extension",
805 // v7.39 (round 541) — pg_dump reads it for every relation of kind
806 // 'f'. SPG has no foreign tables, so it is empty, which is also
807 // what PG reports on a database that has none.
808 "pg_foreign_table",
809 // v7.39 (round 541) — the empty-by-truth family; see
810 // EMPTY_PG_CATALOGS in spg-engine::system_catalog.
811 "pg_event_trigger",
812 "pg_file_settings",
813 "pg_foreign_data_wrapper",
814 "pg_foreign_server",
815 "pg_hba_file_rules",
816 "pg_ident_file_mappings",
817 "pg_init_privs",
818 "pg_parameter_acl",
819 "pg_prepared_xacts",
820 "pg_publication_namespace",
821 "pg_publication_rel",
822 "pg_publication_tables",
823 "pg_replication_origin",
824 "pg_replication_origin_status",
825 "pg_seclabel",
826 "pg_seclabels",
827 "pg_shdepend",
828 "pg_shdescription",
829 "pg_shmem_allocations",
830 "pg_shmem_allocations_numa",
831 "pg_shseclabel",
832 "pg_statistic_ext_data",
833 "pg_stats_ext",
834 "pg_stats_ext_exprs",
835 "pg_subscription_rel",
836 "pg_transform",
837 "pg_user_mapping",
838 "pg_user_mappings",
839 "pg_index",
840 "pg_indexes",
841 "pg_inherits",
842 // v7.39 (round 650) — the text-search catalogs SPG can fill
843 // honestly. `pg_ts_config_map` is deliberately NOT here: it maps
844 // token types to dictionaries and SPG has no token-type model,
845 // the same gap that leaves `ts_token_type` / `ts_debug` unbuilt.
846 "pg_ts_config",
847 "pg_ts_config_map",
848 "pg_ts_dict",
849 "pg_ts_parser",
850 "pg_ts_template",
851 "pg_matviews",
852 "pg_namespace",
853 // v7.39 (round 621)
854 "pg_operator",
855 "pg_policies",
856 "pg_policy",
857 "pg_proc",
858 "pg_publication",
859 "pg_replication_slots",
860 "pg_roles",
861 // v7.39 (round 143) — the rewrite-rule listing view.
862 // v7.39 (round 312) — and the rule catalogue itself, which
863 // `pg_get_ruledef(oid)` resolves against.
864 "pg_rewrite",
865 "pg_rules",
866 "pg_sequence",
867 "pg_settings",
868 "pg_stat_archiver",
869 "pg_stat_bgwriter",
870 "pg_stat_checkpointer",
871 "pg_stat_database",
872 "pg_stat_io",
873 "pg_stat_progress_analyze",
874 "pg_auth_members",
875 "pg_stat_progress_create_index",
876 "pg_stat_progress_vacuum",
877 "pg_stat_replication",
878 "pg_stat_slru",
879 "pg_stat_subscription_stats",
880 "pg_stat_user_functions",
881 "pg_stat_user_indexes",
882 "pg_stat_user_tables",
883 "pg_stat_wal",
884 "pg_prepared_statements",
885 "pg_largeobject",
886 "pg_largeobject_metadata",
887 "pg_statistic",
888 "pg_statistic_ext",
889 // v7.38.18 — the readable view over pg_statistic.
890 "pg_stats",
891 "pg_subscription",
892 "pg_tables",
893 "pg_tablespace",
894 // v7.39 (round 502) — the timezone catalogues. SPG resolved
895 // named zones correctly but could not list them, so a client
896 // populating a timezone picker got "relation does not exist".
897 "pg_timezone_abbrevs",
898 "pg_timezone_names",
899 "pg_trigger",
900 "pg_type",
901 "pg_user",
902 "pg_views",
903];
904
905const MAX_NEST_DEPTH: usize = 64;
906
907/// Stack accounting for the nesting budget, test-only.
908///
909/// `MAX_NEST_DEPTH` is a fixed count calibrated against a frame size
910/// that MOVES: a compiler upgrade grew the parser's debug frames and
911/// silently ate the margin until `nesting_budget_errors_cleanly` went
912/// from erroring cleanly to aborting on a stack overflow. A count
913/// cannot notice that on its own, so the budget is measured here and
914/// held to a ceiling.
915///
916/// The reading has to come from a helper whose OWN frame is the same at
917/// every call: debug slot placement does not follow source order, so a
918/// local's address inside the function under test is not that
919/// function's frame boundary. Two earlier probes were wrong that way —
920/// one read `&self.nest_depth`, which is the `Parser`'s address and
921/// never moves at all.
922#[cfg(test)]
923mod frame_meter {
924 extern crate std;
925 use std::cell::Cell;
926
927 // Per-THREAD, not global. `cargo test` runs tests in parallel and
928 // plenty of them parse nested expressions, so shared statics get
929 // stack addresses from several threads at once and the subtraction
930 // below turns into noise — it read 229,772 bytes per level that way,
931 // while passing when the test was run on its own.
932 std::thread_local! {
933 static AT_LO: Cell<usize> = const { Cell::new(0) };
934 static AT_HI: Cell<usize> = const { Cell::new(0) };
935 }
936
937 pub(super) const SAMPLE_LO: usize = 4;
938 pub(super) const SAMPLE_HI: usize = 24;
939
940 #[inline(never)]
941 pub(super) fn record(depth: usize) {
942 let anchor = 0u8;
943 let at = core::ptr::from_ref(&anchor) as usize;
944 if depth == SAMPLE_LO {
945 AT_LO.with(|c| c.set(at));
946 } else if depth == SAMPLE_HI {
947 AT_HI.with(|c| c.set(at));
948 }
949 }
950
951 /// Bytes of stack one nesting level costs, averaged over the span.
952 pub(super) fn bytes_per_level() -> usize {
953 let lo = AT_LO.with(Cell::get);
954 let hi = AT_HI.with(Cell::get);
955 assert!(lo > 0 && hi > 0, "meter never sampled: lo={lo} hi={hi}");
956 assert!(lo > hi, "stack grew upwards? lo={lo} hi={hi}");
957 (lo - hi) / (SAMPLE_HI - SAMPLE_LO)
958 }
959
960 pub(super) fn reset() {
961 AT_LO.with(|c| c.set(0));
962 AT_HI.with(|c| c.set(0));
963 }
964}
965
966/// v7.39 (read01 geo_ops.c) — prefix `@@` desugar target, out-of-line so
967/// the constructor's temporaries stay off `parse_unary`'s recursion frame.
968#[inline(never)]
969fn build_center_call(e: Expr) -> Expr {
970 Expr::FunctionCall {
971 name: alloc::string::String::from("center"),
972 args: alloc::vec![e],
973 }
974}
975
976/// Max consecutive binary operators at ONE precedence level
977/// (`a OR b OR c …`, `1+1+1…`). The chain builds iteratively at
978/// parse time but evaluates and drops recursively — depth beyond
979/// this overflows 2 MiB worker stacks (debug eval frames run
980/// multiple KiB). `IN (…)` lists are flat and unaffected.
981const MAX_BINARY_CHAIN: usize = 256;
982
983/// v7.22 (round-13 gap 5) — the kind keyword after `CONSTRAINT
984/// <name>` in a CREATE TABLE column list. FOREIGN KEY is not here:
985/// it keeps its dedicated path (`parse_table_level_fk`).
986enum NamedTableConstraintKind {
987 Check,
988 Unique,
989 PrimaryKey,
990 Exclude,
991}
992
993impl Parser {
994 fn new(tokens: Vec<Token>) -> Self {
995 Self::new_with_dialect(tokens, false)
996 }
997
998 fn new_with_dialect(tokens: Vec<Token>, mysql_dialect: bool) -> Self {
999 Self {
1000 tokens,
1001 mysql_dialect,
1002 in_order_by_key: false,
1003 order_key_collation: None,
1004 pos: 0,
1005 nest_depth: 0,
1006 pending_sample_preds: Vec::new(),
1007 pending_select_into: None,
1008 suppress_in_tail: false,
1009 last_consumed: 0,
1010 src: None,
1011 }
1012 }
1013
1014 /// Hand the parser the text it is parsing, for [`Parser::source_span`].
1015 fn with_source(mut self, input: &str, offsets: &[usize]) -> Self {
1016 if self.mysql_dialect {
1017 self.src = Some((input.to_string(), offsets.to_vec()));
1018 }
1019 self
1020 }
1021
1022 /// The source text spanning tokens `start ..= end`, trimmed.
1023 ///
1024 /// The offsets are token STARTS, so the span runs to the start of the
1025 /// token after `end` and gives back the whitespace between them —
1026 /// trimming is what makes `a + b FROM t` end at `b`.
1027 fn source_span(&self, start: usize, end: usize) -> Option<&str> {
1028 let (text, offsets) = self.src.as_ref()?;
1029 let from = *offsets.get(start)?;
1030 let to = *offsets.get(end + 1)?;
1031 text.get(from..to).map(str::trim_end)
1032 }
1033
1034 /// v7.30.2 (mailrs round-25 ask 2) — bump the expr/select
1035 /// nesting depth, erroring out cleanly past the budget.
1036 fn enter_nested(&mut self) -> Result<(), ParseError> {
1037 self.nest_depth += 1;
1038 #[cfg(test)]
1039 frame_meter::record(self.nest_depth);
1040 if self.nest_depth > MAX_NEST_DEPTH {
1041 self.nest_depth -= 1;
1042 return Err(self.err(alloc::format!(
1043 "statement nests deeper than {MAX_NEST_DEPTH} levels"
1044 )));
1045 }
1046 Ok(())
1047 }
1048
1049 fn peek(&self) -> &Token {
1050 // tokens always ends with Eof; pos is clamped in advance().
1051 &self.tokens[self.pos]
1052 }
1053
1054 fn advance(&mut self) -> Token {
1055 let t = mem::replace(&mut self.tokens[self.pos], Token::Eof);
1056 self.last_consumed = self.pos;
1057 if self.pos + 1 < self.tokens.len() {
1058 self.pos += 1;
1059 }
1060 t
1061 }
1062
1063 /// v7.39 (round 340, V56) — the index of the token `advance()` just
1064 /// returned. It was computed as `pos - 1`, which is wrong at both
1065 /// ends: `advance()` parks on the final Eof rather than running off
1066 /// the end (so `SELECT * FROM` named `FROM` where PG says `at end of
1067 /// input`), and after backtracking `pos` is no longer one past the
1068 /// token that failed. Recorded by `advance()` itself instead.
1069 const fn consumed_pos(&self) -> usize {
1070 self.last_consumed
1071 }
1072
1073 fn err(&self, message: String) -> ParseError {
1074 ParseError {
1075 message,
1076 token_pos: self.pos,
1077 }
1078 }
1079
1080 fn expect_eof(&self) -> Result<(), ParseError> {
1081 if matches!(self.peek(), Token::Eof) {
1082 Ok(())
1083 } else {
1084 Err(self.err(format!("expected end of input, got {:?}", self.peek())))
1085 }
1086 }
1087
1088 /// v7.14.0 — swallow every token up to (but not including) the
1089 /// next semicolon / EOF. Used by the dump-noise dispatcher
1090 /// to consume `COMMENT ON …`, `GRANT …`, `LOCK TABLES …`,
1091 /// etc. without modeling each grammar.
1092 /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
1093 /// Kinds SPG stores by name: TABLE / COLUMN / INDEX / VIEW / SEQUENCE /
1094 /// SCHEMA / TYPE / DATABASE / FUNCTION. Anything else (and the multi-word
1095 /// `CONSTRAINT c ON t` / `MATERIALIZED VIEW` forms) keeps the old
1096 /// swallow-as-no-op behaviour so a pg_dump tail still loads.
1097 fn parse_comment_on(&mut self) -> Result<Statement, ParseError> {
1098 let start = self.pos;
1099 self.advance(); // COMMENT
1100 if !matches!(self.peek(), Token::On) {
1101 self.pos = start;
1102 self.consume_until_statement_boundary();
1103 return Ok(Statement::Empty);
1104 }
1105 self.advance(); // ON
1106 let kind = match self.peek() {
1107 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
1108 Token::Table => "table".into(),
1109 _ => {
1110 self.consume_until_statement_boundary();
1111 return Ok(Statement::Empty);
1112 }
1113 };
1114 if !matches!(
1115 kind.as_str(),
1116 "table"
1117 | "column"
1118 | "index"
1119 | "view"
1120 | "sequence"
1121 | "schema"
1122 | "type"
1123 | "database"
1124 | "function"
1125 ) {
1126 self.consume_until_statement_boundary();
1127 return Ok(Statement::Empty);
1128 }
1129 self.advance(); // the kind keyword
1130 // The object name. ⚠️ `expect_ident_like` strips a leading
1131 // `<schema>.` qualifier and returns only the trailing ident (SPG is
1132 // single-schema), which would turn `COMMENT ON COLUMN t.c` into just
1133 // `c`. Read the dotted parts from raw tokens instead, then let a
1134 // 3-part `schema.t.c` drop its leading schema like everywhere else.
1135 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
1136 loop {
1137 match self.advance() {
1138 Token::Ident(s) | Token::QuotedIdent(s) => parts.push(s),
1139 other if unreserved_keyword_text(&other).is_some() => {
1140 parts.push(unreserved_keyword_text(&other).unwrap());
1141 }
1142 other => {
1143 return Err(ParseError {
1144 message: alloc::format!("expected identifier, got {other:?}"),
1145 token_pos: self.consumed_pos(),
1146 });
1147 }
1148 }
1149 if matches!(self.peek(), Token::Dot) {
1150 self.advance();
1151 } else {
1152 break;
1153 }
1154 }
1155 // COLUMN wants `table.column`; every other kind wants a bare name.
1156 let want = if kind == "column" { 2 } else { 1 };
1157 while parts.len() > want {
1158 parts.remove(0);
1159 }
1160 let name = parts.join(".");
1161 // v7.39 (round 710) — `COMMENT ON FUNCTION f(int, text) IS …`.
1162 // pg_dump writes the SIGNATURE, and the paren list was a syntax
1163 // error here — a dump carrying one function comment failed to
1164 // restore. The list is consumed (the comment store keys by name;
1165 // overload-precise comments are the function-predicate follow-up).
1166 if matches!(self.peek(), Token::LParen)
1167 && matches!(
1168 kind.as_str(),
1169 "function" | "procedure" | "aggregate" | "routine"
1170 )
1171 {
1172 let mut depth = 0usize;
1173 loop {
1174 match self.advance() {
1175 Token::LParen => depth += 1,
1176 Token::RParen => {
1177 depth -= 1;
1178 if depth == 0 {
1179 break;
1180 }
1181 }
1182 Token::Eof => {
1183 return Err(self.err(alloc::string::String::from(
1184 "unterminated argument list in COMMENT ON",
1185 )));
1186 }
1187 _ => {}
1188 }
1189 }
1190 }
1191 // `IS`
1192 if !matches!(self.peek(), Token::Is) {
1193 self.expect_keyword_ident("is")?;
1194 } else {
1195 self.advance();
1196 }
1197 let comment = match self.peek() {
1198 Token::Null => {
1199 self.advance();
1200 None
1201 }
1202 _ => Some(self.expect_string_literal()?),
1203 };
1204 Ok(Statement::CommentOn {
1205 kind,
1206 name,
1207 comment,
1208 })
1209 }
1210
1211 /// v7.39 (read01 round 57) — `GRANT <privs> ON <obj> TO <roles> [WITH GRANT
1212 /// OPTION]` / `REVOKE [GRANT OPTION FOR] <privs> ON <obj> FROM <roles>
1213 /// [CASCADE|RESTRICT]`.
1214 ///
1215 /// TABLE privileges are the real ones (stored, enforced, introspectable).
1216 /// Every other object class — SCHEMA / SEQUENCE / DATABASE / FUNCTION / …,
1217 /// and the no-ON `GRANT role TO role` membership form — parses into
1218 /// `GrantObject::Other` and no-ops in the engine, so a pg_dump that grants
1219 /// on them still restores.
1220 fn parse_grant_or_revoke(&mut self, grant: bool) -> Result<Statement, ParseError> {
1221 self.advance(); // GRANT / REVOKE
1222 // REVOKE's optional `GRANT OPTION FOR` prefix.
1223 let mut grant_option = false;
1224 if !grant
1225 && self.peek_keyword_ident("grant")
1226 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("option"))
1227 {
1228 self.advance(); // GRANT
1229 self.advance(); // OPTION
1230 self.expect_keyword_ident("for")?;
1231 grant_option = true;
1232 }
1233 // The privilege list: `ALL [PRIVILEGES] [(cols)]`, or comma-separated
1234 // words each with an optional COLUMN list.
1235 let mut privileges: Vec<GrantPriv> = Vec::new();
1236 if matches!(self.peek(), Token::All) {
1237 self.advance();
1238 if self.peek_keyword_ident("privileges") {
1239 self.advance();
1240 }
1241 // `GRANT ALL (col) ON t TO r` — every column privilege, on that
1242 // column only.
1243 if matches!(self.peek(), Token::LParen) {
1244 let columns = self.parse_grant_column_list()?;
1245 privileges.push(GrantPriv {
1246 word: "ALL".into(),
1247 columns,
1248 });
1249 }
1250 } else {
1251 loop {
1252 // SELECT and INSERT lex as reserved tokens, so they never
1253 // reach `expect_ident_like` as plain idents; the rest
1254 // (UPDATE / DELETE / TRUNCATE / REFERENCES / TRIGGER /
1255 // MAINTAIN) are ordinary identifiers.
1256 let w = match self.peek() {
1257 Token::Select => {
1258 self.advance();
1259 "SELECT".to_string()
1260 }
1261 Token::Insert => {
1262 self.advance();
1263 "INSERT".to_string()
1264 }
1265 // v7.39 (read01 round 60) — CREATE is a privilege word on a
1266 // schema / database, and it lexes as a reserved token.
1267 Token::Create => {
1268 self.advance();
1269 "CREATE".to_string()
1270 }
1271 // NOT upper-cased: in the no-ON shape (`GRANT devs TO
1272 // alice`) these "privilege words" are ROLE NAMES, and a
1273 // role name is case-sensitive. `priv_from_word` folds case
1274 // itself when they really are privileges.
1275 _ => self.expect_ident_like()?,
1276 };
1277 // v7.39 (read01 round 59) — the optional per-privilege COLUMN
1278 // list: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
1279 let columns = if matches!(self.peek(), Token::LParen) {
1280 self.parse_grant_column_list()?
1281 } else {
1282 Vec::new()
1283 };
1284 privileges.push(GrantPriv { word: w, columns });
1285 if matches!(self.peek(), Token::Comma) {
1286 self.advance();
1287 } else {
1288 break;
1289 }
1290 }
1291 }
1292 // v7.39 (read01 round 58) — no ON clause at all = `GRANT devs TO alice`:
1293 // role MEMBERSHIP. The words parsed as "privileges" are the role names.
1294 if !matches!(self.peek(), Token::On) {
1295 let roles: Vec<String> = core::mem::take(&mut privileges)
1296 .into_iter()
1297 .map(|p| p.word)
1298 .collect();
1299 let grantees = self.parse_grantee_list(grant)?;
1300 // `WITH ADMIN OPTION` / `GRANTED BY x` — accepted, ignored (SPG has
1301 // no admin-option layer: a member cannot re-grant).
1302 self.consume_until_statement_boundary();
1303 return Ok(finish_grant(
1304 grant,
1305 GrantStatement {
1306 privileges: Vec::new(),
1307 object: GrantObject::Roles(roles),
1308 grantees,
1309 grant_option,
1310 },
1311 ));
1312 }
1313 self.advance(); // ON
1314 // An optional object-class keyword. `TABLE` (or no keyword at all) is
1315 // the enforced case; anything else parses and no-ops.
1316 let mut class = "TABLE";
1317 match self.peek() {
1318 Token::Table => {
1319 self.advance();
1320 }
1321 Token::All => {
1322 // v7.39 (read01 round 61) — `ALL TABLES IN SCHEMA x` expands to
1323 // every table at GRANT time, like PG. `ALL SEQUENCES/FUNCTIONS
1324 // IN SCHEMA` stay no-ops and keep their own object class.
1325 self.advance(); // ALL
1326 let kind = match self.peek() {
1327 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
1328 // TABLES has its own token (SHOW TABLES owns it).
1329 Token::Tables | Token::Table => "tables".to_string(),
1330 _ => String::new(),
1331 };
1332 if !kind.is_empty() {
1333 self.advance();
1334 }
1335 // `IN SCHEMA <name>`
1336 if matches!(self.peek(), Token::In) {
1337 self.advance();
1338 if self.peek_keyword_ident("schema") {
1339 self.advance();
1340 let _schema = self.expect_ident_like()?;
1341 }
1342 }
1343 if kind != "tables" {
1344 self.consume_until_statement_boundary();
1345 return Ok(finish_grant(
1346 grant,
1347 GrantStatement {
1348 privileges,
1349 object: GrantObject::Other("ALL … IN SCHEMA".into()),
1350 grantees: Vec::new(),
1351 grant_option,
1352 },
1353 ));
1354 }
1355 let grantees = self.parse_grantee_list(grant)?;
1356 self.consume_until_statement_boundary();
1357 return Ok(finish_grant(
1358 grant,
1359 GrantStatement {
1360 privileges,
1361 object: GrantObject::AllTablesInSchema,
1362 grantees,
1363 grant_option,
1364 },
1365 ));
1366 }
1367 Token::Ident(w) | Token::QuotedIdent(w) => {
1368 let lc = w.to_ascii_lowercase();
1369 // v7.39 (read01 round 60) — SEQUENCE / SCHEMA / DATABASE are
1370 // real objects with real ACLs now.
1371 if matches!(lc.as_str(), "sequence" | "schema" | "database") {
1372 self.advance();
1373 let mut names: Vec<String> = Vec::new();
1374 loop {
1375 let mut parts: Vec<String> = Vec::new();
1376 loop {
1377 parts.push(self.expect_ident_like()?);
1378 if matches!(self.peek(), Token::Dot) {
1379 self.advance();
1380 } else {
1381 break;
1382 }
1383 }
1384 names.push(parts.pop().expect("at least one part"));
1385 if matches!(self.peek(), Token::Comma) {
1386 self.advance();
1387 } else {
1388 break;
1389 }
1390 }
1391 let grantees = self.parse_grantee_list(grant)?;
1392 let mut grant_option = grant_option;
1393 if grant && self.peek_keyword_ident("with") {
1394 self.advance();
1395 self.expect_keyword_ident("grant")?;
1396 self.expect_keyword_ident("option")?;
1397 grant_option = true;
1398 }
1399 self.consume_until_statement_boundary();
1400 let object = match lc.as_str() {
1401 "sequence" => GrantObject::Sequences(names),
1402 "schema" => GrantObject::Schemas(names),
1403 _ => GrantObject::Databases(names),
1404 };
1405 return Ok(finish_grant(
1406 grant,
1407 GrantStatement {
1408 privileges,
1409 object,
1410 grantees,
1411 grant_option,
1412 },
1413 ));
1414 }
1415 // v7.39 (read01 round 61) — `ON FUNCTION f(int)` is real. SPG
1416 // keys functions by NAME, so the argument list parses and is
1417 // dropped (an overload set shares one ACL — recorded residual).
1418 if matches!(lc.as_str(), "function" | "procedure" | "routine") {
1419 self.advance();
1420 let mut names: Vec<(String, Option<Vec<String>>)> = Vec::new();
1421 loop {
1422 let mut parts: Vec<String> = Vec::new();
1423 loop {
1424 parts.push(self.expect_ident_like()?);
1425 if matches!(self.peek(), Token::Dot) {
1426 self.advance();
1427 } else {
1428 break;
1429 }
1430 }
1431 let fname = parts.pop().expect("at least one part");
1432 // v7.39 (read01 round 62) — the signature picks the
1433 // overload, so it is captured.
1434 let sig = if matches!(self.peek(), Token::LParen) {
1435 Some(self.parse_function_signature_types()?)
1436 } else {
1437 None
1438 };
1439 names.push((fname, sig));
1440 if matches!(self.peek(), Token::Comma) {
1441 self.advance();
1442 } else {
1443 break;
1444 }
1445 }
1446 let grantees = self.parse_grantee_list(grant)?;
1447 self.consume_until_statement_boundary();
1448 return Ok(finish_grant(
1449 grant,
1450 GrantStatement {
1451 privileges,
1452 object: GrantObject::Functions(names),
1453 grantees,
1454 grant_option,
1455 },
1456 ));
1457 }
1458 if matches!(
1459 lc.as_str(),
1460 "type"
1461 | "domain"
1462 | "language"
1463 | "tablespace"
1464 | "large"
1465 | "foreign"
1466 | "parameter"
1467 ) {
1468 self.consume_until_statement_boundary();
1469 return Ok(finish_grant(
1470 grant,
1471 GrantStatement {
1472 privileges,
1473 object: GrantObject::Other(lc.to_ascii_uppercase()),
1474 grantees: Vec::new(),
1475 grant_option,
1476 },
1477 ));
1478 }
1479 class = "TABLE";
1480 }
1481 _ => {}
1482 }
1483 let _ = class;
1484 // The table list. Schema-qualified names drop their qualifier (SPG is
1485 // single-schema) — but read the dotted parts from raw tokens, since
1486 // `expect_ident_like` would silently swallow the leading part.
1487 let mut tables: Vec<String> = Vec::new();
1488 loop {
1489 let mut parts: Vec<String> = Vec::new();
1490 loop {
1491 parts.push(self.expect_ident_like()?);
1492 if matches!(self.peek(), Token::Dot) {
1493 self.advance();
1494 } else {
1495 break;
1496 }
1497 }
1498 tables.push(parts.pop().expect("at least one part"));
1499 if matches!(self.peek(), Token::Comma) {
1500 self.advance();
1501 } else {
1502 break;
1503 }
1504 }
1505 let grantees = self.parse_grantee_list(grant)?;
1506 if grant && self.peek_keyword_ident("with") {
1507 self.advance();
1508 self.expect_keyword_ident("grant")?;
1509 self.expect_keyword_ident("option")?;
1510 grant_option = true;
1511 }
1512 // REVOKE's trailing CASCADE / RESTRICT — SPG has no dependent grants
1513 // to cascade to (no re-granting), so both are accepted and ignored.
1514 if !grant && (self.peek_keyword_ident("cascade") || self.peek_keyword_ident("restrict")) {
1515 self.advance();
1516 }
1517 Ok(finish_grant(
1518 grant,
1519 GrantStatement {
1520 privileges,
1521 object: GrantObject::Tables(tables),
1522 grantees,
1523 grant_option,
1524 },
1525 ))
1526 }
1527
1528 /// v7.39 (read01 round 62) — the argument TYPES in a function signature:
1529 /// `(int, text)` or `(x int, y text)` (PG accepts either). Returns the type
1530 /// words; the caller normalises them into a signature key.
1531 fn parse_function_signature_types(&mut self) -> Result<Vec<String>, ParseError> {
1532 self.advance(); // (
1533 let mut types: Vec<String> = Vec::new();
1534 if matches!(self.peek(), Token::RParen) {
1535 self.advance();
1536 return Ok(types);
1537 }
1538 loop {
1539 // Collect the words of one argument up to a comma / close paren.
1540 let mut words: Vec<String> = Vec::new();
1541 loop {
1542 match self.peek() {
1543 Token::Comma | Token::RParen | Token::Eof => break,
1544 _ => {}
1545 }
1546 let tok = self.advance();
1547 match tok {
1548 Token::Ident(w) | Token::QuotedIdent(w) => words.push(w),
1549 other => {
1550 if let Some(w) = unreserved_keyword_text(&other) {
1551 words.push(w);
1552 }
1553 }
1554 }
1555 }
1556 // `name TYPE` or a bare `TYPE`. Several of PG's type names are
1557 // themselves several words (`double precision`, `character
1558 // varying`, `timestamp with time zone`), so "two words means the
1559 // first is a parameter name" reads the type off `f(double
1560 // precision)` as `precision`. v7.39 (round 282): recognise the
1561 // multi-word spellings first — a leading word that STARTS one of
1562 // them is part of the type, not a name.
1563 let joined = words.join(" ");
1564 let ty = if words.len() >= 2 && is_multiword_type_phrase(&joined) {
1565 joined
1566 } else if words.len() >= 2 {
1567 words[1..].join(" ")
1568 } else {
1569 words.first().cloned().unwrap_or_default()
1570 };
1571 types.push(ty);
1572 if matches!(self.peek(), Token::Comma) {
1573 self.advance();
1574 } else {
1575 break;
1576 }
1577 }
1578 if matches!(self.peek(), Token::RParen) {
1579 self.advance();
1580 }
1581 Ok(types)
1582 }
1583
1584 /// v7.39 (read01 round 59) — `( col, col, … )` after a privilege word.
1585 fn parse_grant_column_list(&mut self) -> Result<Vec<String>, ParseError> {
1586 self.advance(); // (
1587 let mut cols = Vec::new();
1588 loop {
1589 cols.push(self.expect_ident_like()?);
1590 if matches!(self.peek(), Token::Comma) {
1591 self.advance();
1592 } else {
1593 break;
1594 }
1595 }
1596 if !matches!(self.peek(), Token::RParen) {
1597 return Err(self.err(alloc::format!(
1598 "expected ')' to close the column list, got {:?}",
1599 self.peek()
1600 )));
1601 }
1602 self.advance(); // )
1603 Ok(cols)
1604 }
1605
1606 /// `TO <roles>` (grant) / `FROM <roles>` (revoke). An empty-string entry is
1607 /// PUBLIC.
1608 fn parse_grantee_list(&mut self, grant: bool) -> Result<Vec<String>, ParseError> {
1609 if grant {
1610 if matches!(self.peek(), Token::To) {
1611 self.advance();
1612 } else {
1613 self.expect_keyword_ident("to")?;
1614 }
1615 } else if matches!(self.peek(), Token::From) {
1616 self.advance();
1617 } else {
1618 self.expect_keyword_ident("from")?;
1619 }
1620 let mut grantees: Vec<String> = Vec::new();
1621 loop {
1622 // `GROUP name` is the legacy spelling of a plain role name.
1623 if self.peek_keyword_ident("group") {
1624 self.advance();
1625 }
1626 if self.peek_keyword_ident("public") {
1627 self.advance();
1628 grantees.push(String::new()); // PUBLIC
1629 } else {
1630 grantees.push(self.expect_ident_like()?);
1631 }
1632 if matches!(self.peek(), Token::Comma) {
1633 self.advance();
1634 } else {
1635 break;
1636 }
1637 }
1638 Ok(grantees)
1639 }
1640
1641 /// v7.39 (round 277) — `PREPARE <name> [(type, …)] AS <statement>`.
1642 /// The body keeps its `$N` placeholders; substitution happens at
1643 /// EXECUTE. The declared types are recorded for
1644 /// `pg_prepared_statements.parameter_types` but are not enforced —
1645 /// PG infers when the list is omitted, and SPG resolves the values
1646 /// at substitution time either way.
1647 fn parse_prepare(&mut self) -> Result<Statement, ParseError> {
1648 let start = self.pos;
1649 self.advance(); // PREPARE
1650 // v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'` is 2PC, a
1651 // different statement that happens to share the keyword. PG
1652 // ships with `max_prepared_transactions = 0` and reports it
1653 // this way; SPG has no prepared-transaction registry, so the
1654 // same wording is the accurate answer rather than a dodge.
1655 // Round 277 turned this from a silent no-op into a confusing
1656 // "expected AS in PREPARE" parse error.
1657 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
1658 self.advance();
1659 let gid = match self.advance() {
1660 Token::String(g) => g,
1661 other => {
1662 return Err(self.err(alloc::format!(
1663 "expected a transaction identifier after PREPARE TRANSACTION, got {other:?}"
1664 )));
1665 }
1666 };
1667 return Ok(Statement::PrepareTransaction(gid));
1668 }
1669 let name = self.expect_ident_like()?;
1670 let mut param_types = Vec::new();
1671 if matches!(self.peek(), Token::LParen) {
1672 self.advance();
1673 loop {
1674 let mut ty = self.expect_ident_like()?;
1675 // A parameterised type name (`numeric(10,2)`,
1676 // `varchar(20)`) keeps its argument list in the text.
1677 if matches!(self.peek(), Token::LParen) {
1678 let mut depth = 0usize;
1679 let mut buf = String::from("(");
1680 loop {
1681 match self.advance() {
1682 Token::LParen => {
1683 depth += 1;
1684 if depth > 1 {
1685 buf.push('(');
1686 }
1687 }
1688 Token::RParen => {
1689 depth -= 1;
1690 buf.push(')');
1691 if depth == 0 {
1692 break;
1693 }
1694 }
1695 Token::Comma => buf.push(','),
1696 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
1697 Token::Eof => break,
1698 _ => {}
1699 }
1700 }
1701 ty.push_str(&buf);
1702 }
1703 // r1049 — `PREPARE p(bigint[]) AS …`: the sixth `[]`
1704 // position, same family as the parameter list above.
1705 let array_suffix = self.consume_array_suffix();
1706 ty.push_str(&array_suffix);
1707 param_types.push(ty);
1708 match self.peek() {
1709 Token::Comma => {
1710 self.advance();
1711 }
1712 Token::RParen => {
1713 self.advance();
1714 break;
1715 }
1716 other => {
1717 return Err(self.err(alloc::format!(
1718 "expected ',' or ')' in PREPARE parameter list, got {other:?}"
1719 )));
1720 }
1721 }
1722 }
1723 }
1724 if !matches!(self.peek(), Token::As) {
1725 return Err(self.err(alloc::format!(
1726 "expected AS in PREPARE, got {:?}",
1727 self.peek()
1728 )));
1729 }
1730 self.advance();
1731 let body = self.parse_one_statement()?;
1732 // The Parser holds tokens, not the source text, so the
1733 // statement PG reports in `pg_prepared_statements.statement`
1734 // is rebuilt from the AST rather than sliced from the input.
1735 let _ = start;
1736 let mut source = alloc::format!("PREPARE {}", crate::ast::quote_ident(&name));
1737 if !param_types.is_empty() {
1738 source.push_str(" (");
1739 source.push_str(¶m_types.join(", "));
1740 source.push(')');
1741 }
1742 source.push_str(" AS ");
1743 source.push_str(&alloc::format!("{body}"));
1744 Ok(Statement::Prepare {
1745 name,
1746 param_types,
1747 body: alloc::boxed::Box::new(body),
1748 source,
1749 })
1750 }
1751
1752 /// v7.39 (round 277) — `EXECUTE <name> [(<expr>, …)]`.
1753 fn parse_execute(&mut self) -> Result<Statement, ParseError> {
1754 self.advance(); // EXECUTE
1755 let name = self.expect_ident_like()?;
1756 let mut args = Vec::new();
1757 if matches!(self.peek(), Token::LParen) {
1758 self.advance();
1759 if matches!(self.peek(), Token::RParen) {
1760 self.advance();
1761 } else {
1762 loop {
1763 args.push(self.parse_expr(0)?);
1764 match self.advance() {
1765 Token::Comma => {}
1766 Token::RParen => break,
1767 other => {
1768 return Err(self.err(alloc::format!(
1769 "expected ',' or ')' in EXECUTE arguments, got {other:?}"
1770 )));
1771 }
1772 }
1773 }
1774 }
1775 }
1776 Ok(Statement::Execute { name, args })
1777 }
1778
1779 /// v7.39 (round 277) — `DEALLOCATE {[PREPARE] <name> | ALL}`.
1780 /// v7.39 (round 278) — `CALL <proc>([args])`. There is no
1781 /// procedure catalog yet, so this reports PG's not-found error
1782 /// (with its HINT) rather than pretending the call ran.
1783 /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
1784 /// Bare `DISCARD` is a syntax error in PG; so it is here.
1785 fn parse_discard(&mut self) -> Result<Statement, ParseError> {
1786 self.advance(); // DISCARD
1787 let target = match self.advance() {
1788 Token::All => DiscardTarget::All,
1789 Token::Ident(w) | Token::QuotedIdent(w) => match w.to_ascii_lowercase().as_str() {
1790 "all" => DiscardTarget::All,
1791 "plans" => DiscardTarget::Plans,
1792 "sequences" => DiscardTarget::Sequences,
1793 "temp" | "temporary" => DiscardTarget::Temp,
1794 other => {
1795 return Err(self.err(format!(
1796 "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1797 )));
1798 }
1799 },
1800 other => {
1801 return Err(self.err(format!(
1802 "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1803 )));
1804 }
1805 };
1806 Ok(Statement::Discard(target))
1807 }
1808
1809 /// v7.39 (round 318, V51) — MySQL `KILL [HARD|SOFT] [CONNECTION|QUERY]
1810 /// <expr>`. MariaDB accepts an expression for the id (its own docs use
1811 /// `KILL connection_id()`), and the HARD / SOFT prefixes only pick how
1812 /// aggressively the server interrupts, which SPG does not distinguish.
1813 /// Bare `KILL <id>` means CONNECTION.
1814 fn parse_kill(&mut self) -> Result<Statement, ParseError> {
1815 self.advance(); // KILL
1816 let mut query_only = false;
1817 loop {
1818 // CONNECTION is a reserved keyword token (it also opens
1819 // `CREATE SUBSCRIPTION … CONNECTION '…'`), so it arrives as
1820 // `Token::Connection` rather than a bare ident.
1821 if matches!(self.peek(), Token::Connection) {
1822 self.advance();
1823 break;
1824 }
1825 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
1826 break;
1827 };
1828 match w.to_ascii_lowercase().as_str() {
1829 "hard" | "soft" => {
1830 self.advance();
1831 }
1832 "query" => {
1833 self.advance();
1834 query_only = true;
1835 break;
1836 }
1837 _ => break,
1838 }
1839 }
1840 let id = self.parse_expr(0)?;
1841 Ok(Statement::Kill {
1842 query_only,
1843 id: Box::new(id),
1844 })
1845 }
1846
1847 fn parse_call(&mut self) -> Result<Statement, ParseError> {
1848 self.advance(); // CALL
1849 let name = self.expect_ident_like()?;
1850 self.consume_until_statement_boundary();
1851 Ok(Statement::Call(name))
1852 }
1853
1854 fn parse_deallocate(&mut self) -> Result<Statement, ParseError> {
1855 self.advance(); // DEALLOCATE
1856 // PG accepts an optional noise `PREPARE` keyword here.
1857 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("prepare")) {
1858 self.advance();
1859 }
1860 if matches!(self.peek(), Token::All) {
1861 self.advance();
1862 return Ok(Statement::Deallocate(None));
1863 }
1864 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("all")) {
1865 self.advance();
1866 return Ok(Statement::Deallocate(None));
1867 }
1868 let name = self.expect_ident_like()?;
1869 Ok(Statement::Deallocate(Some(name)))
1870 }
1871
1872 fn consume_until_statement_boundary(&mut self) {
1873 loop {
1874 match self.peek() {
1875 Token::Semicolon | Token::Eof => return,
1876 _ => self.advance(),
1877 };
1878 }
1879 }
1880
1881 /// v7.38.19 — the database name a `CREATE DATABASE` names, skipping
1882 /// an `IF NOT EXISTS`. Consumes only the name; the collation scanner
1883 /// runs after it and eats the rest.
1884 fn scan_database_name(&mut self) -> Option<String> {
1885 // The caller has only PEEKED at `DATABASE`; step past it, or the
1886 // first identifier found is the keyword itself. It was, and
1887 // `pg_database` listed a database called `database`.
1888 if matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w) if w.eq_ignore_ascii_case("database"))
1889 {
1890 self.advance();
1891 }
1892 for kw in ["if", "not", "exists"] {
1893 if matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w) if w.eq_ignore_ascii_case(kw))
1894 {
1895 self.advance();
1896 }
1897 }
1898 match self.peek().clone() {
1899 Token::Ident(w) | Token::QuotedIdent(w) => {
1900 self.advance();
1901 Some(w)
1902 }
1903 _ => None,
1904 }
1905 }
1906
1907 /// v7.38.18 — consume to the statement boundary like
1908 /// `consume_until_statement_boundary`, but pick out the collation a
1909 /// `CREATE DATABASE` asked for on the way.
1910 ///
1911 /// `LC_COLLATE 'de_DE.utf8'` and `LOCALE 'de_DE.utf8'` both count;
1912 /// `LC_CTYPE` does not, because SPG has no separate ctype and
1913 /// pretending to honour it would be the more misleading answer. An
1914 /// `=` between the keyword and the value is optional, as in PG.
1915 ///
1916 /// The whole statement used to be thrown away. Being single-database
1917 /// makes the NAME a no-op; it does not make the collation one.
1918 fn scan_database_collation_until_boundary(&mut self) -> Option<String> {
1919 let mut want_value = false;
1920 let mut found: Option<String> = None;
1921 loop {
1922 let tok = self.peek().clone();
1923 match &tok {
1924 Token::Semicolon | Token::Eof => break,
1925 Token::Ident(w) | Token::QuotedIdent(w)
1926 if w.eq_ignore_ascii_case("lc_collate") || w.eq_ignore_ascii_case("locale") =>
1927 {
1928 want_value = true;
1929 }
1930 Token::Eq if want_value => {}
1931 Token::String(v) if want_value => {
1932 found = Some(v.clone());
1933 want_value = false;
1934 }
1935 Token::Ident(v) | Token::QuotedIdent(v) if want_value => {
1936 found = Some(v.clone());
1937 want_value = false;
1938 }
1939 _ => want_value = false,
1940 }
1941 self.advance();
1942 }
1943 found
1944 }
1945
1946 /// v7.22 (round-13 T2) — consume to the statement boundary like
1947 /// `consume_until_statement_boundary`, but pick out the sequence
1948 /// name on the way: either `SEQUENCE NAME <ident>` (identity
1949 /// columns) or the first string literal (`nextval('<seq>')`).
1950 /// Schema qualifiers and `::regclass` casts are stripped.
1951 fn scan_sequence_name_until_boundary(&mut self) -> Option<String> {
1952 let mut seq: Option<String> = None;
1953 let mut after_sequence_kw = false;
1954 let mut after_name_kw = false;
1955 loop {
1956 match self.peek().clone() {
1957 Token::Semicolon | Token::Eof => break,
1958 Token::Ident(s) | Token::QuotedIdent(s) => {
1959 if after_name_kw && seq.is_none() {
1960 self.advance();
1961 let mut name = s;
1962 // `SEQUENCE NAME public.groups_id_seq` — keep
1963 // the bare name, drop qualifiers.
1964 while matches!(self.peek(), Token::Dot) {
1965 self.advance();
1966 if let Token::Ident(n) | Token::QuotedIdent(n) = self.advance() {
1967 name = n;
1968 }
1969 }
1970 seq = Some(name);
1971 after_name_kw = false;
1972 continue;
1973 }
1974 if after_sequence_kw && s.eq_ignore_ascii_case("name") {
1975 after_name_kw = true;
1976 after_sequence_kw = false;
1977 } else {
1978 after_sequence_kw = s.eq_ignore_ascii_case("sequence");
1979 }
1980 self.advance();
1981 }
1982 Token::String(s) => {
1983 if seq.is_none() {
1984 // `nextval('public.groups_id_seq'::regclass)`
1985 let bare = s
1986 .rsplit_once('.')
1987 .map_or_else(|| s.clone(), |(_, b)| b.to_string());
1988 seq = Some(bare);
1989 }
1990 self.advance();
1991 }
1992 _ => {
1993 after_sequence_kw = false;
1994 after_name_kw = false;
1995 self.advance();
1996 }
1997 }
1998 }
1999 seq
2000 }
2001
2002 /// v7.39 (round 621) — is the next token the keyword `BY`?
2003 ///
2004 /// `pg_get_keywords()` classes `by` as `U` (unreserved), so it is a legal
2005 /// column, table and alias name — and SPG lexed it into a dedicated
2006 /// `Token::By`, which made it unusable as a name ANYWHERE. Of the seven
2007 /// two-letter keywords the lexer knew, this was the only one PG leaves
2008 /// unreserved (`as`, `in`, `on`, `or`, `to` are reserved and `is` is `T`).
2009 ///
2010 /// The token is gone; the three clauses that own the word — GROUP BY,
2011 /// ORDER BY, PARTITION BY — and the handful of other places that expect it
2012 /// ask this instead. Adding it to the unreserved-identifier table was not
2013 /// enough on its own: identifier positions that match the token shape
2014 /// directly (an index's column list, a table alias) never consult that
2015 /// table, so `CREATE INDEX … ON t(by)` and `FROM t AS by` still failed.
2016 /// Not lexing it as a keyword closes the whole class rather than the two
2017 /// positions that happened to be noticed.
2018 fn peek_is_by(&self) -> bool {
2019 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by"))
2020 }
2021
2022 /// v7.39 (round 621) — the optional `CASCADE` / `RESTRICT` trailer a DROP
2023 /// takes. Accepted and dropped: SPG tracks no dependents to cascade to,
2024 /// which is what `DROP TABLE` and `DROP INDEX` have done since v7.14.
2025 fn consume_drop_behaviour(&mut self) {
2026 if matches!(
2027 self.peek(),
2028 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") || s.eq_ignore_ascii_case("restrict")
2029 ) {
2030 self.advance();
2031 }
2032 }
2033
2034 fn expect_ident_like(&mut self) -> Result<String, ParseError> {
2035 let first = match self.advance() {
2036 Token::Ident(s) | Token::QuotedIdent(s) => s,
2037 // v7.37.43-T4 — PG-unreserved keywords are legal identifiers
2038 // per PG's `pg_get_keywords()` classification. SPG tokenizes
2039 // these as named variants for parsing leverage in the
2040 // contexts that own them (`RELEASE SAVEPOINT`, `SHOW name`,
2041 // `BEGIN`, etc.), but they MUST still be usable as table /
2042 // column / alias names in DDL+DML. Sentori migrations like
2043 // 0001_init.sql ship `release TEXT NOT NULL` in the events
2044 // table — the `events.release` column carries the release
2045 // identifier string. Pre-T4 this triggered "expected
2046 // identifier, got Release" and blocked every drop-in user
2047 // whose schema had a column / alias with one of these names.
2048 other if unreserved_keyword_text(&other).is_some() => {
2049 unreserved_keyword_text(&other).unwrap()
2050 }
2051 other => {
2052 return Err(ParseError {
2053 message: format!("expected identifier, got {other:?}"),
2054 token_pos: self.consumed_pos(),
2055 });
2056 }
2057 };
2058 // v7.14.0 — strip optional `<schema>.` prefix. PG dumps
2059 // qualify every name with `public.` (and pg_catalog.* for
2060 // functions); SPG is single-schema so we discard the
2061 // prefix and return only the trailing ident. Same shape
2062 // also handles MySQL `db.tbl` cross-database refs (SPG
2063 // ignores the db part).
2064 if matches!(self.peek(), Token::Dot) {
2065 self.advance();
2066 match self.advance() {
2067 Token::Ident(s) | Token::QuotedIdent(s) => return Ok(s),
2068 other if unreserved_keyword_text(&other).is_some() => {
2069 return Ok(unreserved_keyword_text(&other).unwrap());
2070 }
2071 other => {
2072 return Err(ParseError {
2073 message: format!("expected identifier after '{first}.', got {other:?}"),
2074 token_pos: self.consumed_pos(),
2075 });
2076 }
2077 }
2078 }
2079 Ok(first)
2080 }
2081
2082 #[allow(clippy::too_many_lines)]
2083 fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
2084 // v7.14.0 — empty / comment-only / semicolon-only input
2085 // (after the lexer strips line + block + MySQL
2086 // conditional comments) lands as Statement::Empty.
2087 // pg_dump and mysqldump emit several wrappers that
2088 // collapse to nothing after stripping (`/*!40101 SET …
2089 // */;`, blank lines between statements); the engine
2090 // returns CommandOk no-op so the dump loads cleanly.
2091 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
2092 return Ok(Statement::Empty);
2093 }
2094 // v7.14.0 — pg_dump / mysqldump "noise" statements:
2095 // catalog / metadata DDL that has no behavioural effect
2096 // on SPG's single-schema, single-database, single-user
2097 // model. Consume the whole statement up to the next
2098 // semicolon / EOF and return Empty. This is broader than
2099 // the per-keyword DROP / SET / COMMENT arms but lets the
2100 // long tail of `LOCK TABLES`, `UNLOCK TABLES`, `GRANT`,
2101 // `REVOKE`, `ALTER OWNER TO`, `\restrict`, `\unrestrict`,
2102 // `BEGIN; COMMIT;` wrappers, etc. all pass through.
2103 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
2104 let lc = s.to_ascii_lowercase();
2105 // v7.39 (read01 round 50) — COMMENT ON is a real statement now.
2106 if lc == "comment" {
2107 return self.parse_comment_on();
2108 }
2109 // v7.39 (read01 round 57) — so is GRANT / REVOKE.
2110 if lc == "grant" || lc == "revoke" {
2111 return self.parse_grant_or_revoke(lc == "grant");
2112 }
2113 // v7.39 (round 277) — the SQL-level prepared-statement
2114 // surface is REAL now. It used to be accepted and dropped
2115 // on the theory that "real execution still happens via the
2116 // extended-query flow" — true only for a driver that uses
2117 // that flow; a plain SQL PREPARE / EXECUTE returned no
2118 // rows at all.
2119 if lc == "prepare" {
2120 return self.parse_prepare();
2121 }
2122 if lc == "execute" {
2123 return self.parse_execute();
2124 }
2125 if lc == "deallocate" {
2126 return self.parse_deallocate();
2127 }
2128 // v7.39 (round 278) — `CALL <proc>(<args>)` used to be
2129 // accepted and dropped, so an application's stored-procedure
2130 // invocation reported success and did nothing. SPG has no
2131 // procedure catalog, so every CALL names a procedure that
2132 // does not exist — which is exactly what PG says.
2133 if lc == "call" {
2134 return self.parse_call();
2135 }
2136 // v7.39 (round 318, V51) — MySQL `KILL`. Not dump noise: it
2137 // names one connection and acts on it.
2138 if lc == "kill" {
2139 return self.parse_kill();
2140 }
2141 if lc == "discard" {
2142 return self.parse_discard();
2143 }
2144 // v7.39 (round 696) — REASSIGN OWNED BY <role> [, …] TO <role>.
2145 // Still performs nothing; the roles are carried out so a name
2146 // that does not exist is refused, as PG18 refuses it.
2147 if lc == "reassign" {
2148 self.advance();
2149 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("owned")) {
2150 self.advance();
2151 }
2152 if self.peek_is_by() {
2153 self.advance();
2154 }
2155 // Only the roles BEFORE the TO are the ones that must
2156 // exist — `TO` names the new owner, which PG checks as
2157 // well, so both lists are collected.
2158 let mut names = self.take_comma_separated_names();
2159 if matches!(self.peek(), Token::To) {
2160 self.advance();
2161 names.extend(self.take_comma_separated_names());
2162 }
2163 self.consume_until_statement_boundary();
2164 return Ok(Statement::ValidateOnly {
2165 kind: crate::ast::ValidateOnlyKind::RoleName,
2166 names,
2167 });
2168 }
2169 // v7.39 (round 696) — SECURITY LABEL. PG18 refuses it
2170 // unconditionally with `no security label providers have been
2171 // loaded`, whatever object it names, because none is loaded.
2172 // SPG has none either; accepting it told the caller a label had
2173 // been applied when nothing anywhere records one.
2174 if lc == "security" {
2175 self.consume_until_statement_boundary();
2176 return Ok(Statement::ValidateOnly {
2177 kind: crate::ast::ValidateOnlyKind::SecurityLabel,
2178 names: Vec::new(),
2179 });
2180 }
2181 if is_dump_noise_statement(&lc) {
2182 self.consume_until_statement_boundary();
2183 return Ok(Statement::Empty);
2184 }
2185 }
2186 match self.peek() {
2187 Token::Select => self.parse_select_stmt(),
2188 // v7.37.17 (17.6 siblings) — a statement opening with a
2189 // parenthesized query group: `(SELECT … UNION …)
2190 // INTERSECT …`. parse_bare_select's group arm consumes
2191 // the parens; the select parser handles the outer chain
2192 // and tail.
2193 Token::LParen
2194 if matches!(
2195 self.tokens.get(self.pos + 1),
2196 Some(Token::Select | Token::LParen | Token::Values)
2197 ) =>
2198 {
2199 self.parse_select_stmt()
2200 }
2201 // v7.37.17 (17.6 siblings) — top-level bare VALUES
2202 // statement (`VALUES (1), (2) [ORDER BY …] [LIMIT …]`).
2203 // Lowers to the same UNION ALL chain the FROM-position
2204 // form uses, then reuses the shared SELECT tail.
2205 Token::Values => {
2206 self.advance(); // VALUES
2207 let mut head = self.parse_values_rows_body()?;
2208 self.parse_select_tail_into(&mut head)?;
2209 Ok(Statement::Select(head))
2210 }
2211 // SQL-standard `TABLE name` shorthand for
2212 // `SELECT * FROM name` — pg_dump never emits it, but
2213 // psql users and PG docs use it constantly. Set-op
2214 // chains and the ORDER BY/LIMIT tail compose like any
2215 // SELECT head.
2216 Token::Table
2217 if matches!(
2218 self.tokens.get(self.pos + 1),
2219 Some(Token::Ident(_) | Token::QuotedIdent(_))
2220 ) =>
2221 {
2222 let mut head = self.parse_table_shorthand()?;
2223 self.parse_setop_chain_into(&mut head)?;
2224 self.parse_select_tail_into(&mut head)?;
2225 Ok(Statement::Select(head))
2226 }
2227 // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2228 // body is a dollar-quoted plpgsql block (lexer already
2229 // collapsed `$$…$$` into a single Token::String).
2230 // v7.16.2 — mailrs round-10 A.2: parse the body as a
2231 // real PlPgSqlBlock so the engine can EXECUTE it at
2232 // top level instead of silently swallowing. Pre-
2233 // v7.16.2 the parser threw the body away and the
2234 // engine returned CommandOk for the entire DO; that
2235 // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2236 // $$` into a SEV-1 silent no-op (the IF + the rename
2237 // were both invisible — mailrs's migrate-042 didn't
2238 // actually run). Now the body parses + executes;
2239 // EmbeddedSql inside the block runs immediately
2240 // against the engine (not deferred — we're at top
2241 // level, not inside a trigger row-write loop).
2242 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2243 self.advance();
2244 let body_text = match self.advance() {
2245 Token::String(s) => s,
2246 other => {
2247 return Err(self.err(alloc::format!(
2248 "expected dollar-quoted body after DO, got {other:?}"
2249 )));
2250 }
2251 };
2252 // Optional `LANGUAGE <name>` trailer (idents only).
2253 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2254 self.advance();
2255 let _ = self.expect_ident_like()?;
2256 }
2257 // Parse the body — same shape CREATE FUNCTION
2258 // uses for trigger function bodies. If the body
2259 // doesn't parse cleanly we surface the error
2260 // (better than silent no-op).
2261 let block = parse_plpgsql_body(&body_text)?;
2262 Ok(Statement::DoBlock(block))
2263 }
2264 // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2265 // WITH isn't a reserved token in our lexer — comes through
2266 // as `Token::Ident("with")` (case-insensitive).
2267 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2268 self.advance();
2269 self.parse_with_cte_then_select()
2270 }
2271 // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2272 // an identifier — not a reserved keyword.
2273 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2274 self.advance();
2275 let mut analyze = false;
2276 let mut suggest = false;
2277 let mut costs_off = false;
2278 let mut buffers = false;
2279 let mut timing_off = false;
2280 let mut settings = false;
2281 let mut wal = false;
2282 let mut summary_off = false;
2283 let mut format = crate::ast::ExplainFormat::Text;
2284 // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2285 // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2286 // options are comma-separated. Booleans default to ON
2287 // when the value token is omitted (matches PG).
2288 if matches!(self.peek(), Token::LParen) {
2289 self.advance();
2290 loop {
2291 let opt = match self.peek().clone() {
2292 Token::Ident(s) | Token::QuotedIdent(s) => s,
2293 other => {
2294 return Err(self.err(format!(
2295 "expected option keyword inside EXPLAIN (…), got {other:?}"
2296 )));
2297 }
2298 };
2299 self.advance();
2300 if opt.eq_ignore_ascii_case("suggest") {
2301 suggest = true;
2302 // SUGGEST takes no explicit value today.
2303 } else if opt.eq_ignore_ascii_case("costs") {
2304 // PG syntax: `COSTS [ON | OFF]`. Default
2305 // when value omitted is ON, so plain
2306 // `COSTS` is a no-op. `COSTS OFF` flips.
2307 // `ON` lexes to `Token::On` (reserved
2308 // keyword in JOIN ... ON contexts); accept
2309 // it alongside the bare Ident form so the
2310 // grammar matches PG verbatim.
2311 let value = match self.peek().clone() {
2312 Token::On => {
2313 self.advance();
2314 true
2315 }
2316 Token::Ident(v) | Token::QuotedIdent(v)
2317 if v.eq_ignore_ascii_case("off") =>
2318 {
2319 self.advance();
2320 false
2321 }
2322 Token::Ident(v) | Token::QuotedIdent(v)
2323 if v.eq_ignore_ascii_case("true") =>
2324 {
2325 self.advance();
2326 true
2327 }
2328 _ => true,
2329 };
2330 costs_off = !value;
2331 } else if opt.eq_ignore_ascii_case("analyze")
2332 || opt.eq_ignore_ascii_case("analyse")
2333 {
2334 // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2335 // Same default-ON rule as ANALYZE keyword form.
2336 let value = match self.peek().clone() {
2337 Token::On => {
2338 self.advance();
2339 true
2340 }
2341 Token::Ident(v) | Token::QuotedIdent(v)
2342 if v.eq_ignore_ascii_case("off") =>
2343 {
2344 self.advance();
2345 false
2346 }
2347 Token::Ident(v) | Token::QuotedIdent(v)
2348 if v.eq_ignore_ascii_case("true") =>
2349 {
2350 self.advance();
2351 true
2352 }
2353 _ => true,
2354 };
2355 analyze = value;
2356 } else if opt.eq_ignore_ascii_case("buffers") {
2357 // v7.37.22 — `BUFFERS [ON|OFF]`.
2358 let value = match self.peek().clone() {
2359 Token::On => {
2360 self.advance();
2361 true
2362 }
2363 Token::Ident(v) | Token::QuotedIdent(v)
2364 if v.eq_ignore_ascii_case("off") =>
2365 {
2366 self.advance();
2367 false
2368 }
2369 Token::Ident(v) | Token::QuotedIdent(v)
2370 if v.eq_ignore_ascii_case("true") =>
2371 {
2372 self.advance();
2373 true
2374 }
2375 _ => true,
2376 };
2377 buffers = value;
2378 } else if opt.eq_ignore_ascii_case("timing") {
2379 // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2380 // the measured wall-clock annotation.
2381 let value = match self.peek().clone() {
2382 Token::On => {
2383 self.advance();
2384 true
2385 }
2386 Token::Ident(v) | Token::QuotedIdent(v)
2387 if v.eq_ignore_ascii_case("off") =>
2388 {
2389 self.advance();
2390 false
2391 }
2392 Token::Ident(v) | Token::QuotedIdent(v)
2393 if v.eq_ignore_ascii_case("true") =>
2394 {
2395 self.advance();
2396 true
2397 }
2398 _ => true,
2399 };
2400 timing_off = !value;
2401 } else if opt.eq_ignore_ascii_case("settings") {
2402 settings = true;
2403 } else if opt.eq_ignore_ascii_case("wal") {
2404 wal = true;
2405 } else if opt.eq_ignore_ascii_case("summary") {
2406 // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2407 // gates the trailing Planning/Execution Time
2408 // lines now (was accept-and-no-op).
2409 let value = match self.peek().clone() {
2410 Token::On => {
2411 self.advance();
2412 true
2413 }
2414 Token::Ident(v) | Token::QuotedIdent(v)
2415 if v.eq_ignore_ascii_case("off") =>
2416 {
2417 self.advance();
2418 false
2419 }
2420 Token::Ident(v) | Token::QuotedIdent(v)
2421 if v.eq_ignore_ascii_case("true") =>
2422 {
2423 self.advance();
2424 true
2425 }
2426 _ => true,
2427 };
2428 summary_off = !value;
2429 } else if opt.eq_ignore_ascii_case("verbose")
2430 || opt.eq_ignore_ascii_case("format")
2431 {
2432 // v7.37.22 — accept-but-no-op the remaining
2433 // PG options so EXPLAIN-using clients
2434 // (pgAdmin / DataGrip) don't see syntax
2435 // errors. FORMAT takes a value (text /
2436 // json / yaml / xml); skip the next token
2437 // if it's an ident.
2438 if opt.eq_ignore_ascii_case("format") {
2439 if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2440 {
2441 self.advance();
2442 format = match v.to_ascii_lowercase().as_str() {
2443 "text" => crate::ast::ExplainFormat::Text,
2444 "json" => crate::ast::ExplainFormat::Json,
2445 "xml" => crate::ast::ExplainFormat::Xml,
2446 "yaml" => crate::ast::ExplainFormat::Yaml,
2447 other => {
2448 return Err(self.err(format!(
2449 "EXPLAIN (FORMAT …): unknown format {other:?}; \
2450 supports text, json, xml, yaml"
2451 )));
2452 }
2453 };
2454 }
2455 } else {
2456 // VERBOSE / SUMMARY take optional ON/OFF;
2457 // consume if present.
2458 if matches!(self.peek(), Token::On) {
2459 self.advance();
2460 } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2461 self.peek().clone()
2462 && (v.eq_ignore_ascii_case("off")
2463 || v.eq_ignore_ascii_case("true"))
2464 {
2465 self.advance();
2466 let _ = v;
2467 }
2468 }
2469 } else {
2470 return Err(self.err(format!(
2471 "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2472 )));
2473 }
2474 if matches!(self.peek(), Token::Comma) {
2475 self.advance();
2476 continue;
2477 }
2478 break;
2479 }
2480 if !matches!(self.peek(), Token::RParen) {
2481 return Err(self.err(format!(
2482 "expected ')' after EXPLAIN options, got {:?}",
2483 self.peek()
2484 )));
2485 }
2486 self.advance();
2487 } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2488 && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2489 {
2490 self.advance();
2491 analyze = true;
2492 }
2493 // v7.39 (round 224) — the body may open with WITH (CTEs);
2494 // route through the same CTE-then-SELECT path the top-level
2495 // WITH statement uses. v7.39 (round 225) — DML bodies parse
2496 // too (PG explains INSERT / UPDATE / DELETE).
2497 let inner = match self.peek().clone() {
2498 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2499 self.advance();
2500 self.parse_with_cte_then_select()?
2501 }
2502 Token::Insert => self.parse_insert_stmt(false)?,
2503 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2504 self.advance();
2505 self.parse_update_after_keyword()?
2506 }
2507 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2508 self.advance();
2509 self.parse_delete_after_keyword()?
2510 }
2511 _ => self.parse_select_stmt()?,
2512 };
2513 if !matches!(
2514 inner,
2515 Statement::Select(_)
2516 | Statement::Insert(_)
2517 | Statement::Update(_)
2518 | Statement::Delete(_)
2519 ) {
2520 return Err(self.err(format!(
2521 "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2522 )));
2523 }
2524 Ok(Statement::Explain(crate::ast::ExplainStatement {
2525 analyze,
2526 inner: Box::new(inner),
2527 suggest,
2528 costs_off,
2529 buffers,
2530 timing_off,
2531 settings,
2532 wal,
2533 summary_off,
2534 format,
2535 }))
2536 }
2537 Token::Create => self.parse_create_stmt(),
2538 Token::Insert => self.parse_insert_stmt(false),
2539 // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2540 // spelling; route to the same handler. DESC is the
2541 // reserved ORDER BY token, so it gets its own arm.
2542 Token::Ident(s)
2543 if s.eq_ignore_ascii_case("describe")
2544 && matches!(
2545 self.tokens.get(self.pos + 1),
2546 Some(Token::Ident(_) | Token::QuotedIdent(_))
2547 ) =>
2548 {
2549 self.advance();
2550 let table = self.expect_ident_like()?;
2551 Ok(Statement::ShowColumns(table))
2552 }
2553 Token::Desc
2554 if matches!(
2555 self.tokens.get(self.pos + 1),
2556 Some(Token::Ident(_) | Token::QuotedIdent(_))
2557 ) =>
2558 {
2559 self.advance();
2560 let table = self.expect_ident_like()?;
2561 Ok(Statement::ShowColumns(table))
2562 }
2563 // `COPY table [(cols)] TO STDOUT` — the export half of
2564 // pg_dump's COPY pair (the FROM stdin half rides the
2565 // embed import path). Options need a format design and
2566 // error honestly.
2567 Token::Ident(s)
2568 if s.eq_ignore_ascii_case("copy")
2569 && matches!(
2570 self.tokens.get(self.pos + 1),
2571 Some(Token::Ident(_) | Token::QuotedIdent(_))
2572 ) =>
2573 {
2574 self.advance(); // COPY
2575 let table = self.expect_ident_like()?;
2576 let columns = if matches!(self.peek(), Token::LParen) {
2577 self.advance();
2578 let mut cols = alloc::vec![self.expect_ident_like()?];
2579 while matches!(self.peek(), Token::Comma) {
2580 self.advance();
2581 cols.push(self.expect_ident_like()?);
2582 }
2583 if !matches!(self.peek(), Token::RParen) {
2584 return Err(self.err(format!(
2585 "expected ')' after COPY column list, got {:?}",
2586 self.peek()
2587 )));
2588 }
2589 self.advance();
2590 Some(cols)
2591 } else {
2592 None
2593 };
2594 // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2595 // endpoint. (FROM STDIN still rides the wire/import path —
2596 // its data arrives out of band.)
2597 if matches!(self.peek(), Token::From)
2598 && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2599 {
2600 self.advance(); // FROM
2601 let Token::String(path) = self.advance() else {
2602 unreachable!()
2603 };
2604 let options = self.parse_copy_to_options()?;
2605 return Ok(Statement::CopyFromFile {
2606 table,
2607 columns,
2608 path,
2609 options,
2610 });
2611 }
2612 if !matches!(self.peek(), Token::To) {
2613 return Err(self.err(format!(
2614 "COPY: only TO STDOUT is supported here (FROM stdin \
2615 rides the import path); got {:?}",
2616 self.peek()
2617 )));
2618 }
2619 self.advance();
2620 if matches!(self.peek(), Token::String(_)) {
2621 let Token::String(path) = self.advance() else { unreachable!() };
2622 let options = self.parse_copy_to_options()?;
2623 return Ok(Statement::CopyToFile {
2624 table,
2625 columns,
2626 query: None,
2627 path,
2628 options,
2629 });
2630 }
2631 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2632 return Err(self.err(format!(
2633 "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2634 self.peek()
2635 )));
2636 }
2637 self.advance();
2638 let options = self.parse_copy_to_options()?;
2639 Ok(Statement::CopyTo {
2640 table,
2641 columns,
2642 query: None,
2643 options,
2644 })
2645 }
2646 // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2647 // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2648 // result set is streamed in COPY format (PG's query form).
2649 Token::Ident(s)
2650 if s.eq_ignore_ascii_case("copy")
2651 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2652 {
2653 self.advance(); // COPY
2654 self.advance(); // (
2655 let query = self.parse_select_stmt()?;
2656 if !matches!(self.peek(), Token::RParen) {
2657 return Err(self.err(format!(
2658 "expected ')' after COPY query, got {:?}",
2659 self.peek()
2660 )));
2661 }
2662 self.advance(); // )
2663 if !matches!(self.peek(), Token::To) {
2664 return Err(self.err(format!(
2665 "COPY (query): only TO STDOUT is supported, got {:?}",
2666 self.peek()
2667 )));
2668 }
2669 self.advance();
2670 if matches!(self.peek(), Token::String(_)) {
2671 let Token::String(path) = self.advance() else { unreachable!() };
2672 let options = self.parse_copy_to_options()?;
2673 return Ok(Statement::CopyToFile {
2674 table: String::new(),
2675 columns: None,
2676 query: Some(alloc::boxed::Box::new(query)),
2677 path,
2678 options,
2679 });
2680 }
2681 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2682 return Err(self.err(format!(
2683 "COPY (query): TO supports STDOUT only, got {:?}",
2684 self.peek()
2685 )));
2686 }
2687 self.advance();
2688 let options = self.parse_copy_to_options()?;
2689 Ok(Statement::CopyTo {
2690 table: String::new(),
2691 columns: None,
2692 query: Some(alloc::boxed::Box::new(query)),
2693 options,
2694 })
2695 }
2696 // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2697 // Shares the INSERT body; the replace flag lowers it
2698 // onto ON CONFLICT DO UPDATE with an empty assignment
2699 // list (engine: replace the whole row).
2700 Token::Ident(s)
2701 if s.eq_ignore_ascii_case("replace")
2702 && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2703 {
2704 self.parse_insert_stmt(true)
2705 }
2706 Token::Begin => {
2707 self.advance();
2708 // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2709 // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2710 // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2711 // is consumed first, then the trailing modes — including the
2712 // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2713 // WORK/TRANSACTION). The explicit level, when present, rides the
2714 // statement so `exec_begin` applies it for this transaction.
2715 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2716 {
2717 self.advance();
2718 }
2719 let iso = self.parse_isolation_level_clauses()?;
2720 Ok(Statement::Begin(iso))
2721 }
2722 // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2723 // for BEGIN. START is contextual in PG too; pattern-match
2724 // on the ident here. Iso clauses are parse-and-ignored,
2725 // same as BEGIN above.
2726 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2727 self.advance();
2728 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2729 {
2730 return Err(self.err(alloc::format!(
2731 "expected TRANSACTION after START, got {:?}",
2732 self.peek()
2733 )));
2734 }
2735 self.advance();
2736 let iso = self.parse_isolation_level_clauses()?;
2737 Ok(Statement::Begin(iso))
2738 }
2739 Token::Commit => {
2740 self.advance();
2741 // PG: `COMMIT [WORK | TRANSACTION]`.
2742 if let Token::Ident(w) = self.peek()
2743 && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2744 {
2745 self.advance();
2746 }
2747 Ok(Statement::Commit)
2748 }
2749 // r1066 (7.38 S5.1) — `END [WORK | TRANSACTION]` is PG's
2750 // COMMIT synonym; pgbench's builtin tpcb-like script closes
2751 // every transaction with `END;` and the drop-in aborted on
2752 // it. Only reachable at statement start (CASE … END lives
2753 // inside expressions), so no ambiguity.
2754 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
2755 self.advance();
2756 if let Token::Ident(w) = self.peek()
2757 && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2758 {
2759 self.advance();
2760 }
2761 Ok(Statement::Commit)
2762 }
2763 Token::Rollback => {
2764 self.advance();
2765 // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2766 // savepoint without ending the transaction. Bare
2767 // `ROLLBACK` drops the whole TX.
2768 if matches!(self.peek(), Token::To) {
2769 self.advance();
2770 if matches!(self.peek(), Token::Savepoint) {
2771 self.advance();
2772 }
2773 let name = self.expect_ident_like()?;
2774 Ok(Statement::RollbackToSavepoint(name))
2775 } else {
2776 Ok(Statement::Rollback)
2777 }
2778 }
2779 Token::Savepoint => {
2780 self.advance();
2781 let name = self.expect_ident_like()?;
2782 Ok(Statement::Savepoint(name))
2783 }
2784 Token::Release => {
2785 self.advance();
2786 // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2787 // is optional in standard SQL.
2788 if matches!(self.peek(), Token::Savepoint) {
2789 self.advance();
2790 }
2791 let name = self.expect_ident_like()?;
2792 Ok(Statement::ReleaseSavepoint(name))
2793 }
2794 Token::Show => {
2795 self.advance();
2796 // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2797 // v6.1.2 promoted TABLES to a reserved keyword (for
2798 // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2799 // arrives as `Token::Tables` rather than a bare ident.
2800 // USERS / COLUMNS remain bare idents.
2801 let target = match self.advance() {
2802 Token::Tables => "tables".to_string(),
2803 // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2804 // keyword token; recognise it as the SHOW CREATE
2805 // dispatch keyword too.
2806 Token::Create => "create".to_string(),
2807 // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2808 // keyword too; let SHOW INDEX FROM parse.
2809 Token::Index => "index".to_string(),
2810 // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2811 // reserved (used in aggregate function calls);
2812 // recognise it here so the parser dispatches
2813 // to ShowParameter("all") — the engine returns
2814 // the curated parameter inventory.
2815 Token::All => "all".to_string(),
2816 // v7.38.18 (C12) — `SHOW COUNT(*) WARNINGS`, MySQL's
2817 // spelling for the size of the diagnostics area.
2818 // MySQL-dialect only: PostgreSQL 18.4 answers this
2819 // phrase with `syntax error at or near "("`, and a
2820 // PG session must keep getting exactly that rather
2821 // than a message about an unknown parameter.
2822 // `COUNT` arrives as a bare ident; the `(*)` and the
2823 // trailing keyword are consumed here so the whole
2824 // form reaches the engine as one parameter name.
2825 Token::Ident(ref c)
2826 if self.mysql_dialect
2827 && c.eq_ignore_ascii_case("count")
2828 && matches!(self.peek(), Token::LParen) =>
2829 {
2830 self.advance();
2831 if matches!(self.peek(), Token::Star) {
2832 self.advance();
2833 }
2834 if matches!(self.peek(), Token::RParen) {
2835 self.advance();
2836 }
2837 match self.advance() {
2838 Token::Ident(w) if w.eq_ignore_ascii_case("warnings") => {
2839 return Ok(Statement::ShowParameter(
2840 "count(*) warnings".to_string(),
2841 ));
2842 }
2843 other => {
2844 return Err(self.err(format!(
2845 "expected WARNINGS after SHOW COUNT(*), got {other:?}"
2846 )));
2847 }
2848 }
2849 }
2850 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2851 other => {
2852 return Err(self.err(format!(
2853 "expected SHOW target, got {other:?}"
2854 )));
2855 }
2856 };
2857 match target.as_str() {
2858 "tables" => Ok(Statement::ShowTables),
2859 "users" => Ok(Statement::ShowUsers),
2860 // v7.38 轴 4 — `SHOW transaction_isolation`
2861 // returns the currently-selected isolation level.
2862 "transaction_isolation" => Ok(Statement::ShowParameter(
2863 "transaction_isolation".to_string(),
2864 )),
2865 // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2866 // TABLE <t>` returns a 2-column row: (Table,
2867 // Create Table). mysqldump emits this for every
2868 // table at scrape time; without it the dump
2869 // round-trip stalls.
2870 // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2871 // FROM <t>` (also spelled `SHOW INDEX` and
2872 // `SHOW KEYS`). admin / mysqldump probes use
2873 // it to list per-table indexes.
2874 "indexes" | "index" | "keys" => {
2875 if !matches!(self.peek(), Token::From) {
2876 return Err(self.err(format!(
2877 "expected FROM after SHOW INDEXES, got {:?}",
2878 self.peek()
2879 )));
2880 }
2881 self.advance();
2882 let table = self.expect_ident_like()?;
2883 Ok(Statement::ShowIndexes(table))
2884 }
2885 // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2886 // `SHOW VARIABLES`. Both return a 2-column row
2887 // set listing server-side state; clients probe
2888 // them at connect time.
2889 "status" => Ok(Statement::ShowStatus),
2890 "variables" => {
2891 // r1067 — `SHOW VARIABLES LIKE 'pat'`.
2892 if matches!(self.peek(), Token::Like) {
2893 self.advance();
2894 let pat = match self.advance() {
2895 Token::String(p) => p,
2896 other => {
2897 return Err(self.err(format!(
2898 "SHOW VARIABLES LIKE expects a quoted pattern, got {other:?}"
2899 )));
2900 }
2901 };
2902 return Ok(Statement::ShowVariablesLike(pat));
2903 }
2904 Ok(Statement::ShowVariables)
2905 }
2906 // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2907 "processlist" => Ok(Statement::ShowProcesslist),
2908 "create" => {
2909 // SHOW CREATE TABLE / VIEW / DATABASE — only
2910 // TABLE is supported in v7.17.
2911 let kind = match self.advance() {
2912 Token::Ident(s) | Token::QuotedIdent(s) => s,
2913 Token::Table => "table".to_string(),
2914 other => {
2915 return Err(self.err(format!(
2916 "expected TABLE after SHOW CREATE, got {other:?}"
2917 )));
2918 }
2919 };
2920 if !kind.eq_ignore_ascii_case("table") {
2921 return Err(self.err(format!(
2922 "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2923 )));
2924 }
2925 let name = self.expect_ident_like()?;
2926 Ok(Statement::ShowCreateTable(name))
2927 }
2928 // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2929 // (and `SHOW SCHEMAS` alias). The mysql client uses
2930 // it to populate the database selector at connect
2931 // time; without it `mysql -p` errors before the
2932 // first user query.
2933 "databases" | "schemas" => Ok(Statement::ShowDatabases),
2934 // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2935 // keyword on its own; it lands here as a bare
2936 // ident. Returning all publications + their
2937 // scope summary.
2938 "publications" => Ok(Statement::ShowPublications),
2939 // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2940 "subscriptions" => Ok(Statement::ShowSubscriptions),
2941 "columns" => {
2942 if !matches!(self.peek(), Token::From) {
2943 return Err(self.err(format!(
2944 "expected FROM after SHOW COLUMNS, got {:?}",
2945 self.peek()
2946 )));
2947 }
2948 self.advance();
2949 let table = self.expect_ident_like()?;
2950 Ok(Statement::ShowColumns(table))
2951 }
2952 // v7.38 轴 4 surface — `SHOW <param>` for any
2953 // remaining session / preset parameter name
2954 // (server_version, search_path, client_encoding,
2955 // …). The engine's ShowParameter handler does the
2956 // dispatch; unrecognised names error there with
2957 // a pointer to pg_settings, not at parse time —
2958 // so a driver that issues `SHOW spam_setting`
2959 // gets a clear runtime error instead of a
2960 // confusing "unknown SHOW target".
2961 other => {
2962 // v7.38 (read01 P3.20) — a custom namespaced GUC
2963 // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
2964 // consume the dotted tail so it round-trips with
2965 // `SET app.foo` / `current_setting('app.foo')`.
2966 let mut full = other.to_string();
2967 while matches!(self.peek(), Token::Dot) {
2968 self.advance();
2969 let seg = self.expect_ident_like()?;
2970 full.push('.');
2971 full.push_str(&seg.to_ascii_lowercase());
2972 }
2973 Ok(Statement::ShowParameter(full))
2974 }
2975 }
2976 }
2977 // v6.1.2: `DROP` is now a reserved keyword (it dispatches
2978 // to DROP USER and DROP PUBLICATION today; DROP TABLE /
2979 // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
2980 // arrived as a bare ident; tokenising it dedicatedly
2981 // keeps the dispatch tree small.
2982 Token::Drop => {
2983 self.advance();
2984 match self.peek() {
2985 // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
2986 // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
2987 // around DROP ROLE cleanup. SPG has no role-owner
2988 // model, so consume to boundary as a no-op.
2989 Token::Ident(s) | Token::QuotedIdent(s)
2990 if s.eq_ignore_ascii_case("owned") =>
2991 {
2992 // v7.39 (round 696) — still a no-op (SPG has no
2993 // role-owner model), but the ROLE is carried out so
2994 // the engine can refuse one that does not exist,
2995 // which is what PG18 does.
2996 self.advance();
2997 if self.peek_is_by() {
2998 self.advance();
2999 }
3000 let names = self.take_comma_separated_names();
3001 self.consume_until_statement_boundary();
3002 Ok(Statement::ValidateOnly {
3003 kind: crate::ast::ValidateOnlyKind::RoleName,
3004 names,
3005 })
3006 }
3007 // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
3008 // It drops only a TEMPORARY table, and name resolution
3009 // already prefers the session's own, so the keyword is
3010 // consumed and the ordinary DROP TABLE path runs.
3011 Token::Ident(s) | Token::QuotedIdent(s)
3012 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
3013 {
3014 self.advance();
3015 if !matches!(self.peek(), Token::Table) {
3016 return Err(self.err(alloc::format!(
3017 "expected TABLE after DROP TEMPORARY, got {:?}",
3018 self.peek()
3019 )));
3020 }
3021 self.parse_drop_table_after_keyword()
3022 }
3023 Token::Publication => {
3024 self.advance();
3025 // v7.39 (round 754, F31-B4) — the round-753
3026 // audit probe tripped over the missing
3027 // `IF EXISTS` here (syntax error).
3028 let if_exists = self.consume_if_exists();
3029 let name = self.expect_ident_or_string()?;
3030 Ok(Statement::DropPublication { name, if_exists })
3031 }
3032 Token::Subscription => {
3033 self.advance();
3034 let if_exists = self.consume_if_exists();
3035 let name = self.expect_ident_or_string()?;
3036 Ok(Statement::DropSubscription { name, if_exists })
3037 }
3038 Token::Ident(s) | Token::QuotedIdent(s)
3039 if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
3040 {
3041 self.advance();
3042 // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
3043 // login user IS a role in PG, and SPG's store holds
3044 // both. `IF EXISTS` is accepted on either spelling.
3045 let if_exists = self.consume_if_exists();
3046 let name = self.expect_ident_or_string()?;
3047 Ok(Statement::DropUser { name, if_exists })
3048 }
3049 // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
3050 // CREATE DATABASE has parsed since v7.14 and this did
3051 // not, so `DROP DATABASE IF EXISTS x` — what every
3052 // teardown script and pg_dumpall preamble opens with —
3053 // came back as a syntax error, which IF EXISTS cannot
3054 // soften. The name is carried so the engine can answer
3055 // the way PG does; PG never lets this succeed on a
3056 // single-database server, since the name is either
3057 // unknown ("database … does not exist", or a notice
3058 // under IF EXISTS) or the one you are connected to
3059 // ("cannot drop the currently open database").
3060 Token::Ident(s) | Token::QuotedIdent(s)
3061 if s.eq_ignore_ascii_case("database") =>
3062 {
3063 self.advance();
3064 let if_exists = self.consume_if_exists();
3065 let name = self.expect_ident_or_string()?;
3066 self.consume_until_statement_boundary();
3067 Ok(Statement::DropDatabase { name, if_exists })
3068 }
3069 // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
3070 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
3071 self.advance();
3072 let if_exists = self.consume_if_exists();
3073 let name = self.expect_ident_like()?;
3074 // ON <table>
3075 if !matches!(self.peek(), Token::On) {
3076 return Err(self.err(alloc::format!(
3077 "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
3078 self.peek()
3079 )));
3080 }
3081 self.advance();
3082 let table = self.expect_ident_like()?;
3083 Ok(Statement::DropTrigger {
3084 name,
3085 table,
3086 if_exists,
3087 })
3088 }
3089 // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
3090 // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
3091 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
3092 self.advance();
3093 let if_exists = self.consume_if_exists();
3094 let name = self.expect_ident_like()?;
3095 if !matches!(self.peek(), Token::On) {
3096 return Err(self.err(alloc::format!(
3097 "expected ON <table> after DROP RULE {name:?}, got {:?}",
3098 self.peek()
3099 )));
3100 }
3101 self.advance();
3102 let table = self.expect_ident_like()?;
3103 // Optional CASCADE / RESTRICT — accepted, no effect.
3104 self.consume_until_statement_boundary();
3105 Ok(Statement::DropRule {
3106 name,
3107 table,
3108 if_exists,
3109 })
3110 }
3111 // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
3112 // v7.12.4 ignores any optional arg-list (signature-
3113 // based overload disambiguation lands in v7.12.5+).
3114 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
3115 self.advance();
3116 let if_exists = self.consume_if_exists();
3117 let name = self.expect_ident_like()?;
3118 // v7.39 (read01 round 62) — the argument list identifies
3119 // WHICH overload to drop, so it is captured, not
3120 // discarded. `DROP FUNCTION f` (no list) is legal when
3121 // the name is unambiguous; the engine enforces that.
3122 let args = if matches!(self.peek(), Token::LParen) {
3123 Some(self.parse_function_signature_types()?)
3124 } else {
3125 None
3126 };
3127 // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
3128 // trailer, which `DROP TABLE` and `DROP INDEX` have
3129 // accepted since v7.14 and this one refused outright.
3130 // pg_dump writes it, so refusing was a parse error in
3131 // the middle of a restore. SPG drops the function
3132 // either way — it tracks no dependents to cascade to —
3133 // which is the same reading the other two give it.
3134 self.consume_drop_behaviour();
3135 Ok(Statement::DropFunction {
3136 name,
3137 args,
3138 if_exists,
3139 })
3140 }
3141 // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
3142 // [CASCADE|RESTRICT]. pg_dump and mysqldump both
3143 // emit DROP TABLE IF EXISTS at the head of every
3144 // CREATE TABLE block so re-importing a dump
3145 // overwrites prior state. SPG accepts and removes
3146 // matching tables; CASCADE/RESTRICT trailers
3147 // accepted silently.
3148 Token::Table => self.parse_drop_table_after_keyword(),
3149 // v7.14.0 — DROP INDEX [IF EXISTS] name
3150 // [CASCADE|RESTRICT]. PG / mysqldump emit this
3151 // for partial-index renames and pgvector
3152 // migrations. SPG removes the matching index;
3153 // IF EXISTS makes the drop idempotent.
3154 Token::Index => {
3155 self.advance();
3156 let if_exists = self.consume_if_exists();
3157 let name = self.expect_ident_like()?;
3158 if matches!(
3159 self.peek(),
3160 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3161 || s.eq_ignore_ascii_case("restrict")
3162 ) {
3163 self.advance();
3164 }
3165 Ok(Statement::DropIndex { name, if_exists })
3166 }
3167 // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3168 // [CASCADE|RESTRICT]. SPG is single-database;
3169 // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3170 // name [, name…] [CASCADE | RESTRICT]. Real
3171 // unregister (was silent no-op pre-v7.17).
3172 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3173 self.advance();
3174 let if_exists = self.consume_if_exists();
3175 let mut names = vec![self.expect_ident_like()?];
3176 while matches!(self.peek(), Token::Comma) {
3177 self.advance();
3178 names.push(self.expect_ident_like()?);
3179 }
3180 if matches!(
3181 self.peek(),
3182 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3183 || s.eq_ignore_ascii_case("restrict")
3184 ) {
3185 self.advance();
3186 }
3187 Ok(Statement::DropSchema { names, if_exists })
3188 }
3189 // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3190 // name [, name…] [CASCADE|RESTRICT].
3191 Token::Ident(s) | Token::QuotedIdent(s)
3192 if s.eq_ignore_ascii_case("type") =>
3193 {
3194 self.advance();
3195 let if_exists = self.consume_if_exists();
3196 let mut names = vec![self.expect_ident_like()?];
3197 while matches!(self.peek(), Token::Comma) {
3198 self.advance();
3199 names.push(self.expect_ident_like()?);
3200 }
3201 if matches!(
3202 self.peek(),
3203 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3204 || s.eq_ignore_ascii_case("restrict")
3205 ) {
3206 self.advance();
3207 }
3208 Ok(Statement::DropType { names, if_exists })
3209 }
3210 // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3211 // name [, name…] [CASCADE|RESTRICT].
3212 Token::Ident(s) | Token::QuotedIdent(s)
3213 if s.eq_ignore_ascii_case("domain") =>
3214 {
3215 self.advance();
3216 let if_exists = self.consume_if_exists();
3217 let mut names = vec![self.expect_ident_like()?];
3218 while matches!(self.peek(), Token::Comma) {
3219 self.advance();
3220 names.push(self.expect_ident_like()?);
3221 }
3222 if matches!(
3223 self.peek(),
3224 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3225 || s.eq_ignore_ascii_case("restrict")
3226 ) {
3227 self.advance();
3228 }
3229 Ok(Statement::DropDomain { names, if_exists })
3230 }
3231 // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3232 // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3233 Token::Ident(s) | Token::QuotedIdent(s)
3234 if s.eq_ignore_ascii_case("materialized") =>
3235 {
3236 self.advance();
3237 let nxt = self.peek().clone();
3238 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3239 {
3240 return Err(self.err(alloc::format!(
3241 "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3242 )));
3243 }
3244 self.advance();
3245 let if_exists = self.consume_if_exists();
3246 let mut names = vec![self.expect_ident_like()?];
3247 while matches!(self.peek(), Token::Comma) {
3248 self.advance();
3249 names.push(self.expect_ident_like()?);
3250 }
3251 if matches!(
3252 self.peek(),
3253 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3254 || s.eq_ignore_ascii_case("restrict")
3255 ) {
3256 self.advance();
3257 }
3258 Ok(Statement::DropMaterializedView { names, if_exists })
3259 }
3260 // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3261 // name [, name…] [CASCADE|RESTRICT].
3262 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3263 self.advance();
3264 let if_exists = self.consume_if_exists();
3265 let mut names = vec![self.expect_ident_like()?];
3266 while matches!(self.peek(), Token::Comma) {
3267 self.advance();
3268 names.push(self.expect_ident_like()?);
3269 }
3270 if matches!(
3271 self.peek(),
3272 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3273 || s.eq_ignore_ascii_case("restrict")
3274 ) {
3275 self.advance();
3276 }
3277 Ok(Statement::DropView { names, if_exists })
3278 }
3279 // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3280 // [CASCADE|RESTRICT]. Real removal from catalog
3281 // (was a silent no-op pre-v7.17).
3282 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3283 self.advance();
3284 let if_exists = self.consume_if_exists();
3285 let mut names = vec![self.expect_ident_like()?];
3286 while matches!(self.peek(), Token::Comma) {
3287 self.advance();
3288 names.push(self.expect_ident_like()?);
3289 }
3290 if matches!(
3291 self.peek(),
3292 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3293 || s.eq_ignore_ascii_case("restrict")
3294 ) {
3295 self.advance();
3296 }
3297 Ok(Statement::DropSequence { names, if_exists })
3298 }
3299 // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3300 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3301 self.advance();
3302 self.parse_drop_policy_after_keyword()
3303 }
3304 // v7.37.17 (17.6 siblings) — DROP <target> for
3305 // targets SPG doesn't natively track. pg_dump
3306 // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3307 // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3308 // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3309 // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3310 // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3311 // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3312 // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3313 // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3314 // etc. — accept + Empty-return so pg_dump tails
3315 // load through. Materialized-view drop dispatches
3316 // to the existing DropTable path when the token
3317 // is Materialized-View-shaped (elsewhere in
3318 // this parser).
3319 Token::Ident(s) | Token::QuotedIdent(s)
3320 if s.eq_ignore_ascii_case("text")
3321 // The DROP dispatch matches on PEEK — `text` is
3322 // not yet consumed, so SEARCH/CONFIGURATION sit
3323 // at pos+1/pos+2 (the round-695 trap's mirror).
3324 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3325 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3326 {
3327 // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3328 // validates the name; DICTIONARY / PARSER / TEMPLATE
3329 // stay in the noise arm below.
3330 self.advance(); // TEXT
3331 self.advance(); // SEARCH
3332 self.advance(); // CONFIGURATION
3333 let if_exists = self.consume_if_exists();
3334 let names = self.take_comma_separated_names();
3335 self.consume_until_statement_boundary();
3336 if if_exists {
3337 return Ok(Statement::Empty);
3338 }
3339 Ok(Statement::ValidateOnly {
3340 kind: crate::ast::ValidateOnlyKind::TsConfigName,
3341 names,
3342 })
3343 }
3344 Token::Ident(s) | Token::QuotedIdent(s)
3345 if matches!(
3346 s.to_ascii_lowercase().as_str(),
3347 "type"
3348 | "domain"
3349 | "operator"
3350 | "cast"
3351 // `text` = TEXT SEARCH DICTIONARY / PARSER /
3352 // TEMPLATE (CONFIGURATION intercepted above).
3353 | "text"
3354 | "materialized"
3355 | "large"
3356 | "role"
3357 | "access"
3358 | "procedure"
3359 | "routine"
3360 ) =>
3361 {
3362 self.consume_until_statement_boundary();
3363 Ok(Statement::Empty)
3364 }
3365 // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3366 // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3367 // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3368 // foreign-data warning family (round 706) so a
3369 // CREATE→DROP sequence in a dump stays consistent.
3370 Token::Ident(s) | Token::QuotedIdent(s)
3371 if s.eq_ignore_ascii_case("server")
3372 || s.eq_ignore_ascii_case("foreign") =>
3373 {
3374 self.advance();
3375 self.consume_until_statement_boundary();
3376 Ok(Statement::ValidateOnly {
3377 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3378 names: Vec::new(),
3379 })
3380 }
3381 Token::Ident(s) | Token::QuotedIdent(s)
3382 if s.eq_ignore_ascii_case("collation")
3383 || s.eq_ignore_ascii_case("tablespace") =>
3384 {
3385 let kind = if s.eq_ignore_ascii_case("collation") {
3386 crate::ast::ValidateOnlyKind::CollationName
3387 } else {
3388 crate::ast::ValidateOnlyKind::TablespaceName
3389 };
3390 self.advance();
3391 let if_exists = self.consume_if_exists();
3392 let names = self.take_comma_separated_names();
3393 self.consume_until_statement_boundary();
3394 if if_exists {
3395 return Ok(Statement::Empty);
3396 }
3397 Ok(Statement::ValidateOnly { kind, names })
3398 }
3399 Token::Ident(s) | Token::QuotedIdent(s)
3400 if s.eq_ignore_ascii_case("event") =>
3401 {
3402 self.advance();
3403 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3404 {
3405 self.advance();
3406 }
3407 let if_exists = self.consume_if_exists();
3408 let names = self.take_comma_separated_names();
3409 self.consume_until_statement_boundary();
3410 if if_exists {
3411 return Ok(Statement::Empty);
3412 }
3413 Ok(Statement::ValidateOnly {
3414 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3415 names,
3416 })
3417 }
3418 // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3419 // leave the noise list; see the ValidateOnly kinds.
3420 Token::Ident(s) | Token::QuotedIdent(s)
3421 if s.eq_ignore_ascii_case("conversion")
3422 || s.eq_ignore_ascii_case("language")
3423 // `DROP PROCEDURAL LANGUAGE` puts the modifier
3424 // FIRST — the first draft looked for it after.
3425 || s.eq_ignore_ascii_case("procedural") =>
3426 {
3427 let kind = if s.eq_ignore_ascii_case("conversion") {
3428 crate::ast::ValidateOnlyKind::ConversionName
3429 } else {
3430 crate::ast::ValidateOnlyKind::LanguageName
3431 };
3432 self.advance();
3433 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3434 {
3435 self.advance();
3436 }
3437 let if_exists = self.consume_if_exists();
3438 let names = self.take_comma_separated_names();
3439 self.consume_until_statement_boundary();
3440 if if_exists {
3441 return Ok(Statement::Empty);
3442 }
3443 Ok(Statement::ValidateOnly { kind, names })
3444 }
3445 // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3446 // name(argtypes)[, …]`. Parsed for real so the engine
3447 // can answer as PG does; see Statement::DropAggregate.
3448 Token::Ident(s) | Token::QuotedIdent(s)
3449 if s.eq_ignore_ascii_case("aggregate") =>
3450 {
3451 self.advance();
3452 let if_exists = self.consume_if_exists();
3453 let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3454 loop {
3455 let name = self.expect_ident_like()?;
3456 if !matches!(self.peek(), Token::LParen) {
3457 return Err(self.err(alloc::format!(
3458 "expected argument list after DROP AGGREGATE {name}"
3459 )));
3460 }
3461 self.advance();
3462 let mut args: Vec<String> = Vec::new();
3463 let mut star = false;
3464 loop {
3465 match self.peek().clone() {
3466 Token::RParen => {
3467 self.advance();
3468 break;
3469 }
3470 Token::Star => {
3471 self.advance();
3472 star = true;
3473 }
3474 Token::Comma => {
3475 self.advance();
3476 }
3477 _ => {
3478 // A type name may be multi-token
3479 // (`double precision`); glue idents
3480 // until , or ).
3481 let mut t = self.expect_ident_like()?;
3482 while let Token::Ident(nx) = self.peek() {
3483 let nx = nx.clone();
3484 self.advance();
3485 t.push(' ');
3486 t.push_str(&nx);
3487 }
3488 args.push(t);
3489 }
3490 }
3491 }
3492 items.push((name, if star { None } else { Some(args) }));
3493 if matches!(self.peek(), Token::Comma) {
3494 self.advance();
3495 } else {
3496 break;
3497 }
3498 }
3499 self.consume_until_statement_boundary();
3500 Ok(Statement::DropAggregate { if_exists, items })
3501 }
3502 // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3503 // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3504 // installed; `IF EXISTS` is the spelling that says do
3505 // not, and it keeps the no-op.
3506 Token::Ident(s) | Token::QuotedIdent(s)
3507 if s.eq_ignore_ascii_case("extension") =>
3508 {
3509 self.advance();
3510 let if_exists = self.consume_if_exists();
3511 let names = self.take_comma_separated_names();
3512 self.consume_until_statement_boundary();
3513 if if_exists {
3514 return Ok(Statement::Empty);
3515 }
3516 Ok(Statement::ValidateOnly {
3517 kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3518 names,
3519 })
3520 }
3521 Token::Ident(s) | Token::QuotedIdent(s)
3522 if s.eq_ignore_ascii_case("statistics") =>
3523 {
3524 self.parse_drop_statistics_after_drop()
3525 }
3526 other => Err(self.err(format!(
3527 "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3528 SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3529 ))),
3530 }
3531 }
3532 // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3533 // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3534 // and accepted before the view name. SPG materialised
3535 // views re-evaluate on read (always-fresh semantics), so
3536 // the CONCURRENTLY-vs-serial distinction has no runtime
3537 // effect — the refresh body does not block readers either
3538 // way. Same accept-and-no-op pattern as DETACH PARTITION
3539 // CONCURRENTLY (16.5).
3540 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3541 self.advance();
3542 let nxt = self.peek().clone();
3543 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3544 {
3545 return Err(self.err(alloc::format!(
3546 "expected MATERIALIZED after REFRESH, got {nxt:?}"
3547 )));
3548 }
3549 self.advance();
3550 let nxt2 = self.peek().clone();
3551 if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3552 {
3553 return Err(self.err(alloc::format!(
3554 "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3555 )));
3556 }
3557 self.advance();
3558 // Optional CONCURRENTLY noise word — consumed without
3559 // changing semantics.
3560 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3561 {
3562 self.advance();
3563 }
3564 let name = self.expect_ident_like()?;
3565 let with_data = self.parse_optional_with_data(true)?;
3566 Ok(Statement::RefreshMaterializedView { name, with_data })
3567 }
3568 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3569 self.advance();
3570 self.parse_update_after_keyword()
3571 }
3572 // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3573 // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3574 // [CASCADE | RESTRICT]. Clears every row from each named
3575 // table. Parses at the top level; the engine dispatcher
3576 // walks Statement::Truncate.
3577 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3578 self.advance();
3579 // Optional TABLE noise word — PG accepts both the reserved
3580 // token and the bare identifier spelling.
3581 if matches!(self.peek(), Token::Table)
3582 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3583 {
3584 self.advance();
3585 }
3586 // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3587 // not absorbed. The lookahead keeps a table genuinely
3588 // called `only` working: the keyword is a keyword only
3589 // when a name follows it.
3590 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3591 if s.eq_ignore_ascii_case("only"))
3592 && matches!(
3593 self.tokens.get(self.pos + 1),
3594 Some(Token::Ident(_) | Token::QuotedIdent(_))
3595 );
3596 if only {
3597 self.advance();
3598 }
3599 // Table names (comma-separated).
3600 let mut tables = Vec::new();
3601 loop {
3602 tables.push(self.expect_ident_like()?);
3603 if matches!(self.peek(), Token::Comma) {
3604 self.advance();
3605 continue;
3606 }
3607 break;
3608 }
3609 // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3610 let mut restart_identity = false;
3611 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3612 {
3613 self.advance();
3614 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3615 {
3616 self.advance();
3617 restart_identity = true;
3618 }
3619 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3620 {
3621 self.advance();
3622 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3623 {
3624 self.advance();
3625 }
3626 }
3627 // Optional CASCADE / RESTRICT.
3628 let mut cascade = false;
3629 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3630 {
3631 self.advance();
3632 cascade = true;
3633 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3634 {
3635 self.advance();
3636 }
3637 Ok(Statement::Truncate {
3638 tables,
3639 restart_identity,
3640 cascade,
3641 only,
3642 })
3643 }
3644 // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3645 // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3646 // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3647 // rows change so the index tree is always up-to-date;
3648 // REINDEX is a strict no-op. Accept the whole statement
3649 // shape to boundary for pg_dump round-trip compatibility.
3650 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3651 // v7.39 (round 535) — the target is CARRIED now. SPG has no
3652 // index bloat to rebuild, so the work stays a no-op, but PG
3653 // validates what it was pointed at and this swallowed the
3654 // name at parse time — `REINDEX TABLE typo` reported
3655 // success. Measured on PG18: INDEX / TABLE name a relation,
3656 // SCHEMA a schema, SYSTEM nothing.
3657 self.advance();
3658 self.parse_reindex_tail()
3659 }
3660 // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3661 // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3662 // SPG has no MVCC bloat today (Phase D visibility map
3663 // queues with v7.38); the freezer collapses hot-tier
3664 // rows into cold segments automatically. VACUUM is a
3665 // no-op — pg_dump maintenance scripts and Discourse's
3666 // periodic-maintenance path both emit it.
3667 // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3668 // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3669 // actual bloat, so the pre-MVCC accept-and-ignore posture
3670 // became a silent no-op on a customer's manual reclaim.
3671 // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3672 // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3673 // ANALYZE is captured, the optional table name is captured.
3674 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3675 self.advance();
3676 // Parenthesised option list: absorb it.
3677 if matches!(self.peek(), Token::LParen) {
3678 let mut depth = 0usize;
3679 loop {
3680 match self.advance() {
3681 Token::LParen => depth += 1,
3682 Token::RParen => {
3683 depth -= 1;
3684 if depth == 0 {
3685 break;
3686 }
3687 }
3688 Token::Eof => break,
3689 _ => {}
3690 }
3691 }
3692 }
3693 let mut analyze = false;
3694 let mut table: Option<String> = None;
3695 loop {
3696 match self.peek() {
3697 // v7.39 (round 535) — `FULL` lexes as a keyword, not
3698 // an identifier, so the loop below broke out on it and
3699 // dropped the table name: `VACUUM FULL nosuch` was
3700 // accepted where `VACUUM nosuch` was refused.
3701 Token::Full => {
3702 self.advance();
3703 }
3704 Token::Ident(w) | Token::QuotedIdent(w) => {
3705 let wl = w.to_ascii_lowercase();
3706 match wl.as_str() {
3707 "full" | "freeze" | "verbose" => {
3708 self.advance();
3709 }
3710 "analyze" | "analyse" => {
3711 analyze = true;
3712 self.advance();
3713 }
3714 _ => {
3715 table = Some(self.expect_ident_like()?);
3716 break;
3717 }
3718 }
3719 }
3720 _ => break,
3721 }
3722 }
3723 // Optional trailing column list / anything else to the
3724 // statement boundary (PG accepts per-column ANALYZE).
3725 self.consume_until_statement_boundary();
3726 Ok(Statement::Vacuum { table, analyze })
3727 }
3728 // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3729 // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3730 // <index>. PG stores rows in physical order matching
3731 // an index; SPG's hot-tier is append-only + cold-tier
3732 // is segment-frozen, so clustering has no persistent
3733 // effect. Accept-and-no-op for pg_dump compat.
3734 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3735 // v7.39 (round 535) — same as REINDEX above: the relation is
3736 // carried so the engine can refuse one that does not exist.
3737 // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3738 self.advance();
3739 self.parse_cluster_tail()
3740 }
3741 // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3742 // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3743 // optional string payload; UNLISTEN takes a channel or `*`.
3744 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3745 self.advance();
3746 let ch = match self.advance() {
3747 Token::Ident(c) | Token::QuotedIdent(c) => c,
3748 other => {
3749 return Err(self.err(format!(
3750 "expected channel name after LISTEN, got {other:?}"
3751 )));
3752 }
3753 };
3754 Ok(Statement::Listen(ch))
3755 }
3756 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3757 self.advance();
3758 let channel = match self.advance() {
3759 Token::Ident(c) | Token::QuotedIdent(c) => c,
3760 other => {
3761 return Err(self.err(format!(
3762 "expected channel name after NOTIFY, got {other:?}"
3763 )));
3764 }
3765 };
3766 let payload = if matches!(self.peek(), Token::Comma) {
3767 self.advance();
3768 match self.advance() {
3769 Token::String(p) => Some(p),
3770 other => {
3771 return Err(self.err(format!(
3772 "expected string payload after NOTIFY <channel>, got {other:?}"
3773 )));
3774 }
3775 }
3776 } else {
3777 None
3778 };
3779 Ok(Statement::Notify { channel, payload })
3780 }
3781 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3782 self.advance();
3783 match self.advance() {
3784 Token::Star => Ok(Statement::Unlisten(None)),
3785 Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3786 other => Err(self.err(format!(
3787 "expected channel name or * after UNLISTEN, got {other:?}"
3788 ))),
3789 }
3790 }
3791 // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3792 // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3793 // process-wide write lock today; explicit LOCK has no
3794 // effect. Accept-and-no-op for pg_dump / migration
3795 // compat.
3796 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3797 self.advance();
3798 // v7.39 (round 696) — the LOCK still has no effect (SPG's
3799 // engine holds a process-wide write lock), but the TABLE
3800 // NAME is now carried out so the engine can refuse one that
3801 // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3802 // READ|WRITE` is a different statement with the same first
3803 // word; it keeps the old no-op, because a MySQL dump's
3804 // bracket names tables it is about to create.
3805 let mysql_tables = matches!(self.peek(), Token::Ident(k)
3806 if k.eq_ignore_ascii_case("tables"));
3807 if mysql_tables {
3808 self.consume_until_statement_boundary();
3809 return Ok(Statement::Empty);
3810 }
3811 if matches!(self.peek(), Token::Table) {
3812 self.advance();
3813 }
3814 let names = self.take_comma_separated_names();
3815 self.consume_until_statement_boundary();
3816 Ok(Statement::ValidateOnly {
3817 kind: crate::ast::ValidateOnlyKind::LockTable,
3818 names,
3819 })
3820 }
3821 // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3822 // durability marker + snapshot in PG. SPG has WAL
3823 // checkpointing on a byte / time schedule (v7.37.10
3824 // 60s / 4 MiB defaults). The bare statement parses to
3825 // `Statement::Empty` here (the no_std engine owns no
3826 // WAL / snapshot); v7.37 Epic Du wires the HOST
3827 // (embedded `Database::execute_buffered`, via
3828 // `sql_is_checkpoint`) to force an immediate synchronous
3829 // checkpoint through `Database::checkpoint` — a real
3830 // durability barrier, matching PG.
3831 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3832 self.advance();
3833 self.consume_until_statement_boundary();
3834 Ok(Statement::Empty)
3835 }
3836 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3837 self.advance();
3838 self.parse_delete_after_keyword()
3839 }
3840 // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3841 // ALTER is not a reserved keyword in the lexer — handled
3842 // as a bare ident here.
3843 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3844 self.advance();
3845 self.parse_alter_after_keyword()
3846 }
3847 // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3848 // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3849 // additions needed.
3850 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3851 self.advance();
3852 self.parse_wait_after_keyword()
3853 }
3854 // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3855 // Bare ANALYZE → analyse every user table; ANALYZE
3856 // <name> → re-stats one. The argument is an optional
3857 // ident (or quoted ident); anything else is a parse
3858 // error.
3859 // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3860 // `WHERE` filter (carved out per V6_7_DESIGN.md
3861 // STABILITY). Lex order: identifier "compact" → "cold"
3862 // → "segments". Anything else after `COMPACT` is a
3863 // parse error.
3864 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3865 self.advance();
3866 let next = self.peek().clone();
3867 let cold = match next {
3868 Token::Ident(s) | Token::QuotedIdent(s) => s,
3869 _ => {
3870 return Err(
3871 self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3872 );
3873 }
3874 };
3875 if !cold.eq_ignore_ascii_case("cold") {
3876 return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3877 }
3878 self.advance();
3879 let next = self.peek().clone();
3880 let segments = match next {
3881 Token::Ident(s) | Token::QuotedIdent(s) => s,
3882 _ => {
3883 return Err(self.err(format!(
3884 "expected SEGMENTS after COMPACT COLD, got {:?}",
3885 self.peek()
3886 )));
3887 }
3888 };
3889 if !segments.eq_ignore_ascii_case("segments") {
3890 return Err(self.err(format!(
3891 "expected SEGMENTS after COMPACT COLD, got {segments:?}"
3892 )));
3893 }
3894 self.advance();
3895 Ok(Statement::CompactColdSegments)
3896 }
3897 // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
3898 // Parsed as a case-insensitive identifier since MERGE
3899 // isn't a reserved lexer keyword (collides with the
3900 // mysqldump `ALGORITHM = MERGE` view clause if it
3901 // were); the inner parser drives the rest of the
3902 // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
3903 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
3904 self.advance();
3905 self.parse_merge_after_keyword()
3906 }
3907 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
3908 self.advance();
3909 let target = match self.peek() {
3910 Token::Eof | Token::Semicolon => None,
3911 Token::Ident(_) | Token::QuotedIdent(_) => {
3912 Some(self.expect_ident_like()?)
3913 }
3914 other => {
3915 return Err(self.err(format!(
3916 "expected table name or end of statement after ANALYZE, got {other:?}"
3917 )));
3918 }
3919 };
3920 // v7.39 (round 776, F31 J7) — the per-column form
3921 // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
3922 // here while the VACUUM arm already consumed it; SPG
3923 // analyzes whole tables, so the list parses and is
3924 // accepted like the VACUUM path's.
3925 if target.is_some() && matches!(self.peek(), Token::LParen) {
3926 self.advance();
3927 loop {
3928 let _ = self.expect_ident_like()?;
3929 match self.peek() {
3930 Token::Comma => {
3931 self.advance();
3932 }
3933 Token::RParen => {
3934 self.advance();
3935 break;
3936 }
3937 other => {
3938 return Err(self.err(format!(
3939 "expected ',' or ')' in ANALYZE column list, got {other:?}"
3940 )));
3941 }
3942 }
3943 }
3944 }
3945 Ok(Statement::Analyze(target))
3946 }
3947 // v7.12.1 — `SET <name> [TO|=] <value>`. The
3948 // `default_text_search_config` parameter is consumed
3949 // by the FTS function dispatcher; other parameter
3950 // names are recorded but treated as a no-op so PG
3951 // dump output loads.
3952 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
3953 self.advance();
3954 // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
3955 // adds `SET GLOBAL` too (and the alias `SET @@global.name =
3956 // …` which the SessionVar path handles). `LOCAL` is the only
3957 // one that changes semantics — it scopes the change to the
3958 // current transaction — so capture it; SESSION / GLOBAL are
3959 // accepted and treated as the default session scope.
3960 let mut set_local = false;
3961 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
3962 let q = s.to_ascii_lowercase();
3963 if q == "local" || q == "session" || q == "global" {
3964 set_local = q == "local";
3965 self.advance();
3966 }
3967 }
3968 // 7.38.1 S5.2 — PG `SET [SESSION] AUTHORIZATION
3969 // { DEFAULT | <role> }`. pg_dump's ACL section switches
3970 // to the object owner with it. SPG maps it onto the
3971 // session-role machinery (recorded delta RD-10: PG moves
3972 // session_user too; SPG moves the effective role).
3973 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3974 if s.eq_ignore_ascii_case("authorization"))
3975 {
3976 self.advance(); // AUTHORIZATION
3977 let role = match self.peek().clone() {
3978 Token::Default => {
3979 self.advance();
3980 None
3981 }
3982 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
3983 self.advance();
3984 Some(s)
3985 }
3986 _ => None,
3987 };
3988 return Ok(Statement::SetRole(role));
3989 }
3990 // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
3991 // <collation>]` — change the connection client
3992 // charset. SPG stores UTF-8 always and orders
3993 // bytewise; accept as a no-op.
3994 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
3995 {
3996 self.advance();
3997 // Charset ident-or-string.
3998 if matches!(
3999 self.peek(),
4000 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4001 ) {
4002 self.advance();
4003 }
4004 // Optional `COLLATE <name>`.
4005 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
4006 {
4007 self.advance();
4008 if matches!(
4009 self.peek(),
4010 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4011 ) {
4012 self.advance();
4013 }
4014 }
4015 return Ok(Statement::Empty);
4016 }
4017 // v7.37.17 (17.6 sibling) — PG `SET ROLE
4018 // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
4019 // uses this to switch to the object owner before
4020 // recreating tables. SPG has no role system so this
4021 // is a no-op.
4022 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
4023 {
4024 self.advance(); // ROLE
4025 // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
4026 // reset to the login identity; a name / string sets the
4027 // effective role that drives current_user + RLS.
4028 let role = match self.peek().clone() {
4029 Token::Default => {
4030 self.advance();
4031 None
4032 }
4033 Token::Ident(s) | Token::QuotedIdent(s)
4034 if s.eq_ignore_ascii_case("none") =>
4035 {
4036 self.advance();
4037 None
4038 }
4039 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4040 self.advance();
4041 Some(s)
4042 }
4043 _ => None,
4044 };
4045 return Ok(Statement::SetRole(role));
4046 }
4047 // v7.37.17 (17.6 sibling) — PG `SET SESSION
4048 // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
4049 // ISO SQL surface). pg_dump prepends this to fix
4050 // the isolation level for the restore session. SPG
4051 // defaults to READ COMMITTED and doesn't yet honor
4052 // session-set isolation across statements — accept
4053 // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
4054 // per-tx form is handled elsewhere.
4055 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
4056 {
4057 self.advance(); // CHARACTERISTICS
4058 self.consume_until_statement_boundary();
4059 return Ok(Statement::Empty);
4060 }
4061 // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
4062 // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
4063 // pg_dump emits this to control the deferrability of
4064 // FK / UNIQUE constraints across a bulk restore. SPG
4065 // has no deferrable-constraint machinery today; the
4066 // FK checker is strict-immediate. Accept-and-no-op
4067 // for pg_dump round-trip compatibility.
4068 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
4069 {
4070 self.advance(); // CONSTRAINTS
4071 // v7.39 (round 288) — no longer a no-op: the trailing
4072 // DEFERRED / IMMEDIATE sets the transaction's timing.
4073 // v7.39 (round 308, V29) — and the names are kept.
4074 // They used to be skipped over on the way to the
4075 // DEFERRED keyword, so a named form silently behaved
4076 // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
4077 // every deferrable constraint in the transaction.
4078 let mut names: alloc::vec::Vec<alloc::string::String> =
4079 alloc::vec::Vec::new();
4080 if matches!(self.peek(), Token::All) {
4081 self.advance();
4082 } else {
4083 loop {
4084 let mut n = self.expect_ident_like()?;
4085 // A schema-qualified name (`public.fk_a`)
4086 // identifies the same constraint; PG resolves
4087 // it by the trailing segment.
4088 while matches!(self.peek(), Token::Dot) {
4089 self.advance();
4090 n = self.expect_ident_like()?;
4091 }
4092 names.push(n);
4093 if matches!(self.peek(), Token::Comma) {
4094 self.advance();
4095 } else {
4096 break;
4097 }
4098 }
4099 }
4100 let deferred = match self.peek() {
4101 Token::Ident(s) | Token::QuotedIdent(s)
4102 if s.eq_ignore_ascii_case("deferred") =>
4103 {
4104 true
4105 }
4106 Token::Ident(s) | Token::QuotedIdent(s)
4107 if s.eq_ignore_ascii_case("immediate") =>
4108 {
4109 false
4110 }
4111 other => {
4112 return Err(self.err(alloc::format!(
4113 "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
4114 )));
4115 }
4116 };
4117 self.advance();
4118 return Ok(Statement::SetConstraints { names, deferred });
4119 }
4120 // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
4121 // { DEFAULT | '<role>' | <ident> }` (mailrs
4122 // round-10 A.1). pg_dump preamble emits the
4123 // `DEFAULT` form to reset session authorization.
4124 //
4125 // v7.39 (round 697) — this said "SPG has no role system so
4126 // this is a strict no-op". SPG has had one since round 58;
4127 // the comment outlived it, and with it the reason a name
4128 // that is not a role was accepted here. It still switches
4129 // no authorization — what it does now is refuse a role
4130 // that does not exist, as PG18 does. PG also accepts `RESET SESSION
4131 // AUTHORIZATION` (handled by the RESET parser
4132 // elsewhere). Reference:
4133 // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
4134 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
4135 {
4136 self.advance(); // AUTHORIZATION
4137 match self.peek().clone() {
4138 Token::Default => {
4139 self.advance();
4140 }
4141 Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4142 self.advance();
4143 return Ok(Statement::ValidateOnly {
4144 kind: crate::ast::ValidateOnlyKind::RoleName,
4145 names: alloc::vec![r],
4146 });
4147 }
4148 other => {
4149 return Err(self.err(alloc::format!(
4150 "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4151 )));
4152 }
4153 }
4154 return Ok(Statement::Empty);
4155 }
4156 // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4157 // ISOLATION LEVEL { READ COMMITTED | READ
4158 // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4159 // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4160 // PG-standard surface. v7.37.8 accepts the syntax
4161 // and tracks the selected level on
4162 // `Engine::current_isolation_level()`; the actual
4163 // MVCC / SSI semantics implementation lands in
4164 // the 轴 4 isolation framework (separate train).
4165 // PG itself maps READ UNCOMMITTED to READ COMMITTED
4166 // internally; SPG behaves the same (effectively
4167 // READ COMMITTED at every level today).
4168 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4169 {
4170 self.advance(); // TRANSACTION
4171 let level = self.parse_isolation_level_clauses()?.unwrap_or_default();
4172 return Ok(Statement::SetTransaction { isolation: level });
4173 }
4174 // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4175 // alias — same accept-as-no-op as SET NAMES.
4176 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4177 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4178 {
4179 self.advance(); // CHARACTER
4180 self.advance(); // SET
4181 if matches!(
4182 self.peek(),
4183 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4184 ) {
4185 self.advance();
4186 }
4187 return Ok(Statement::Empty);
4188 }
4189 // v7.39 (GUC) — PG spells the timezone GUC as two
4190 // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4191 // where <value> is a string/ident or the LOCAL /
4192 // DEFAULT keyword (both mean "back to the default").
4193 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4194 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4195 {
4196 self.advance(); // TIME
4197 self.advance(); // ZONE
4198 let value = match self.peek().clone() {
4199 Token::Ident(s)
4200 if s.eq_ignore_ascii_case("local")
4201 || s.eq_ignore_ascii_case("default") =>
4202 {
4203 self.advance();
4204 crate::ast::SetValue::Default
4205 }
4206 Token::Default => {
4207 self.advance();
4208 crate::ast::SetValue::Default
4209 }
4210 _ => self.parse_set_value()?,
4211 };
4212 return Ok(Statement::SetParameter {
4213 name: "timezone".into(),
4214 value,
4215 local: set_local,
4216 });
4217 }
4218 // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4219 // MySQL USER-variable assignment: its own per-session
4220 // namespace, an arbitrary expression on the right, and `:=`
4221 // as a second spelling of `=`. It used to fall into the
4222 // session-PARAMETER list below, whose values are literals and
4223 // whose store nothing reads back under a `@` name — so the
4224 // assignment reported success and vanished.
4225 //
4226 // A `@@`-prefixed LHS is a real engine setting and keeps the
4227 // old path.
4228 if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4229 return self.parse_set_user_vars();
4230 }
4231 // v7.14.0 — multi-assignment form
4232 // `SET a = 1, b = 2, …`. Single-assignment is the
4233 // 1-element case. Each LHS may be a regular ident
4234 // or a SessionVar (`@VAR` / `@@VAR`).
4235 let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4236 loop {
4237 let lhs = match self.peek().clone() {
4238 Token::SessionVar(s) => {
4239 self.advance();
4240 s
4241 }
4242 Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4243 other => {
4244 return Err(self.err(format!(
4245 "expected parameter name after SET, got {other:?}"
4246 )));
4247 }
4248 };
4249 // Accept either `=` or the bare `TO` keyword.
4250 match self.peek() {
4251 Token::Eq => {
4252 self.advance();
4253 }
4254 Token::To => {
4255 self.advance();
4256 }
4257 other => {
4258 return Err(self.err(format!(
4259 "expected `=` or TO after SET {lhs}, got {other:?}"
4260 )));
4261 }
4262 }
4263 let mut value = self.parse_set_value()?;
4264 // v7.39 (GUC) — disambiguate the comma: `, name =` /
4265 // `, name TO` continues a MySQL-style multi-assign,
4266 // anything else is a PG list VALUE
4267 // (`SET search_path = myschema, public`) folded into
4268 // one comma-joined string.
4269 while matches!(self.peek(), Token::Comma) {
4270 let is_assign = matches!(
4271 self.tokens.get(self.pos + 1),
4272 Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4273 ) && matches!(
4274 self.tokens.get(self.pos + 2),
4275 Some(Token::Eq | Token::To)
4276 );
4277 if is_assign {
4278 break;
4279 }
4280 self.advance(); // comma
4281 let next = self.parse_set_value()?;
4282 let joined = alloc::format!(
4283 "{}, {}",
4284 set_value_text(&value),
4285 set_value_text(&next)
4286 );
4287 value = crate::ast::SetValue::String(joined);
4288 }
4289 pairs.push((lhs, value));
4290 if matches!(self.peek(), Token::Comma) {
4291 self.advance();
4292 continue;
4293 }
4294 break;
4295 }
4296 if pairs.len() == 1 {
4297 let (name, value) = pairs.into_iter().next().unwrap();
4298 Ok(Statement::SetParameter {
4299 name,
4300 value,
4301 local: set_local,
4302 })
4303 } else {
4304 Ok(Statement::SetParameterList(pairs))
4305 }
4306 }
4307 // v7.12.1 — `RESET <name>` / `RESET ALL`.
4308 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4309 self.advance();
4310 match self.peek().clone() {
4311 Token::All => {
4312 self.advance();
4313 Ok(Statement::ResetParameter(None))
4314 }
4315 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4316 self.advance();
4317 Ok(Statement::ResetParameter(None))
4318 }
4319 // v7.39 (RLS) — `RESET ROLE` clears the session role.
4320 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4321 self.advance();
4322 Ok(Statement::SetRole(None))
4323 }
4324 // 7.38.1 S5.2 — `RESET SESSION AUTHORIZATION`
4325 // (pg_dump's return from the owner switch).
4326 Token::Ident(s) | Token::QuotedIdent(s)
4327 if s.eq_ignore_ascii_case("session")
4328 && matches!(
4329 self.tokens.get(self.pos + 1),
4330 Some(Token::Ident(a) | Token::QuotedIdent(a))
4331 if a.eq_ignore_ascii_case("authorization")
4332 ) =>
4333 {
4334 self.advance(); // SESSION
4335 self.advance(); // AUTHORIZATION
4336 Ok(Statement::SetRole(None))
4337 }
4338 _ => {
4339 let name = self.parse_set_param_name()?;
4340 Ok(Statement::ResetParameter(Some(name)))
4341 }
4342 }
4343 }
4344 // v7.39 (round 218) — server-side cursors.
4345 Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4346 Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4347 Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4348 Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4349 self.advance();
4350 match self.peek().clone() {
4351 Token::All => {
4352 self.advance();
4353 Ok(Statement::CloseCursor { name: None })
4354 }
4355 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4356 self.advance();
4357 Ok(Statement::CloseCursor { name: None })
4358 }
4359 Token::Ident(n) | Token::QuotedIdent(n) => {
4360 self.advance();
4361 Ok(Statement::CloseCursor { name: Some(n) })
4362 }
4363 other => Err(self.err(format!(
4364 "expected cursor name or ALL after CLOSE, got {other:?}"
4365 ))),
4366 }
4367 }
4368 other => Err(self.err(format!(
4369 "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4370 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4371 ))),
4372 }
4373 }
4374
4375 /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4376 /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4377 /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4378 /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4379 fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4380 self.advance(); // DECLARE
4381 let name = match self.advance() {
4382 Token::Ident(n) | Token::QuotedIdent(n) => n,
4383 other => {
4384 return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4385 }
4386 };
4387 let mut scroll: Option<bool> = None;
4388 loop {
4389 match self.peek() {
4390 Token::Ident(s)
4391 if s.eq_ignore_ascii_case("binary")
4392 || s.eq_ignore_ascii_case("insensitive")
4393 || s.eq_ignore_ascii_case("asensitive") =>
4394 {
4395 self.advance();
4396 }
4397 Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4398 self.advance();
4399 scroll = Some(true);
4400 }
4401 Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4402 {
4403 self.advance(); // NO
4404 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4405 return Err(self.err(format!(
4406 "expected SCROLL after NO in DECLARE, got {:?}",
4407 self.peek()
4408 )));
4409 }
4410 self.advance();
4411 scroll = Some(false);
4412 }
4413 _ => break,
4414 }
4415 }
4416 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4417 return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4418 }
4419 self.advance();
4420 let mut hold = false;
4421 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4422 self.advance();
4423 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4424 return Err(self.err(format!(
4425 "expected HOLD after WITH in DECLARE, got {:?}",
4426 self.peek()
4427 )));
4428 }
4429 self.advance();
4430 hold = true;
4431 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4432 self.advance();
4433 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4434 return Err(self.err(format!(
4435 "expected HOLD after WITHOUT in DECLARE, got {:?}",
4436 self.peek()
4437 )));
4438 }
4439 self.advance();
4440 }
4441 if !matches!(self.peek(), Token::For) {
4442 return Err(self.err(format!(
4443 "expected FOR before the cursor query, got {:?}",
4444 self.peek()
4445 )));
4446 }
4447 self.advance();
4448 let query = self.parse_one_statement()?;
4449 Ok(Statement::DeclareCursor {
4450 name,
4451 scroll,
4452 hold,
4453 query: alloc::boxed::Box::new(query),
4454 })
4455 }
4456
4457 /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4458 /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4459 /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4460 fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4461 use crate::ast::CursorDirection as D;
4462 self.advance(); // FETCH / MOVE
4463 let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4464 let neg = if matches!(this.peek(), Token::Minus) {
4465 this.advance();
4466 true
4467 } else {
4468 false
4469 };
4470 match this.advance() {
4471 Token::Integer(v) => Ok(if neg { -v } else { v }),
4472 other => Err(this.err(format!("expected count, got {other:?}"))),
4473 }
4474 };
4475 let direction = match self.peek().clone() {
4476 Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4477 self.advance();
4478 D::Next
4479 }
4480 Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4481 self.advance();
4482 D::Prior
4483 }
4484 Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4485 self.advance();
4486 D::First
4487 }
4488 Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4489 self.advance();
4490 D::Last
4491 }
4492 Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4493 self.advance();
4494 D::Absolute(signed_count(self)?)
4495 }
4496 Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4497 self.advance();
4498 D::Relative(signed_count(self)?)
4499 }
4500 Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4501 self.advance();
4502 match self.peek().clone() {
4503 Token::All => {
4504 self.advance();
4505 D::All
4506 }
4507 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4508 self.advance();
4509 D::All
4510 }
4511 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4512 _ => D::Next, // bare FORWARD = FORWARD 1
4513 }
4514 }
4515 Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4516 self.advance();
4517 match self.peek().clone() {
4518 Token::All => {
4519 self.advance();
4520 D::BackwardAll
4521 }
4522 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4523 self.advance();
4524 D::BackwardAll
4525 }
4526 Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4527 _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4528 }
4529 }
4530 Token::All => {
4531 self.advance();
4532 D::All
4533 }
4534 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4535 self.advance();
4536 D::All
4537 }
4538 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4539 // Bare `FETCH <name>` — direction defaults to NEXT.
4540 _ => D::Next,
4541 };
4542 // Optional FROM / IN.
4543 if matches!(self.peek(), Token::From)
4544 || matches!(self.peek(), Token::In)
4545 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4546 {
4547 self.advance();
4548 }
4549 let name = match self.advance() {
4550 Token::Ident(n) | Token::QuotedIdent(n) => n,
4551 other => {
4552 return Err(self.err(format!("expected cursor name, got {other:?}")));
4553 }
4554 };
4555 Ok(if is_move {
4556 Statement::MoveCursor { name, direction }
4557 } else {
4558 Statement::FetchCursor { name, direction }
4559 })
4560 }
4561
4562 /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4563 /// [(kind, …)] ON <col>, … FROM <table>`.
4564 fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4565 self.advance(); // STATISTICS
4566 // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4567 let mut if_not_exists = false;
4568 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4569 && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4570 {
4571 self.advance();
4572 self.advance();
4573 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4574 self.advance();
4575 if_not_exists = true;
4576 }
4577 }
4578 let name = self.expect_ident_like()?;
4579 let mut kinds = Vec::new();
4580 if matches!(self.peek(), Token::LParen) {
4581 self.advance();
4582 loop {
4583 let k = self.expect_ident_like()?;
4584 // PG stores the single letters; accept the spelled-out
4585 // names the SQL uses and record what PG records.
4586 kinds.push(match k.to_ascii_lowercase().as_str() {
4587 "ndistinct" => String::from("d"),
4588 "dependencies" => String::from("f"),
4589 "mcv" => String::from("m"),
4590 other => {
4591 return Err(
4592 self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4593 );
4594 }
4595 });
4596 match self.advance() {
4597 Token::Comma => {}
4598 Token::RParen => break,
4599 other => {
4600 return Err(self.err(alloc::format!(
4601 "expected ',' or ')' in statistics kind list, got {other:?}"
4602 )));
4603 }
4604 }
4605 }
4606 }
4607 if !matches!(self.peek(), Token::On) {
4608 return Err(self.err(alloc::format!(
4609 "expected ON in CREATE STATISTICS, got {:?}",
4610 self.peek()
4611 )));
4612 }
4613 self.advance();
4614 let mut columns = Vec::new();
4615 loop {
4616 columns.push(self.expect_ident_like()?);
4617 if matches!(self.peek(), Token::Comma) {
4618 self.advance();
4619 } else {
4620 break;
4621 }
4622 }
4623 if !matches!(self.peek(), Token::From) {
4624 return Err(self.err(alloc::format!(
4625 "expected FROM in CREATE STATISTICS, got {:?}",
4626 self.peek()
4627 )));
4628 }
4629 self.advance();
4630 let table = self.expect_ident_like()?;
4631 Ok(Statement::CreateStatistics {
4632 name,
4633 if_not_exists,
4634 kinds,
4635 columns,
4636 table,
4637 })
4638 }
4639
4640 /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4641 /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4642 /// entered with the `TABLE` keyword still unconsumed. Extracted so
4643 /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4644 /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4645 /// forward call.
4646 fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4647 self.advance(); // TABLE
4648 let if_exists = self.consume_if_exists();
4649 let mut names: Vec<String> = Vec::new();
4650 loop {
4651 names.push(self.expect_ident_like()?);
4652 if matches!(self.peek(), Token::Comma) {
4653 self.advance();
4654 continue;
4655 }
4656 break;
4657 }
4658 if matches!(
4659 self.peek(),
4660 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4661 || s.eq_ignore_ascii_case("restrict")
4662 ) {
4663 self.advance();
4664 }
4665 Ok(Statement::DropTable { names, if_exists })
4666 }
4667
4668 fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4669 self.advance(); // STATISTICS
4670 let mut if_exists = false;
4671 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4672 && matches!(self.tokens.get(self.pos + 1),
4673 Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4674 {
4675 self.advance();
4676 self.advance();
4677 if_exists = true;
4678 }
4679 let name = self.expect_ident_like()?;
4680 Ok(Statement::DropStatistics { name, if_exists })
4681 }
4682
4683 fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4684 debug_assert!(matches!(self.peek(), Token::Create));
4685 self.advance();
4686 match self.peek() {
4687 Token::Table => self.parse_create_table_stmt_after_create(),
4688 Token::Index => self.parse_create_index_stmt_after_create(false),
4689 // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4690 // object now. It used to be consumed by the CREATE-noise
4691 // arm, so a pg_dump that declares extended statistics
4692 // restored silently without them and reflection showed
4693 // nothing.
4694 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4695 self.parse_create_statistics_after_create()
4696 }
4697 // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4698 // The `UNIQUE` modifier turns a partial index into a
4699 // partial-uniqueness invariant (only rows matching the
4700 // WHERE predicate are checked for duplicates). mailrs
4701 // K1 (3 hits: email_templates default, calendar_events
4702 // master, calendar_events instance).
4703 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4704 self.advance();
4705 if !matches!(self.peek(), Token::Index) {
4706 return Err(self.err(alloc::format!(
4707 "expected INDEX after CREATE UNIQUE, got {:?}",
4708 self.peek()
4709 )));
4710 }
4711 self.parse_create_index_stmt_after_create(true)
4712 }
4713 Token::Publication => {
4714 self.advance();
4715 self.parse_create_publication_after_keyword()
4716 }
4717 Token::Subscription => {
4718 self.advance();
4719 self.parse_create_subscription_after_keyword()
4720 }
4721 // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4722 // USER isn't a reserved keyword — we look for the bare
4723 // identifier so the lexer doesn't have to grow a token.
4724 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4725 self.advance();
4726 self.parse_create_user_after_keyword(true)
4727 }
4728 // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4729 // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4730 // the default of the LOGIN attribute.
4731 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4732 self.advance();
4733 self.parse_create_user_after_keyword(false)
4734 }
4735 // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4736 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4737 self.advance();
4738 self.parse_create_policy_after_keyword()
4739 }
4740 // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4741 // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4742 // no-op. mailrs follow-up F3.
4743 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4744 self.advance();
4745 self.parse_create_extension_after_keyword()
4746 }
4747 // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4748 // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4749 // optional; absorb it here and forward to the
4750 // per-kind parsers with the flag. OR is a reserved
4751 // keyword token.
4752 Token::Or => {
4753 self.advance();
4754 let next = self.peek();
4755 let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4756 return Err(self.err(alloc::format!(
4757 "expected REPLACE after CREATE OR, got {next:?}"
4758 )));
4759 };
4760 if !s2.eq_ignore_ascii_case("replace") {
4761 return Err(self.err(alloc::format!(
4762 "expected REPLACE after CREATE OR, got {s2:?}"
4763 )));
4764 }
4765 self.advance();
4766 self.parse_create_function_or_trigger_after_or_replace(true)
4767 }
4768 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4769 self.advance();
4770 self.parse_create_function_after_keyword(false)
4771 }
4772 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4773 self.advance();
4774 self.parse_create_trigger_after_keyword(false)
4775 }
4776 // v7.39 (round 139) — CREATE RULE …
4777 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4778 self.advance();
4779 self.parse_create_rule_after_keyword(false)
4780 }
4781 // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4782 // trigger is a row-level AFTER trigger that additionally carries
4783 // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4784 // path already tolerates and skips those clauses, so consuming the
4785 // CONSTRAINT keyword and reusing it makes the statement parse and the
4786 // trigger fire. (The deferral timing itself is not yet honoured —
4787 // SPG fires it as a plain AFTER trigger, which is correct behaviour
4788 // for every non-deferred use.)
4789 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4790 self.advance();
4791 if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4792 if t.eq_ignore_ascii_case("trigger"))
4793 {
4794 return Err(self.err(alloc::format!(
4795 "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4796 self.peek()
4797 )));
4798 }
4799 self.advance();
4800 self.parse_create_trigger_after_keyword(false)
4801 }
4802 // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4803 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4804 self.advance();
4805 self.parse_create_sequence_after_keyword(false)
4806 }
4807 // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4808 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4809 self.advance();
4810 self.parse_create_view_after_keyword(false, false, false)
4811 }
4812 // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4813 // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4814 // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4815 // appear (in any order) between `CREATE` and `VIEW` in
4816 // every mysqldump-emitted view. Pre-2.6 the parser
4817 // rejected the prefix and the customer's whole view
4818 // backup failed on the first view. The hints are pure
4819 // planner / permission metadata; SPG's view-rewrite
4820 // path is semantically equivalent for all three
4821 // algorithms in v7.17 (TEMPTABLE differs only in
4822 // perf for huge views — out of v7.17 scope), and
4823 // DEFINER / SQL SECURITY are pure single-user
4824 // permissioning that SPG ignores by design.
4825 Token::Ident(s) | Token::QuotedIdent(s)
4826 if s.eq_ignore_ascii_case("algorithm")
4827 || s.eq_ignore_ascii_case("definer")
4828 || s.eq_ignore_ascii_case("sql") =>
4829 {
4830 self.consume_mysql_view_prefix()?;
4831 // After absorbing ALGORITHM / DEFINER / SQL SECURITY
4832 // (in any order, in any combination), the next
4833 // keyword must be VIEW. mysqldump never emits these
4834 // prefixes on non-view statements.
4835 let next = self.peek().clone();
4836 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
4837 if s2.eq_ignore_ascii_case("view"))
4838 {
4839 self.advance();
4840 self.parse_create_view_after_keyword(false, false, false)
4841 } else {
4842 Err(self.err(alloc::format!(
4843 "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
4844 )))
4845 }
4846 }
4847 // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
4848 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
4849 self.advance();
4850 self.parse_create_type_after_keyword()
4851 }
4852 // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
4853 // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
4854 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
4855 self.advance();
4856 self.parse_create_domain_after_keyword()
4857 }
4858 // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
4859 // name [AUTHORIZATION user]. Real catalog registry
4860 // (was silent-no-op'd pre-v7.17).
4861 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
4862 self.advance();
4863 let if_not_exists = self.parse_if_not_exists();
4864 let name = self.expect_ident_like()?;
4865 // Optional `AUTHORIZATION <user>` trailer — accepted,
4866 // ignored (single-user catalog).
4867 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4868 if s.eq_ignore_ascii_case("authorization"))
4869 {
4870 self.advance();
4871 let _ = self.expect_ident_like()?;
4872 }
4873 Ok(Statement::CreateSchema { name, if_not_exists })
4874 }
4875 // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
4876 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
4877 self.advance();
4878 let next = self.peek().clone();
4879 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4880 {
4881 self.advance();
4882 self.parse_create_materialized_view_after_keyword()
4883 } else {
4884 Err(self.err(alloc::format!(
4885 "expected VIEW after CREATE MATERIALIZED, got {next:?}"
4886 )))
4887 }
4888 }
4889 // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
4890 // no-op below), an UNLOGGED table is a real, fully-usable table in
4891 // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
4892 // durability optimisation is a follow-up), so a dump / app that
4893 // declares UNLOGGED tables works instead of failing to parse.
4894 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
4895 self.advance(); // UNLOGGED
4896 if matches!(self.peek(), Token::Table) {
4897 self.parse_create_table_stmt_after_create()
4898 } else {
4899 Err(self.err(format!(
4900 "expected TABLE after CREATE UNLOGGED, got {:?}",
4901 self.peek()
4902 )))
4903 }
4904 }
4905 Token::Ident(s) | Token::QuotedIdent(s)
4906 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
4907 {
4908 self.advance();
4909 // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
4910 let next = self.peek().clone();
4911 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
4912 {
4913 self.advance();
4914 self.parse_create_sequence_after_keyword(true)
4915 } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4916 {
4917 self.advance();
4918 self.parse_create_view_after_keyword(false, false, true)
4919 } else {
4920 // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
4921 // consumed and answered OK while creating nothing, so
4922 // every statement that touched the table afterwards failed
4923 // with "table not found" — the DDL itself lied. It is a
4924 // real CREATE TABLE now, marked temporary so the executor
4925 // puts it in the session's own namespace. An optional
4926 // TABLE keyword may or may not be present (`CREATE TEMP t`
4927 // is not legal, but the keyword is consumed by the
4928 // CREATE TABLE parser itself).
4929 let stmt = self.parse_create_table_stmt_after_create()?;
4930 match stmt {
4931 Statement::CreateTable(mut c) => {
4932 c.temporary = true;
4933 Ok(Statement::CreateTable(c))
4934 }
4935 // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
4936 // CTAS node, which needs the same session namespace.
4937 Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
4938 m.temporary = true;
4939 Ok(Statement::CreateMaterializedView(m))
4940 }
4941 other => Ok(other),
4942 }
4943 }
4944 }
4945 // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
4946 // BEGIN <body> END`. The body may reference `@var`
4947 // session variables, SET statements, internal `;`
4948 // terminators, etc. SPG has no procedure runtime, so
4949 // consume the whole `CREATE PROCEDURE … END` block as
4950 // a no-op so mysqldump scripts that include stored
4951 // routines load through. The matching-END consumer
4952 // tracks BEGIN/END nesting depth to handle nested
4953 // BEGIN blocks correctly.
4954 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
4955 self.consume_mysql_routine_body();
4956 Ok(Statement::Empty)
4957 }
4958 // v7.14.0 — pg_dump / mysqldump emit
4959 // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
4960 // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
4961 // SPG is single-schema / single-database; these have
4962 // no behavioural effect, so consume + return Empty.
4963 // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
4964 // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
4965 // moved up to real parser branches. DATABASE / ROLE /
4966 // POLICY / OPERATOR stay no-op forever
4967 // (single-database, hardcoded roles).
4968 Token::Ident(s) | Token::QuotedIdent(s)
4969 if matches!(
4970 s.to_ascii_lowercase().as_str(),
4971 "database"
4972 | "role"
4973 | "operator"
4974 | "cast"
4975 | "aggregate"
4976 | "language"
4977 | "collation"
4978 | "conversion"
4979 // v7.17.0 Phase 8 (audit N6) — rarely-
4980 // emitted pg_dump shapes that should
4981 // load through without a parser error.
4982 // SPG has no planner statistics catalog,
4983 // no event-trigger hooks, no foreign-
4984 // data-wrapper infrastructure; consume
4985 // + return Empty.
4986 | "statistics"
4987 | "event"
4988 // v7.37.17 (17.6 siblings) — additional CREATE
4989 // targets pg_dump / operator install scripts
4990 // may emit that SPG has no matching machinery
4991 // for. Consume + Empty-return.
4992 | "text"
4993 | "tablespace"
4994 | "access"
4995 | "large"
4996 ) =>
4997 {
4998 // DATABASE is the one member of this list PG refuses
4999 // inside a transaction block; the rest (ROLE, CAST,
5000 // TABLESPACE, …) it runs there quite happily, so only
5001 // this one is named. Still a no-op otherwise — SPG is
5002 // single-database.
5003 let is_database = s.eq_ignore_ascii_case("database");
5004 // The name is the first token after DATABASE, past an
5005 // `IF NOT EXISTS`.
5006 let name = if is_database {
5007 self.scan_database_name()
5008 } else {
5009 None
5010 };
5011 let collation = if is_database {
5012 self.scan_database_collation_until_boundary()
5013 } else {
5014 self.consume_until_statement_boundary();
5015 None
5016 };
5017 if is_database {
5018 return Ok(Statement::NoOpPreventedInTransaction {
5019 what: String::from("CREATE DATABASE"),
5020 collation,
5021 name,
5022 });
5023 }
5024 Ok(Statement::Empty)
5025 }
5026 // v7.39 (round 706) — the foreign-data family leaves the silent
5027 // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
5028 // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
5029 // FDW machinery), but the ENGINE now warns, so a restore log
5030 // says what will not function instead of reporting success.
5031 Token::Ident(s) | Token::QuotedIdent(s)
5032 if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
5033 {
5034 self.consume_until_statement_boundary();
5035 Ok(Statement::ValidateOnly {
5036 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
5037 names: Vec::new(),
5038 })
5039 }
5040 other => Err(self.err(format!(
5041 "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
5042 ))),
5043 }
5044 }
5045
5046 /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
5047 /// keyword decides whether we parse a function or trigger
5048 /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
5049 /// PROCEDURE) — those land in later releases.
5050 fn parse_create_function_or_trigger_after_or_replace(
5051 &mut self,
5052 or_replace: bool,
5053 ) -> Result<Statement, ParseError> {
5054 let tok = self.peek();
5055 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5056 return Err(self.err(alloc::format!(
5057 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
5058 )));
5059 };
5060 if s.eq_ignore_ascii_case("function") {
5061 self.advance();
5062 self.parse_create_function_after_keyword(or_replace)
5063 } else if s.eq_ignore_ascii_case("trigger") {
5064 self.advance();
5065 self.parse_create_trigger_after_keyword(or_replace)
5066 } else if s.eq_ignore_ascii_case("rule") {
5067 // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
5068 self.advance();
5069 self.parse_create_rule_after_keyword(or_replace)
5070 } else if s.eq_ignore_ascii_case("view") {
5071 // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
5072 self.advance();
5073 self.parse_create_view_after_keyword(or_replace, false, false)
5074 } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
5075 // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
5076 self.advance();
5077 let nxt = self.peek().clone();
5078 if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
5079 {
5080 self.advance();
5081 self.parse_create_view_after_keyword(or_replace, false, true)
5082 } else {
5083 Err(self.err(alloc::format!(
5084 "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
5085 )))
5086 }
5087 } else {
5088 Err(self.err(alloc::format!(
5089 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
5090 )))
5091 }
5092 }
5093
5094 /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
5095 /// SPG doesn't have a registry; pgvector / similar are
5096 /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
5097 /// the syntax lets dual-target schemas keep the line.
5098 fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
5099 // Optional `IF NOT EXISTS`.
5100 self.consume_if_not_exists();
5101 let name = self.expect_ident_like()?;
5102 // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
5103 // CASCADE / FROM '<v>' clauses; we don't model them.
5104 loop {
5105 match self.peek() {
5106 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
5107 self.advance();
5108 continue;
5109 }
5110 Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
5111 self.advance();
5112 let _ = self.expect_ident_like()?;
5113 continue;
5114 }
5115 Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
5116 self.advance();
5117 // String or ident literal.
5118 let _ = self.advance();
5119 continue;
5120 }
5121 Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
5122 self.advance();
5123 let _ = self.advance();
5124 continue;
5125 }
5126 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
5127 self.advance();
5128 continue;
5129 }
5130 _ => break,
5131 }
5132 }
5133 // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
5134 // nosuch` reported success and `pg_extension` then did not list it,
5135 // which is the accept-and-do-nothing shape F31 exists to find.
5136 Ok(Statement::ValidateOnly {
5137 kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
5138 names: alloc::vec![name],
5139 })
5140 }
5141
5142 /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
5143 /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
5144 /// already been consumed by the caller. Grammar accepted:
5145 ///
5146 /// name `(` arg-list `)`
5147 /// `RETURNS` return-type
5148 /// [ `LANGUAGE` ident ]
5149 /// `AS` $$ body $$
5150 /// [ `LANGUAGE` ident ]
5151 ///
5152 /// Either `LANGUAGE` position is allowed; PG accepts both.
5153 fn parse_create_function_after_keyword(
5154 &mut self,
5155 or_replace: bool,
5156 ) -> Result<Statement, ParseError> {
5157 let name = self.expect_ident_like()?;
5158 // Argument list. v7.12.4 commonly sees the empty `()`
5159 // (trigger functions); typed args parse and round-trip
5160 // but the executor only invokes nullary functions.
5161 if !matches!(self.peek(), Token::LParen) {
5162 return Err(self.err(alloc::format!(
5163 "expected '(' after function name {name:?}, got {:?}",
5164 self.peek()
5165 )));
5166 }
5167 self.advance();
5168 let args = self.parse_function_arg_list()?;
5169 // RETURNS clause.
5170 let tok = self.peek();
5171 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5172 return Err(self.err(alloc::format!(
5173 "expected RETURNS after function arg list, got {tok:?}"
5174 )));
5175 };
5176 if !s.eq_ignore_ascii_case("returns") {
5177 return Err(self.err(alloc::format!(
5178 "expected RETURNS after function arg list, got {s:?}"
5179 )));
5180 }
5181 self.advance();
5182 let returns = self.parse_function_return()?;
5183 // Optional LANGUAGE clause (PG also accepts after AS — we'll
5184 // re-check after the body too).
5185 let mut language: Option<String> = self.parse_optional_language()?;
5186 // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5187 // either side of the body and in any order, interleaved with
5188 // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5189 // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5190 // PG's own pg_dump output did not restore.
5191 let mut attrs = FunctionAttrs::default();
5192 loop {
5193 let before = self.pos;
5194 self.parse_function_attrs_into(&mut attrs)?;
5195 if language.is_none() {
5196 language = self.parse_optional_language()?;
5197 }
5198 if self.pos == before {
5199 break;
5200 }
5201 }
5202 // `AS` followed by a $$-quoted body (lexer already
5203 // collapses both `$$…$$` and `$tag$…$tag$` to a single
5204 // Token::String). AS is a reserved keyword (Token::As).
5205 if !matches!(self.peek(), Token::As) {
5206 return Err(self.err(alloc::format!(
5207 "expected AS before function body, got {:?}",
5208 self.peek()
5209 )));
5210 }
5211 self.advance();
5212 let body_text = match self.peek() {
5213 Token::String(s) => {
5214 let body = s.clone();
5215 self.advance();
5216 body
5217 }
5218 other => {
5219 return Err(self.err(alloc::format!(
5220 "expected $$-quoted function body after AS, got {other:?}"
5221 )));
5222 }
5223 };
5224 // Trailing clauses — PG's other accepted position for both the
5225 // LANGUAGE and the attributes.
5226 loop {
5227 let before = self.pos;
5228 self.parse_function_attrs_into(&mut attrs)?;
5229 if language.is_none() {
5230 language = self.parse_optional_language()?;
5231 }
5232 if self.pos == before {
5233 break;
5234 }
5235 }
5236 let language = language.unwrap_or_else(|| String::from("sql"));
5237 // PL/pgSQL bodies get structure-parsed. Other languages
5238 // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5239 // recognise) round-trip as Raw text — the executor errors
5240 // when invoked with a clear unsupported message.
5241 let body = if language.eq_ignore_ascii_case("plpgsql") {
5242 match parse_plpgsql_body(&body_text) {
5243 Ok(block) => FunctionBody::PlPgSql(block),
5244 // Best-effort: if the body parser doesn't yet
5245 // support a construct used inside, fall back to
5246 // raw — keeps `CREATE FUNCTION` itself working
5247 // (catalogue accepts), executor errors on
5248 // invocation only.
5249 Err(_) => FunctionBody::Raw(body_text),
5250 }
5251 } else {
5252 FunctionBody::Raw(body_text)
5253 };
5254 Ok(Statement::CreateFunction(CreateFunctionStatement {
5255 name,
5256 or_replace,
5257 args,
5258 returns,
5259 language,
5260 body,
5261 attrs,
5262 }))
5263 }
5264
5265 /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5266 /// attribute clauses into `attrs`, stopping at the first token that
5267 /// is not one. Measured against PG 18.4, which accepts them in any
5268 /// order and on either side of the body.
5269 fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5270 loop {
5271 let word = match self.peek() {
5272 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5273 // NOT LEAKPROOF — NOT is a reserved keyword token.
5274 Token::Not
5275 if matches!(
5276 self.tokens.get(self.pos + 1),
5277 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5278 ) =>
5279 {
5280 self.advance();
5281 self.advance();
5282 attrs.leakproof = false;
5283 continue;
5284 }
5285 _ => return Ok(()),
5286 };
5287 match word.as_str() {
5288 "immutable" => {
5289 self.advance();
5290 attrs.volatility = FunctionVolatility::Immutable;
5291 }
5292 "stable" => {
5293 self.advance();
5294 attrs.volatility = FunctionVolatility::Stable;
5295 }
5296 "volatile" => {
5297 self.advance();
5298 attrs.volatility = FunctionVolatility::Volatile;
5299 }
5300 "strict" => {
5301 self.advance();
5302 attrs.strict = true;
5303 }
5304 "leakproof" => {
5305 self.advance();
5306 attrs.leakproof = true;
5307 }
5308 // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5309 // spelled-out forms of STRICT and its opposite.
5310 "returns" | "called" => {
5311 let strict = word == "returns";
5312 let mut probe = self.pos + 1;
5313 if strict {
5314 // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5315 // is not ours.
5316 match self.tokens.get(probe) {
5317 Some(Token::Null) => probe += 1,
5318 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5319 _ => return Ok(()),
5320 }
5321 }
5322 let ok = matches!(self.tokens.get(probe), Some(Token::On))
5323 || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5324 if !ok {
5325 return Ok(());
5326 }
5327 probe += 1;
5328 match self.tokens.get(probe) {
5329 Some(Token::Null) => probe += 1,
5330 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5331 _ => return Ok(()),
5332 }
5333 match self.tokens.get(probe) {
5334 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5335 _ => return Ok(()),
5336 }
5337 self.pos = probe;
5338 attrs.strict = strict;
5339 }
5340 "security" | "external" => {
5341 // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5342 let mut probe = self.pos + 1;
5343 if word == "external" {
5344 match self.tokens.get(probe) {
5345 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5346 probe += 1;
5347 }
5348 _ => return Ok(()),
5349 }
5350 }
5351 let definer = match self.tokens.get(probe) {
5352 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5353 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5354 _ => return Ok(()),
5355 };
5356 self.pos = probe + 1;
5357 attrs.security_definer = definer;
5358 }
5359 "parallel" => {
5360 let level = match self.tokens.get(self.pos + 1) {
5361 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5362 FunctionParallel::Safe
5363 }
5364 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5365 FunctionParallel::Restricted
5366 }
5367 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5368 FunctionParallel::Unsafe
5369 }
5370 _ => return Ok(()),
5371 };
5372 self.pos += 2;
5373 attrs.parallel = level;
5374 }
5375 "cost" | "rows" => {
5376 let Some(n) = self.peek_number_at(self.pos + 1) else {
5377 return Ok(());
5378 };
5379 self.pos += 2;
5380 if word == "cost" {
5381 attrs.cost = Some(n);
5382 } else {
5383 attrs.rows = Some(n);
5384 }
5385 }
5386 _ => return Ok(()),
5387 }
5388 }
5389 }
5390
5391 /// The numeric literal at `idx`, if there is one.
5392 fn peek_number_at(&self, idx: usize) -> Option<f64> {
5393 match self.tokens.get(idx)? {
5394 Token::Integer(n) => Some(*n as f64),
5395 Token::Float(f) => Some(*f),
5396 Token::Numeric(t) => t.parse::<f64>().ok(),
5397 _ => None,
5398 }
5399 }
5400
5401 /// Closing `)`-terminated argument list. v7.12.4 commonly
5402 /// sees the empty `()`; typed args round-trip but the
5403 /// executor (yet) doesn't invoke them.
5404 /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5405 /// it away, which is what PG does with one on a function parameter.
5406 fn skip_type_modifier(&mut self) {
5407 if !matches!(self.peek(), Token::LParen) {
5408 return;
5409 }
5410 // Only a numeric modifier — anything else is not one, and eating
5411 // it would swallow real grammar.
5412 let mut i = self.pos + 1;
5413 let mut seen_number = false;
5414 loop {
5415 match self.tokens.get(i) {
5416 Some(Token::Integer(_)) => seen_number = true,
5417 Some(Token::Comma) => {}
5418 Some(Token::RParen) => break,
5419 _ => return,
5420 }
5421 i += 1;
5422 }
5423 if !seen_number {
5424 return;
5425 }
5426 while self.pos <= i {
5427 self.advance();
5428 }
5429 }
5430
5431 fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5432 let mut args: Vec<FunctionArg> = Vec::new();
5433 if matches!(self.peek(), Token::RParen) {
5434 self.advance();
5435 return Ok(args);
5436 }
5437 loop {
5438 // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5439 // a reserved token; OUT / INOUT are bare idents.
5440 let mode = if matches!(self.peek(), Token::In) {
5441 self.advance();
5442 FunctionArgMode::In
5443 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5444 {
5445 self.advance();
5446 FunctionArgMode::Out
5447 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5448 {
5449 self.advance();
5450 FunctionArgMode::InOut
5451 } else {
5452 FunctionArgMode::In
5453 };
5454 // Optional name. The next token is either a name
5455 // (followed by a type ident) or the type itself.
5456 // Disambiguate by peeking ahead: if the token after
5457 // the next ident is also an ident, we treat the
5458 // first as the name.
5459 // v7.39 (round 315, V19) — take EVERY ident-like word up to
5460 // the comma or paren, then decide. Reading at most two of
5461 // them could not spell `x double precision` at all, and
5462 // silently mis-read the bare `double precision` as a
5463 // parameter named "double" — which is what made the same
5464 // signature key two different ways.
5465 let (name, ty_token) = {
5466 let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5467 while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5468 words.push(self.expect_ident_like()?);
5469 }
5470 // v7.39 (round 344) — a length / precision modifier on the
5471 // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5472 // accepts it and DROPS it — `pg_get_function_arguments`
5473 // reports plain `character varying` / `numeric`, measured on
5474 // 18.4 — but SPG raised `syntax error at or near "("`,
5475 // because the modifier's parens were never consumed.
5476 self.skip_type_modifier();
5477 // r1049 — `f(v bigint[])`. The array suffix parsed in
5478 // the column position, the cast position and (r1038)
5479 // the RETURNS position, but not here: the fifth
5480 // member of the same family, reported by sentori as
5481 // presumably the same code. It is now.
5482 let array_suffix = self.consume_array_suffix();
5483 let whole = words.join(" ");
5484 let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5485 {
5486 (Some(words[0].clone()), words[1..].join(" "))
5487 } else {
5488 (None, whole)
5489 };
5490 ty_token.push_str(&array_suffix);
5491 (name, ty_token)
5492 };
5493 // Type — try to map to ColumnTypeName, else Raw.
5494 let ty = match map_type_ident_to_column_type_name(&ty_token) {
5495 Some(t) => FunctionArgType::Typed(t),
5496 None => FunctionArgType::Raw(ty_token),
5497 };
5498 args.push(FunctionArg { mode, name, ty });
5499 match self.peek() {
5500 Token::Comma => {
5501 self.advance();
5502 continue;
5503 }
5504 Token::RParen => {
5505 self.advance();
5506 return Ok(args);
5507 }
5508 other => {
5509 return Err(self.err(alloc::format!(
5510 "expected , or ) in function arg list, got {other:?}"
5511 )));
5512 }
5513 }
5514 }
5515 }
5516
5517 fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5518 // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5519 // function whose row shape is named inline.
5520 if matches!(self.peek(), Token::Table)
5521 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5522 {
5523 self.advance(); // TABLE
5524 self.advance(); // (
5525 let mut cols: Vec<String> = Vec::new();
5526 loop {
5527 let cname = self.expect_ident_like()?;
5528 let mut ty: Vec<String> = Vec::new();
5529 loop {
5530 match self.peek() {
5531 Token::Comma | Token::RParen | Token::Eof => break,
5532 _ => {}
5533 }
5534 match self.advance() {
5535 Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5536 other => {
5537 if let Some(w) = unreserved_keyword_text(&other) {
5538 ty.push(w);
5539 }
5540 }
5541 }
5542 }
5543 cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5544 if matches!(self.peek(), Token::Comma) {
5545 self.advance();
5546 } else {
5547 break;
5548 }
5549 }
5550 if matches!(self.peek(), Token::RParen) {
5551 self.advance();
5552 }
5553 return Ok(FunctionReturn::Other(alloc::format!(
5554 "TABLE({})",
5555 cols.join(", ")
5556 )));
5557 }
5558 let ident = self.expect_ident_like()?;
5559 // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5560 if ident.eq_ignore_ascii_case("setof") {
5561 let inner = self.expect_ident_like()?;
5562 let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5563 return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5564 }
5565 if ident.eq_ignore_ascii_case("trigger") {
5566 return Ok(FunctionReturn::Trigger);
5567 }
5568 if ident.eq_ignore_ascii_case("void") {
5569 return Ok(FunctionReturn::Void);
5570 }
5571 // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5572 // RETURN position did not, so the `[` was a syntax error and the
5573 // whole migration stopped. sentori worked around it by returning
5574 // zero-padded text.
5575 let suffix = self.consume_array_suffix();
5576 if !suffix.is_empty() {
5577 return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5578 }
5579 match map_type_ident_to_column_type_name(&ident) {
5580 Some(t) => Ok(FunctionReturn::Type(t)),
5581 None => Ok(FunctionReturn::Other(ident)),
5582 }
5583 }
5584
5585 /// Consume any `[]` / `[N]` array markers after a type name and give
5586 /// back their text. Empty when there are none.
5587 fn consume_array_suffix(&mut self) -> String {
5588 let mut out = String::new();
5589 while matches!(self.peek(), Token::LBracket) {
5590 self.advance();
5591 // `[N]` is accepted and, as in PG, the length is not enforced.
5592 if let Token::Integer(n) = self.peek().clone() {
5593 self.advance();
5594 out.push_str(&alloc::format!("[{n}]"));
5595 } else {
5596 out.push_str("[]");
5597 }
5598 if matches!(self.peek(), Token::RBracket) {
5599 self.advance();
5600 }
5601 }
5602 out
5603 }
5604
5605 fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5606 match self.peek() {
5607 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5608 self.advance();
5609 let lang = self.expect_ident_like()?;
5610 Ok(Some(lang.to_ascii_lowercase()))
5611 }
5612 _ => Ok(None),
5613 }
5614 }
5615
5616 /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5617 /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5618 /// (expr)]*`. The `DOMAIN` keyword has already been
5619 /// consumed. PG allows the trailing constraints in any
5620 /// order; we approximate with a small loop.
5621 fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5622 let name = self.expect_ident_like()?;
5623 // Optional `AS`.
5624 if matches!(self.peek(), Token::As) {
5625 self.advance();
5626 }
5627 // v7.39 (round 259) — keep the raw type NAME when the base is not
5628 // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5629 // parent domain.
5630 let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _) =
5631 self.parse_type_with_implied_flags()?;
5632 let mut default: Option<Expr> = None;
5633 let mut not_null = false;
5634 let mut checks: Vec<Expr> = Vec::new();
5635 loop {
5636 match self.peek() {
5637 Token::Default => {
5638 if default.is_some() {
5639 return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5640 }
5641 self.advance();
5642 default = Some(self.parse_expr(0)?);
5643 }
5644 Token::Not => {
5645 self.advance();
5646 if !matches!(self.peek(), Token::Null) {
5647 return Err(self.err(alloc::format!(
5648 "expected NULL after NOT in DOMAIN, got {:?}",
5649 self.peek()
5650 )));
5651 }
5652 self.advance();
5653 not_null = true;
5654 }
5655 Token::Null => {
5656 self.advance();
5657 // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5658 // is the default-nullable marker (PG accepts it),
5659 // but AFTER a NOT NULL it is a conflict PG refuses
5660 // (`conflicting NULL/NOT NULL constraints`,
5661 // PG18-measured); the old arm no-opped both ways.
5662 if not_null {
5663 return Err(self.err(alloc::string::String::from(
5664 "conflicting NULL/NOT NULL constraints",
5665 )));
5666 }
5667 }
5668 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5669 self.advance();
5670 if !matches!(self.peek(), Token::LParen) {
5671 return Err(self.err(alloc::format!(
5672 "expected '(' after CHECK in DOMAIN, got {:?}",
5673 self.peek()
5674 )));
5675 }
5676 self.advance();
5677 let expr = self.parse_expr(0)?;
5678 if !matches!(self.peek(), Token::RParen) {
5679 return Err(self.err(alloc::format!(
5680 "expected ')' after CHECK expr, got {:?}",
5681 self.peek()
5682 )));
5683 }
5684 self.advance();
5685 checks.push(expr);
5686 }
5687 // CONSTRAINT <name> CHECK (…) — PG accepts a name
5688 // prefix on the constraint; we drop the name and
5689 // recurse into the constraint parsing.
5690 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5691 self.advance();
5692 let _ = self.expect_ident_like()?;
5693 }
5694 _ => break,
5695 }
5696 }
5697 Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5698 name,
5699 base_type,
5700 base_domain: base_user_ref,
5701 default,
5702 not_null,
5703 checks,
5704 }))
5705 }
5706
5707 /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5708 /// ('a', 'b', …)`. The `TYPE` keyword has already been
5709 /// consumed.
5710 fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5711 let name = self.expect_ident_like()?;
5712 // Required `AS`.
5713 if !matches!(self.peek(), Token::As) {
5714 return Err(self.err(alloc::format!(
5715 "expected AS after CREATE TYPE {name:?}, got {:?}",
5716 self.peek()
5717 )));
5718 }
5719 self.advance();
5720 // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5721 // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5722 // on the next token: `(` = composite, ident `ENUM` = enum.
5723 if matches!(self.peek(), Token::LParen) {
5724 self.advance();
5725 let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5726 let mut field_user_types: Vec<Option<String>> = Vec::new();
5727 // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5728 // is legal PG (an attribute-less composite; measured — the old
5729 // e2e note claimed PG requires at least one attribute).
5730 if matches!(self.peek(), Token::RParen) {
5731 self.advance();
5732 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5733 name,
5734 kind: crate::ast::TypeKind::Composite {
5735 fields,
5736 field_user_types,
5737 },
5738 }));
5739 }
5740 loop {
5741 let field_name = self.expect_ident_like()?;
5742 // v7.39 (round 264) — keep the raw type name when it is not
5743 // a builtin: that is how a NESTED composite field records
5744 // which composite it holds.
5745 let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _) =
5746 self.parse_type_with_implied_flags()?;
5747 fields.push((field_name, field_type));
5748 field_user_types.push(field_user_ref);
5749 if matches!(self.peek(), Token::Comma) {
5750 self.advance();
5751 continue;
5752 }
5753 if matches!(self.peek(), Token::RParen) {
5754 self.advance();
5755 break;
5756 }
5757 return Err(self.err(alloc::format!(
5758 "expected , or ) in composite field list, got {:?}",
5759 self.peek()
5760 )));
5761 }
5762 if fields.is_empty() {
5763 return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5764 }
5765 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5766 name,
5767 kind: crate::ast::TypeKind::Composite {
5768 fields,
5769 field_user_types,
5770 },
5771 }));
5772 }
5773 // Required `ENUM` ident.
5774 let kind_ident = match self.peek().clone() {
5775 Token::Ident(s) | Token::QuotedIdent(s) => s,
5776 other => {
5777 return Err(self.err(alloc::format!(
5778 "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5779 )));
5780 }
5781 };
5782 if !kind_ident.eq_ignore_ascii_case("enum") {
5783 return Err(self.err(alloc::format!(
5784 "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5785 )));
5786 }
5787 self.advance();
5788 if !matches!(self.peek(), Token::LParen) {
5789 return Err(self.err(alloc::format!(
5790 "expected '(' after ENUM, got {:?}",
5791 self.peek()
5792 )));
5793 }
5794 self.advance();
5795 let mut labels: Vec<String> = Vec::new();
5796 loop {
5797 match self.peek().clone() {
5798 Token::String(s) => {
5799 self.advance();
5800 labels.push(s);
5801 }
5802 other => {
5803 return Err(
5804 self.err(alloc::format!("expected enum label string, got {other:?}"))
5805 );
5806 }
5807 }
5808 if matches!(self.peek(), Token::Comma) {
5809 self.advance();
5810 continue;
5811 }
5812 if matches!(self.peek(), Token::RParen) {
5813 self.advance();
5814 break;
5815 }
5816 return Err(self.err(alloc::format!(
5817 "expected , or ) in ENUM label list, got {:?}",
5818 self.peek()
5819 )));
5820 }
5821 if labels.is_empty() {
5822 return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5823 }
5824 Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5825 name,
5826 kind: crate::ast::TypeKind::Enum { labels },
5827 }))
5828 }
5829
5830 /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
5831 /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
5832 /// The `CREATE MATERIALIZED VIEW` keywords have already been
5833 /// consumed.
5834 fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
5835 let if_not_exists = self.parse_if_not_exists();
5836 let name = self.expect_ident_like()?;
5837 let mut columns: Vec<String> = Vec::new();
5838 if matches!(self.peek(), Token::LParen) {
5839 self.advance();
5840 loop {
5841 let c = self.expect_ident_like()?;
5842 columns.push(c);
5843 if matches!(self.peek(), Token::Comma) {
5844 self.advance();
5845 continue;
5846 }
5847 if matches!(self.peek(), Token::RParen) {
5848 self.advance();
5849 break;
5850 }
5851 return Err(self.err(alloc::format!(
5852 "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
5853 self.peek()
5854 )));
5855 }
5856 }
5857 if !matches!(self.peek(), Token::As) {
5858 return Err(self.err(alloc::format!(
5859 "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
5860 self.peek()
5861 )));
5862 }
5863 self.advance();
5864 // v7.39 (round 151) — a WITH-headed body is legal (read-only
5865 // CTEs only; the engine rejects data-modifying ones with PG's
5866 // message). A trailing `WITH [NO] DATA` can't START the body,
5867 // so WITH here heads the query.
5868 let body = if self.peek_is_with_kw() {
5869 self.advance();
5870 self.parse_nested_with_select()?
5871 } else {
5872 let body_stmt = self.parse_select_stmt()?;
5873 let Statement::Select(body) = body_stmt else {
5874 return Err(self.err(alloc::format!(
5875 "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
5876 )));
5877 };
5878 body
5879 };
5880 // Optional trailing `WITH [NO] DATA`.
5881 let with_data = self.parse_optional_with_data(true)?;
5882 Ok(Statement::CreateMaterializedView(
5883 crate::ast::CreateMaterializedViewStatement {
5884 temporary: false,
5885 name,
5886 if_not_exists,
5887 columns,
5888 body,
5889 with_data,
5890 as_plain_table: false,
5891 },
5892 ))
5893 }
5894
5895 /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
5896 /// `default_when_absent` is what to return if the tail is
5897 /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
5898 /// WITH DATA).
5899 fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
5900 let save = self.pos;
5901 // `WITH` is an Ident (not reserved in the lexer).
5902 let is_with = match self.peek() {
5903 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
5904 _ => false,
5905 };
5906 if !is_with {
5907 return Ok(default_when_absent);
5908 }
5909 self.advance();
5910 // Optional `NO`.
5911 let mut with_data = true;
5912 let is_no = match self.peek() {
5913 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
5914 _ => false,
5915 };
5916 if is_no {
5917 self.advance();
5918 with_data = false;
5919 }
5920 // Required `DATA` ident.
5921 let is_data = match self.peek() {
5922 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
5923 _ => false,
5924 };
5925 if is_data {
5926 self.advance();
5927 Ok(with_data)
5928 } else {
5929 // Caller's WITH wasn't WITH-DATA — rewind so the outer
5930 // parser can interpret it.
5931 self.pos = save;
5932 Ok(default_when_absent)
5933 }
5934 }
5935
5936 /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
5937 /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
5938 /// All keyword prefixes have already been consumed; the flags
5939 /// say which were present.
5940 fn parse_create_view_after_keyword(
5941 &mut self,
5942 or_replace: bool,
5943 _materialized_unused: bool,
5944 temporary: bool,
5945 ) -> Result<Statement, ParseError> {
5946 let if_not_exists = self.parse_if_not_exists();
5947 let name = self.expect_ident_like()?;
5948 // Optional `(col, col, …)` rename list.
5949 let mut columns: Vec<String> = Vec::new();
5950 if matches!(self.peek(), Token::LParen) {
5951 self.advance();
5952 loop {
5953 let c = self.expect_ident_like()?;
5954 columns.push(c);
5955 if matches!(self.peek(), Token::Comma) {
5956 self.advance();
5957 continue;
5958 }
5959 if matches!(self.peek(), Token::RParen) {
5960 self.advance();
5961 break;
5962 }
5963 return Err(self.err(alloc::format!(
5964 "expected , or ) in VIEW column list, got {:?}",
5965 self.peek()
5966 )));
5967 }
5968 }
5969 // Required `AS`.
5970 if !matches!(self.peek(), Token::As) {
5971 return Err(self.err(alloc::format!(
5972 "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
5973 self.peek()
5974 )));
5975 }
5976 self.advance();
5977 // Body: a regular SELECT statement. v7.39 (round 151) — a
5978 // WITH-headed body is legal too (read-only CTEs only; the
5979 // engine rejects data-modifying ones with PG's message).
5980 // Disambiguation vs `WITH CHECK OPTION`: a body can't START
5981 // with the check-option clause, so WITH here heads the query.
5982 let body = if self.peek_is_with_kw() {
5983 self.advance();
5984 self.parse_nested_with_select()?
5985 } else {
5986 let body_stmt = self.parse_select_stmt()?;
5987 let Statement::Select(body) = body_stmt else {
5988 return Err(self.err(alloc::format!(
5989 "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
5990 )));
5991 };
5992 body
5993 };
5994 // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
5995 // The SELECT parser stops before a trailing WITH, so it lands here.
5996 let check_option = if matches!(self.peek(),
5997 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
5998 {
5999 self.advance(); // WITH
6000 let opt = match self.peek() {
6001 Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
6002 self.advance();
6003 crate::ast::ViewCheckOption::Local
6004 }
6005 Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
6006 self.advance();
6007 crate::ast::ViewCheckOption::Cascaded
6008 }
6009 // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
6010 _ => crate::ast::ViewCheckOption::Cascaded,
6011 };
6012 if !matches!(self.peek(),
6013 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
6014 {
6015 return Err(self.err(alloc::format!(
6016 "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
6017 self.peek()
6018 )));
6019 }
6020 self.advance(); // CHECK
6021 if !matches!(self.peek(),
6022 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
6023 {
6024 return Err(self.err(alloc::format!(
6025 "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
6026 self.peek()
6027 )));
6028 }
6029 self.advance(); // OPTION
6030 Some(opt)
6031 } else {
6032 None
6033 };
6034 Ok(Statement::CreateView(crate::ast::CreateViewStatement {
6035 name,
6036 or_replace,
6037 if_not_exists,
6038 temporary,
6039 columns,
6040 body,
6041 check_option,
6042 }))
6043 }
6044
6045 /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
6046 /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
6047 /// consumed; `temporary` carries whether TEMPORARY was seen.
6048 fn parse_create_sequence_after_keyword(
6049 &mut self,
6050 temporary: bool,
6051 ) -> Result<Statement, ParseError> {
6052 let if_not_exists = self.parse_if_not_exists();
6053 let name = self.expect_ident_like()?;
6054 // Optional `AS data_type`.
6055 let data_type = if matches!(self.peek(), Token::As) {
6056 self.advance();
6057 Some(self.parse_sequence_data_type()?)
6058 } else {
6059 None
6060 };
6061 let options = self.parse_sequence_options(/* allow_restart = */ false)?;
6062 Ok(Statement::CreateSequence(
6063 crate::ast::CreateSequenceStatement {
6064 name,
6065 if_not_exists,
6066 temporary,
6067 data_type,
6068 options,
6069 },
6070 ))
6071 }
6072
6073 /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
6074 /// already been consumed; this is reached after `SEQUENCE`.
6075 /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
6076 fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
6077 use crate::ast::AlterDomainAction as A;
6078 let name = self.expect_ident_like()?;
6079 // DROP / SET / ADD lex as reserved keyword tokens, not idents.
6080 let kw = match self.peek() {
6081 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6082 Token::Drop => alloc::string::String::from("drop"),
6083 Token::Default => alloc::string::String::from("default"),
6084 other => {
6085 return Err(self.err(alloc::format!(
6086 "expected an ALTER DOMAIN action, got {other:?}"
6087 )));
6088 }
6089 };
6090 let action = match kw.as_str() {
6091 "add" => {
6092 self.advance();
6093 let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
6094 {
6095 self.advance();
6096 Some(self.expect_ident_like()?)
6097 } else {
6098 None
6099 };
6100 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
6101 return Err(self.err(alloc::format!(
6102 "ALTER DOMAIN ADD supports CHECK only, got {:?}",
6103 self.peek()
6104 )));
6105 }
6106 self.advance();
6107 if !matches!(self.peek(), Token::LParen) {
6108 return Err(self.err("expected '(' after CHECK".into()));
6109 }
6110 self.advance();
6111 let check = self.parse_expr(0)?;
6112 if !matches!(self.peek(), Token::RParen) {
6113 return Err(self.err("expected ')' after CHECK expression".into()));
6114 }
6115 self.advance();
6116 A::AddConstraint { name: cname, check }
6117 }
6118 "drop" => {
6119 self.advance();
6120 match self.peek() {
6121 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
6122 self.advance();
6123 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
6124 {
6125 self.advance();
6126 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
6127 {
6128 return Err(self.err("expected EXISTS after IF".into()));
6129 }
6130 self.advance();
6131 true
6132 } else {
6133 false
6134 };
6135 let cn = self.expect_ident_like()?;
6136 A::DropConstraint {
6137 name: cn,
6138 if_exists,
6139 }
6140 }
6141 Token::Default => {
6142 self.advance();
6143 A::DropDefault
6144 }
6145 Token::Not => {
6146 self.advance();
6147 if !matches!(self.peek(), Token::Null) {
6148 return Err(self.err("expected NULL after NOT".into()));
6149 }
6150 self.advance();
6151 A::DropNotNull
6152 }
6153 other => {
6154 return Err(self.err(alloc::format!(
6155 "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
6156 )));
6157 }
6158 }
6159 }
6160 "set" => {
6161 self.advance();
6162 match self.peek() {
6163 Token::Default => {
6164 self.advance();
6165 A::SetDefault(self.parse_expr(0)?)
6166 }
6167 Token::Not => {
6168 self.advance();
6169 if !matches!(self.peek(), Token::Null) {
6170 return Err(self.err("expected NULL after NOT".into()));
6171 }
6172 self.advance();
6173 A::SetNotNull
6174 }
6175 other => {
6176 return Err(self.err(alloc::format!(
6177 "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6178 )));
6179 }
6180 }
6181 }
6182 "rename" => {
6183 self.advance();
6184 if !matches!(self.peek(), Token::To) {
6185 return Err(self.err("expected TO after RENAME".into()));
6186 }
6187 self.advance();
6188 A::RenameTo(self.expect_ident_like()?)
6189 }
6190 other => {
6191 return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6192 }
6193 };
6194 Ok(Statement::AlterDomain { name, action })
6195 }
6196
6197 fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6198 let if_exists = self.parse_if_exists();
6199 let name = self.expect_ident_like()?;
6200 // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6201 // the option list (PG allows only one or the other).
6202 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6203 self.advance();
6204 if matches!(self.peek(), Token::To) {
6205 self.advance();
6206 } else {
6207 self.expect_keyword_ident("to")?;
6208 }
6209 let new = self.expect_ident_like()?;
6210 return Ok(Statement::AlterSequence(
6211 crate::ast::AlterSequenceStatement {
6212 name,
6213 if_exists,
6214 options: crate::ast::SequenceOptions::default(),
6215 rename_to: Some(new),
6216 },
6217 ));
6218 }
6219 let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6220 Ok(Statement::AlterSequence(
6221 crate::ast::AlterSequenceStatement {
6222 name,
6223 if_exists,
6224 options,
6225 rename_to: None,
6226 },
6227 ))
6228 }
6229
6230 fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6231 let kw = self.expect_ident_like()?;
6232 match kw.to_ascii_lowercase().as_str() {
6233 "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6234 "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6235 "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6236 other => Err(self.err(alloc::format!(
6237 "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6238 ))),
6239 }
6240 }
6241
6242 fn parse_sequence_options(
6243 &mut self,
6244 allow_restart: bool,
6245 ) -> Result<crate::ast::SequenceOptions, ParseError> {
6246 use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6247 let mut opts = SequenceOptions::default();
6248 #[allow(clippy::while_let_loop)]
6249 loop {
6250 // Match an ident; stop at any non-ident token (sentinel,
6251 // semicolon, end of statement).
6252 let kw_lc = match self.peek() {
6253 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6254 _ => break,
6255 };
6256 match kw_lc.as_str() {
6257 "increment" => {
6258 self.advance();
6259 // Optional BY.
6260 if self.peek_is_by() {
6261 self.advance();
6262 }
6263 opts.increment = Some(self.expect_signed_int()?);
6264 }
6265 "minvalue" => {
6266 self.advance();
6267 opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6268 }
6269 "maxvalue" => {
6270 self.advance();
6271 opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6272 }
6273 "no" => {
6274 self.advance();
6275 let what = self.expect_ident_like()?;
6276 match what.to_ascii_lowercase().as_str() {
6277 "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6278 "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6279 "cycle" => opts.cycle = Some(false),
6280 other => {
6281 return Err(self.err(alloc::format!(
6282 "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6283 )));
6284 }
6285 }
6286 }
6287 "start" => {
6288 self.advance();
6289 // Optional WITH.
6290 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6291 if s.eq_ignore_ascii_case("with"))
6292 {
6293 self.advance();
6294 }
6295 opts.start = Some(self.expect_signed_int()?);
6296 }
6297 "restart" if allow_restart => {
6298 self.advance();
6299 // Optional WITH n; bare RESTART means restart at START.
6300 let mut with_val: Option<i64> = None;
6301 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6302 if s.eq_ignore_ascii_case("with"))
6303 {
6304 self.advance();
6305 with_val = Some(self.expect_signed_int()?);
6306 } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6307 with_val = Some(self.expect_signed_int()?);
6308 }
6309 opts.restart = Some(with_val);
6310 }
6311 "cache" => {
6312 self.advance();
6313 opts.cache = Some(self.expect_signed_int()?);
6314 }
6315 "cycle" => {
6316 self.advance();
6317 opts.cycle = Some(true);
6318 }
6319 "owned" => {
6320 self.advance();
6321 match self.peek() {
6322 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6323 self.advance();
6324 }
6325 other => {
6326 return Err(
6327 self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6328 );
6329 }
6330 }
6331 // OWNED BY {NONE | tab.col}. Read just one ident
6332 // (NOT expect_ident_like which would auto-strip
6333 // a schema prefix and consume the `.col` we need).
6334 let first = match self.advance() {
6335 Token::Ident(s) | Token::QuotedIdent(s) => s,
6336 other => {
6337 return Err(self.err(alloc::format!(
6338 "expected identifier or NONE after OWNED BY, got {other:?}"
6339 )));
6340 }
6341 };
6342 if first.eq_ignore_ascii_case("none") {
6343 opts.owned_by = Some(SequenceOwnedBy::None);
6344 } else if matches!(self.peek(), Token::Dot) {
6345 self.advance();
6346 let second = match self.advance() {
6347 Token::Ident(s) | Token::QuotedIdent(s) => s,
6348 other => {
6349 return Err(self.err(alloc::format!(
6350 "expected column name after OWNED BY {first}., got {other:?}"
6351 )));
6352 }
6353 };
6354 // v7.17 dump-compat fix — pg_dump emits
6355 // OWNED BY clauses as
6356 // `schema.table.column` (three segments).
6357 // If a third `.<ident>` follows, treat the
6358 // first ident as schema (drop it; SPG is
6359 // single-schema) and the middle / last
6360 // pair as table.column. Otherwise it's
6361 // the two-segment form table.column.
6362 if matches!(self.peek(), Token::Dot) {
6363 self.advance();
6364 let third = match self.advance() {
6365 Token::Ident(s) | Token::QuotedIdent(s) => s,
6366 other => {
6367 return Err(self.err(alloc::format!(
6368 "expected column name after OWNED BY {first}.{second}., got {other:?}"
6369 )));
6370 }
6371 };
6372 let _ = first; // schema prefix discarded
6373 opts.owned_by = Some(SequenceOwnedBy::Column {
6374 table: second,
6375 column: third,
6376 });
6377 } else {
6378 opts.owned_by = Some(SequenceOwnedBy::Column {
6379 table: first,
6380 column: second,
6381 });
6382 }
6383 } else {
6384 return Err(self.err(alloc::format!(
6385 "expected table.column or NONE after OWNED BY, got {first:?}"
6386 )));
6387 }
6388 }
6389 _ => break,
6390 }
6391 }
6392 Ok(opts)
6393 }
6394
6395 fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6396 let neg = if matches!(self.peek(), Token::Minus) {
6397 self.advance();
6398 true
6399 } else {
6400 false
6401 };
6402 match self.peek() {
6403 Token::Integer(n) => {
6404 let v = *n;
6405 self.advance();
6406 Ok(if neg { -v } else { v })
6407 }
6408 other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6409 }
6410 }
6411
6412 /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6413 /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6414 /// clause is fully accepted and discarded — SPG always runs
6415 /// constraint checks immediately (single-writer model). The
6416 /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6417 /// in either order (per the SQL spec they're independent),
6418 /// though pg_dump always emits them in the canonical
6419 /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6420 /// Stops at the first token that isn't part of the clause.
6421 fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6422 self.consume_deferrable_clauses_timed().map(|_| ())
6423 }
6424
6425 /// v7.39 (round 288) — the same scan, but reporting what it saw:
6426 /// `(deferrable, initially_deferred)`. The clauses were parsed and
6427 /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6428 /// NOT DEFERRABLE and a circular-FK migration could not load.
6429 fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6430 let mut deferrable = false;
6431 let mut initially_deferred = false;
6432 loop {
6433 // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6434 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6435 self.advance();
6436 deferrable = true;
6437 if self.consume_optional_initially_clause()? {
6438 initially_deferred = true;
6439 }
6440 continue;
6441 }
6442 // `NOT DEFERRABLE` — already worked pre-3.1.
6443 if matches!(self.peek(), Token::Not) {
6444 let look = self.tokens.get(self.pos + 1);
6445 if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6446 self.advance(); // NOT
6447 self.advance(); // DEFERRABLE
6448 deferrable = false;
6449 initially_deferred = false;
6450 let _ = self.consume_optional_initially_clause()?;
6451 continue;
6452 }
6453 break;
6454 }
6455 // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6456 // accepts this without a leading [NOT] DEFERRABLE
6457 // (the timing keyword alone). pg_dump occasionally
6458 // emits it on FK constraints that inherit timing.
6459 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6460 if self.consume_optional_initially_clause()? {
6461 initially_deferred = true;
6462 // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6463 deferrable = true;
6464 }
6465 continue;
6466 }
6467 break;
6468 }
6469 Ok((deferrable, initially_deferred))
6470 }
6471
6472 /// Helper for [`consume_optional_deferrable_clauses`]. When the
6473 /// next token is `INITIALLY`, consume it plus the required
6474 /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6475 /// Returns true when the timing seen was `DEFERRED`.
6476 fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6477 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6478 return Ok(false);
6479 }
6480 self.advance(); // INITIALLY
6481 match self.advance() {
6482 Token::Ident(s)
6483 if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6484 {
6485 Ok(s.eq_ignore_ascii_case("deferred"))
6486 }
6487 other => Err(self.err(alloc::format!(
6488 "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6489 ))),
6490 }
6491 }
6492
6493 /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6494 /// in its entirety so the parser returns Empty without
6495 /// touching the runtime. The CREATE+PROCEDURE keywords are
6496 /// already consumed; this swallows everything from the
6497 /// procedure name through the matching `END`, including
6498 /// nested `BEGIN`/`END` blocks, internal `;` terminators
6499 /// (DELIMITER `//` makes the script splitter forward the
6500 /// whole block as one statement), `@var` session-variable
6501 /// references, and the trailing terminator.
6502 ///
6503 /// Tracks nesting depth so:
6504 /// BEGIN
6505 /// IF cond THEN
6506 /// BEGIN ... END;
6507 /// END IF;
6508 /// END
6509 /// terminates at the outer END.
6510 fn consume_mysql_routine_body(&mut self) {
6511 // Outer skeleton: name, (...), optional clauses, BEGIN
6512 // <body> END [;]. Scan for the first BEGIN — anything
6513 // before it is signature decoration we don't care about.
6514 // Once inside BEGIN, count up on BEGIN, down on END.
6515 let mut depth: i32 = 0;
6516 let mut started = false;
6517 loop {
6518 match self.peek().clone() {
6519 Token::Begin => {
6520 self.advance();
6521 depth += 1;
6522 started = true;
6523 }
6524 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6525 self.advance();
6526 if started {
6527 depth -= 1;
6528 if depth <= 0 {
6529 // Optional trailing ident (`END IF`,
6530 // `END LOOP`, `END WHILE`, `END CASE`,
6531 // `END label_name`) — eat the next
6532 // ident if present so we don't
6533 // mistake `END IF;` for the outer
6534 // close.
6535 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6536 // If the next token is one of the
6537 // PL/SQL block-closer keywords,
6538 // the END belongs to an inner
6539 // block; bump depth back up.
6540 let is_inner_close = matches!(
6541 self.peek(),
6542 Token::Ident(s) | Token::QuotedIdent(s)
6543 if matches!(
6544 s.to_ascii_lowercase().as_str(),
6545 "if" | "loop" | "while" | "case" | "repeat"
6546 )
6547 );
6548 if is_inner_close {
6549 self.advance();
6550 depth += 1;
6551 continue;
6552 }
6553 }
6554 // Eat optional trailing `;`.
6555 if matches!(self.peek(), Token::Semicolon) {
6556 self.advance();
6557 }
6558 return;
6559 }
6560 }
6561 }
6562 Token::Eof => return,
6563 _ => {
6564 self.advance();
6565 }
6566 }
6567 }
6568 }
6569
6570 /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6571 /// that appear between `CREATE` and `VIEW` in mysqldump output:
6572 ///
6573 /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6574 /// * `DEFINER = <user>` (user may be a quoted string, a bare
6575 /// ident, or `ident @ ident-or-quoted-string` host form)
6576 /// * `SQL SECURITY {DEFINER|INVOKER}`
6577 ///
6578 /// Each clause may appear at most once but in any order.
6579 /// The hints are pure planner / permission metadata that
6580 /// SPG's view-rewrite engine handles uniformly; we accept
6581 /// and discard. Returns `Ok(())` once a non-clause token is
6582 /// peeked (the caller then checks for the `VIEW` keyword).
6583 fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6584 loop {
6585 match self.peek().clone() {
6586 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6587 self.advance(); // ALGORITHM
6588 // Optional `=`. MySQL spec requires it but be
6589 // generous.
6590 if matches!(self.peek(), Token::Eq) {
6591 self.advance();
6592 }
6593 // UNDEFINED / MERGE / TEMPTABLE — accept any
6594 // bare ident; unknown values still parse so
6595 // future MySQL versions don't break.
6596 if matches!(
6597 self.peek(),
6598 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6599 ) {
6600 self.advance();
6601 }
6602 }
6603 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6604 self.advance(); // DEFINER
6605 if matches!(self.peek(), Token::Eq) {
6606 self.advance();
6607 }
6608 // User: quoted string, ident, OR ident @ host
6609 // (host may itself be quoted or bare).
6610 match self.peek().clone() {
6611 Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6612 self.advance();
6613 // Optional `@host`.
6614 if matches!(self.peek(), Token::At) {
6615 self.advance();
6616 if matches!(
6617 self.peek(),
6618 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6619 ) {
6620 self.advance();
6621 }
6622 }
6623 }
6624 _ => {}
6625 }
6626 }
6627 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6628 // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6629 // when followed by SECURITY — the dispatcher must
6630 // not consume a bare `SQL` token (it's not a
6631 // legal CREATE prefix on its own).
6632 let save = self.pos;
6633 self.advance(); // SQL
6634 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6635 if s2.eq_ignore_ascii_case("security"))
6636 {
6637 self.advance(); // SECURITY
6638 // DEFINER / INVOKER trailing ident.
6639 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6640 self.advance();
6641 }
6642 } else {
6643 // Not a SQL SECURITY clause — roll back and
6644 // bail; the caller will error out cleanly.
6645 self.pos = save;
6646 return Ok(());
6647 }
6648 }
6649 _ => return Ok(()),
6650 }
6651 }
6652 }
6653
6654 fn parse_if_not_exists(&mut self) -> bool {
6655 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6656 {
6657 let save = self.pos;
6658 self.advance();
6659 if matches!(self.peek(), Token::Not) {
6660 self.advance();
6661 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6662 {
6663 self.advance();
6664 return true;
6665 }
6666 }
6667 self.pos = save;
6668 }
6669 false
6670 }
6671
6672 fn parse_if_exists(&mut self) -> bool {
6673 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6674 {
6675 let save = self.pos;
6676 self.advance();
6677 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6678 {
6679 self.advance();
6680 return true;
6681 }
6682 self.pos = save;
6683 }
6684 false
6685 }
6686
6687 /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6688 /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6689 /// been consumed.
6690 fn parse_create_trigger_after_keyword(
6691 &mut self,
6692 or_replace: bool,
6693 ) -> Result<Statement, ParseError> {
6694 let name = self.expect_ident_like()?;
6695 let timing = {
6696 let ident = self.expect_ident_like()?;
6697 if ident.eq_ignore_ascii_case("before") {
6698 TriggerTiming::Before
6699 } else if ident.eq_ignore_ascii_case("after") {
6700 TriggerTiming::After
6701 } else if ident.eq_ignore_ascii_case("instead") {
6702 let next = self.expect_ident_like()?;
6703 if !next.eq_ignore_ascii_case("of") {
6704 return Err(self.err(alloc::format!(
6705 "expected OF after INSTEAD in trigger timing, got {next:?}"
6706 )));
6707 }
6708 TriggerTiming::InsteadOf
6709 } else {
6710 return Err(self.err(alloc::format!(
6711 "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6712 )));
6713 }
6714 };
6715 // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6716 // OR is a reserved keyword token (Token::Or), not an Ident.
6717 // v7.13.0 — after an UPDATE event we may optionally see
6718 // `OF col, col, …` (mailrs round-5 G7). Columns are
6719 // captured into `update_columns` once across the whole
6720 // events list; multiple `UPDATE OF` clauses are rejected.
6721 let mut events: Vec<TriggerEvent> = Vec::new();
6722 let mut update_columns: Vec<String> = Vec::new();
6723 let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6724 events.push(first_ev);
6725 if !first_cols.is_empty() {
6726 update_columns = first_cols;
6727 }
6728 while matches!(self.peek(), Token::Or) {
6729 self.advance();
6730 let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6731 events.push(ev);
6732 if !cols.is_empty() {
6733 if !update_columns.is_empty() {
6734 return Err(
6735 self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6736 );
6737 }
6738 update_columns = cols;
6739 }
6740 }
6741 // ON <table>
6742 let tok = self.peek();
6743 let Token::On = tok else {
6744 return Err(self.err(alloc::format!(
6745 "expected ON after trigger events, got {tok:?}"
6746 )));
6747 };
6748 self.advance();
6749 let table = self.expect_ident_like()?;
6750 // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6751 // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6752 // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6753 // the trigger as a plain AFTER trigger (correct for every non-deferred
6754 // use; deferral timing is not yet honoured).
6755 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6756 if s.eq_ignore_ascii_case("from"))
6757 {
6758 self.advance();
6759 let _reftable = self.expect_ident_like()?;
6760 }
6761 self.consume_optional_deferrable_clauses()?;
6762 // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6763 // keyword (Token::For); EACH / ROW / STATEMENT are bare
6764 // idents.
6765 if !matches!(self.peek(), Token::For) {
6766 return Err(self.err(alloc::format!(
6767 "expected FOR EACH ROW / STATEMENT, got {:?}",
6768 self.peek()
6769 )));
6770 }
6771 self.advance();
6772 let for_each = {
6773 let e = self.expect_ident_like()?;
6774 if !e.eq_ignore_ascii_case("each") {
6775 return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6776 }
6777 let unit = self.expect_ident_like()?;
6778 if unit.eq_ignore_ascii_case("row") {
6779 TriggerForEach::Row
6780 } else if unit.eq_ignore_ascii_case("statement") {
6781 TriggerForEach::Statement
6782 } else {
6783 return Err(self.err(alloc::format!(
6784 "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6785 )));
6786 }
6787 };
6788 // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6789 let when_condition = if matches!(self.peek(),
6790 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6791 {
6792 self.advance();
6793 Some(self.parse_paren_expr("WHEN")?)
6794 } else {
6795 None
6796 };
6797 // EXECUTE FUNCTION/PROCEDURE name(...)
6798 let exec = self.expect_ident_like()?;
6799 if !exec.eq_ignore_ascii_case("execute") {
6800 return Err(self.err(alloc::format!(
6801 "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6802 )));
6803 }
6804 let fn_or_proc = self.expect_ident_like()?;
6805 if !(fn_or_proc.eq_ignore_ascii_case("function")
6806 || fn_or_proc.eq_ignore_ascii_case("procedure"))
6807 {
6808 return Err(self.err(alloc::format!(
6809 "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6810 )));
6811 }
6812 let function = self.expect_ident_like()?;
6813 // Optional empty arg list `()`.
6814 if matches!(self.peek(), Token::LParen) {
6815 self.advance();
6816 if !matches!(self.peek(), Token::RParen) {
6817 return Err(self.err(alloc::format!(
6818 "v7.12.4 trigger function calls take no args; got {:?}",
6819 self.peek()
6820 )));
6821 }
6822 self.advance();
6823 }
6824 Ok(Statement::CreateTrigger(CreateTriggerStatement {
6825 name,
6826 or_replace,
6827 timing,
6828 events,
6829 table,
6830 for_each,
6831 function,
6832 update_columns,
6833 when_condition,
6834 }))
6835 }
6836
6837 /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
6838 /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
6839 fn parse_create_rule_after_keyword(
6840 &mut self,
6841 or_replace: bool,
6842 ) -> Result<Statement, ParseError> {
6843 let name = self.expect_ident_like()?;
6844 if !matches!(self.peek(), Token::As) {
6845 return Err(self.err(alloc::format!(
6846 "expected AS in CREATE RULE, got {:?}",
6847 self.peek()
6848 )));
6849 }
6850 self.advance();
6851 if !matches!(self.peek(), Token::On) {
6852 return Err(self.err(alloc::format!(
6853 "expected ON in CREATE RULE, got {:?}",
6854 self.peek()
6855 )));
6856 }
6857 self.advance();
6858 let event = self.parse_rule_event()?;
6859 if !matches!(self.peek(), Token::To)
6860 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
6861 {
6862 return Err(self.err(alloc::format!(
6863 "expected TO after rule event, got {:?}",
6864 self.peek()
6865 )));
6866 }
6867 self.advance();
6868 let table = self.expect_ident_like()?;
6869 // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
6870 let when_condition = if matches!(self.peek(), Token::Where) {
6871 self.advance();
6872 Some(self.parse_expr(0)?)
6873 } else {
6874 None
6875 };
6876 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
6877 {
6878 return Err(self.err(alloc::format!(
6879 "expected DO in CREATE RULE, got {:?}",
6880 self.peek()
6881 )));
6882 }
6883 self.advance();
6884 // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
6885 let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
6886 {
6887 self.advance();
6888 true
6889 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
6890 self.advance();
6891 false
6892 } else {
6893 false
6894 };
6895 // `NOTHING` | `( cmd; … )` | `cmd`.
6896 let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
6897 {
6898 self.advance();
6899 Vec::new()
6900 } else if matches!(self.peek(), Token::LParen) {
6901 self.advance();
6902 let mut cmds = Vec::new();
6903 loop {
6904 cmds.push(self.parse_one_statement()?);
6905 if matches!(self.peek(), Token::Semicolon) {
6906 self.advance();
6907 if matches!(self.peek(), Token::RParen) {
6908 break;
6909 }
6910 continue;
6911 }
6912 break;
6913 }
6914 if !matches!(self.peek(), Token::RParen) {
6915 return Err(self.err(alloc::format!(
6916 "expected ) closing the CREATE RULE command list, got {:?}",
6917 self.peek()
6918 )));
6919 }
6920 self.advance();
6921 cmds
6922 } else {
6923 alloc::vec![self.parse_one_statement()?]
6924 };
6925 Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
6926 name,
6927 or_replace,
6928 event,
6929 table,
6930 instead,
6931 when_condition,
6932 commands,
6933 }))
6934 }
6935
6936 /// v7.39 (round 139) — a rule event keyword → uppercase string.
6937 fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
6938 if matches!(self.peek(), Token::Insert) {
6939 self.advance();
6940 return Ok(alloc::string::String::from("INSERT"));
6941 }
6942 if matches!(self.peek(), Token::Select) {
6943 self.advance();
6944 return Ok(alloc::string::String::from("SELECT"));
6945 }
6946 match self.peek() {
6947 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
6948 self.advance();
6949 Ok(alloc::string::String::from("UPDATE"))
6950 }
6951 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
6952 self.advance();
6953 Ok(alloc::string::String::from("DELETE"))
6954 }
6955 other => Err(self.err(alloc::format!(
6956 "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
6957 ))),
6958 }
6959 }
6960
6961 /// v7.13.0 — parse one trigger event, then optionally consume
6962 /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
6963 /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
6964 fn parse_trigger_event_with_optional_of(
6965 &mut self,
6966 ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
6967 let ev = self.parse_trigger_event()?;
6968 if !matches!(ev, TriggerEvent::Update) {
6969 return Ok((ev, Vec::new()));
6970 }
6971 // `OF` is a bare ident.
6972 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
6973 return Ok((ev, Vec::new()));
6974 }
6975 self.advance(); // OF
6976 let mut cols: Vec<String> = Vec::new();
6977 loop {
6978 cols.push(self.expect_ident_like()?);
6979 if matches!(self.peek(), Token::Comma) {
6980 self.advance();
6981 continue;
6982 }
6983 break;
6984 }
6985 if cols.is_empty() {
6986 return Err(
6987 self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
6988 );
6989 }
6990 Ok((ev, cols))
6991 }
6992
6993 /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
6994 /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
6995 /// before `BEGIN`, and IF / RAISE / embedded SQL statements
6996 /// inside the body.
6997 /// Called by [`parse_plpgsql_body`] after the body's tokens
6998 /// have been lexed into this temporary parser.
6999 pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
7000 // v7.12.6 — optional DECLARE prelude.
7001 let declarations = if matches!(
7002 self.peek(),
7003 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
7004 ) {
7005 self.advance();
7006 self.parse_plpgsql_declare_block()?
7007 } else {
7008 Vec::new()
7009 };
7010 // BEGIN keyword (PL/pgSQL — distinct from the SQL
7011 // `BEGIN` transaction-start, but we can reuse the
7012 // reserved Token::Begin since the body is a separate
7013 // lex/parse context).
7014 if !matches!(self.peek(), Token::Begin) {
7015 return Err(self.err(alloc::format!(
7016 "expected BEGIN at start of plpgsql block, got {:?}",
7017 self.peek()
7018 )));
7019 }
7020 self.advance();
7021 let statements = self.parse_plpgsql_stmt_list_until_end()?;
7022 // v7.37.20 (20.10) — optional EXCEPTION clause between the
7023 // body's last statement and the trailing END. When present
7024 // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
7025 // arms terminated by END.
7026 let exception_handlers = if matches!(
7027 self.peek(),
7028 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
7029 ) {
7030 self.advance();
7031 self.parse_plpgsql_exception_handlers()?
7032 } else {
7033 Vec::new()
7034 };
7035 Ok(PlPgSqlBlock {
7036 declarations,
7037 statements,
7038 exception_handlers,
7039 })
7040 }
7041
7042 /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
7043 /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
7044 fn parse_plpgsql_exception_handlers(
7045 &mut self,
7046 ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
7047 let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
7048 loop {
7049 // Stop at END — the block-level trailing END LOOP / END;
7050 // is handled by the caller.
7051 if matches!(
7052 self.peek(),
7053 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
7054 ) {
7055 return Ok(out);
7056 }
7057 // WHEN <cond> [OR <cond>]* THEN <body>
7058 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7059 {
7060 return Err(self.err(alloc::format!(
7061 "expected WHEN or END inside EXCEPTION clause, got {:?}",
7062 self.peek()
7063 )));
7064 }
7065 self.advance();
7066 let mut conditions: Vec<String> = Vec::new();
7067 conditions.push(self.expect_ident_like()?);
7068 while matches!(self.peek(), Token::Or) {
7069 self.advance();
7070 conditions.push(self.expect_ident_like()?);
7071 }
7072 let then_kw = self.expect_ident_like()?;
7073 if !then_kw.eq_ignore_ascii_case("then") {
7074 return Err(self.err(alloc::format!(
7075 "expected THEN after WHEN condition list, got {then_kw:?}"
7076 )));
7077 }
7078 let body = self.parse_plpgsql_stmt_list_until_end()?;
7079 out.push(crate::ast::ExceptionHandler { conditions, body });
7080 }
7081 }
7082
7083 /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
7084 /// prelude. Caller has already consumed `DECLARE`. We stop
7085 /// reading entries when we hit `BEGIN`.
7086 fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
7087 let mut out: Vec<PlPgSqlDeclare> = Vec::new();
7088 loop {
7089 if matches!(self.peek(), Token::Begin) {
7090 return Ok(out);
7091 }
7092 let name = self.expect_ident_like()?;
7093 // v7.37.20 (20.7) — type inference: if the next token is
7094 // `:=` or `=` (no explicit type), infer from the default
7095 // expression. Otherwise the ident that follows is the
7096 // declared type.
7097 //
7098 // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
7099 // (PG-standard). SPG parse-accepts and treats identically
7100 // to inference — the eventual runtime value determines
7101 // the local's type, which is faithful to how SPG handles
7102 // untyped locals today (see 20.7). Full compile-time
7103 // catalog lookup queues with v7.40 PL/pgSQL epic.
7104 let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
7105 // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
7106 // downstream declaration walker to type the local by
7107 // the runtime type of the default expression.
7108 FunctionArgType::Raw("_infer_".into())
7109 } else {
7110 let ty_token = self.expect_ident_like()?;
7111 // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
7112 // consume optional `.<ident>` qualifier + `%<KW>`
7113 // suffix. Both qualifier and suffix map to _infer_.
7114 if matches!(self.peek(), Token::Dot) {
7115 self.advance();
7116 let _ = self.expect_ident_like()?;
7117 }
7118 if matches!(self.peek(), Token::Percent) {
7119 self.advance();
7120 // Consume the trailing TYPE / ROWTYPE ident.
7121 let _ = self.expect_ident_like()?;
7122 FunctionArgType::Raw("_infer_".into())
7123 } else {
7124 match map_type_ident_to_column_type_name(&ty_token) {
7125 Some(t) => FunctionArgType::Typed(t),
7126 None => FunctionArgType::Raw(ty_token),
7127 }
7128 }
7129 };
7130 let default = match self.peek() {
7131 Token::ColonEq => {
7132 self.advance();
7133 Some(self.parse_expr(0)?)
7134 }
7135 Token::Eq => {
7136 // PL/pgSQL also accepts `=` for the
7137 // DECLARE default (PG treats them the same
7138 // in this position).
7139 self.advance();
7140 Some(self.parse_expr(0)?)
7141 }
7142 _ => None,
7143 };
7144 // Mandatory `;` between declarations.
7145 if !matches!(self.peek(), Token::Semicolon) {
7146 return Err(self.err(alloc::format!(
7147 "expected ; after DECLARE entry for {name:?}, got {:?}",
7148 self.peek()
7149 )));
7150 }
7151 self.advance();
7152 out.push(PlPgSqlDeclare { name, ty, default });
7153 }
7154 }
7155
7156 /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
7157 /// the terminating `END;` (or `END IF;` etc — handled by the
7158 /// per-construct sub-parsers). Used by both the outer block
7159 /// and the IF/ELSE branch bodies.
7160 fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
7161 let mut statements: Vec<PlPgSqlStmt> = Vec::new();
7162 loop {
7163 // Allow trailing semicolons + END.
7164 while matches!(self.peek(), Token::Semicolon) {
7165 self.advance();
7166 }
7167 // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7168 if matches!(
7169 self.peek(),
7170 Token::Ident(s) | Token::QuotedIdent(s)
7171 if s.eq_ignore_ascii_case("end")
7172 || s.eq_ignore_ascii_case("else")
7173 || s.eq_ignore_ascii_case("elsif")
7174 || s.eq_ignore_ascii_case("elseif")
7175 || s.eq_ignore_ascii_case("exception")
7176 || s.eq_ignore_ascii_case("when")
7177 ) {
7178 return Ok(statements);
7179 }
7180 // Otherwise: one statement, then expect `;` or
7181 // a block-terminator keyword.
7182 let stmt = self.parse_plpgsql_stmt()?;
7183 statements.push(stmt);
7184 match self.peek() {
7185 Token::Semicolon => {
7186 self.advance();
7187 }
7188 Token::Ident(s) | Token::QuotedIdent(s)
7189 if s.eq_ignore_ascii_case("end")
7190 || s.eq_ignore_ascii_case("else")
7191 || s.eq_ignore_ascii_case("elsif")
7192 || s.eq_ignore_ascii_case("elseif")
7193 || s.eq_ignore_ascii_case("exception")
7194 || s.eq_ignore_ascii_case("when") =>
7195 {
7196 // Final statement of the block without `;`.
7197 }
7198 other => {
7199 return Err(self.err(alloc::format!(
7200 "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7201 )));
7202 }
7203 }
7204 }
7205 }
7206
7207 fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7208 // RETURN keyword?
7209 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7210 {
7211 self.advance();
7212 return self.parse_plpgsql_return();
7213 }
7214 // v7.12.6 — IF block.
7215 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7216 {
7217 self.advance();
7218 return self.parse_plpgsql_if();
7219 }
7220 // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7221 // Detected by peeking that token pos+3 is Ident("execute").
7222 if matches!(self.peek(), Token::For)
7223 && matches!(
7224 self.tokens.get(self.pos + 1),
7225 Some(Token::Ident(_) | Token::QuotedIdent(_))
7226 )
7227 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7228 && matches!(
7229 self.tokens.get(self.pos + 3),
7230 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7231 )
7232 {
7233 self.advance(); // FOR
7234 let var = self.expect_ident_like()?;
7235 self.advance(); // IN
7236 self.advance(); // EXECUTE
7237 // Prescan for LOOP at paren depth 0 so parse_expr stops
7238 // before the LOOP keyword (same trick as the bare-SELECT
7239 // ForQuery arm).
7240 let mut depth: i32 = 0;
7241 let mut loop_pos: Option<usize> = None;
7242 let mut scan = self.pos;
7243 while scan < self.tokens.len() {
7244 match self.tokens.get(scan) {
7245 Some(Token::LParen) => depth += 1,
7246 Some(Token::RParen) => depth -= 1,
7247 Some(Token::Ident(s) | Token::QuotedIdent(s))
7248 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7249 {
7250 loop_pos = Some(scan);
7251 break;
7252 }
7253 _ => {}
7254 }
7255 scan += 1;
7256 }
7257 let loop_pos = loop_pos.ok_or_else(|| {
7258 self.err(alloc::format!(
7259 "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7260 ))
7261 })?;
7262 let saved_loop = self.tokens[loop_pos].clone();
7263 self.tokens[loop_pos] = Token::Semicolon;
7264 let expr_result = self.parse_expr(0);
7265 self.tokens[loop_pos] = saved_loop;
7266 let sql_expr = expr_result?;
7267 let loop_kw = self.expect_ident_like()?;
7268 if !loop_kw.eq_ignore_ascii_case("loop") {
7269 return Err(self.err(alloc::format!(
7270 "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7271 )));
7272 }
7273 let body = self.parse_plpgsql_stmt_list_until_end()?;
7274 let end_kw = self.expect_ident_like()?;
7275 if !end_kw.eq_ignore_ascii_case("end") {
7276 return Err(self.err(alloc::format!(
7277 "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7278 )));
7279 }
7280 let loop_kw2 = self.expect_ident_like()?;
7281 if !loop_kw2.eq_ignore_ascii_case("loop") {
7282 return Err(self.err(alloc::format!(
7283 "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7284 )));
7285 }
7286 return Ok(PlPgSqlStmt::ForExecute {
7287 var,
7288 sql_expr,
7289 body,
7290 });
7291 }
7292 // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7293 //
7294 // Two syntactic forms:
7295 // FOR var IN SELECT ... ORDER BY ... LOOP ...
7296 // FOR var IN (SELECT ...) LOOP ...
7297 //
7298 // Bare-SELECT form: to keep parse_select_stmt from swallowing
7299 // the trailing `LOOP` keyword as a table alias, we prescan
7300 // forward to find LOOP at paren depth 0, splice a fake
7301 // Semicolon at that position (so SELECT parses cleanly),
7302 // then re-splice LOOP back in.
7303 //
7304 // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7305 // LOOP directly — no scan required.
7306 if matches!(self.peek(), Token::For)
7307 && matches!(
7308 self.tokens.get(self.pos + 1),
7309 Some(Token::Ident(_) | Token::QuotedIdent(_))
7310 )
7311 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7312 && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7313 || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7314 {
7315 self.advance(); // FOR
7316 let var = self.expect_ident_like()?;
7317 // IN
7318 self.advance();
7319 let query = if matches!(self.peek(), Token::LParen) {
7320 // Paren-wrapped SELECT.
7321 self.advance();
7322 let inner = self.parse_select_stmt()?;
7323 let Statement::Select(q) = inner else {
7324 return Err(self.err(alloc::format!(
7325 "expected SELECT inside (…), got {:?}",
7326 self.peek()
7327 )));
7328 };
7329 if !matches!(self.peek(), Token::RParen) {
7330 return Err(self.err(alloc::format!(
7331 "expected ')' after FOR-IN-SELECT body, got {:?}",
7332 self.peek()
7333 )));
7334 }
7335 self.advance();
7336 q
7337 } else {
7338 // Bare SELECT: prescan to find the LOOP boundary.
7339 let mut depth: i32 = 0;
7340 let mut loop_pos: Option<usize> = None;
7341 let mut scan = self.pos;
7342 while scan < self.tokens.len() {
7343 match self.tokens.get(scan) {
7344 Some(Token::LParen) => depth += 1,
7345 Some(Token::RParen) => depth -= 1,
7346 Some(Token::Ident(s) | Token::QuotedIdent(s))
7347 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7348 {
7349 loop_pos = Some(scan);
7350 break;
7351 }
7352 _ => {}
7353 }
7354 scan += 1;
7355 }
7356 let loop_pos = loop_pos.ok_or_else(|| {
7357 self.err(alloc::format!(
7358 "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7359 ))
7360 })?;
7361 // Swap the LOOP token with a synthetic Semicolon so
7362 // parse_select_stmt stops there, then restore afterward.
7363 let saved_loop = self.tokens[loop_pos].clone();
7364 self.tokens[loop_pos] = Token::Semicolon;
7365 let parse_result = self.parse_select_stmt();
7366 self.tokens[loop_pos] = saved_loop;
7367 let inner = parse_result?;
7368 let Statement::Select(q) = inner else {
7369 return Err(self.err(alloc::format!(
7370 "expected SELECT after FOR <var> IN, got {:?}",
7371 self.peek()
7372 )));
7373 };
7374 q
7375 };
7376 let loop_kw = self.expect_ident_like()?;
7377 if !loop_kw.eq_ignore_ascii_case("loop") {
7378 return Err(self.err(alloc::format!(
7379 "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7380 )));
7381 }
7382 let body = self.parse_plpgsql_stmt_list_until_end()?;
7383 let end_kw = self.expect_ident_like()?;
7384 if !end_kw.eq_ignore_ascii_case("end") {
7385 return Err(self.err(alloc::format!(
7386 "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7387 )));
7388 }
7389 let loop_kw2 = self.expect_ident_like()?;
7390 if !loop_kw2.eq_ignore_ascii_case("loop") {
7391 return Err(self.err(alloc::format!(
7392 "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7393 )));
7394 }
7395 return Ok(PlPgSqlStmt::ForQuery {
7396 var,
7397 query: Box::new(query),
7398 body,
7399 });
7400 }
7401 // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7402 // FOR is a reserved keyword token (Token::For).
7403 if matches!(self.peek(), Token::For)
7404 && matches!(
7405 self.tokens.get(self.pos + 1),
7406 Some(Token::Ident(_) | Token::QuotedIdent(_))
7407 )
7408 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7409 {
7410 self.advance(); // FOR
7411 let var = self.expect_ident_like()?;
7412 if !matches!(self.peek(), Token::In) {
7413 return Err(self.err(alloc::format!(
7414 "expected IN after FOR <var>, got {:?}",
7415 self.peek()
7416 )));
7417 }
7418 self.advance();
7419 let reverse = matches!(
7420 self.peek(),
7421 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7422 );
7423 if reverse {
7424 self.advance();
7425 }
7426 let start = self.parse_expr(0)?;
7427 if !matches!(self.peek(), Token::DotDot) {
7428 return Err(self.err(alloc::format!(
7429 "expected '..' between FOR loop bounds, got {:?}",
7430 self.peek()
7431 )));
7432 }
7433 self.advance();
7434 let end = self.parse_expr(0)?;
7435 let loop_kw = self.expect_ident_like()?;
7436 if !loop_kw.eq_ignore_ascii_case("loop") {
7437 return Err(self.err(alloc::format!(
7438 "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7439 )));
7440 }
7441 let body = self.parse_plpgsql_stmt_list_until_end()?;
7442 let end_kw = self.expect_ident_like()?;
7443 if !end_kw.eq_ignore_ascii_case("end") {
7444 return Err(self.err(alloc::format!(
7445 "expected END LOOP after FOR body, got {end_kw:?}"
7446 )));
7447 }
7448 let loop_kw2 = self.expect_ident_like()?;
7449 if !loop_kw2.eq_ignore_ascii_case("loop") {
7450 return Err(self.err(alloc::format!(
7451 "expected END LOOP after FOR body, got END {loop_kw2:?}"
7452 )));
7453 }
7454 return Ok(PlPgSqlStmt::ForRange {
7455 var,
7456 start,
7457 end,
7458 reverse,
7459 body,
7460 });
7461 }
7462 // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7463 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7464 {
7465 self.advance();
7466 let body = self.parse_plpgsql_stmt_list_until_end()?;
7467 let end_kw = self.expect_ident_like()?;
7468 if !end_kw.eq_ignore_ascii_case("end") {
7469 return Err(self.err(alloc::format!(
7470 "expected END LOOP after LOOP body, got {end_kw:?}"
7471 )));
7472 }
7473 let loop_kw = self.expect_ident_like()?;
7474 if !loop_kw.eq_ignore_ascii_case("loop") {
7475 return Err(self.err(alloc::format!(
7476 "expected END LOOP after LOOP body, got END {loop_kw:?}"
7477 )));
7478 }
7479 return Ok(PlPgSqlStmt::Loop { body });
7480 }
7481 // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7482 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7483 {
7484 self.advance();
7485 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7486 {
7487 self.advance();
7488 Some(self.parse_expr(0)?)
7489 } else {
7490 None
7491 };
7492 return Ok(PlPgSqlStmt::Exit { when });
7493 }
7494 // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7495 // already-parsed Statement or a runtime-computed SQL string.
7496 // The disambiguator vs the extended-query-protocol `EXECUTE
7497 // <stmt_name>` (which is a top-level Statement, not a
7498 // plpgsql line) is that inside a DO block / trigger body the
7499 // EXECUTE keyword ALWAYS refers to dynamic SQL.
7500 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7501 {
7502 self.advance();
7503 let sql = self.parse_expr(0)?;
7504 return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7505 }
7506 // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7507 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7508 {
7509 self.advance();
7510 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7511 {
7512 self.advance();
7513 Some(self.parse_expr(0)?)
7514 } else {
7515 None
7516 };
7517 return Ok(PlPgSqlStmt::Continue { when });
7518 }
7519 // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7520 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7521 {
7522 self.advance();
7523 let condition = self.parse_expr(0)?;
7524 let loop_kw = self.expect_ident_like()?;
7525 if !loop_kw.eq_ignore_ascii_case("loop") {
7526 return Err(self.err(alloc::format!(
7527 "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7528 )));
7529 }
7530 let body = self.parse_plpgsql_stmt_list_until_end()?;
7531 // Expect END LOOP.
7532 let end_kw = self.expect_ident_like()?;
7533 if !end_kw.eq_ignore_ascii_case("end") {
7534 return Err(self.err(alloc::format!(
7535 "expected END LOOP after WHILE body, got {end_kw:?}"
7536 )));
7537 }
7538 let loop_kw2 = self.expect_ident_like()?;
7539 if !loop_kw2.eq_ignore_ascii_case("loop") {
7540 return Err(self.err(alloc::format!(
7541 "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7542 )));
7543 }
7544 return Ok(PlPgSqlStmt::While { condition, body });
7545 }
7546 // v7.12.6 — RAISE.
7547 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7548 {
7549 self.advance();
7550 return self.parse_plpgsql_raise();
7551 }
7552 // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7553 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7554 {
7555 self.advance();
7556 let condition = self.parse_expr(0)?;
7557 let message = if matches!(self.peek(), Token::Comma) {
7558 self.advance();
7559 Some(self.parse_expr(0)?)
7560 } else {
7561 None
7562 };
7563 return Ok(PlPgSqlStmt::Assert { condition, message });
7564 }
7565 // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7566 // "PERFORM is equivalent to SELECT but discards the
7567 // result." Side effects (function calls, RAISE inside
7568 // SQL functions, etc.) still execute. We desugar to
7569 // `SELECT <body>` and wrap in EmbeddedSql so the engine's
7570 // existing embedded-statement path handles execution +
7571 // result-discard cleanly. The result is naturally
7572 // discarded because EmbeddedSql doesn't propagate row
7573 // sets back to the plpgsql interpreter.
7574 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7575 {
7576 self.advance();
7577 // Splice a synthetic Token::Select into the stream at
7578 // the current position so parse_select_stmt parses the
7579 // remainder as a normal SELECT body. Token-stream
7580 // surgery mirrors the try_parse_plpgsql_select_into
7581 // pattern used for SELECT … INTO desugaring.
7582 self.tokens.insert(self.pos, Token::Select);
7583 let select = self.parse_select_stmt()?;
7584 let Statement::Select(s) = select else {
7585 return Err(self.err(alloc::format!(
7586 "expected SELECT body after PERFORM, got {:?}",
7587 self.peek()
7588 )));
7589 };
7590 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7591 }
7592 // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7593 // plpgsql-specific shape (mailrs round-10 migrate-042).
7594 // PG's SELECT INTO at top-level SQL would CREATE a new
7595 // table; inside plpgsql it ASSIGNS the query result to
7596 // a local variable. We detect the INTO at paren-depth
7597 // 0 between SELECT and the statement boundary; if
7598 // found, split the token stream into "pre-INTO
7599 // projection" + "var" + "post-INTO FROM/WHERE…" and
7600 // rebuild as a SelectInto with a regular SELECT body
7601 // (no INTO clause).
7602 if matches!(self.peek(), Token::Select)
7603 && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7604 {
7605 return Ok(PlPgSqlStmt::SelectInto {
7606 var: var_name,
7607 body: Box::new(select_body),
7608 });
7609 }
7610 // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7611 // SELECT can appear directly inside a trigger body; we
7612 // recurse into the regular Statement parser, which will
7613 // stop at the trailing `;` (which our caller then
7614 // consumes).
7615 // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7616 // also embed ALTER / CREATE / DROP statements; route
7617 // those through the same parser so the DO body parses
7618 // cleanly.
7619 if matches!(self.peek(), Token::Insert)
7620 || matches!(self.peek(), Token::Select)
7621 || matches!(self.peek(), Token::Create)
7622 || matches!(self.peek(), Token::Drop)
7623 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7624 if s.eq_ignore_ascii_case("update")
7625 || s.eq_ignore_ascii_case("delete")
7626 || s.eq_ignore_ascii_case("alter"))
7627 {
7628 let stmt = self.parse_one_statement()?;
7629 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7630 }
7631 // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7632 // followed by `:=` and an expression.
7633 let target = self.parse_plpgsql_assign_target()?;
7634 // PL/pgSQL assignment uses `:=`. The lexer represents
7635 // this as a colon followed by `=`; check both shapes.
7636 match self.peek() {
7637 Token::ColonEq => {
7638 self.advance();
7639 }
7640 Token::Colon => {
7641 self.advance();
7642 if !matches!(self.peek(), Token::Eq) {
7643 return Err(self.err(alloc::format!(
7644 "expected := after plpgsql assign target, got `:` then {:?}",
7645 self.peek()
7646 )));
7647 }
7648 self.advance();
7649 }
7650 other => {
7651 return Err(self.err(alloc::format!(
7652 "expected := after plpgsql assign target, got {other:?}"
7653 )));
7654 }
7655 }
7656 let value = self.parse_expr(0)?;
7657 Ok(PlPgSqlStmt::Assign { target, value })
7658 }
7659
7660 /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7661 /// [ELSE body] END IF`. `IF` keyword already consumed.
7662 fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7663 let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7664 let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7665 loop {
7666 // <expr> THEN
7667 let cond = self.parse_expr(0)?;
7668 let then_kw = self.expect_ident_like()?;
7669 if !then_kw.eq_ignore_ascii_case("then") {
7670 return Err(self.err(alloc::format!(
7671 "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7672 )));
7673 }
7674 let body = self.parse_plpgsql_stmt_list_until_end()?;
7675 branches.push((cond, body));
7676 // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7677 match self.peek() {
7678 Token::Ident(s) | Token::QuotedIdent(s)
7679 if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7680 {
7681 self.advance();
7682 continue;
7683 }
7684 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7685 self.advance();
7686 else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7687 break;
7688 }
7689 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7690 break;
7691 }
7692 other => {
7693 return Err(self.err(alloc::format!(
7694 "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7695 )));
7696 }
7697 }
7698 }
7699 // Expect `END IF` (the END keyword is the one we're
7700 // looking at right now).
7701 let end_kw = self.expect_ident_like()?;
7702 if !end_kw.eq_ignore_ascii_case("end") {
7703 return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7704 }
7705 let if_kw = self.expect_ident_like()?;
7706 if !if_kw.eq_ignore_ascii_case("if") {
7707 return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7708 }
7709 Ok(PlPgSqlStmt::If {
7710 branches,
7711 else_branch,
7712 })
7713 }
7714
7715 /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7716 /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7717 /// is already consumed.
7718 fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7719 let lvl_ident = self.expect_ident_like()?;
7720 let level = match lvl_ident.to_ascii_lowercase().as_str() {
7721 "notice" => RaiseLevel::Notice,
7722 "warning" => RaiseLevel::Warning,
7723 "info" => RaiseLevel::Info,
7724 "log" => RaiseLevel::Log,
7725 "debug" => RaiseLevel::Debug,
7726 "exception" => RaiseLevel::Exception,
7727 other => {
7728 return Err(self.err(alloc::format!(
7729 "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7730 )));
7731 }
7732 };
7733 // Message: required for v7.12.6. PG accepts a bare
7734 // RAISE-rethrow form (no message), reserved for future
7735 // RAISE-no-args support.
7736 let Token::String(msg) = self.peek() else {
7737 return Err(self.err(alloc::format!(
7738 "expected RAISE message string, got {:?}",
7739 self.peek()
7740 )));
7741 };
7742 let message = msg.clone();
7743 self.advance();
7744 // Optional comma-separated args (PG `%` format substitution).
7745 let mut args: Vec<Expr> = Vec::new();
7746 while matches!(self.peek(), Token::Comma) {
7747 self.advance();
7748 args.push(self.parse_expr(0)?);
7749 }
7750 Ok(PlPgSqlStmt::Raise {
7751 level,
7752 message,
7753 args,
7754 })
7755 }
7756
7757 /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7758 /// <projection> INTO <var> [FROM …]` (mailrs round-10
7759 /// migrate-042). Returns `(rebuilt_select_without_into,
7760 /// var_name)` when the pattern matches; `None` for
7761 /// regular SELECTs (those go through the embedded-SQL
7762 /// path). Token-stream surgery so the rebuilt SELECT
7763 /// parses through the regular `parse_select_stmt`.
7764 #[allow(clippy::too_many_lines)]
7765 fn try_parse_plpgsql_select_into(
7766 &mut self,
7767 ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7768 // Scan forward from `self.pos + 1` (past Token::Select)
7769 // for Token::Into at paren-depth 0, stopping at the
7770 // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7771 // end the plpgsql statement.
7772 let start = self.pos;
7773 let mut into_pos: Option<usize> = None;
7774 let mut depth: i32 = 0;
7775 let mut i = start + 1;
7776 while i < self.tokens.len() {
7777 match &self.tokens[i] {
7778 Token::LParen => depth += 1,
7779 Token::RParen => depth -= 1,
7780 Token::Semicolon if depth == 0 => break,
7781 Token::Ident(s)
7782 if depth == 0
7783 && (s.eq_ignore_ascii_case("end")
7784 || s.eq_ignore_ascii_case("else")
7785 || s.eq_ignore_ascii_case("elsif")) =>
7786 {
7787 break;
7788 }
7789 Token::Into if depth == 0 => {
7790 into_pos = Some(i);
7791 break;
7792 }
7793 _ => {}
7794 }
7795 i += 1;
7796 }
7797 let Some(into_at) = into_pos else {
7798 return Ok(None);
7799 };
7800 // The token immediately after INTO must be the target
7801 // var ident; anything else (e.g. INSERT INTO table)
7802 // ruled out by the depth-0 check above. Capture it.
7803 let var = match self.tokens.get(into_at + 1) {
7804 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7805 other => {
7806 return Err(self.err(alloc::format!(
7807 "expected variable name after SELECT … INTO, got {other:?}"
7808 )));
7809 }
7810 };
7811 // Find the end of the plpgsql SELECT INTO statement —
7812 // same boundary rules as the depth-0 scan above.
7813 let mut end = into_at + 2;
7814 let mut depth2: i32 = 0;
7815 while end < self.tokens.len() {
7816 match &self.tokens[end] {
7817 Token::LParen => depth2 += 1,
7818 Token::RParen => depth2 -= 1,
7819 Token::Semicolon if depth2 == 0 => break,
7820 Token::Ident(s)
7821 if depth2 == 0
7822 && (s.eq_ignore_ascii_case("end")
7823 || s.eq_ignore_ascii_case("else")
7824 || s.eq_ignore_ascii_case("elsif")) =>
7825 {
7826 break;
7827 }
7828 _ => {}
7829 }
7830 end += 1;
7831 }
7832 // Rebuild a token stream that represents the SELECT
7833 // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
7834 // post-var tokens up to statement end]. Run the
7835 // regular `parse_select_stmt` against it.
7836 let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
7837 for j in start..into_at {
7838 rebuilt.push(self.tokens[j].clone());
7839 }
7840 for j in (into_at + 2)..end {
7841 rebuilt.push(self.tokens[j].clone());
7842 }
7843 rebuilt.push(Token::Eof);
7844 let saved_pos = self.pos;
7845 let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
7846 self.pos = 0;
7847 // parse_select_stmt → parse_bare_select consumes Token::Select itself.
7848 if !matches!(self.peek(), Token::Select) {
7849 self.tokens = saved_tokens;
7850 self.pos = saved_pos;
7851 return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
7852 }
7853 let sel = self.parse_select_stmt();
7854 self.tokens = saved_tokens;
7855 self.pos = end;
7856 let sel = sel?;
7857 let Statement::Select(body) = sel else {
7858 return Err(self.err(alloc::format!(
7859 "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
7860 )));
7861 };
7862 Ok(Some((body, var)))
7863 }
7864
7865 fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
7866 // v7.16.1 — read the head token DIRECTLY rather than
7867 // via `expect_ident_like`. The v7.14.0 schema-qualifier
7868 // strip (`public.t` → `t`) inside `expect_ident_like`
7869 // greedily consumes any `ident . ident` pair, which
7870 // silently turned every `NEW.col := …` /
7871 // `OLD.col := …` plpgsql assignment into a Local("col")
7872 // assignment — the head "new"/"old" was eaten as if it
7873 // were a schema name and the Dot was consumed too, so
7874 // this function's own `peek() == Token::Dot` check
7875 // below never fired. Every BEFORE trigger that rewrote
7876 // a NEW cell was a silent no-op for two major releases
7877 // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
7878 // gate failures were investigated as v7.16.1 backlog.
7879 let head = match self.advance() {
7880 Token::Ident(s) | Token::QuotedIdent(s) => s,
7881 other => {
7882 return Err(self.err(alloc::format!(
7883 "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
7884 )));
7885 }
7886 };
7887 if matches!(self.peek(), Token::Dot) {
7888 self.advance();
7889 let col = self.expect_ident_like()?;
7890 if head.eq_ignore_ascii_case("new") {
7891 return Ok(AssignTarget::NewColumn(col));
7892 }
7893 if head.eq_ignore_ascii_case("old") {
7894 return Ok(AssignTarget::OldColumn(col));
7895 }
7896 return Err(self.err(alloc::format!(
7897 "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
7898 got {head:?}.<col>"
7899 )));
7900 }
7901 Ok(AssignTarget::Local(head))
7902 }
7903
7904 fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7905 // RETURN NEW / OLD / NULL — bare-ident forms.
7906 match self.peek() {
7907 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
7908 self.advance();
7909 return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
7910 }
7911 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
7912 self.advance();
7913 return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
7914 }
7915 Token::Null => {
7916 self.advance();
7917 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7918 }
7919 // Bare `RETURN;` (no value) — treated as `RETURN NULL`
7920 // per PL/pgSQL convention.
7921 Token::Semicolon => {
7922 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7923 }
7924 _ => {}
7925 }
7926 // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
7927 // EXECUTE <expr>. In a DO block context RETURN QUERY has no
7928 // caller-visible effect (blocks don't return sets), so we
7929 // desugar it identically to PERFORM: parse the SELECT (or
7930 // EXECUTE dynamic) as embedded SQL that runs for side
7931 // effects and discards the result. RETURN NEXT <expr>
7932 // (single-row accumulator) queues with v7.40 SETOF function
7933 // infrastructure.
7934 // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
7935 // and keep going.
7936 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
7937 {
7938 self.advance();
7939 let e = self.parse_expr(0)?;
7940 return Ok(PlPgSqlStmt::ReturnNext(e));
7941 }
7942 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
7943 {
7944 self.advance();
7945 // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
7946 // rows go to the set, like the static form. It used to desugar to a
7947 // bare ExecuteDynamic, whose result was DISCARDED.
7948 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7949 {
7950 self.advance();
7951 let sql = self.parse_expr(0)?;
7952 return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
7953 }
7954 // Bare RETURN QUERY <select>. If the current token is
7955 // not already SELECT (e.g., the user wrote `RETURN QUERY
7956 // <projection> FROM ...` in a shorthand — rare but PG
7957 // accepts a bare projection here), splice one in. Same
7958 // trick as PERFORM.
7959 if !matches!(self.peek(), Token::Select) {
7960 self.tokens.insert(self.pos, Token::Select);
7961 }
7962 let select = self.parse_select_stmt()?;
7963 let Statement::Select(s) = select else {
7964 return Err(self.err(alloc::format!(
7965 "expected SELECT body after RETURN QUERY, got {:?}",
7966 self.peek()
7967 )));
7968 };
7969 // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
7970 // to an embedded side-effect SELECT whose rows were DISCARDED, which
7971 // in a SETOF function is the entire answer thrown away.
7972 return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
7973 }
7974 // Fall through: parse a full expression.
7975 let e = self.parse_expr(0)?;
7976 Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
7977 }
7978
7979 fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
7980 // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
7981 // are ident-shaped (the parser keys off case-insensitive
7982 // match — same shape used by the top-level Update / Delete
7983 // dispatchers at parse_one_statement).
7984 if matches!(self.peek(), Token::Insert) {
7985 self.advance();
7986 return Ok(TriggerEvent::Insert);
7987 }
7988 match self.peek() {
7989 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7990 self.advance();
7991 Ok(TriggerEvent::Update)
7992 }
7993 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7994 self.advance();
7995 Ok(TriggerEvent::Delete)
7996 }
7997 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
7998 self.advance();
7999 Ok(TriggerEvent::Truncate)
8000 }
8001 other => Err(self.err(alloc::format!(
8002 "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
8003 ))),
8004 }
8005 }
8006
8007 /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
8008 /// - (no clause) → implicit `FOR ALL TABLES`
8009 /// - `FOR ALL TABLES`
8010 /// - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
8011 /// - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
8012 /// accepted as an SPG lenience. PG18-measured (round 753): PG
8013 /// REJECTS the bare plural (`invalid publication object list`,
8014 /// TABLES only pairs with IN SCHEMA); the old note claimed an
8015 /// unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
8016 fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
8017 let name = self.expect_ident_or_string()?;
8018 // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
8019 // shape so existing publications keep parsing identically.
8020 let scope = if matches!(self.peek(), Token::For) {
8021 self.advance();
8022 if matches!(self.peek(), Token::All) {
8023 self.advance();
8024 if !matches!(self.peek(), Token::Tables) {
8025 return Err(self.err(format!(
8026 "expected TABLES after FOR ALL, got {:?}",
8027 self.peek()
8028 )));
8029 }
8030 self.advance();
8031 if matches!(self.peek(), Token::Except) {
8032 self.advance();
8033 let tables = self.parse_publication_table_list()?;
8034 PublicationScope::AllTablesExcept(tables)
8035 } else {
8036 PublicationScope::AllTables
8037 }
8038 } else if matches!(self.peek(), Token::Table) {
8039 self.advance();
8040 let tables = self.parse_publication_table_list()?;
8041 PublicationScope::ForTables(tables)
8042 } else if matches!(self.peek(), Token::Tables) {
8043 // v7.39 (round 754, F31-B5) — PG18-measured: the bare
8044 // plural (`FOR TABLES t`) is REJECTED (`invalid
8045 // publication object list`); TABLES only pairs with
8046 // `IN SCHEMA`. The old arm accepted it on an
8047 // unverifiable "PG 19 accepts both" claim.
8048 self.advance();
8049 if !matches!(self.peek(), Token::In) {
8050 return Err(self.err(alloc::string::String::from(
8051 "invalid publication object list",
8052 )));
8053 }
8054 self.advance();
8055 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
8056 return Err(self.err(format!(
8057 "expected SCHEMA after FOR TABLES IN, got {:?}",
8058 self.peek()
8059 )));
8060 }
8061 self.advance();
8062 let schema = self.expect_ident_or_string()?;
8063 PublicationScope::TablesInSchema(schema)
8064 } else {
8065 return Err(self.err(format!(
8066 "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
8067 self.peek()
8068 )));
8069 }
8070 } else {
8071 PublicationScope::AllTables
8072 };
8073 Ok(Statement::CreatePublication(CreatePublicationStatement {
8074 name,
8075 scope,
8076 }))
8077 }
8078
8079 /// v6.1.3 — Comma-separated identifier list for the publication
8080 /// FOR-clause. Requires at least one entry; empty list is a
8081 /// parse error (PG behaviour). Quoted idents are accepted; the
8082 /// names round-trip through `Display` as `quote_ident(name)`.
8083 ///
8084 /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
8085 /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
8086 /// pg_dump output. SPG's publication state today is per-table
8087 /// only (matching the pre-PG-15 surface); the col list + WHERE
8088 /// are parsed so dumps load through and the table name reaches
8089 /// `PublicationScope::ForTables`, but the filter is not enforced
8090 /// at publish time. Re-open when a customer dogfood gate
8091 /// requires per-row-filter or column-subset publish semantics
8092 /// (which gates on persistent slot state landing first, 21.12).
8093 fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
8094 let first = self.parse_publication_table_entry()?;
8095 let mut out = alloc::vec![first];
8096 while matches!(self.peek(), Token::Comma) {
8097 self.advance();
8098 out.push(self.parse_publication_table_entry()?);
8099 }
8100 Ok(out)
8101 }
8102
8103 /// One table entry inside a FOR TABLE clause:
8104 /// tab_name [ (col, col, …) ] [ WHERE (predicate) ]
8105 /// Returns just the table name; the column list + WHERE predicate
8106 /// are consumed and discarded per the parse-accept-discard
8107 /// commitment above.
8108 fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
8109 let name = self.expect_ident_like()?;
8110 // Optional column list — `(col, col, …)`.
8111 if matches!(self.peek(), Token::LParen) {
8112 self.advance();
8113 // Empty parens are a PG error too; require ≥ 1 column.
8114 let _ = self.expect_ident_like()?;
8115 while matches!(self.peek(), Token::Comma) {
8116 self.advance();
8117 let _ = self.expect_ident_like()?;
8118 }
8119 if !matches!(self.peek(), Token::RParen) {
8120 return Err(self.err(alloc::format!(
8121 "expected ')' to close publication column list, got {:?}",
8122 self.peek()
8123 )));
8124 }
8125 self.advance();
8126 }
8127 // Optional row filter — `WHERE (predicate)`.
8128 if matches!(self.peek(), Token::Where) {
8129 self.advance();
8130 if !matches!(self.peek(), Token::LParen) {
8131 return Err(self.err(alloc::format!(
8132 "expected '(' after WHERE in publication row filter, got {:?}",
8133 self.peek()
8134 )));
8135 }
8136 self.advance();
8137 let _ = self.parse_expr(0)?;
8138 if !matches!(self.peek(), Token::RParen) {
8139 return Err(self.err(alloc::format!(
8140 "expected ')' to close publication WHERE filter, got {:?}",
8141 self.peek()
8142 )));
8143 }
8144 self.advance();
8145 }
8146 Ok(name)
8147 }
8148
8149 /// v6.1.4 — `CREATE SUBSCRIPTION <name>
8150 /// CONNECTION '<conn>'
8151 /// PUBLICATION <pub> [, <pub> ...]`.
8152 ///
8153 /// The clause order is fixed (CONNECTION first, then
8154 /// PUBLICATION) to match PG. No WITH-options accepted in
8155 /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
8156 fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
8157 let name = self.expect_ident_or_string()?;
8158 if !matches!(self.peek(), Token::Connection) {
8159 return Err(self.err(format!(
8160 "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
8161 self.peek()
8162 )));
8163 }
8164 self.advance();
8165 let conn_str = self.expect_string_literal()?;
8166 if !matches!(self.peek(), Token::Publication) {
8167 return Err(self.err(format!(
8168 "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8169 self.peek()
8170 )));
8171 }
8172 self.advance();
8173 // Reuse the publication FOR-list parser shape: at least one
8174 // identifier, comma-separated.
8175 let first = self.expect_ident_like()?;
8176 let mut publications = alloc::vec![first];
8177 while matches!(self.peek(), Token::Comma) {
8178 self.advance();
8179 publications.push(self.expect_ident_like()?);
8180 }
8181 Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8182 name,
8183 conn_str,
8184 publications,
8185 }))
8186 }
8187
8188 /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8189 /// All keywords after `WAIT` are bare idents in v6.1.x; no
8190 /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8191 /// that fit `u64`.
8192 /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8193 /// qualifier is a *namespace* the app owns (`app.user_id`,
8194 /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8195 /// to discard. So parse the raw segments here instead of
8196 /// `expect_ident_like`, which strips a leading `schema.` qualifier
8197 /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8198 /// a single segment and round-trip unchanged.
8199 fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8200 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8201 loop {
8202 let seg = match self.advance() {
8203 Token::Ident(s) | Token::QuotedIdent(s) => s,
8204 other if unreserved_keyword_text(&other).is_some() => {
8205 unreserved_keyword_text(&other).unwrap()
8206 }
8207 other => {
8208 return Err(ParseError {
8209 message: format!("expected parameter name, got {other:?}"),
8210 token_pos: self.consumed_pos(),
8211 });
8212 }
8213 };
8214 parts.push(seg);
8215 if matches!(self.peek(), Token::Dot) {
8216 self.advance();
8217 continue;
8218 }
8219 break;
8220 }
8221 Ok(parts.join(".").to_ascii_lowercase())
8222 }
8223
8224 fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8225 Self::parse_set_value_inner(self)
8226 }
8227
8228 fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8229 match self.advance() {
8230 Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8231 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8232 Ok(crate::ast::SetValue::Default)
8233 }
8234 Token::Ident(s) | Token::QuotedIdent(s) => {
8235 let mut accum = s;
8236 while matches!(self.peek(), Token::Dot) {
8237 self.advance();
8238 let next = self.expect_ident_like()?;
8239 accum.push('.');
8240 accum.push_str(&next);
8241 }
8242 Ok(crate::ast::SetValue::Ident(accum))
8243 }
8244 Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8245 Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8246 // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8247 // spellings that lex as keyword tokens, not idents:
8248 // `SET standard_conforming_strings = on` is in every
8249 // pg_dump preamble (`off` already lexes as an ident).
8250 // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8251 // DEFAULT lexes as its keyword token, so the ident arm above
8252 // never saw it and the everyday reset form was a syntax error.
8253 Token::Default => Ok(crate::ast::SetValue::Default),
8254 Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8255 Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8256 Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8257 // v7.14.0 — MySQL session/user variable RHS
8258 // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8259 // Wrap as Ident so the SET handler can record it; the
8260 // engine treats `@VAR` / `@@VAR` values as opaque
8261 // strings.
8262 Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8263 // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8264 // is the common MySQL preamble shape. Allow a `+` or
8265 // `-` prefix on negative numerics for parity with PG
8266 // (some param defaults are negative).
8267 Token::Minus => match self.advance() {
8268 Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8269 Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8270 other => Err(self.err(format!(
8271 "expected numeric after `-` in SET value, got {other:?}"
8272 ))),
8273 },
8274 other => Err(self.err(format!(
8275 "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8276 ))),
8277 }
8278 }
8279
8280 /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8281 /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8282 /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8283 /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8284 /// present). Modes are comma-separated per PG; SPG also
8285 /// accepts space-separated for tolerance. READ ONLY / WRITE
8286 /// / DEFERRABLE are parsed-and-ignored (recorded for future
8287 /// surface but not behaviorally honoured today).
8288 /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8289 /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8290 /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8291 /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8292 /// session default rather than forcing READ COMMITTED.
8293 fn parse_isolation_level_clauses(&mut self) -> Result<Option<IsolationLevel>, ParseError> {
8294 let mut level = IsolationLevel::default();
8295 let mut have_level = false;
8296 loop {
8297 // ISOLATION LEVEL …
8298 let saw_isolation =
8299 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8300 if saw_isolation {
8301 self.advance(); // ISOLATION
8302 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8303 return Err(self.err(alloc::format!(
8304 "expected LEVEL after ISOLATION, got {:?}",
8305 self.peek()
8306 )));
8307 }
8308 self.advance(); // LEVEL
8309 // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8310 let w1 = self
8311 .expect_ident_like()
8312 .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8313 let lc = w1.to_ascii_lowercase();
8314 level = match lc.as_str() {
8315 "serializable" => IsolationLevel::Serializable,
8316 "repeatable" => {
8317 // Expect READ
8318 let w2 = self
8319 .expect_ident_like()
8320 .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8321 if !w2.eq_ignore_ascii_case("read") {
8322 return Err(self.err(alloc::format!(
8323 "expected READ after REPEATABLE, got {w2:?}"
8324 )));
8325 }
8326 IsolationLevel::RepeatableRead
8327 }
8328 "read" => {
8329 let w2 = self
8330 .expect_ident_like()
8331 .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8332 match w2.to_ascii_lowercase().as_str() {
8333 "committed" => IsolationLevel::ReadCommitted,
8334 "uncommitted" => IsolationLevel::ReadUncommitted,
8335 other => {
8336 return Err(self.err(alloc::format!(
8337 "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8338 )));
8339 }
8340 }
8341 }
8342 other => {
8343 return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8344 }
8345 };
8346 have_level = true;
8347 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8348 // READ ONLY | READ WRITE — parsed, not behaviorally honoured.
8349 self.advance();
8350 match self.peek().clone() {
8351 Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8352 self.advance();
8353 }
8354 Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8355 self.advance();
8356 }
8357 other => {
8358 return Err(self.err(alloc::format!(
8359 "expected ONLY or WRITE after READ, got {other:?}"
8360 )));
8361 }
8362 }
8363 } else if matches!(self.peek(), Token::Not) {
8364 // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8365 self.advance();
8366 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8367 return Err(self.err(alloc::format!(
8368 "expected DEFERRABLE after NOT, got {:?}",
8369 self.peek()
8370 )));
8371 }
8372 self.advance();
8373 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8374 {
8375 self.advance();
8376 } else {
8377 break;
8378 }
8379 // Optional comma between modes.
8380 if matches!(self.peek(), Token::Comma) {
8381 self.advance();
8382 }
8383 }
8384 Ok(have_level.then_some(level))
8385 }
8386
8387 fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8388 // FOR is a v6.1.2-reserved keyword (Token::For). The
8389 // other two are bare idents — they've never needed lexer
8390 // support and we keep it that way.
8391 if !matches!(self.peek(), Token::For) {
8392 return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8393 }
8394 self.advance();
8395 self.expect_keyword_ident("wal")?;
8396 self.expect_keyword_ident("position")?;
8397 let pos = self.expect_u64_literal()?;
8398 let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8399 {
8400 self.advance();
8401 self.expect_keyword_ident("timeout")?;
8402 Some(self.expect_u64_literal()?)
8403 } else {
8404 None
8405 };
8406 Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8407 }
8408
8409 /// v6.1.7 helper — consume a `Token::Integer` and check it
8410 /// fits `u64`. WAL positions and millisecond timeouts are
8411 /// non-negative.
8412 fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8413 match self.advance() {
8414 Token::Integer(n) if n >= 0 => Ok(n as u64),
8415 Token::Integer(n) => Err(ParseError {
8416 message: format!("expected non-negative integer, got {n}"),
8417 token_pos: self.consumed_pos(),
8418 }),
8419 other => Err(ParseError {
8420 message: format!("expected integer literal, got {other:?}"),
8421 token_pos: self.consumed_pos(),
8422 }),
8423 }
8424 }
8425
8426 /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8427 /// ROLE '<role>' (defaults to readonly). All string slots accept
8428 /// either a quoted ident or a quoted string literal.
8429 /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8430 /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8431 ///
8432 /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8433 /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8434 /// wire role) still parses — it is a different axis from the PG attributes.
8435 /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8436 /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8437 /// or RESET, so the plain attribute forms keep their old path.
8438 fn peeks_db_role_setting(&self) -> bool {
8439 let mut i = self.pos + 1; // past the object's name
8440 let word = |p: usize| -> Option<String> {
8441 match self.tokens.get(p) {
8442 Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8443 Some(Token::In) => Some(String::from("in")),
8444 _ => None,
8445 }
8446 };
8447 if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8448 i += 3; // IN DATABASE <name>
8449 }
8450 matches!(word(i).as_deref(), Some("set" | "reset"))
8451 }
8452
8453 fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8454 use crate::ast::SetDbRoleSettingStatement;
8455 // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8456 // identifier, so the ordinary name reader refuses it. Same trap
8457 // as TABLE / INDEX / FULL / DEFAULT before it.
8458 let name = if matches!(self.peek(), Token::All) {
8459 self.advance();
8460 String::from("all")
8461 } else {
8462 self.expect_ident_or_string()?
8463 };
8464 // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8465 let all = name.eq_ignore_ascii_case("all");
8466 let (mut database, mut role) = if is_database {
8467 (Some(name), None)
8468 } else if all {
8469 (None, None)
8470 } else {
8471 (None, Some(name))
8472 };
8473 if matches!(self.peek(), Token::In) {
8474 self.advance();
8475 self.advance(); // DATABASE
8476 database = Some(self.expect_ident_or_string()?);
8477 }
8478 let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8479 self.advance(); // SET | RESET
8480 if resetting && matches!(self.peek(), Token::All) {
8481 self.advance();
8482 self.consume_until_statement_boundary();
8483 return Ok(Statement::SetDbRoleSetting(Box::new(
8484 SetDbRoleSettingStatement {
8485 database,
8486 role,
8487 param: None,
8488 value: None,
8489 },
8490 )));
8491 }
8492 let param = self.expect_ident_like()?;
8493 let value = if resetting {
8494 None
8495 } else {
8496 // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8497 // KEYWORD, so the ident-only check missed it and consumed
8498 // the word itself as the value — the same trap as ALL, one
8499 // clause over.
8500 if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8501 self.advance();
8502 }
8503 Some(self.take_guc_value())
8504 };
8505 self.consume_until_statement_boundary();
8506 Ok(Statement::SetDbRoleSetting(Box::new(
8507 SetDbRoleSettingStatement {
8508 database,
8509 role,
8510 param: Some(param),
8511 value,
8512 },
8513 )))
8514 }
8515
8516 /// The remainder of a `SET <p> = …` clause as PG renders it back:
8517 /// a quoted literal loses its quotes, a bare word or number does not.
8518 fn take_guc_value(&mut self) -> String {
8519 match self.advance() {
8520 Token::String(s) => s,
8521 Token::Integer(n) => format!("{n}"),
8522 Token::Float(f) => format!("{f}"),
8523 Token::Ident(s) | Token::QuotedIdent(s) => s,
8524 other => format!("{other:?}"),
8525 }
8526 }
8527
8528 fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8529 let name = self.expect_ident_or_string()?;
8530 if self.peek_keyword_ident("with") {
8531 self.advance();
8532 }
8533 let mut password = String::new();
8534 let mut role = String::new();
8535 let mut login: Option<bool> = None;
8536 let mut inherit: Option<bool> = None;
8537 let mut superuser: Option<bool> = None;
8538 // Not a `while let`: the pattern would borrow `self` across the
8539 // body, which calls `self.advance()` / `self.expect_*` (&mut).
8540 #[allow(clippy::while_let_loop)]
8541 loop {
8542 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8543 break;
8544 };
8545 match w.to_ascii_lowercase().as_str() {
8546 "password" => {
8547 self.advance();
8548 password = self.expect_string_literal()?;
8549 }
8550 // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8551 // is the same slot.
8552 "encrypted" => {
8553 self.advance();
8554 self.expect_keyword_ident("password")?;
8555 password = self.expect_string_literal()?;
8556 }
8557 "login" => {
8558 self.advance();
8559 login = Some(true);
8560 }
8561 "nologin" => {
8562 self.advance();
8563 login = Some(false);
8564 }
8565 "inherit" => {
8566 self.advance();
8567 inherit = Some(true);
8568 }
8569 "noinherit" => {
8570 self.advance();
8571 inherit = Some(false);
8572 }
8573 "superuser" => {
8574 self.advance();
8575 superuser = Some(true);
8576 }
8577 "nosuperuser" => {
8578 self.advance();
8579 superuser = Some(false);
8580 }
8581 // SPG's own coarse wire role: `ROLE 'readwrite'`.
8582 "role" => {
8583 self.advance();
8584 role = self.expect_string_literal()?;
8585 }
8586 // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8587 // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8588 // accepted and ignored so a pg_dump role block restores. They
8589 // gate capabilities SPG does not have.
8590 "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8591 | "noreplication" | "bypassrls" | "nobypassrls" => {
8592 self.advance();
8593 }
8594 "connection" => {
8595 self.advance();
8596 self.expect_keyword_ident("limit")?;
8597 self.advance(); // the number
8598 }
8599 "valid" => {
8600 self.advance();
8601 self.expect_keyword_ident("until")?;
8602 self.expect_string_literal()?;
8603 }
8604 _ => break,
8605 }
8606 }
8607 if role.is_empty() {
8608 role = "readonly".to_string();
8609 }
8610 Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8611 name,
8612 password,
8613 role,
8614 login,
8615 inherit,
8616 superuser,
8617 is_user,
8618 }))
8619 }
8620
8621 /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8622 /// consumed the USING / WITH CHECK keyword.
8623 fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8624 if !matches!(self.peek(), Token::LParen) {
8625 return Err(self.err(alloc::format!(
8626 "expected '(' after {clause}, got {:?}",
8627 self.peek()
8628 )));
8629 }
8630 self.advance();
8631 let e = self.parse_expr(0)?;
8632 if !matches!(self.peek(), Token::RParen) {
8633 return Err(self.err(alloc::format!(
8634 "expected ')' to close {clause}, got {:?}",
8635 self.peek()
8636 )));
8637 }
8638 self.advance();
8639 Ok(e)
8640 }
8641
8642 /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8643 fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8644 let mut roles = Vec::new();
8645 loop {
8646 roles.push(self.expect_ident_like()?);
8647 if matches!(self.peek(), Token::Comma) {
8648 self.advance();
8649 } else {
8650 break;
8651 }
8652 }
8653 Ok(roles)
8654 }
8655
8656 /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8657 /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8658 /// `CREATE POLICY`.
8659 fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8660 use crate::ast::PolicyCmd;
8661 let name = self.expect_ident_like()?;
8662 if !matches!(self.peek(), Token::On) {
8663 return Err(self.err(alloc::format!(
8664 "expected ON after CREATE POLICY name, got {:?}",
8665 self.peek()
8666 )));
8667 }
8668 self.advance();
8669 let table = self.expect_ident_like()?;
8670
8671 let mut permissive = true;
8672 if matches!(self.peek(), Token::As) {
8673 self.advance();
8674 let w = self.expect_ident_like()?;
8675 permissive = if w.eq_ignore_ascii_case("permissive") {
8676 true
8677 } else if w.eq_ignore_ascii_case("restrictive") {
8678 false
8679 } else {
8680 return Err(self.err(alloc::format!(
8681 "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8682 )));
8683 };
8684 }
8685
8686 let mut cmd = PolicyCmd::All;
8687 if matches!(self.peek(), Token::For) {
8688 self.advance();
8689 cmd = self.parse_policy_cmd()?;
8690 }
8691
8692 let mut roles = Vec::new();
8693 if matches!(self.peek(), Token::To) {
8694 self.advance();
8695 roles = self.parse_policy_roles()?;
8696 }
8697
8698 let mut using = None;
8699 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8700 {
8701 self.advance();
8702 using = Some(self.parse_paren_expr("USING")?);
8703 }
8704
8705 let mut with_check = None;
8706 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8707 {
8708 self.advance();
8709 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8710 {
8711 return Err(self.err(alloc::format!(
8712 "expected CHECK after WITH, got {:?}",
8713 self.peek()
8714 )));
8715 }
8716 self.advance();
8717 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8718 }
8719
8720 // Clause-per-command matrix (PG wording).
8721 match cmd {
8722 PolicyCmd::Insert => {
8723 if using.is_some() {
8724 return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8725 }
8726 }
8727 PolicyCmd::Select | PolicyCmd::Delete => {
8728 if with_check.is_some() {
8729 return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8730 }
8731 }
8732 PolicyCmd::Update | PolicyCmd::All => {}
8733 }
8734
8735 Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8736 name,
8737 table,
8738 permissive,
8739 cmd,
8740 roles,
8741 using,
8742 with_check,
8743 }))
8744 }
8745
8746 /// v7.39 (RLS) — the command word after `FOR`.
8747 fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8748 use crate::ast::PolicyCmd;
8749 match self.peek().clone() {
8750 Token::All => {
8751 self.advance();
8752 Ok(PolicyCmd::All)
8753 }
8754 Token::Select => {
8755 self.advance();
8756 Ok(PolicyCmd::Select)
8757 }
8758 Token::Insert => {
8759 self.advance();
8760 Ok(PolicyCmd::Insert)
8761 }
8762 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8763 self.advance();
8764 Ok(PolicyCmd::Update)
8765 }
8766 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8767 self.advance();
8768 Ok(PolicyCmd::Delete)
8769 }
8770 other => Err(self.err(alloc::format!(
8771 "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8772 ))),
8773 }
8774 }
8775
8776 /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8777 /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8778 fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8779 let name = self.expect_ident_like()?;
8780 if !matches!(self.peek(), Token::On) {
8781 return Err(self.err(alloc::format!(
8782 "expected ON after ALTER POLICY name, got {:?}",
8783 self.peek()
8784 )));
8785 }
8786 self.advance();
8787 let table = self.expect_ident_like()?;
8788
8789 // RENAME TO new
8790 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8791 {
8792 self.advance();
8793 if !matches!(self.peek(), Token::To) {
8794 return Err(self.err(alloc::format!(
8795 "expected TO after RENAME, got {:?}",
8796 self.peek()
8797 )));
8798 }
8799 self.advance();
8800 let new = self.expect_ident_like()?;
8801 return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8802 name,
8803 table,
8804 rename_to: Some(new),
8805 roles: None,
8806 using: None,
8807 with_check: None,
8808 }));
8809 }
8810
8811 let mut roles = None;
8812 if matches!(self.peek(), Token::To) {
8813 self.advance();
8814 roles = Some(self.parse_policy_roles()?);
8815 }
8816 let mut using = None;
8817 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8818 {
8819 self.advance();
8820 using = Some(self.parse_paren_expr("USING")?);
8821 }
8822 let mut with_check = None;
8823 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8824 {
8825 self.advance();
8826 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8827 {
8828 return Err(self.err(alloc::format!(
8829 "expected CHECK after WITH, got {:?}",
8830 self.peek()
8831 )));
8832 }
8833 self.advance();
8834 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8835 }
8836 Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8837 name,
8838 table,
8839 rename_to: None,
8840 roles,
8841 using,
8842 with_check,
8843 }))
8844 }
8845
8846 /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
8847 /// `DROP POLICY`.
8848 fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8849 let if_exists = self.consume_if_exists();
8850 let name = self.expect_ident_like()?;
8851 if !matches!(self.peek(), Token::On) {
8852 return Err(self.err(alloc::format!(
8853 "expected ON after DROP POLICY name, got {:?}",
8854 self.peek()
8855 )));
8856 }
8857 self.advance();
8858 let table = self.expect_ident_like()?;
8859 Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
8860 name,
8861 table,
8862 if_exists,
8863 }))
8864 }
8865}
8866fn wrap_from_leaves(
8867 e: &mut Expr,
8868 names: &[String],
8869 make: &dyn Fn(Expr) -> Expr,
8870 refs: &dyn Fn(&Expr) -> bool,
8871) {
8872 if let Expr::Column(c) = e {
8873 if c.qualifier
8874 .as_deref()
8875 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
8876 {
8877 let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
8878 *e = make(taken);
8879 }
8880 return;
8881 }
8882 match e {
8883 Expr::Binary { lhs, rhs, .. } => {
8884 wrap_from_leaves(lhs, names, make, refs);
8885 wrap_from_leaves(rhs, names, make, refs);
8886 }
8887 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
8888 wrap_from_leaves(expr, names, make, refs)
8889 }
8890 Expr::FunctionCall { args, .. } => {
8891 for a in args.iter_mut() {
8892 wrap_from_leaves(a, names, make, refs);
8893 }
8894 }
8895 Expr::Case {
8896 operand,
8897 branches,
8898 else_branch,
8899 } => {
8900 if let Some(o) = operand.as_deref_mut() {
8901 wrap_from_leaves(o, names, make, refs);
8902 }
8903 for (w, t) in branches.iter_mut() {
8904 wrap_from_leaves(w, names, make, refs);
8905 wrap_from_leaves(t, names, make, refs);
8906 }
8907 if let Some(el) = else_branch.as_deref_mut() {
8908 wrap_from_leaves(el, names, make, refs);
8909 }
8910 }
8911 // Compound variants the walk doesn't decompose: keep the
8912 // pre-D.30 behavior — wrap the whole sub-expr if it touches
8913 // a source table, so nothing regresses.
8914 other => {
8915 if refs(other) {
8916 let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
8917 *other = make(taken);
8918 }
8919 }
8920 }
8921}
8922
8923/// v7.39 (round 241) — does this expression reference any of the FROM /
8924/// USING table names (shared by the UPDATE…FROM and DELETE…USING
8925/// lowerings)?
8926fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
8927 match e {
8928 Expr::Column(c) => c
8929 .qualifier
8930 .as_deref()
8931 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
8932 Expr::Binary { lhs, rhs, .. } => {
8933 expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
8934 }
8935 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
8936 Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
8937 Expr::Case {
8938 operand,
8939 branches,
8940 else_branch,
8941 } => {
8942 operand
8943 .as_deref()
8944 .is_some_and(|o| expr_refs_tables(o, names))
8945 || branches
8946 .iter()
8947 .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
8948 || else_branch
8949 .as_deref()
8950 .is_some_and(|el| expr_refs_tables(el, names))
8951 }
8952 _ => false,
8953 }
8954}
8955
8956impl Parser {
8957 /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
8958 /// Caller already consumed the leading `UPDATE` ident.
8959 /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
8960 /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
8961 /// after the target name has been read. `JOIN` is a reserved token;
8962 /// the qualifiers are bare idents.
8963 fn peek_is_update_join_start(&self) -> bool {
8964 match self.peek() {
8965 // JOIN and its qualifiers are reserved lexer tokens (the grammar
8966 // dedicates arms to `LEFT [OUTER] JOIN` and friends).
8967 Token::Join
8968 | Token::Inner
8969 | Token::Left
8970 | Token::Right
8971 | Token::Cross
8972 | Token::Full => true,
8973 // NATURAL / STRAIGHT_JOIN arrive as bare idents.
8974 Token::Ident(s) | Token::QuotedIdent(s) => {
8975 matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
8976 }
8977 _ => false,
8978 }
8979 }
8980
8981 /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
8982 /// USER-variable assignment. Its own per-session namespace, an arbitrary
8983 /// expression on the right, and `:=` as a second spelling of `=`.
8984 ///
8985 /// Out-of-line (`inline(never)`): the statement-parse frame it is called
8986 /// from sits on the nesting recursion chain (a CTE body, a subquery),
8987 /// and holding this loop's `Vec` + `String` locals there overflowed the
8988 /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
8989 #[inline(never)]
8990 fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
8991 let mut assigns: Vec<(String, Expr)> = Vec::new();
8992 let mut settings: Vec<(String, Expr)> = Vec::new();
8993 loop {
8994 // v7.39 (round 554) — a plain NAME here is a session
8995 // setting, not a user variable. mysqldump writes the two in
8996 // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
8997 // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
8998 // changes it — and this refused the mixture outright, so no
8999 // dump could be restored past its preamble.
9000 if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
9001 self.advance();
9002 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9003 return Err(self.err(alloc::format!(
9004 "expected `=` after {name}, got {:?}",
9005 self.peek()
9006 )));
9007 }
9008 self.advance();
9009 let value = self.parse_expr(0)?;
9010 settings.push((name.to_ascii_lowercase(), value));
9011 if matches!(self.peek(), Token::Comma) {
9012 self.advance();
9013 continue;
9014 }
9015 break;
9016 }
9017 let Token::SessionVar(raw) = self.peek().clone() else {
9018 return Err(self.err(alloc::format!(
9019 "expected a user variable after SET, got {:?}",
9020 self.peek()
9021 )));
9022 };
9023 if raw.starts_with("@@") {
9024 return Err(self.err(alloc::string::String::from(
9025 "cannot mix `@@` settings with `@` user variables in one SET",
9026 )));
9027 }
9028 self.advance();
9029 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9030 return Err(self.err(alloc::format!(
9031 "expected `=` or `:=` after {raw}, got {:?}",
9032 self.peek()
9033 )));
9034 }
9035 self.advance();
9036 let value = self.parse_expr(0)?;
9037 assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
9038 if matches!(self.peek(), Token::Comma) {
9039 self.advance();
9040 continue;
9041 }
9042 break;
9043 }
9044 Ok(Statement::SetUserVars(assigns, settings))
9045 }
9046
9047 fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
9048 // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
9049 // NAMED `only` until now, which failed on `relation "only" does
9050 // not exist`. The lookahead is what keeps a table actually
9051 // called `only` working: the keyword is only a keyword when a
9052 // TABLE NAME follows it — and `SET` arrives as an identifier
9053 // here, so `UPDATE only SET a = 2` would otherwise take `SET`
9054 // for the table and die on the `=`. Measured by the pin.
9055 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9056 if s.eq_ignore_ascii_case("only"))
9057 && matches!(
9058 self.tokens.get(self.pos + 1),
9059 Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
9060 );
9061 if only {
9062 self.advance();
9063 }
9064 let table = self.expect_ident_like()?;
9065 // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
9066 // bare spelling; a bare identifier that is the SET keyword itself
9067 // is the clause, not an alias.
9068 // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
9069 // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
9070 // multi-table form, and swallowing `LEFT` as `a`'s alias made the
9071 // following JOIN a syntax error.
9072 let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
9073 let alias = if matches!(self.peek(), Token::As) {
9074 self.advance();
9075 Some(self.expect_ident_like()?)
9076 } else {
9077 match self.peek() {
9078 Token::Ident(s) | Token::QuotedIdent(s)
9079 if !s.eq_ignore_ascii_case("set") && !starts_join =>
9080 {
9081 let a = s.clone();
9082 self.advance();
9083 Some(a)
9084 }
9085 _ => None,
9086 }
9087 };
9088 // v7.39 (round 420) — MySQL's multi-table UPDATE:
9089 // UPDATE a, b SET a.v = b.v WHERE a.id = b.id
9090 // UPDATE a JOIN b ON a.id = b.id SET a.v = b.v + 1
9091 // UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
9092 // The FIRST table is the mutation target and the rest are sources —
9093 // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
9094 // SPG already lowers onto correlated subqueries. So rewind, let
9095 // `parse_from_clause` read the whole list (it handles aliases, comma
9096 // lists, and every JOIN form), then peel the target off the front.
9097 let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
9098 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9099 {
9100 // NOTE: `advance()` destroys the tokens it returns
9101 // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
9102 // is NOT possible — the tail is read forward, once, through the
9103 // same grammar `parse_from_clause` uses after its primary.
9104 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9105 let mut joins = self.parse_from_joins(&target_qual)?;
9106 if joins.is_empty() {
9107 return Err(self.err(alloc::string::String::from(
9108 "multi-table UPDATE needs at least one source table",
9109 )));
9110 }
9111 let head = joins.remove(0);
9112 // A LEFT join keeps every target row (the unmatched ones see NULL
9113 // on the source side), so it must NOT get the EXISTS row filter
9114 // the inner / comma forms use.
9115 let outer = matches!(head.kind, crate::ast::JoinKind::Left);
9116 let src = FromClause {
9117 primary: head.table,
9118 joins,
9119 };
9120 (Some(src), head.on, outer)
9121 } else {
9122 (None, None, false)
9123 };
9124 self.expect_keyword_ident("set")?;
9125 let mut assignments = Vec::new();
9126 loop {
9127 // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
9128 // …)` — the parenthesized multi-assignment. Expressions
9129 // assign positionally; a subquery RHS clones per column
9130 // keeping only the Nth projection item.
9131 if matches!(self.peek(), Token::LParen) {
9132 self.advance();
9133 let mut cols = alloc::vec![self.expect_ident_like()?];
9134 while matches!(self.peek(), Token::Comma) {
9135 self.advance();
9136 cols.push(self.expect_ident_like()?);
9137 }
9138 if !matches!(self.peek(), Token::RParen) {
9139 return Err(self.err(format!(
9140 "expected ')' after SET column list, got {:?}",
9141 self.peek()
9142 )));
9143 }
9144 self.advance();
9145 if !matches!(self.peek(), Token::Eq) {
9146 return Err(self.err(format!(
9147 "expected `=` after SET column list, got {:?}",
9148 self.peek()
9149 )));
9150 }
9151 self.advance();
9152 if !matches!(self.peek(), Token::LParen) {
9153 return Err(self.err(format!(
9154 "expected '(' after SET (…) =, got {:?}",
9155 self.peek()
9156 )));
9157 }
9158 self.advance();
9159 if matches!(self.peek(), Token::Select) {
9160 let inner = match self.parse_select_stmt()? {
9161 Statement::Select(s) => s,
9162 other => {
9163 return Err(self.err(alloc::format!(
9164 "expected SELECT in SET (…) = (SELECT …), got {other:?}"
9165 )));
9166 }
9167 };
9168 if !matches!(self.peek(), Token::RParen) {
9169 return Err(self.err(format!(
9170 "expected ')' after SET subquery, got {:?}",
9171 self.peek()
9172 )));
9173 }
9174 self.advance();
9175 if inner.items.len() != cols.len() {
9176 return Err(self.err(alloc::format!(
9177 "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9178 cols.len(),
9179 inner.items.len()
9180 )));
9181 }
9182 for (i, col) in cols.into_iter().enumerate() {
9183 let mut sub = inner.clone();
9184 sub.items = alloc::vec![sub.items[i].clone()];
9185 assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9186 }
9187 } else {
9188 let mut exprs = alloc::vec![self.parse_expr(0)?];
9189 while matches!(self.peek(), Token::Comma) {
9190 self.advance();
9191 exprs.push(self.parse_expr(0)?);
9192 }
9193 if !matches!(self.peek(), Token::RParen) {
9194 return Err(self.err(format!(
9195 "expected ')' after SET row values, got {:?}",
9196 self.peek()
9197 )));
9198 }
9199 self.advance();
9200 if exprs.len() != cols.len() {
9201 return Err(self.err(alloc::format!(
9202 "SET (…) = (…) arity mismatch: {} columns, {} values",
9203 cols.len(),
9204 exprs.len()
9205 )));
9206 }
9207 for (col, e) in cols.into_iter().zip(exprs) {
9208 assignments.push((col, e));
9209 }
9210 }
9211 if matches!(self.peek(), Token::Comma) {
9212 self.advance();
9213 continue;
9214 }
9215 break;
9216 }
9217 // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9218 // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9219 // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9220 // `public.` dump qualifiers), so the qualifier has to be read off
9221 // the token stream first — otherwise `SET b.v = 888` would write
9222 // to the TARGET table's `v` while naming a source table, a
9223 // silent-wrong. A qualifier naming a SOURCE table means a
9224 // multi-TARGET update — mutating two tables in one statement —
9225 // which SPG does not model, so it is refused loudly.
9226 let set_qual: Option<String> = if mysql_from.is_some()
9227 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9228 {
9229 match self.peek() {
9230 Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9231 _ => None,
9232 }
9233 } else {
9234 None
9235 };
9236 let col = self.expect_ident_like()?;
9237 if let Some(q) = set_qual {
9238 let names_target = q.eq_ignore_ascii_case(&table)
9239 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9240 if !names_target {
9241 return Err(self.err(alloc::format!(
9242 "multi-table UPDATE can only assign to its first table \
9243 ({table}); `{q}.{col}` targets another table"
9244 )));
9245 }
9246 }
9247 // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9248 // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9249 // `__column_default` marker lowering just below). PG assigns to the
9250 // i-th (1-based) element, NULL-padding when i exceeds the length.
9251 if matches!(self.peek(), Token::LBracket) {
9252 self.advance();
9253 let index = self.parse_expr(0)?;
9254 // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9255 // (and the open `arr[lo:]`), lowered to
9256 // `__array_assign_slice`. Only the single-subscript form
9257 // parsed before, so a slice assignment was a syntax error.
9258 let mut slice_hi: Option<Option<Expr>> = None;
9259 if matches!(self.peek(), Token::Colon) {
9260 self.advance();
9261 slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9262 None
9263 } else {
9264 Some(self.parse_expr(0)?)
9265 });
9266 }
9267 if !matches!(self.peek(), Token::RBracket) {
9268 return Err(self.err(format!(
9269 "expected `]` after array subscript in UPDATE SET, got {:?}",
9270 self.peek()
9271 )));
9272 }
9273 self.advance();
9274 if !matches!(self.peek(), Token::Eq) {
9275 return Err(self.err(format!(
9276 "expected `=` after array subscript in UPDATE SET, got {:?}",
9277 self.peek()
9278 )));
9279 }
9280 self.advance();
9281 let value = self.parse_expr(0)?;
9282 // PG merges several subscript writes to the same column into one
9283 // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9284 // assignment to `col` rather than each overwriting the original.
9285 let existing = assignments.iter().position(|(c, _)| c == &col);
9286 let base = match existing {
9287 Some(i) => assignments[i].1.clone(),
9288 None => Expr::Column(ColumnName {
9289 qualifier: None,
9290 name: col.clone(),
9291 }),
9292 };
9293 let assigned = match slice_hi {
9294 None => Expr::FunctionCall {
9295 name: "__array_assign".to_string(),
9296 args: alloc::vec![base, index, value],
9297 },
9298 Some(hi) => Expr::FunctionCall {
9299 name: "__array_assign_slice".to_string(),
9300 args: alloc::vec![
9301 base,
9302 index,
9303 hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9304 value,
9305 ],
9306 },
9307 };
9308 match existing {
9309 Some(i) => assignments[i].1 = assigned,
9310 None => assignments.push((col, assigned)),
9311 }
9312 if matches!(self.peek(), Token::Comma) {
9313 self.advance();
9314 continue;
9315 }
9316 break;
9317 }
9318 if !matches!(self.peek(), Token::Eq) {
9319 return Err(self.err(format!(
9320 "expected `=` after column name in UPDATE SET, got {:?}",
9321 self.peek()
9322 )));
9323 }
9324 self.advance();
9325 // `SET col = DEFAULT` — the column's declared default;
9326 // rides out as a marker call the update executor
9327 // resolves against the schema.
9328 let value = if matches!(self.peek(), Token::Default) {
9329 self.advance();
9330 Expr::FunctionCall {
9331 name: "__column_default".to_string(),
9332 args: Vec::new(),
9333 }
9334 } else {
9335 self.parse_expr(0)?
9336 };
9337 assignments.push((col, value));
9338 if matches!(self.peek(), Token::Comma) {
9339 self.advance();
9340 continue;
9341 }
9342 break;
9343 }
9344 // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9345 // update. Lowers onto the correlated-subquery machinery:
9346 // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9347 // and each assignment that references a FROM-list table
9348 // wraps into a correlated scalar subquery
9349 // (SELECT expr FROM src WHERE cond). Equivalent for the
9350 // unique-join shape (the overwhelmingly common one); a
9351 // multi-match, which PG resolves by arbitrary pick,
9352 // surfaces as a scalar-subquery cardinality error instead
9353 // of a silent arbitrary result.
9354 // v7.39 (round 420) — the MySQL multi-table form supplies the source
9355 // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9356 // the SAME lowering below. Both spellings together is not legal in
9357 // either dialect.
9358 let from_clause = if let Some(fc) = mysql_from {
9359 if matches!(self.peek(), Token::From) {
9360 return Err(self.err(alloc::string::String::from(
9361 "multi-table UPDATE already names its sources; drop the FROM clause",
9362 )));
9363 }
9364 Some(fc)
9365 } else if matches!(self.peek(), Token::From) {
9366 self.advance();
9367 Some(self.parse_from_clause()?)
9368 } else {
9369 None
9370 };
9371 let where_ = if matches!(self.peek(), Token::Where) {
9372 self.advance();
9373 Some(self.parse_expr(0)?)
9374 } else {
9375 None
9376 };
9377 // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9378 // and the TARGET-row filter are NOT the same predicate once a LEFT
9379 // join is involved:
9380 // * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9381 // one conjunction, and the whole thing filters target rows via
9382 // EXISTS.
9383 // * LEFT join: only the ON predicate belongs inside the source
9384 // subquery. The WHERE still filters TARGET rows (with source
9385 // columns read through the correlated subquery, which yields NULL
9386 // for an unmatched row — exactly LEFT-join semantics).
9387 // Round 420 folded ON into WHERE unconditionally and then dropped the
9388 // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9389 // WHERE a.id > 1` updated EVERY row.
9390 let sub_where = match (mysql_on.clone(), where_.clone()) {
9391 _ if mysql_outer => mysql_on.clone(),
9392 (Some(on), Some(w)) => Some(Expr::Binary {
9393 lhs: Box::new(on),
9394 op: crate::ast::BinOp::And,
9395 rhs: Box::new(w),
9396 }),
9397 (Some(on), None) => Some(on),
9398 (None, w) => w,
9399 };
9400 // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9401 // has no such clause on UPDATE, so this is accepted only under the
9402 // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9403 let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9404 let mut returning = self.parse_optional_returning()?;
9405 // v7.39 (round 533) — kept for the engine, which can resolve the
9406 // UNQUALIFIED leaves this lowering has to leave alone.
9407 let from_sources = from_clause.as_ref().map(|fc| {
9408 alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9409 from: fc.clone(),
9410 sub_where: sub_where.clone(),
9411 })
9412 });
9413 let (assignments, where_) = if let Some(fc) = from_clause {
9414 let names: Vec<String> = core::iter::once(&fc.primary)
9415 .chain(fc.joins.iter().map(|j| &j.table))
9416 .flat_map(|t| {
9417 t.alias
9418 .clone()
9419 .into_iter()
9420 .chain(core::iter::once(t.name.clone()))
9421 })
9422 .collect();
9423 let refs_list = |e: &Expr| -> bool {
9424 fn walk(e: &Expr, names: &[String]) -> bool {
9425 match e {
9426 Expr::Column(c) => c
9427 .qualifier
9428 .as_deref()
9429 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9430 Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9431 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9432 Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9433 Expr::Case {
9434 operand,
9435 branches,
9436 else_branch,
9437 } => {
9438 operand.as_deref().is_some_and(|o| walk(o, names))
9439 || branches
9440 .iter()
9441 .any(|(w, t)| walk(w, names) || walk(t, names))
9442 || else_branch.as_deref().is_some_and(|el| walk(el, names))
9443 }
9444 _ => false,
9445 }
9446 }
9447 walk(e, &names)
9448 };
9449 let sub_select = |items: Vec<SelectItem>| SelectStatement {
9450 locking: None,
9451 ctes: Vec::new(),
9452 distinct: false,
9453 distinct_on: Vec::new(),
9454 items,
9455 from: Some(fc.clone()),
9456 where_: sub_where.clone(),
9457 group_by: None,
9458 group_by_all: false,
9459 having: None,
9460 unions: Vec::new(),
9461 order_by: Vec::new(),
9462 limit: None,
9463 offset: None,
9464 limit_with_ties: false,
9465 window_check_exprs: Vec::new(),
9466 };
9467 // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9468 // assignment RHS with a correlated scalar subquery, instead of
9469 // wrapping the whole RHS. Wrapping the whole expr moved a target-
9470 // column reference (`SET v = v + u.bonus`, where `v` is the target
9471 // table's column) inside a subquery whose FROM only has the source
9472 // table, so the unqualified `v` resolved against the source and
9473 // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9474 // context — where they belong — fixes it; only the source columns
9475 // (`u.bonus`) become subqueries. A whole-expr fallback covers
9476 // compound variants the leaf-walk doesn't decompose.
9477 let make_subq = |inner: Expr| {
9478 Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9479 expr: inner,
9480 alias: None,
9481 }])))
9482 };
9483 let assignments = assignments
9484 .into_iter()
9485 .map(|(col, mut expr)| {
9486 wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9487 (col, expr)
9488 })
9489 .collect();
9490 let exists = Expr::Exists {
9491 subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9492 expr: Expr::Literal(Literal::Integer(1)),
9493 alias: None,
9494 }])),
9495 negated: false,
9496 };
9497 // v7.39 (round 241) — RETURNING may reference the FROM-list
9498 // tables too (`RETURNING emp.id, dept.name`); the same
9499 // leaf-to-correlated-subquery lowering the assignments get.
9500 // Without it the qualifier died at eval with "unknown table
9501 // qualifier". (RETURNING was parsed before this block — the
9502 // lowering is a pure AST transformation.)
9503 if let Some(items) = returning.as_mut() {
9504 for item in items.iter_mut() {
9505 if let SelectItem::Expr { expr, .. } = item {
9506 wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9507 }
9508 }
9509 }
9510 // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9511 // EVERY matching target row: it gets no EXISTS filter, but the
9512 // caller's WHERE still applies, with source columns read through
9513 // the correlated subquery (NULL when unmatched — LEFT-join
9514 // semantics). `sub_where` above already excluded the WHERE from
9515 // the source subquery for this case.
9516 if mysql_outer {
9517 let mut outer = where_;
9518 if let Some(w) = outer.as_mut() {
9519 wrap_from_leaves(w, &names, &make_subq, &refs_list);
9520 }
9521 (assignments, outer)
9522 } else {
9523 (assignments, Some(exists))
9524 }
9525 } else {
9526 (assignments, where_)
9527 };
9528 Ok(Statement::Update(crate::ast::UpdateStatement {
9529 ctes: Vec::new(),
9530 table,
9531 only,
9532 alias,
9533 assignments,
9534 from_sources,
9535 where_,
9536 order_limit: update_order_limit,
9537 returning,
9538 }))
9539 }
9540
9541 /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9542 /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9543 /// clause and its meaning are identical, so both call this rather than
9544 /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9545 /// legal. PG has no such clause on either statement, so it is read only
9546 /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9547 /// errors.
9548 ///
9549 /// `#[inline(never)]`: its locals would otherwise land on the statement-
9550 /// parsing recursion frame, which is what tipped the 512 KiB nesting
9551 /// stack in round 430.
9552 #[inline(never)]
9553 fn parse_mysql_dml_order_limit(
9554 &mut self,
9555 what: &str,
9556 ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9557 if !self.mysql_dialect {
9558 return Ok(None);
9559 }
9560 let order_by = self.parse_order_by_keys()?;
9561 let limit = if matches!(self.peek(), Token::Limit) {
9562 self.advance();
9563 let tok = self.advance();
9564 let Token::Integer(n) = tok else {
9565 return Err(self.err(alloc::format!(
9566 "expected integer after {what} LIMIT, got {tok:?}"
9567 )));
9568 };
9569 // MySQL rejects the `LIMIT offset, count` form here — only a
9570 // single row count is legal on a DML statement.
9571 if matches!(self.peek(), Token::Comma) {
9572 return Err(self.err(alloc::format!(
9573 "{what} LIMIT takes a row count, not an offset"
9574 )));
9575 }
9576 let n = u32::try_from(n)
9577 .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9578 Some(n)
9579 } else {
9580 None
9581 };
9582 if order_by.is_empty() && limit.is_none() {
9583 return Ok(None);
9584 }
9585 Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9586 order_by,
9587 limit,
9588 })))
9589 }
9590
9591 /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9592 /// the leading `DELETE` ident.
9593 fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9594 // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9595 // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9596 // USING a, b WHERE …` — the third MySQL spelling — needs no special
9597 // parse here; it reaches the existing USING path with the target
9598 // repeated in the list, which the source-list peel below handles.)
9599 // More than one name is a multi-TARGET delete, which SPG does not
9600 // model; it is refused rather than half-applied.
9601 let mysql_pre_target: Option<String> =
9602 if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9603 let first = self.expect_ident_like()?;
9604 if matches!(self.peek(), Token::Comma) {
9605 return Err(self.err(alloc::format!(
9606 "multi-table DELETE can only delete from one table; \
9607 `DELETE {first}, …` names several"
9608 )));
9609 }
9610 Some(first)
9611 } else {
9612 None
9613 };
9614 if !matches!(self.peek(), Token::From) {
9615 return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9616 }
9617 self.advance();
9618 // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9619 // lookahead as the UPDATE spelling.
9620 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9621 if s.eq_ignore_ascii_case("only"))
9622 && matches!(
9623 self.tokens.get(self.pos + 1),
9624 Some(Token::Ident(_) | Token::QuotedIdent(_))
9625 );
9626 if only {
9627 self.advance();
9628 }
9629 let table = self.expect_ident_like()?;
9630 // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9631 // spelling must not swallow the clause keywords that can follow
9632 // the target.
9633 let alias = if matches!(self.peek(), Token::As) {
9634 self.advance();
9635 Some(self.expect_ident_like()?)
9636 } else {
9637 match self.peek() {
9638 Token::Ident(s) | Token::QuotedIdent(s)
9639 if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9640 {
9641 let a = s.clone();
9642 self.advance();
9643 Some(a)
9644 }
9645 _ => None,
9646 }
9647 };
9648 // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9649 // through the SAME join grammar the FROM clause uses (see the
9650 // `advance()`-destroys-tokens note on `parse_from_joins`).
9651 let mut mysql_on: Option<Expr> = None;
9652 let mut mysql_outer = false;
9653 let mysql_using = if mysql_pre_target.is_some()
9654 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9655 {
9656 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9657 let mut joins = self.parse_from_joins(&target_qual)?;
9658 if joins.is_empty() {
9659 return Err(self.err(alloc::string::String::from(
9660 "multi-table DELETE needs at least one source table",
9661 )));
9662 }
9663 let head = joins.remove(0);
9664 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9665 mysql_on = head.on;
9666 Some(FromClause {
9667 primary: head.table,
9668 joins,
9669 })
9670 } else {
9671 None
9672 };
9673 // The pre-FROM target must be the table the FROM names (or its
9674 // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9675 // is not the scan target.
9676 if let Some(t) = &mysql_pre_target {
9677 let names_target = t.eq_ignore_ascii_case(&table)
9678 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9679 if !names_target {
9680 return Err(self.err(alloc::format!(
9681 "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9682 )));
9683 }
9684 }
9685 // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9686 // delete. Same lowering as UPDATE … FROM: the WHERE
9687 // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9688 // target row by the correlated machinery.
9689 let using_clause = if let Some(fc) = mysql_using {
9690 Some(fc)
9691 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9692 self.advance();
9693 let mut fc = self.parse_from_clause()?;
9694 // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9695 // repeats the TARGET as the first USING entry (PG's spelling
9696 // lists only the extra sources). Peel it so the source subquery
9697 // does not re-scan — and shadow — the target table.
9698 let primary_is_target =
9699 fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9700 if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9701 let head = fc.joins.remove(0);
9702 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9703 mysql_on = head.on;
9704 fc = FromClause {
9705 primary: head.table,
9706 joins: fc.joins,
9707 };
9708 }
9709 Some(fc)
9710 } else {
9711 None
9712 };
9713 let where_ = if matches!(self.peek(), Token::Where) {
9714 self.advance();
9715 Some(self.parse_expr(0)?)
9716 } else {
9717 None
9718 };
9719 // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9720 // read before RETURNING (MariaDB's own extension trails the LIMIT).
9721 let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9722 let mut returning = self.parse_optional_returning()?;
9723 let where_ = if let Some(fc) = using_clause {
9724 // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9725 // a USING-table reference in RETURNING becomes a correlated
9726 // scalar subquery over the USING list.
9727 let names: Vec<String> = core::iter::once(&fc.primary)
9728 .chain(fc.joins.iter().map(|j| &j.table))
9729 .flat_map(|t| {
9730 t.alias
9731 .clone()
9732 .into_iter()
9733 .chain(core::iter::once(t.name.clone()))
9734 })
9735 .collect();
9736 // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9737 // join filters the SOURCE subquery on the ON predicate alone and
9738 // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9739 // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9740 // rows); every other form folds ON and WHERE into one EXISTS.
9741 let sub_where = match (mysql_on.clone(), where_.clone()) {
9742 _ if mysql_outer => mysql_on.clone(),
9743 (Some(on), Some(w)) => Some(Expr::Binary {
9744 lhs: Box::new(on),
9745 op: crate::ast::BinOp::And,
9746 rhs: Box::new(w),
9747 }),
9748 (Some(on), None) => Some(on),
9749 (None, w) => w,
9750 };
9751 let exists_where = sub_where.clone();
9752 let sub_fc = fc.clone();
9753 let make_subq = move |leaf: Expr| -> Expr {
9754 Expr::ScalarSubquery(Box::new(SelectStatement {
9755 locking: None,
9756 ctes: Vec::new(),
9757 distinct: false,
9758 distinct_on: Vec::new(),
9759 items: alloc::vec![SelectItem::Expr {
9760 expr: leaf,
9761 alias: None,
9762 }],
9763 from: Some(sub_fc.clone()),
9764 where_: sub_where.clone(),
9765 group_by: None,
9766 group_by_all: false,
9767 having: None,
9768 unions: Vec::new(),
9769 order_by: Vec::new(),
9770 limit: None,
9771 offset: None,
9772 limit_with_ties: false,
9773 window_check_exprs: Vec::new(),
9774 }))
9775 };
9776 let refs = |e: &Expr| expr_refs_tables(e, &names);
9777 if let Some(items) = returning.as_mut() {
9778 for item in items.iter_mut() {
9779 if let SelectItem::Expr { expr, .. } = item {
9780 wrap_from_leaves(expr, &names, &make_subq, &refs);
9781 }
9782 }
9783 }
9784 // A LEFT join deletes the target rows the WHERE selects, reading
9785 // source columns through the correlated subquery (NULL when
9786 // unmatched); no EXISTS row filter.
9787 if mysql_outer {
9788 let mut outer = where_;
9789 if let Some(w) = outer.as_mut() {
9790 wrap_from_leaves(w, &names, &make_subq, &refs);
9791 }
9792 outer
9793 } else {
9794 Some(Expr::Exists {
9795 subquery: Box::new(SelectStatement {
9796 locking: None,
9797 ctes: Vec::new(),
9798 distinct: false,
9799 distinct_on: Vec::new(),
9800 items: alloc::vec![SelectItem::Expr {
9801 expr: Expr::Literal(Literal::Integer(1)),
9802 alias: None,
9803 }],
9804 from: Some(fc),
9805 where_: exists_where,
9806 group_by: None,
9807 group_by_all: false,
9808 having: None,
9809 unions: Vec::new(),
9810 order_by: Vec::new(),
9811 limit: None,
9812 offset: None,
9813 limit_with_ties: false,
9814 window_check_exprs: Vec::new(),
9815 }),
9816 negated: false,
9817 })
9818 }
9819 } else {
9820 where_
9821 };
9822 Ok(Statement::Delete(crate::ast::DeleteStatement {
9823 ctes: Vec::new(),
9824 table,
9825 only,
9826 alias,
9827 where_,
9828 order_limit: delete_order_limit,
9829 returning,
9830 }))
9831 }
9832
9833 /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
9834 /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
9835 /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
9836 /// keyword. v7.17 surface:
9837 /// * source: table reference (subquery source is a follow-up)
9838 /// * actions: UPDATE SET / DELETE / DO NOTHING (matched);
9839 /// INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
9840 /// * AND-conditioned WHEN clauses; clauses tried in declaration
9841 /// order
9842 fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
9843 // INTO
9844 let is_into_kw = matches!(self.peek(), Token::Into)
9845 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
9846 if !is_into_kw {
9847 return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
9848 }
9849 self.advance();
9850 let target = self.expect_ident_like()?;
9851 // Optional alias — bare ident before USING.
9852 let target_alias = match self.peek() {
9853 Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
9854 Some(self.expect_ident_like()?)
9855 }
9856 _ => None,
9857 };
9858 // USING
9859 let is_using_kw = matches!(
9860 self.peek(),
9861 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
9862 );
9863 if !is_using_kw {
9864 return Err(self.err(format!(
9865 "expected USING after MERGE INTO target, got {:?}",
9866 self.peek()
9867 )));
9868 }
9869 self.advance();
9870 // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
9871 // <table> [alias]`. PG requires an alias after a subquery source.
9872 let (source, source_select) = if matches!(self.peek(), Token::LParen) {
9873 self.advance(); // (
9874 // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
9875 // constant-SELECT lowering the derived-table parser uses
9876 // (PG deletes through this form; it was a parse error).
9877 let inner = if matches!(self.peek(), Token::Values) {
9878 self.advance(); // VALUES
9879 Statement::Select(self.parse_values_rows_body()?)
9880 } else {
9881 self.parse_select_stmt()?
9882 };
9883 match self.advance() {
9884 Token::RParen => {}
9885 other => {
9886 return Err(self.err(format!(
9887 "expected ')' after MERGE USING subquery, got {other:?}"
9888 )));
9889 }
9890 }
9891 let Statement::Select(sub) = inner else {
9892 return Err(self.err("MERGE USING subquery must be a SELECT".into()));
9893 };
9894 (String::new(), Some(Box::new(sub)))
9895 } else {
9896 (self.expect_ident_like()?, None)
9897 };
9898 let source_alias = match self.peek() {
9899 Token::Ident(s) | Token::QuotedIdent(s)
9900 if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
9901 {
9902 Some(self.expect_ident_like()?)
9903 }
9904 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
9905 self.advance(); // AS
9906 Some(self.expect_ident_like()?)
9907 }
9908 _ => None,
9909 };
9910 // v7.39 (round 768, F31-D5) — optional positional column-alias
9911 // list after the source alias (`s(id, v)`).
9912 let mut source_column_aliases: Vec<String> = Vec::new();
9913 if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
9914 self.advance();
9915 loop {
9916 source_column_aliases.push(self.expect_ident_like()?);
9917 match self.peek() {
9918 Token::Comma => {
9919 self.advance();
9920 }
9921 Token::RParen => {
9922 self.advance();
9923 break;
9924 }
9925 other => {
9926 return Err(self.err(format!(
9927 "expected ',' or ')' in MERGE source column list, got {other:?}"
9928 )));
9929 }
9930 }
9931 }
9932 }
9933 if source_select.is_some() && source_alias.is_none() {
9934 return Err(self.err("MERGE USING (subquery) requires an alias".into()));
9935 }
9936 // ON
9937 if !matches!(self.peek(), Token::On) {
9938 return Err(self.err(format!(
9939 "expected ON after MERGE … USING source, got {:?}",
9940 self.peek()
9941 )));
9942 }
9943 self.advance();
9944 let on = self.parse_expr(0)?;
9945 // One or more WHEN clauses.
9946 let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
9947 loop {
9948 let is_when_kw = matches!(
9949 self.peek(),
9950 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
9951 );
9952 if !is_when_kw {
9953 break;
9954 }
9955 self.advance(); // WHEN
9956 // [NOT] MATCHED
9957 let matched = if matches!(self.peek(), Token::Not) {
9958 self.advance();
9959 crate::ast::MergeMatched::NotMatched
9960 } else {
9961 crate::ast::MergeMatched::Matched
9962 };
9963 let is_matched_kw = matches!(
9964 self.peek(),
9965 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
9966 );
9967 if !is_matched_kw {
9968 return Err(self.err(format!(
9969 "expected MATCHED in WHEN clause, got {:?}",
9970 self.peek()
9971 )));
9972 }
9973 self.advance();
9974 // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
9975 // BY TARGET is the default (a synonym); BY SOURCE flips the clause
9976 // to fire for target rows no source row matches.
9977 let mut matched = matched;
9978 if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
9979 self.advance();
9980 match self.peek() {
9981 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
9982 self.advance();
9983 matched = crate::ast::MergeMatched::NotMatchedBySource;
9984 }
9985 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
9986 self.advance();
9987 }
9988 other => {
9989 return Err(self.err(format!(
9990 "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
9991 )));
9992 }
9993 }
9994 }
9995 // Optional AND <expr>
9996 let condition = if matches!(self.peek(), Token::And) {
9997 self.advance();
9998 Some(self.parse_expr(0)?)
9999 } else {
10000 None
10001 };
10002 // THEN
10003 let is_then_kw = matches!(
10004 self.peek(),
10005 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
10006 );
10007 if !is_then_kw {
10008 return Err(self.err(format!(
10009 "expected THEN in WHEN clause, got {:?}",
10010 self.peek()
10011 )));
10012 }
10013 self.advance();
10014 // Action: INSERT / UPDATE / DELETE / DO NOTHING
10015 let action = match self.peek().clone() {
10016 Token::Insert => {
10017 self.advance();
10018 // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
10019 // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
10020 // VALUES (…)` omits it and fills every column in declaration
10021 // order. PG accepts this; SPG used to require the list.
10022 let mut columns: Vec<String> = Vec::new();
10023 if matches!(self.peek(), Token::LParen) {
10024 self.advance();
10025 loop {
10026 columns.push(self.expect_ident_like()?);
10027 if matches!(self.peek(), Token::Comma) {
10028 self.advance();
10029 continue;
10030 }
10031 break;
10032 }
10033 if !matches!(self.peek(), Token::RParen) {
10034 return Err(self.err(format!(
10035 "expected ')' after INSERT column list, got {:?}",
10036 self.peek()
10037 )));
10038 }
10039 self.advance();
10040 }
10041 // VALUES (...)
10042 if !matches!(self.peek(), Token::Values) {
10043 return Err(self.err(format!(
10044 "expected VALUES in MERGE INSERT, got {:?}",
10045 self.peek()
10046 )));
10047 }
10048 self.advance();
10049 if !matches!(self.peek(), Token::LParen) {
10050 return Err(self.err(format!(
10051 "expected '(' after VALUES in MERGE INSERT, got {:?}",
10052 self.peek()
10053 )));
10054 }
10055 self.advance();
10056 let mut values: Vec<crate::ast::Expr> = Vec::new();
10057 loop {
10058 values.push(self.parse_expr(0)?);
10059 if matches!(self.peek(), Token::Comma) {
10060 self.advance();
10061 continue;
10062 }
10063 break;
10064 }
10065 if !matches!(self.peek(), Token::RParen) {
10066 return Err(self.err(format!(
10067 "expected ')' after MERGE INSERT values, got {:?}",
10068 self.peek()
10069 )));
10070 }
10071 self.advance();
10072 // Empty column list = positional into every column, so the
10073 // count is checked against the table arity at execution.
10074 if !columns.is_empty() && columns.len() != values.len() {
10075 return Err(self.err(format!(
10076 "MERGE INSERT column count ({}) ≠ value count ({})",
10077 columns.len(),
10078 values.len()
10079 )));
10080 }
10081 crate::ast::MergeAction::Insert { columns, values }
10082 }
10083 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
10084 self.advance();
10085 // SET
10086 let is_set_kw = matches!(
10087 self.peek(),
10088 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
10089 );
10090 if !is_set_kw {
10091 return Err(self.err(format!(
10092 "expected SET after UPDATE in MERGE, got {:?}",
10093 self.peek()
10094 )));
10095 }
10096 self.advance();
10097 let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
10098 loop {
10099 let col = self.expect_ident_like()?;
10100 if !matches!(self.peek(), Token::Eq) {
10101 return Err(self.err(format!(
10102 "expected '=' in MERGE UPDATE assignment, got {:?}",
10103 self.peek()
10104 )));
10105 }
10106 self.advance();
10107 let expr = self.parse_expr(0)?;
10108 assignments.push((col, expr));
10109 if matches!(self.peek(), Token::Comma) {
10110 self.advance();
10111 continue;
10112 }
10113 break;
10114 }
10115 crate::ast::MergeAction::Update { assignments }
10116 }
10117 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
10118 self.advance();
10119 crate::ast::MergeAction::Delete
10120 }
10121 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
10122 self.advance();
10123 let is_nothing_kw = matches!(
10124 self.peek(),
10125 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
10126 );
10127 if !is_nothing_kw {
10128 return Err(self.err(format!(
10129 "expected NOTHING after DO in MERGE clause, got {:?}",
10130 self.peek()
10131 )));
10132 }
10133 self.advance();
10134 crate::ast::MergeAction::DoNothing
10135 }
10136 other => {
10137 return Err(self.err(format!(
10138 "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
10139 )));
10140 }
10141 };
10142 // PG's grammar simply has no INSERT production under BY SOURCE
10143 // (a target row already exists there) — same syntax error.
10144 if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
10145 && matches!(action, crate::ast::MergeAction::Insert { .. })
10146 {
10147 return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
10148 }
10149 clauses.push(crate::ast::MergeWhenClause {
10150 matched,
10151 condition,
10152 action,
10153 });
10154 }
10155 if clauses.is_empty() {
10156 return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
10157 }
10158 // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
10159 // unconditional (no `AND`) WHEN of the same match kind: it could
10160 // never fire. Check per match kind in clause order.
10161 let mut seen_unconditional_matched = false;
10162 let mut seen_unconditional_not_matched = false;
10163 let mut seen_unconditional_by_source = false;
10164 for c in &clauses {
10165 let seen = match c.matched {
10166 crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10167 crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10168 crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10169 };
10170 if *seen {
10171 return Err(self.err(String::from(
10172 "unreachable WHEN clause specified after unconditional WHEN clause",
10173 )));
10174 }
10175 if c.condition.is_none() {
10176 *seen = true;
10177 }
10178 }
10179 // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10180 let returning = self.parse_optional_returning()?;
10181 Ok(Statement::Merge(crate::ast::MergeStatement {
10182 // Attached by `parse_with_cte_then_select` when the MERGE
10183 // heads a WITH clause (round 149).
10184 ctes: Vec::new(),
10185 target,
10186 target_alias,
10187 source,
10188 source_alias,
10189 source_select,
10190 source_column_aliases,
10191 on,
10192 clauses,
10193 returning,
10194 }))
10195 }
10196
10197 /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10198 /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10199 /// as SELECT, so `RETURNING *`, `RETURNING col`,
10200 /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10201 fn parse_optional_returning(
10202 &mut self,
10203 ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10204 let is_returning_kw = matches!(
10205 self.peek(),
10206 Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10207 );
10208 if !is_returning_kw {
10209 return Ok(None);
10210 }
10211 self.advance();
10212 let mut items = Vec::new();
10213 loop {
10214 items.push(self.parse_select_item()?);
10215 if matches!(self.peek(), Token::Comma) {
10216 self.advance();
10217 continue;
10218 }
10219 break;
10220 }
10221 Ok(Some(items))
10222 }
10223
10224 /// v6.0.4 — parse the tail of an ALTER statement after the
10225 /// leading `ALTER` keyword has been consumed. Only one form is
10226 /// supported in v6.0.4:
10227 ///
10228 /// ```text
10229 /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10230 /// ```
10231 fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10232 // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10233 // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10234 // exclusion) is accepted by stripping the `ONLY` keyword
10235 // before the table parse.
10236 // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10237 // and the long PG-dump tail are accepted as no-ops.
10238 match self.advance() {
10239 Token::Index => {}
10240 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10241 // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10242 // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10243 Token::Table => {
10244 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10245 self.advance();
10246 }
10247 return self.parse_alter_table_after_keyword();
10248 }
10249 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10250 return self.parse_alter_policy_after_keyword();
10251 }
10252 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10253 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10254 self.advance();
10255 }
10256 return self.parse_alter_table_after_keyword();
10257 }
10258 // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10259 // of the silent-noop tail.
10260 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10261 return self.parse_alter_sequence_after_keyword();
10262 }
10263 // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10264 // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10265 // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10266 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10267 // NB: the match arm consumed `TYPE` via self.advance(); the
10268 // cursor is now at the type name — do NOT advance again.
10269 let type_name = self.expect_ident_like()?;
10270 let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10271 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10272 if is_add_value {
10273 self.advance(); // ADD
10274 self.advance(); // VALUE
10275 // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10276 // IF/EXISTS as identifiers.
10277 let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10278 {
10279 let n1 = self.tokens.get(self.pos + 1);
10280 let n2 = self.tokens.get(self.pos + 2);
10281 if matches!(n1, Some(Token::Not))
10282 && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10283 {
10284 self.advance();
10285 self.advance();
10286 self.advance();
10287 true
10288 } else {
10289 false
10290 }
10291 } else {
10292 false
10293 };
10294 let label = self.expect_string_literal()?;
10295 let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10296 {
10297 let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10298 self.advance();
10299 let anchor = self.expect_string_literal()?;
10300 Some((is_before, anchor))
10301 } else {
10302 None
10303 };
10304 return Ok(Statement::AlterTypeAddValue {
10305 type_name,
10306 label,
10307 if_not_exists,
10308 position,
10309 });
10310 }
10311 // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10312 // Used to fall into the no-op tail below: accepted, silently
10313 // ignored. `RENAME TO <newtype>` keeps falling through.
10314 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10315 && matches!(
10316 self.tokens.get(self.pos + 1),
10317 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10318 )
10319 {
10320 self.advance(); // RENAME
10321 self.advance(); // VALUE
10322 let old = self.expect_string_literal()?;
10323 if matches!(self.peek(), Token::To) {
10324 self.advance();
10325 } else {
10326 self.expect_keyword_ident("to")?;
10327 }
10328 let new = self.expect_string_literal()?;
10329 return Ok(Statement::AlterTypeRenameValue {
10330 type_name,
10331 old,
10332 new,
10333 });
10334 }
10335 // Other ALTER TYPE forms — the ACTION stays a no-op
10336 // (pg_dump tail), but v7.39 (round 708) the NAME is
10337 // validated: `ALTER TYPE nosuch RENAME TO x` reported
10338 // success for a type that does not exist.
10339 self.consume_until_statement_boundary();
10340 return Ok(Statement::ValidateOnly {
10341 kind: crate::ast::ValidateOnlyKind::TypeName,
10342 names: alloc::vec![type_name],
10343 });
10344 }
10345 // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10346 // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10347 // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10348 // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10349 // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10350 // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10351 // pg_dump no-op list below: every form used to report success
10352 // and change nothing, which is worse than refusing outright
10353 // (a migration dropping a constraint kept rejecting data).
10354 // NOTE: the enclosing `match self.advance()` already consumed
10355 // the DOMAIN keyword, so the name is next.
10356 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10357 return self.parse_alter_domain_after_keyword();
10358 }
10359 // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10360 // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10361 // used to fall into the pg_dump no-op tail below, so a DBA
10362 // setting a per-role default was told it worked and nothing
10363 // happened. Intercepted here, BEFORE that tail.
10364 // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10365 // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10366 // interception below exists: swallowed with the no-op tail, an
10367 // unknown parameter name was ACCEPTED where PG18 answers
10368 // `unrecognized configuration parameter`. SPG applies nothing
10369 // either way — there is no postgresql.auto.conf — but it now
10370 // says so about a name it does not know.
10371 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10372 // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10373 // already consumed here. An extra advance eats the SET and
10374 // the parameter name is never seen — which is exactly the
10375 // bug a panic in this branch disproved: the branch WAS on
10376 // the path, the reading of it was wrong.
10377 let mut parameter = None;
10378 // SET <name> … | RESET <name> | RESET ALL
10379 if matches!(self.peek(), Token::Ident(k)
10380 if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10381 {
10382 self.advance();
10383 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10384 && !n.eq_ignore_ascii_case("all")
10385 {
10386 self.advance();
10387 // A dotted GUC (`plpgsql.check_asserts`) is two
10388 // tokens; keep the whole name.
10389 let mut full = n;
10390 while matches!(self.peek(), Token::Dot) {
10391 self.advance();
10392 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10393 full.push('.');
10394 full.push_str(&t);
10395 }
10396 }
10397 parameter = Some(full);
10398 }
10399 }
10400 self.consume_until_statement_boundary();
10401 return Ok(Statement::AlterSystem { parameter });
10402 }
10403 Token::Ident(s) | Token::QuotedIdent(s)
10404 if matches!(
10405 s.to_ascii_lowercase().as_str(),
10406 "role" | "user" | "database"
10407 ) && self.peeks_db_role_setting() =>
10408 {
10409 let is_database = s.eq_ignore_ascii_case("database");
10410 return self.parse_db_role_setting(is_database);
10411 }
10412 // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10413 // (the non-SET forms; SET/RESET took the branch above). The
10414 // attributes still no-op — recorded, and the ignored PASSWORD
10415 // is ledgered as its own follow-up — but the ROLE is validated:
10416 // any name was accepted for a role that does not exist.
10417 Token::Ident(s) | Token::QuotedIdent(s)
10418 if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10419 {
10420 // NB: the enclosing `match self.advance()` already consumed
10421 // ROLE/USER — the round-695 trap, hit again in this round's
10422 // first draft (the name was eaten and WITH parsed as the
10423 // role). The cursor is at the name.
10424 let name = self.expect_ident_or_string()?;
10425 // v7.39 (round 750) — scan the attribute tail for
10426 // PASSWORD. Everything else stays a recorded no-op, but
10427 // a dropped credential rotation is a SECURITY bug:
10428 // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10429 // changed nothing, so the old password kept working.
10430 // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10431 // NULL` clears the credential.
10432 let mut password: Option<Option<String>> = None;
10433 loop {
10434 match self.peek() {
10435 Token::Semicolon | Token::Eof => break,
10436 Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10437 self.advance();
10438 match self.advance() {
10439 Token::String(p) => password = Some(Some(p)),
10440 Token::Null => password = Some(None),
10441 Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10442 password = Some(None);
10443 }
10444 other => {
10445 return Err(self.err(alloc::format!(
10446 "expected password string or NULL after PASSWORD, got {other:?}"
10447 )));
10448 }
10449 }
10450 }
10451 _ => {
10452 self.advance();
10453 }
10454 }
10455 }
10456 if name.eq_ignore_ascii_case("all") {
10457 // `ALTER ROLE ALL …` names every role; nothing to check.
10458 return Ok(Statement::Empty);
10459 }
10460 if let Some(pw) = password {
10461 return Ok(Statement::AlterRolePassword { name, password: pw });
10462 }
10463 return Ok(Statement::ValidateOnly {
10464 kind: crate::ast::ValidateOnlyKind::RoleName,
10465 names: alloc::vec![name],
10466 });
10467 }
10468 // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10469 // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10470 // list far enough to validate the NAME; the actions still no-op.
10471 // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10472 // models none of them and their dumps are rare.)
10473 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10474 let name = self.expect_ident_or_string()?;
10475 self.consume_until_statement_boundary();
10476 return Ok(Statement::ValidateOnly {
10477 kind: crate::ast::ValidateOnlyKind::CollationName,
10478 names: alloc::vec![name],
10479 });
10480 }
10481 Token::Ident(s) | Token::QuotedIdent(s)
10482 if s.eq_ignore_ascii_case("text")
10483 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10484 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10485 {
10486 self.advance(); // SEARCH
10487 self.advance(); // CONFIGURATION
10488 let name = self.expect_ident_like()?;
10489 self.consume_until_statement_boundary();
10490 return Ok(Statement::ValidateOnly {
10491 kind: crate::ast::ValidateOnlyKind::TsConfigName,
10492 names: alloc::vec![name],
10493 });
10494 }
10495 Token::Ident(s) | Token::QuotedIdent(s)
10496 if s.eq_ignore_ascii_case("event")
10497 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10498 {
10499 self.advance(); // TRIGGER
10500 let name = self.expect_ident_like()?;
10501 self.consume_until_statement_boundary();
10502 return Ok(Statement::ValidateOnly {
10503 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10504 names: alloc::vec![name],
10505 });
10506 }
10507 Token::Ident(s) | Token::QuotedIdent(s)
10508 if s.eq_ignore_ascii_case("large")
10509 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10510 {
10511 self.advance(); // OBJECT
10512 let oid = match self.advance() {
10513 Token::Integer(n) => alloc::format!("{n}"),
10514 other => {
10515 return Err(
10516 self.err(alloc::format!("expected large object oid, got {other:?}"))
10517 );
10518 }
10519 };
10520 self.consume_until_statement_boundary();
10521 return Ok(Statement::ValidateOnly {
10522 kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10523 names: alloc::vec![oid],
10524 });
10525 }
10526 // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10527 // argument-list parse as DROP AGGREGATE (round 707); the
10528 // action no-ops, the existence check is real.
10529 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10530 // Same round-695 trap as above: AGGREGATE is already
10531 // consumed; the cursor is at the name.
10532 let name = self.expect_ident_like()?;
10533 let mut names = alloc::vec![name];
10534 if matches!(self.peek(), Token::LParen) {
10535 self.advance();
10536 loop {
10537 match self.peek().clone() {
10538 Token::RParen => {
10539 self.advance();
10540 break;
10541 }
10542 Token::Star => {
10543 self.advance();
10544 names.push(String::from("*"));
10545 }
10546 Token::Comma => {
10547 self.advance();
10548 }
10549 _ => {
10550 let mut t = self.expect_ident_like()?;
10551 while let Token::Ident(nx) = self.peek() {
10552 let nx = nx.clone();
10553 self.advance();
10554 t.push(' ');
10555 t.push_str(&nx);
10556 }
10557 names.push(t);
10558 }
10559 }
10560 }
10561 }
10562 self.consume_until_statement_boundary();
10563 return Ok(Statement::ValidateOnly {
10564 kind: crate::ast::ValidateOnlyKind::AggregateName,
10565 names,
10566 });
10567 }
10568 Token::Ident(s) | Token::QuotedIdent(s)
10569 if matches!(
10570 s.to_ascii_lowercase().as_str(),
10571 "view"
10572 | "function"
10573 | "database"
10574 | "schema"
10575 | "owner"
10576 | "default"
10577 | "extension"
10578 | "materialized"
10579 | "publication"
10580 | "subscription"
10581 // v7.37.17 (17.6 siblings) — additional ALTER
10582 // targets pg_dump / pg_dumpall / operator DB
10583 // migration scripts commonly emit. SPG has
10584 // no matching machinery for any of these; the
10585 // parser accepts + Empty-returns so pg_dump
10586 // tail statements don't stall.
10587 | "tablespace"
10588 | "language"
10589 | "operator"
10590 | "conversion"
10591 | "statistics"
10592 | "server"
10593 | "foreign"
10594 // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10595 // / TEMPLATE (CONFIGURATION intercepted above).
10596 | "text"
10597 ) =>
10598 {
10599 self.consume_until_statement_boundary();
10600 return Ok(Statement::Empty);
10601 }
10602 other => {
10603 return Err(self.err(format!(
10604 "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10605 after ALTER, got {other:?}"
10606 )));
10607 }
10608 }
10609 // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10610 // (mailrs migrate-042 ships these). The presence of an
10611 // IF EXISTS makes the subsequent name lookup tolerate
10612 // a missing index — engine returns CommandOk no-op.
10613 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10614 let next = self.tokens.get(self.pos + 1);
10615 if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10616 self.advance();
10617 self.advance();
10618 true
10619 } else {
10620 false
10621 }
10622 } else {
10623 false
10624 };
10625 let name = self.expect_ident_like()?;
10626 // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10627 // Detect BEFORE the REBUILD path so the existing REBUILD
10628 // arm stays untouched.
10629 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10630 self.advance();
10631 if matches!(self.peek(), Token::To) {
10632 self.advance();
10633 } else {
10634 self.expect_keyword_ident("to")?;
10635 }
10636 let new = self.expect_ident_like()?;
10637 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10638 name,
10639 target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10640 }));
10641 }
10642 // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10643 // A syntax error before; the index is validated, the params no-op.
10644 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10645 || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10646 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10647 {
10648 self.consume_until_statement_boundary();
10649 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10650 name,
10651 target: crate::ast::AlterIndexTarget::StorageParams,
10652 }));
10653 }
10654 // REBUILD
10655 self.expect_keyword_ident("rebuild")?;
10656 // Optional: WITH (encoding = <enc>)
10657 let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10658 self.advance();
10659 if !matches!(self.peek(), Token::LParen) {
10660 return Err(self.err(format!(
10661 "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10662 self.peek()
10663 )));
10664 }
10665 self.advance();
10666 self.expect_keyword_ident("encoding")?;
10667 if !matches!(self.peek(), Token::Eq) {
10668 return Err(self.err(format!(
10669 "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10670 self.peek()
10671 )));
10672 }
10673 self.advance();
10674 let enc_ident = match self.advance() {
10675 Token::Ident(s) | Token::QuotedIdent(s) => s,
10676 other => {
10677 return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10678 }
10679 };
10680 let enc = match enc_ident.to_ascii_lowercase().as_str() {
10681 "f32" => VecEncoding::F32,
10682 "sq8" => VecEncoding::Sq8,
10683 "half" => VecEncoding::F16,
10684 other => {
10685 return Err(self.err(format!(
10686 "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10687 )));
10688 }
10689 };
10690 if !matches!(self.peek(), Token::RParen) {
10691 return Err(self.err(format!(
10692 "expected ')' after encoding value, got {:?}",
10693 self.peek()
10694 )));
10695 }
10696 self.advance();
10697 Some(enc)
10698 } else {
10699 None
10700 };
10701 Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10702 name,
10703 target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10704 }))
10705 }
10706
10707 /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10708 /// only `SET` form currently supported; future v6.7.x can add
10709 /// more SET subjects without changing the dispatch shape.
10710 /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10711 /// subactions. Single-subaction shape stays a 1-element vec.
10712 fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10713 let table_name = self.expect_ident_like()?;
10714 let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10715 loop {
10716 let subaction = self.parse_alter_table_subaction()?;
10717 // ADD COLUMN with inline REFERENCES emits both an
10718 // AddColumn and an AddForeignKey subaction; the
10719 // helper returns 1 or 2 items.
10720 targets.extend(subaction);
10721 if matches!(self.peek(), Token::Comma) {
10722 self.advance();
10723 continue;
10724 }
10725 break;
10726 }
10727 Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10728 name: table_name,
10729 targets,
10730 }))
10731 }
10732
10733 /// Parse one ALTER TABLE subaction. Returns a Vec because
10734 /// inline `REFERENCES` on `ADD COLUMN` produces both an
10735 /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10736 fn parse_alter_table_subaction(
10737 &mut self,
10738 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10739 match self.peek() {
10740 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10741 self.advance();
10742 // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10743 // storage parameters: paren-prefixed; consume.
10744 if matches!(self.peek(), Token::LParen) {
10745 self.consume_until_statement_boundary();
10746 return Ok(Vec::new());
10747 }
10748 let setting = self.expect_ident_like()?;
10749 if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10750 if !matches!(self.peek(), Token::Eq) {
10751 return Err(self.err(alloc::format!(
10752 "expected '=' after hot_tier_bytes, got {:?}",
10753 self.peek()
10754 )));
10755 }
10756 self.advance();
10757 let n = self.expect_u64_literal()?;
10758 return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10759 }
10760 // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10761 // accept-and-no-op for ALTER TABLE SET <subject>
10762 // forms that pg_dump emits but SPG either treats
10763 // as N/A (single-tenant, single-owner, no shared
10764 // tablespaces) or accepts the dump-side declaration
10765 // without runtime effect:
10766 // SET SCHEMA <name> (18.11)
10767 // SET TABLESPACE <name> (18.8)
10768 // SET LOGGED / UNLOGGED (18.7 alt-form)
10769 // SET WITHOUT CLUSTER (18.13)
10770 // SET WITHOUT OIDS (PG legacy)
10771 // SET (option = value, …) (storage parameters)
10772 // SET REPLICA IDENTITY {…} (18.14)
10773 if setting.eq_ignore_ascii_case("schema")
10774 || setting.eq_ignore_ascii_case("tablespace")
10775 || setting.eq_ignore_ascii_case("logged")
10776 || setting.eq_ignore_ascii_case("unlogged")
10777 || setting.eq_ignore_ascii_case("without")
10778 {
10779 self.consume_until_statement_boundary();
10780 return Ok(Vec::new());
10781 }
10782 if setting.eq_ignore_ascii_case("replica") {
10783 // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10784 self.consume_until_statement_boundary();
10785 return Ok(Vec::new());
10786 }
10787 // SET (option=value, …) — storage parameters.
10788 if matches!(self.peek(), Token::LParen) {
10789 self.consume_until_statement_boundary();
10790 return Ok(Vec::new());
10791 }
10792 Err(self.err(alloc::format!(
10793 "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10794 hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10795 WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10796 )))
10797 }
10798 // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10799 // not ignored: round 645 gave SPG the inheritance the
10800 // v7.37.18 no-op said it did not have.
10801 Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10802 self.advance();
10803 let parent = self.expect_ident_like()?;
10804 self.consume_until_statement_boundary();
10805 Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10806 parent,
10807 detach: false
10808 }])
10809 }
10810 // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10811 // LEVEL SECURITY`, which has its own RLS arm below — without
10812 // the guard this swallowed NO FORCE as a no-op.
10813 Token::Ident(s)
10814 if s.eq_ignore_ascii_case("no")
10815 && !matches!(
10816 self.tokens.get(self.pos + 1),
10817 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10818 ) =>
10819 {
10820 self.advance();
10821 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
10822 if k.eq_ignore_ascii_case("inherit"))
10823 {
10824 self.advance();
10825 let parent = self.expect_ident_like()?;
10826 self.consume_until_statement_boundary();
10827 return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10828 parent,
10829 detach: true
10830 }]);
10831 }
10832 self.consume_until_statement_boundary();
10833 Ok(Vec::new())
10834 }
10835 // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
10836 // single-owner, so there is still nothing to record.
10837 //
10838 // v7.39 (round 652) — but the name now reaches the engine,
10839 // which refuses a role that does not exist as PG does. The
10840 // no-op was swallowing the whole statement, so a dump naming
10841 // a role this server never heard of restored clean and left
10842 // the table owned by whoever ran the restore.
10843 Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
10844 self.advance();
10845 if matches!(self.peek(), Token::To) {
10846 self.advance();
10847 }
10848 let role = self.expect_ident_like()?;
10849 Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
10850 role
10851 }])
10852 }
10853 // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
10854 // PG sets a hint; SPG doesn't have clustered storage, so the
10855 // hint itself stays a no-op.
10856 //
10857 // v7.39 (round 652) — the index name is checked now. PG
10858 // errors on one that does not exist, and swallowing that let
10859 // a typo'd CLUSTER ON pass silently.
10860 Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
10861 self.advance();
10862 // `ON` is a reserved token, not an ident.
10863 if !matches!(self.peek(), Token::On) {
10864 return Err(self.err(alloc::format!(
10865 "expected ON after CLUSTER, got {:?}",
10866 self.peek()
10867 )));
10868 }
10869 self.advance();
10870 let index = self.expect_ident_like()?;
10871 Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
10872 index: Some(index)
10873 }])
10874 }
10875 // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
10876 // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
10877 // what a logical decoder puts in the old-tuple image; SPG's
10878 // replication is SQL-text, so there is nothing to record.
10879 // Accept-and-no-op (it used to be a parse error).
10880 Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
10881 self.advance();
10882 // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
10883 // validates the index; DEFAULT / FULL / NOTHING stay no-op.
10884 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
10885 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
10886 {
10887 self.advance(); // IDENTITY
10888 self.advance(); // USING
10889 if matches!(self.peek(), Token::Index)
10890 || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
10891 {
10892 self.advance();
10893 }
10894 let index = self.expect_ident_like()?;
10895 self.consume_until_statement_boundary();
10896 return Ok(alloc::vec![
10897 crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
10898 ]);
10899 }
10900 self.consume_until_statement_boundary();
10901 Ok(Vec::new())
10902 }
10903 // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
10904 //
10905 // v7.39 (round 652) — it used to consume the statement and
10906 // return nothing, on the stated theory that SPG validated at
10907 // ADD CONSTRAINT time so there was never anything left to
10908 // validate. Measured against PG18, ADD CONSTRAINT did not
10909 // scan the existing rows at all — the comment described a
10910 // property SPG did not have, which is why nobody looked. Both
10911 // halves are real now: ADD scans unless told NOT VALID, and
10912 // this scans what NOT VALID skipped.
10913 Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
10914 self.advance();
10915 self.expect_keyword_ident("constraint")?;
10916 let name = self.expect_ident_like()?;
10917 Ok(alloc::vec![
10918 crate::ast::AlterTableTarget::ValidateConstraint { name }
10919 ])
10920 }
10921 // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
10922 // SET (option = value, …). PG uses it to clear per-table
10923 // storage params like fillfactor or autovacuum_*. SPG
10924 // engine-manages those parameters; accept-and-no-op.
10925 Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
10926 self.advance();
10927 self.consume_until_statement_boundary();
10928 Ok(Vec::new())
10929 }
10930 // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
10931 // type-of binding (PG 9.0+). SPG composite types
10932 // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
10933 // TABLE OF is rare and inverse of CREATE TABLE OF.
10934 // Accept-and-no-op until a customer dump round-trips it.
10935 Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
10936 self.advance();
10937 // v7.39 (round 710) — the type name is validated now.
10938 let type_name = self.expect_ident_like()?;
10939 self.consume_until_statement_boundary();
10940 Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
10941 type_name
10942 }])
10943 }
10944 // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
10945 // (reserved keyword) rather than Token::Ident("not"),
10946 // so it needs its own arm. Accept-and-no-op same as OF.
10947 Token::Not => {
10948 self.advance();
10949 self.consume_until_statement_boundary();
10950 Ok(Vec::new())
10951 }
10952 // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
10953 Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
10954 self.advance();
10955 self.expect_row_level_security()?;
10956 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10957 enabled: None,
10958 force: Some(true),
10959 }])
10960 }
10961 // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
10962 Token::Ident(s)
10963 if s.eq_ignore_ascii_case("no")
10964 && matches!(
10965 self.tokens.get(self.pos + 1),
10966 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10967 ) =>
10968 {
10969 self.advance(); // NO
10970 self.advance(); // FORCE
10971 self.expect_row_level_security()?;
10972 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10973 enabled: None,
10974 force: Some(false),
10975 }])
10976 }
10977 // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
10978 // (sets relrowsecurity). The guard requires the next token to be
10979 // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
10980 Token::Ident(s)
10981 if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
10982 && matches!(
10983 self.tokens.get(self.pos + 1),
10984 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
10985 ) =>
10986 {
10987 let enabled = s.eq_ignore_ascii_case("enable");
10988 self.advance(); // ENABLE/DISABLE
10989 self.expect_row_level_security()?;
10990 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10991 enabled: Some(enabled),
10992 force: None,
10993 }])
10994 }
10995 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
10996 self.advance();
10997 // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
10998 // {INDEX|KEY} [name] (cols)`, which every ORM migration
10999 // emits. The same grammar CREATE TABLE already accepts
11000 // inline (`KEY idx (a)`, prefix lengths and all), so it goes
11001 // through the SAME parser — an ALTER-only copy would be a
11002 // second place for the two to drift.
11003 if self.peek_mysql_inline_key_start() {
11004 return Ok(match self.parse_mysql_inline_key()? {
11005 Some(c) => {
11006 alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
11007 }
11008 // FULLTEXT / SPATIAL parse and are accepted as a
11009 // no-op here exactly as they are inline.
11010 None => Vec::new(),
11011 });
11012 }
11013 // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
11014 // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
11015 // PRIMARY KEY this way; mysqldump emits both.
11016 // Peek-only dispatch (no advance) — `advance()`
11017 // destructively replaces consumed tokens with Eof,
11018 // so saved-pos restore would land on Eofs.
11019 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
11020 {
11021 // The next-but-one ident is the constraint
11022 // name; the one after THAT is the kind.
11023 let kind_pos = self.pos + 2;
11024 let kind = self.tokens.get(kind_pos).cloned();
11025 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
11026 {
11027 let fk = self.parse_table_level_fk()?;
11028 return Ok(alloc::vec![
11029 crate::ast::AlterTableTarget::AddForeignKey(fk)
11030 ]);
11031 }
11032 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
11033 {
11034 self.advance(); // CONSTRAINT
11035 // v7.39 (read01 round 48) — keep the name; the engine
11036 // stores it now instead of dropping it on the floor.
11037 let con_name = self.expect_ident_like()?;
11038 self.advance(); // PRIMARY
11039 self.expect_keyword_ident("key")?;
11040 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11041 // v7.39 (round 711) — the ALTER form carries the
11042 // timing too (pg_dump writes it here).
11043 let (deferrable, initially_deferred) =
11044 self.consume_deferrable_clauses_timed()?;
11045 return Ok(alloc::vec![
11046 crate::ast::AlterTableTarget::AddTableConstraint(
11047 crate::ast::TableConstraint::PrimaryKey {
11048 name: Some(con_name),
11049 columns: cols,
11050 deferrable,
11051 initially_deferred,
11052 }
11053 )
11054 ]);
11055 }
11056 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
11057 {
11058 self.advance(); // CONSTRAINT
11059 // v7.39 (read01 round 48) — keep the name.
11060 let con_name = self.expect_ident_like()?;
11061 // v7.22 (mailrs round-13 gap 6) — delegate so
11062 // the optional `NULLS [NOT] DISTINCT` modifier
11063 // parses here too (pg_dump emits the ALTER
11064 // form; semantics enforced by the engine
11065 // since v7.13).
11066 let mut uc = self.parse_table_level_unique()?;
11067 if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
11068 *name = Some(con_name);
11069 }
11070 return Ok(alloc::vec![
11071 crate::ast::AlterTableTarget::AddTableConstraint(uc)
11072 ]);
11073 }
11074 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
11075 {
11076 self.advance(); // CONSTRAINT
11077 // v7.39 (read01 round 48) — keep the name.
11078 let con_name = self.expect_ident_like()?;
11079 self.advance(); // CHECK
11080 if !matches!(self.peek(), Token::LParen) {
11081 return Err(self.err(alloc::format!(
11082 "expected '(' after CHECK, got {:?}", self.peek()
11083 )));
11084 }
11085 self.advance();
11086 let expr = self.parse_expr(0)?;
11087 if matches!(self.peek(), Token::RParen) {
11088 self.advance();
11089 }
11090 let not_valid = self.parse_not_valid_suffix();
11091 return Ok(alloc::vec![
11092 crate::ast::AlterTableTarget::AddTableConstraint(
11093 crate::ast::TableConstraint::Check {
11094 name: Some(con_name),
11095 expr,
11096 not_valid,
11097 }
11098 )
11099 ]);
11100 }
11101 // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
11102 // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
11103 // exclusion constraints via this ALTER form.
11104 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
11105 {
11106 self.advance(); // CONSTRAINT
11107 let con_name = self.expect_ident_like()?;
11108 let mut ex = self.parse_table_level_exclude()?;
11109 if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
11110 *name = Some(con_name);
11111 }
11112 return Ok(alloc::vec![
11113 crate::ast::AlterTableTarget::AddTableConstraint(ex)
11114 ]);
11115 }
11116 // Unknown kind — fall through to FK path which
11117 // produces a descriptive parse error.
11118 }
11119 let is_fk = matches!(
11120 self.peek(),
11121 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
11122 || s.eq_ignore_ascii_case("foreign")
11123 );
11124 if is_fk {
11125 let fk = self.parse_table_level_fk()?;
11126 return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
11127 }
11128 // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
11129 // (no CONSTRAINT prefix) — same dispatch.
11130 match self.peek().clone() {
11131 Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
11132 self.advance();
11133 self.expect_keyword_ident("key")?;
11134 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11135 let (deferrable, initially_deferred) =
11136 self.consume_deferrable_clauses_timed()?;
11137 return Ok(alloc::vec![
11138 crate::ast::AlterTableTarget::AddTableConstraint(
11139 crate::ast::TableConstraint::PrimaryKey {
11140 name: None,
11141 columns: cols,
11142 deferrable,
11143 initially_deferred,
11144 }
11145 )
11146 ]);
11147 }
11148 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
11149 // v7.22 — delegate (NULLS [NOT] DISTINCT).
11150 let uc = self.parse_table_level_unique()?;
11151 return Ok(alloc::vec![
11152 crate::ast::AlterTableTarget::AddTableConstraint(uc)
11153 ]);
11154 }
11155 // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
11156 // prefix). The other three bare forms were here and
11157 // this one was not, so it fell through to the column
11158 // path and came back as "unexpected reserved keyword
11159 // 'check' at start of column definition".
11160 _ if self.peek_table_level_check_start() => {
11161 let chk = self.parse_table_level_check()?;
11162 let not_valid = self.parse_not_valid_suffix();
11163 let crate::ast::TableConstraint::Check { expr, .. } = chk else {
11164 unreachable!("parse_table_level_check returns Check")
11165 };
11166 return Ok(alloc::vec![
11167 crate::ast::AlterTableTarget::AddTableConstraint(
11168 crate::ast::TableConstraint::Check {
11169 name: None,
11170 expr,
11171 not_valid,
11172 }
11173 )
11174 ]);
11175 }
11176 // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11177 Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11178 let ex = self.parse_table_level_exclude()?;
11179 return Ok(alloc::vec![
11180 crate::ast::AlterTableTarget::AddTableConstraint(ex)
11181 ]);
11182 }
11183 _ => {}
11184 }
11185 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11186 self.advance();
11187 }
11188 let mut if_not_exists = false;
11189 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11190 self.advance();
11191 if !matches!(self.peek(), Token::Not) {
11192 return Err(self.err(alloc::format!(
11193 "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11194 self.peek()
11195 )));
11196 }
11197 self.advance();
11198 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11199 return Err(self.err(alloc::format!(
11200 "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11201 self.peek()
11202 )));
11203 }
11204 self.advance();
11205 if_not_exists = true;
11206 }
11207 // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11208 // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11209 // returns ColumnDef + an optional inline FK.
11210 let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11211 let col_name = column.name.clone();
11212 let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11213 column,
11214 if_not_exists,
11215 }];
11216 if let Some(mut fk) = col_level_fk {
11217 if fk.columns.is_empty() {
11218 fk.columns.push(col_name);
11219 }
11220 out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11221 }
11222 Ok(out)
11223 }
11224 Token::Drop => {
11225 self.advance();
11226 // v7.13.3 — dispatch on the next token. mailrs round-7
11227 // S8 closed DROP COLUMN; round-6 S7 closed
11228 // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11229 // RESTRICT modifiers.
11230 // DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11231 // DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11232 let subject = match self.peek() {
11233 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11234 self.advance();
11235 "constraint"
11236 }
11237 Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11238 self.advance();
11239 "column"
11240 }
11241 // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11242 // `INDEX` lexes as the reserved Token::Index, so it is
11243 // unambiguous. `KEY` is a plain ident, and PG allows a
11244 // column literally named "key", so only read it as the
11245 // keyword when a name follows it.
11246 Token::Index => {
11247 self.advance();
11248 "index"
11249 }
11250 Token::Ident(s)
11251 if s.eq_ignore_ascii_case("key")
11252 && matches!(
11253 self.tokens.get(self.pos + 1),
11254 Some(Token::Ident(_) | Token::QuotedIdent(_))
11255 ) =>
11256 {
11257 self.advance();
11258 "index"
11259 }
11260 // PG-canonical bare `DROP <col>` without COLUMN
11261 // keyword is also valid; treat any other ident
11262 // as the column name.
11263 Token::Ident(_) | Token::QuotedIdent(_) => "column",
11264 other => {
11265 return Err(self.err(alloc::format!(
11266 "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11267 )));
11268 }
11269 };
11270 let mut if_exists = false;
11271 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11272 let n1 = self.tokens.get(self.pos + 1);
11273 if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11274 self.advance();
11275 self.advance();
11276 if_exists = true;
11277 }
11278 }
11279 let name = self.expect_ident_like()?;
11280 let mut cascade = false;
11281 if matches!(
11282 self.peek(),
11283 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11284 || s.eq_ignore_ascii_case("restrict")
11285 ) {
11286 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11287 {
11288 cascade = true;
11289 }
11290 self.advance();
11291 }
11292 if subject == "index" {
11293 Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11294 name,
11295 if_exists,
11296 }])
11297 } else if subject == "constraint" {
11298 Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11299 name,
11300 if_exists,
11301 }])
11302 } else {
11303 Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11304 column: name,
11305 if_exists,
11306 cascade,
11307 }])
11308 }
11309 }
11310 Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11311 self.advance();
11312 // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11313 // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11314 // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11315 // immediately; accept-and-no-op.
11316 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11317 self.advance();
11318 self.consume_until_statement_boundary();
11319 return Ok(Vec::new());
11320 }
11321 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11322 self.advance();
11323 }
11324 let col_name = self.expect_ident_like()?;
11325 match self.peek() {
11326 Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11327 self.advance();
11328 }
11329 // v7.14.0 — pg_dump emits BIGSERIAL via
11330 // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11331 // nextval('seq')` (the sequence is created
11332 // separately). SPG's BIGSERIAL already uses
11333 // AUTO_INCREMENT; accept SET DEFAULT / DROP
11334 // DEFAULT / SET NOT NULL / DROP NOT NULL as
11335 // engine no-ops by consuming the tail.
11336 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11337 // v7.22 (round-13 T2) — `SET DEFAULT
11338 // nextval('…')` is how pg_dump spells a
11339 // SERIAL column (plain integer in CREATE
11340 // TABLE + this ALTER). It used to be
11341 // swallowed as a no-op, which silently
11342 // STRIPPED auto-increment from imported
11343 // schemas — the first post-import INSERT
11344 // without an explicit id then violated NOT
11345 // NULL. Lower it to the auto-increment
11346 // marker instead.
11347 let is_default_nextval =
11348 matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11349 && matches!(
11350 self.tokens.get(self.pos + 2),
11351 Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11352 );
11353 if is_default_nextval {
11354 let seq_name = self.scan_sequence_name_until_boundary();
11355 return Ok(alloc::vec![
11356 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11357 column: col_name,
11358 seq_name,
11359 }
11360 ]);
11361 }
11362 // v7.37.18 (18.1 + 18.2) — proper lowering.
11363 self.advance(); // consume "set"
11364 match self.peek().clone() {
11365 Token::Default => {
11366 self.advance();
11367 let default_expr = self.parse_expr(0)?;
11368 return Ok(alloc::vec![
11369 crate::ast::AlterTableTarget::AlterColumnSetDefault {
11370 column: col_name,
11371 default_expr,
11372 }
11373 ]);
11374 }
11375 Token::Not => {
11376 self.advance();
11377 if !matches!(self.peek(), Token::Null) {
11378 return Err(self.err(alloc::format!(
11379 "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11380 self.peek()
11381 )));
11382 }
11383 self.advance();
11384 return Ok(alloc::vec![
11385 crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11386 column: col_name,
11387 }
11388 ]);
11389 }
11390 // `SET EXPRESSION AS (expr)` (PG 17) — change a
11391 // stored generated column's expression and
11392 // recompute existing rows.
11393 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11394 self.advance(); // EXPRESSION
11395 if matches!(self.peek(), Token::As) {
11396 self.advance();
11397 }
11398 let expr = self.parse_expr(0)?;
11399 return Ok(alloc::vec![
11400 crate::ast::AlterTableTarget::AlterColumnSetExpression {
11401 column: col_name,
11402 expr,
11403 }
11404 ]);
11405 }
11406 other => {
11407 // Other SET subjects (STATISTICS,
11408 // STORAGE, COMPRESSION, …) stay no-ops —
11409 // storage hints with no SPG semantics.
11410 let _ = other;
11411 self.consume_until_statement_boundary();
11412 return Ok(Vec::new());
11413 }
11414 }
11415 }
11416 Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11417 self.advance(); // consume "drop"
11418 return self.parse_alter_column_drop_tail(col_name);
11419 }
11420 Token::Drop => {
11421 self.advance(); // consume Drop token
11422 return self.parse_alter_column_drop_tail(col_name);
11423 }
11424 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11425 // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11426 // GENERATED { ALWAYS | BY DEFAULT } AS
11427 // IDENTITY ( … )`: pg_dump's spelling for
11428 // identity columns. Same auto-increment
11429 // lowering as the nextval default; the
11430 // sequence options inside the parens are
11431 // no-ops under SPG's max+1 semantics.
11432 let is_generated = matches!(
11433 self.tokens.get(self.pos + 1),
11434 Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11435 );
11436 if !is_generated {
11437 return Err(self.err(alloc::format!(
11438 "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11439 self.tokens.get(self.pos + 1)
11440 )));
11441 }
11442 let seq_name = self.scan_sequence_name_until_boundary();
11443 return Ok(alloc::vec![
11444 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11445 column: col_name,
11446 seq_name,
11447 }
11448 ]);
11449 }
11450 // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11451 // column: floor the next allocated value at n (bare
11452 // RESTART = restart from the start value, 1).
11453 Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11454 self.advance();
11455 let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11456 {
11457 self.advance();
11458 let neg = if matches!(self.peek(), Token::Minus) {
11459 self.advance();
11460 true
11461 } else {
11462 false
11463 };
11464 match self.advance() {
11465 Token::Integer(v) => Some(if neg { -v } else { v }),
11466 other => {
11467 return Err(self.err(alloc::format!(
11468 "expected integer after RESTART WITH, got {other:?}"
11469 )));
11470 }
11471 }
11472 } else {
11473 None
11474 };
11475 return Ok(alloc::vec![
11476 crate::ast::AlterTableTarget::AlterColumnRestart {
11477 column: col_name,
11478 with,
11479 }
11480 ]);
11481 }
11482 other => {
11483 return Err(self.err(alloc::format!(
11484 "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11485 )));
11486 }
11487 }
11488 // v7.39 (round 713) — the type parser has consumed a
11489 // trailing `COLLATE <name>` since Phase 2.5, and
11490 // `parse_column_type_name` discarded it: `ALTER COLUMN t
11491 // TYPE text COLLATE "C"` parsed clean and changed
11492 // nothing. Keep the clause; the engine re-collates.
11493 let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _) =
11494 self.parse_type_with_implied_flags()?;
11495 let collation = if coll_explicit {
11496 coll_name.map(|n| (coll, n))
11497 } else {
11498 None
11499 };
11500 let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11501 {
11502 self.advance();
11503 Some(self.parse_expr(0)?)
11504 } else {
11505 None
11506 };
11507 Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11508 column: col_name,
11509 new_type,
11510 using,
11511 collation,
11512 }])
11513 }
11514 // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11515 // PG also supports `RENAME TO new_table` for table-name
11516 // rename; that surface is deferred (pg_dump never emits
11517 // it). If the first post-RENAME ident is `TO`, the user
11518 // is asking for table rename — error with a clear
11519 // message rather than misparsing `TO` as a column name.
11520 Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11521 self.advance();
11522 // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11523 // table-name rename (mailrs round-10 A.5 — used
11524 // by migrate-042's `RENAME TO email_contacts`).
11525 // `TO` lexes as Token::To.
11526 if matches!(self.peek(), Token::To)
11527 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11528 {
11529 self.advance();
11530 let new = self.expect_ident_like()?;
11531 return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11532 new,
11533 }]);
11534 }
11535 // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11536 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11537 self.advance();
11538 let old = self.expect_ident_like()?;
11539 if matches!(self.peek(), Token::To) {
11540 self.advance();
11541 } else {
11542 self.expect_keyword_ident("to")?;
11543 }
11544 let new = self.expect_ident_like()?;
11545 return Ok(alloc::vec![
11546 crate::ast::AlterTableTarget::RenameConstraint { old, new }
11547 ]);
11548 }
11549 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11550 self.advance();
11551 }
11552 let old = self.expect_ident_like()?;
11553 // `TO` is a reserved keyword token; accept both
11554 // Token::To and Token::Ident("to") for consistency.
11555 if matches!(self.peek(), Token::To) {
11556 self.advance();
11557 } else {
11558 self.expect_keyword_ident("to")?;
11559 }
11560 let new = self.expect_ident_like()?;
11561 Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11562 old,
11563 new,
11564 }])
11565 }
11566 // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11567 // { ALL | <name> }`. pg_dump --disable-triggers wraps
11568 // every data block with these. Real disable semantics —
11569 // not no-op — because reload correctness assumes the
11570 // triggers don't fire (rows already carry their
11571 // computed values from prod).
11572 Token::Ident(s)
11573 if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11574 {
11575 let enabled = s.eq_ignore_ascii_case("enable");
11576 self.advance();
11577 // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11578 // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11579 // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11580 // pg_dump output) — anything else falls through to
11581 // the catch-all error below.
11582 // v7.22 (round-13 T3) — mysqldump wraps every data
11583 // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11584 // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11585 // maintains indexes incrementally — engine no-op.
11586 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11587 self.advance();
11588 return Ok(Vec::new());
11589 }
11590 // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11591 // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11592 // to gate triggers on session_replication_role; SPG
11593 // has no replica role, so the prefix is consumed and
11594 // treated identically to the plain ENABLE/DISABLE
11595 // TRIGGER form.
11596 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11597 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11598 {
11599 self.advance();
11600 }
11601 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11602 return Err(self.err(alloc::format!(
11603 "expected TRIGGER after {}, got {:?}",
11604 if enabled { "ENABLE" } else { "DISABLE" },
11605 self.peek()
11606 )));
11607 }
11608 self.advance();
11609 // `ALL` lexes as Token::All (reserved); also
11610 // accept Token::Ident("all") for symmetry.
11611 // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11612 // TRIGGER selectors. USER (= all user triggers) is
11613 // semantically ALL here; REPLICA / ALWAYS gate on
11614 // session_replication_role which SPG doesn't track.
11615 // All map to TriggerSelector::All.
11616 let which = if matches!(self.peek(), Token::All)
11617 || matches!(self.peek(), Token::Ident(s)
11618 if s.eq_ignore_ascii_case("all")
11619 || s.eq_ignore_ascii_case("user")
11620 || s.eq_ignore_ascii_case("replica")
11621 || s.eq_ignore_ascii_case("always"))
11622 {
11623 self.advance();
11624 crate::ast::TriggerSelector::All
11625 } else {
11626 let name = self.expect_ident_like()?;
11627 crate::ast::TriggerSelector::Named(name)
11628 };
11629 Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11630 which,
11631 enabled,
11632 }])
11633 }
11634 // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11635 Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11636 self.advance();
11637 if !matches!(self.peek(), Token::Partition)
11638 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11639 if s.eq_ignore_ascii_case("partition"))
11640 {
11641 return Err(self.err(alloc::format!(
11642 "expected PARTITION after ATTACH, got {:?}",
11643 self.peek()
11644 )));
11645 }
11646 self.advance();
11647 let child = self.expect_ident_like()?;
11648 let bounds = self.parse_partition_bounds_tail()?;
11649 Ok(alloc::vec![
11650 crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11651 ])
11652 }
11653 // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11654 Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11655 self.advance();
11656 if !matches!(self.peek(), Token::Partition)
11657 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11658 if s.eq_ignore_ascii_case("partition"))
11659 {
11660 return Err(self.err(alloc::format!(
11661 "expected PARTITION after DETACH, got {:?}",
11662 self.peek()
11663 )));
11664 }
11665 self.advance();
11666 let child = self.expect_ident_like()?;
11667 let mut concurrently = false;
11668 let mut finalize = false;
11669 loop {
11670 match self.peek().clone() {
11671 Token::Ident(s) | Token::QuotedIdent(s)
11672 if s.eq_ignore_ascii_case("concurrently") =>
11673 {
11674 self.advance();
11675 concurrently = true;
11676 }
11677 Token::Ident(s) | Token::QuotedIdent(s)
11678 if s.eq_ignore_ascii_case("finalize") =>
11679 {
11680 self.advance();
11681 finalize = true;
11682 }
11683 _ => break,
11684 }
11685 }
11686 Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11687 child,
11688 concurrently,
11689 finalize,
11690 }])
11691 }
11692 other => Err(self.err(alloc::format!(
11693 "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11694 ))),
11695 }
11696 }
11697
11698 /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11699 /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11700 /// TABLE … ATTACH PARTITION. Shares the same grammar as
11701 /// `parse_partition_of_tail`'s bounds branch.
11702 /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11703 /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11704 /// lowering each to the respective AlterTableTarget. Any
11705 /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11706 /// no-op via consume_until_statement_boundary.
11707 fn parse_alter_column_drop_tail(
11708 &mut self,
11709 col_name: String,
11710 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11711 match self.peek().clone() {
11712 Token::Default => {
11713 self.advance();
11714 Ok(alloc::vec![
11715 crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11716 ])
11717 }
11718 Token::Not => {
11719 self.advance();
11720 if !matches!(self.peek(), Token::Null) {
11721 return Err(self.err(alloc::format!(
11722 "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11723 self.peek()
11724 )));
11725 }
11726 self.advance();
11727 Ok(alloc::vec![
11728 crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11729 ])
11730 }
11731 // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11732 // generated column into a plain column.
11733 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11734 self.advance();
11735 // v7.39 (round 187, U10) — IF EXISTS was consumed but
11736 // dropped, so the engine still errored on a plain
11737 // column; PG's semantics are NOTICE + skip.
11738 let mut if_exists = false;
11739 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11740 self.advance();
11741 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11742 self.advance();
11743 if_exists = true;
11744 }
11745 }
11746 Ok(alloc::vec![
11747 crate::ast::AlterTableTarget::AlterColumnDropExpression {
11748 column: col_name,
11749 if_exists,
11750 }
11751 ])
11752 }
11753 // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11754 // identity column into a plain column.
11755 Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11756 self.advance();
11757 let mut if_exists = false;
11758 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11759 self.advance();
11760 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11761 self.advance();
11762 if_exists = true;
11763 }
11764 }
11765 Ok(alloc::vec![
11766 crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11767 column: col_name,
11768 if_exists,
11769 }
11770 ])
11771 }
11772 _ => {
11773 self.consume_until_statement_boundary();
11774 Ok(Vec::new())
11775 }
11776 }
11777 }
11778
11779 /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11780 /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11781 /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11782 /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11783 fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11784 let mut opts = crate::ast::CopyOptions::default();
11785 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11786 return Ok(opts);
11787 }
11788 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11789 self.advance();
11790 }
11791 if matches!(self.peek(), Token::LParen) {
11792 self.advance();
11793 loop {
11794 self.parse_one_copy_option(&mut opts)?;
11795 match self.peek() {
11796 Token::Comma => {
11797 self.advance();
11798 }
11799 Token::RParen => {
11800 self.advance();
11801 break;
11802 }
11803 other => {
11804 return Err(self.err(alloc::format!(
11805 "expected ',' or ')' in COPY options, got {other:?}"
11806 )));
11807 }
11808 }
11809 }
11810 } else {
11811 while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11812 self.parse_one_copy_option(&mut opts)?;
11813 }
11814 }
11815 if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11816 return Err(self.err(alloc::format!(
11817 "unexpected token after COPY options: {:?}",
11818 self.peek()
11819 )));
11820 }
11821 Ok(opts)
11822 }
11823
11824 fn parse_one_copy_option(
11825 &mut self,
11826 opts: &mut crate::ast::CopyOptions,
11827 ) -> Result<(), ParseError> {
11828 use crate::ast::CopyFormat;
11829 // The option keyword. NULL lexes as its own token; the rest are
11830 // bare identifiers.
11831 let kw = match self.advance() {
11832 Token::Null => alloc::string::String::from("NULL"),
11833 Token::Ident(s) => s.to_uppercase(),
11834 other => {
11835 return Err(self.err(alloc::format!(
11836 "expected a COPY option keyword, got {other:?}"
11837 )));
11838 }
11839 };
11840 match kw.as_str() {
11841 "FORMAT" => {
11842 let fmt = self.expect_ident_like()?;
11843 match fmt.to_ascii_uppercase().as_str() {
11844 "CSV" => opts.format = CopyFormat::Csv,
11845 "TEXT" => opts.format = CopyFormat::Text,
11846 other => {
11847 return Err(self.err(alloc::format!(
11848 "COPY format \"{}\" not recognized",
11849 other.to_ascii_lowercase()
11850 )));
11851 }
11852 }
11853 }
11854 // Legacy bare format keywords.
11855 "CSV" => opts.format = CopyFormat::Csv,
11856 "TEXT" => opts.format = CopyFormat::Text,
11857 "HEADER" => {
11858 opts.header = match self.peek() {
11859 Token::True => {
11860 self.advance();
11861 true
11862 }
11863 Token::False => {
11864 self.advance();
11865 false
11866 }
11867 Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
11868 self.advance();
11869 true
11870 }
11871 Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
11872 self.advance();
11873 false
11874 }
11875 // Bare HEADER (no boolean) means HEADER true.
11876 _ => true,
11877 };
11878 }
11879 // r1066 (7.38 S5.1) — pgbench 14+ loads with
11880 // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
11881 // vacuum bookkeeping on a freshly created/truncated
11882 // table; SPG's per-statement visibility makes it a
11883 // faithful no-op, and rejecting it aborted `pgbench -i`
11884 // against the drop-in. Accept ON/OFF/bare, change nothing.
11885 "FREEZE" => match self.peek() {
11886 Token::True | Token::False => {
11887 self.advance();
11888 }
11889 Token::Ident(s)
11890 if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
11891 {
11892 self.advance();
11893 }
11894 _ => {}
11895 },
11896 "DELIMITER" | "QUOTE" | "ESCAPE" => {
11897 let s = match self.advance() {
11898 Token::String(s) => s,
11899 other => {
11900 return Err(self.err(alloc::format!(
11901 "COPY {kw} expects a single-character string, got {other:?}"
11902 )));
11903 }
11904 };
11905 // v7.39 (round 247) — PG's wording (0A000), keyword in
11906 // lowercase: "COPY delimiter must be a single one-byte
11907 // character".
11908 let one_byte_err = || {
11909 self.err(alloc::format!(
11910 "COPY {} must be a single one-byte character",
11911 kw.to_ascii_lowercase()
11912 ))
11913 };
11914 let mut chars = s.chars();
11915 let c = chars.next().ok_or_else(one_byte_err)?;
11916 if chars.next().is_some() || c.len_utf8() != 1 {
11917 return Err(one_byte_err());
11918 }
11919 match kw.as_str() {
11920 "DELIMITER" => opts.delimiter = Some(c),
11921 "QUOTE" => opts.quote = Some(c),
11922 _ => opts.escape = Some(c),
11923 }
11924 }
11925 // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
11926 "FORCE_QUOTE" => {
11927 if matches!(self.peek(), Token::Star) {
11928 self.advance();
11929 opts.force_quote = Some(Vec::new());
11930 } else {
11931 if !matches!(self.peek(), Token::LParen) {
11932 return Err(self.err(alloc::format!(
11933 "expected '(' or '*' after FORCE_QUOTE, got {:?}",
11934 self.peek()
11935 )));
11936 }
11937 self.advance();
11938 let mut cols = Vec::new();
11939 loop {
11940 cols.push(self.expect_ident_like()?);
11941 match self.peek() {
11942 Token::Comma => {
11943 self.advance();
11944 }
11945 Token::RParen => {
11946 self.advance();
11947 break;
11948 }
11949 other => {
11950 return Err(self.err(alloc::format!(
11951 "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
11952 )));
11953 }
11954 }
11955 }
11956 opts.force_quote = Some(cols);
11957 }
11958 }
11959 "NULL" => {
11960 opts.null_str = Some(match self.advance() {
11961 Token::String(s) => s,
11962 other => {
11963 return Err(self.err(alloc::format!(
11964 "COPY NULL expects a quoted string, got {other:?}"
11965 )));
11966 }
11967 });
11968 }
11969 // v7.39 (round 265) — the two CSV FROM-side column lists. Same
11970 // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
11971 // FORCE_NULL too.
11972 "FORCE_NOT_NULL" | "FORCE_NULL" => {
11973 let cols = self.parse_copy_column_list(&kw)?;
11974 if kw == "FORCE_NOT_NULL" {
11975 opts.force_not_null = Some(cols);
11976 } else {
11977 opts.force_null = Some(cols);
11978 }
11979 }
11980 other => {
11981 // PG's wording, lowercased option name.
11982 return Err(self.err(alloc::format!(
11983 "option \"{}\" not recognized",
11984 other.to_ascii_lowercase()
11985 )));
11986 }
11987 }
11988 Ok(())
11989 }
11990
11991 /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
11992 /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
11993 /// is the `*` spelling.
11994 fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
11995 if matches!(self.peek(), Token::Star) {
11996 self.advance();
11997 return Ok(Vec::new());
11998 }
11999 if !matches!(self.peek(), Token::LParen) {
12000 return Err(self.err(alloc::format!(
12001 "expected '(' or '*' after {kw}, got {:?}",
12002 self.peek()
12003 )));
12004 }
12005 self.advance();
12006 let mut cols = Vec::new();
12007 loop {
12008 cols.push(self.expect_ident_like()?);
12009 match self.peek() {
12010 Token::Comma => {
12011 self.advance();
12012 }
12013 Token::RParen => {
12014 self.advance();
12015 break;
12016 }
12017 other => {
12018 return Err(self.err(alloc::format!(
12019 "expected ',' or ')' in {kw} list, got {other:?}"
12020 )));
12021 }
12022 }
12023 }
12024 Ok(cols)
12025 }
12026
12027 fn parse_partition_bounds_tail(
12028 &mut self,
12029 ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
12030 use crate::ast::PartitionOfBoundsAst;
12031 match self.peek() {
12032 Token::Default => {
12033 self.advance();
12034 Ok(PartitionOfBoundsAst::Default)
12035 }
12036 Token::For => {
12037 self.advance();
12038 if !matches!(self.peek(), Token::Values) {
12039 return Err(
12040 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
12041 );
12042 }
12043 self.advance();
12044 let want_with = matches!(
12045 self.peek(),
12046 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
12047 );
12048 if want_with {
12049 self.advance();
12050 if !matches!(self.peek(), Token::LParen) {
12051 return Err(self.err(format!(
12052 "expected '(' after FOR VALUES WITH, got {:?}",
12053 self.peek()
12054 )));
12055 }
12056 self.advance();
12057 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
12058 loop {
12059 let key = self.expect_ident_like()?;
12060 let n = match self.peek().clone() {
12061 Token::Integer(v) if u32::try_from(v).is_ok() => {
12062 self.advance();
12063 v as u32
12064 }
12065 other => {
12066 return Err(self.err(format!(
12067 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
12068 )));
12069 }
12070 };
12071 match key.to_ascii_uppercase().as_str() {
12072 "MODULUS" => modulus = Some(n),
12073 "REMAINDER" => remainder = Some(n),
12074 other => {
12075 return Err(self.err(format!(
12076 "FOR VALUES WITH: unknown key {other:?}; \
12077 expected MODULUS or REMAINDER"
12078 )));
12079 }
12080 }
12081 match self.peek() {
12082 Token::Comma => {
12083 self.advance();
12084 }
12085 Token::RParen => {
12086 self.advance();
12087 break;
12088 }
12089 other => {
12090 return Err(self.err(format!(
12091 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
12092 )));
12093 }
12094 }
12095 }
12096 let modulus = modulus
12097 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
12098 let remainder = remainder.ok_or_else(|| {
12099 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
12100 })?;
12101 if modulus == 0 {
12102 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
12103 }
12104 if remainder >= modulus {
12105 return Err(self.err(format!(
12106 "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
12107 )));
12108 }
12109 return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
12110 }
12111 match self.peek() {
12112 Token::From => {
12113 self.advance();
12114 let lower = Box::new(self.parse_partition_bound_expr()?);
12115 if !matches!(self.peek(), Token::To) {
12116 return Err(self.err(format!(
12117 "expected TO after FROM (...), got {:?}",
12118 self.peek()
12119 )));
12120 }
12121 self.advance();
12122 let upper = Box::new(self.parse_partition_bound_expr()?);
12123 Ok(PartitionOfBoundsAst::Range { lower, upper })
12124 }
12125 Token::In => {
12126 self.advance();
12127 if !matches!(self.peek(), Token::LParen) {
12128 return Err(self.err(format!(
12129 "expected '(' after FOR VALUES IN, got {:?}",
12130 self.peek()
12131 )));
12132 }
12133 self.advance();
12134 let mut values = Vec::new();
12135 loop {
12136 values.push(self.parse_expr(0)?);
12137 match self.peek() {
12138 Token::Comma => {
12139 self.advance();
12140 }
12141 Token::RParen => {
12142 self.advance();
12143 break;
12144 }
12145 other => {
12146 return Err(self.err(format!(
12147 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
12148 )));
12149 }
12150 }
12151 }
12152 if values.is_empty() {
12153 return Err(
12154 self.err("FOR VALUES IN requires at least one literal".to_string())
12155 );
12156 }
12157 Ok(PartitionOfBoundsAst::List { values })
12158 }
12159 other => Err(self.err(format!(
12160 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
12161 ))),
12162 }
12163 }
12164 other => Err(self.err(format!(
12165 "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
12166 ))),
12167 }
12168 }
12169
12170 /// v7.16.2 — peek for `information_schema.<tbl>` /
12171 /// `pg_catalog.<tbl>` triples and, if matched, consume all
12172 /// three tokens + return a synthetic table name the engine's
12173 /// SELECT path recognises as a virtual view. Returns `None`
12174 /// when the head doesn't look like a meta-qualified name.
12175 /// Used by `parse_table_ref` to bypass the
12176 /// `expect_ident_like` schema-strip for these specific PG
12177 /// meta schemas (mailrs round-10 A.3).
12178 fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12179 // Extract the schema name. Must be a plain ident token.
12180 let schema = match self.tokens.get(self.pos) {
12181 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12182 _ => return None,
12183 };
12184 // Dot.
12185 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12186 return None;
12187 }
12188 // The table-side ident may lex as a reserved keyword
12189 // (e.g. `Token::Tables`). Tolerate the common ones via a
12190 // helper that reads the trailing token's underlying name.
12191 let tbl = match self.tokens.get(self.pos + 2)? {
12192 Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12193 Token::Tables => "tables".to_string(),
12194 // Other PG meta table names that may collide with
12195 // reserved keywords land here as needed.
12196 _ => return None,
12197 };
12198 // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12199 // names so the synthetic name doesn't double-prefix
12200 // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12201 let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12202 ("__spg_info_", tbl.to_ascii_lowercase())
12203 } else if schema.eq_ignore_ascii_case("pg_catalog") {
12204 // v7.39 (round 541) — only the catalogs SPG actually
12205 // synthesises are rewritten, which is what the BARE path
12206 // has always checked. Anything else keeps its own name and
12207 // takes the ordinary route: `pg_stat_activity` and friends
12208 // resolve through meta_view_result, and a name that is no
12209 // catalog at all gets PG's "relation does not exist"
12210 // instead of a message about a view SPG cannot materialise.
12211 let lowered = tbl.to_ascii_lowercase();
12212 if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12213 self.advance(); // schema
12214 self.advance(); // dot
12215 self.advance(); // tbl
12216 return Some((lowered.clone(), lowered));
12217 }
12218 let bare = lowered
12219 .strip_prefix("pg_")
12220 .map(alloc::string::String::from)
12221 .unwrap_or(lowered);
12222 ("__spg_pg_", bare)
12223 } else if schema.eq_ignore_ascii_case("mysql") {
12224 // v7.17.0 Phase 3.P0-65 — MySQL system schema
12225 // (`mysql.user`, `mysql.db`). Same synthetic-name
12226 // shape as pg_catalog.
12227 ("__spg_mysql_", tbl.to_ascii_lowercase())
12228 } else {
12229 return None;
12230 };
12231 self.advance(); // schema
12232 self.advance(); // dot
12233 self.advance(); // tbl
12234 Some((
12235 alloc::format!("{prefix}{normalised}"),
12236 tbl.to_ascii_lowercase(),
12237 ))
12238 }
12239
12240 /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12241 /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12242 /// implicit front of every search_path, so a bare reference to a
12243 /// known catalog table always means the catalog table. Only the
12244 /// names the engine actually synthesises are recognised — any
12245 /// other `pg_*` ident stays a user table (mailrs embed round-12).
12246 fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12247 // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12248 // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12249 // `pg_catalog` at the front of every search_path. (pg_stat_activity
12250 // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12251 // through the meta_view_result path instead, and already resolve
12252 // bare — they must NOT be listed here or the __spg_ rewrite would
12253 // mis-target them.)
12254 const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12255 let name = match self.tokens.get(self.pos) {
12256 Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12257 _ => return None,
12258 };
12259 // A following dot means this ident is a schema qualifier,
12260 // not a table name — let the qualified path handle it.
12261 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12262 return None;
12263 }
12264 if !PG_META_TABLES.contains(&name.as_str()) {
12265 return None;
12266 }
12267 self.advance();
12268 let bare = name.strip_prefix("pg_").unwrap_or(&name);
12269 Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12270 }
12271
12272 /// Consume a bare ident if its lowercase matches `kw`, else err.
12273 /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12274 /// Peeks only; the caller advances.
12275 fn peek_keyword_ident(&self, kw: &str) -> bool {
12276 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12277 }
12278
12279 fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12280 match self.advance() {
12281 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12282 other => Err(ParseError {
12283 message: format!("expected {kw:?}, got {other:?}"),
12284 token_pos: self.consumed_pos(),
12285 }),
12286 }
12287 }
12288
12289 /// Accept either a quoted identifier (`"foo"`) or a quoted string
12290 /// literal (`'foo'`) — same shape used by CREATE USER for the
12291 /// username slot.
12292 fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12293 match self.advance() {
12294 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12295 other => Err(ParseError {
12296 message: format!("expected identifier or string, got {other:?}"),
12297 token_pos: self.consumed_pos(),
12298 }),
12299 }
12300 }
12301
12302 fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12303 match self.advance() {
12304 Token::String(s) => Ok(s),
12305 other => Err(ParseError {
12306 message: format!("expected quoted string, got {other:?}"),
12307 token_pos: self.consumed_pos(),
12308 }),
12309 }
12310 }
12311
12312 fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12313 // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12314 // subqueries recurse through here without passing
12315 // parse_expr; share the same nesting budget.
12316 self.enter_nested()?;
12317 let r = self.parse_select_stmt_inner();
12318 self.nest_depth -= 1;
12319 r
12320 }
12321
12322 fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12323 // Caller dispatches on Token::Select; the inner helper handles
12324 // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12325 // get a fresh bare-select parse and may not have their own ORDER
12326 // BY / LIMIT.
12327 let mut head = self.parse_bare_select()?;
12328 let into = self.pending_select_into.take();
12329 self.parse_setop_chain_into(&mut head)?;
12330 self.parse_select_tail_into(&mut head)?;
12331 // v7.38.19 — `SELECT … INTO t` lowers to the SAME node as
12332 // `CREATE TABLE t AS SELECT …`, which is what a comment in
12333 // `ast.rs` has claimed since v7.38 and what only CTAS actually
12334 // did. The tail (ORDER BY / LIMIT) is parsed first so it belongs
12335 // to the body, as it does in PostgreSQL.
12336 if let Some((name, temporary)) = into {
12337 return Ok(Statement::CreateMaterializedView(
12338 crate::ast::CreateMaterializedViewStatement {
12339 temporary,
12340 name,
12341 if_not_exists: false,
12342 columns: Vec::new(),
12343 body: head,
12344 with_data: true,
12345 as_plain_table: true,
12346 },
12347 ));
12348 }
12349 Ok(Statement::Select(head))
12350 }
12351
12352 /// v7.37.17 (17.6 siblings) — the three SQL set operations
12353 /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12354 /// token), and INTERSECT [ALL] (a bare ident — it was never
12355 /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12356 /// tighter than UNION / EXCEPT — the executor folds the chain
12357 /// left-to-right, which is already correct for LEADING
12358 /// intersects; an INTERSECT pair that FOLLOWS a union/except
12359 /// pair nests into that previous peer, so A UNION B INTERSECT C
12360 /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12361 /// groups.
12362 fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12363 // A parenthesized group arrives with its own (already
12364 // regrouped) unions on `head`; only the pairs THIS chain
12365 // appends participate in the precedence regroup below —
12366 // nesting an outer INTERSECT into a group-internal peer
12367 // would dissolve the explicit grouping.
12368 let boundary = head.unions.len();
12369 loop {
12370 let base = match self.peek() {
12371 Token::Union => UnionKind::Distinct,
12372 Token::Except => UnionKind::Except,
12373 Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12374 _ => break,
12375 };
12376 self.advance();
12377 let kind = if matches!(self.peek(), Token::All) {
12378 self.advance();
12379 match base {
12380 UnionKind::Distinct => UnionKind::All,
12381 UnionKind::Except => UnionKind::ExceptAll,
12382 _ => UnionKind::IntersectAll,
12383 }
12384 } else {
12385 base
12386 };
12387 let peer = self.parse_bare_select()?;
12388 head.unions.push((kind, peer));
12389 }
12390 let mut pairs = core::mem::take(&mut head.unions);
12391 let tail = pairs.split_off(boundary);
12392 let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12393 for (kind, peer) in tail {
12394 let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12395 // An intersect nests into the previous element of THIS
12396 // chain only; with no new previous element it stays at
12397 // the outer level (the left fold applies it to the
12398 // whole head, group included).
12399 match (
12400 is_intersect,
12401 regrouped.len() > boundary,
12402 regrouped.last_mut(),
12403 ) {
12404 (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12405 _ => regrouped.push((kind, peer)),
12406 }
12407 }
12408 head.unions = regrouped;
12409 Ok(())
12410 }
12411
12412 /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12413 /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12414 /// the top-level bare VALUES statement reuses it verbatim.
12415 /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12416 /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12417 /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12418 /// where the grouping-set universe is still in scope.
12419 fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12420 if !matches!(self.peek(), Token::Order) {
12421 return Ok(Vec::new());
12422 }
12423 self.advance();
12424 if !self.peek_is_by() {
12425 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12426 }
12427 self.advance();
12428 let mut keys = Vec::new();
12429 loop {
12430 // v7.39 (round 691) — save/restore, the discipline this parser
12431 // already uses around `pending_sample_preds`, so a subquery inside
12432 // a key neither inherits nor leaks the channel.
12433 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12434 let saved_coll = self.order_key_collation.take();
12435 let parsed = self.parse_expr(0);
12436 self.in_order_by_key = saved_flag;
12437 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12438 let expr = parsed?;
12439 let desc = if matches!(self.peek(), Token::Desc) {
12440 self.advance();
12441 true
12442 } else if matches!(self.peek(), Token::Asc) {
12443 self.advance();
12444 false
12445 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12446 // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12447 // one ordering per type, so the btree comparison operators map
12448 // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12449 // would need a custom operator class — honest error.
12450 self.advance();
12451 match self.advance() {
12452 Token::Lt | Token::LtEq => false,
12453 Token::Gt | Token::GtEq => true,
12454 other => {
12455 return Err(self.err(alloc::format!(
12456 "ORDER BY USING supports the btree comparison \
12457 operators (< <= > >=); got {other:?}"
12458 )));
12459 }
12460 }
12461 } else {
12462 false
12463 };
12464 // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12465 let nulls_first = self.parse_optional_nulls_placement()?;
12466 keys.push(OrderBy {
12467 expr,
12468 desc,
12469 nulls_first,
12470 collation,
12471 });
12472 if matches!(self.peek(), Token::Comma) {
12473 self.advance();
12474 } else {
12475 break;
12476 }
12477 }
12478 Ok(keys)
12479 }
12480
12481 fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12482 // v7.39 (round 135) — a grouping-set query may have already parsed +
12483 // rewritten its ORDER BY (to reference synthetic grouping columns); if
12484 // no ORDER BY token is present, keep that pre-set order_by rather than
12485 // clobbering it with an empty list.
12486 let parsed_keys = self.parse_order_by_keys()?;
12487 head.order_by = if parsed_keys.is_empty() {
12488 core::mem::take(&mut head.order_by)
12489 } else {
12490 parsed_keys
12491 };
12492 // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12493 // order. PG's grammar takes a limit clause and an offset clause
12494 // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12495 // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12496 // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12497 // spelling died on `expected end of input, got Limit`.
12498 //
12499 // Each may appear at most once, and LIMIT and FETCH FIRST are
12500 // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12501 // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12502 // A second one is left unconsumed here, which the caller reports
12503 // as trailing input rather than silently taking the last.
12504 let mut saw_limit = false;
12505 let mut saw_offset = false;
12506 loop {
12507 if !saw_limit && matches!(self.peek(), Token::Limit) {
12508 self.advance();
12509 // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12510 // PG synonyms for "no limit". Treat both as None
12511 // (no head.limit set) so the engine's existing
12512 // unlimited-result path takes over. Reject was the
12513 // pre-5.1 behaviour and broke pg_dump-flavoured
12514 // tooling that occasionally emits LIMIT NULL.
12515 if self.consume_limit_unbounded_sentinel() {
12516 head.limit = None;
12517 } else {
12518 let first = self.parse_limit_expr("LIMIT")?;
12519 // MySQL `LIMIT offset, count` — the first number is
12520 // the offset when a comma follows.
12521 if matches!(self.peek(), Token::Comma) {
12522 self.advance();
12523 let count = self.parse_limit_expr("LIMIT")?;
12524 head.offset = Some(first);
12525 saw_offset = true;
12526 head.limit = Some(count);
12527 } else {
12528 head.limit = Some(first);
12529 }
12530 }
12531 saw_limit = true;
12532 continue;
12533 }
12534 if !saw_offset && matches!(self.peek(), Token::Offset) {
12535 self.advance();
12536 // PG also accepts an optional `ROW` / `ROWS` trailer
12537 // after the offset value (`OFFSET 10 ROWS`). The
12538 // FETCH-FIRST branch below relies on the same.
12539 let off = self.parse_limit_expr("OFFSET")?;
12540 self.consume_optional_rows_keyword();
12541 head.offset = Some(off);
12542 saw_offset = true;
12543 continue;
12544 }
12545 // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12546 // the SQL-standard alias for LIMIT. PG accepts both
12547 // spellings interchangeably; pg_dump emits FETCH FIRST in
12548 // newer versions. We map it onto `head.limit` so the
12549 // engine path is unified.
12550 if !saw_limit
12551 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12552 if s.eq_ignore_ascii_case("fetch"))
12553 {
12554 self.advance(); // FETCH
12555 // `FIRST` or `NEXT` (both legal per SQL standard).
12556 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12557 if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12558 {
12559 self.advance();
12560 }
12561 // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12562 // implicit 1 — but we always consume one if present).
12563 let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12564 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12565 {
12566 // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12567 crate::ast::LimitExpr::Literal(1)
12568 } else {
12569 self.parse_limit_expr("FETCH FIRST")?
12570 };
12571 // Eat `ROW` / `ROWS` if not already consumed above.
12572 self.consume_optional_rows_keyword();
12573 // Optional `ONLY` (the spec form) — or the SQL:2008
12574 // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12575 // now honours WITH TIES by extending past the LIMIT
12576 // truncation point through every row that shares the
12577 // last-kept row's ORDER BY key.
12578 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12579 if s.eq_ignore_ascii_case("only"))
12580 {
12581 self.advance();
12582 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12583 if s.eq_ignore_ascii_case("with"))
12584 {
12585 self.advance(); // WITH
12586 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12587 if s.eq_ignore_ascii_case("ties"))
12588 {
12589 self.advance();
12590 head.limit_with_ties = true;
12591 }
12592 }
12593 head.limit = Some(count);
12594 saw_limit = true;
12595 continue;
12596 }
12597 break;
12598 }
12599 // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12600 // FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12601 // [ OF table_name [, …] ]
12602 // [ NOWAIT | SKIP LOCKED ]
12603 // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12604 // FOR SHARE OF t2`). SPG is a single-writer engine — every
12605 // SELECT already returns a consistent snapshot — so these
12606 // are accept-and-discard: the parser absorbs them so
12607 // mailrs / Rails / Django code paths that emit `SELECT
12608 // … FOR UPDATE` for advisory pessimistic locking load
12609 // without a parser error. The on-disk locking model is
12610 // unchanged; callers that rely on FOR UPDATE for read-
12611 // through-write ordering still get the right answer
12612 // because SPG serialises writes anyway.
12613 head.locking = self
12614 .consume_optional_for_lock_clauses()
12615 .map(alloc::boxed::Box::new);
12616 Ok(())
12617 }
12618
12619 /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12620 /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12621 /// LOCKED ]` trailers. Each clause is fully accepted and
12622 /// discarded — SPG's single-writer model already satisfies the
12623 /// callers' implicit ordering requirement. Stops at the first
12624 /// token that isn't `FOR`.
12625 fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12626 // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12627 // not discarded. PG keeps the strongest of several clauses; the
12628 // policy of the last one wins, which is what this loop records.
12629 let mut seen: Option<crate::ast::LockingClause> = None;
12630 while matches!(self.peek(), Token::For) {
12631 // v7.37.14 (A2.5-stub) — record that this query asked
12632 // for a row lock the parser is about to silently
12633 // discard. Operators surface the count via
12634 // `spg_sql::silent_for_update_count()` so they can
12635 // gauge how much of the workload depends on advisory
12636 // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12637 // before v7.37.15's per-row tuple locking lands.
12638 crate::record_silent_for_update_clause();
12639 self.advance(); // FOR
12640 // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12641 // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12642 let mut no_key = false;
12643 let mut key = false;
12644 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12645 if s.eq_ignore_ascii_case("no"))
12646 {
12647 self.advance(); // NO
12648 no_key = true;
12649 // The next ident should be KEY but be generous;
12650 // anything followed by UPDATE/SHARE is accepted.
12651 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12652 if s.eq_ignore_ascii_case("key"))
12653 {
12654 self.advance(); // KEY
12655 }
12656 }
12657 // `KEY` prefix (PG `FOR KEY SHARE`).
12658 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12659 if s.eq_ignore_ascii_case("key"))
12660 {
12661 self.advance(); // KEY
12662 key = true;
12663 }
12664 // Lock-strength keyword: UPDATE / SHARE. Required, but
12665 // we're lenient — an unexpected token here just bails
12666 // (we already consumed FOR; caller's downstream
12667 // dispatch will error if anything actually depends on
12668 // the trailing tokens).
12669 let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12670 if s.eq_ignore_ascii_case("update"));
12671 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12672 if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12673 {
12674 self.advance();
12675 use crate::ast::LockStrength as LS;
12676 let strength = match (is_update, no_key, key) {
12677 (true, true, _) => LS::NoKeyUpdate,
12678 (true, _, _) => LS::Update,
12679 (false, _, true) => LS::KeyShare,
12680 (false, _, _) => LS::Share,
12681 };
12682 seen = Some(crate::ast::LockingClause {
12683 strength,
12684 of_tables: alloc::vec::Vec::new(),
12685 policy: crate::ast::LockWait::Wait,
12686 });
12687 } else {
12688 // FOR by itself (or `FOR KEY` with nothing after) —
12689 // give up on the lock-clause path. We've already
12690 // advanced past FOR; further attempts to parse
12691 // here would clobber state.
12692 return seen;
12693 }
12694 // Optional `OF tbl[, tbl …]`. mailrs emits this when
12695 // joining and locking only a subset of tables.
12696 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12697 if s.eq_ignore_ascii_case("of"))
12698 {
12699 self.advance(); // OF
12700 #[allow(clippy::while_let_loop)]
12701 loop {
12702 match self.peek() {
12703 Token::Ident(_) | Token::QuotedIdent(_) => {
12704 // v7.39 (round 294) — the name is CAPTURED now: PG
12705 // validates it against the FROM clause, and an
12706 // uncaptured list silently means "lock everything".
12707 let mut nm = match self.advance() {
12708 Token::Ident(n) | Token::QuotedIdent(n) => n,
12709 _ => alloc::string::String::new(),
12710 };
12711 // Optional schema-qualified `schema.table`.
12712 if matches!(self.peek(), Token::Dot) {
12713 self.advance();
12714 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12715 {
12716 self.advance();
12717 nm = n;
12718 }
12719 }
12720 if let Some(c) = seen.as_mut() {
12721 c.of_tables.push(nm);
12722 }
12723 }
12724 _ => break,
12725 }
12726 if matches!(self.peek(), Token::Comma) {
12727 self.advance();
12728 } else {
12729 break;
12730 }
12731 }
12732 }
12733 // Optional `NOWAIT` | `SKIP LOCKED`.
12734 match self.peek().clone() {
12735 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12736 self.advance();
12737 if let Some(c) = seen.as_mut() {
12738 c.policy = crate::ast::LockWait::NoWait;
12739 }
12740 }
12741 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12742 self.advance(); // SKIP
12743 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12744 if s.eq_ignore_ascii_case("locked"))
12745 {
12746 self.advance(); // LOCKED
12747 if let Some(c) = seen.as_mut() {
12748 c.policy = crate::ast::LockWait::SkipLocked;
12749 }
12750 }
12751 }
12752 _ => {}
12753 }
12754 // Loop: PG allows multiple FOR clauses chained.
12755 }
12756 seen
12757 }
12758
12759 /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12760 /// Bind value gets resolved during prepared-statement Execute;
12761 /// the Pratt expression parser would over-accept here (e.g.
12762 /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12763 /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12764 /// sentinel tokens (PG synonyms for "no limit"). Returns true
12765 /// when one was consumed; caller skips the regular
12766 /// limit-value parse and leaves `head.limit` at None.
12767 fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12768 if matches!(self.peek(), Token::Null) {
12769 self.advance();
12770 return true;
12771 }
12772 if matches!(self.peek(), Token::All) {
12773 self.advance();
12774 return true;
12775 }
12776 false
12777 }
12778
12779 /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12780 /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12781 /// SQL-standard shape. No-op when missing.
12782 fn consume_optional_rows_keyword(&mut self) {
12783 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12784 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12785 {
12786 self.advance();
12787 }
12788 }
12789
12790 /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12791 ///
12792 /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12793 /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12794 /// constant, which is why that spelling keeps the token path below.
12795 ///
12796 /// Constants are folded here rather than carried into the tree: the
12797 /// 15+ execution paths that read the row count go through
12798 /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12799 /// means "no limit". A clause the engine could not resolve would
12800 /// therefore return the WHOLE table instead of failing. Folding at
12801 /// parse time keeps that impossible; a non-constant clause is still
12802 /// a clean error (recorded residual — closing it wants a resolution
12803 /// pre-pass on the simple-query path, where `substitute_placeholders`
12804 /// does not run).
12805 fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12806 // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12807 // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12808 // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12809 // ONLY` both work (its grammar takes a c_expr). Both measured
12810 // against PG 18.4 in round 305.
12811 if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12812 return self.parse_limit_constant(label);
12813 }
12814 // One pass, no rewind: `advance()` takes each token by
12815 // `mem::replace`, so a consumed token reads back as Eof and this
12816 // parser cannot backtrack. Everything — bare literal included —
12817 // is therefore folded from the parsed expression rather than
12818 // re-read from the token stream.
12819 let start = self.pos;
12820 let e = self.parse_expr(0)?;
12821 if let crate::ast::Expr::Placeholder(n) = e {
12822 return Ok(crate::ast::LimitExpr::Placeholder(n));
12823 }
12824 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12825 match fold_limit_constant(&e) {
12826 Some(Ok(v)) if v < 0 => Err(ParseError {
12827 message: alloc::format!("{neg_label} must not be negative"),
12828 token_pos: start,
12829 }),
12830 Some(Ok(v)) => u32::try_from(v)
12831 .map(crate::ast::LimitExpr::Literal)
12832 .map_err(|_| ParseError {
12833 message: alloc::format!("{label} value too large: {v}"),
12834 token_pos: start,
12835 }),
12836 Some(Err(message)) => Err(ParseError {
12837 message: message.replace("{L}", neg_label),
12838 token_pos: start,
12839 }),
12840 // v7.39 (round 305, V23) — not foldable at parse time
12841 // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
12842 // expression; the engine evaluates it once before dispatch.
12843 None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
12844 }
12845 }
12846
12847 fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12848 // v7.39 (round 239) — PG's row-count clause takes a bigint with its
12849 // coercion rules, not just an integer token: a NUMERIC rounds half
12850 // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
12851 // refused with PG's wording ("LIMIT must not be negative", 2201W /
12852 // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
12853 // content, failing as an input-syntax error on the value. General
12854 // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
12855 // they need an Expr-carrying LimitExpr variant.
12856 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12857 let err_at = |message: alloc::string::String, pos: usize| ParseError {
12858 message,
12859 token_pos: pos,
12860 };
12861 match self.advance() {
12862 Token::Integer(n) if n >= 0 => u32::try_from(n)
12863 .map(crate::ast::LimitExpr::Literal)
12864 .map_err(|_| ParseError {
12865 message: alloc::format!("{label} value too large: {n}"),
12866 token_pos: self.consumed_pos(),
12867 }),
12868 Token::Integer(_) => Err(err_at(
12869 alloc::format!("{neg_label} must not be negative"),
12870 self.pos.saturating_sub(1),
12871 )),
12872 Token::Numeric(t) => {
12873 let pos = self.pos.saturating_sub(1);
12874 let v: f64 = t.parse().map_err(|_| {
12875 err_at(
12876 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12877 pos,
12878 )
12879 })?;
12880 if v < 0.0 {
12881 return Err(err_at(
12882 alloc::format!("{neg_label} must not be negative"),
12883 pos,
12884 ));
12885 }
12886 // Round half away from zero — PG's numeric→bigint cast.
12887 // (no_std: no f64::round; v is non-negative, so truncating
12888 // v + 0.5 is the same thing.)
12889 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
12890 let rounded = (v + 0.5) as u64;
12891 u32::try_from(rounded)
12892 .map(crate::ast::LimitExpr::Literal)
12893 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
12894 }
12895 Token::Minus => {
12896 let pos = self.pos.saturating_sub(1);
12897 match self.peek() {
12898 Token::Integer(_) | Token::Numeric(_) => {
12899 self.advance();
12900 Err(err_at(
12901 alloc::format!("{neg_label} must not be negative"),
12902 pos,
12903 ))
12904 }
12905 other => Err(err_at(
12906 alloc::format!(
12907 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12908 ),
12909 pos,
12910 )),
12911 }
12912 }
12913 Token::String(t) => {
12914 let pos = self.pos.saturating_sub(1);
12915 match t.trim().parse::<i64>() {
12916 Ok(n) if n < 0 => Err(err_at(
12917 alloc::format!("{neg_label} must not be negative"),
12918 pos,
12919 )),
12920 Ok(n) => u32::try_from(n)
12921 .map(crate::ast::LimitExpr::Literal)
12922 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
12923 Err(_) => Err(err_at(
12924 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12925 pos,
12926 )),
12927 }
12928 }
12929 Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
12930 other => Err(ParseError {
12931 message: alloc::format!(
12932 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12933 ),
12934 token_pos: self.consumed_pos(),
12935 }),
12936 }
12937 }
12938
12939 /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
12940 /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
12941 /// `unions` empty and `order_by` / `limit` `None`; the top-level
12942 /// `parse_select_stmt` is responsible for filling those in.
12943 /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
12944 /// call in the expression tree to the per-set integer bitmask
12945 /// (PG semantics: one bit per argument, MSB first; 1 = the key
12946 /// is dropped in this grouping set). Runs during the ROLLUP /
12947 /// CUBE / GROUPING SETS expansion, where the set is known.
12948 /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
12949 /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
12950 fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
12951 if let Expr::FunctionCall { name, .. } = expr
12952 && name.eq_ignore_ascii_case("grouping")
12953 {
12954 if !out.iter().any(|e| e == expr) {
12955 out.push(expr.clone());
12956 }
12957 return;
12958 }
12959 match expr {
12960 Expr::Binary { lhs, rhs, .. } => {
12961 Self::collect_grouping_calls(lhs, out);
12962 Self::collect_grouping_calls(rhs, out);
12963 }
12964 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12965 Self::collect_grouping_calls(expr, out)
12966 }
12967 Expr::FunctionCall { args, .. } => {
12968 for a in args {
12969 Self::collect_grouping_calls(a, out);
12970 }
12971 }
12972 Expr::Case {
12973 operand,
12974 branches,
12975 else_branch,
12976 } => {
12977 if let Some(o) = operand {
12978 Self::collect_grouping_calls(o, out);
12979 }
12980 for (c, v) in branches {
12981 Self::collect_grouping_calls(c, out);
12982 Self::collect_grouping_calls(v, out);
12983 }
12984 if let Some(x) = else_branch {
12985 Self::collect_grouping_calls(x, out);
12986 }
12987 }
12988 _ => {}
12989 }
12990 }
12991
12992 /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
12993 /// `grp_exprs[k]` with a reference to the synthetic ordering column
12994 /// `__grp_ord_k` (injected per grouping-set branch).
12995 fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
12996 if let Expr::FunctionCall { name, .. } = expr
12997 && name.eq_ignore_ascii_case("grouping")
12998 {
12999 if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
13000 *expr = Expr::Column(crate::ast::ColumnName {
13001 qualifier: None,
13002 name: alloc::format!("__grp_ord_{k}"),
13003 });
13004 }
13005 return;
13006 }
13007 match expr {
13008 Expr::Binary { lhs, rhs, .. } => {
13009 Self::rewrite_grouping_to_col(lhs, grp_exprs);
13010 Self::rewrite_grouping_to_col(rhs, grp_exprs);
13011 }
13012 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13013 Self::rewrite_grouping_to_col(expr, grp_exprs)
13014 }
13015 Expr::FunctionCall { args, .. } => {
13016 for a in args {
13017 Self::rewrite_grouping_to_col(a, grp_exprs);
13018 }
13019 }
13020 Expr::Case {
13021 operand,
13022 branches,
13023 else_branch,
13024 } => {
13025 if let Some(o) = operand {
13026 Self::rewrite_grouping_to_col(o, grp_exprs);
13027 }
13028 for (c, v) in branches {
13029 Self::rewrite_grouping_to_col(c, grp_exprs);
13030 Self::rewrite_grouping_to_col(v, grp_exprs);
13031 }
13032 if let Some(x) = else_branch {
13033 Self::rewrite_grouping_to_col(x, grp_exprs);
13034 }
13035 }
13036 _ => {}
13037 }
13038 }
13039
13040 /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
13041 /// as the list of key sets it contributes. A bare expression is one
13042 /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
13043 /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
13044 /// the concatenation of its items' sets, where an item is itself an
13045 /// element, a parenthesized key list, or the empty set `()`. A
13046 /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
13047 /// move together.
13048 fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
13049 let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
13050 // ROLLUP ( … ) / CUBE ( … )
13051 if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
13052 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
13053 {
13054 let is_cube = is_kw(self.peek(), "cube");
13055 self.advance(); // ROLLUP / CUBE
13056 self.advance(); // (
13057 let mut units: Vec<Vec<Expr>> = Vec::new();
13058 loop {
13059 if matches!(self.peek(), Token::LParen) {
13060 // Composite unit: (a, b) rolls up as one.
13061 self.advance();
13062 let mut unit = Vec::new();
13063 if !matches!(self.peek(), Token::RParen) {
13064 loop {
13065 unit.push(self.parse_expr(0)?);
13066 match self.peek() {
13067 Token::Comma => {
13068 self.advance();
13069 }
13070 Token::RParen => break,
13071 other => {
13072 return Err(self.err(format!(
13073 "expected ',' or ')' in grouping unit, got {other:?}"
13074 )));
13075 }
13076 }
13077 }
13078 }
13079 self.advance(); // )
13080 units.push(unit);
13081 } else {
13082 units.push(alloc::vec![self.parse_expr(0)?]);
13083 }
13084 match self.peek() {
13085 Token::Comma => {
13086 self.advance();
13087 }
13088 Token::RParen => break,
13089 other => {
13090 return Err(self.err(format!(
13091 "expected ',' or ')' in grouping list, got {other:?}"
13092 )));
13093 }
13094 }
13095 }
13096 self.advance(); // )
13097 let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
13098 units
13099 .iter()
13100 .zip(unit_sel.iter())
13101 .filter(|(_, keep)| **keep)
13102 .flat_map(|(u, _)| u.iter().cloned())
13103 .collect()
13104 };
13105 let n = units.len();
13106 if is_cube {
13107 let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
13108 .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
13109 .collect();
13110 subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
13111 return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
13112 }
13113 return Ok((0..=n)
13114 .rev()
13115 .map(|keep| {
13116 let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
13117 flatten(&sel)
13118 })
13119 .collect());
13120 }
13121 // GROUPING SETS ( item [, item]* )
13122 if is_kw(self.peek(), "grouping")
13123 && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
13124 {
13125 self.advance(); // GROUPING
13126 self.advance(); // SETS
13127 if !matches!(self.peek(), Token::LParen) {
13128 return Err(self.err(format!(
13129 "expected '(' after GROUPING SETS, got {:?}",
13130 self.peek()
13131 )));
13132 }
13133 self.advance(); // outer (
13134 let mut sets: Vec<Vec<Expr>> = Vec::new();
13135 loop {
13136 if matches!(self.peek(), Token::LParen) {
13137 // A parenthesized key list (or the empty set).
13138 self.advance();
13139 let mut set = Vec::new();
13140 if !matches!(self.peek(), Token::RParen) {
13141 loop {
13142 set.push(self.parse_expr(0)?);
13143 match self.peek() {
13144 Token::Comma => {
13145 self.advance();
13146 }
13147 Token::RParen => break,
13148 other => {
13149 return Err(self.err(format!(
13150 "expected ',' or ')' in grouping set, got {other:?}"
13151 )));
13152 }
13153 }
13154 }
13155 }
13156 self.advance(); // )
13157 sets.push(set);
13158 } else {
13159 // A nested element: ROLLUP/CUBE/GROUPING SETS or a
13160 // bare expression.
13161 sets.extend(self.parse_grouping_element()?);
13162 }
13163 match self.peek() {
13164 Token::Comma => {
13165 self.advance();
13166 }
13167 Token::RParen => break,
13168 other => {
13169 return Err(self.err(format!(
13170 "expected ',' or ')' after a grouping set, got {other:?}"
13171 )));
13172 }
13173 }
13174 }
13175 self.advance(); // outer )
13176 return Ok(sets);
13177 }
13178 Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
13179 }
13180
13181 fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
13182 // v7.38 (read01) — a reference to a key that is dropped in this grouping
13183 // set evaluates to NULL, at any depth. Previously only a *top-level*
13184 // select item equal to a dropped key was nullified, so a key nested in
13185 // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13186 // column and failed to resolve against the set's synthetic schema.
13187 if dropped.iter().any(|d| d == expr) {
13188 *expr = Expr::Literal(Literal::Null);
13189 return;
13190 }
13191 if let Expr::FunctionCall { name, args } = expr
13192 && name.eq_ignore_ascii_case("grouping")
13193 {
13194 let mut mask: i64 = 0;
13195 for a in args.iter() {
13196 mask <<= 1;
13197 if dropped.iter().any(|d| d == a) {
13198 mask |= 1;
13199 }
13200 }
13201 // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13202 // literal: a bare integer in a select item is indistinguishable
13203 // from a positional reference once `ORDER BY 1` substitutes the
13204 // item back in, and the round-232 position check then read the
13205 // mask value as an out-of-range position. The cast changes
13206 // nothing semantically (grouping() is integer).
13207 *expr = Expr::Cast {
13208 expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13209 target: crate::ast::CastTarget::Int,
13210 };
13211 return;
13212 }
13213 // Generic recursion over the common expression shapes the
13214 // SELECT list uses; anything without child expressions is
13215 // left alone.
13216 match expr {
13217 Expr::FunctionCall { args, .. } => {
13218 for a in args {
13219 Self::substitute_grouping_calls(a, dropped);
13220 }
13221 }
13222 Expr::Binary { lhs, rhs, .. } => {
13223 Self::substitute_grouping_calls(lhs, dropped);
13224 Self::substitute_grouping_calls(rhs, dropped);
13225 }
13226 Expr::Unary { expr: inner, .. } => {
13227 Self::substitute_grouping_calls(inner, dropped);
13228 }
13229 Expr::Cast { expr: inner, .. } => {
13230 Self::substitute_grouping_calls(inner, dropped);
13231 }
13232 Expr::Case {
13233 operand,
13234 branches,
13235 else_branch,
13236 } => {
13237 if let Some(op) = operand {
13238 Self::substitute_grouping_calls(op, dropped);
13239 }
13240 for (w, t) in branches {
13241 Self::substitute_grouping_calls(w, dropped);
13242 Self::substitute_grouping_calls(t, dropped);
13243 }
13244 if let Some(e) = else_branch {
13245 Self::substitute_grouping_calls(e, dropped);
13246 }
13247 }
13248 // v7.38 (read01) — recurse into the remaining child-bearing shapes
13249 // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13250 // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13251 // …` is the canonical rollup-total label idiom).
13252 Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13253 Expr::Like { expr, pattern, .. } => {
13254 Self::substitute_grouping_calls(expr, dropped);
13255 Self::substitute_grouping_calls(pattern, dropped);
13256 }
13257 Expr::InList { expr, list, .. } => {
13258 Self::substitute_grouping_calls(expr, dropped);
13259 for item in list {
13260 Self::substitute_grouping_calls(item, dropped);
13261 }
13262 }
13263 Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13264 Expr::Array(items) => {
13265 for item in items {
13266 Self::substitute_grouping_calls(item, dropped);
13267 }
13268 }
13269 Expr::ArraySubscript { target, index } => {
13270 Self::substitute_grouping_calls(target, dropped);
13271 Self::substitute_grouping_calls(index, dropped);
13272 }
13273 Expr::ArraySlice { target, lo, hi } => {
13274 Self::substitute_grouping_calls(target, dropped);
13275 if let Some(lo) = lo {
13276 Self::substitute_grouping_calls(lo, dropped);
13277 }
13278 if let Some(hi) = hi {
13279 Self::substitute_grouping_calls(hi, dropped);
13280 }
13281 }
13282 Expr::AnyAll { expr, array, .. } => {
13283 Self::substitute_grouping_calls(expr, dropped);
13284 Self::substitute_grouping_calls(array, dropped);
13285 }
13286 _ => {}
13287 }
13288 }
13289
13290 fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13291 // v7.37.17 (17.6 siblings) — parenthesized set-operation
13292 // group: `( <select chain> )` usable anywhere a query block
13293 // is (head or peer of an outer chain). The group's own
13294 // unions ride the returned SelectStatement; the executor's
13295 // nested-peer recursion runs them.
13296 if matches!(self.peek(), Token::LParen)
13297 && matches!(
13298 self.tokens.get(self.pos + 1),
13299 Some(Token::Select | Token::LParen | Token::Values)
13300 )
13301 {
13302 self.advance(); // (
13303 self.enter_nested()?;
13304 // v7.37 D.20 — a group whose head is a VALUES list:
13305 // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13306 // otherwise recurse into a nested SELECT/group head.
13307 let mut head = (if matches!(self.peek(), Token::Values) {
13308 self.advance(); // VALUES
13309 self.parse_values_rows_body()
13310 } else {
13311 self.parse_bare_select()
13312 })
13313 .and_then(|mut h| {
13314 self.parse_setop_chain_into(&mut h)?;
13315 Ok(h)
13316 });
13317 self.nest_depth -= 1;
13318 let mut head = match &mut head {
13319 Ok(h) => core::mem::take(h),
13320 Err(_) => return head,
13321 };
13322 // v7.37.17 (17.6 siblings) — group-internal tail:
13323 // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13324 // group head, then wrap the group as a derived table
13325 // (SELECT * FROM (group)) so the outer chain / outer
13326 // tail can't clobber the group's own ordering or limit.
13327 let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13328 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13329 if s.eq_ignore_ascii_case("fetch"));
13330 if has_tail {
13331 self.parse_select_tail_into(&mut head)?;
13332 head = SelectStatement {
13333 locking: None,
13334 ctes: Vec::new(),
13335 distinct: false,
13336 distinct_on: Vec::new(),
13337 items: alloc::vec![SelectItem::Wildcard],
13338 from: Some(FromClause {
13339 primary: TableRef {
13340 name: "subquery".to_string(),
13341 alias: None,
13342 only: false,
13343 as_of_segment: None,
13344 unnest_expr: None,
13345 unnest_column_aliases: Vec::new(),
13346 with_ordinality: false,
13347 generate_series_args: None,
13348 lateral_subquery: Some(Box::new(head)),
13349 jsonb_each_text_arg: None,
13350 table_fn_call: None,
13351 rows_from: None,
13352 json_table: None,
13353 scalar_fn_item: false,
13354 },
13355 joins: Vec::new(),
13356 }),
13357 where_: None,
13358 group_by: None,
13359 group_by_all: false,
13360 having: None,
13361 unions: Vec::new(),
13362 order_by: Vec::new(),
13363 limit: None,
13364 offset: None,
13365 limit_with_ties: false,
13366 window_check_exprs: Vec::new(),
13367 };
13368 }
13369 if !matches!(self.peek(), Token::RParen) {
13370 return Err(self.err(format!(
13371 "expected ')' after parenthesized query group, got {:?}",
13372 self.peek()
13373 )));
13374 }
13375 self.advance();
13376 return Ok(head);
13377 }
13378 // `TABLE name` shorthand as a query block — valid anywhere
13379 // a SELECT head is (set-op peers included).
13380 if matches!(self.peek(), Token::Table)
13381 && matches!(
13382 self.tokens.get(self.pos + 1),
13383 Some(Token::Ident(_) | Token::QuotedIdent(_))
13384 )
13385 {
13386 return self.parse_table_shorthand();
13387 }
13388 if !matches!(self.peek(), Token::Select) {
13389 return Err(self.err(format!(
13390 "expected SELECT to start a query block, got {:?}",
13391 self.peek()
13392 )));
13393 }
13394 self.advance();
13395 let distinct = if matches!(self.peek(), Token::Distinct) {
13396 self.advance();
13397 true
13398 } else {
13399 false
13400 };
13401 // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13402 // keep the first row (per ORDER BY) of each group the
13403 // expressions define. Django's .distinct('field') shape.
13404 let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13405 self.advance(); // ON
13406 if !matches!(self.peek(), Token::LParen) {
13407 return Err(self.err(format!(
13408 "expected '(' after DISTINCT ON, got {:?}",
13409 self.peek()
13410 )));
13411 }
13412 self.advance();
13413 let mut exprs = Vec::new();
13414 loop {
13415 exprs.push(self.parse_expr(0)?);
13416 match self.peek() {
13417 Token::Comma => {
13418 self.advance();
13419 }
13420 Token::RParen => break,
13421 other => {
13422 return Err(self.err(format!(
13423 "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13424 )));
13425 }
13426 }
13427 }
13428 self.advance(); // )
13429 exprs
13430 } else {
13431 Vec::new()
13432 };
13433 let mut items = self.parse_select_list()?;
13434 // v7.38.19 — `SELECT … INTO <table>`, PostgreSQL's other spelling
13435 // of CTAS. It sits exactly here in PG's grammar, right after the
13436 // target list.
13437 //
13438 // A comment in `ast.rs` has said since v7.38 that CTAS and
13439 // `SELECT INTO` lower to the same node. Only CTAS ever did:
13440 // `SELECT i INTO t FROM src` answered `syntax error at or near
13441 // "INTO"`, which the differential found while measuring what
13442 // PostgreSQL tags each of the five materialising forms with. A
13443 // comment describing a capability the code does not have is the
13444 // defect this version has been finding all day, and this is the
13445 // one it found in the parser.
13446 //
13447 // `INTO` is captured rather than consumed here: the name has to
13448 // travel out of a function that returns a `SelectStatement`, and
13449 // the caller lowers the whole thing to the CTAS node.
13450 if matches!(self.peek(), Token::Into) {
13451 self.advance();
13452 // `TEMP` / `TEMPORARY` / `UNLOGGED` / `TABLE` are modifiers on
13453 // the target, not part of its name. SPG has one storage
13454 // class, so `UNLOGGED` is accepted and means nothing, which
13455 // is what it already means on `CREATE TABLE`.
13456 let mut temporary = false;
13457 loop {
13458 match self.peek().clone() {
13459 Token::Ident(w) | Token::QuotedIdent(w)
13460 if w.eq_ignore_ascii_case("temp")
13461 || w.eq_ignore_ascii_case("temporary") =>
13462 {
13463 temporary = true;
13464 self.advance();
13465 }
13466 Token::Ident(w) | Token::QuotedIdent(w)
13467 if w.eq_ignore_ascii_case("unlogged") =>
13468 {
13469 self.advance();
13470 }
13471 Token::Table => {
13472 self.advance();
13473 }
13474 _ => break,
13475 }
13476 }
13477 let name = match self.peek().clone() {
13478 Token::Ident(w) | Token::QuotedIdent(w) => {
13479 self.advance();
13480 w
13481 }
13482 other => {
13483 return Err(self.err(alloc::format!(
13484 "expected a table name after SELECT … INTO, got {other:?}"
13485 )));
13486 }
13487 };
13488 self.pending_select_into = Some((name, temporary));
13489 }
13490 // Scope the TABLESAMPLE lowering channel to this SELECT:
13491 // stash whatever an enclosing select accumulated, collect
13492 // our own FROM's predicates, restore after the combine.
13493 let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13494 let mut from = if matches!(self.peek(), Token::From) {
13495 self.advance();
13496 Some(self.parse_from_clause()?)
13497 } else {
13498 None
13499 };
13500 // v7.37 D.22 — a set-returning function in the projection with no FROM
13501 // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13502 // rows. Move the first SRF projection item to a FROM-position derived
13503 // table and replace it in the projection with a reference to its output
13504 // column; sibling scalar columns repeat per SRF row. PG names the output
13505 // column after the function (or its AS alias). Reuses the FROM-SRF
13506 // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13507 // works via the targetlist-SRF path.
13508 // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13509 // `SELECT * FROM f(args)` — the record's fields become the columns, which
13510 // is exactly what the function's own row shape already is. Anywhere else
13511 // (per outer row, or beside other items) it would need a real record-typed
13512 // projection, so it says so rather than answering something else.
13513 if let [
13514 SelectItem::Expr {
13515 expr: Expr::FunctionCall { name, args },
13516 ..
13517 },
13518 ] = items.as_slice()
13519 && name == "__record_expand"
13520 {
13521 let Some(Expr::FunctionCall {
13522 name: inner_name,
13523 args: inner_args,
13524 }) = args.first()
13525 else {
13526 return Err(self.err(
13527 "(<expr>).* expands a function's record — it needs a function call".into(),
13528 ));
13529 };
13530 if from.is_some() {
13531 return Err(self.err(
13532 "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13533 .into(),
13534 ));
13535 }
13536 let fn_ref = TableRef {
13537 name: inner_name.clone(),
13538 alias: None,
13539 only: false,
13540 as_of_segment: None,
13541 unnest_expr: None,
13542 unnest_column_aliases: Vec::new(),
13543 with_ordinality: false,
13544 generate_series_args: None,
13545 lateral_subquery: None,
13546 jsonb_each_text_arg: None,
13547 table_fn_call: Some(Box::new((
13548 inner_name.to_ascii_lowercase(),
13549 inner_args.clone(),
13550 ))),
13551 rows_from: None,
13552 json_table: None,
13553 scalar_fn_item: false,
13554 };
13555 items = alloc::vec![SelectItem::Wildcard];
13556 from = Some(FromClause {
13557 primary: fn_ref,
13558 joins: Vec::new(),
13559 });
13560 }
13561 // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13562 // FROM, keeps its marker: the ENGINE lowers it, because naming the
13563 // record's fields takes the catalog. It becomes a LATERAL of the same
13564 // function plus one item per declared column — the machinery rounds 65
13565 // and 69 already built.
13566 // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13567 // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13568 // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13569 // express, since the lifted one becomes a scan and the other would
13570 // expand per its rows (a cross product, not a zip). So when the
13571 // projection holds more than one top-level function call, the lift steps
13572 // aside and the engine's target-list expansion takes the whole list.
13573 let fn_call_items = items
13574 .iter()
13575 .filter(|it| {
13576 matches!(
13577 it,
13578 SelectItem::Expr {
13579 expr: Expr::FunctionCall { .. },
13580 ..
13581 }
13582 )
13583 })
13584 .count();
13585 if from.is_none() && fn_call_items <= 1 {
13586 let mut found: Option<(usize, TableRef, String)> = None;
13587 for (i, item) in items.iter().enumerate() {
13588 if let SelectItem::Expr {
13589 expr: Expr::FunctionCall { name, args },
13590 alias,
13591 } = item
13592 {
13593 let lname = name.to_ascii_lowercase();
13594 let colname = alias.clone().unwrap_or_else(|| lname.clone());
13595 let (unnest, gs) = match lname.as_str() {
13596 "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13597 "generate_series" if (2..=3).contains(&args.len()) => {
13598 (None, Some(args.clone()))
13599 }
13600 // v7.38 (read01) — generate_subscripts(arr, dim) in a
13601 // no-FROM projection yields the 1-based subscripts, i.e.
13602 // generate_series(1, array_length(arr, dim)); an invalid
13603 // dimension makes array_length NULL → 0 rows, as in PG.
13604 "generate_subscripts" if args.len() == 2 => (
13605 None,
13606 Some(alloc::vec![
13607 Expr::Literal(Literal::Integer(1)),
13608 Expr::FunctionCall {
13609 name: "array_length".to_string(),
13610 args: args.clone(),
13611 },
13612 ]),
13613 ),
13614 // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13615 // in a no-FROM projection unnest their *_to_array form.
13616 "string_to_table" | "regexp_split_to_table" => {
13617 let array_fn = if lname == "string_to_table" {
13618 "string_to_array"
13619 } else {
13620 "regexp_split_to_array"
13621 };
13622 (
13623 Some(Box::new(Expr::FunctionCall {
13624 name: array_fn.to_string(),
13625 args: args.clone(),
13626 })),
13627 None,
13628 )
13629 }
13630 // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13631 // a no-FROM projection expand per element. The scalar form
13632 // returns the elements as a TEXT array, so unnest over the
13633 // same call materialises one row each (same rewrite the
13634 // FROM-clause form uses).
13635 "jsonb_array_elements"
13636 | "json_array_elements"
13637 | "jsonb_array_elements_text"
13638 | "json_array_elements_text"
13639 if args.len() == 1 =>
13640 {
13641 (
13642 Some(Box::new(Expr::FunctionCall {
13643 name: lname.clone(),
13644 args: args.clone(),
13645 })),
13646 None,
13647 )
13648 }
13649 // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13650 // in a no-FROM projection expands per match (scalar form
13651 // returns the matches as a TEXT array → unnest).
13652 "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13653 Some(Box::new(Expr::FunctionCall {
13654 name: lname.clone(),
13655 args: args.clone(),
13656 })),
13657 None,
13658 ),
13659 _ => continue,
13660 };
13661 found = Some((
13662 i,
13663 TableRef {
13664 name: colname.clone(),
13665 alias: Some(colname.clone()),
13666 only: false,
13667 as_of_segment: None,
13668 unnest_expr: unnest,
13669 unnest_column_aliases: alloc::vec![colname.clone()],
13670 with_ordinality: false,
13671 generate_series_args: gs,
13672 lateral_subquery: None,
13673 jsonb_each_text_arg: None,
13674 table_fn_call: None,
13675 rows_from: None,
13676 json_table: None,
13677 scalar_fn_item: false,
13678 },
13679 colname,
13680 ));
13681 break;
13682 }
13683 }
13684 if let Some((idx, tref, colname)) = found {
13685 from = Some(FromClause {
13686 primary: tref,
13687 joins: Vec::new(),
13688 });
13689 items[idx] = SelectItem::Expr {
13690 expr: Expr::Column(ColumnName {
13691 qualifier: None,
13692 name: colname.clone(),
13693 }),
13694 alias: Some(colname),
13695 };
13696 }
13697 }
13698 let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13699 let where_ = if matches!(self.peek(), Token::Where) {
13700 self.advance();
13701 Some(self.parse_expr(0)?)
13702 } else {
13703 None
13704 };
13705 let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13706 Some(match acc {
13707 Some(w) => Expr::Binary {
13708 lhs: Box::new(pred),
13709 op: crate::ast::BinOp::And,
13710 rhs: Box::new(w),
13711 },
13712 None => pred,
13713 })
13714 });
13715 self.pending_sample_preds = enclosing_sample_preds;
13716 let mut group_by_all = false;
13717 // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13718 // share one expansion: `grouping_sets` lists the key subsets
13719 // (first = primary, assigned to stmt.group_by; the rest
13720 // become UNION ALL peers), `grouping_universe` is the full
13721 // key list used to compute each peer's dropped keys.
13722 let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13723 let mut grouping_universe: Vec<Expr> = Vec::new();
13724 // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13725 // A BOOL, not the key list: this frame is the statement parser's, and
13726 // round 430 measured that a `Vec` local here is enough on its own to
13727 // tip the 512 KiB nesting guard. The keys are recoverable from
13728 // `grouping_universe`, which a rollup fills with exactly them.
13729 let mut mysql_rollup = false;
13730 let group_by = if matches!(self.peek(), Token::Group) {
13731 self.advance();
13732 if !self.peek_is_by() {
13733 return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13734 }
13735 self.advance();
13736 // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13737 // every non-aggregate SELECT-list item later.
13738 if matches!(self.peek(), Token::All) {
13739 self.advance();
13740 group_by_all = true;
13741 None
13742 } else {
13743 // v7.39 (round 242) — PG's general grouping-element grammar:
13744 // GROUP BY [DISTINCT] element [, element]*, where an element
13745 // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13746 // SETS (…) — mixed freely. Each element yields a list of
13747 // key sets; the query's grouping sets are the CARTESIAN
13748 // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13749 // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13750 // content. ROLLUP/CUBE members may be composite
13751 // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13752 // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13753 // parser handled only a lone ROLLUP/CUBE/GS as the whole
13754 // clause.
13755 let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13756 self.advance();
13757 true
13758 } else {
13759 false
13760 };
13761 let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13762 loop {
13763 element_sets.push(self.parse_grouping_element()?);
13764 if matches!(self.peek(), Token::Comma) {
13765 self.advance();
13766 } else {
13767 break;
13768 }
13769 }
13770 let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13771 for el in &element_sets {
13772 let mut next: Vec<Vec<Expr>> = Vec::new();
13773 for base in &total {
13774 for set in el {
13775 let mut merged = base.clone();
13776 for k in set {
13777 if !merged.iter().any(|m| m == k) {
13778 merged.push(k.clone());
13779 }
13780 }
13781 next.push(merged);
13782 }
13783 }
13784 total = next;
13785 }
13786 // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13787 // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13788 // The keys and the aggregates come out identical; the ROW
13789 // ORDER does not, and that is the part a report depends on.
13790 // MySQL interleaves each group's subtotal right after its
13791 // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13792 // where the union-of-grouping-sets expansion emits every
13793 // leaf first and then every subtotal. MariaDB REFUSES an
13794 // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13795 // order itself — measured on MariaDB 11 and MySQL 9.7, which
13796 // agree on the order and disagree only on whether ORDER BY
13797 // is allowed (MySQL allows it; SPG allows it too, since
13798 // refusing would break the clients that can write it).
13799 if self.mysql_dialect
13800 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13801 && matches!(
13802 self.tokens.get(self.pos + 1),
13803 Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13804 )
13805 {
13806 self.advance(); // WITH
13807 self.advance(); // ROLLUP
13808 let keys = total.into_iter().next().unwrap_or_default();
13809 mysql_rollup = true;
13810 // n+1 prefixes, largest first — the same expansion
13811 // `ROLLUP (…)` produces.
13812 total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13813 }
13814 if distinct_sets {
13815 let mut seen: Vec<Vec<String>> = Vec::new();
13816 total.retain(|set| {
13817 let mut key: Vec<String> =
13818 set.iter().map(|e| alloc::format!("{e}")).collect();
13819 key.sort();
13820 if seen.contains(&key) {
13821 false
13822 } else {
13823 seen.push(key);
13824 true
13825 }
13826 });
13827 }
13828 if total.len() > 1 {
13829 let mut universe: Vec<Expr> = Vec::new();
13830 for set in &total {
13831 for k in set {
13832 if !universe.iter().any(|u| u == k) {
13833 universe.push(k.clone());
13834 }
13835 }
13836 }
13837 grouping_universe = universe;
13838 let primary = total[0].clone();
13839 grouping_sets = total;
13840 Some(primary)
13841 } else {
13842 // One set (a plain GROUP BY list, or a single-set
13843 // spelling like GROUPING SETS ((a, b))). An EMPTY
13844 // single set — GROUPING SETS (()) — stays
13845 // `Some(vec![])`: the grand-total group, which must
13846 // run the aggregate path.
13847 Some(total.into_iter().next().unwrap_or_default())
13848 }
13849 }
13850 } else {
13851 None
13852 };
13853 let having = if matches!(self.peek(), Token::Having) {
13854 self.advance();
13855 Some(self.parse_expr(0)?)
13856 } else {
13857 None
13858 };
13859 // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
13860 // OVER w parsed to a marker above; inline each definition
13861 // into the referencing WindowFunction nodes.
13862 let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
13863 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
13864 self.advance();
13865 loop {
13866 let wname = self.expect_ident_like()?;
13867 if !matches!(self.peek(), Token::As) {
13868 return Err(self.err(format!(
13869 "expected AS after WINDOW {wname}, got {:?}",
13870 self.peek()
13871 )));
13872 }
13873 self.advance();
13874 // v7.39 (round 229) — PG rejects a redefinition outright.
13875 if window_defs
13876 .iter()
13877 .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
13878 {
13879 return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
13880 }
13881 let def = self.parse_over_clause()?;
13882 // A definition may itself copy an earlier one
13883 // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
13884 // so resolve it against the defs already in scope. Same
13885 // copy rules as an `OVER (w1 …)` in the select list.
13886 let mut probe = Expr::WindowFunction {
13887 name: String::new(),
13888 args: Vec::new(),
13889 partition_by: def.0,
13890 order_by: def.1,
13891 frame: def.2,
13892 null_treatment: crate::ast::NullTreatment::Respect,
13893 filter: None,
13894 };
13895 Self::substitute_named_windows(&mut probe, &window_defs)
13896 .map_err(|m| self.err(m))?;
13897 let Expr::WindowFunction {
13898 partition_by,
13899 order_by,
13900 frame,
13901 ..
13902 } = probe
13903 else {
13904 unreachable!("probe is a WindowFunction")
13905 };
13906 window_defs.push((wname, (partition_by, order_by, frame)));
13907 if matches!(self.peek(), Token::Comma) {
13908 self.advance();
13909 continue;
13910 }
13911 break;
13912 }
13913 }
13914 // v7.39 (round 705) — which definitions did anything reference?
13915 // The ones nothing did used to be dropped here, unexamined, so
13916 // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
13917 // definition whether referenced or not. Their key expressions ride
13918 // out on the statement for the engine to resolve.
13919 let mut window_refs: Vec<String> = Vec::new();
13920 if !window_defs.is_empty() {
13921 for it in &items {
13922 if let SelectItem::Expr { expr, .. } = it {
13923 Self::collect_named_window_refs(expr, &mut window_refs);
13924 }
13925 }
13926 }
13927 let window_check_exprs: Vec<Expr> = window_defs
13928 .iter()
13929 .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
13930 .flat_map(|(_, (partition, order, _))| {
13931 partition
13932 .iter()
13933 .cloned()
13934 .chain(order.iter().map(|(e, _, _)| e.clone()))
13935 })
13936 .collect();
13937 if !window_defs.is_empty()
13938 || items
13939 .iter()
13940 .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
13941 {
13942 for it in &mut items {
13943 if let SelectItem::Expr { expr, .. } = it {
13944 Self::substitute_named_windows(expr, &window_defs)
13945 .map_err(|m| self.err(m))?;
13946 }
13947 }
13948 }
13949 // `GROUP BY 1` — positional keys substitute with the Nth
13950 // select item's expression (same contract ORDER BY has had
13951 // since v6.x). Out-of-range positions error.
13952 let group_by = match group_by {
13953 Some(mut keys) => {
13954 for k in &mut keys {
13955 if let Expr::Literal(Literal::Integer(n)) = k {
13956 let idx = *n;
13957 if idx < 1 || idx as usize > items.len() {
13958 return Err(self.err(alloc::format!(
13959 "GROUP BY position {idx} is not in select list"
13960 )));
13961 }
13962 match &items[(idx - 1) as usize] {
13963 SelectItem::Expr { expr, .. } => *k = expr.clone(),
13964 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
13965 return Err(self.err(alloc::format!(
13966 "GROUP BY position {idx} references a wildcard item"
13967 )));
13968 }
13969 }
13970 }
13971 }
13972 Some(keys)
13973 }
13974 None => None,
13975 };
13976 let mut stmt = SelectStatement {
13977 locking: None,
13978 ctes: Vec::new(),
13979 distinct,
13980 distinct_on,
13981 items,
13982 from,
13983 where_,
13984 group_by,
13985 group_by_all,
13986 having,
13987 unions: Vec::new(),
13988 order_by: Vec::new(),
13989 limit: None,
13990 offset: None,
13991 limit_with_ties: false,
13992 window_check_exprs,
13993 };
13994 // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
13995 // first set is the primary (already on stmt.group_by); each
13996 // further set becomes a UNION ALL peer with its dropped
13997 // keys (universe minus the set) replaced by NULL literals
13998 // in the peer's items and group_by. PG-legal: non-grouped
13999 // select items must be group keys or aggregates, so a
14000 // dropped key's occurrences in the projection are exactly
14001 // the ones to nullify.
14002 // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
14003 // over a plain GROUP BY (every argument must be a group key; the
14004 // mask is then 0) and rejects anything else with 42803. SPG's
14005 // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
14006 // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
14007 // function `grouping`".
14008 if grouping_sets.len() <= 1 {
14009 let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
14010 let mut calls: Vec<Expr> = Vec::new();
14011 for item in &stmt.items {
14012 if let SelectItem::Expr { expr, .. } = item {
14013 Self::collect_grouping_calls(expr, &mut calls);
14014 }
14015 }
14016 if let Some(h) = &stmt.having {
14017 Self::collect_grouping_calls(h, &mut calls);
14018 }
14019 for call in &calls {
14020 let Expr::FunctionCall { args, .. } = call else {
14021 continue;
14022 };
14023 for a in args {
14024 if !keys.iter().any(|k| k == a) {
14025 return Err(self.err(
14026 "arguments to GROUPING must be grouping expressions of the associated query level"
14027 .to_string(),
14028 ));
14029 }
14030 }
14031 }
14032 if !calls.is_empty() {
14033 for item in &mut stmt.items {
14034 if let SelectItem::Expr { expr, .. } = item {
14035 Self::substitute_grouping_calls(expr, &[]);
14036 }
14037 }
14038 if let Some(h) = &mut stmt.having {
14039 Self::substitute_grouping_calls(h, &[]);
14040 }
14041 }
14042 }
14043 if grouping_sets.len() > 1 {
14044 // The primary set's own dropped keys nullify in the
14045 // HEAD's projection too (GROUPING SETS's first set may
14046 // omit keys other sets use).
14047 let primary = grouping_sets[0].clone();
14048 let head_dropped: Vec<Expr> = grouping_universe
14049 .iter()
14050 .filter(|u| !primary.iter().any(|k| k == *u))
14051 .cloned()
14052 .collect();
14053 for set in grouping_sets.iter().skip(1) {
14054 let mut peer = stmt.clone();
14055 peer.unions = Vec::new();
14056 let dropped: Vec<&Expr> = grouping_universe
14057 .iter()
14058 .filter(|u| !set.iter().any(|k| k == *u))
14059 .collect();
14060 // Empty set = grand-total group: `Some(vec![])` forces
14061 // the aggregate path (one collapsed row) instead of a
14062 // per-row passthrough. See the primary-set note above.
14063 peer.group_by = Some(set.clone());
14064 let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
14065 for item in &mut peer.items {
14066 if let SelectItem::Expr { expr, alias } = item {
14067 if dropped.iter().any(|d| *d == expr) {
14068 // v7.39 — keep the dropped key's name on the
14069 // NULL literal so the UNION output column
14070 // (and any top-level ORDER BY on it) still
14071 // resolves.
14072 if alias.is_none()
14073 && let Expr::Column(c) = &expr
14074 {
14075 *alias = Some(c.name.clone());
14076 }
14077 *expr = Expr::Literal(Literal::Null);
14078 } else {
14079 Self::substitute_grouping_calls(expr, &dropped_owned);
14080 }
14081 }
14082 }
14083 if let Some(h) = &mut peer.having {
14084 Self::substitute_grouping_calls(h, &dropped_owned);
14085 }
14086 stmt.unions.push((UnionKind::All, peer));
14087 }
14088 for item in &mut stmt.items {
14089 if let SelectItem::Expr { expr, alias } = item {
14090 if head_dropped.iter().any(|d| d == expr) {
14091 if alias.is_none()
14092 && let Expr::Column(c) = &expr
14093 {
14094 *alias = Some(c.name.clone());
14095 }
14096 *expr = Expr::Literal(Literal::Null);
14097 } else {
14098 Self::substitute_grouping_calls(expr, &head_dropped);
14099 }
14100 }
14101 }
14102 if let Some(h) = &mut stmt.having {
14103 Self::substitute_grouping_calls(h, &head_dropped);
14104 }
14105 // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
14106 // (while `grouping_universe` / the per-branch sets are in scope). For
14107 // each grouping() call in it, inject a per-branch hidden column
14108 // `__grp_ord_K` carrying that branch's mask into the head + every
14109 // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
14110 // preserves this pre-set order_by; the engine strips `__grp_ord_*`
14111 // from the final output. A standalone grouping-set query has ORDER BY
14112 // (not an explicit set-op) next, so consuming it here is safe.
14113 // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
14114 // rollup carries the hierarchical order: sort by the grouping
14115 // keys with the rolled-up NULLs last, which is exactly the
14116 // interleaving both oracles emit. A client's own ORDER BY wins,
14117 // which is what MySQL does (MariaDB refuses to let one be
14118 // written at all).
14119 // The synthesised keys have to travel the SAME path a written
14120 // ORDER BY does: the block below is what turns a `grouping()`
14121 // call into the per-branch `__grp_ord_K` column the engine can
14122 // actually sort on. Bypassing it left a bare `grouping(text)`
14123 // for the evaluator to reject.
14124 let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
14125 self.parse_order_by_keys()?
14126 } else if mysql_rollup {
14127 Self::mysql_rollup_order(&grouping_universe)
14128 } else {
14129 Vec::new()
14130 };
14131 if !synthesised_or_parsed.is_empty() {
14132 let mut order_keys = synthesised_or_parsed;
14133 let mut grp_exprs: Vec<Expr> = Vec::new();
14134 for ob in &order_keys {
14135 Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
14136 }
14137 for (k, gexpr) in grp_exprs.iter().enumerate() {
14138 let colname = alloc::format!("__grp_ord_{k}");
14139 // Head branch (primary set) uses `head_dropped`.
14140 let mut he = gexpr.clone();
14141 Self::substitute_grouping_calls(&mut he, &head_dropped);
14142 stmt.items.push(SelectItem::Expr {
14143 expr: he,
14144 alias: Some(colname.clone()),
14145 });
14146 // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
14147 for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14148 let set = &grouping_sets[i + 1];
14149 let dropped: Vec<Expr> = grouping_universe
14150 .iter()
14151 .filter(|u| !set.iter().any(|k| k == *u))
14152 .cloned()
14153 .collect();
14154 let mut pe = gexpr.clone();
14155 Self::substitute_grouping_calls(&mut pe, &dropped);
14156 peer.items.push(SelectItem::Expr {
14157 expr: pe,
14158 alias: Some(colname.clone()),
14159 });
14160 }
14161 }
14162 for ob in &mut order_keys {
14163 Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
14164 }
14165 stmt.order_by = order_keys;
14166 }
14167 }
14168 Ok(stmt)
14169 }
14170
14171 /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
14172 /// as ORDER BY keys.
14173 ///
14174 /// Per key: the rollup marker, then the key. Sorting on the key alone
14175 /// is not enough, and a table with a NULL in it says why — MariaDB puts
14176 /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
14177 /// the ROLLUP-introduced NULL last, and both print as NULL.
14178 /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
14179 /// real group including the data-NULL one, 1 only for the row the
14180 /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
14181 /// rolls up to NULL|2, a|1, b|3, NULL|6.
14182 ///
14183 /// `#[inline(never)]`: its locals must not join the statement parser's
14184 /// frame, which round 430 measured sitting against the nesting guard.
14185 #[inline(never)]
14186 fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
14187 let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
14188 for e in keys {
14189 out.push(OrderBy {
14190 expr: Expr::FunctionCall {
14191 name: "grouping".into(),
14192 args: alloc::vec![e.clone()],
14193 },
14194 desc: false,
14195 nulls_first: None,
14196 collation: None,
14197 });
14198 out.push(OrderBy {
14199 expr: e.clone(),
14200 desc: false,
14201 // MySQL orders NULL first on an ascending key.
14202 nulls_first: Some(true),
14203 collation: None,
14204 });
14205 }
14206 out
14207 }
14208
14209 /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
14210 /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
14211 #[inline(never)]
14212 fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
14213 use crate::ast::MaintainKind;
14214 self.skip_paren_option_list();
14215 let kind = match self.peek() {
14216 // `TABLE` and `INDEX` lex as keywords, not identifiers.
14217 Token::Table | Token::Index => {
14218 self.advance();
14219 MaintainKind::ReindexRelation
14220 }
14221 Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
14222 "index" | "table" => {
14223 self.advance();
14224 MaintainKind::ReindexRelation
14225 }
14226 "schema" => {
14227 self.advance();
14228 MaintainKind::ReindexSchema
14229 }
14230 "system" | "database" => {
14231 self.advance();
14232 MaintainKind::Whole
14233 }
14234 // PG requires the object type; anything else is the
14235 // caller's problem, not something to swallow.
14236 _ => MaintainKind::ReindexRelation,
14237 },
14238 _ => MaintainKind::Whole,
14239 };
14240 // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
14241 // allows the plain form, so the modifier is recorded rather than
14242 // skipped. It still has no effect on how the reindex runs.
14243 let mut concurrently = false;
14244 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14245 self.advance();
14246 concurrently = true;
14247 }
14248 let target = self.take_optional_maintain_name();
14249 self.consume_until_statement_boundary();
14250 Ok(Statement::Maintain {
14251 kind,
14252 concurrently,
14253 target,
14254 })
14255 }
14256
14257 /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14258 /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14259 #[inline(never)]
14260 fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14261 use crate::ast::MaintainKind;
14262 self.skip_paren_option_list();
14263 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14264 self.advance();
14265 }
14266 let target = self.take_optional_maintain_name();
14267 self.consume_until_statement_boundary();
14268 Ok(Statement::Maintain {
14269 kind: if target.is_some() {
14270 MaintainKind::ClusterRelation
14271 } else {
14272 MaintainKind::Whole
14273 },
14274 // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14275 // transaction block quite happily (measured).
14276 concurrently: false,
14277 target,
14278 })
14279 }
14280
14281 /// The next token as a relation / schema name, when there is one.
14282 fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14283 match self.peek() {
14284 Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14285 Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14286 _ => None,
14287 },
14288 _ => None,
14289 }
14290 }
14291
14292 /// A parenthesised option list, absorbed.
14293 fn skip_paren_option_list(&mut self) {
14294 if !matches!(self.peek(), Token::LParen) {
14295 return;
14296 }
14297 let mut depth = 0usize;
14298 loop {
14299 match self.advance() {
14300 Token::LParen => depth += 1,
14301 Token::RParen => {
14302 depth -= 1;
14303 if depth == 0 {
14304 return;
14305 }
14306 }
14307 Token::Eof => return,
14308 _ => {}
14309 }
14310 }
14311 }
14312
14313 /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14314 /// column list.
14315 ///
14316 /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14317 /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14318 /// / ALL. The three that describe physical storage have no meaning
14319 /// here, so they parse and change nothing rather than making a
14320 /// dump that mentions them fail to load.
14321 ///
14322 /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14323 /// parse chain the nesting sentinel is tuned against.
14324 #[inline(never)]
14325 fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14326 self.advance(); // LIKE
14327 let source = self.expect_ident_like()?;
14328 let mut options = crate::ast::LikeOptions::default();
14329 loop {
14330 let including = match self.peek() {
14331 Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14332 Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14333 _ => break,
14334 };
14335 self.advance();
14336 // `ALL` lexes as its own keyword, not an identifier.
14337 let opt = if matches!(self.peek(), Token::All) {
14338 self.advance();
14339 alloc::string::String::from("all")
14340 } else {
14341 self.expect_ident_like()?
14342 };
14343 let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14344 o.defaults = on;
14345 o.constraints = on;
14346 o.identity = on;
14347 o.generated = on;
14348 o.indexes = on;
14349 o.comments = on;
14350 };
14351 match opt.to_ascii_lowercase().as_str() {
14352 "all" => set(&mut options, including),
14353 "defaults" => options.defaults = including,
14354 "constraints" => options.constraints = including,
14355 "identity" => options.identity = including,
14356 "generated" => options.generated = including,
14357 "indexes" => options.indexes = including,
14358 "comments" => options.comments = including,
14359 // No storage model to copy into.
14360 "storage" | "statistics" | "compression" => {}
14361 other => {
14362 return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14363 }
14364 }
14365 }
14366 Ok(crate::ast::LikeSpec {
14367 source,
14368 at,
14369 options,
14370 })
14371 }
14372
14373 fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14374 // Caller already consumed CREATE; we're sitting on TABLE.
14375 debug_assert!(matches!(self.peek(), Token::Table));
14376 self.advance();
14377 let if_not_exists = self.consume_if_not_exists();
14378 let name = self.expect_ident_like()?;
14379 // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14380 // child shape has no column list; the child inherits its
14381 // columns from the parent at engine-DDL time. Detect it
14382 // before the `(` requirement below.
14383 if matches!(self.peek(), Token::Partition)
14384 && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14385 {
14386 self.advance(); // PARTITION
14387 self.advance(); // of
14388 let partition_of = self.parse_partition_of_tail()?;
14389 return Ok(Statement::CreateTable(CreateTableStatement {
14390 temporary: false,
14391 name,
14392 columns: Vec::new(),
14393 like_specs: Vec::new(),
14394 inherits: Vec::new(),
14395 if_not_exists,
14396 foreign_keys: Vec::new(),
14397 table_constraints: Vec::new(),
14398 partition_by: None,
14399 partition_of: Some(partition_of),
14400 }));
14401 }
14402 // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14403 // the materialized-view materialisation path (run the SELECT, infer the
14404 // column types, create + populate the table) but marks the node so the
14405 // executor creates a plain table without a mat-view registry entry.
14406 if matches!(self.peek(), Token::As) {
14407 self.advance();
14408 let body_stmt = self.parse_select_stmt()?;
14409 let Statement::Select(body) = body_stmt else {
14410 return Err(self.err(format!(
14411 "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14412 )));
14413 };
14414 let with_data = self.parse_optional_with_data(true)?;
14415 return Ok(Statement::CreateMaterializedView(
14416 crate::ast::CreateMaterializedViewStatement {
14417 temporary: false,
14418 name,
14419 if_not_exists,
14420 columns: Vec::new(),
14421 body,
14422 with_data,
14423 as_plain_table: true,
14424 },
14425 ));
14426 }
14427 if !matches!(self.peek(), Token::LParen) {
14428 return Err(self.err(format!(
14429 "expected '(' after table name, got {:?}",
14430 self.peek()
14431 )));
14432 }
14433 self.advance();
14434 let mut columns = Vec::new();
14435 let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14436 let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14437 let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14438 loop {
14439 // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14440 // column list. It is how a child that adds nothing of its own is
14441 // written, and this loop demanded at least one entry: `syntax
14442 // error at or near ")"`. The child takes the parent's columns,
14443 // which the INHERITS clause already arranges.
14444 if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14445 self.advance();
14446 break;
14447 }
14448 // v7.6.0 / v7.9.18 — distinguish table-level constraint
14449 // clauses from column definitions. Constraints start
14450 // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14451 // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14452 // a column.
14453 if self.peek_table_level_pk_start() {
14454 table_constraints.push(self.parse_table_level_primary_key()?);
14455 } else if matches!(self.peek(), Token::Like) {
14456 // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14457 // <opt> ]*`. The source table's shape lives in the catalog,
14458 // so this records the clause and the engine expands it.
14459 like_specs.push(self.parse_create_table_like(columns.len())?);
14460 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14461 // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14462 table_constraints.push(self.parse_table_level_exclude()?);
14463 } else if self.peek_table_level_unique_start() {
14464 table_constraints.push(self.parse_table_level_unique()?);
14465 } else if self.peek_table_level_check_start() {
14466 // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14467 table_constraints.push(self.parse_table_level_check()?);
14468 } else if self.peek_mysql_inline_key_start() {
14469 // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14470 // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14471 // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14472 // inside the column list. Skip name + paren list;
14473 // for UNIQUE KEY, register as a UC.
14474 if let Some(uc) = self.parse_mysql_inline_key()? {
14475 table_constraints.push(uc);
14476 }
14477 } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14478 // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14479 // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14480 // CHECK is named, and the named-CONSTRAINT arm used
14481 // to accept FOREIGN KEY only. The name is accepted
14482 // and discarded — same handling as every other SPG
14483 // constraint name.
14484 self.advance(); // CONSTRAINT
14485 // v7.39 (read01 round 48) — the name is kept now: the schema
14486 // stores it, so DROP / RENAME CONSTRAINT can find it.
14487 let con_name = self.expect_ident_like()?;
14488 let mut tc = match kind {
14489 NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14490 NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14491 NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14492 NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14493 };
14494 match &mut tc {
14495 crate::ast::TableConstraint::Check { name, .. }
14496 | crate::ast::TableConstraint::Unique { name, .. }
14497 | crate::ast::TableConstraint::PrimaryKey { name, .. }
14498 | crate::ast::TableConstraint::Exclude { name, .. } => {
14499 *name = Some(con_name);
14500 }
14501 _ => {}
14502 }
14503 table_constraints.push(tc);
14504 } else if self.peek_constraint_or_fk_start() {
14505 foreign_keys.push(self.parse_table_level_fk()?);
14506 } else {
14507 let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14508 // v7.13.0 — fold inline UNIQUE / CHECK column
14509 // constraints into table-level entries so the
14510 // engine path stays uniform.
14511 if col.is_unique {
14512 table_constraints.push(crate::ast::TableConstraint::Unique {
14513 name: None,
14514 columns: alloc::vec![col.name.clone()],
14515 nulls_not_distinct: col.unique_nulls_not_distinct,
14516 deferrable: col.constraint_deferrable,
14517 initially_deferred: col.constraint_initially_deferred,
14518 });
14519 }
14520 if let Some(check_expr) = col.check.clone() {
14521 table_constraints.push(crate::ast::TableConstraint::Check {
14522 name: None,
14523 expr: check_expr,
14524 not_valid: false,
14525 });
14526 }
14527 columns.push(col);
14528 if let Some(fk) = col_level_fk {
14529 foreign_keys.push(fk);
14530 }
14531 }
14532 match self.peek() {
14533 Token::Comma => {
14534 self.advance();
14535 }
14536 Token::RParen => {
14537 self.advance();
14538 break;
14539 }
14540 other => {
14541 return Err(
14542 self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14543 );
14544 }
14545 }
14546 }
14547 // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14548 // `CREATE TABLE k (LIKE t)` is a complete definition even though
14549 // nothing is written between the parentheses.
14550 // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14551 // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14552 // empty parentheses were a parse error in their own right — quite apart
14553 // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14554 // SPG does not have (filed separately).
14555 let _ = &like_specs;
14556 // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14557 // It sits between the column list and the MySQL table options,
14558 // and it was a syntax error until this round.
14559 let mut inherits: Vec<String> = Vec::new();
14560 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14561 if k.eq_ignore_ascii_case("inherits"))
14562 {
14563 self.advance();
14564 if !matches!(self.peek(), Token::LParen) {
14565 return Err(self.err(alloc::format!(
14566 "expected ( after INHERITS, got {:?}",
14567 self.peek()
14568 )));
14569 }
14570 self.advance();
14571 loop {
14572 inherits.push(self.expect_ident_like()?);
14573 if matches!(self.peek(), Token::Comma) {
14574 self.advance();
14575 continue;
14576 }
14577 break;
14578 }
14579 if !matches!(self.peek(), Token::RParen) {
14580 return Err(self.err(alloc::format!(
14581 "expected ) closing INHERITS, got {:?}",
14582 self.peek()
14583 )));
14584 }
14585 self.advance();
14586 }
14587 // v7.14.0 — consume MySQL/MariaDB table options after the
14588 // closing `)`. mysqldump emits things like
14589 // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14590 // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14591 // SPG accepts all forms as no-ops (each option is
14592 // `<ident> [=] <ident-or-string>` separated by whitespace).
14593 self.consume_mysql_table_options();
14594 // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14595 // SPG has no per-table reloptions, so accept and ignore them so a
14596 // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14597 self.consume_with_reloptions();
14598 // v7.37.6-B — declarative-partition-parent suffix
14599 // (`PARTITION BY RANGE (key_col)`) sits after the column
14600 // list + MySQL table-options. v7.37.6-B only accepts RANGE
14601 // and locks the key column at one ident; the engine then
14602 // verifies the column type is TIMESTAMPTZ.
14603 let partition_by = if matches!(self.peek(), Token::Partition) {
14604 self.advance(); // PARTITION
14605 if !self.peek_is_by() {
14606 return Err(self.err(format!(
14607 "expected BY after PARTITION, got {:?}",
14608 self.peek()
14609 )));
14610 }
14611 self.advance();
14612 Some(self.parse_partition_by_tail()?)
14613 } else {
14614 None
14615 };
14616 Ok(Statement::CreateTable(CreateTableStatement {
14617 temporary: false,
14618 name,
14619 columns,
14620 like_specs,
14621 inherits,
14622 if_not_exists,
14623 foreign_keys,
14624 table_constraints,
14625 partition_by,
14626 partition_of: None,
14627 }))
14628 }
14629
14630 /// v7.37.6-B — case-insensitive ident match helper for the
14631 /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14632 /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14633 /// didn't burn a global keyword slot for each (see the
14634 /// `Token::Partition` doc-comment in `lexer.rs`).
14635 fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14636 matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14637 }
14638
14639 /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14640 /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14641 fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14642 use crate::ast::{PartitionBySpec, PartitionKindAst};
14643 let kind = match self.peek() {
14644 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14645 self.advance();
14646 PartitionKindAst::Range
14647 }
14648 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14649 self.advance();
14650 PartitionKindAst::List
14651 }
14652 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14653 self.advance();
14654 PartitionKindAst::Hash
14655 }
14656 other => {
14657 return Err(self.err(format!(
14658 "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14659 )));
14660 }
14661 };
14662 if !matches!(self.peek(), Token::LParen) {
14663 return Err(self.err(format!(
14664 "expected '(' after PARTITION BY <strategy>, got {:?}",
14665 self.peek()
14666 )));
14667 }
14668 self.advance();
14669 let mut key_columns = Vec::new();
14670 loop {
14671 key_columns.push(self.expect_ident_like()?);
14672 match self.peek() {
14673 Token::Comma => {
14674 self.advance();
14675 }
14676 Token::RParen => {
14677 self.advance();
14678 break;
14679 }
14680 other => {
14681 return Err(self.err(format!(
14682 "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14683 )));
14684 }
14685 }
14686 }
14687 if key_columns.is_empty() {
14688 return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14689 }
14690 Ok(PartitionBySpec { kind, key_columns })
14691 }
14692
14693 /// v7.37.6-B — after `PARTITION OF`, expect
14694 /// <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14695 /// or
14696 /// <parent> DEFAULT
14697 fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14698 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14699 let parent_name = self.expect_ident_like()?;
14700 // v7.37.6-B rejects an explicit column list — the child
14701 // inherits from the parent. mailrs round-7 taught us that
14702 // CREATE TABLE-side schema reconciliation hides drift, so
14703 // we surface this as a parse error rather than silently
14704 // ignoring user columns.
14705 if matches!(self.peek(), Token::LParen) {
14706 return Err(self.err(
14707 "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14708 at v7.37.6-B; the child inherits its columns from the parent"
14709 .to_string(),
14710 ));
14711 }
14712 let bounds = match self.peek() {
14713 Token::Default => {
14714 self.advance();
14715 PartitionOfBoundsAst::Default
14716 }
14717 Token::For => {
14718 self.advance();
14719 if !matches!(self.peek(), Token::Values) {
14720 return Err(
14721 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14722 );
14723 }
14724 self.advance();
14725 // WITH is not a reserved Token in the lexer — it lexes
14726 // as Token::Ident("with"). Disambiguate manually.
14727 let want_with = matches!(
14728 self.peek(),
14729 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14730 );
14731 if want_with {
14732 self.advance();
14733 if !matches!(self.peek(), Token::LParen) {
14734 return Err(self.err(format!(
14735 "expected '(' after FOR VALUES WITH, got {:?}",
14736 self.peek()
14737 )));
14738 }
14739 self.advance();
14740 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14741 loop {
14742 let key = self.expect_ident_like()?;
14743 let n = match self.peek().clone() {
14744 Token::Integer(v) if u32::try_from(v).is_ok() => {
14745 self.advance();
14746 v as u32
14747 }
14748 other => {
14749 return Err(self.err(format!(
14750 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14751 )));
14752 }
14753 };
14754 match key.to_ascii_uppercase().as_str() {
14755 "MODULUS" => modulus = Some(n),
14756 "REMAINDER" => remainder = Some(n),
14757 other => {
14758 return Err(self.err(format!(
14759 "FOR VALUES WITH: unknown key {other:?}; \
14760 expected MODULUS or REMAINDER"
14761 )));
14762 }
14763 }
14764 match self.peek() {
14765 Token::Comma => {
14766 self.advance();
14767 }
14768 Token::RParen => {
14769 self.advance();
14770 break;
14771 }
14772 other => {
14773 return Err(self.err(format!(
14774 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14775 )));
14776 }
14777 }
14778 }
14779 let modulus = modulus
14780 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14781 let remainder = remainder.ok_or_else(|| {
14782 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14783 })?;
14784 if modulus == 0 {
14785 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14786 }
14787 if remainder >= modulus {
14788 return Err(self.err(format!(
14789 "FOR VALUES WITH: REMAINDER ({remainder}) \
14790 must be < MODULUS ({modulus})"
14791 )));
14792 }
14793 PartitionOfBoundsAst::Hash { modulus, remainder }
14794 } else {
14795 match self.peek() {
14796 Token::From => {
14797 self.advance();
14798 let lower = Box::new(self.parse_partition_bound_expr()?);
14799 if !matches!(self.peek(), Token::To) {
14800 return Err(self.err(format!(
14801 "expected TO after FROM (...), got {:?}",
14802 self.peek()
14803 )));
14804 }
14805 self.advance();
14806 let upper = Box::new(self.parse_partition_bound_expr()?);
14807 PartitionOfBoundsAst::Range { lower, upper }
14808 }
14809 // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14810 Token::In => {
14811 self.advance();
14812 if !matches!(self.peek(), Token::LParen) {
14813 return Err(self.err(format!(
14814 "expected '(' after FOR VALUES IN, got {:?}",
14815 self.peek()
14816 )));
14817 }
14818 self.advance();
14819 let mut values = Vec::new();
14820 loop {
14821 values.push(self.parse_expr(0)?);
14822 match self.peek() {
14823 Token::Comma => {
14824 self.advance();
14825 }
14826 Token::RParen => {
14827 self.advance();
14828 break;
14829 }
14830 other => {
14831 return Err(self.err(format!(
14832 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
14833 )));
14834 }
14835 }
14836 }
14837 if values.is_empty() {
14838 return Err(self.err(
14839 "FOR VALUES IN requires at least one literal".to_string(),
14840 ));
14841 }
14842 PartitionOfBoundsAst::List { values }
14843 }
14844 other => {
14845 return Err(self.err(format!(
14846 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
14847 )));
14848 }
14849 }
14850 }
14851 }
14852 other => {
14853 return Err(self.err(format!(
14854 "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
14855 )));
14856 }
14857 };
14858 Ok(PartitionOfSpec {
14859 parent_name,
14860 bounds,
14861 })
14862 }
14863
14864 /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
14865 /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
14866 /// markers (no-arg builtins) so the engine resolves them
14867 /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
14868 fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
14869 if !matches!(self.peek(), Token::LParen) {
14870 return Err(self.err(format!(
14871 "expected '(' before partition bound, got {:?}",
14872 self.peek()
14873 )));
14874 }
14875 self.advance();
14876 let expr = match self.peek() {
14877 Token::Ident(s) | Token::QuotedIdent(s)
14878 if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
14879 {
14880 let name = s.to_ascii_uppercase();
14881 self.advance();
14882 crate::ast::Expr::FunctionCall {
14883 name,
14884 args: Vec::new(),
14885 }
14886 }
14887 _ => self.parse_expr(0)?,
14888 };
14889 if !matches!(self.peek(), Token::RParen) {
14890 return Err(self.err(format!(
14891 "expected ')' after partition bound, got {:?}",
14892 self.peek()
14893 )));
14894 }
14895 self.advance();
14896 Ok(expr)
14897 }
14898
14899 /// v7.14.0 — true when the next tokens look like an inline
14900 /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
14901 /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
14902 /// — each followed by an optional name + `(...)`. Critical:
14903 /// a column NAMED `key` / `index` (PG accepts as ident) must
14904 /// NOT be mistaken for the KEY constraint shape. We disambig
14905 /// by requiring the keyword to be followed by either `(` or
14906 /// `<ident> (`.
14907 fn peek_mysql_inline_key_start(&self) -> bool {
14908 let cur = self.peek();
14909 // Shapes:
14910 // KEY (cols)
14911 // KEY name (cols)
14912 // INDEX (cols)
14913 // INDEX name (cols)
14914 // UNIQUE KEY [name] (cols)
14915 // UNIQUE INDEX [name] (cols)
14916 // FULLTEXT [KEY|INDEX] [name] (cols)
14917 // SPATIAL [KEY|INDEX] [name] (cols)
14918 let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
14919 // tokens at skip = the position AFTER the index-form
14920 // keywords (KEY/INDEX) have been consumed.
14921 match self.tokens.get(skip) {
14922 Some(Token::LParen) => true,
14923 Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
14924 matches!(self.tokens.get(skip + 1), Some(Token::LParen))
14925 }
14926 _ => false,
14927 }
14928 };
14929 // `INDEX` lexes as Token::Index (reserved), not as
14930 // Token::Ident("index"). Both shapes count as a KEY/INDEX
14931 // start; the peek helper below handles either.
14932 let is_key_or_index_tok = |t: &Token| -> bool {
14933 matches!(t, Token::Index)
14934 || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
14935 };
14936 match cur {
14937 Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
14938 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14939 after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
14940 }
14941 Token::Ident(s)
14942 if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
14943 {
14944 let nxt = self.tokens.get(self.pos + 1);
14945 let after_after = if nxt.is_some_and(is_key_or_index_tok) {
14946 self.pos + 2
14947 } else {
14948 self.pos + 1
14949 };
14950 after_keyword_followed_by_paren_or_ident_paren(after_after)
14951 }
14952 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
14953 let nxt = self.tokens.get(self.pos + 1);
14954 if !nxt.is_some_and(is_key_or_index_tok) {
14955 return false;
14956 }
14957 after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
14958 }
14959 _ => false,
14960 }
14961 }
14962
14963 /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
14964 /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
14965 /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
14966 /// returns Some(TableConstraint::Index) so the engine builds
14967 /// a real BTree index on the leading column (mysqldump
14968 /// `KEY idx_posts_author (author_id)` shape).
14969 /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
14970 /// (the storage layer has no matching AM).
14971 fn parse_mysql_inline_key(
14972 &mut self,
14973 ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
14974 // Detect UNIQUE prefix.
14975 let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
14976 {
14977 self.advance();
14978 true
14979 } else {
14980 false
14981 };
14982 // Consume FULLTEXT / SPATIAL prefix and record which one
14983 // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
14984 // dedicated TableConstraint variant so the engine can
14985 // build a tsvector-GIN; SPATIAL still has no matching
14986 // AM, so it falls back to accept-as-no-op.
14987 let mut is_fulltext = false;
14988 let mut is_spatial = false;
14989 if let Token::Ident(s) = self.peek().clone() {
14990 if s.eq_ignore_ascii_case("fulltext") {
14991 self.advance();
14992 is_fulltext = true;
14993 } else if s.eq_ignore_ascii_case("spatial") {
14994 self.advance();
14995 is_spatial = true;
14996 }
14997 }
14998 // KEY / INDEX keyword. `INDEX` lexes as Token::Index
14999 // (reserved); accept either token shape.
15000 match self.peek() {
15001 Token::Index => {
15002 self.advance();
15003 }
15004 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15005 self.advance();
15006 }
15007 other => {
15008 return Err(self.err(alloc::format!(
15009 "expected KEY/INDEX in inline index declaration, got {other:?}"
15010 )));
15011 }
15012 }
15013 // Optional index name (an ident before the `(`).
15014 // v7.15.0 — capture the name when present so the engine
15015 // builds the secondary index under the user's chosen
15016 // name (matches mysqldump's `KEY idx_x (col)` shape).
15017 let mut idx_name: Option<String> = None;
15018 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
15019 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
15020 {
15021 if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
15022 idx_name = Some(s);
15023 }
15024 }
15025 // Optional `USING BTREE` / `USING HASH` (MySQL).
15026 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15027 self.advance();
15028 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15029 self.advance();
15030 }
15031 }
15032 // Required column list `(col [, col]*)`.
15033 if !matches!(self.peek(), Token::LParen) {
15034 return Err(self.err(alloc::format!(
15035 "expected '(' in inline KEY/INDEX, got {:?}",
15036 self.peek()
15037 )));
15038 }
15039 self.advance();
15040 let mut cols: Vec<String> = Vec::new();
15041 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
15042 self.advance();
15043 cols.push(s);
15044 // Skip optional `(length)` per-column prefix.
15045 if matches!(self.peek(), Token::LParen) {
15046 let mut depth = 1usize;
15047 self.advance();
15048 while depth > 0 {
15049 match self.peek() {
15050 Token::LParen => depth += 1,
15051 Token::RParen => depth -= 1,
15052 Token::Eof => break,
15053 _ => {}
15054 }
15055 self.advance();
15056 }
15057 }
15058 // Skip optional ASC / DESC.
15059 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
15060 || matches!(self.peek(), Token::Asc | Token::Desc)
15061 {
15062 self.advance();
15063 }
15064 if matches!(self.peek(), Token::Comma) {
15065 self.advance();
15066 continue;
15067 }
15068 break;
15069 }
15070 if matches!(self.peek(), Token::RParen) {
15071 self.advance();
15072 }
15073 // Trailing options on the inline index — comment / etc.
15074 // Skip until comma or `)`.
15075 while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
15076 self.advance();
15077 }
15078 if cols.is_empty() {
15079 return Ok(None);
15080 }
15081 if is_unique {
15082 // Carry the captured idx_name on UNIQUE too so future
15083 // engine work can name the underlying BTree
15084 // accordingly; today the unique-constraint installer
15085 // synthesises the name itself, but Display round-trip
15086 // benefits from preserving it.
15087 Ok(Some(crate::ast::TableConstraint::Unique {
15088 name: idx_name,
15089 columns: cols,
15090 nulls_not_distinct: false,
15091 // MySQL inline UNIQUE KEY has no deferral vocabulary.
15092 deferrable: false,
15093 initially_deferred: false,
15094 }))
15095 } else if is_fulltext {
15096 // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
15097 // routes through `TableConstraint::FulltextIndex`;
15098 // the engine builds a tsvector-GIN over each named
15099 // column so MATCH AGAINST gets a real inverted
15100 // index instead of a silently-dropped declaration.
15101 Ok(Some(crate::ast::TableConstraint::FulltextIndex {
15102 name: idx_name,
15103 columns: cols,
15104 }))
15105 } else if is_spatial {
15106 // SPG has no native SPATIAL AM. Accept-as-no-op
15107 // (declaration is parsed, but no index is built).
15108 Ok(None)
15109 } else {
15110 // v7.15.0 — plain KEY / INDEX builds a real BTree
15111 // secondary index.
15112 Ok(Some(crate::ast::TableConstraint::Index {
15113 name: idx_name,
15114 columns: cols,
15115 }))
15116 }
15117 }
15118
15119 /// v7.14.0 — consume MySQL/MariaDB table-options tail after
15120 /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
15121 /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
15122 /// (in any order, separated by whitespace).
15123 /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
15124 /// storage-parameter clause on CREATE TABLE. SPG has no per-table
15125 /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
15126 /// bare ident here, and only the parenthesised form is reloptions (so this
15127 /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
15128 fn consume_with_reloptions(&mut self) {
15129 let is_with = matches!(
15130 self.peek(),
15131 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15132 );
15133 if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
15134 return;
15135 }
15136 self.advance(); // WITH
15137 self.advance(); // (
15138 let mut depth = 1u32;
15139 while depth > 0 && !matches!(self.peek(), Token::Eof) {
15140 match self.peek() {
15141 Token::LParen => depth += 1,
15142 Token::RParen => depth -= 1,
15143 _ => {}
15144 }
15145 self.advance();
15146 }
15147 }
15148
15149 fn consume_mysql_table_options(&mut self) {
15150 loop {
15151 // Heuristic: a table option is an ident (or `DEFAULT`
15152 // reserved keyword) followed by `=` and an
15153 // ident / string / integer.
15154 let name_lc = match self.peek().clone() {
15155 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15156 Token::Default => alloc::string::String::from("default"),
15157 _ => break,
15158 };
15159 let known = matches!(
15160 name_lc.as_str(),
15161 "engine"
15162 | "default"
15163 | "charset"
15164 | "collate"
15165 | "auto_increment"
15166 | "row_format"
15167 | "comment"
15168 | "pack_keys"
15169 | "stats_persistent"
15170 | "stats_auto_recalc"
15171 | "stats_sample_pages"
15172 | "key_block_size"
15173 | "tablespace"
15174 | "min_rows"
15175 | "max_rows"
15176 | "checksum"
15177 | "delay_key_write"
15178 | "insert_method"
15179 | "data"
15180 | "index"
15181 | "encryption"
15182 | "compression"
15183 );
15184 if !known {
15185 break;
15186 }
15187 self.advance(); // option name
15188 // `DEFAULT` optional prefix is followed by `CHARSET` /
15189 // `COLLATE`; consume the next ident too.
15190 if name_lc == "default" {
15191 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15192 self.advance();
15193 }
15194 }
15195 if matches!(self.peek(), Token::Eq) {
15196 self.advance();
15197 }
15198 match self.peek() {
15199 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_) | Token::Integer(_) => {
15200 self.advance();
15201 }
15202 _ => {}
15203 }
15204 }
15205 }
15206
15207 /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
15208 /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
15209 /// sure (otherwise a column literally named `primary` would
15210 /// be mistaken).
15211 fn peek_table_level_pk_start(&self) -> bool {
15212 let cur = self.peek();
15213 let nxt = self.tokens.get(self.pos + 1);
15214 let nxt2 = self.tokens.get(self.pos + 2);
15215 let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
15216 let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
15217 let is_lparen = matches!(nxt2, Some(Token::LParen));
15218 is_primary && is_key && is_lparen
15219 }
15220
15221 /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
15222 /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
15223 /// (mailrs round-5 G10).
15224 fn peek_table_level_unique_start(&self) -> bool {
15225 let cur = self.peek();
15226 let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
15227 if !is_unique {
15228 return false;
15229 }
15230 let n1 = self.tokens.get(self.pos + 1);
15231 // Plain `UNIQUE (…)`.
15232 if matches!(n1, Some(Token::LParen)) {
15233 return true;
15234 }
15235 // `UNIQUE NULLS [NOT] DISTINCT (…)`.
15236 let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
15237 if !is_nulls {
15238 return false;
15239 }
15240 let n2 = self.tokens.get(self.pos + 2);
15241 let n3 = self.tokens.get(self.pos + 3);
15242 let n4 = self.tokens.get(self.pos + 4);
15243 // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15244 if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15245 return true;
15246 }
15247 // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15248 if matches!(n2, Some(Token::Not))
15249 && matches!(n3, Some(Token::Distinct))
15250 && matches!(n4, Some(Token::LParen))
15251 {
15252 return true;
15253 }
15254 false
15255 }
15256
15257 fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15258 self.advance(); // PRIMARY
15259 self.advance(); // KEY
15260 let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15261 // v7.39 (round 711) — the trailer's values are CARRIED now; round
15262 // 621 consumed and dropped them (the storing half of F08).
15263 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15264 Ok(crate::ast::TableConstraint::PrimaryKey {
15265 name: None,
15266 columns,
15267 deferrable,
15268 initially_deferred,
15269 })
15270 }
15271
15272 fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15273 self.advance(); // UNIQUE
15274 // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15275 // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15276 // is `NULLS DISTINCT` per the SQL standard.
15277 let mut nulls_not_distinct = false;
15278 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15279 let n1 = self.tokens.get(self.pos + 1);
15280 let n2 = self.tokens.get(self.pos + 2);
15281 let is_not = matches!(n1, Some(Token::Not));
15282 let is_distinct = matches!(n2, Some(Token::Distinct));
15283 if is_not && is_distinct {
15284 self.advance(); // NULLS
15285 self.advance(); // NOT
15286 self.advance(); // DISTINCT
15287 nulls_not_distinct = true;
15288 } else if matches!(n1, Some(Token::Distinct)) {
15289 self.advance(); // NULLS
15290 self.advance(); // DISTINCT
15291 }
15292 }
15293 let columns = self.parse_paren_ident_list("UNIQUE")?;
15294 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15295 Ok(crate::ast::TableConstraint::Unique {
15296 name: None,
15297 columns,
15298 nulls_not_distinct,
15299 deferrable,
15300 initially_deferred,
15301 })
15302 }
15303
15304 /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15305 /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15306 /// expression.
15307 /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15308 /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15309 /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15310 /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15311 /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15312 /// commit: `NOT` starts no other suffix here, but reading both
15313 /// tokens before advancing keeps the caller's error message intact
15314 /// if someone writes `NOT NULL` by mistake.
15315 fn parse_not_valid_suffix(&mut self) -> bool {
15316 if !matches!(self.peek(), Token::Not) {
15317 return false;
15318 }
15319 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15320 {
15321 return false;
15322 }
15323 self.advance();
15324 self.advance();
15325 true
15326 }
15327
15328 fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15329 self.advance(); // EXCLUDE
15330 // Optional `USING <method>`.
15331 let mut method = None;
15332 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15333 self.advance();
15334 method = Some(match self.advance() {
15335 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15336 other => {
15337 return Err(self.err(alloc::format!(
15338 "expected index method after USING, got {other:?}"
15339 )));
15340 }
15341 });
15342 }
15343 if !matches!(self.peek(), Token::LParen) {
15344 return Err(self.err(alloc::format!(
15345 "expected '(' after EXCLUDE, got {:?}",
15346 self.peek()
15347 )));
15348 }
15349 self.advance();
15350 let mut elements: Vec<(String, String)> = Vec::new();
15351 loop {
15352 let col = match self.advance() {
15353 Token::Ident(s) | Token::QuotedIdent(s) => s,
15354 other => {
15355 return Err(self.err(alloc::format!(
15356 "expected column name in EXCLUDE, got {other:?}"
15357 )));
15358 }
15359 };
15360 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15361 return Err(self.err(alloc::format!(
15362 "expected WITH after EXCLUDE column, got {:?}",
15363 self.peek()
15364 )));
15365 }
15366 self.advance();
15367 let op = match self.advance() {
15368 Token::InetOverlap => String::from("&&"),
15369 Token::Intersects => String::from("?#"),
15370 Token::IsBelow => String::from("<^"),
15371 Token::IsAbove => String::from(">^"),
15372 Token::PatternLt => String::from("~<~"),
15373 Token::PatternLtEq => String::from("~<=~"),
15374 Token::PatternGt => String::from("~>~"),
15375 Token::PatternGtEq => String::from("~>=~"),
15376 Token::TsMatchOld => String::from("@@@"),
15377 Token::Eq => String::from("="),
15378 Token::JsonContains => String::from("@>"),
15379 Token::JsonContainedBy => String::from("<@"),
15380 Token::OverLeft => String::from("&<"),
15381 Token::OverRight => String::from("&>"),
15382 other => {
15383 return Err(self.err(alloc::format!(
15384 "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15385 )));
15386 }
15387 };
15388 elements.push((col, op));
15389 if matches!(self.peek(), Token::Comma) {
15390 self.advance();
15391 continue;
15392 }
15393 break;
15394 }
15395 if !matches!(self.peek(), Token::RParen) {
15396 return Err(self.err(alloc::format!(
15397 "expected ')' to close EXCLUDE, got {:?}",
15398 self.peek()
15399 )));
15400 }
15401 self.advance();
15402 Ok(crate::ast::TableConstraint::Exclude {
15403 name: None,
15404 method,
15405 elements,
15406 })
15407 }
15408
15409 fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15410 self.advance(); // CHECK
15411 if !matches!(self.peek(), Token::LParen) {
15412 return Err(self.err(alloc::format!(
15413 "expected '(' after CHECK, got {:?}",
15414 self.peek()
15415 )));
15416 }
15417 self.advance();
15418 let expr = self.parse_expr(0)?;
15419 if !matches!(self.peek(), Token::RParen) {
15420 return Err(self.err(alloc::format!(
15421 "expected ')' to close CHECK predicate, got {:?}",
15422 self.peek()
15423 )));
15424 }
15425 self.advance();
15426 // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15427 // are no existing rows for PG to skip, so it rejects the suffix.
15428 Ok(crate::ast::TableConstraint::Check {
15429 name: None,
15430 expr,
15431 not_valid: false,
15432 })
15433 }
15434
15435 /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15436 fn peek_table_level_check_start(&self) -> bool {
15437 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15438 }
15439
15440 /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15441 /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15442 /// on the dedicated FK path (`parse_table_level_fk` consumes its
15443 /// own CONSTRAINT prefix).
15444 fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15445 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15446 return None;
15447 }
15448 // tokens[pos+1] is the constraint name (any ident-like);
15449 // tokens[pos+2] is the kind keyword.
15450 match self.tokens.get(self.pos + 2) {
15451 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15452 Some(NamedTableConstraintKind::Check)
15453 }
15454 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15455 Some(NamedTableConstraintKind::Unique)
15456 }
15457 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15458 Some(NamedTableConstraintKind::PrimaryKey)
15459 }
15460 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15461 Some(NamedTableConstraintKind::Exclude)
15462 }
15463 _ => None,
15464 }
15465 }
15466
15467 fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15468 if !matches!(self.peek(), Token::LParen) {
15469 return Err(self.err(alloc::format!(
15470 "expected '(' after {ctx}, got {:?}",
15471 self.peek()
15472 )));
15473 }
15474 self.advance();
15475 let mut out = Vec::new();
15476 loop {
15477 out.push(self.expect_ident_like()?);
15478 match self.peek() {
15479 Token::Comma => {
15480 self.advance();
15481 }
15482 Token::RParen => {
15483 self.advance();
15484 break;
15485 }
15486 other => {
15487 return Err(self.err(alloc::format!(
15488 "expected ',' or ')' in {ctx} list, got {other:?}"
15489 )));
15490 }
15491 }
15492 }
15493 if out.is_empty() {
15494 return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15495 }
15496 Ok(out)
15497 }
15498
15499 /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15500 /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15501 /// table-level FK; a column def never starts with either keyword
15502 /// (column names are not in this reserved set).
15503 fn peek_constraint_or_fk_start(&self) -> bool {
15504 let is_constraint_kw = matches!(
15505 self.peek(),
15506 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15507 );
15508 let is_foreign_kw = matches!(
15509 self.peek(),
15510 Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15511 );
15512 is_constraint_kw || is_foreign_kw
15513 }
15514
15515 /// v7.6.0 — parse a table-level FK clause:
15516 /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15517 /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15518 fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15519 let mut name: Option<String> = None;
15520 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15521 self.advance();
15522 name = Some(self.expect_ident_like()?);
15523 }
15524 // `FOREIGN`
15525 match self.advance() {
15526 Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15527 other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15528 }
15529 // `KEY`
15530 match self.advance() {
15531 Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15532 other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15533 }
15534 // `(col, col, ...)`
15535 if !matches!(self.peek(), Token::LParen) {
15536 return Err(self.err(format!(
15537 "expected '(' after FOREIGN KEY, got {:?}",
15538 self.peek()
15539 )));
15540 }
15541 self.advance();
15542 let mut columns = Vec::new();
15543 loop {
15544 columns.push(self.expect_ident_like()?);
15545 match self.peek() {
15546 Token::Comma => {
15547 self.advance();
15548 }
15549 Token::RParen => {
15550 self.advance();
15551 break;
15552 }
15553 other => {
15554 return Err(self.err(format!(
15555 "expected ',' or ')' in FK column list, got {other:?}"
15556 )));
15557 }
15558 }
15559 }
15560 if columns.is_empty() {
15561 return Err(self.err("FOREIGN KEY requires at least one column".into()));
15562 }
15563 let (
15564 parent_table,
15565 parent_columns,
15566 on_delete,
15567 on_update,
15568 match_type,
15569 deferrable,
15570 initially_deferred,
15571 ) = self.parse_references_tail(columns.len())?;
15572 Ok(ForeignKeyConstraint {
15573 name,
15574 columns,
15575 parent_table,
15576 parent_columns,
15577 on_delete,
15578 on_update,
15579 match_type,
15580 deferrable,
15581 initially_deferred,
15582 })
15583 }
15584
15585 /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15586 /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15587 /// the local column count, used to default the parent column
15588 /// list when omitted (SQL spec: parent's PK is implied).
15589 fn parse_references_tail(
15590 &mut self,
15591 expected_arity: usize,
15592 ) -> Result<
15593 (
15594 String,
15595 Vec<String>,
15596 FkAction,
15597 FkAction,
15598 crate::ast::MatchType,
15599 // v7.39 (round 288) — deferrable, initially_deferred.
15600 bool,
15601 bool,
15602 ),
15603 ParseError,
15604 > {
15605 match self.advance() {
15606 Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15607 other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15608 }
15609 let parent_table = self.expect_ident_like()?;
15610 let mut parent_columns: Vec<String> = Vec::new();
15611 if matches!(self.peek(), Token::LParen) {
15612 self.advance();
15613 loop {
15614 parent_columns.push(self.expect_ident_like()?);
15615 match self.peek() {
15616 Token::Comma => {
15617 self.advance();
15618 }
15619 Token::RParen => {
15620 self.advance();
15621 break;
15622 }
15623 other => {
15624 return Err(self.err(format!(
15625 "expected ',' or ')' in REFERENCES column list, got {other:?}"
15626 )));
15627 }
15628 }
15629 }
15630 }
15631 if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15632 return Err(self.err(format!(
15633 "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15634 expected_arity,
15635 parent_columns.len()
15636 )));
15637 }
15638 // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15639 // it between the referenced column list and the ON / DEFERRABLE
15640 // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15641 // is skipped when any referencing column is NULL), so SIMPLE —
15642 // the default, and the only spelling pg_dump emits — is accepted
15643 // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15644 // mixed-NULL rule, which is not wired yet; reject them honestly
15645 // rather than silently applying SIMPLE (PG itself errors on
15646 // MATCH PARTIAL as "not yet implemented").
15647 let mut match_type = crate::ast::MatchType::Simple;
15648 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15649 self.advance();
15650 // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15651 // SIMPLE / PARTIAL arrive as bare identifiers.
15652 let kind = match self.advance() {
15653 Token::Full => "FULL".to_string(),
15654 Token::Ident(s) => s.to_uppercase(),
15655 other => {
15656 return Err(self.err(format!(
15657 "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15658 )));
15659 }
15660 };
15661 match kind.as_str() {
15662 "SIMPLE" => {} // Default — match_type stays Simple.
15663 // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15664 // when ALL referencing columns are NULL; a mixed-NULL key errors.
15665 "FULL" => match_type = crate::ast::MatchType::Full,
15666 "PARTIAL" => {
15667 return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15668 }
15669 _ => {
15670 return Err(self.err(format!(
15671 "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15672 )));
15673 }
15674 }
15675 }
15676 // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15677 // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15678 // <action>` / `ON UPDATE <action>` in either order. PG /
15679 // pg_dump emits the timing clause AFTER the ON clauses
15680 // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15681 // but the SQL spec allows either order. We loop over
15682 // every possible trailer and dispatch on the next token,
15683 // stopping when nothing matches. Phase 3.1 changes the
15684 // bare DEFERRABLE form from hard-error to accept-as-
15685 // immediate; SPG is single-writer with no deferred-
15686 // constraint window so the runtime semantics are always
15687 // immediate even when INITIALLY DEFERRED is requested.
15688 // PG's default referential action (no ON DELETE / ON UPDATE
15689 // clause) is NO ACTION, not RESTRICT — the two enforce
15690 // identically in SPG (single-writer, no deferred window; see the
15691 // shared match arm in constraints.rs) but information_schema.
15692 // referential_constraints must report NO ACTION to match PG.
15693 let mut on_delete = FkAction::NoAction;
15694 let mut on_update = FkAction::NoAction;
15695 let mut seen_on_delete = false;
15696 let mut seen_on_update = false;
15697 let mut deferrable = false;
15698 let mut initially_deferred = false;
15699 loop {
15700 // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15701 let before = self.pos;
15702 let (d, idef) = self.consume_deferrable_clauses_timed()?;
15703 if self.pos != before {
15704 deferrable = d;
15705 initially_deferred = idef;
15706 continue;
15707 }
15708 // ON DELETE / ON UPDATE.
15709 if !matches!(self.peek(), Token::On) {
15710 break;
15711 }
15712 self.advance();
15713 let which = self.advance();
15714 let action = self.parse_fk_action()?;
15715 match which {
15716 Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15717 if seen_on_delete {
15718 return Err(self.err("ON DELETE specified twice".into()));
15719 }
15720 seen_on_delete = true;
15721 on_delete = action;
15722 }
15723 Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15724 if seen_on_update {
15725 return Err(self.err("ON UPDATE specified twice".into()));
15726 }
15727 seen_on_update = true;
15728 on_update = action;
15729 }
15730 other => {
15731 return Err(
15732 self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15733 );
15734 }
15735 }
15736 }
15737 Ok((
15738 parent_table,
15739 parent_columns,
15740 on_delete,
15741 on_update,
15742 match_type,
15743 deferrable,
15744 initially_deferred,
15745 ))
15746 }
15747
15748 /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15749 /// NO ACTION`.
15750 fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15751 match self.advance() {
15752 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15753 Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15754 Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15755 Token::Null => Ok(FkAction::SetNull),
15756 Token::Default => Ok(FkAction::SetDefault),
15757 other => Err(self.err(format!(
15758 "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15759 ))),
15760 },
15761 Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15762 Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15763 other => Err(self.err(format!(
15764 "expected ACTION after NO in FK action, got {other:?}"
15765 ))),
15766 },
15767 other => Err(self.err(format!(
15768 "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15769 ))),
15770 }
15771 }
15772
15773 /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15774 /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15775 fn consume_if_not_exists(&mut self) -> bool {
15776 // `IF` arrives as a bare Ident (we don't reserve it because it
15777 // also appears mid-expression in PG, though we don't support
15778 // those forms yet).
15779 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15780 if !looks_like_if {
15781 return false;
15782 }
15783 // Peek one ahead before committing: only consume IF when it's
15784 // actually `IF NOT EXISTS`.
15785 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
15786 return false;
15787 }
15788 if !matches!(
15789 self.tokens.get(self.pos + 2),
15790 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15791 ) {
15792 return false;
15793 }
15794 self.advance(); // IF
15795 self.advance(); // NOT
15796 self.advance(); // EXISTS
15797 true
15798 }
15799
15800 /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
15801 /// Consumes IF EXISTS as a pair; returns false otherwise
15802 /// without consuming any tokens.
15803 /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
15804 /// ENABLE/DISABLE/FORCE/NO FORCE.
15805 fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
15806 for kw in ["row", "level", "security"] {
15807 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
15808 {
15809 return Err(self.err(alloc::format!(
15810 "expected {} in ROW LEVEL SECURITY, got {:?}",
15811 kw.to_ascii_uppercase(),
15812 self.peek()
15813 )));
15814 }
15815 self.advance();
15816 }
15817 Ok(())
15818 }
15819
15820 fn consume_if_exists(&mut self) -> bool {
15821 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15822 if !looks_like_if {
15823 return false;
15824 }
15825 if !matches!(
15826 self.tokens.get(self.pos + 1),
15827 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15828 ) {
15829 return false;
15830 }
15831 self.advance(); // IF
15832 self.advance(); // EXISTS
15833 true
15834 }
15835
15836 /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
15837 /// qualifiers after an index column ref. ASC / DESC are
15838 /// reserved tokens; NULLS / FIRST / LAST are bare idents.
15839 /// We accept and discard them since single-column BTree
15840 /// stores rows in natural key order today.
15841 /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
15842 /// ORDER BY key. Returns None when absent.
15843 fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
15844 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15845 return Ok(None);
15846 }
15847 self.advance();
15848 match self.advance() {
15849 Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
15850 Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
15851 other => Err(self.err(alloc::format!(
15852 "expected FIRST or LAST after NULLS, got {other:?}"
15853 ))),
15854 }
15855 }
15856
15857 /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
15858 /// rather than discarded.
15859 ///
15860 /// SPG's index does not scan in a direction — column ordering is
15861 /// intrinsic to the storage — but `pg_indexes.indexdef` is a
15862 /// reproduction of the DDL, and dropping the clause meant
15863 /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
15864 /// dump lost it, and a schema diff saw drift on every run.
15865 fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
15866 let mut order = crate::ast::IndexColumnOrder::default();
15867 loop {
15868 match self.peek() {
15869 Token::Asc => {
15870 self.advance();
15871 }
15872 Token::Desc => {
15873 order.descending = true;
15874 self.advance();
15875 }
15876 Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
15877 let look = self.tokens.get(self.pos + 1);
15878 if matches!(
15879 look,
15880 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
15881 || k.eq_ignore_ascii_case("last")
15882 ) {
15883 self.advance();
15884 order.nulls_first = Some(matches!(
15885 self.advance(),
15886 Token::Ident(k) if k.eq_ignore_ascii_case("first")
15887 ));
15888 } else {
15889 break;
15890 }
15891 }
15892 _ => break,
15893 }
15894 }
15895 order
15896 }
15897
15898 fn parse_create_index_stmt_after_create(
15899 &mut self,
15900 is_unique: bool,
15901 ) -> Result<Statement, ParseError> {
15902 // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
15903 debug_assert!(matches!(self.peek(), Token::Index));
15904 self.advance();
15905 // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
15906 // SPG's CREATE INDEX is synchronous end-to-end today (real
15907 // CONCURRENTLY variant with restartable scans queues with
15908 // v7.39 indexes epic), so the modifier has no runtime effect
15909 // — same accept-and-no-op shape as v7.37.16.5 DETACH
15910 // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
15911 // VIEW CONCURRENTLY.
15912 let mut concurrently = false;
15913 if matches!(
15914 self.peek(),
15915 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
15916 ) {
15917 self.advance();
15918 concurrently = true;
15919 }
15920 let if_not_exists = self.consume_if_not_exists();
15921 // v7.39 (read01 round 93) — the index name is optional (PG since
15922 // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
15923 // When the token after `[IF NOT EXISTS]` is already `ON`, no name
15924 // was given; leave it empty and the engine derives a PG-style
15925 // `<table>_<cols>_idx` name at CREATE time (with collision counter).
15926 let name = if matches!(self.peek(), Token::On) {
15927 String::new()
15928 } else {
15929 self.expect_ident_like()?
15930 };
15931 if !matches!(self.peek(), Token::On) {
15932 return Err(self.err(format!(
15933 "expected ON after CREATE INDEX <name>, got {:?}",
15934 self.peek()
15935 )));
15936 }
15937 self.advance();
15938 let table = self.expect_ident_like()?;
15939 // Optional `USING <method>` — only recognised method in v2.0 is
15940 // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
15941 // ident `using` (we don't promote it to a reserved keyword
15942 // because it isn't reserved anywhere else in our SQL surface).
15943 let mut method_name: Option<String> = None;
15944 let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15945 self.advance();
15946 let m = self.expect_ident_like()?;
15947 method_name = Some(m.to_ascii_lowercase());
15948 match m.to_ascii_lowercase().as_str() {
15949 "hnsw" => IndexMethod::Hnsw,
15950 "btree" => IndexMethod::BTree,
15951 "brin" => IndexMethod::Brin,
15952 // v7.12.3 — real GIN inverted index over `tsvector`.
15953 // v7.9.26b's `USING gin` → BTree silent fallback is
15954 // gone; the engine validates that the indexed column
15955 // is `tsvector` at CREATE INDEX time.
15956 "gin" => IndexMethod::Gin,
15957 // v7.9.26b — PG `pg_dump` emits `USING gist` /
15958 // `USING spgist` / `USING hash` for their built-in
15959 // AMs that SPG doesn't have a matching
15960 // implementation for; degrade to BTree on the
15961 // leading column so the schema loads + the index
15962 // catalogue stays consistent. Operator pays the
15963 // planner cost only for the queries that would have
15964 // used the specialised AM.
15965 "gist" | "spgist" | "hash" => IndexMethod::BTree,
15966 // v7.11.3 — pgvector ships both `ivfflat` and
15967 // `hnsw`. Customers shouldn't have to choose
15968 // their on-disk index method based on what SPG
15969 // implements; accept `ivfflat` as a synonym for
15970 // `hnsw` so PG schemas using either method drop
15971 // in. The vector distance op (`<->` / `<#>` /
15972 // `<=>`) at query time still picks the metric.
15973 "ivfflat" => IndexMethod::Hnsw,
15974 other => {
15975 return Err(self.err(alloc::format!(
15976 "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
15977 )));
15978 }
15979 }
15980 } else {
15981 IndexMethod::BTree
15982 };
15983 if !matches!(self.peek(), Token::LParen) {
15984 return Err(self.err(format!(
15985 "expected '(' before indexed column, got {:?}",
15986 self.peek()
15987 )));
15988 }
15989 self.advance();
15990 // v6.8.2 — accept either a bare column ident (legacy) or
15991 // an expression `fn(col, …)` for expression indexes.
15992 // Distinguish by peeking the token *after* the current
15993 // ident: `ident )` is the legacy column-only path;
15994 // anything else triggers the Pratt expression parser.
15995 // (`advance()` uses `mem::replace` to nil out the current
15996 // slot, so we can't save+rewind cleanly — peek-ahead via
15997 // direct index avoids the mutation.)
15998 let mut opclass: Option<String> = None;
15999 let mut key_collation: Option<String> = None;
16000 let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
16001 // Single column with `)` immediately after — fast path.
16002 // v7.9.29 — also: bare column followed by `,` (the
16003 // multi-column form `(a, b, c)`). Without this branch
16004 // the leading ident gets pulled into `parse_expr`
16005 // which then sets `expression = Some(Column(a))` and
16006 // breaks Display round-trip on the multi-column shape.
16007 Token::Ident(s) | Token::QuotedIdent(s)
16008 if matches!(
16009 self.tokens.get(self.pos + 1),
16010 Some(Token::RParen | Token::Comma)
16011 ) =>
16012 {
16013 self.advance();
16014 (s, None)
16015 }
16016 // v7.9.22 — single column followed by a pgvector
16017 // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
16018 // v7.15.0 — capture the opclass instead of discarding
16019 // it so the engine can dispatch (e.g. `gin_trgm_ops`
16020 // → real trigram-shingle GIN over a TEXT column).
16021 // Vector/HNSW opclasses still take their distance
16022 // metric from the query operator (`<->` / `<#>` /
16023 // `<=>`), so for those callers the opclass stays
16024 // informational.
16025 // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
16026 // opclass: `(embedding public.vector_cosine_ops)`. Strip
16027 // the schema and dispatch on the bare opclass, the same
16028 // treatment table/type names get.
16029 Token::Ident(s) | Token::QuotedIdent(s)
16030 if matches!(
16031 self.tokens.get(self.pos + 1),
16032 Some(Token::Ident(_) | Token::QuotedIdent(_))
16033 ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
16034 && matches!(
16035 self.tokens.get(self.pos + 3),
16036 Some(Token::Ident(op) | Token::QuotedIdent(op))
16037 if is_vector_opclass_name(op)
16038 ) =>
16039 {
16040 self.advance(); // column name
16041 self.advance(); // schema qualifier
16042 self.advance(); // dot
16043 let op_tok = self.advance();
16044 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16045 opclass = Some(op.to_ascii_lowercase());
16046 }
16047 (s, None)
16048 }
16049 // r1038 — an operator class is recognised by its POSITION, not
16050 // by a list of names. It used to be `is_vector_opclass_name`,
16051 // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
16052 // sentori's migration wrote — was a syntax error while
16053 // `USING gin (doc)` parsed. Anything sitting between a column
16054 // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
16055 // two bare identifiers in a row are not valid there otherwise.
16056 Token::Ident(s) | Token::QuotedIdent(s)
16057 if matches!(
16058 self.tokens.get(self.pos + 1),
16059 Some(Token::Ident(op) | Token::QuotedIdent(op))
16060 if is_vector_opclass_name(op) || Self::opclass_position_follows(
16061 self.tokens.get(self.pos + 2)
16062 )
16063 ) =>
16064 {
16065 self.advance(); // column name
16066 // Capture the opclass token, lower-cased for
16067 // case-insensitive engine dispatch.
16068 let op_tok = self.advance();
16069 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16070 opclass = Some(op.to_ascii_lowercase());
16071 }
16072 (s, None)
16073 }
16074 Token::Ident(_) | Token::QuotedIdent(_) => {
16075 // v7.39 (round 538) — an explicit COLLATE on the key,
16076 // read by LOOKAHEAD because `parse_expr` absorbs the
16077 // clause as a no-op (SPG orders text by bytes, which is
16078 // the C collation, so it changes nothing to honour). PG
16079 // still PRINTS it: an explicitly written `"C"` and the
16080 // collation a column inherits are different collation
16081 // OBJECTS even where they sort identically, which is why
16082 // `(a COLLATE "C")` shows on a C-collation database too.
16083 if matches!(
16084 self.tokens.get(self.pos + 1),
16085 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
16086 ) {
16087 key_collation = match self.tokens.get(self.pos + 2) {
16088 Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
16089 Some(n.clone())
16090 }
16091 _ => None,
16092 };
16093 }
16094 let key_expr = self.parse_expr(0)?;
16095 let primary = extract_first_column(&key_expr).ok_or_else(|| {
16096 self.err("expression index key must reference at least one column".into())
16097 })?;
16098 (primary, Some(key_expr))
16099 }
16100 // v7.37.43-T4 — parenthesised expression index key
16101 // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
16102 // PG's CREATE INDEX requires the expression to be in
16103 // its own parens to disambiguate function calls from
16104 // column lists, so this `LParen` is the inner open-paren
16105 // of an expression key. parse_expr handles the recursive
16106 // descent and consumes the matching `RParen`.
16107 Token::LParen => {
16108 let key_expr = self.parse_expr(0)?;
16109 let primary = extract_first_column(&key_expr).ok_or_else(|| {
16110 self.err("expression index key must reference at least one column".into())
16111 })?;
16112 (primary, Some(key_expr))
16113 }
16114 other => {
16115 return Err(self.err(format!(
16116 "expected column ident or expression, got {other:?}"
16117 )));
16118 }
16119 };
16120 // v7.9.14 — accept extra comma-separated columns inside
16121 // the index key parens (`CREATE INDEX … (a, b, c)`).
16122 // mailrs F2. Each extra column may carry an optional
16123 // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
16124 // — parsed and discarded; SPG doesn't honour direction
16125 // on a BTree index today (column ordering is intrinsic
16126 // to the storage). v7.10 will widen to genuine composite
16127 // index keys.
16128 let mut extra_columns: Vec<String> = Vec::new();
16129 // The leading column may also have ASC/DESC after it — and that
16130 // one is the column SPG indexes, so its clause is kept.
16131 let key_order = self.consume_optional_index_column_qualifiers();
16132 while matches!(self.peek(), Token::Comma) {
16133 self.advance();
16134 let extra = self.expect_ident_like()?;
16135 let _ = self.consume_optional_index_column_qualifiers();
16136 extra_columns.push(extra);
16137 }
16138 if !matches!(self.peek(), Token::RParen) {
16139 return Err(self.err(format!(
16140 "expected ')' after indexed column / expression, got {:?}",
16141 self.peek()
16142 )));
16143 }
16144 self.advance();
16145 // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
16146 // index-only-scan annotation. Bare ident (not a reserved
16147 // keyword) so we test by case-insensitive string match.
16148 let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
16149 {
16150 self.advance();
16151 if !matches!(self.peek(), Token::LParen) {
16152 return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
16153 }
16154 self.advance();
16155 let mut cols = Vec::new();
16156 loop {
16157 cols.push(self.expect_ident_like()?);
16158 match self.peek() {
16159 Token::Comma => {
16160 self.advance();
16161 }
16162 Token::RParen => {
16163 self.advance();
16164 break;
16165 }
16166 other => {
16167 return Err(self.err(format!(
16168 "expected ',' or ')' in INCLUDE list, got {other:?}"
16169 )));
16170 }
16171 }
16172 }
16173 cols
16174 } else {
16175 Vec::new()
16176 };
16177 // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
16178 // storage parameters. pgvector emits `WITH (lists = N)` for
16179 // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
16180 // SPG's HNSW picks its own parameters today (tunable via
16181 // env vars), so the WITH clause is informational and dropped.
16182 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
16183 self.advance();
16184 if !matches!(self.peek(), Token::LParen) {
16185 return Err(self.err(format!(
16186 "expected '(' after WITH in CREATE INDEX, got {:?}",
16187 self.peek()
16188 )));
16189 }
16190 self.advance();
16191 loop {
16192 if matches!(self.peek(), Token::RParen) {
16193 self.advance();
16194 break;
16195 }
16196 // Drain `key = value` or bare `key` tokens.
16197 let _ = self.advance(); // key
16198 if matches!(self.peek(), Token::Eq) {
16199 self.advance();
16200 let _ = self.advance(); // value (int / string / ident)
16201 }
16202 match self.peek() {
16203 Token::Comma => {
16204 self.advance();
16205 }
16206 Token::RParen => {
16207 self.advance();
16208 break;
16209 }
16210 other => {
16211 return Err(self.err(format!(
16212 "expected ',' or ')' in WITH (…) clause, got {other:?}"
16213 )));
16214 }
16215 }
16216 }
16217 }
16218 // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
16219 // which sits between the key list and the WHERE clause.
16220 let mut nulls_not_distinct = false;
16221 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16222 let n1 = self.tokens.get(self.pos + 1);
16223 let n2 = self.tokens.get(self.pos + 2);
16224 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
16225 self.advance(); // NULLS
16226 self.advance(); // NOT
16227 self.advance(); // DISTINCT
16228 nulls_not_distinct = true;
16229 } else if matches!(n1, Some(Token::Distinct)) {
16230 self.advance(); // NULLS
16231 self.advance(); // DISTINCT
16232 }
16233 }
16234 // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
16235 let partial_predicate = if matches!(self.peek(), Token::Where) {
16236 self.advance();
16237 Some(self.parse_expr(0)?)
16238 } else {
16239 None
16240 };
16241 // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16242 // sense: uniqueness over an ANN structure has no clean
16243 // semantics. Reject early. (BRIN UNIQUE is similarly
16244 // meaningless — block both.)
16245 if is_unique && !matches!(method, IndexMethod::BTree) {
16246 return Err(self.err(alloc::format!(
16247 "UNIQUE is only supported on BTree indexes, got USING {:?}",
16248 method
16249 )));
16250 }
16251 Ok(Statement::CreateIndex(CreateIndexStatement {
16252 concurrently,
16253 name,
16254 key_order,
16255 key_collation,
16256 table,
16257 column,
16258 nulls_not_distinct,
16259 method,
16260 if_not_exists,
16261 included_columns,
16262 partial_predicate,
16263 extra_columns: extra_columns.clone(),
16264 expression,
16265 is_unique,
16266 opclass,
16267 method_name,
16268 }))
16269 }
16270
16271 /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16272 /// column-level `REFERENCES ...` clause. The trailing FK is
16273 /// normalised into table-level shape (single-element columns +
16274 /// parent_columns) so the engine sees one uniform constraint list.
16275 fn parse_column_def_with_fk(
16276 &mut self,
16277 ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16278 let col = self.parse_column_def()?;
16279 // v7.39 (round 308, V29) — an explicitly named inline FK:
16280 // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16281 // loop leaves this spelling intact precisely so the name can be
16282 // kept here; PG reports it in violation messages and matches it
16283 // in `SET CONSTRAINTS`.
16284 let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16285 {
16286 self.advance();
16287 Some(self.expect_ident_like()?)
16288 } else {
16289 None
16290 };
16291 // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16292 let inline_references = matches!(
16293 self.peek(),
16294 Token::Ident(s) if s.eq_ignore_ascii_case("references")
16295 );
16296 if !inline_references {
16297 return Ok((col, None));
16298 }
16299 let (
16300 parent_table,
16301 parent_columns,
16302 on_delete,
16303 on_update,
16304 match_type,
16305 deferrable,
16306 initially_deferred,
16307 ) = self.parse_references_tail(1)?;
16308 let fk = ForeignKeyConstraint {
16309 name: declared_name,
16310 columns: vec![col.name.clone()],
16311 parent_table,
16312 parent_columns,
16313 on_delete,
16314 on_update,
16315 match_type,
16316 deferrable,
16317 initially_deferred,
16318 };
16319 Ok((col, Some(fk)))
16320 }
16321
16322 /// v7.13.0 — parse a column type (consuming the type ident and
16323 /// any trailing parameters / `[]`), without surrounding column
16324 /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16325 /// Returns the resolved `ColumnTypeName` plus implied
16326 /// `(auto_increment, not_null)` flags from PG SERIAL family
16327 /// shorthands — callers that don't expect those (ALTER COLUMN
16328 /// TYPE) can discard them.
16329 fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16330 let (ty, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16331 Ok(ty)
16332 }
16333
16334 #[allow(clippy::type_complexity)]
16335 fn parse_type_with_implied_flags(
16336 &mut self,
16337 ) -> Result<
16338 (
16339 ColumnTypeName,
16340 bool,
16341 bool,
16342 Option<String>,
16343 Collation,
16344 // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16345 bool,
16346 // v7.39 (round 676) — the collation NAME as written, which the
16347 // `Collation` enum above cannot carry.
16348 Option<String>,
16349 bool,
16350 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16351 // list captured at type-parse time. None for all
16352 // non-ENUM types.
16353 Option<Vec<String>>,
16354 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16355 // list. Distinct from ENUM (subset semantics).
16356 Option<Vec<String>>,
16357 // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16358 // width, lost when the type collapses to SmallInt / Int.
16359 Option<MysqlIntWidth>,
16360 // v7.39 (round 424) — declared fractional-seconds precision of a
16361 // MySQL temporal column (bare spelling = 0). None under PG.
16362 Option<u8>,
16363 ),
16364 ParseError,
16365 > {
16366 let mut ty_ident = match self.advance() {
16367 Token::Ident(s) => s,
16368 // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16369 // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16370 // '<span>'` literal grammar. As a column type it lands
16371 // here directly; downstream resolution still uses the
16372 // canonical lowercase string.
16373 Token::Interval => "interval".to_string(),
16374 other => {
16375 return Err(ParseError {
16376 message: format!("expected column type, got {other:?}"),
16377 token_pos: self.consumed_pos(),
16378 });
16379 }
16380 };
16381 // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16382 // pg_dump qualifies extension types (`public.vector(1024)`).
16383 // SPG is single-namespace; drop the schema and resolve the
16384 // bare type — same treatment table names already get.
16385 while matches!(self.peek(), Token::Dot) {
16386 self.advance();
16387 ty_ident = self.expect_ident_like()?;
16388 }
16389 let mut implied_auto_increment = false;
16390 let mut implied_not_null = false;
16391 let mut user_type_ref: Option<String> = None;
16392 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16393 // value list, captured here and bubbled up through the
16394 // ColumnDef so the engine can attach it to the column
16395 // schema (and validate INSERT cells against it).
16396 let mut inline_enum_variants: Option<Vec<String>> = None;
16397 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16398 let mut inline_set_variants: Option<Vec<String>> = None;
16399 // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16400 // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16401 // collapses to SmallInt / Int. Only under the MySQL dialect.
16402 let mut mysql_int_width: Option<MysqlIntWidth> = None;
16403 // v7.39 (round 424) — the declared fractional-seconds precision of a
16404 // MySQL temporal column. Set by the temporal arms below; stays None
16405 // for PG (whose temporal columns keep full microseconds).
16406 let mut mysql_fsp: Option<u8> = None;
16407 let mut ty = match ty_ident.as_str() {
16408 // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16409 "smallserial" | "serial2" => {
16410 implied_auto_increment = true;
16411 implied_not_null = true;
16412 ColumnTypeName::SmallInt
16413 }
16414 "serial" | "serial4" => {
16415 implied_auto_increment = true;
16416 implied_not_null = true;
16417 ColumnTypeName::Int
16418 }
16419 "bigserial" | "serial8" => {
16420 implied_auto_increment = true;
16421 implied_not_null = true;
16422 ColumnTypeName::BigInt
16423 }
16424 // MySQL flavours we accept by aliasing to the closest SPG
16425 // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16426 // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16427 // 24-bit) → INT. UNSIGNED modifiers are consumed below
16428 // without semantic effect.
16429 // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16430 // PG's internal type names; pg_dump and hand-written PG schemas
16431 // use them interchangeably with smallint / int / bigint (the cast
16432 // path already accepted them, only the column grammar didn't).
16433 "smallint" | "int2" => {
16434 // v7.14.0 — MySQL display-width on integers
16435 // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16436 // parenthesised number is purely cosmetic — it
16437 // doesn't change storage. Accept + discard.
16438 self.consume_optional_paren_size();
16439 ColumnTypeName::SmallInt
16440 }
16441 // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16442 // canonical encoding for BOOLEAN. Every MySQL driver
16443 // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16444 // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16445 // 4.3 SPG classified TINYINT(1) as SmallInt, which
16446 // gave the customer i16-shaped values where the app
16447 // expected bool — a Tier-A silent type drift on
16448 // mysqldump restores. Now: `TINYINT(1)` → Bool;
16449 // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16450 // stay SmallInt (the legacy width-agnostic path).
16451 "tinyint" => {
16452 let width = self.peek_optional_paren_size_value();
16453 self.consume_optional_paren_size();
16454 if width == Some(1) {
16455 ColumnTypeName::Bool
16456 } else {
16457 // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16458 // lost width so the write path can enforce -128..127.
16459 if self.mysql_dialect {
16460 mysql_int_width = Some(MysqlIntWidth::Tiny);
16461 }
16462 ColumnTypeName::SmallInt
16463 }
16464 }
16465 "mediumint" => {
16466 self.consume_optional_paren_size();
16467 // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16468 if self.mysql_dialect {
16469 mysql_int_width = Some(MysqlIntWidth::Medium);
16470 }
16471 ColumnTypeName::Int
16472 }
16473 "int" | "integer" | "int4" => {
16474 self.consume_optional_paren_size();
16475 ColumnTypeName::Int
16476 }
16477 "bigint" | "int8" => {
16478 self.consume_optional_paren_size();
16479 ColumnTypeName::BigInt
16480 }
16481 // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16482 // (mailrs round-5 G6). Consume the optional `PRECISION`
16483 // tail when the type keyword was `double` / `DOUBLE`.
16484 //
16485 // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16486 // FLOAT". `FLOAT(p)` picks the width the way PG does:
16487 // p in 1..=24 is real, 25..=53 is double precision, and
16488 // anything else is an error.
16489 "float" | "double" | "real" => {
16490 if ty_ident.eq_ignore_ascii_case("double")
16491 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16492 {
16493 self.advance();
16494 }
16495 if ty_ident.eq_ignore_ascii_case("real") {
16496 // v7.39 (round 274) — the two dialects genuinely
16497 // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16498 // synonym for DOUBLE (8-byte). Round 269 made REAL
16499 // 32-bit globally and thereby narrowed the stored
16500 // precision of every MySQL REAL column.
16501 if self.mysql_dialect {
16502 ColumnTypeName::Float
16503 } else {
16504 ColumnTypeName::Real
16505 }
16506 } else if ty_ident.eq_ignore_ascii_case("float")
16507 && self.mysql_dialect
16508 && matches!(self.peek(), Token::LParen)
16509 && self.peek_paren_has_comma()
16510 {
16511 // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16512 // display form (`FLOAT(10,2)`), which PG has no
16513 // equivalent of. It was `syntax error at or near ","`,
16514 // so the whole CREATE failed. The digits are a display
16515 // hint only; SPG stores the full double.
16516 self.consume_optional_paren_size();
16517 ColumnTypeName::Float
16518 } else if ty_ident.eq_ignore_ascii_case("float")
16519 && matches!(self.peek(), Token::LParen)
16520 {
16521 // PG words the two bounds differently, and
16522 // parse_paren_size already rejects a zero.
16523 let p = self.parse_paren_size("FLOAT")?;
16524 if p > 53 {
16525 return Err(self.err(String::from(
16526 "precision for type float must be less than 54 bits",
16527 )));
16528 }
16529 if p <= 24 {
16530 ColumnTypeName::Real
16531 } else {
16532 ColumnTypeName::Float
16533 }
16534 } else {
16535 ColumnTypeName::Float
16536 }
16537 }
16538 // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16539 "float4" => ColumnTypeName::Real,
16540 "float8" => ColumnTypeName::Float,
16541 "text" => ColumnTypeName::Text,
16542 // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16543 // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16544 // real MySQL schema and NONE of them existed: the CREATE
16545 // failed outright with `type "blob" does not exist`, so the
16546 // table was never made. The sizes differ only in MySQL's
16547 // maximum length, which SPG does not cap, so they collapse
16548 // onto TEXT and BYTEA the way the unsized spellings do.
16549 "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16550 "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16551 // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16552 // enforce, consumed so the declaration parses.
16553 "varbinary" | "binary" => {
16554 self.consume_optional_paren_size();
16555 ColumnTypeName::Bytes
16556 }
16557 "name" => ColumnTypeName::Name,
16558 "xid" => ColumnTypeName::Xid,
16559 "oid" => ColumnTypeName::Oid,
16560 "xid8" => ColumnTypeName::Xid8,
16561 "bool" | "boolean" => ColumnTypeName::Bool,
16562 // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16563 // an unbounded `character varying`, which the arm below has always
16564 // read as text. Only the short spelling demanded a length, so
16565 // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16566 // there is — failed on `VARCHAR type requires (N)` while the long
16567 // spelling of the same thing was accepted. The same asymmetry
16568 // round 613 closed on the CAST side, here on the DDL side.
16569 "varchar" => {
16570 if matches!(self.peek(), Token::LParen) {
16571 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16572 } else {
16573 ColumnTypeName::Text
16574 }
16575 }
16576 // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16577 // `character` below (SQL standard).
16578 "char" => {
16579 if matches!(self.peek(), Token::LParen) {
16580 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16581 } else {
16582 ColumnTypeName::Char(1)
16583 }
16584 }
16585 // pg_dump's canonical spellings: `character varying(n)` = varchar,
16586 // `character(n)` = char, bare `character` = char(1). Unbounded
16587 // `character varying` maps to text.
16588 "character" => {
16589 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16590 self.advance();
16591 if matches!(self.peek(), Token::LParen) {
16592 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16593 } else {
16594 ColumnTypeName::Text
16595 }
16596 } else if matches!(self.peek(), Token::LParen) {
16597 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16598 } else {
16599 ColumnTypeName::Char(1)
16600 }
16601 }
16602 "vector" => {
16603 let dim = self.parse_paren_size("VECTOR")?;
16604 let encoding = self.parse_optional_vector_encoding()?;
16605 ColumnTypeName::Vector { dim, encoding }
16606 }
16607 // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16608 // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16609 // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16610 // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16611 // DECIMAL(10,2))` — how nearly every money column is written,
16612 // in either dialect — was a syntax error and the table was
16613 // never created. `FIXED` is MySQL's alias alone, so it is
16614 // taken only in that dialect.
16615 "numeric" | "decimal" | "dec" => {
16616 let (precision, scale) = self.parse_optional_numeric_params()?;
16617 ColumnTypeName::Numeric(precision, scale)
16618 }
16619 "fixed" if self.mysql_dialect => {
16620 let (precision, scale) = self.parse_optional_numeric_params()?;
16621 ColumnTypeName::Numeric(precision, scale)
16622 }
16623 "date" => ColumnTypeName::Date,
16624 // MySQL's `DATETIME` is the same domain as standard
16625 // `TIMESTAMP` — accept both spellings.
16626 "timestamp" | "datetime" => {
16627 // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16628 // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16629 // TIME ZONE` clause, so consume it first.
16630 // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16631 // (it truncates on write and pads on render), so capture it;
16632 // a bare spelling means precision 0 there. PG stores µs always
16633 // and keeps `None`.
16634 let n = self.take_optional_paren_size();
16635 if self.mysql_dialect {
16636 mysql_fsp = Some(n.unwrap_or(0).min(6));
16637 }
16638 // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16639 // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16640 // the full form. SPG canonicalises:
16641 // - WITH TIME ZONE → Timestamptz
16642 // - WITHOUT TIME ZONE → Timestamp
16643 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16644 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16645 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16646 {
16647 self.advance(); // WITH
16648 self.advance(); // TIME
16649 self.advance(); // ZONE
16650 ColumnTypeName::Timestamptz
16651 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16652 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16653 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16654 {
16655 self.advance(); // WITHOUT
16656 self.advance(); // TIME
16657 self.advance(); // ZONE
16658 ColumnTypeName::Timestamp
16659 } else {
16660 // A second `(precision)` cannot legally follow, but the
16661 // old grammar tolerated it; keep that tolerance.
16662 self.consume_optional_paren_size();
16663 ColumnTypeName::Timestamp
16664 }
16665 }
16666 // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16667 // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16668 // only PG-wire OID differs.
16669 "timestamptz" => {
16670 self.consume_optional_paren_size();
16671 ColumnTypeName::Timestamptz
16672 }
16673 // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16674 // validation. We accept the JSONB spelling too because
16675 // most PG clients default to it; SPG doesn't distinguish
16676 // the two (no path-operator perf advantage to model).
16677 "json" => ColumnTypeName::Json,
16678 "jsonb" => ColumnTypeName::Jsonb,
16679 // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16680 // surface here. Same storage shape; mapping happens at
16681 // the engine side via the ColumnTypeName → DataType
16682 // resolver. Literal forms are handled at coerce_value
16683 // time so the lexer stays untouched.
16684 "bytea" | "bytes" => ColumnTypeName::Bytes,
16685 // v7.17.0 Phase 7 — PG network address types
16686 // v7.17.0 had a Text-backed fallback here for
16687 // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16688 // each to a first-class type; the keywords are
16689 // bound below in the ζ-A block.
16690 // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16691 // The actual `to_tsvector` / `@@` / `ts_rank` surface
16692 // arrives in v7.12.1+; the type itself loads here so
16693 // mailrs's `scripts/init-schema.sql` runs unmodified.
16694 "tsvector" => ColumnTypeName::TsVector,
16695 "tsquery" => ColumnTypeName::TsQuery,
16696 // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
16697 // surface for Django / Rails / Hibernate's default
16698 // PK pattern.
16699 "uuid" => ColumnTypeName::Uuid,
16700 // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
16701 // Storage = three-field {months, days, micros}, catalog
16702 // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
16703 // line `INTERVAL` was parser-rejected at CREATE TABLE.
16704 "interval" => {
16705 // pg_dump emits field-qualified forms like `INTERVAL DAY TO
16706 // SECOND` and an optional `(p)` precision. SPG stores the full
16707 // {months,days,micros}; consume + ignore the qualifier/precision.
16708 while matches!(self.peek(), Token::To)
16709 || matches!(self.peek(), Token::Ident(s) if matches!(
16710 s.to_ascii_lowercase().as_str(),
16711 "year" | "month" | "day" | "hour" | "minute" | "second"
16712 ))
16713 {
16714 self.advance();
16715 }
16716 self.consume_optional_paren_size();
16717 ColumnTypeName::Interval
16718 }
16719 // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
16720 // i64 microseconds since 00:00:00. Wire OID 1083.
16721 // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
16722 "time" => {
16723 // v7.39 (round 424) — MySQL TIME carries a semantic
16724 // fractional-seconds precision, bare meaning 0.
16725 let n = self.take_optional_paren_size();
16726 if self.mysql_dialect {
16727 mysql_fsp = Some(n.unwrap_or(0).min(6));
16728 }
16729 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16730 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16731 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16732 {
16733 self.advance();
16734 self.advance();
16735 self.advance();
16736 ColumnTypeName::TimeTz
16737 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16738 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16739 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16740 {
16741 self.advance();
16742 self.advance();
16743 self.advance();
16744 ColumnTypeName::Time
16745 } else {
16746 ColumnTypeName::Time
16747 }
16748 }
16749 // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
16750 // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
16751 "year" => ColumnTypeName::Year,
16752 // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
16753 // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
16754 "timetz" => ColumnTypeName::TimeTz,
16755 // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
16756 // Wire OID 790.
16757 "money" => ColumnTypeName::Money,
16758 // v7.17.0 Phase 3.P0-38 — PG range types.
16759 "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
16760 "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
16761 "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
16762 "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
16763 "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
16764 "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
16765 // v7.37.5 δ — PG 14+ multirange keywords.
16766 "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
16767 "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
16768 "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
16769 "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
16770 "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
16771 "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
16772 // v7.37.5 ε — PG geometry scalar keywords.
16773 "point" => ColumnTypeName::Point,
16774 "lseg" => ColumnTypeName::Lseg,
16775 "path" => ColumnTypeName::Path,
16776 "box" => ColumnTypeName::PgBox,
16777 "polygon" => ColumnTypeName::Polygon,
16778 "line" => ColumnTypeName::Line,
16779 "circle" => ColumnTypeName::Circle,
16780 // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
16781 "inet" => ColumnTypeName::Inet,
16782 "cidr" => ColumnTypeName::Cidr,
16783 "macaddr" => ColumnTypeName::Macaddr,
16784 "macaddr8" => ColumnTypeName::Macaddr8,
16785 // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
16786 // width in the value, so the optional `(N)` typmod is accepted and
16787 // ignored (the column stores whatever width it's given).
16788 "bit" => {
16789 let varying = matches!(
16790 self.peek(),
16791 Token::Ident(k) if k.eq_ignore_ascii_case("varying")
16792 );
16793 if varying {
16794 self.advance();
16795 }
16796 // v7.39 (round 281) — the length used to be parsed and
16797 // dropped, so `bit(3)` accepted a five-bit string.
16798 let n = if matches!(self.peek(), Token::LParen) {
16799 self.parse_paren_size("BIT")?
16800 } else {
16801 0
16802 };
16803 if varying {
16804 ColumnTypeName::BitVarying(n)
16805 } else {
16806 ColumnTypeName::Bit(n)
16807 }
16808 }
16809 "varbit" => {
16810 let n = if matches!(self.peek(), Token::LParen) {
16811 self.parse_paren_size("VARBIT")?
16812 } else {
16813 0
16814 };
16815 ColumnTypeName::BitVarying(n)
16816 }
16817 "xml" => ColumnTypeName::Xml,
16818 // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
16819 "hstore" => ColumnTypeName::Hstore,
16820 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
16821 // `ENUM('a','b','c')`. Storage is TEXT; the value
16822 // list lands on `inline_enum_variants` for the
16823 // engine to validate INSERT cells against. Empty
16824 // value list is a parse error (matches MySQL).
16825 "enum" => {
16826 // Expect the opening `(`.
16827 if !matches!(self.peek(), Token::LParen) {
16828 return Err(self.err(alloc::format!(
16829 "expected '(' after ENUM, got {:?}",
16830 self.peek()
16831 )));
16832 }
16833 self.advance();
16834 let mut variants: Vec<String> = Vec::new();
16835 loop {
16836 match self.advance() {
16837 Token::String(s) => variants.push(s),
16838 other => {
16839 return Err(self.err(alloc::format!(
16840 "ENUM(...) expects string literal variants, got {other:?}"
16841 )));
16842 }
16843 }
16844 match self.peek() {
16845 Token::Comma => {
16846 self.advance();
16847 continue;
16848 }
16849 Token::RParen => {
16850 self.advance();
16851 break;
16852 }
16853 other => {
16854 return Err(self.err(alloc::format!(
16855 "expected ',' or ')' in ENUM(...), got {other:?}"
16856 )));
16857 }
16858 }
16859 }
16860 if variants.is_empty() {
16861 return Err(self.err("ENUM(...) must declare at least one variant".into()));
16862 }
16863 inline_enum_variants = Some(variants);
16864 // Storage is plain TEXT; the variant list lives on
16865 // the ColumnSchema side.
16866 ColumnTypeName::Text
16867 }
16868 // v7.17.0 Phase 3.P0-37 — MySQL inline SET
16869 // `SET('a','b','c')`. Same parse shape as ENUM;
16870 // semantics differ (subset rather than pick-one).
16871 "set" => {
16872 if !matches!(self.peek(), Token::LParen) {
16873 return Err(self.err(alloc::format!(
16874 "expected '(' after SET, got {:?}",
16875 self.peek()
16876 )));
16877 }
16878 self.advance();
16879 let mut variants: Vec<String> = Vec::new();
16880 loop {
16881 match self.advance() {
16882 Token::String(s) => variants.push(s),
16883 other => {
16884 return Err(self.err(alloc::format!(
16885 "SET(...) expects string literal variants, got {other:?}"
16886 )));
16887 }
16888 }
16889 match self.peek() {
16890 Token::Comma => {
16891 self.advance();
16892 continue;
16893 }
16894 Token::RParen => {
16895 self.advance();
16896 break;
16897 }
16898 other => {
16899 return Err(self.err(alloc::format!(
16900 "expected ',' or ')' in SET(...), got {other:?}"
16901 )));
16902 }
16903 }
16904 }
16905 if variants.is_empty() {
16906 return Err(self.err("SET(...) must declare at least one variant".into()));
16907 }
16908 inline_set_variants = Some(variants);
16909 ColumnTypeName::Text
16910 }
16911 _other => {
16912 // v7.17.0 Phase 1.4 — unknown ident → defer
16913 // resolution to the engine. Stored as Text in
16914 // ColumnTypeName + the original name carried as
16915 // `user_type_ref` so CREATE TABLE can look up
16916 // user-defined enum / domain types.
16917 user_type_ref = Some(ty_ident.clone());
16918 ColumnTypeName::Text
16919 }
16920 };
16921 // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
16922 // right after the type keyword. Pre-4.4 SPG consumed +
16923 // discarded the keyword, leaving a customer column
16924 // declared `id INT UNSIGNED NOT NULL` silently accepting
16925 // negative values — a Tier-A correctness drift where
16926 // application invariants (auto-increment-IDs never
16927 // negative) silently broke on cutover. Now: capture as
16928 // a column flag, persist on the schema, enforce at
16929 // INSERT / UPDATE time.
16930 let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
16931 {
16932 self.advance();
16933 true
16934 } else {
16935 false
16936 };
16937 // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
16938 // `<type> COLLATE <name>` post-fixes on text columns. SPG
16939 // stores text as UTF-8 always so CHARACTER SET is still a
16940 // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
16941 // name: it gets classified into a `Collation` variant the
16942 // engine consults at WHERE-eval time. PG `default` /
16943 // `pg_catalog.default` / `C` / `POSIX` collations all
16944 // resolve to `Binary` (the prior behaviour); `_ci` /
16945 // `case_insensitive` / `nocase` shift to CaseInsensitive.
16946 // The schema-qualifier form (`pg_catalog.default`) lexes
16947 // as `Ident '.' Ident` — peek for the `.` and consume both
16948 // halves so it's treated as one collation name. PG's
16949 // `IDENT.IDENT` collation form (which can appear here) is
16950 // resolved by Collation::from_collation_name on the bare
16951 // identifier after the dot.
16952 let mut collation = Collation::Binary;
16953 // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
16954 // clause was written. The engine needs this to tell an explicit
16955 // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
16956 // clause at all: both resolve to `Collation::Binary`, but under the
16957 // MySQL dialect the latter takes the folding default collation.
16958 let mut collation_explicit = false;
16959 let mut collation_name: Option<alloc::string::String> = None;
16960 loop {
16961 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
16962 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
16963 {
16964 self.advance(); // CHARACTER
16965 self.advance(); // SET
16966 if matches!(
16967 self.peek(),
16968 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
16969 ) {
16970 self.advance();
16971 }
16972 continue;
16973 }
16974 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
16975 self.advance(); // COLLATE
16976 // Accept Ident / QuotedIdent / String AND the
16977 // keyword-tokenised `Default` (PG `pg_catalog.default`
16978 // and bare `DEFAULT` collation names — `default` is a
16979 // reserved word so the lexer hands back Token::Default
16980 // not Token::Ident).
16981 let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
16982 match this.peek().clone() {
16983 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
16984 this.advance();
16985 Some(s)
16986 }
16987 Token::Default => {
16988 this.advance();
16989 Some(alloc::string::String::from("default"))
16990 }
16991 _ => None,
16992 }
16993 };
16994 let raw = if let Some(head) = read_collation_atom(self) {
16995 // Schema-qualified PG form: `pg_catalog.default`.
16996 if matches!(self.peek(), Token::Dot) {
16997 self.advance();
16998 let tail = read_collation_atom(self).unwrap_or_default();
16999 alloc::format!("{head}.{tail}")
17000 } else {
17001 head
17002 }
17003 } else {
17004 alloc::string::String::new()
17005 };
17006 if !raw.is_empty() {
17007 collation_explicit = true;
17008 // v7.39 (round 676) — keep the name too. The enum below
17009 // folds C / POSIX / en_US / default into one value, and
17010 // `pg_attribute.attcollation` has to tell them apart.
17011 // The schema qualifier goes: PG's `pg_catalog.default`
17012 // and a bare `default` name the same collation.
17013 // v7.39 (round 679) — strip a SCHEMA qualifier, not an
17014 // encoding suffix. Round 676 used `rsplit('.')` for
17015 // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
17016 // PG writes `pg_catalog.default` (qualifier) and
17017 // `en_US.utf8` (locale + encoding) with the same
17018 // separator. Only `pg_catalog.` is a qualifier, and it
17019 // is the only one PG's own dumps emit.
17020 let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
17021 let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
17022 collation_name = Some(alloc::string::String::from(bare));
17023 let parsed = Collation::from_collation_name(&raw);
17024 // Last COLLATE clause wins, but `Binary` from a
17025 // bare keyword like `default` should not
17026 // silently downgrade a stronger one set earlier
17027 // on the same column. v7.17 only ships one
17028 // non-Binary variant so a simple OR is enough.
17029 if parsed != Collation::Binary {
17030 collation = parsed;
17031 }
17032 }
17033 continue;
17034 }
17035 break;
17036 }
17037 // v7.10.10 — postfix `[]` widens the base type to its array
17038 // type. PG accepts `TYPE[]` after any base type and so does
17039 // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
17040 // all through; the old "only TEXT[]" note was stale).
17041 if matches!(self.peek(), Token::LBracket) {
17042 self.advance();
17043 if !matches!(self.peek(), Token::RBracket) {
17044 return Err(self.err(alloc::format!(
17045 "TEXT[] takes no dimension; got {:?}",
17046 self.peek()
17047 )));
17048 }
17049 self.advance();
17050 // v7.11.13 — widened to INT[] and BIGINT[] in addition
17051 // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
17052 // still error here.
17053 ty = match ty {
17054 ColumnTypeName::Text => ColumnTypeName::TextArray,
17055 ColumnTypeName::Int => ColumnTypeName::IntArray,
17056 ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
17057 // v7.37.5 β-P4 — INTERVAL[] via the same postfix
17058 // `[]` grammar. Wire OID 1187.
17059 ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
17060 // v7.37.5 γ — full PG array-of-scalar family.
17061 ColumnTypeName::Bool => ColumnTypeName::BoolArray,
17062 ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
17063 ColumnTypeName::Float => ColumnTypeName::FloatArray,
17064 // NUMERIC(p, s) loses its precision params at the
17065 // array level (matches PG: `NUMERIC[]` is untyped,
17066 // per-element precision flows through values).
17067 ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
17068 ColumnTypeName::Date => ColumnTypeName::DateArray,
17069 ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
17070 ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
17071 ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
17072 ColumnTypeName::Json => ColumnTypeName::JsonArray,
17073 ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
17074 ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
17075 // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
17076 // the array level (matches PG semantics where the
17077 // element precision is per-row, not column-wide).
17078 ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
17079 ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
17080 // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
17081 // follow-up.
17082 ColumnTypeName::Money => ColumnTypeName::MoneyArray,
17083 other => {
17084 return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
17085 }
17086 };
17087 // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
17088 // for INT/TEXT/BIGINT. Anything else is an error.
17089 if matches!(self.peek(), Token::LBracket) {
17090 self.advance();
17091 if !matches!(self.peek(), Token::RBracket) {
17092 return Err(self.err(alloc::format!(
17093 "TYPE[][] second dimension takes no size; got {:?}",
17094 self.peek()
17095 )));
17096 }
17097 self.advance();
17098 ty = match ty {
17099 ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
17100 ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
17101 ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
17102 // v7.39 (read01 round 75) — bool[][].
17103 ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
17104 other => {
17105 return Err(self.err(alloc::format!(
17106 "v7.17 2D arrays support INT[][] / BIGINT[][] / \
17107 TEXT[][] only; got {other:?}"
17108 )));
17109 }
17110 };
17111 }
17112 }
17113 Ok((
17114 ty,
17115 implied_auto_increment,
17116 implied_not_null,
17117 user_type_ref,
17118 collation,
17119 collation_explicit,
17120 collation_name,
17121 is_unsigned,
17122 inline_enum_variants,
17123 inline_set_variants,
17124 mysql_int_width,
17125 mysql_fsp,
17126 ))
17127 }
17128
17129 fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
17130 // v7.20 — PG reserves the table-constraint keywords, so a
17131 // BARE `UNIQUE` / `PRIMARY` / … in column position is a
17132 // malformed constraint clause (e.g. `UNIQUE a` missing its
17133 // parens), not a column named "unique". Since v7.17's
17134 // unknown-type leniency (`user_type_ref`) such a clause
17135 // would otherwise parse as a column with a user-defined
17136 // type — silently accepting invalid DDL. Quoted
17137 // identifiers ("unique" / `unique`) remain valid names.
17138 if let Token::Ident(s) = self.peek()
17139 && [
17140 "unique",
17141 "primary",
17142 "foreign",
17143 "constraint",
17144 "check",
17145 "references",
17146 "exclude",
17147 ]
17148 .iter()
17149 .any(|kw| s.eq_ignore_ascii_case(kw))
17150 {
17151 return Err(self.err(alloc::format!(
17152 "unexpected reserved keyword '{s}' at start of column definition \
17153 (malformed table constraint?)"
17154 )));
17155 }
17156 let name = self.expect_ident_like()?;
17157 let (
17158 ty,
17159 implied_auto_increment,
17160 implied_not_null,
17161 user_type_ref,
17162 collation,
17163 collation_explicit,
17164 collation_name,
17165 is_unsigned,
17166 inline_enum_variants,
17167 inline_set_variants,
17168 mysql_int_width,
17169 mysql_fsp,
17170 ) = self.parse_type_with_implied_flags()?;
17171 // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
17172 // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
17173 // each at most once.
17174 let mut default: Option<Expr> = None;
17175 let mut nullable = !implied_not_null;
17176 let mut nullability_seen = implied_not_null;
17177 let mut auto_increment = implied_auto_increment;
17178 let mut is_primary_key = false;
17179 let mut is_unique = false;
17180 let mut unique_nulls_not_distinct = false;
17181 let mut constraint_deferrable = false;
17182 let mut constraint_initially_deferred = false;
17183 let mut check: Option<Expr> = None;
17184 let mut on_update_runtime: Option<Expr> = None;
17185 let mut generated_stored_expr: Option<Box<Expr>> = None;
17186 let mut identity_always = false;
17187 loop {
17188 // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
17189 // not-null constraints by name and pg_dump emits them
17190 // inline: `id bigint CONSTRAINT contacts_id_not_null1
17191 // NOT NULL`. Accept and discard the name; whatever
17192 // constraint follows is parsed by the arms below.
17193 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
17194 // v7.39 (round 308, V29) — a name on an inline
17195 // REFERENCES belongs to the FOREIGN KEY, and the caller
17196 // (`parse_column_def_with_fk`) is what builds it, so
17197 // leave the whole clause for it. Dropping the name here
17198 // is what made `CONSTRAINT fk_a REFERENCES …` come back
17199 // as the synthesised `c_pid_fkey` — which then could
17200 // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
17201 // `advance()` takes tokens by `mem::replace`, so there
17202 // is no rewinding once consumed.
17203 if matches!(
17204 self.tokens.get(self.pos + 2),
17205 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
17206 ) {
17207 break;
17208 }
17209 self.advance();
17210 let _name = self.expect_ident_like()?;
17211 continue;
17212 }
17213 // v7.39 (round 379) — MySQL's SHORT generated-column form
17214 // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
17215 // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
17216 // below), but hand-written schemas and app migrations use this.
17217 // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
17218 // SPG computes-and-stores either way, like the long form.
17219 if matches!(self.peek(), Token::As) {
17220 self.advance();
17221 if !matches!(self.peek(), Token::LParen) {
17222 return Err(self.err(alloc::format!(
17223 "expected '(' after AS in a generated column, got {:?}",
17224 self.peek()
17225 )));
17226 }
17227 self.advance();
17228 let expr = self.parse_expr(0)?;
17229 if !matches!(self.peek(), Token::RParen) {
17230 return Err(self.err(alloc::format!(
17231 "expected ')' after AS (<expr>), got {:?}",
17232 self.peek()
17233 )));
17234 }
17235 self.advance();
17236 if matches!(self.peek(), Token::Ident(s)
17237 if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
17238 {
17239 self.advance();
17240 }
17241 generated_stored_expr = Some(alloc::boxed::Box::new(expr));
17242 continue;
17243 }
17244 // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
17245 // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
17246 // the modern replacement for SERIAL in hand-written
17247 // schemas). Both flavours map onto the auto-increment
17248 // machinery — SPG's serial semantics ≈ BY DEFAULT;
17249 // ALWAYS's reject-explicit-values nuance is documented
17250 // leniency. Generated EXPRESSION columns
17251 // (`AS (expr) STORED`) are not supported: error loudly
17252 // instead of silently storing NULLs.
17253 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
17254 self.advance();
17255 let mut saw_generated_always = false;
17256 match self.peek().clone() {
17257 Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
17258 self.advance();
17259 saw_generated_always = true;
17260 }
17261 Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
17262 self.advance();
17263 if !matches!(self.peek(), Token::Default) {
17264 return Err(self.err(alloc::format!(
17265 "expected DEFAULT after GENERATED BY, got {:?}",
17266 self.peek()
17267 )));
17268 }
17269 self.advance();
17270 }
17271 other => {
17272 return Err(self.err(alloc::format!(
17273 "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
17274 )));
17275 }
17276 }
17277 if !matches!(self.peek(), Token::As) {
17278 return Err(self.err(alloc::format!(
17279 "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
17280 self.peek()
17281 )));
17282 }
17283 self.advance();
17284 // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
17285 // ( <expr> ) STORED` stored computed-column. The
17286 // expression is captured for the engine to recompute
17287 // on every INSERT / UPDATE. v7.37.7 accepts the
17288 // STORED keyword only; PG also has VIRTUAL, which
17289 // v7.37.7 carves out (sentori only uses STORED).
17290 if matches!(self.peek(), Token::LParen) {
17291 self.advance();
17292 let expr = self.parse_expr(0)?;
17293 if !matches!(self.peek(), Token::RParen) {
17294 return Err(self.err(alloc::format!(
17295 "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
17296 self.peek()
17297 )));
17298 }
17299 self.advance();
17300 let stored = match self.peek() {
17301 Token::Ident(s) | Token::QuotedIdent(s)
17302 if s.eq_ignore_ascii_case("stored") =>
17303 {
17304 self.advance();
17305 true
17306 }
17307 // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
17308 // generated columns. SPG computes them on write and
17309 // persists like STORED; the two are observably
17310 // identical for query results (the value, recompute
17311 // on base-column change, and NOT NULL enforcement all
17312 // match), so a PG 18 schema/dump using VIRTUAL loads
17313 // and behaves correctly. The compute-on-read storage
17314 // saving is an invisible internal difference.
17315 Token::Ident(s) | Token::QuotedIdent(s)
17316 if s.eq_ignore_ascii_case("virtual") =>
17317 {
17318 self.advance();
17319 false
17320 }
17321 other => {
17322 return Err(self.err(alloc::format!(
17323 "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
17324 got {other:?}"
17325 )));
17326 }
17327 };
17328 let _ = stored; // STORED / VIRTUAL both compute-and-store.
17329 generated_stored_expr = Some(Box::new(expr));
17330 continue;
17331 }
17332 self.expect_keyword_ident("identity")?;
17333 // Optional `(START WITH 1 INCREMENT BY 1 …)` —
17334 // consume the balanced parens and discard (SPG's
17335 // auto-increment is max+1-scan based).
17336 if matches!(self.peek(), Token::LParen) {
17337 let mut depth = 0usize;
17338 loop {
17339 match self.advance() {
17340 Token::LParen => depth += 1,
17341 Token::RParen => {
17342 depth -= 1;
17343 if depth == 0 {
17344 break;
17345 }
17346 }
17347 Token::Eof => {
17348 return Err(self.err(
17349 "unterminated sequence-options parens after IDENTITY".into(),
17350 ));
17351 }
17352 _ => {}
17353 }
17354 }
17355 }
17356 auto_increment = true;
17357 // v7.38 (read01) — remember the ALWAYS flavour so the engine
17358 // can reject explicit non-DEFAULT INSERT values (unless
17359 // OVERRIDING SYSTEM VALUE) the way PG does.
17360 identity_always = saw_generated_always;
17361 // PG identity columns are implicitly NOT NULL.
17362 nullable = false;
17363 continue;
17364 }
17365 // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
17366 // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
17367 // is accepted today. The "ON" token is an Ident
17368 // (not reserved) — peek before consuming.
17369 if matches!(self.peek(), Token::On)
17370 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17371 {
17372 self.advance(); // ON
17373 self.advance(); // update
17374 // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17375 let next = self.peek().clone();
17376 match next {
17377 Token::Ident(s) | Token::QuotedIdent(s)
17378 if s.eq_ignore_ascii_case("current_timestamp") =>
17379 {
17380 self.advance();
17381 // Optional `(N)` precision.
17382 if matches!(self.peek(), Token::LParen) {
17383 self.advance();
17384 if !matches!(self.peek(), Token::Integer(_)) {
17385 return Err(self.err(alloc::format!(
17386 "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17387 self.peek()
17388 )));
17389 }
17390 self.advance();
17391 if !matches!(self.peek(), Token::RParen) {
17392 return Err(self.err(alloc::format!(
17393 "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17394 self.peek()
17395 )));
17396 }
17397 self.advance();
17398 }
17399 on_update_runtime = Some(Expr::FunctionCall {
17400 name: "now".into(),
17401 args: Vec::new(),
17402 });
17403 continue;
17404 }
17405 other => {
17406 return Err(self.err(alloc::format!(
17407 "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17408 )));
17409 }
17410 }
17411 }
17412 if matches!(self.peek(), Token::Default) {
17413 if default.is_some() {
17414 return Err(self.err("DEFAULT specified twice".into()));
17415 }
17416 self.advance();
17417 default = Some(self.parse_expr(0)?);
17418 continue;
17419 }
17420 // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17421 // token with NOT NULL and sits EARLIER in the loop than the
17422 // deferrability arm, so without the lookahead it was reported as
17423 // "NOT NULL specified twice" (or "expected NULL after NOT").
17424 if matches!(self.peek(), Token::Not)
17425 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17426 {
17427 // NOT DEFERRABLE — explicit immediate; nothing to carry.
17428 self.consume_optional_deferrable_clauses()?;
17429 continue;
17430 }
17431 if matches!(self.peek(), Token::Not) {
17432 if nullability_seen {
17433 return Err(self.err("NOT NULL specified twice".into()));
17434 }
17435 self.advance();
17436 if !matches!(self.peek(), Token::Null) {
17437 return Err(self.err(format!(
17438 "expected NULL after NOT in column def, got {:?}",
17439 self.peek()
17440 )));
17441 }
17442 self.advance();
17443 nullable = false;
17444 nullability_seen = true;
17445 continue;
17446 }
17447 // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17448 // "this column is nullable" marker (the default in
17449 // standard SQL anyway). mysqldump emits it routinely
17450 // (`col TYPE NULL DEFAULT NULL` for nullable
17451 // timestamps etc). Accept + no-op.
17452 if matches!(self.peek(), Token::Null) {
17453 if nullability_seen && !nullable {
17454 // v7.39 (round 761, F31 tranche 2 #31) — PG's
17455 // sentence, PG18-measured (the table name is the
17456 // caller's; the column half is exact).
17457 return Err(self.err(alloc::format!(
17458 "conflicting NULL/NOT NULL declarations for column \"{name}\""
17459 )));
17460 }
17461 self.advance();
17462 nullable = true;
17463 nullability_seen = true;
17464 continue;
17465 }
17466 // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17467 // arrives as a bare Ident. Match either, case-insensitive.
17468 if let Token::Ident(s) = self.peek()
17469 && (s.eq_ignore_ascii_case("auto_increment")
17470 || s.eq_ignore_ascii_case("autoincrement"))
17471 {
17472 if auto_increment {
17473 return Err(self.err("AUTO_INCREMENT specified twice".into()));
17474 }
17475 self.advance();
17476 auto_increment = true;
17477 continue;
17478 }
17479 // v7.9.13 — inline `PRIMARY KEY` column constraint
17480 // (mailrs F1). Implies `NOT NULL`. The engine creates
17481 // a BTree index for the PK column at CREATE TABLE time
17482 // so FK parent-side index lookups resolve.
17483 // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17484 // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17485 // spelling was a parse error, so a pg_dump carrying one stopped
17486 // mid-restore. The clauses are consumed by the same helper the FK
17487 // path has used since round 288 and recorded nowhere: SPG enforces
17488 // the constraint IMMEDIATELY either way, which fails earlier than
17489 // PG inside a transaction that violates-then-repairs — a refusal,
17490 // not a wrong answer. True deferral is the open remainder of F08.
17491 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17492 || (matches!(self.peek(), Token::Not)
17493 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
17494 {
17495 // v7.39 (round 711) — CARRIED now (the storing half of
17496 // F08); round 621 only consumed.
17497 let (d, idef) = self.consume_deferrable_clauses_timed()?;
17498 constraint_deferrable |= d;
17499 constraint_initially_deferred |= idef;
17500 continue;
17501 }
17502 if let Token::Ident(s) = self.peek()
17503 && s.eq_ignore_ascii_case("primary")
17504 {
17505 if is_primary_key {
17506 return Err(self.err("PRIMARY KEY specified twice".into()));
17507 }
17508 // Peek-ahead for the required `KEY` token.
17509 let next = self.tokens.get(self.pos + 1);
17510 let next_is_key = matches!(
17511 next,
17512 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17513 );
17514 if !next_is_key {
17515 return Err(self.err(format!(
17516 "expected KEY after PRIMARY in column def, got {:?}",
17517 next
17518 )));
17519 }
17520 self.advance(); // PRIMARY
17521 self.advance(); // KEY
17522 is_primary_key = true;
17523 if nullability_seen && nullable {
17524 return Err(self.err(
17525 "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17526 ));
17527 }
17528 nullable = false;
17529 nullability_seen = true;
17530 continue;
17531 }
17532 // v7.13.0 — inline `UNIQUE` column constraint
17533 // (mailrs round-5 G2). Fold into a single-column
17534 // table-level UNIQUE at CREATE TABLE post-process time.
17535 if let Token::Ident(s) = self.peek()
17536 && s.eq_ignore_ascii_case("unique")
17537 {
17538 if is_unique {
17539 return Err(self.err("UNIQUE specified twice".into()));
17540 }
17541 self.advance();
17542 is_unique = true;
17543 // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17544 // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17545 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17546 let n1 = self.tokens.get(self.pos + 1);
17547 let n2 = self.tokens.get(self.pos + 2);
17548 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17549 self.advance(); // NULLS
17550 self.advance(); // NOT
17551 self.advance(); // DISTINCT
17552 unique_nulls_not_distinct = true;
17553 } else if matches!(n1, Some(Token::Distinct)) {
17554 self.advance(); // NULLS
17555 self.advance(); // DISTINCT
17556 }
17557 }
17558 continue;
17559 }
17560 // v7.13.0 — inline `CHECK (<expr>)` column constraint
17561 // (mailrs round-5 G3). PG semantics: column-level
17562 // CHECK is equivalent to a table-level CHECK. Multiple
17563 // inline CHECKs on the same column AND together.
17564 if let Token::Ident(s) = self.peek()
17565 && s.eq_ignore_ascii_case("check")
17566 {
17567 self.advance();
17568 if !matches!(self.peek(), Token::LParen) {
17569 return Err(self.err(alloc::format!(
17570 "expected '(' after CHECK in column def, got {:?}",
17571 self.peek()
17572 )));
17573 }
17574 self.advance();
17575 let pred = self.parse_expr(0)?;
17576 if !matches!(self.peek(), Token::RParen) {
17577 return Err(self.err(alloc::format!(
17578 "expected ')' to close CHECK predicate, got {:?}",
17579 self.peek()
17580 )));
17581 }
17582 self.advance();
17583 check = Some(match check.take() {
17584 Some(prev) => Expr::Binary {
17585 op: BinOp::And,
17586 lhs: Box::new(prev),
17587 rhs: Box::new(pred),
17588 },
17589 None => pred,
17590 });
17591 continue;
17592 }
17593 break;
17594 }
17595 Ok(ColumnDef {
17596 name,
17597 ty,
17598 nullable,
17599 default,
17600 auto_increment,
17601 is_primary_key,
17602 is_unique,
17603 unique_nulls_not_distinct,
17604 constraint_deferrable,
17605 constraint_initially_deferred,
17606 check,
17607 user_type_ref,
17608 on_update_runtime,
17609 collation,
17610 collation_explicit,
17611 collation_name,
17612 is_unsigned,
17613 inline_enum_variants,
17614 inline_set_variants,
17615 generated_stored_expr,
17616 identity_always,
17617 mysql_int_width,
17618 mysql_fsp,
17619 })
17620 }
17621
17622 /// `NUMERIC` may appear without parameters, with one (precision
17623 /// only, scale=0), or with both. Returns `(precision, scale)` with
17624 /// 0 = unspecified for the bare form.
17625 fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17626 if !matches!(self.peek(), Token::LParen) {
17627 // Bare `NUMERIC` — PG treats this as "unlimited precision";
17628 // we surface it as precision=0 to mean "unconstrained" so
17629 // the engine doesn't need a separate variant.
17630 return Ok((0, 0));
17631 }
17632 self.advance();
17633 // v7.39 (round 272) — PG's declared precision runs to 1000, and
17634 // it words the out-of-range case with the value it saw. SPG
17635 // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17636 // accepts failed to parse at all; values wider than i128 are
17637 // carried by the arbitrary-precision form.
17638 let precision = match self.advance() {
17639 Token::Integer(n) if (1..=1000).contains(&n) => {
17640 u16::try_from(n).expect("range-checked")
17641 }
17642 Token::Integer(n) => {
17643 return Err(ParseError {
17644 message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17645 token_pos: self.consumed_pos(),
17646 });
17647 }
17648 other => {
17649 return Err(ParseError {
17650 message: format!(
17651 "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17652 ),
17653 token_pos: self.consumed_pos(),
17654 });
17655 }
17656 };
17657 // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17658 // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17659 // then overflows). A negative scale rounds to tens / hundreds / …
17660 let scale = if matches!(self.peek(), Token::Comma) {
17661 self.advance();
17662 let neg = if matches!(self.peek(), Token::Minus) {
17663 self.advance();
17664 true
17665 } else {
17666 false
17667 };
17668 match self.advance() {
17669 Token::Integer(n) => {
17670 let signed = if neg { -n } else { n };
17671 if !(-1000..=1000).contains(&signed) {
17672 return Err(ParseError {
17673 message: format!(
17674 "NUMERIC scale {signed} must be between -1000 and 1000"
17675 ),
17676 token_pos: self.consumed_pos(),
17677 });
17678 }
17679 i16::try_from(signed).expect("range-checked")
17680 }
17681 other => {
17682 return Err(ParseError {
17683 message: format!("NUMERIC scale must be an integer, got {other:?}"),
17684 token_pos: self.consumed_pos(),
17685 });
17686 }
17687 }
17688 } else {
17689 0
17690 };
17691 if !matches!(self.peek(), Token::RParen) {
17692 return Err(self.err(format!(
17693 "expected ')' to close NUMERIC params, got {:?}",
17694 self.peek()
17695 )));
17696 }
17697 self.advance();
17698 Ok((precision, scale))
17699 }
17700
17701 /// Parse `(N)` where `N` is a positive integer literal — used by the
17702 /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
17703 /// for the error message.
17704 /// v6.0.1: parse the optional `USING <encoding>` clause that
17705 /// follows `VECTOR(N)` in a column definition. Missing clause
17706 /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
17707 /// ident → `ParseError` listing the encodings recognised today.
17708 fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
17709 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
17710 return Ok(VecEncoding::F32);
17711 }
17712 // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
17713 // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
17714 // consume the token when the very next token is a known
17715 // vector-encoding keyword (SQ8 / HALF). Otherwise leave
17716 // `USING` for the caller — it's the rewrite-expression form.
17717 let n1 = self.tokens.get(self.pos + 1);
17718 let next_is_encoding = matches!(
17719 n1,
17720 Some(Token::Ident(s))
17721 if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
17722 );
17723 if !next_is_encoding {
17724 return Ok(VecEncoding::F32);
17725 }
17726 self.advance();
17727 let enc_ident = match self.advance() {
17728 Token::Ident(s) => s,
17729 other => {
17730 return Err(self.err(format!(
17731 "expected vector encoding after USING, got {other:?}"
17732 )));
17733 }
17734 };
17735 match enc_ident.to_ascii_lowercase().as_str() {
17736 "sq8" => Ok(VecEncoding::Sq8),
17737 // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
17738 // binary16 per-element storage.
17739 "half" => Ok(VecEncoding::F16),
17740 other => Err(self.err(format!(
17741 "unknown vector encoding {other:?}; supported: SQ8, HALF"
17742 ))),
17743 }
17744 }
17745
17746 /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
17747 /// without consuming it. Returns `Some(N)` when the next
17748 /// tokens are `( <int> )`; None otherwise. Used by the
17749 /// TINYINT classifier to decide whether to map to Bool or
17750 /// SmallInt.
17751 fn peek_optional_paren_size_value(&self) -> Option<i64> {
17752 if !matches!(self.peek(), Token::LParen) {
17753 return None;
17754 }
17755 let next = self.tokens.get(self.pos + 1)?;
17756 let n = match next {
17757 Token::Integer(n) => *n,
17758 _ => return None,
17759 };
17760 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17761 return None;
17762 }
17763 Some(n)
17764 }
17765
17766 /// v7.14.0 — consume an optional MySQL display-width
17767 /// parenthesised number after an integer type, returning
17768 /// nothing. `TINYINT(1)` etc.
17769 /// v7.39 (round 360) — does the parenthesised group ahead contain a
17770 /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
17771 fn peek_paren_has_comma(&self) -> bool {
17772 let mut i = self.pos + 1;
17773 let mut depth = 1usize;
17774 while depth > 0 {
17775 match self.tokens.get(i) {
17776 Some(Token::LParen) => depth += 1,
17777 Some(Token::RParen) => depth -= 1,
17778 Some(Token::Comma) if depth == 1 => return true,
17779 None | Some(Token::Eof) => return false,
17780 _ => {}
17781 }
17782 i += 1;
17783 }
17784 false
17785 }
17786
17787 /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
17788 /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
17789 /// fractional-seconds precision that drives write truncation and render
17790 /// padding, where `consume_optional_paren_size` throws it away.
17791 /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
17792 fn take_optional_paren_size(&mut self) -> Option<u8> {
17793 let Some(Token::Integer(n)) = self
17794 .tokens
17795 .get(self.pos + 1)
17796 .filter(|_| matches!(self.peek(), Token::LParen))
17797 .cloned()
17798 else {
17799 self.consume_optional_paren_size();
17800 return None;
17801 };
17802 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17803 self.consume_optional_paren_size();
17804 return None;
17805 }
17806 self.consume_optional_paren_size();
17807 u8::try_from(n).ok()
17808 }
17809
17810 fn consume_optional_paren_size(&mut self) {
17811 if !matches!(self.peek(), Token::LParen) {
17812 return;
17813 }
17814 self.advance();
17815 // Skip until matching RParen (allow nested or any tokens).
17816 let mut depth = 1usize;
17817 while depth > 0 {
17818 match self.peek() {
17819 Token::LParen => depth += 1,
17820 Token::RParen => depth -= 1,
17821 Token::Eof => return,
17822 _ => {}
17823 }
17824 self.advance();
17825 }
17826 }
17827
17828 fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
17829 if !matches!(self.peek(), Token::LParen) {
17830 return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
17831 }
17832 self.advance();
17833 let n = match self.advance() {
17834 Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
17835 message: format!("{label} size too large: {n}"),
17836 token_pos: self.consumed_pos(),
17837 })?,
17838 other => {
17839 return Err(ParseError {
17840 message: format!("expected positive integer {label} size, got {other:?}"),
17841 token_pos: self.consumed_pos(),
17842 });
17843 }
17844 };
17845 if !matches!(self.peek(), Token::RParen) {
17846 return Err(self.err(format!(
17847 "expected ')' after {label} size, got {:?}",
17848 self.peek()
17849 )));
17850 }
17851 self.advance();
17852 Ok(n)
17853 }
17854
17855 /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
17856 /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
17857 /// key, like MySQL) whose action skips conflicting rows.
17858 /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
17859 /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
17860 /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
17861 /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
17862 /// common bulk-upsert spellings —
17863 /// INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
17864 /// REPLACE INTO t SELECT …
17865 /// — were a parse error / a duplicate-key failure respectively.
17866 ///
17867 /// Precedence: an explicitly written clause beats a statement-level flag.
17868 /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
17869 /// implicit `REPLACE` and `IGNORE` lowerings.
17870 fn parse_insert_conflict_clause(
17871 &mut self,
17872 replace: bool,
17873 ignore: bool,
17874 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17875 if let Some(c) = self.parse_optional_on_duplicate_key()? {
17876 return Ok(Some(c));
17877 }
17878 if let Some(c) = self.parse_optional_on_conflict()? {
17879 return Ok(Some(c));
17880 }
17881 if replace {
17882 // REPLACE INTO = delete-then-insert, which PG spells as
17883 // `ON CONFLICT DO UPDATE SET` over every column; the engine
17884 // reads an empty assignment list as "take the incoming row".
17885 return Ok(Some(crate::ast::OnConflictClause {
17886 target_columns: Vec::new(),
17887 index_where: None,
17888 constraint_name: None,
17889 mysql_lowered: true,
17890 action: crate::ast::OnConflictAction::Update {
17891 assignments: Vec::new(),
17892 where_: None,
17893 },
17894 }));
17895 }
17896 if ignore {
17897 return Ok(Some(Self::insert_ignore_clause()));
17898 }
17899 Ok(None)
17900 }
17901
17902 /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
17903 /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
17904 /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
17905 /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
17906 fn parse_optional_on_duplicate_key(
17907 &mut self,
17908 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17909 if !(matches!(self.peek(), Token::On)
17910 && matches!(self.tokens.get(self.pos + 1),
17911 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
17912 {
17913 return Ok(None);
17914 }
17915 self.advance(); // ON
17916 self.advance(); // DUPLICATE
17917 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
17918 return Err(self.err(format!(
17919 "expected KEY after ON DUPLICATE, got {:?}",
17920 self.peek()
17921 )));
17922 }
17923 self.advance();
17924 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
17925 return Err(self.err(format!(
17926 "expected UPDATE after ON DUPLICATE KEY, got {:?}",
17927 self.peek()
17928 )));
17929 }
17930 self.advance();
17931 let mut assignments: Vec<(String, Expr)> = Vec::new();
17932 loop {
17933 let col = self.expect_ident_like()?;
17934 if !matches!(self.peek(), Token::Eq) {
17935 return Err(self.err(format!(
17936 "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
17937 self.peek()
17938 )));
17939 }
17940 self.advance();
17941 let mut expr = self.parse_expr(0)?;
17942 Self::rewrite_mysql_values_refs(&mut expr);
17943 assignments.push((col, expr));
17944 if matches!(self.peek(), Token::Comma) {
17945 self.advance();
17946 continue;
17947 }
17948 break;
17949 }
17950 Ok(Some(crate::ast::OnConflictClause {
17951 target_columns: Vec::new(),
17952 index_where: None,
17953 constraint_name: None,
17954 mysql_lowered: true,
17955 action: crate::ast::OnConflictAction::Update {
17956 assignments,
17957 where_: None,
17958 },
17959 }))
17960 }
17961
17962 fn insert_ignore_clause() -> crate::ast::OnConflictClause {
17963 crate::ast::OnConflictClause {
17964 target_columns: Vec::new(),
17965 index_where: None,
17966 constraint_name: None,
17967 mysql_lowered: true,
17968 action: crate::ast::OnConflictAction::Nothing,
17969 }
17970 }
17971
17972 fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
17973 debug_assert!(
17974 matches!(self.peek(), Token::Insert)
17975 || (replace
17976 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
17977 );
17978 self.advance();
17979 // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
17980 // would raise a duplicate-key error instead of failing the statement,
17981 // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
17982 // plain ident to the lexer; only the MySQL dialect accepts it here.
17983 let ignore = self.mysql_dialect
17984 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
17985 if ignore {
17986 self.advance();
17987 }
17988 if !matches!(self.peek(), Token::Into) {
17989 return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
17990 }
17991 self.advance();
17992 let table = self.expect_ident_like()?;
17993 // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
17994 // grammar requires the AS keyword here (a bare identifier would be
17995 // ambiguous with a column list). The alias is what the ON CONFLICT
17996 // DO UPDATE expressions refer to the target row by.
17997 let alias = if matches!(self.peek(), Token::As) {
17998 self.advance();
17999 Some(self.expect_ident_like()?)
18000 } else {
18001 None
18002 };
18003 // v7.39 (round 428) — MySQL's SET-form INSERT:
18004 // INSERT INTO t SET a = 1, b = 'x'
18005 // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
18006 // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
18007 // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
18008 // measured). So it lowers to the column list + one VALUES row and
18009 // rejoins the ordinary path, which already handles every one of
18010 // those. PG has no such spelling, hence the dialect gate.
18011 if self.mysql_dialect
18012 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
18013 {
18014 self.advance(); // SET
18015 let mut names = Vec::new();
18016 let mut values = Vec::new();
18017 loop {
18018 names.push(self.expect_ident_like()?);
18019 if !matches!(self.peek(), Token::Eq) {
18020 return Err(self.err(alloc::format!(
18021 "expected '=' in INSERT … SET, got {:?}",
18022 self.peek()
18023 )));
18024 }
18025 self.advance();
18026 // `SET a = DEFAULT` rides the same `__column_default` marker
18027 // the VALUES-row and UPDATE-SET paths use; the INSERT
18028 // executor resolves it against the target column.
18029 if matches!(self.peek(), Token::Default) {
18030 self.advance();
18031 values.push(Expr::FunctionCall {
18032 name: "__column_default".to_string(),
18033 args: Vec::new(),
18034 });
18035 } else {
18036 values.push(self.parse_expr(0)?);
18037 }
18038 if matches!(self.peek(), Token::Comma) {
18039 self.advance();
18040 continue;
18041 }
18042 break;
18043 }
18044 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18045 let returning = self.parse_optional_returning()?;
18046 return Ok(Statement::Insert(InsertStatement {
18047 ctes: Vec::new(),
18048 table,
18049 alias,
18050 columns: Some(names),
18051 rows: alloc::vec![values],
18052 select_source: None,
18053 // MySQL's SET form has no `OVERRIDING …` clause (that is
18054 // PG's identity-column spelling).
18055 overriding: Overriding::None,
18056 mysql_ignore: ignore,
18057 on_conflict,
18058 returning,
18059 }));
18060 }
18061 // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
18062 // v7.39 (round 151) — a SELECT or WITH right after the paren is
18063 // a parenthesized query source instead (PG select_with_parens:
18064 // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
18065 // both keywords are reserved in PG, so no column list can start
18066 // with them.
18067 let columns = if matches!(self.peek(), Token::LParen) {
18068 self.advance();
18069 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18070 let select_stmt = if self.peek_is_with_kw() {
18071 self.advance();
18072 self.parse_nested_with_select()?
18073 } else {
18074 match self.parse_select_stmt()? {
18075 Statement::Select(s) => s,
18076 other => {
18077 return Err(self.err(alloc::format!(
18078 "expected SELECT in parenthesized INSERT source, got {other:?}"
18079 )));
18080 }
18081 }
18082 };
18083 if !matches!(self.peek(), Token::RParen) {
18084 return Err(self.err(format!(
18085 "expected ')' after parenthesized INSERT source, got {:?}",
18086 self.peek()
18087 )));
18088 }
18089 self.advance();
18090 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18091 let returning = self.parse_optional_returning()?;
18092 return Ok(Statement::Insert(InsertStatement {
18093 ctes: Vec::new(),
18094 table,
18095 alias: alias.clone(),
18096 columns: None,
18097 rows: Vec::new(),
18098 select_source: Some(Box::new(select_stmt)),
18099 on_conflict,
18100 returning,
18101 overriding: Overriding::None,
18102 mysql_ignore: ignore,
18103 }));
18104 }
18105 let mut names = Vec::new();
18106 loop {
18107 names.push(self.expect_ident_like()?);
18108 match self.peek() {
18109 Token::Comma => {
18110 self.advance();
18111 }
18112 Token::RParen => {
18113 self.advance();
18114 break;
18115 }
18116 other => {
18117 return Err(self.err(format!(
18118 "expected ',' or ')' in INSERT column list, got {other:?}"
18119 )));
18120 }
18121 }
18122 }
18123 Some(names)
18124 } else {
18125 None
18126 };
18127 // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
18128 // OVERRIDING SYSTEM VALUE for its identity columns. The clause
18129 // is captured on the statement so the engine can apply PG's
18130 // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
18131 let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
18132 {
18133 self.advance();
18134 let which = self.expect_ident_like()?;
18135 let ov = if which.eq_ignore_ascii_case("system") {
18136 Overriding::System
18137 } else if which.eq_ignore_ascii_case("user") {
18138 Overriding::User
18139 } else {
18140 return Err(self.err(format!(
18141 "expected SYSTEM or USER after OVERRIDING, got {which:?}"
18142 )));
18143 };
18144 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
18145 return Err(self.err(format!(
18146 "expected VALUE after OVERRIDING {}, got {:?}",
18147 which.to_ascii_uppercase(),
18148 self.peek()
18149 )));
18150 }
18151 self.advance();
18152 ov
18153 } else {
18154 Overriding::None
18155 };
18156 // `INSERT INTO t DEFAULT VALUES` — a single row made
18157 // entirely of column defaults. Lower to the permuted
18158 // column-list path with an empty list: every schema column
18159 // is unmapped, so the engine fills each from its default
18160 // (serials advance, plain defaults evaluate, the rest NULL).
18161 if matches!(self.peek(), Token::Default) {
18162 self.advance();
18163 if !matches!(self.peek(), Token::Values) {
18164 return Err(self.err(format!(
18165 "expected VALUES after DEFAULT in INSERT, got {:?}",
18166 self.peek()
18167 )));
18168 }
18169 self.advance();
18170 if columns.is_some() {
18171 return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
18172 }
18173 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18174 let returning = self.parse_optional_returning()?;
18175 return Ok(Statement::Insert(InsertStatement {
18176 ctes: Vec::new(),
18177 table,
18178 alias: alias.clone(),
18179 columns: Some(Vec::new()),
18180 rows: alloc::vec![Vec::new()],
18181 select_source: None,
18182 on_conflict,
18183 returning,
18184 overriding,
18185 mysql_ignore: ignore,
18186 }));
18187 }
18188 // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
18189 // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
18190 // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
18191 // SELECT …`) heads the SOURCE select, as in PG (the statement's
18192 // own WITH comes before INSERT).
18193 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18194 let select_stmt = if self.peek_is_with_kw() {
18195 self.advance();
18196 self.parse_nested_with_select()?
18197 } else {
18198 match self.parse_select_stmt()? {
18199 Statement::Select(s) => s,
18200 other => {
18201 return Err(self.err(alloc::format!(
18202 "expected SELECT after INSERT INTO ... target, got {other:?}"
18203 )));
18204 }
18205 }
18206 };
18207 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18208 let returning = self.parse_optional_returning()?;
18209 return Ok(Statement::Insert(InsertStatement {
18210 ctes: Vec::new(),
18211 table,
18212 alias: alias.clone(),
18213 columns,
18214 rows: Vec::new(),
18215 select_source: Some(Box::new(select_stmt)),
18216 on_conflict,
18217 returning,
18218 overriding,
18219 mysql_ignore: ignore,
18220 }));
18221 }
18222 if !matches!(self.peek(), Token::Values) {
18223 return Err(self.err(format!(
18224 "expected VALUES or SELECT after table name, got {:?}",
18225 self.peek()
18226 )));
18227 }
18228 self.advance();
18229 if !matches!(self.peek(), Token::LParen) {
18230 return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
18231 }
18232 let mut rows = Vec::new();
18233 loop {
18234 // Each iteration consumes one `(expr, expr, …)` tuple.
18235 if !matches!(self.peek(), Token::LParen) {
18236 return Err(self.err(format!(
18237 "expected '(' for next VALUES tuple, got {:?}",
18238 self.peek()
18239 )));
18240 }
18241 self.advance();
18242 let mut tuple = Vec::new();
18243 loop {
18244 // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
18245 // the column's declared default for that slot. Rides out as the
18246 // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
18247 // path uses; the INSERT executor resolves it per target column.
18248 if matches!(self.peek(), Token::Default) {
18249 self.advance();
18250 tuple.push(Expr::FunctionCall {
18251 name: "__column_default".to_string(),
18252 args: Vec::new(),
18253 });
18254 } else {
18255 tuple.push(self.parse_expr(0)?);
18256 }
18257 match self.peek() {
18258 Token::Comma => {
18259 self.advance();
18260 }
18261 Token::RParen => {
18262 self.advance();
18263 break;
18264 }
18265 other => {
18266 return Err(self.err(format!(
18267 "expected ',' or ')' in VALUES tuple, got {other:?}"
18268 )));
18269 }
18270 }
18271 }
18272 if tuple.is_empty() {
18273 return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
18274 }
18275 rows.push(tuple);
18276 // Continue with comma-separated tuples.
18277 if matches!(self.peek(), Token::Comma) {
18278 self.advance();
18279 } else {
18280 break;
18281 }
18282 }
18283 // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
18284 // to ON CONFLICT DO UPDATE with an empty conflict target
18285 // (the engine picks the table's first unique index, which
18286 // matches MySQL's any-unique-key behaviour for the common
18287 // single-key case). `VALUES(col)` in the assignments is
18288 // MySQL's spelling of EXCLUDED.col.
18289 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18290 let returning = self.parse_optional_returning()?;
18291 Ok(Statement::Insert(InsertStatement {
18292 ctes: Vec::new(),
18293 table,
18294 alias,
18295 columns,
18296 rows,
18297 select_source: None,
18298 on_conflict,
18299 returning,
18300 overriding,
18301 mysql_ignore: ignore,
18302 }))
18303 }
18304
18305 /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
18306 /// the incoming row's value — exactly PG's EXCLUDED.col.
18307 fn rewrite_mysql_values_refs(e: &mut Expr) {
18308 match e {
18309 Expr::FunctionCall { name, args }
18310 if name.eq_ignore_ascii_case("values")
18311 && args.len() == 1
18312 && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
18313 {
18314 let Expr::Column(c) = &args[0] else {
18315 unreachable!("guarded above");
18316 };
18317 *e = Expr::Column(crate::ast::ColumnName {
18318 qualifier: Some("EXCLUDED".to_string()),
18319 name: c.name.clone(),
18320 });
18321 }
18322 Expr::FunctionCall { args, .. } => {
18323 for a in args {
18324 Self::rewrite_mysql_values_refs(a);
18325 }
18326 }
18327 Expr::Binary { lhs, rhs, .. } => {
18328 Self::rewrite_mysql_values_refs(lhs);
18329 Self::rewrite_mysql_values_refs(rhs);
18330 }
18331 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
18332 Self::rewrite_mysql_values_refs(expr);
18333 }
18334 Expr::Case {
18335 operand,
18336 branches,
18337 else_branch,
18338 } => {
18339 if let Some(op) = operand {
18340 Self::rewrite_mysql_values_refs(op);
18341 }
18342 for (w, t) in branches {
18343 Self::rewrite_mysql_values_refs(w);
18344 Self::rewrite_mysql_values_refs(t);
18345 }
18346 if let Some(el) = else_branch {
18347 Self::rewrite_mysql_values_refs(el);
18348 }
18349 }
18350 _ => {}
18351 }
18352 }
18353
18354 /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
18355 /// clause sitting between the INSERT body and the trailing
18356 /// RETURNING. All keywords come in as bare idents; `ON` is
18357 /// a reserved Token though.
18358 fn parse_optional_on_conflict(
18359 &mut self,
18360 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18361 if !matches!(self.peek(), Token::On) {
18362 return Ok(None);
18363 }
18364 // Peek further: we want exactly "ON CONFLICT ...". If the
18365 // next ident isn't "conflict", let some other parser handle.
18366 let next_is_conflict = matches!(
18367 self.tokens.get(self.pos + 1),
18368 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18369 );
18370 if !next_is_conflict {
18371 return Ok(None);
18372 }
18373 self.advance(); // ON
18374 self.advance(); // CONFLICT
18375 // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18376 // the constraint instead of listing columns (the pg_dump
18377 // form); the engine resolves it.
18378 let mut constraint_name: Option<String> = None;
18379 if matches!(self.peek(), Token::On) {
18380 self.advance(); // ON
18381 match self.advance() {
18382 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18383 }
18384 other => {
18385 return Err(self.err(alloc::format!(
18386 "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18387 )));
18388 }
18389 }
18390 constraint_name = Some(self.expect_ident_like()?);
18391 }
18392 // Optional `(col [, col]*)` target list.
18393 let mut target_columns: Vec<String> = Vec::new();
18394 if matches!(self.peek(), Token::LParen) {
18395 self.advance();
18396 loop {
18397 target_columns.push(self.expect_ident_like()?);
18398 match self.peek() {
18399 Token::Comma => {
18400 self.advance();
18401 }
18402 Token::RParen => {
18403 self.advance();
18404 break;
18405 }
18406 other => {
18407 return Err(self.err(alloc::format!(
18408 "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18409 )));
18410 }
18411 }
18412 }
18413 }
18414 // v7.39 (round 240) — optional index predicate after the target
18415 // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18416 // PARTIAL unique index; SPG's arbiters are full indexes, which
18417 // satisfy any predicate, so it is parsed and carried but not
18418 // consulted (recorded residual: partial-unique-index arbiters).
18419 let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18420 self.advance();
18421 Some(self.parse_expr(0)?)
18422 } else {
18423 None
18424 };
18425 // Required `DO`.
18426 match self.advance() {
18427 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18428 other => {
18429 return Err(self.err(alloc::format!(
18430 "expected DO after ON CONFLICT [(…)], got {other:?}"
18431 )));
18432 }
18433 }
18434 // Action: NOTHING | UPDATE SET …
18435 let action = match self.advance() {
18436 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18437 crate::ast::OnConflictAction::Nothing
18438 }
18439 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18440 self.parse_on_conflict_update_action()?
18441 }
18442 other => {
18443 return Err(self.err(alloc::format!(
18444 "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18445 )));
18446 }
18447 };
18448 Ok(Some(crate::ast::OnConflictClause {
18449 target_columns,
18450 index_where,
18451 constraint_name,
18452 mysql_lowered: false,
18453 action,
18454 }))
18455 }
18456
18457 /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18458 /// `SET col = expr [, …] [WHERE cond]`. Caller already
18459 /// consumed `UPDATE`.
18460 fn parse_on_conflict_update_action(
18461 &mut self,
18462 ) -> Result<crate::ast::OnConflictAction, ParseError> {
18463 // `SET`
18464 match self.advance() {
18465 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18466 other => {
18467 return Err(self.err(alloc::format!(
18468 "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18469 )));
18470 }
18471 }
18472 let mut assignments: Vec<(String, Expr)> = Vec::new();
18473 loop {
18474 let col = self.expect_ident_like()?;
18475 if !matches!(self.peek(), Token::Eq) {
18476 return Err(self.err(alloc::format!(
18477 "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18478 self.peek()
18479 )));
18480 }
18481 self.advance();
18482 let value = self.parse_expr(0)?;
18483 assignments.push((col, value));
18484 if matches!(self.peek(), Token::Comma) {
18485 self.advance();
18486 continue;
18487 }
18488 break;
18489 }
18490 let where_ = if matches!(self.peek(), Token::Where) {
18491 self.advance();
18492 Some(self.parse_expr(0)?)
18493 } else {
18494 None
18495 };
18496 Ok(crate::ast::OnConflictAction::Update {
18497 assignments,
18498 where_,
18499 })
18500 }
18501
18502 fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18503 let mut items = Vec::new();
18504 // v7.39 (round 341, V66) — PG's target list may be EMPTY
18505 // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18506 // answers one zero-column row per row of t, and a bare `SELECT`
18507 // answers a single zero-column row. SPG required at least one
18508 // item, so both were syntax errors. Recognised by the token that
18509 // follows — nothing that can start an expression appears here.
18510 if self.select_list_is_empty_here() {
18511 return Ok(items);
18512 }
18513 loop {
18514 items.push(self.parse_select_item()?);
18515 if matches!(self.peek(), Token::Comma) {
18516 self.advance();
18517 } else {
18518 break;
18519 }
18520 }
18521 Ok(items)
18522 }
18523
18524 /// Is the target list empty at this point — i.e. does the next token
18525 /// end the SELECT's item list rather than start an item?
18526 fn select_list_is_empty_here(&self) -> bool {
18527 match self.peek() {
18528 Token::From
18529 | Token::Where
18530 | Token::Group
18531 | Token::Having
18532 | Token::Order
18533 | Token::Limit
18534 | Token::Offset
18535 | Token::Semicolon
18536 | Token::RParen
18537 | Token::Union
18538 | Token::Except
18539 | Token::Eof => true,
18540 // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18541 // with unreserved keywords, so they arrive as plain idents.
18542 Token::Ident(s) => {
18543 s.eq_ignore_ascii_case("fetch")
18544 || s.eq_ignore_ascii_case("window")
18545 || s.eq_ignore_ascii_case("intersect")
18546 }
18547 _ => false,
18548 }
18549 }
18550
18551 fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18552 if matches!(self.peek(), Token::Star) {
18553 self.advance();
18554 return Ok(SelectItem::Wildcard);
18555 }
18556 // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18557 // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18558 // choke on the `*` ("expected identifier, got Star"). The lookahead is
18559 // `<ident> . *` with nothing binding tighter.
18560 if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18561 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18562 && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18563 {
18564 self.advance(); // qualifier
18565 self.advance(); // .
18566 self.advance(); // *
18567 return Ok(SelectItem::QualifiedWildcard(q));
18568 }
18569 }
18570 let start_tok = self.pos;
18571 let expr = self.parse_expr(0)?;
18572 let end_tok = self.consumed_pos();
18573 // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18574 // multi-column function returns into columns. Marked here and lowered in
18575 // `parse_bare_select`, where the FROM clause is in hand.
18576 if matches!(self.peek(), Token::Dot)
18577 && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18578 {
18579 self.advance(); // .
18580 self.advance(); // *
18581 return Ok(SelectItem::Expr {
18582 expr: Expr::FunctionCall {
18583 name: "__record_expand".to_string(),
18584 args: alloc::vec![expr],
18585 },
18586 alias: None,
18587 });
18588 }
18589 let alias = match self.parse_optional_alias()? {
18590 Some(a) => Some(a),
18591 None => self.mysql_item_label(&expr, start_tok, end_tok),
18592 };
18593 Ok(SelectItem::Expr { expr, alias })
18594 }
18595
18596 /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18597 /// carries no `AS`, filled in here so every downstream path reports it
18598 /// without knowing the rule. `None` leaves the item un-aliased, which is
18599 /// what a PG session always gets.
18600 ///
18601 /// Measured against MariaDB 11, three rules and no more:
18602 ///
18603 /// | item | label | why |
18604 /// |------------------|------------|------------------------------|
18605 /// | `lbl.a` | `a` | a column reports its name |
18606 /// | `'it''s'` | `it's` | a string reports its VALUE |
18607 /// | `a + b` | `a + b` | anything else, source text |
18608 ///
18609 /// The third is why this lives in the parser at all: the label is the
18610 /// text the client WROTE, down to the spacing, so it cannot be printed
18611 /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18612 ///
18613 /// Comments survive, and that is right: through a `mariadb` CLI both
18614 /// servers answer `a + b` for `SELECT a /* c */ + b`, but that is the
18615 /// CLIENT stripping the comment before it sends. Asked over the raw
18616 /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18617 /// produces.
18618 fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18619 if !self.mysql_dialect {
18620 return None;
18621 }
18622 match expr {
18623 // A column already reports its own name downstream; naming it
18624 // again here would only re-state the qualifier the label drops.
18625 Expr::Column(_) => None,
18626 Expr::Literal(Literal::String(v)) => Some(v.clone()),
18627 _ => self.source_span(start_tok, end_tok).map(str::to_string),
18628 }
18629 }
18630
18631 /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18632 /// consumed VALUES keyword. Each row lowers to a constant SELECT
18633 /// with PG's default column1..columnN names; subsequent rows
18634 /// chain as UNION ALL peers. Shared by the FROM-position
18635 /// `( VALUES … )` arm and the top-level bare VALUES statement.
18636 fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
18637 let mut row_selects: Vec<SelectStatement> = Vec::new();
18638 loop {
18639 if !matches!(self.peek(), Token::LParen) {
18640 return Err(self.err(alloc::format!(
18641 "expected '(' to start a VALUES row, got {:?}",
18642 self.peek()
18643 )));
18644 }
18645 self.advance(); // (
18646 let mut items: Vec<SelectItem> = Vec::new();
18647 loop {
18648 let expr = self.parse_expr(0)?;
18649 items.push(SelectItem::Expr {
18650 expr,
18651 alias: Some(alloc::format!("column{}", items.len() + 1)),
18652 });
18653 match self.peek() {
18654 Token::Comma => {
18655 self.advance();
18656 }
18657 Token::RParen => break,
18658 other => {
18659 return Err(self.err(alloc::format!(
18660 "expected ',' or ')' in VALUES row, got {other:?}"
18661 )));
18662 }
18663 }
18664 }
18665 self.advance(); // )
18666 row_selects.push(SelectStatement {
18667 locking: None,
18668 ctes: Vec::new(),
18669 distinct: false,
18670 distinct_on: Vec::new(),
18671 items,
18672 from: None,
18673 where_: None,
18674 group_by: None,
18675 group_by_all: false,
18676 having: None,
18677 unions: Vec::new(),
18678 order_by: Vec::new(),
18679 limit: None,
18680 offset: None,
18681 limit_with_ties: false,
18682 window_check_exprs: Vec::new(),
18683 });
18684 if matches!(self.peek(), Token::Comma) {
18685 self.advance();
18686 continue;
18687 }
18688 break;
18689 }
18690 let mut head = row_selects.remove(0);
18691 head.unions = row_selects
18692 .into_iter()
18693 .map(|s| (UnionKind::All, s))
18694 .collect();
18695 Ok(head)
18696 }
18697
18698 fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
18699 // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
18700 // children. It was read as a table NAMED `only`, so the query
18701 // failed on `relation "only" does not exist`.
18702 //
18703 // v7.39 (round 644) — and it is no longer a no-op. Round 621
18704 // absorbed the keyword, reasoning that SPG's children are
18705 // separate relations a plain scan does not descend into, so ONLY
18706 // already described the scan. That stopped being true when a
18707 // partition parent started unioning its children: measured,
18708 // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
18709 // where PG answers 0. The flag is carried now.
18710 let mut only = false;
18711 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
18712 && matches!(
18713 self.tokens.get(self.pos + 1),
18714 Some(Token::Ident(_) | Token::QuotedIdent(_))
18715 )
18716 {
18717 only = true;
18718 self.advance();
18719 }
18720 // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
18721 // for these SRFs the keyword is noise at parse time: the
18722 // join executor already substitutes outer-column references
18723 // into unnest_expr / generate_series_args per outer row
18724 // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
18725 // licences the correlation even without the keyword. Absorb
18726 // it and fall through to the SRF arms below.
18727 // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
18728 // just the four builtin SRFs: a user set-returning function on a JOIN's
18729 // right side is the whole point of LATERAL. The keyword stays noise at
18730 // parse time — the join executor substitutes the outer row into the
18731 // call's arguments per outer row.
18732 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18733 && matches!(
18734 self.tokens.get(self.pos + 1),
18735 // The json_each family has its OWN `LATERAL …` arm below, which
18736 // needs to see the keyword — absorbing it here would send those
18737 // calls down the generic table-function channel instead.
18738 Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
18739 )
18740 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18741 {
18742 self.advance(); // LATERAL
18743 }
18744 // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
18745 // set-returning function whose argument may reference a
18746 // preceding FROM item. We rewrite this to
18747 // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
18748 // AS __srf__) AS <alias>` so the existing LATERAL subquery
18749 // executor handles per-outer-row evaluation and the
18750 // SRF-primary jsonb_each_text path handles the inner
18751 // materialisation. Sentori 0067 backfill is the dogfood
18752 // shape.
18753 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18754 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
18755 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18756 {
18757 self.advance(); // LATERAL
18758 let each_fn = match self.peek() {
18759 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18760 _ => unreachable!(),
18761 };
18762 self.advance(); // jsonb_each[_text] / json_each[_text]
18763 self.advance(); // (
18764 let arg = self.parse_expr(0)?;
18765 if !matches!(self.peek(), Token::RParen) {
18766 return Err(self.err(alloc::format!(
18767 "expected ')' after LATERAL {each_fn}() argument, got {:?}",
18768 self.peek()
18769 )));
18770 }
18771 self.advance();
18772 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18773 let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18774 // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
18775 // FROM jsonb_each_text(<arg>) AS __srf__
18776 // PG's `AS kv(key, value)` column-alias list maps
18777 // positions to names; default to (key, value) when
18778 // omitted (matching the SRF's natural column names).
18779 let srf_alias = "__srf__".to_string();
18780 let key_alias = column_aliases
18781 .first()
18782 .cloned()
18783 .unwrap_or_else(|| "key".to_string());
18784 let value_alias = column_aliases
18785 .get(1)
18786 .cloned()
18787 .unwrap_or_else(|| "value".to_string());
18788 let inner_select = crate::ast::SelectStatement {
18789 locking: None,
18790 ctes: Vec::new(),
18791 distinct: false,
18792 distinct_on: Vec::new(),
18793 items: alloc::vec![
18794 crate::ast::SelectItem::Expr {
18795 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18796 qualifier: Some(srf_alias.clone()),
18797 name: "key".to_string(),
18798 }),
18799 alias: Some(key_alias),
18800 },
18801 crate::ast::SelectItem::Expr {
18802 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18803 qualifier: Some(srf_alias.clone()),
18804 name: "value".to_string(),
18805 }),
18806 alias: Some(value_alias),
18807 },
18808 ],
18809 from: Some(crate::ast::FromClause {
18810 primary: TableRef {
18811 name: srf_alias.clone(),
18812 alias: Some(srf_alias.clone()),
18813 only: false,
18814 as_of_segment: None,
18815 unnest_expr: None,
18816 unnest_column_aliases: Vec::new(),
18817 with_ordinality: false,
18818 generate_series_args: None,
18819 lateral_subquery: None,
18820 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18821 table_fn_call: None,
18822 rows_from: None,
18823 json_table: None,
18824 scalar_fn_item: false,
18825 },
18826 joins: Vec::new(),
18827 }),
18828 where_: None,
18829 group_by: None,
18830 group_by_all: false,
18831 having: None,
18832 unions: Vec::new(),
18833 order_by: Vec::new(),
18834 limit: None,
18835 offset: None,
18836 limit_with_ties: false,
18837 window_check_exprs: Vec::new(),
18838 };
18839 return Ok(TableRef {
18840 name: alias.clone(),
18841 alias: Some(alias),
18842 only: false,
18843 as_of_segment: None,
18844 unnest_expr: None,
18845 unnest_column_aliases: Vec::new(),
18846 with_ordinality: false,
18847 generate_series_args: None,
18848 lateral_subquery: Some(Box::new(inner_select)),
18849 jsonb_each_text_arg: None,
18850 table_fn_call: None,
18851 rows_from: None,
18852 json_table: None,
18853 scalar_fn_item: false,
18854 });
18855 }
18856 // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
18857 // without an explicit `LATERAL` keyword is the same shape
18858 // PG accepts (SRF naturally licences lateral correlation).
18859 // We mirror the LATERAL rewrite when the argument syntactic-
18860 // ally references an outer column (Column { qualifier:
18861 // Some(_), … }). For simplicity we apply the rewrite
18862 // whenever the SRF directly follows JOIN/CROSS JOIN/comma
18863 // in the FROM-list — caller-side join parsing positions
18864 // this peek correctly.
18865 // (Implementation note: detection lives below; the LATERAL
18866 // branch above already covers the explicit form; the bare
18867 // form falls through to the plain SRF arm and the engine
18868 // treats it as a constant-arg SRF if no outer reference is
18869 // present.)
18870 // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
18871 // table. Detect at the head so it claims precedence over
18872 // every other table-ref shape (unnest / generate_series /
18873 // bare ident); the lateral subquery itself follows the
18874 // regular SELECT grammar.
18875 // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
18876 // t(cols)`. Each row lowers to a constant SELECT with PG's
18877 // default column1..columnN names; subsequent rows chain as
18878 // UNION ALL peers. The result rides the derived-table
18879 // lateral_subquery channel — zero executor work.
18880 if matches!(self.peek(), Token::LParen)
18881 && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
18882 {
18883 self.advance(); // (
18884 self.advance(); // VALUES
18885 let head = self.parse_values_rows_body()?;
18886 if !matches!(self.peek(), Token::RParen) {
18887 return Err(self.err(alloc::format!(
18888 "expected ')' after VALUES list, got {:?}",
18889 self.peek()
18890 )));
18891 }
18892 self.advance();
18893 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18894 let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
18895 return Ok(TableRef {
18896 name,
18897 alias: alias_ident,
18898 only: false,
18899 as_of_segment: None,
18900 unnest_expr: None,
18901 unnest_column_aliases: column_aliases,
18902 with_ordinality: false,
18903 generate_series_args: None,
18904 lateral_subquery: Some(Box::new(head)),
18905 jsonb_each_text_arg: None,
18906 table_fn_call: None,
18907 rows_from: None,
18908 json_table: None,
18909 scalar_fn_item: false,
18910 });
18911 }
18912 // v7.37.17 (17.6 siblings) — plain derived table:
18913 // `FROM ( SELECT … ) [AS] alias`. Rides the same
18914 // lateral_subquery channel the explicit LATERAL form uses —
18915 // an uncorrelated inner SELECT executes identically. The
18916 // inner parse carries UNION tails (they live on
18917 // SelectStatement.unions).
18918 // v7.37 D.20 — the derived-table inner may itself be a
18919 // parenthesized set-operation group (`FROM ((SELECT…) UNION
18920 // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
18921 // bare `(SELECT …)`. parse_one_statement already routes a leading
18922 // `(` set-op group (its LParen arm) and a leading WITH
18923 // (parse_with_cte_then_select), so widen the second-token gate to
18924 // Select | LParen | WITH.
18925 // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
18926 // PG's spelling of `SELECT * FROM t` and is accepted wherever a
18927 // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
18928 // has existed since the shorthand landed and `parse_bare_select`
18929 // already routes it ("valid anywhere a SELECT head is"); what was
18930 // missing is this second-token gate, and the CTE body's dispatch
18931 // below. Round 868 found both by putting the shorthand in a
18932 // subquery — the top-level forms had been the only ones tested.
18933 if matches!(self.peek(), Token::LParen)
18934 && (matches!(
18935 self.tokens.get(self.pos + 1),
18936 Some(Token::Select | Token::LParen | Token::Table)
18937 ) || matches!(self.tokens.get(self.pos + 1),
18938 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
18939 {
18940 self.advance(); // (
18941 let inner = match self.parse_one_statement()? {
18942 Statement::Select(s) => s,
18943 other => {
18944 return Err(self.err(alloc::format!(
18945 "expected SELECT inside derived table ( … ), got {other:?}"
18946 )));
18947 }
18948 };
18949 if !matches!(self.peek(), Token::RParen) {
18950 return Err(self.err(alloc::format!(
18951 "expected ')' after derived-table subquery, got {:?}",
18952 self.peek()
18953 )));
18954 }
18955 self.advance();
18956 // `AS t(a, b)` column-alias list rides the
18957 // unnest_column_aliases field (same positional-rename
18958 // contract the unnest SRFs use).
18959 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18960 let name = alias_ident
18961 .clone()
18962 .unwrap_or_else(|| "subquery".to_string());
18963 return Ok(TableRef {
18964 name,
18965 alias: alias_ident,
18966 only: false,
18967 as_of_segment: None,
18968 unnest_expr: None,
18969 unnest_column_aliases: column_aliases,
18970 with_ordinality: false,
18971 generate_series_args: None,
18972 lateral_subquery: Some(Box::new(inner)),
18973 jsonb_each_text_arg: None,
18974 table_fn_call: None,
18975 rows_from: None,
18976 json_table: None,
18977 scalar_fn_item: false,
18978 });
18979 }
18980 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18981 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18982 {
18983 self.advance(); // LATERAL
18984 self.advance(); // (
18985 // Parse the inner SELECT.
18986 let inner = match self.parse_one_statement()? {
18987 Statement::Select(s) => s,
18988 other => {
18989 return Err(self.err(alloc::format!(
18990 "expected SELECT inside LATERAL ( … ), got {other:?}"
18991 )));
18992 }
18993 };
18994 if !matches!(self.peek(), Token::RParen) {
18995 return Err(self.err(alloc::format!(
18996 "expected ')' after LATERAL subquery, got {:?}",
18997 self.peek()
18998 )));
18999 }
19000 self.advance();
19001 // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
19002 // `(VALUES …) t(g)` derived table round-trips through view-body
19003 // Display, which renders on the lateral_subquery channel).
19004 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19005 let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
19006 return Ok(TableRef {
19007 name,
19008 alias: alias_ident,
19009 only: false,
19010 as_of_segment: None,
19011 unnest_expr: None,
19012 unnest_column_aliases: column_aliases,
19013 with_ordinality: false,
19014 generate_series_args: None,
19015 lateral_subquery: Some(Box::new(inner)),
19016 jsonb_each_text_arg: None,
19017 table_fn_call: None,
19018 rows_from: None,
19019 json_table: None,
19020 scalar_fn_item: false,
19021 });
19022 }
19023 // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
19024 // function as a FROM item. Emits one row per (key, value)
19025 // pair in the JSONB object argument as TEXT columns. May
19026 // be wrapped in CROSS JOIN LATERAL when the argument
19027 // references a preceding FROM item (sentori migration
19028 // 0067 backfill shape: `CROSS JOIN LATERAL
19029 // jsonb_each_text(t.json_col) AS kv(key, value)`).
19030 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
19031 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19032 {
19033 let each_fn = match self.peek() {
19034 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19035 _ => unreachable!(),
19036 };
19037 self.advance(); // jsonb_each[_text] / json_each[_text]
19038 self.advance(); // (
19039 let arg = self.parse_expr(0)?;
19040 if !matches!(self.peek(), Token::RParen) {
19041 return Err(self.err(alloc::format!(
19042 "expected ')' after {each_fn}() argument, got {:?}",
19043 self.peek()
19044 )));
19045 }
19046 self.advance();
19047 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19048 let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19049 return Ok(TableRef {
19050 name,
19051 alias: alias_ident,
19052 only: false,
19053 as_of_segment: None,
19054 unnest_expr: None,
19055 // `AS t(k, v)` renames key/value positionally, same as the
19056 // LATERAL-position form already does.
19057 unnest_column_aliases: column_aliases,
19058 with_ordinality: false,
19059 generate_series_args: None,
19060 lateral_subquery: None,
19061 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19062 table_fn_call: None,
19063 rows_from: None,
19064 json_table: None,
19065 scalar_fn_item: false,
19066 });
19067 }
19068 // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
19069 // (+ json_ variants) — record-returning JSON functions with a
19070 // column-definition list. Desugar to a derived table that
19071 // projects each declared column from the JSON via `->>` + a cast,
19072 // over `jsonb_array_elements(J)` for the *set (per-element) form.
19073 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
19074 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19075 {
19076 return self.parse_json_to_record_from();
19077 }
19078 // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
19079 // row is a text[] of capture groups, so it cannot desugar to unnest
19080 // (that would flatten the array). Wrap it as a derived table
19081 // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
19082 // SRF path already emits one text[] row per match. PG names the column
19083 // `regexp_matches`; an `AS a(col)` alias overrides it.
19084 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19085 if s.eq_ignore_ascii_case("regexp_matches"))
19086 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19087 {
19088 self.advance(); // fn name
19089 self.advance(); // (
19090 let mut fn_args: Vec<Expr> = Vec::new();
19091 loop {
19092 fn_args.push(self.parse_expr(0)?);
19093 if matches!(self.peek(), Token::Comma) {
19094 self.advance();
19095 continue;
19096 }
19097 break;
19098 }
19099 if !matches!(self.peek(), Token::RParen) {
19100 return Err(self.err(alloc::format!(
19101 "expected ')' after regexp_matches() arguments, got {:?}",
19102 self.peek()
19103 )));
19104 }
19105 self.advance();
19106 // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
19107 // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
19108 // it, so it died on the `with` token while every other table function
19109 // accepted it.
19110 let with_ordinality = self.absorb_with_ordinality();
19111 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19112 let table_alias = alias_ident
19113 .clone()
19114 .unwrap_or_else(|| "regexp_matches".to_string());
19115 // PG names a single-column function's output column after the ALIAS
19116 // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
19117 // `m` reads as that column and not as a whole-row composite. Naming
19118 // it after the function regardless made `SELECT m[1] FROM … AS m`
19119 // subscript a record.
19120 let col_name = column_aliases
19121 .first()
19122 .cloned()
19123 .or_else(|| alias_ident.clone())
19124 .unwrap_or_else(|| "regexp_matches".to_string());
19125 let inner = crate::ast::SelectStatement {
19126 locking: None,
19127 ctes: Vec::new(),
19128 distinct: false,
19129 distinct_on: Vec::new(),
19130 items: alloc::vec![SelectItem::Expr {
19131 expr: Expr::FunctionCall {
19132 name: "regexp_matches".to_string(),
19133 args: fn_args,
19134 },
19135 alias: Some(col_name),
19136 }],
19137 from: None,
19138 where_: None,
19139 group_by: None,
19140 group_by_all: false,
19141 having: None,
19142 unions: Vec::new(),
19143 order_by: Vec::new(),
19144 limit: None,
19145 offset: None,
19146 limit_with_ties: false,
19147 window_check_exprs: Vec::new(),
19148 };
19149 return Ok(TableRef {
19150 name: table_alias.clone(),
19151 alias: Some(table_alias),
19152 only: false,
19153 as_of_segment: None,
19154 unnest_expr: None,
19155 unnest_column_aliases: column_aliases,
19156 with_ordinality,
19157 generate_series_args: None,
19158 lateral_subquery: Some(Box::new(inner)),
19159 jsonb_each_text_arg: None,
19160 table_fn_call: None,
19161 rows_from: None,
19162 json_table: None,
19163 // regexp_matches returns text[], a base type: `SELECT m FROM
19164 // regexp_matches(…) AS m` is the array, not a composite wrapping it.
19165 scalar_fn_item: true,
19166 });
19167 }
19168 // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
19169 // / json_ variants as a FROM item. Rewritten into
19170 // `unnest(<same fn>(<expr>))`: the scalar form returns the
19171 // elements as a TEXT array, and the existing unnest SRF path
19172 // materialises one row per element. PG's natural column name
19173 // is `value`; an `AS a(col)` column-alias list overrides it.
19174 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19175 if s.eq_ignore_ascii_case("jsonb_array_elements")
19176 || s.eq_ignore_ascii_case("json_array_elements")
19177 || s.eq_ignore_ascii_case("jsonb_array_elements_text")
19178 || s.eq_ignore_ascii_case("json_array_elements_text")
19179 || s.eq_ignore_ascii_case("jsonb_object_keys")
19180 || s.eq_ignore_ascii_case("json_object_keys")
19181 || s.eq_ignore_ascii_case("jsonb_path_query")
19182 || s.eq_ignore_ascii_case("json_path_query")
19183 || s.eq_ignore_ascii_case("generate_subscripts")
19184 || s.eq_ignore_ascii_case("string_to_table")
19185 || s.eq_ignore_ascii_case("regexp_split_to_table"))
19186 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19187 {
19188 let fn_name = match self.peek() {
19189 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19190 _ => unreachable!(),
19191 };
19192 self.advance(); // fn name
19193 self.advance(); // (
19194 let mut fn_args: Vec<Expr> = Vec::new();
19195 loop {
19196 fn_args.push(self.parse_expr(0)?);
19197 if matches!(self.peek(), Token::Comma) {
19198 self.advance();
19199 continue;
19200 }
19201 break;
19202 }
19203 if !matches!(self.peek(), Token::RParen) {
19204 return Err(self.err(alloc::format!(
19205 "expected ')' after {fn_name}() arguments, got {:?}",
19206 self.peek()
19207 )));
19208 }
19209 self.advance();
19210 let with_ordinality = self.absorb_with_ordinality();
19211 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19212 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
19213 // PG's natural column name: the array-elements SRFs
19214 // declare an OUT parameter `value`; jsonb_object_keys
19215 // and generate_subscripts have none, so the column is
19216 // named after the function. A bare table alias on a
19217 // single-column SRF renames the column too (PG: `FROM
19218 // generate_subscripts(a, 1) AS s` projects column s) —
19219 // except for the OUT-parameter SRFs, whose column stays
19220 // `value` under a bare alias.
19221 let natural_col = if fn_name.ends_with("_array_elements")
19222 || fn_name.ends_with("_array_elements_text")
19223 {
19224 "value".to_string()
19225 } else {
19226 alias_ident.clone().unwrap_or_else(|| fn_name.clone())
19227 };
19228 let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
19229 // Keep any further entries — the second names the
19230 // ordinality column under WITH ORDINALITY.
19231 srf_cols.extend(column_aliases.into_iter().skip(1));
19232 // The *_to_table SRFs are row-streams over the existing
19233 // *_to_array scalars — map the call target; the display
19234 // name (alias / column defaults) keeps the SRF spelling.
19235 let call_name = match fn_name.as_str() {
19236 "string_to_table" => "string_to_array".to_string(),
19237 "regexp_split_to_table" => "regexp_split_to_array".to_string(),
19238 _ => fn_name,
19239 };
19240 // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
19241 // preceding FROM item (bare or qualified column) is correlated;
19242 // route it through the per-outer-row lateral channel.
19243 let expr = crate::ast::Expr::FunctionCall {
19244 name: call_name,
19245 args: fn_args,
19246 };
19247 let correlated = Self::expr_has_any_column(&expr);
19248 let tref = TableRef {
19249 name,
19250 alias: alias_ident,
19251 only: false,
19252 as_of_segment: None,
19253 unnest_expr: Some(Box::new(expr)),
19254 unnest_column_aliases: srf_cols,
19255 with_ordinality,
19256 generate_series_args: None,
19257 lateral_subquery: None,
19258 jsonb_each_text_arg: None,
19259 table_fn_call: None,
19260 rows_from: None,
19261 json_table: None,
19262 // Each of these returns a BASE type (jsonb / text / int), so the item's
19263 // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
19264 // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
19265 scalar_fn_item: !with_ordinality,
19266 };
19267 return Ok(if correlated {
19268 Self::wrap_correlated_srf(tref)
19269 } else {
19270 tref
19271 });
19272 }
19273 // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
19274 // explicit parallel-zip syntax. Each entry lowers to its
19275 // array-returning scalar form (unnest(x) → x itself; the
19276 // FROM-SRF rewrite family → their scalar array calls) and
19277 // the list rides the multi-arg unnest zip channel:
19278 // NULL-padded to the longest, WITH ORDINALITY appends the
19279 // counter. generate_series has no scalar array form and
19280 // errors honestly.
19281 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
19282 && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
19283 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19284 {
19285 self.advance(); // ROWS
19286 self.advance(); // FROM
19287 self.advance(); // (
19288 let mut entries: Vec<Expr> = Vec::new();
19289 // v7.39 (read01 round 74) — the generic channel, filled in parallel.
19290 // Used only when some entry has no array form.
19291 let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
19292 loop {
19293 let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
19294 if !matches!(self.peek(), Token::LParen) {
19295 return Err(self.err(alloc::format!(
19296 "expected '(' after {fn_name} in ROWS FROM, got {:?}",
19297 self.peek()
19298 )));
19299 }
19300 self.advance();
19301 let mut fn_args: Vec<Expr> = Vec::new();
19302 if !matches!(self.peek(), Token::RParen) {
19303 loop {
19304 fn_args.push(self.parse_expr(0)?);
19305 if matches!(self.peek(), Token::Comma) {
19306 self.advance();
19307 continue;
19308 }
19309 break;
19310 }
19311 }
19312 if !matches!(self.peek(), Token::RParen) {
19313 return Err(self.err(alloc::format!(
19314 "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
19315 self.peek()
19316 )));
19317 }
19318 self.advance();
19319 let entry = match fn_name.as_str() {
19320 "unnest" => {
19321 if fn_args.len() != 1 {
19322 return Err(
19323 self.err("unnest inside ROWS FROM takes exactly one array".into())
19324 );
19325 }
19326 fn_args.pop().expect("len checked")
19327 }
19328 "jsonb_array_elements"
19329 | "json_array_elements"
19330 | "jsonb_array_elements_text"
19331 | "json_array_elements_text"
19332 | "jsonb_object_keys"
19333 | "json_object_keys"
19334 | "generate_subscripts" => crate::ast::Expr::FunctionCall {
19335 name: fn_name,
19336 args: fn_args,
19337 },
19338 "string_to_table" => crate::ast::Expr::FunctionCall {
19339 name: "string_to_array".to_string(),
19340 args: fn_args,
19341 },
19342 "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
19343 name: "regexp_split_to_array".to_string(),
19344 args: fn_args,
19345 },
19346 // v7.39 (read01 round 74) — an SRF with no array form
19347 // (`generate_series`, a user `RETURNS SETOF` function) has no
19348 // scalar expression to zip, so the WHOLE list switches to the
19349 // rows_from channel, which runs each function and zips the
19350 // rows themselves. The all-array case keeps the old lowering:
19351 // it is well-trodden and this must not disturb it.
19352 _ => {
19353 generic.push((fn_name, fn_args));
19354 if matches!(self.peek(), Token::Comma) {
19355 self.advance();
19356 continue;
19357 }
19358 break;
19359 }
19360 };
19361 generic.push((
19362 // The array-able entries carry their lowered expr along, so a
19363 // MIXED list still works: the engine sees the scalar array
19364 // form and unnests it.
19365 "__array".to_string(),
19366 alloc::vec![entry.clone()],
19367 ));
19368 entries.push(entry);
19369 if matches!(self.peek(), Token::Comma) {
19370 self.advance();
19371 continue;
19372 }
19373 break;
19374 }
19375 if !matches!(self.peek(), Token::RParen) {
19376 return Err(self.err(alloc::format!(
19377 "expected ')' to close ROWS FROM, got {:?}",
19378 self.peek()
19379 )));
19380 }
19381 self.advance();
19382 let with_ordinality = self.absorb_with_ordinality();
19383 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19384 let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19385 // v7.39 (read01 round 74) — some entry had no array form, so the whole
19386 // list rides the generic channel.
19387 if generic.iter().any(|(n, _)| n != "__array") {
19388 let correlated = generic
19389 .iter()
19390 .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19391 let tref = TableRef {
19392 name,
19393 alias: alias_ident,
19394 only: false,
19395 as_of_segment: None,
19396 unnest_expr: None,
19397 unnest_column_aliases,
19398 with_ordinality,
19399 generate_series_args: None,
19400 lateral_subquery: None,
19401 jsonb_each_text_arg: None,
19402 table_fn_call: None,
19403 rows_from: Some(generic),
19404 json_table: None,
19405 scalar_fn_item: false,
19406 };
19407 return Ok(if correlated {
19408 Self::wrap_correlated_srf(tref)
19409 } else {
19410 tref
19411 });
19412 }
19413 let correlated = entries.iter().any(Self::expr_has_any_column);
19414 let expr = if entries.len() == 1 {
19415 entries.pop().expect("len checked")
19416 } else {
19417 crate::ast::Expr::FunctionCall {
19418 name: "__unnest_zip".to_string(),
19419 args: entries,
19420 }
19421 };
19422 let tref = TableRef {
19423 name,
19424 alias: alias_ident,
19425 only: false,
19426 as_of_segment: None,
19427 unnest_expr: Some(Box::new(expr)),
19428 unnest_column_aliases,
19429 with_ordinality,
19430 generate_series_args: None,
19431 lateral_subquery: None,
19432 jsonb_each_text_arg: None,
19433 table_fn_call: None,
19434 rows_from: None,
19435 json_table: None,
19436 scalar_fn_item: false,
19437 };
19438 return Ok(if correlated {
19439 Self::wrap_correlated_srf(tref)
19440 } else {
19441 tref
19442 });
19443 }
19444 // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19445 // source. Detect at the head before the bare-ident fallback;
19446 // unnest is not a reserved token.
19447 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19448 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19449 {
19450 self.advance(); // unnest
19451 self.advance(); // (
19452 let mut srf_args = alloc::vec![self.parse_expr(0)?];
19453 while matches!(self.peek(), Token::Comma) {
19454 self.advance();
19455 srf_args.push(self.parse_expr(0)?);
19456 }
19457 if !matches!(self.peek(), Token::RParen) {
19458 return Err(self.err(alloc::format!(
19459 "expected ')' after unnest() argument, got {:?}",
19460 self.peek()
19461 )));
19462 }
19463 self.advance();
19464 // Multi-arg unnest(a, b, …) zips the arrays in
19465 // parallel, NULL-padding to the longest (PG's ROWS
19466 // FROM shorthand). Lower onto the unnest channel as an
19467 // internal marker call the executors unpack.
19468 let expr = if srf_args.len() == 1 {
19469 srf_args.pop().expect("len checked")
19470 } else {
19471 crate::ast::Expr::FunctionCall {
19472 name: "__unnest_zip".to_string(),
19473 args: srf_args,
19474 }
19475 };
19476 let with_ordinality = self.absorb_with_ordinality();
19477 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19478 let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19479 let correlated = Self::expr_has_any_column(&expr);
19480 let tref = TableRef {
19481 name,
19482 alias: alias_ident,
19483 only: false,
19484 as_of_segment: None,
19485 unnest_expr: Some(Box::new(expr)),
19486 unnest_column_aliases,
19487 with_ordinality,
19488 generate_series_args: None,
19489 lateral_subquery: None,
19490 jsonb_each_text_arg: None,
19491 table_fn_call: None,
19492 rows_from: None,
19493 json_table: None,
19494 scalar_fn_item: false,
19495 };
19496 return Ok(if correlated {
19497 Self::wrap_correlated_srf(tref)
19498 } else {
19499 tref
19500 });
19501 }
19502 // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19503 // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19504 // generic table-fn arg parser can't read), so it is intercepted
19505 // here BEFORE the generic dispatch. The doc expr may reference
19506 // outer columns (implicit LATERAL) — same correlated-wrap rule.
19507 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19508 if s.eq_ignore_ascii_case("json_table"))
19509 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19510 {
19511 let tref = self.parse_json_table_ref()?;
19512 let correlated = tref
19513 .json_table
19514 .as_deref()
19515 .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19516 return Ok(if correlated {
19517 Self::wrap_correlated_srf(tref)
19518 } else {
19519 tref
19520 });
19521 }
19522 // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19523 // functions dispatched by name (`pg_partition_tree('t')`,
19524 // `pg_partition_ancestors('t')`). Same head-detection shape as
19525 // unnest; the engine executor owns the row shape per function.
19526 // v7.39 (read01 round 65) — and a USER function in FROM position
19527 // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19528 // (generate_series / unnest / the json_each family) keep it — their arms
19529 // sit further down, so they are excluded here by name rather than by
19530 // ordering. Anything else that is an ident followed by `(` is a table
19531 // function; the engine executor decides whether it is a builtin, a
19532 // set-returning user function, or an error.
19533 // 7.38.1 S5.1 — pg_dump spells its table functions
19534 // schema-qualified (`pg_catalog.pg_options_to_table(...)`);
19535 // strip the pg_catalog prefix here so the same head-detection
19536 // fires. Only pg_catalog: a user schema's `s.f(x)` keeps its
19537 // meaning.
19538 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
19539 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
19540 && matches!(
19541 self.tokens.get(self.pos + 2),
19542 Some(Token::Ident(_) | Token::QuotedIdent(_))
19543 )
19544 && matches!(self.tokens.get(self.pos + 3), Some(Token::LParen))
19545 {
19546 self.advance(); // pg_catalog
19547 self.advance(); // .
19548 }
19549 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19550 if !s.eq_ignore_ascii_case("generate_series")
19551 && !s.eq_ignore_ascii_case("unnest")
19552 && !is_json_each_name(s))
19553 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19554 {
19555 // Body out-of-line — this parse sits on the FROM/subquery
19556 // recursion chain (debug frame-cliff discipline).
19557 // v7.39 (read01 round 69) — a call whose arguments reference an outer
19558 // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19559 // outer row, so it rides the lateral channel. Same rule the unnest
19560 // arm uses.
19561 let tref = self.parse_table_fn_ref()?;
19562 let correlated = tref
19563 .table_fn_call
19564 .as_deref()
19565 .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19566 return Ok(if correlated {
19567 Self::wrap_correlated_srf(tref)
19568 } else {
19569 tref
19570 });
19571 }
19572 // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19573 // [, step])` set-returning source. Same shape as unnest:
19574 // detect at the head, parse the comma-separated arg list,
19575 // dispatch downstream through the engine's set-returning
19576 // path. Supports integer triplets (mailrs's `WITH row_no AS
19577 // (SELECT * FROM generate_series(1, N))` pattern) and
19578 // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19579 // date-range iteration pattern, which pre-3.10 had no
19580 // direct equivalent in SPG).
19581 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19582 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19583 {
19584 self.advance(); // generate_series
19585 self.advance(); // (
19586 let mut args: Vec<Expr> = Vec::new();
19587 loop {
19588 args.push(self.parse_expr(0)?);
19589 if matches!(self.peek(), Token::Comma) {
19590 self.advance();
19591 continue;
19592 }
19593 break;
19594 }
19595 if !matches!(self.peek(), Token::RParen) {
19596 return Err(self.err(alloc::format!(
19597 "expected ')' after generate_series() arguments, got {:?}",
19598 self.peek()
19599 )));
19600 }
19601 self.advance();
19602 if args.len() < 2 || args.len() > 3 {
19603 return Err(self.err(alloc::format!(
19604 "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19605 args.len()
19606 )));
19607 }
19608 let with_ordinality = self.absorb_with_ordinality();
19609 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19610 let name = alias_ident
19611 .clone()
19612 .unwrap_or_else(|| "generate_series".to_string());
19613 let correlated = args.iter().any(Self::expr_has_any_column);
19614 let tref = TableRef {
19615 name,
19616 alias: alias_ident,
19617 only: false,
19618 as_of_segment: None,
19619 unnest_expr: None,
19620 unnest_column_aliases: column_aliases,
19621 with_ordinality,
19622 generate_series_args: Some(args),
19623 lateral_subquery: None,
19624 jsonb_each_text_arg: None,
19625 table_fn_call: None,
19626 rows_from: None,
19627 json_table: None,
19628 scalar_fn_item: false,
19629 };
19630 return Ok(if correlated {
19631 Self::wrap_correlated_srf(tref)
19632 } else {
19633 tref
19634 });
19635 }
19636 // v7.16.2 — preserve information_schema / pg_catalog
19637 // qualifiers (mailrs round-10 A.3). The generic
19638 // `expect_ident_like` strip silently drops the schema;
19639 // we want the engine to recognise these PG meta tables
19640 // and synthesise rows from the live catalog. Produce a
19641 // synthetic name (`__spg_info_columns` etc.) so the
19642 // engine's SELECT-side router can dispatch without
19643 // clashing with any user-defined `columns` table.
19644 let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
19645 (synth, Some(orig))
19646 } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
19647 (synth, Some(orig))
19648 } else {
19649 (self.expect_ident_like()?, None)
19650 };
19651 // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
19652 // time-travel clause. Parse BEFORE the alias so the
19653 // alias can still ride at the tail (`tbl AS OF SEGMENT
19654 // '5' alias`). `AS` is a reserved keyword token, while
19655 // `OF` and `SEGMENT` are bare idents.
19656 let as_of_segment = if matches!(self.peek(), Token::As)
19657 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
19658 {
19659 self.advance(); // AS
19660 self.advance(); // OF
19661 let kw = match self.peek().clone() {
19662 Token::Ident(s) | Token::QuotedIdent(s) => s,
19663 other => {
19664 return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
19665 }
19666 };
19667 if !kw.eq_ignore_ascii_case("segment") {
19668 return Err(self.err(format!(
19669 "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
19670 )));
19671 }
19672 self.advance();
19673 // Segment id literal — accept either a string or
19674 // integer for operator ergonomics.
19675 let id = match self.advance() {
19676 Token::String(s) => s
19677 .parse::<u32>()
19678 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19679 Token::Integer(n) => u32::try_from(n)
19680 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19681 other => {
19682 return Err(self.err(format!(
19683 "expected segment id literal after AS OF SEGMENT, got {other:?}"
19684 )));
19685 }
19686 };
19687 Some(id)
19688 } else {
19689 None
19690 };
19691 // TABLESAMPLE is not a reserved token — keep the bare-ident
19692 // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
19693 let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
19694 {
19695 None
19696 } else {
19697 self.parse_optional_alias()?
19698 };
19699 // r1052 — a catalog name rewritten to its synthetic form keeps
19700 // the WRITTEN name as the relation's alias, so `pg_cast.oid`
19701 // still binds after `pg_cast` became `__spg_pg_cast`. PG
19702 // semantics: the visible name of `pg_catalog.pg_cast` IS
19703 // `pg_cast`. Without this, every table-name-qualified column
19704 // on a synthesised catalog answered "missing FROM-clause
19705 // entry" — which is the wall pg_dump hit on its first
19706 // pg_proc/pg_cast query.
19707 let alias = match (&alias, &meta_original) {
19708 (None, Some(orig)) if *orig != name => Some(orig.clone()),
19709 _ => alias,
19710 };
19711 // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
19712 // (PG grammar). BERNOULLI lowers to a per-row
19713 // `random() < p/100` conjunct on the enclosing SELECT's
19714 // WHERE — exact row-level Bernoulli semantics. SYSTEM
19715 // shares the lowering: SPG has no page structure to
19716 // sample, and the row-level form returns the same expected
19717 // fraction. REPEATABLE(seed) promises a deterministic
19718 // sample SPG cannot honour yet — honest error rather than
19719 // a silently ignored seed.
19720 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
19721 self.advance();
19722 let method = self.expect_ident_like()?;
19723 if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
19724 return Err(self.err(alloc::format!(
19725 "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
19726 )));
19727 }
19728 if !matches!(self.peek(), Token::LParen) {
19729 return Err(self.err(alloc::format!(
19730 "expected '(' after TABLESAMPLE {}, got {:?}",
19731 method.to_ascii_uppercase(),
19732 self.peek()
19733 )));
19734 }
19735 self.advance();
19736 let percent = self.parse_expr(0)?;
19737 if !matches!(self.peek(), Token::RParen) {
19738 return Err(self.err(alloc::format!(
19739 "expected ')' after TABLESAMPLE percentage, got {:?}",
19740 self.peek()
19741 )));
19742 }
19743 self.advance();
19744 // REPEATABLE(seed) → a deterministic per-row draw seeded by
19745 // `seed`, so the sample is stable across repeats and rescans.
19746 // Non-REPEATABLE keeps the non-deterministic `random()` draw.
19747 let mut sample_seed: Option<Expr> = None;
19748 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
19749 self.advance();
19750 if !matches!(self.peek(), Token::LParen) {
19751 return Err(self.err(alloc::format!(
19752 "expected '(' after REPEATABLE, got {:?}",
19753 self.peek()
19754 )));
19755 }
19756 self.advance();
19757 let seed = self.parse_expr(0)?;
19758 if !matches!(self.peek(), Token::RParen) {
19759 return Err(self.err(alloc::format!(
19760 "expected ')' after REPEATABLE seed, got {:?}",
19761 self.peek()
19762 )));
19763 }
19764 self.advance();
19765 sample_seed = Some(seed);
19766 }
19767 let draw = match sample_seed {
19768 Some(seed) => Expr::FunctionCall {
19769 name: "__tsm_fract".to_string(),
19770 args: alloc::vec![seed],
19771 },
19772 None => Expr::FunctionCall {
19773 name: "random".to_string(),
19774 args: Vec::new(),
19775 },
19776 };
19777 self.pending_sample_preds.push(Expr::Binary {
19778 lhs: Box::new(draw),
19779 op: crate::ast::BinOp::Lt,
19780 rhs: Box::new(Expr::Binary {
19781 lhs: Box::new(percent),
19782 op: crate::ast::BinOp::Div,
19783 rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
19784 }),
19785 });
19786 }
19787 Ok(TableRef {
19788 name,
19789 alias,
19790 only,
19791 as_of_segment,
19792 unnest_expr: None,
19793 unnest_column_aliases: Vec::new(),
19794 with_ordinality: false,
19795 generate_series_args: None,
19796 lateral_subquery: None,
19797 jsonb_each_text_arg: None,
19798 table_fn_call: None,
19799 rows_from: None,
19800 json_table: None,
19801 scalar_fn_item: false,
19802 })
19803 }
19804
19805 /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
19806 /// but also accepts `AS alias(col [, col, …])` — the
19807 /// PG-standard table-function column-list form. The column
19808 /// list is only honoured when paired with `UNNEST(...)` in
19809 /// the parent; other call sites currently discard it.
19810 /// True when the expression tree contains a qualified column
19811 /// reference (`t.col`) — the syntactic marker that an SRF
19812 /// argument correlates with a preceding FROM item.
19813 fn expr_has_qualified_column(e: &Expr) -> bool {
19814 match e {
19815 Expr::Column(c) => c.qualifier.is_some(),
19816 Expr::Binary { lhs, rhs, .. } => {
19817 Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
19818 }
19819 Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
19820 Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
19821 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
19822 Expr::Case {
19823 operand,
19824 branches,
19825 else_branch,
19826 } => {
19827 operand
19828 .as_deref()
19829 .is_some_and(Self::expr_has_qualified_column)
19830 || branches.iter().any(|(w, t)| {
19831 Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
19832 })
19833 || else_branch
19834 .as_deref()
19835 .is_some_and(Self::expr_has_qualified_column)
19836 }
19837 _ => false,
19838 }
19839 }
19840
19841 /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
19842 /// counts a bare (unqualified) column. A set-returning function has no
19843 /// input columns of its own, so ANY column in its arguments is an outer
19844 /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
19845 fn expr_has_any_column(e: &Expr) -> bool {
19846 match e {
19847 Expr::Column(_) => true,
19848 Expr::Binary { lhs, rhs, .. } => {
19849 Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
19850 }
19851 Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
19852 Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
19853 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
19854 // v7.39 (round 759, F31-B8b) — a column INSIDE an array
19855 // constructor or subscript fell to the `_ => false` arm, so
19856 // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
19857 // channel and the eager peer eval answered `column "x" does
19858 // not exist` (the substitution walker already recurses both
19859 // shapes; only this detector was blind to them).
19860 Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
19861 Expr::ArraySubscript { target, index } => {
19862 Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
19863 }
19864 Expr::Case {
19865 operand,
19866 branches,
19867 else_branch,
19868 } => {
19869 operand.as_deref().is_some_and(Self::expr_has_any_column)
19870 || branches
19871 .iter()
19872 .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
19873 || else_branch
19874 .as_deref()
19875 .is_some_and(Self::expr_has_any_column)
19876 }
19877 _ => false,
19878 }
19879 }
19880
19881 /// Wrap a correlated SRF table ref (`unnest(t.col)` /
19882 /// `generate_series(1, t.n)`) into the lateral_subquery
19883 /// channel: `SELECT * FROM <srf>` executes per outer row with
19884 /// outer references substituted (v7.37.43-T4.5 machinery).
19885 /// Uncorrelated SRFs stay on their plain channels.
19886 fn wrap_correlated_srf(srf: TableRef) -> TableRef {
19887 let name = srf.name.clone();
19888 let alias = srf.alias.clone();
19889 let inner = crate::ast::SelectStatement {
19890 locking: None,
19891 ctes: Vec::new(),
19892 distinct: false,
19893 distinct_on: Vec::new(),
19894 items: alloc::vec![crate::ast::SelectItem::Wildcard],
19895 from: Some(crate::ast::FromClause {
19896 primary: srf,
19897 joins: Vec::new(),
19898 }),
19899 where_: None,
19900 group_by: None,
19901 group_by_all: false,
19902 having: None,
19903 unions: Vec::new(),
19904 order_by: Vec::new(),
19905 limit: None,
19906 offset: None,
19907 limit_with_ties: false,
19908 window_check_exprs: Vec::new(),
19909 };
19910 TableRef {
19911 name,
19912 alias,
19913 only: false,
19914 as_of_segment: None,
19915 unnest_expr: None,
19916 unnest_column_aliases: Vec::new(),
19917 with_ordinality: false,
19918 generate_series_args: None,
19919 lateral_subquery: Some(Box::new(inner)),
19920 jsonb_each_text_arg: None,
19921 table_fn_call: None,
19922 rows_from: None,
19923 json_table: None,
19924 scalar_fn_item: false,
19925 }
19926 }
19927
19928 /// True when the expression tree contains an unresolved
19929 /// `OVER w` marker (see parse_over_clause).
19930 fn expr_has_named_window(e: &Expr) -> bool {
19931 match e {
19932 Expr::WindowFunction { partition_by, .. } => matches!(
19933 partition_by.as_slice(),
19934 [Expr::Column(c)] if matches!(
19935 c.qualifier.as_deref(),
19936 Some("__named_window__") | Some("__named_window_ref__")
19937 )
19938 ),
19939 Expr::Binary { lhs, rhs, .. } => {
19940 Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
19941 }
19942 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
19943 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
19944 Expr::Case {
19945 operand,
19946 branches,
19947 else_branch,
19948 } => {
19949 operand.as_deref().is_some_and(Self::expr_has_named_window)
19950 || branches.iter().any(|(w, t)| {
19951 Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
19952 })
19953 || else_branch
19954 .as_deref()
19955 .is_some_and(Self::expr_has_named_window)
19956 }
19957 _ => false,
19958 }
19959 }
19960
19961 /// v7.39 (round 705) — the NAMES the expression references through the
19962 /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
19963 /// definitions nothing referenced. Traversal mirrors
19964 /// `expr_has_named_window` above.
19965 fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
19966 match e {
19967 Expr::WindowFunction { partition_by, .. } => {
19968 if let [Expr::Column(c)] = partition_by.as_slice()
19969 && matches!(
19970 c.qualifier.as_deref(),
19971 Some("__named_window__") | Some("__named_window_ref__")
19972 )
19973 {
19974 into.push(c.name.clone());
19975 }
19976 }
19977 Expr::Binary { lhs, rhs, .. } => {
19978 Self::collect_named_window_refs(lhs, into);
19979 Self::collect_named_window_refs(rhs, into);
19980 }
19981 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19982 Self::collect_named_window_refs(expr, into);
19983 }
19984 Expr::FunctionCall { args, .. } => {
19985 for a in args {
19986 Self::collect_named_window_refs(a, into);
19987 }
19988 }
19989 Expr::Case {
19990 operand,
19991 branches,
19992 else_branch,
19993 } => {
19994 if let Some(o) = operand.as_deref() {
19995 Self::collect_named_window_refs(o, into);
19996 }
19997 for (w, t) in branches {
19998 Self::collect_named_window_refs(w, into);
19999 Self::collect_named_window_refs(t, into);
20000 }
20001 if let Some(eb) = else_branch.as_deref() {
20002 Self::collect_named_window_refs(eb, into);
20003 }
20004 }
20005 _ => {}
20006 }
20007 }
20008
20009 /// Inline named-window definitions into the `OVER w` markers.
20010 /// An unknown name errors (PG: window "w" does not exist).
20011 #[allow(clippy::type_complexity)]
20012 fn substitute_named_windows(
20013 e: &mut Expr,
20014 defs: &[(
20015 String,
20016 (
20017 Vec<Expr>,
20018 Vec<(Expr, bool, Option<bool>)>,
20019 Option<WindowFrame>,
20020 ),
20021 )],
20022 ) -> Result<(), String> {
20023 match e {
20024 Expr::WindowFunction {
20025 partition_by,
20026 order_by,
20027 frame,
20028 ..
20029 } => {
20030 // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
20031 // from the bare `OVER w1` (a plain reference).
20032 let named = match partition_by.as_slice() {
20033 [Expr::Column(c)] => match c.qualifier.as_deref() {
20034 Some("__named_window__") => Some((c.name.clone(), false)),
20035 Some("__named_window_ref__") => Some((c.name.clone(), true)),
20036 _ => None,
20037 },
20038 _ => None,
20039 };
20040 if let Some((wname, is_copy)) = named {
20041 let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
20042 else {
20043 return Err(alloc::format!("window {wname:?} does not exist"));
20044 };
20045 if !is_copy {
20046 *partition_by = def.0.clone();
20047 *order_by = def.1.clone();
20048 *frame = def.2.clone();
20049 return Ok(());
20050 }
20051 // v7.39 (round 229) — PG's copy rules, probed against
20052 // 18.4: a copy inherits the partitioning, may supply an
20053 // ordering only when the base has none, and may not copy
20054 // a base that already carries a frame (its own frame
20055 // would be ambiguous with the inherited one).
20056 if !def.1.is_empty() && !order_by.is_empty() {
20057 return Err(alloc::format!(
20058 "cannot override ORDER BY clause of window \"{wname}\""
20059 ));
20060 }
20061 if def.2.is_some() {
20062 return Err(alloc::format!(
20063 "cannot copy window \"{wname}\" because it has a frame clause"
20064 ));
20065 }
20066 *partition_by = def.0.clone();
20067 if order_by.is_empty() {
20068 *order_by = def.1.clone();
20069 }
20070 }
20071 Ok(())
20072 }
20073 Expr::Binary { lhs, rhs, .. } => {
20074 Self::substitute_named_windows(lhs, defs)?;
20075 Self::substitute_named_windows(rhs, defs)
20076 }
20077 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20078 Self::substitute_named_windows(expr, defs)
20079 }
20080 Expr::FunctionCall { args, .. } => {
20081 for a in args {
20082 Self::substitute_named_windows(a, defs)?;
20083 }
20084 Ok(())
20085 }
20086 Expr::Case {
20087 operand,
20088 branches,
20089 else_branch,
20090 } => {
20091 if let Some(op) = operand {
20092 Self::substitute_named_windows(op, defs)?;
20093 }
20094 for (w, t) in branches {
20095 Self::substitute_named_windows(w, defs)?;
20096 Self::substitute_named_windows(t, defs)?;
20097 }
20098 if let Some(el) = else_branch {
20099 Self::substitute_named_windows(el, defs)?;
20100 }
20101 Ok(())
20102 }
20103 _ => Ok(()),
20104 }
20105 }
20106
20107 /// SQL-standard `TABLE name` shorthand — builds the equivalent
20108 /// `SELECT * FROM name` head. Callers own set-op chain / tail
20109 /// composition.
20110 fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
20111 debug_assert!(matches!(self.peek(), Token::Table));
20112 self.advance(); // TABLE
20113 let tname = self.expect_ident_like()?;
20114 Ok(SelectStatement {
20115 locking: None,
20116 ctes: Vec::new(),
20117 distinct: false,
20118 distinct_on: Vec::new(),
20119 items: alloc::vec![SelectItem::Wildcard],
20120 from: Some(FromClause {
20121 primary: TableRef {
20122 name: tname,
20123 alias: None,
20124 only: false,
20125 as_of_segment: None,
20126 unnest_expr: None,
20127 unnest_column_aliases: Vec::new(),
20128 with_ordinality: false,
20129 generate_series_args: None,
20130 lateral_subquery: None,
20131 jsonb_each_text_arg: None,
20132 table_fn_call: None,
20133 rows_from: None,
20134 json_table: None,
20135 scalar_fn_item: false,
20136 },
20137 joins: Vec::new(),
20138 }),
20139 where_: None,
20140 group_by: None,
20141 group_by_all: false,
20142 having: None,
20143 unions: Vec::new(),
20144 order_by: Vec::new(),
20145 limit: None,
20146 offset: None,
20147 limit_with_ties: false,
20148 window_check_exprs: Vec::new(),
20149 })
20150 }
20151
20152 /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
20153 /// variants) → a derived table that reads each declared column out of
20154 /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
20155 /// `jsonb_array_elements(J)` (one row per element, column `value`);
20156 /// the scalar *record form projects a single row straight off `J`.
20157 /// Rides the existing lateral-subquery channel, so no new executor or
20158 /// AST is needed.
20159 fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
20160 use crate::ast::{
20161 BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
20162 };
20163 let fn_name = match self.peek() {
20164 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20165 _ => unreachable!("caller guarded is_json_to_record_name"),
20166 };
20167 self.advance(); // fn name
20168 self.advance(); // (
20169 let mut arg = self.parse_expr(0)?;
20170 // populate_record(base, json): the base only carries the record
20171 // type here — the JSON argument is the second expression.
20172 let mut base: Option<Expr> = None;
20173 if matches!(self.peek(), Token::Comma) {
20174 self.advance();
20175 base = Some(arg);
20176 arg = self.parse_expr(0)?;
20177 }
20178 if !matches!(self.peek(), Token::RParen) {
20179 return Err(self.err(alloc::format!(
20180 "expected ')' after {fn_name}() argument, got {:?}",
20181 self.peek()
20182 )));
20183 }
20184 self.advance(); // )
20185 let is_set = fn_name.ends_with("recordset");
20186 // `[AS] alias ( col type [, …] )` column-definition list.
20187 if matches!(self.peek(), Token::As) {
20188 self.advance();
20189 }
20190 let alias_opt = match self.peek() {
20191 Token::Ident(s) | Token::QuotedIdent(s) => {
20192 let a = s.clone();
20193 self.advance();
20194 Some(a)
20195 }
20196 _ => None,
20197 };
20198 // v7.39 (read01 round 76) — the populate family's canonical PG
20199 // spelling carries no column list at all: the row shape comes from
20200 // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
20201 // j)`). The parser has no catalog, so hand the two arguments to the
20202 // engine's table-function channel, which does. Only `*_to_record*`
20203 // (whose base is bare `record`) genuinely requires the list.
20204 if !matches!(self.peek(), Token::LParen) {
20205 if let Some(base_expr) = base {
20206 let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
20207 return Ok(TableRef {
20208 name: alias.clone(),
20209 alias: Some(alias),
20210 only: false,
20211 as_of_segment: None,
20212 unnest_expr: None,
20213 unnest_column_aliases: Vec::new(),
20214 with_ordinality: false,
20215 generate_series_args: None,
20216 lateral_subquery: None,
20217 jsonb_each_text_arg: None,
20218 table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
20219 rows_from: None,
20220 json_table: None,
20221 scalar_fn_item: false,
20222 });
20223 }
20224 return Err(self.err(alloc::format!(
20225 "expected '(' to start the {fn_name} column-definition list, got {:?}",
20226 self.peek()
20227 )));
20228 }
20229 let Some(alias) = alias_opt else {
20230 return Err(self.err(alloc::format!(
20231 "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
20232 )));
20233 };
20234 self.advance(); // (
20235 let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
20236 loop {
20237 let col = self.expect_ident_like()?;
20238 let ty = self.parse_cast_target()?;
20239 coldefs.push((col, ty));
20240 if matches!(self.peek(), Token::Comma) {
20241 self.advance();
20242 continue;
20243 }
20244 if matches!(self.peek(), Token::RParen) {
20245 self.advance();
20246 break;
20247 }
20248 return Err(self.err(alloc::format!(
20249 "expected ',' or ')' in {fn_name} column list, got {:?}",
20250 self.peek()
20251 )));
20252 }
20253 if coldefs.is_empty() {
20254 return Err(self.err(alloc::format!(
20255 "{fn_name} column-definition list must declare at least one column"
20256 )));
20257 }
20258 // Per column: (base ->> 'col')::type AS col. The base is the
20259 // per-element `value` column for the *set form, or the argument
20260 // itself for the scalar record form.
20261 let items: Vec<SelectItem> = coldefs
20262 .into_iter()
20263 .map(|(col, ty)| {
20264 let base = if is_set {
20265 Expr::Column(ColumnName {
20266 qualifier: None,
20267 name: "value".to_string(),
20268 })
20269 } else {
20270 arg.clone()
20271 };
20272 SelectItem::Expr {
20273 expr: Expr::Cast {
20274 expr: Box::new(Expr::Binary {
20275 lhs: Box::new(base),
20276 op: BinOp::JsonGetText,
20277 rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
20278 }),
20279 target: ty,
20280 },
20281 alias: Some(col),
20282 }
20283 })
20284 .collect();
20285 let from = if is_set {
20286 let elem_fn = if fn_name.starts_with("jsonb") {
20287 "jsonb_array_elements"
20288 } else {
20289 "json_array_elements"
20290 };
20291 Some(FromClause {
20292 primary: TableRef {
20293 name: "value".to_string(),
20294 alias: None,
20295 only: false,
20296 as_of_segment: None,
20297 unnest_expr: Some(Box::new(Expr::FunctionCall {
20298 name: elem_fn.to_string(),
20299 args: alloc::vec![arg],
20300 })),
20301 unnest_column_aliases: alloc::vec!["value".to_string()],
20302 with_ordinality: false,
20303 generate_series_args: None,
20304 lateral_subquery: None,
20305 jsonb_each_text_arg: None,
20306 table_fn_call: None,
20307 rows_from: None,
20308 json_table: None,
20309 scalar_fn_item: false,
20310 },
20311 joins: Vec::new(),
20312 })
20313 } else {
20314 None
20315 };
20316 let inner = SelectStatement {
20317 locking: None,
20318 ctes: Vec::new(),
20319 distinct: false,
20320 distinct_on: Vec::new(),
20321 items,
20322 from,
20323 where_: None,
20324 group_by: None,
20325 group_by_all: false,
20326 having: None,
20327 unions: Vec::new(),
20328 order_by: Vec::new(),
20329 limit: None,
20330 offset: None,
20331 limit_with_ties: false,
20332 window_check_exprs: Vec::new(),
20333 };
20334 Ok(TableRef {
20335 name: alias.clone(),
20336 alias: Some(alias),
20337 only: false,
20338 as_of_segment: None,
20339 unnest_expr: None,
20340 unnest_column_aliases: Vec::new(),
20341 with_ordinality: false,
20342 generate_series_args: None,
20343 lateral_subquery: Some(Box::new(inner)),
20344 jsonb_each_text_arg: None,
20345 table_fn_call: None,
20346 rows_from: None,
20347 json_table: None,
20348 scalar_fn_item: false,
20349 })
20350 }
20351
20352 /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
20353 /// Returns true when the clause was present. `WITH` alone (a
20354 /// CTE can never start here) is not enough — the ORDINALITY
20355 /// ident must follow, so a stray WITH still errors downstream.
20356 fn absorb_with_ordinality(&mut self) -> bool {
20357 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
20358 && matches!(self.tokens.get(self.pos + 1),
20359 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
20360 {
20361 self.advance();
20362 self.advance();
20363 true
20364 } else {
20365 false
20366 }
20367 }
20368
20369 /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
20370 /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
20371 /// Out-of-line: the caller sits on the FROM recursion chain.
20372 #[inline(never)]
20373 fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
20374 let fn_name = match self.advance() {
20375 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20376 _ => unreachable!("caller peeked an ident"),
20377 };
20378 self.advance(); // (
20379 let mut args: Vec<Expr> = Vec::new();
20380 if !matches!(self.peek(), Token::RParen) {
20381 loop {
20382 args.push(self.parse_expr(0)?);
20383 if matches!(self.peek(), Token::Comma) {
20384 self.advance();
20385 continue;
20386 }
20387 break;
20388 }
20389 }
20390 if !matches!(self.peek(), Token::RParen) {
20391 return Err(self.err(alloc::format!(
20392 "expected ')' after {fn_name}() arguments, got {:?}",
20393 self.peek()
20394 )));
20395 }
20396 self.advance();
20397 // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20398 // counter column rides after the function's own, and the alias list
20399 // names it.
20400 let with_ordinality = self.absorb_with_ordinality();
20401 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20402 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20403 Ok(TableRef {
20404 name,
20405 alias: alias_ident,
20406 only: false,
20407 as_of_segment: None,
20408 unnest_expr: None,
20409 unnest_column_aliases,
20410 with_ordinality,
20411 generate_series_args: None,
20412 lateral_subquery: None,
20413 jsonb_each_text_arg: None,
20414 table_fn_call: Some(Box::new((fn_name, args))),
20415 rows_from: None,
20416 json_table: None,
20417 scalar_fn_item: false,
20418 })
20419 }
20420
20421 /// v7.39 (round 205, JSON_TABLE) — parse
20422 /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20423 /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20424 /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20425 #[inline(never)]
20426 fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20427 self.advance(); // json_table
20428 self.advance(); // (
20429 let doc = Box::new(self.parse_expr(0)?);
20430 self.expect_comma_json_table()?;
20431 let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20432 // Optional `PASSING <expr> AS <name> [, …]`.
20433 let mut passing: Vec<(String, Expr)> = Vec::new();
20434 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20435 self.advance();
20436 loop {
20437 let e = self.parse_expr(0)?;
20438 if !matches!(self.peek(), Token::As) {
20439 return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20440 }
20441 self.advance();
20442 let vname = match self.advance() {
20443 Token::Ident(s) | Token::QuotedIdent(s) => s,
20444 other => {
20445 return Err(self.err(alloc::format!(
20446 "expected PASSING variable name, got {other:?}"
20447 )));
20448 }
20449 };
20450 passing.push((vname, e));
20451 if matches!(self.peek(), Token::Comma) {
20452 self.advance();
20453 continue;
20454 }
20455 break;
20456 }
20457 }
20458 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20459 return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20460 }
20461 self.advance();
20462 let columns = self.parse_json_table_columns()?;
20463 if !matches!(self.peek(), Token::RParen) {
20464 return Err(self.err(alloc::format!(
20465 "expected ')' to close JSON_TABLE, got {:?}",
20466 self.peek()
20467 )));
20468 }
20469 self.advance();
20470 let alias_ident = self.parse_optional_alias()?;
20471 let name = alias_ident
20472 .clone()
20473 .unwrap_or_else(|| String::from("json_table"));
20474 Ok(TableRef {
20475 name,
20476 alias: alias_ident,
20477 only: false,
20478 as_of_segment: None,
20479 unnest_expr: None,
20480 unnest_column_aliases: Vec::new(),
20481 with_ordinality: false,
20482 generate_series_args: None,
20483 lateral_subquery: None,
20484 jsonb_each_text_arg: None,
20485 table_fn_call: None,
20486 rows_from: None,
20487 json_table: Some(Box::new(crate::ast::JsonTable {
20488 doc,
20489 row_path,
20490 columns,
20491 passing,
20492 })),
20493 scalar_fn_item: false,
20494 })
20495 }
20496
20497 fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20498 if !matches!(self.peek(), Token::Comma) {
20499 return Err(self.err(alloc::format!(
20500 "expected ',' after JSON_TABLE document, got {:?}",
20501 self.peek()
20502 )));
20503 }
20504 self.advance();
20505 Ok(())
20506 }
20507
20508 fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20509 match self.advance() {
20510 Token::String(s) => Ok(s),
20511 other => Err(self.err(alloc::format!(
20512 "expected {what} string literal, got {other:?}"
20513 ))),
20514 }
20515 }
20516
20517 /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20518 #[inline(never)]
20519 fn parse_json_table_columns(
20520 &mut self,
20521 ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20522 if !matches!(self.peek(), Token::LParen) {
20523 return Err(self.err("expected '(' after COLUMNS".into()));
20524 }
20525 self.advance();
20526 let mut cols = Vec::new();
20527 loop {
20528 cols.push(self.parse_json_table_one_column()?);
20529 if matches!(self.peek(), Token::Comma) {
20530 self.advance();
20531 continue;
20532 }
20533 break;
20534 }
20535 if !matches!(self.peek(), Token::RParen) {
20536 return Err(self.err(alloc::format!(
20537 "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20538 self.peek()
20539 )));
20540 }
20541 self.advance();
20542 Ok(cols)
20543 }
20544
20545 fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20546 use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20547 // NESTED [PATH] '<p>' COLUMNS (...)
20548 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20549 self.advance();
20550 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20551 self.advance();
20552 }
20553 let path = self.parse_json_string_literal("NESTED PATH")?;
20554 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20555 return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20556 }
20557 self.advance();
20558 let columns = self.parse_json_table_columns()?;
20559 return Ok(JsonTableColumn::Nested { path, columns });
20560 }
20561 // <name> ...
20562 let name = match self.advance() {
20563 Token::Ident(s) | Token::QuotedIdent(s) => s,
20564 other => {
20565 return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20566 }
20567 };
20568 // <name> FOR ORDINALITY
20569 if matches!(self.peek(), Token::For) {
20570 self.advance();
20571 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20572 return Err(self.err("expected ORDINALITY after FOR".into()));
20573 }
20574 self.advance();
20575 return Ok(JsonTableColumn::Ordinality { name });
20576 }
20577 // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20578 let ty = self.parse_column_type_name()?;
20579 let mut format_json = false;
20580 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20581 self.advance();
20582 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20583 return Err(self.err("expected JSON after FORMAT".into()));
20584 }
20585 self.advance();
20586 format_json = true;
20587 }
20588 let mut exists = false;
20589 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20590 self.advance();
20591 exists = true;
20592 }
20593 let mut path = alloc::format!("$.{name}");
20594 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20595 self.advance();
20596 path = self.parse_json_string_literal("column PATH")?;
20597 }
20598 if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20599 // `FORMAT JSON` after PATH (alternate placement).
20600 self.advance();
20601 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20602 self.advance();
20603 }
20604 format_json = true;
20605 }
20606 let mut wrapper = false;
20607 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20608 self.advance();
20609 // optional CONDITIONAL/UNCONDITIONAL
20610 if matches!(self.peek(), Token::Ident(s)
20611 if s.eq_ignore_ascii_case("unconditional")
20612 || s.eq_ignore_ascii_case("conditional"))
20613 {
20614 self.advance();
20615 }
20616 if !matches!(self.peek(), Token::Ident(s)
20617 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20618 {
20619 return Err(self.err("expected WRAPPER after WITH".into()));
20620 }
20621 self.advance();
20622 // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20623 if matches!(self.peek(), Token::Ident(s)
20624 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20625 {
20626 self.advance();
20627 }
20628 wrapper = true;
20629 }
20630 // ON EMPTY / ON ERROR clauses (two, in any order).
20631 let mut on_empty = JsonTableOnBehavior::Null;
20632 let mut on_error = JsonTableOnBehavior::Null;
20633 for _ in 0..2 {
20634 let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20635 {
20636 self.advance();
20637 Some(JsonTableOnBehavior::Error)
20638 } else if matches!(self.peek(), Token::Null) {
20639 self.advance();
20640 Some(JsonTableOnBehavior::Null)
20641 } else if matches!(self.peek(), Token::Default) {
20642 self.advance();
20643 Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
20644 } else {
20645 None
20646 };
20647 let Some(behavior) = behavior else { break };
20648 // `ON {EMPTY|ERROR}`
20649 if !matches!(self.peek(), Token::On) {
20650 return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
20651 }
20652 self.advance();
20653 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
20654 self.advance();
20655 on_empty = behavior;
20656 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
20657 self.advance();
20658 on_error = behavior;
20659 } else {
20660 return Err(self.err("expected EMPTY or ERROR after ON".into()));
20661 }
20662 }
20663 Ok(JsonTableColumn::Regular {
20664 name,
20665 ty,
20666 path,
20667 exists,
20668 format_json,
20669 wrapper,
20670 on_empty,
20671 on_error,
20672 })
20673 }
20674
20675 fn parse_optional_alias_with_columns(
20676 &mut self,
20677 ) -> Result<(Option<String>, Vec<String>), ParseError> {
20678 let alias = self.parse_optional_alias()?;
20679 if alias.is_none() {
20680 return Ok((None, Vec::new()));
20681 }
20682 let mut cols: Vec<String> = Vec::new();
20683 if matches!(self.peek(), Token::LParen) {
20684 self.advance();
20685 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
20686 self.advance();
20687 cols.push(s);
20688 if matches!(self.peek(), Token::Comma) {
20689 self.advance();
20690 continue;
20691 }
20692 break;
20693 }
20694 if matches!(self.peek(), Token::RParen) {
20695 self.advance();
20696 }
20697 }
20698 Ok((alias, cols))
20699 }
20700
20701 /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
20702 /// whose keyword token was already consumed and whose `(` is the
20703 /// current token. Factored out of `parse_atom` (and marked
20704 /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
20705 /// recursive `parse_atom` frame — inlining them there enlarges the
20706 /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
20707 /// against, risking an overflow before the budget triggers.
20708 #[inline(never)]
20709 fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
20710 self.advance(); // (
20711 let mut args = Vec::new();
20712 if !matches!(self.peek(), Token::RParen) {
20713 loop {
20714 args.push(self.parse_expr(0)?);
20715 match self.peek() {
20716 Token::Comma => {
20717 self.advance();
20718 }
20719 Token::RParen => break,
20720 other => {
20721 return Err(self.err(alloc::format!(
20722 "expected ',' or ')' in {name}() args, got {other:?}"
20723 )));
20724 }
20725 }
20726 }
20727 }
20728 self.advance(); // )
20729 Ok(Expr::FunctionCall {
20730 name: name.into(),
20731 args,
20732 })
20733 }
20734
20735 /// FROM-clause: a primary table reference plus zero-or-more joined
20736 /// peers expressed via either `, <table>` (cross-product, no ON) or
20737 /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
20738 /// v1.10 keeps the join list flat (left-associative nested-loop
20739 /// semantics).
20740 fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
20741 let primary = self.parse_table_ref()?;
20742 let primary_qual = primary
20743 .alias
20744 .clone()
20745 .unwrap_or_else(|| primary.name.clone());
20746 let joins = self.parse_from_joins(&primary_qual)?;
20747 Ok(FromClause { primary, joins })
20748 }
20749
20750 /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
20751 /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
20752 /// SAME grammar after its target table has already been consumed.
20753 /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
20754 /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
20755 /// be parsed forward, once.)
20756 /// `left_primary_qual` is the qualifier (alias, else name) of whatever
20757 /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
20758 /// target in the MySQL multi-table form. It only feeds the `USING (…)`
20759 /// desugaring, which needs a name for the left side of each equality.
20760 fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
20761 let mut joins = Vec::new();
20762 loop {
20763 // `, <table>` — cross-product with no ON.
20764 if matches!(self.peek(), Token::Comma) {
20765 self.advance();
20766 let table = self.parse_table_ref()?;
20767 joins.push(FromJoin {
20768 kind: JoinKind::Cross,
20769 table,
20770 on: None,
20771 using_cols: None,
20772 natural: false,
20773 });
20774 continue;
20775 }
20776 // v7.37.16 — optional leading `NATURAL` before the join
20777 // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
20778 // not a lexer keyword (it arrives as a bare Ident), so match
20779 // it case-insensitively here. When present, no ON/USING
20780 // clause is allowed — the common columns are resolved at
20781 // execution time.
20782 let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
20783 if natural {
20784 self.advance();
20785 }
20786 // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
20787 // CROSS JOIN, and bare JOIN (defaults to INNER).
20788 let kind =
20789 match self.peek() {
20790 Token::Inner => {
20791 self.advance();
20792 if !matches!(self.peek(), Token::Join) {
20793 return Err(self
20794 .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
20795 }
20796 self.advance();
20797 JoinKind::Inner
20798 }
20799 Token::Left => {
20800 self.advance();
20801 if matches!(self.peek(), Token::Outer) {
20802 self.advance();
20803 }
20804 if !matches!(self.peek(), Token::Join) {
20805 return Err(self.err(format!(
20806 "expected JOIN after LEFT [OUTER], got {:?}",
20807 self.peek()
20808 )));
20809 }
20810 self.advance();
20811 JoinKind::Left
20812 }
20813 Token::Cross => {
20814 self.advance();
20815 if !matches!(self.peek(), Token::Join) {
20816 return Err(self
20817 .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
20818 }
20819 self.advance();
20820 JoinKind::Cross
20821 }
20822 // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
20823 Token::Right => {
20824 self.advance();
20825 if matches!(self.peek(), Token::Outer) {
20826 self.advance();
20827 }
20828 if !matches!(self.peek(), Token::Join) {
20829 return Err(self.err(format!(
20830 "expected JOIN after RIGHT [OUTER], got {:?}",
20831 self.peek()
20832 )));
20833 }
20834 self.advance();
20835 JoinKind::Right
20836 }
20837 // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
20838 Token::Full => {
20839 self.advance();
20840 if matches!(self.peek(), Token::Outer) {
20841 self.advance();
20842 }
20843 if !matches!(self.peek(), Token::Join) {
20844 return Err(self.err(format!(
20845 "expected JOIN after FULL [OUTER], got {:?}",
20846 self.peek()
20847 )));
20848 }
20849 self.advance();
20850 JoinKind::FullOuter
20851 }
20852 Token::Join => {
20853 self.advance();
20854 JoinKind::Inner
20855 }
20856 _ => break,
20857 };
20858 let table = self.parse_table_ref()?;
20859 // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
20860 // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
20861 // where prev_table is the most-recent left-side table
20862 // (the previous join's table if any, else the FROM primary).
20863 // PG semantics around column merging are richer (USING'd
20864 // cols become deduplicated single output columns); for
20865 // sugar purposes the predicate-only form covers the
20866 // baseline corpus shape and chained `… JOIN x USING (k)
20867 // JOIN y USING (k)` calls.
20868 // v7.37.16 — NATURAL joins carry no ON/USING clause; the
20869 // common columns resolve at execution time.
20870 if natural {
20871 joins.push(FromJoin {
20872 kind,
20873 table,
20874 on: None,
20875 using_cols: None,
20876 natural: true,
20877 });
20878 continue;
20879 }
20880 let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
20881 // v7.37.16 — capture the USING column list (in addition to
20882 // the ON desugar below) so the executor can perform PG's
20883 // column-merge on the output side.
20884 let mut using_cols: Option<Vec<String>> = None;
20885 let on = if matches!(self.peek(), Token::On) {
20886 self.advance();
20887 Some(self.parse_expr(0)?)
20888 } else if using_match {
20889 self.advance();
20890 if !matches!(self.peek(), Token::LParen) {
20891 return Err(
20892 self.err(format!("expected '(' after USING, got {:?}", self.peek()))
20893 );
20894 }
20895 self.advance();
20896 let mut cols: Vec<String> = Vec::new();
20897 loop {
20898 match self.peek().clone() {
20899 Token::Ident(s) | Token::QuotedIdent(s) => {
20900 self.advance();
20901 cols.push(s);
20902 }
20903 other => {
20904 return Err(self.err(format!(
20905 "expected column name inside USING (…), got {other:?}"
20906 )));
20907 }
20908 }
20909 match self.peek() {
20910 Token::Comma => {
20911 self.advance();
20912 continue;
20913 }
20914 Token::RParen => {
20915 self.advance();
20916 break;
20917 }
20918 other => {
20919 return Err(self.err(format!(
20920 "expected ',' or ')' inside USING (…), got {other:?}"
20921 )));
20922 }
20923 }
20924 }
20925 if cols.is_empty() {
20926 return Err(self.err("USING (…) requires at least one column".to_string()));
20927 }
20928 using_cols = Some(cols.clone());
20929 // Pick the left-side alias: prev join's table if any,
20930 // else FROM primary. Use alias when present, else
20931 // table name (PG-equivalent qualifier).
20932 let left_qual: String = joins
20933 .last()
20934 .map(|j| {
20935 j.table
20936 .alias
20937 .clone()
20938 .unwrap_or_else(|| j.table.name.clone())
20939 })
20940 .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
20941 let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
20942 let mut iter = cols.into_iter().map(|c| Expr::Binary {
20943 lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20944 qualifier: Some(left_qual.clone()),
20945 name: c.clone(),
20946 })),
20947 op: crate::ast::BinOp::Eq,
20948 rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20949 qualifier: Some(right_qual.clone()),
20950 name: c,
20951 })),
20952 });
20953 let first = iter.next().expect("at least one col");
20954 Some(iter.fold(first, |acc, pred| Expr::Binary {
20955 lhs: alloc::boxed::Box::new(acc),
20956 op: crate::ast::BinOp::And,
20957 rhs: alloc::boxed::Box::new(pred),
20958 }))
20959 } else if kind == JoinKind::Cross {
20960 None
20961 } else {
20962 return Err(self.err(format!(
20963 "expected ON or USING after {:?} JOIN, got {:?}",
20964 kind,
20965 self.peek()
20966 )));
20967 };
20968 joins.push(FromJoin {
20969 kind,
20970 table,
20971 on,
20972 using_cols,
20973 natural: false,
20974 });
20975 }
20976 Ok(joins)
20977 }
20978
20979 /// Optional alias after an expression or table:
20980 /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
20981 /// accepted (PG-style implicit alias). Returns `None` if the next token
20982 /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
20983 fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
20984 if matches!(self.peek(), Token::As) {
20985 self.advance();
20986 // v7.39 (round 340, V56) — after AS the next token MUST be an
20987 // identifier. This used to return None and "let the caller
20988 // surface the error on the next expectation", but when AS is
20989 // the LAST token there is no next expectation: `SELECT 1 AS`
20990 // parsed clean and silently dropped the alias. PG rejects it.
20991 if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
20992 return self.expect_ident_like().map(Some);
20993 }
20994 return Err(self.err(alloc::format!(
20995 "expected an alias after AS, got {:?}",
20996 self.peek()
20997 )));
20998 }
20999 // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
21000 // grammar reserves a long list of follow-keywords from the
21001 // alias slot. SPG's bareword approximation: skip a small
21002 // set of idents that would otherwise be swallowed as the
21003 // table alias and break trailing clauses like CREATE
21004 // MATERIALIZED VIEW … WITH [NO] DATA or future ON
21005 // CONFLICT WHERE shapes.
21006 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
21007 if is_alias_stopword(s) {
21008 return Ok(None);
21009 }
21010 return Ok(self.expect_ident_like().ok());
21011 }
21012 Ok(None)
21013 }
21014
21015 /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
21016 fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21017 // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
21018 // error beats a stack overflow (an overflow aborts the
21019 // embedding host process).
21020 self.enter_nested()?;
21021 let r = self.parse_expr_inner(min_prec);
21022 self.nest_depth -= 1;
21023 r
21024 }
21025
21026 /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
21027 /// When the upcoming tokens form one, return the underlying
21028 /// operator token and the position just past the closing paren
21029 /// so the binary loop can dispatch on the plain operator.
21030 fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
21031 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
21032 return None;
21033 }
21034 if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
21035 return None;
21036 }
21037 let mut i = self.pos + 2;
21038 // Optional schema qualifier (pg_catalog.<op> etc.).
21039 if matches!(self.tokens.get(i), Some(Token::Ident(_)))
21040 && matches!(self.tokens.get(i + 1), Some(Token::Dot))
21041 {
21042 i += 2;
21043 }
21044 let op_tok = self.tokens.get(i)?.clone();
21045 if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
21046 return None;
21047 }
21048 Some((i + 2, op_tok))
21049 }
21050
21051 /// PG operator symbols that lower onto function calls in
21052 /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
21053 /// family → regexp_like, comparison rung), `^@` (starts_with,
21054 /// comparison rung), `^` (power, tighter than `*`), `#`
21055 /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
21056 /// subset of the OR bits so the subtraction never borrows).
21057 fn try_symbol_operator(
21058 &mut self,
21059 lhs: &Expr,
21060 min_prec: u8,
21061 ) -> Result<Option<Expr>, ParseError> {
21062 enum Sym {
21063 Regex { ci: bool, negated: bool },
21064 Like { ci: bool, negated: bool },
21065 StartsWith,
21066 Power,
21067 Xor,
21068 RangeAdjacent,
21069 }
21070 // v7.39 (IS-precedence knife) — the low-precedence postfix
21071 // predicates ride this existing leaf call (zero new frame slots
21072 // on the nesting chain).
21073 if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
21074 return Ok(Some(e));
21075 }
21076 let (sym, prec): (Sym, u8) = match self.peek() {
21077 Token::Tilde => (
21078 Sym::Regex {
21079 ci: false,
21080 negated: false,
21081 },
21082 5,
21083 ),
21084 Token::TildeStar => (
21085 Sym::Regex {
21086 ci: true,
21087 negated: false,
21088 },
21089 5,
21090 ),
21091 Token::NotTilde => (
21092 Sym::Regex {
21093 ci: false,
21094 negated: true,
21095 },
21096 5,
21097 ),
21098 Token::NotTildeStar => (
21099 Sym::Regex {
21100 ci: true,
21101 negated: true,
21102 },
21103 5,
21104 ),
21105 // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
21106 Token::DoubleTilde => (
21107 Sym::Like {
21108 ci: false,
21109 negated: false,
21110 },
21111 5,
21112 ),
21113 Token::DoubleTildeStar => (
21114 Sym::Like {
21115 ci: true,
21116 negated: false,
21117 },
21118 5,
21119 ),
21120 Token::NotDoubleTilde => (
21121 Sym::Like {
21122 ci: false,
21123 negated: true,
21124 },
21125 5,
21126 ),
21127 Token::NotDoubleTildeStar => (
21128 Sym::Like {
21129 ci: true,
21130 negated: true,
21131 },
21132 5,
21133 ),
21134 Token::CaretAt => (Sym::StartsWith, 5),
21135 // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
21136 // tighter than `* / & |`, which the prec-9 rung preserves —
21137 // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
21138 Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
21139 Token::Caret => (Sym::Power, 9),
21140 // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
21141 // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
21142 Token::Hash => (Sym::Xor, 6),
21143 Token::Adjacent => (Sym::RangeAdjacent, 5),
21144 _ => return Ok(None),
21145 };
21146 if prec < min_prec {
21147 return Ok(None);
21148 }
21149 self.advance();
21150 let rhs = self.parse_expr(prec + 1)?;
21151 let out = match sym {
21152 Sym::Regex { ci, negated } => {
21153 let mut args = alloc::vec![lhs.clone(), rhs];
21154 if ci {
21155 args.push(Expr::Literal(Literal::String(String::from("i"))));
21156 }
21157 maybe_not(
21158 Expr::FunctionCall {
21159 name: String::from("regexp_like"),
21160 args,
21161 },
21162 negated,
21163 )
21164 }
21165 Sym::Like { ci, negated } => Expr::Like {
21166 expr: alloc::boxed::Box::new(lhs.clone()),
21167 pattern: alloc::boxed::Box::new(rhs),
21168 negated,
21169 case_insensitive: ci,
21170 },
21171 Sym::StartsWith => Expr::FunctionCall {
21172 name: String::from("starts_with"),
21173 args: alloc::vec![lhs.clone(), rhs],
21174 },
21175 Sym::Power => Expr::FunctionCall {
21176 name: String::from("power"),
21177 args: alloc::vec![lhs.clone(), rhs],
21178 },
21179 // `#` bitwise XOR — a real operator now (was desugared to
21180 // `(a|b)-(a&b)`, algebraically identical for integers but
21181 // undefined for bit strings; the direct op handles both).
21182 Sym::Xor => Expr::Binary {
21183 lhs: Box::new(lhs.clone()),
21184 op: BinOp::BitXor,
21185 rhs: Box::new(rhs),
21186 },
21187 // range `-|-` "is adjacent to" — lowered to a catalog function.
21188 Sym::RangeAdjacent => Expr::FunctionCall {
21189 name: String::from("range_adjacent"),
21190 args: alloc::vec![lhs.clone(), rhs],
21191 },
21192 };
21193 Ok(Some(out))
21194 }
21195
21196 /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
21197 /// predicates, moved out of the tight postfix-cast loop: PG binds
21198 /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
21199 /// looser than EVERY binary operator (only NOT/AND/OR are looser),
21200 /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
21201 /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
21202 /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
21203 /// when nothing at this position belongs to the family. Out-of-line
21204 /// (`inline(never)`): the caller sits on the per-nesting-level frame
21205 /// chain that MAX_NEST_DEPTH is tuned against.
21206 #[inline(never)]
21207 fn parse_postfix_predicate(
21208 &mut self,
21209 lhs: &Expr,
21210 min_prec: u8,
21211 ) -> Result<Option<Expr>, ParseError> {
21212 // Reached through try_symbol_operator (an existing leaf call of
21213 // the binary loop) so NO new stack slots land on the per-nesting
21214 // frame chain; the lhs clones only when a predicate actually
21215 // consumes it.
21216 match self.peek() {
21217 // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
21218 // comparison family rung 5 (each +1 from the pre-XOR ladder).
21219 Token::Is if min_prec <= 4 => {}
21220 Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
21221 Token::Not
21222 if min_prec <= 5
21223 && matches!(
21224 self.tokens.get(self.pos + 1),
21225 Some(Token::Between | Token::In | Token::Like)
21226 ) => {}
21227 Token::Not | Token::Ident(_)
21228 if min_prec <= 5
21229 && (matches!(self.peek(), Token::Ident(s)
21230 if s.eq_ignore_ascii_case("ilike")
21231 || (self.mysql_dialect
21232 && (s.eq_ignore_ascii_case("regexp")
21233 || s.eq_ignore_ascii_case("rlike")))
21234 || (s.eq_ignore_ascii_case("similar")
21235 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
21236 || (matches!(self.peek(), Token::Not)
21237 && matches!(self.tokens.get(self.pos + 1),
21238 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21239 || (self.mysql_dialect
21240 && (s.eq_ignore_ascii_case("regexp")
21241 || s.eq_ignore_ascii_case("rlike")))
21242 || s.eq_ignore_ascii_case("similar")))) => {}
21243 _ => return Ok(None),
21244 }
21245 let mut expr = lhs.clone();
21246 // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
21247 // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
21248 if min_prec <= 4 {
21249 if matches!(self.peek(), Token::Is) {
21250 self.advance();
21251 let negated = if matches!(self.peek(), Token::Not) {
21252 self.advance();
21253 true
21254 } else {
21255 false
21256 };
21257 // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
21258 // mailrs pg_dump.
21259 if matches!(self.peek(), Token::Distinct) {
21260 self.advance();
21261 if !matches!(self.peek(), Token::From) {
21262 return Err(self.err(format!(
21263 "expected FROM after IS{} DISTINCT, got {:?}",
21264 if negated { " NOT" } else { "" },
21265 self.peek()
21266 )));
21267 }
21268 self.advance();
21269 // Right-hand side: parse at the same precedence
21270 // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
21271 // groups as `x IS DISTINCT FROM (a + b)`.
21272 let rhs = self.parse_expr(5)?;
21273 let op = if negated {
21274 BinOp::IsNotDistinctFrom
21275 } else {
21276 BinOp::IsDistinctFrom
21277 };
21278 expr = Expr::Binary {
21279 op,
21280 lhs: Box::new(expr),
21281 rhs: Box::new(rhs),
21282 };
21283 {
21284 return Ok(Some(expr));
21285 }
21286 }
21287 // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
21288 // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
21289 // Lowers onto pg_is_json(x, kind); NOT wraps the
21290 // call in a logical negation.
21291 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21292 if s.eq_ignore_ascii_case("json"))
21293 {
21294 self.advance(); // JSON
21295 let kind = match self.peek() {
21296 Token::Ident(s) | Token::QuotedIdent(s)
21297 if matches!(
21298 s.to_ascii_lowercase().as_str(),
21299 "value" | "object" | "array" | "scalar"
21300 ) =>
21301 {
21302 let k = s.to_ascii_lowercase();
21303 self.advance();
21304 k
21305 }
21306 _ => "value".to_string(),
21307 };
21308 let call = Expr::FunctionCall {
21309 name: "pg_is_json".to_string(),
21310 args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
21311 };
21312 expr = if negated {
21313 Expr::Unary {
21314 op: UnOp::Not,
21315 expr: Box::new(call),
21316 }
21317 } else {
21318 call
21319 };
21320 {
21321 return Ok(Some(expr));
21322 }
21323 }
21324 // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
21325 // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
21326 // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
21327 {
21328 let form_kw = match self.peek() {
21329 Token::Ident(s) | Token::QuotedIdent(s)
21330 if matches!(
21331 s.to_ascii_uppercase().as_str(),
21332 "NFC" | "NFD" | "NFKC" | "NFKD"
21333 ) && matches!(
21334 self.tokens.get(self.pos + 1),
21335 Some(Token::Ident(n) | Token::QuotedIdent(n))
21336 if n.eq_ignore_ascii_case("normalized")
21337 ) =>
21338 {
21339 Some(s.to_ascii_uppercase())
21340 }
21341 _ => None,
21342 };
21343 let bare_normalized = form_kw.is_none()
21344 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21345 if s.eq_ignore_ascii_case("normalized"));
21346 if form_kw.is_some() || bare_normalized {
21347 if form_kw.is_some() {
21348 self.advance(); // form keyword
21349 }
21350 self.advance(); // NORMALIZED
21351 let mut args = alloc::vec![expr];
21352 if let Some(f) = form_kw {
21353 args.push(Expr::Literal(Literal::String(f)));
21354 }
21355 let call = Expr::FunctionCall {
21356 name: "is_normalized".to_string(),
21357 args,
21358 };
21359 expr = if negated {
21360 Expr::Unary {
21361 op: UnOp::Not,
21362 expr: Box::new(call),
21363 }
21364 } else {
21365 call
21366 };
21367 {
21368 return Ok(Some(expr));
21369 }
21370 }
21371 }
21372 // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
21373 // three-valued boolean tests. IS TRUE/FALSE never
21374 // return NULL, so they lower to CASE forms whose
21375 // ELSE catches the NULL branch; IS UNKNOWN on a
21376 // boolean is exactly IS NULL.
21377 if matches!(self.peek(), Token::True | Token::False)
21378 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
21379 {
21380 let tok = self.advance();
21381 let test = match tok {
21382 Token::True => Some(true),
21383 Token::False => Some(false),
21384 _ => None, // UNKNOWN
21385 };
21386 // v7.39 (round 328, V45) — kept as what the user
21387 // wrote. These used to be lowered here into `CASE` /
21388 // `IS NULL`; the semantics were right but the AST no
21389 // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
21390 // was echoed back as
21391 // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
21392 expr = Expr::BoolTest {
21393 expr: Box::new(expr),
21394 value: test,
21395 negated,
21396 };
21397 {
21398 return Ok(Some(expr));
21399 }
21400 }
21401 if !matches!(self.peek(), Token::Null) {
21402 return Err(self.err(format!(
21403 "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21404 if negated { " NOT" } else { "" },
21405 self.peek()
21406 )));
21407 }
21408 self.advance();
21409 expr = Expr::IsNull {
21410 expr: Box::new(expr),
21411 negated,
21412 };
21413 {
21414 return Ok(Some(expr));
21415 }
21416 }
21417 }
21418 // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21419 if min_prec <= 5 {
21420 // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21421 // Look one token ahead so a stray `NOT` not followed by any of
21422 // these flows through to the early return below untouched.
21423 let negated = if matches!(self.peek(), Token::Not) {
21424 let next = self.tokens.get(self.pos + 1);
21425 matches!(next, Some(Token::Between | Token::In | Token::Like))
21426 || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21427 || (self.mysql_dialect
21428 && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21429 || s.eq_ignore_ascii_case("similar"))
21430 } else {
21431 false
21432 };
21433 if negated {
21434 self.advance();
21435 }
21436 if matches!(self.peek(), Token::Between) {
21437 expr = self.parse_between_tail(expr, negated)?;
21438 {
21439 return Ok(Some(expr));
21440 }
21441 }
21442 if matches!(self.peek(), Token::In) {
21443 if self.suppress_in_tail && !negated {
21444 // POSITION(sub IN str) — IN belongs to the
21445 // enclosing function syntax; stop here.
21446 {
21447 return Ok(None);
21448 }
21449 }
21450 expr = self.parse_in_tail(expr, negated)?;
21451 {
21452 return Ok(Some(expr));
21453 }
21454 }
21455 // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21456 // lowers onto the internal __similar_to(expr, pat[, esc]) call
21457 // (the SQL→regex transform runs inside, in the backtracking-
21458 // friendly shape SPG's matcher needs).
21459 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21460 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21461 {
21462 self.advance(); // SIMILAR
21463 self.advance(); // TO
21464 let pattern = self.parse_expr(6)?;
21465 let mut args = alloc::vec![expr, pattern];
21466 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21467 self.advance();
21468 args.push(self.parse_expr(6)?);
21469 }
21470 let call = Expr::FunctionCall {
21471 name: "__similar_to".to_string(),
21472 args,
21473 };
21474 expr = maybe_not(call, negated);
21475 {
21476 return Ok(Some(expr));
21477 }
21478 }
21479 if matches!(self.peek(), Token::Like) {
21480 self.advance();
21481 // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21482 if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21483 expr = q;
21484 {
21485 return Ok(Some(expr));
21486 }
21487 }
21488 // Pattern at the same precedence as other comparison RHSes —
21489 // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21490 let mut pattern = self.parse_expr(6)?;
21491 // `ESCAPE 'c'` — rewrite a literal pattern to the
21492 // default backslash escape at parse time. Custom
21493 // escapes on non-literal patterns would need
21494 // matcher support; error honestly.
21495 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21496 self.advance();
21497 let esc = self.parse_expr(6)?;
21498 pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21499 }
21500 expr = Expr::Like {
21501 expr: Box::new(expr),
21502 pattern: Box::new(pattern),
21503 negated,
21504 case_insensitive: false,
21505 };
21506 {
21507 return Ok(Some(expr));
21508 }
21509 }
21510 // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21511 // keyword reaches us as a plain identifier.
21512 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21513 self.advance();
21514 if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21515 expr = q;
21516 {
21517 return Ok(Some(expr));
21518 }
21519 }
21520 let pattern = self.parse_expr(6)?;
21521 expr = Expr::Like {
21522 expr: Box::new(expr),
21523 pattern: Box::new(pattern),
21524 negated,
21525 case_insensitive: true,
21526 };
21527 {
21528 return Ok(Some(expr));
21529 }
21530 }
21531 // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21532 // operator (RLIKE is the alias). It is a keyword, not `~`, and
21533 // matches case-insensitively under the default collation, so it
21534 // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21535 // `~*` operator uses, wrapped in NOT when negated.
21536 if self.mysql_dialect
21537 && matches!(self.peek(), Token::Ident(s)
21538 if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21539 {
21540 self.advance();
21541 let pattern = self.parse_expr(6)?;
21542 let call = Expr::FunctionCall {
21543 name: String::from("regexp_like"),
21544 args: alloc::vec![
21545 expr,
21546 pattern,
21547 Expr::Literal(Literal::String(String::from("i"))),
21548 ],
21549 };
21550 return Ok(Some(maybe_not(call, negated)));
21551 }
21552 }
21553 let _ = expr;
21554 Ok(None)
21555 }
21556
21557 fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21558 let mut lhs = self.parse_unary()?;
21559 let mut chain_len = 0usize;
21560 loop {
21561 // OPERATOR([schema.]op) reduces to its underlying
21562 // operator token before the normal dispatch.
21563 let explicit = self.peek_explicit_operator();
21564 let dispatch = match &explicit {
21565 Some((_, tok)) => self.binop_here(tok),
21566 None => self.binop_here(self.peek()),
21567 };
21568 let Some((op, prec)) = dispatch else {
21569 // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21570 // of the symbol family. `binop_here` answers None for them
21571 // because they lower onto function calls rather than a
21572 // BinOp, and the fallback below reads `self.peek()` — the
21573 // word OPERATOR, not the operator. `pg_dump` writes every
21574 // catalog predicate this way, so its first query failed
21575 // and no dump ran:
21576 //
21577 // AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21578 //
21579 // Collapsing the wrapper to the operator it names puts the
21580 // token where the fallback already looks.
21581 if let Some((next, op_tok)) = explicit {
21582 self.tokens.splice(self.pos..next, [op_tok]);
21583 }
21584 if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21585 lhs = e;
21586 chain_len += 1;
21587 if chain_len > MAX_BINARY_CHAIN {
21588 return Err(self.err(alloc::format!(
21589 "more than {MAX_BINARY_CHAIN} chained binary operators"
21590 )));
21591 }
21592 continue;
21593 }
21594 break;
21595 };
21596 if prec < min_prec {
21597 break;
21598 }
21599 // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21600 // iteratively but evaluates and drops recursively;
21601 // depth beyond the budget overflows worker stacks.
21602 chain_len += 1;
21603 if chain_len > MAX_BINARY_CHAIN {
21604 return Err(self.err(alloc::format!(
21605 "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21606 )));
21607 }
21608 match explicit {
21609 Some((end_pos, _)) => self.pos = end_pos,
21610 None => {
21611 self.advance();
21612 }
21613 }
21614 // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21615 // ANY is a bare ident; ALL is a reserved Token. Both
21616 // require an immediate `(` to disambiguate from
21617 // identifier columns named `any` / `all`.
21618 let any_kind = match self.peek() {
21619 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21620 Some(false)
21621 }
21622 Token::Ident(s) | Token::QuotedIdent(s)
21623 if (s.eq_ignore_ascii_case("any")
21624 || s.eq_ignore_ascii_case("some")
21625 || s.eq_ignore_ascii_case("all"))
21626 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21627 {
21628 Some(!s.eq_ignore_ascii_case("all"))
21629 }
21630 _ => None,
21631 };
21632 if let Some(is_any) = any_kind {
21633 lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21634 continue;
21635 }
21636 let rhs = self.parse_expr(prec + 1)?;
21637 lhs = Expr::Binary {
21638 lhs: Box::new(lhs),
21639 op,
21640 rhs: Box::new(rhs),
21641 };
21642 }
21643 Ok(lhs)
21644 }
21645
21646 /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
21647 /// and the array form.
21648 ///
21649 /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
21650 /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
21651 /// this block's `Expr` temporaries and four `format!` sites slots in
21652 /// that frame on every level of `((((1))))`, which never reaches it.
21653 #[inline(never)]
21654 fn parse_any_all_rhs(
21655 &mut self,
21656 lhs: Expr,
21657 op: BinOp,
21658 is_any: bool,
21659 ) -> Result<Expr, ParseError> {
21660 self.advance(); // ident
21661 self.advance(); // (
21662 // `x op ANY (SELECT …)` — the quantified-subquery
21663 // form. `= ANY` is exactly IN; the other operators
21664 // lower onto EXISTS over the subquery as a derived
21665 // table, comparing against its single projection
21666 // aliased __v (x's columns resolve correlated).
21667 // ALL is the negated-EXISTS complement; a NULL
21668 // element makes PG return NULL where this lowering
21669 // returns true — the NOT NULL column case (the
21670 // practical one) is exact.
21671 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
21672 // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
21673 // legal PG too (round-151 sibling). Out-of-line
21674 // (#[inline(never)] helper) — this sits on
21675 // parse_expr's recursive frame and the two-armed
21676 // SELECT temporary blew the nesting-budget stack.
21677 let mut sub = self.parse_any_all_select_body()?;
21678 if !matches!(self.peek(), Token::RParen) {
21679 return Err(self.err(alloc::format!(
21680 "expected ')' after ANY/ALL subquery, got {:?}",
21681 self.peek()
21682 )));
21683 }
21684 self.advance();
21685 if sub.items.len() != 1 {
21686 return Err(self.err(alloc::format!(
21687 "ANY/ALL subquery must return one column, got {}",
21688 sub.items.len()
21689 )));
21690 }
21691 if is_any && matches!(op, BinOp::Eq) {
21692 return Ok(Expr::InSubquery {
21693 expr: Box::new(lhs),
21694 subquery: Box::new(sub),
21695 negated: false,
21696 });
21697 }
21698 // The engine's subquery resolvers materialise
21699 // the single-column result into an ARRAY the
21700 // existing AnyAll three-valued eval consumes.
21701 return Ok(Expr::AnyAll {
21702 expr: Box::new(lhs),
21703 op,
21704 array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
21705 is_any,
21706 });
21707 }
21708 let arr = self.parse_expr(0)?;
21709 if !matches!(self.peek(), Token::RParen) {
21710 return Err(self.err(alloc::format!(
21711 "expected ')' after ANY/ALL argument, got {:?}",
21712 self.peek()
21713 )));
21714 }
21715 self.advance();
21716 Ok(Expr::AnyAll {
21717 expr: Box::new(lhs),
21718 op,
21719 array: Box::new(arr),
21720 is_any,
21721 })
21722 }
21723
21724 /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
21725 /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
21726 #[inline(never)]
21727 fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
21728 self.advance();
21729 let e = self.parse_expr(9)?;
21730 Ok(build_center_call(e))
21731 }
21732
21733 /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
21734 /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
21735 /// unary minus.
21736 ///
21737 /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
21738 /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
21739 /// the Expr-sized local stays out of that frame.
21740 #[inline(never)]
21741 fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21742 self.advance();
21743 let e = self.parse_expr(9)?;
21744 Ok(Expr::FunctionCall {
21745 name: alloc::string::String::from(name),
21746 args: alloc::vec![e],
21747 })
21748 }
21749
21750 /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
21751 /// (horizontal). Out-of-line from `parse_unary` (frame budget).
21752 #[inline(never)]
21753 fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
21754 self.advance();
21755 let e = self.parse_expr(9)?;
21756 Ok(Expr::FunctionCall {
21757 name: alloc::string::String::from(if vertical {
21758 "isvertical"
21759 } else {
21760 "ishorizontal"
21761 }),
21762 args: alloc::vec![e],
21763 })
21764 }
21765
21766 /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
21767 /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
21768 /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
21769 #[inline(never)]
21770 fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
21771 self.advance();
21772 let e = self.parse_expr(9)?;
21773 Ok(Expr::Cast {
21774 expr: Box::new(e),
21775 target: CastTarget::Named("binary".to_string()),
21776 })
21777 }
21778
21779 /// The prefix operators that share one shape: take the token, parse
21780 /// an operand at `prec`, wrap it.
21781 ///
21782 /// `#[inline(never)]`, and one function instead of five arms, for the
21783 /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
21784 /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
21785 /// debug build gives EVERY arm's locals a slot in the frame, whichever
21786 /// arm runs. `((((1))))` reaches none of these arms and was carrying
21787 /// five `Expr`-sized locals per level for them anyway.
21788 #[inline(never)]
21789 fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
21790 self.advance();
21791 let e = self.parse_expr(prec)?;
21792 Ok(Expr::Unary {
21793 op,
21794 expr: Box::new(e),
21795 })
21796 }
21797
21798 /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
21799 /// and separate from it because of the literal folding below and the
21800 /// `format!` temporaries that folding needs.
21801 #[inline(never)]
21802 fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
21803 self.advance();
21804 // v7.39 (round 549) — fold the sign into an integer literal that
21805 // only fits once it is negative.
21806 //
21807 // `9223372036854775808` is one past i64::MAX, so the lexer hands
21808 // it over as a NUMERIC and `-` on a numeric stays numeric. PG
21809 // folds the sign first, so `-9223372036854775808` is a bigint
21810 // there — and `-9223372036854775808 - 1` raises "bigint out of
21811 // range" where SPG quietly answered -9223372036854775809, a value
21812 // no bigint can hold. The arithmetic itself was already checked;
21813 // only the literal's type was wrong.
21814 if let Token::Numeric(lit) = self.peek()
21815 && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
21816 {
21817 self.advance();
21818 return Ok(Expr::Literal(Literal::Integer(folded)));
21819 }
21820 // Unary minus binds tighter than `*`/`/` (now at prec 7 after
21821 // `<->` slotted into 5 and arithmetic shifted up).
21822 let e = self.parse_expr(9)?;
21823 Ok(Expr::Unary {
21824 op: UnOp::Neg,
21825 expr: Box::new(e),
21826 })
21827 }
21828
21829 /// tsquery `!!` prefix negation, lowered to the catalog function.
21830 /// Binds like unary minus. Out-of-line for the frame reason on
21831 /// `parse_unary_op`.
21832 #[inline(never)]
21833 fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
21834 self.advance();
21835 let e = self.parse_expr(9)?;
21836 Ok(Expr::FunctionCall {
21837 name: String::from("tsquery_not"),
21838 args: alloc::vec![e],
21839 })
21840 }
21841
21842 fn parse_unary(&mut self) -> Result<Expr, ParseError> {
21843 match self.peek() {
21844 // NOT binds tighter than AND / XOR / OR but looser than
21845 // comparisons — its operand takes everything ≥ the comparison
21846 // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
21847 // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
21848 // was rung 3, behaviour-identical when 3 was unused; AND now
21849 // occupies 3, so this must be 4 to keep NOT tighter than AND.)
21850 Token::Not => self.parse_unary_op(UnOp::Not, 4),
21851 // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
21852 // The body is out-of-line: `parse_unary` is one of the three
21853 // frames the parser's MAX_NEST_DEPTH is tuned against, and an
21854 // inline arm here overflowed the native stack in
21855 // `nesting_budget_errors_cleanly` — the guard test caught it,
21856 // exactly as the eval-side cliff did in rounds 346 and 351.
21857 Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
21858 self.parse_binary_prefix()
21859 }
21860 // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
21861 // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
21862 // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
21863 Token::Bang => self.parse_unary_op(UnOp::Not, 9),
21864 Token::Minus => self.parse_prefix_minus(),
21865 // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
21866 // worked only because the lexer reads it as one signed literal;
21867 // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
21868 // PG18 and MariaDB take all of them. Binds like unary minus.
21869 Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
21870 // Bitwise NOT binds like unary minus.
21871 Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
21872 // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
21873 // "center of" operator; desugars to center(x). The whole arm
21874 // is out-of-line: parse_unary sits on the per-nesting-level
21875 // frame chain that MAX_NEST_DEPTH is tuned against, so no
21876 // Expr-sized local may live in this frame.
21877 Token::TsMatch => self.parse_prefix_center(),
21878 // v7.39 (round 508) — the prefix operators that are named
21879 // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
21880 // is length. Out-of-line for the same nesting-frame reason as
21881 // parse_prefix_center — parse_unary sits on the recursive cycle
21882 // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
21883 // live in this frame.
21884 Token::At => self.parse_prefix_call("abs"),
21885 Token::Hash => self.parse_prefix_call("npoints"),
21886 Token::AtMinusAt => self.parse_prefix_call("length"),
21887 // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
21888 // "is horizontal" (lseg / line); desugars to the existing
21889 // isvertical()/ishorizontal() functions. Out-of-line for the
21890 // same nesting-frame reason as parse_prefix_center.
21891 Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
21892 Token::GeomHoriz => self.parse_prefix_geom_axis(false),
21893 Token::DoubleBang => self.parse_prefix_tsquery_not(),
21894 _ => self.parse_atom(),
21895 }
21896 }
21897
21898 /// Parse a parenthesised scalar subquery body after the caller has consumed
21899 /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
21900 /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
21901 /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
21902 /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
21903 /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
21904 /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
21905 /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
21906 /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
21907 /// which sits on the recursive nesting-budget cycle (a few extra bytes there
21908 /// tips the deep-nesting test into a stack overflow).
21909 #[inline(never)]
21910 fn array_subquery_ahead(&self) -> bool {
21911 if !matches!(self.peek(), Token::LParen) {
21912 return false;
21913 }
21914 matches!(
21915 self.tokens.get(self.pos + 1),
21916 Some(Token::Select | Token::Values)
21917 ) || matches!(
21918 self.tokens.get(self.pos + 1),
21919 Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
21920 )
21921 }
21922
21923 /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
21924 /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
21925 /// locals stay off parse_atom's recursive frame (round 105).
21926 #[inline(never)]
21927 fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
21928 self.advance(); // consume `[`
21929 let mut items: Vec<Expr> = Vec::new();
21930 if !matches!(self.peek(), Token::RBracket) {
21931 loop {
21932 // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
21933 // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
21934 if matches!(self.peek(), Token::LBracket) {
21935 items.push(self.parse_array_bracket_body()?);
21936 } else {
21937 items.push(self.parse_expr(0)?);
21938 }
21939 match self.peek() {
21940 Token::Comma => {
21941 self.advance();
21942 }
21943 Token::RBracket => break,
21944 other => {
21945 return Err(self.err(alloc::format!(
21946 "expected ',' or ']' in ARRAY literal, got {other:?}"
21947 )));
21948 }
21949 }
21950 }
21951 }
21952 self.advance(); // consume `]`
21953 Ok(Expr::Array(items))
21954 }
21955
21956 /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
21957 /// is already consumed; the current token is `(`. Desugars to a scalar
21958 /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
21959 /// the subquery's single-column rows in order — reusing the existing
21960 /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
21961 /// keeps the large `Statement` local off parse_atom's recursive frame.
21962 #[inline(never)]
21963 fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
21964 self.advance(); // consume `(`
21965 let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
21966 if w.eq_ignore_ascii_case("with"));
21967 let sub = if is_with {
21968 self.advance(); // WITH
21969 self.parse_with_cte_then_select()?
21970 } else {
21971 self.parse_select_stmt()?
21972 };
21973 if !matches!(self.peek(), Token::RParen) {
21974 return Err(self.err(alloc::format!(
21975 "expected ')' to close ARRAY(subquery), got {:?}",
21976 self.peek()
21977 )));
21978 }
21979 self.advance(); // consume `)`
21980 // Reuse the parser to build the array_agg wrapper from the subquery's
21981 // canonical text — avoids hand-constructing the derived-table AST.
21982 let wrapper = alloc::format!(
21983 "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
21984 );
21985 let stmt = parse_statement(&wrapper)
21986 .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
21987 let Statement::Select(sel) = stmt else {
21988 return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
21989 };
21990 Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
21991 }
21992
21993 #[inline(never)]
21994 fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
21995 let inner = if is_with {
21996 self.advance(); // WITH
21997 self.parse_with_cte_then_select()?
21998 } else {
21999 self.parse_select_stmt()?
22000 };
22001 match self.advance() {
22002 Token::RParen => {
22003 let Statement::Select(s) = inner else {
22004 return Err(ParseError {
22005 message: "scalar subquery body must be a SELECT".into(),
22006 token_pos: self.consumed_pos(),
22007 });
22008 };
22009 Ok(Expr::ScalarSubquery(Box::new(s)))
22010 }
22011 other => Err(ParseError {
22012 message: format!("expected ')' after scalar subquery, got {other:?}"),
22013 token_pos: self.consumed_pos(),
22014 }),
22015 }
22016 }
22017
22018 /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
22019 /// literals. The lexer splits them into an ident + string; recombine
22020 /// here. Out-of-line and returning `Option` so `parse_atom` — the
22021 /// recursive frame the 768 KiB stack budget is tuned against — pays no
22022 /// frame for the `body` / `bits` strings and their char loops (the
22023 /// round-367 frame cliff, M20).
22024 #[inline(never)]
22025 fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
22026 let is_hex = match self.peek() {
22027 Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
22028 Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
22029 _ => return None,
22030 };
22031 if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
22032 return None;
22033 }
22034 self.advance();
22035 let Token::String(body) = self.advance() else {
22036 unreachable!("guarded above");
22037 };
22038 // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
22039 // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
22040 // (hex pairs, even count required — MariaDB errors on an odd
22041 // count); `b'1010'` packs its bits big-endian, left-padded to a
22042 // byte. Lower both onto the bytea cast.
22043 if self.mysql_dialect {
22044 if is_hex {
22045 if body.len() % 2 == 1 {
22046 return Some(Err(self.err(alloc::format!(
22047 "invalid hex string literal X'{body}': odd digit count"
22048 ))));
22049 }
22050 for c in body.chars() {
22051 if !c.is_ascii_hexdigit() {
22052 return Some(Err(
22053 self.err(alloc::format!("invalid hexadecimal digit {c:?} in X'…'"))
22054 ));
22055 }
22056 }
22057 return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
22058 }
22059 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22060 return Some(Err(
22061 self.err(alloc::format!("invalid binary digit {bad:?} in b'…'"))
22062 ));
22063 }
22064 return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
22065 }
22066 let bits = if is_hex {
22067 let mut out = String::with_capacity(body.len() * 4);
22068 for c in body.chars() {
22069 let Some(d) = c.to_digit(16) else {
22070 return Some(Err(self.err(alloc::format!(
22071 "invalid hexadecimal digit {c:?} in X'…' bit string"
22072 ))));
22073 };
22074 out.push_str(&alloc::format!("{d:04b}"));
22075 }
22076 out
22077 } else {
22078 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22079 return Some(Err(self.err(alloc::format!(
22080 "invalid binary digit {bad:?} in B'…' bit string"
22081 ))));
22082 }
22083 body
22084 };
22085 // Route through the postfix-cast loop so a chained cast like
22086 // `B'1010'::int` attaches onto the implicit `::bit` cast instead
22087 // of erroring at the `::`.
22088 // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
22089 // literal keeps its exact length, while an explicit `::bit` cast is
22090 // bit(1) with pad/truncate semantics (PG).
22091 Some(self.finish_postfix_casts(Expr::Cast {
22092 expr: Box::new(Expr::Literal(Literal::String(bits))),
22093 target: CastTarget::Named("__bit_literal".to_string()),
22094 }))
22095 }
22096
22097 fn parse_atom(&mut self) -> Result<Expr, ParseError> {
22098 if let Some(res) = self.try_parse_bit_string_literal() {
22099 return res;
22100 }
22101 let tok_pos = self.pos;
22102 match self.advance() {
22103 Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
22104 Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
22105 // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
22106 // carrying the source mantissa + scale so no precision is lost. A
22107 // literal too wide for i128 falls back to double precision.
22108 // Out-of-line (#[inline(never)]) — this arm sits on the
22109 // parse_expr recursion chain; its expansion locals must not
22110 // widen the recursive frame (debug frame-cliff discipline).
22111 Token::Numeric(s) => match numeric_token_to_literal(s) {
22112 Ok(lit) => Ok(Expr::Literal(lit)),
22113 Err(msg) => Err(self.err(msg)),
22114 },
22115 Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
22116 // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
22117 // (the lexer only emits this token in the MySQL dialect). Lower
22118 // onto the existing bytea cast; out-of-line to keep this arm off
22119 // the parse recursion frame.
22120 Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
22121 Token::True => Ok(Expr::Literal(Literal::Bool(true))),
22122 Token::False => Ok(Expr::Literal(Literal::Bool(false))),
22123 Token::Null => Ok(Expr::Literal(Literal::Null)),
22124 // v6.1.1 — `$N` placeholder. The actual Value lookup
22125 // happens in the engine eval path against the prepared-
22126 // statement bind buffer.
22127 Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
22128 Token::LParen => {
22129 // v4.10: `(SELECT ...)` in expression position is a
22130 // scalar subquery; otherwise it's a parenthesised
22131 // expression. Peek for SELECT keyword to dispatch.
22132 // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
22133 // lexes as Ident("with") (not a reserved token). The subquery body
22134 // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
22135 // so its large `Statement` local stays out of parse_atom's stack
22136 // frame — parse_atom is on the recursive `((…))` cycle and the
22137 // nesting budget is tuned to its frame size).
22138 let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22139 if s.eq_ignore_ascii_case("with"));
22140 if matches!(self.peek(), Token::Select) || is_with {
22141 self.parse_paren_scalar_subquery(is_with)
22142 } else {
22143 let e = self.parse_expr(0)?;
22144 // `(a, b, …)` — a row constructor. Valid only
22145 // in front of a comparison operator or [NOT]
22146 // IN; both expand at parse time (lexicographic
22147 // comparison / OR'd row equalities).
22148 if matches!(self.peek(), Token::Comma) {
22149 let mut row = alloc::vec![e];
22150 while matches!(self.peek(), Token::Comma) {
22151 self.advance();
22152 row.push(self.parse_expr(0)?);
22153 }
22154 if !matches!(self.peek(), Token::RParen) {
22155 return Err(self.err(alloc::format!(
22156 "expected ')' after row constructor, got {:?}",
22157 self.peek()
22158 )));
22159 }
22160 self.advance();
22161 // A bare `(a, b, …)` row constructor can carry postfix
22162 // (`::text`, `.field`) just like `ROW(a, b, …)`; the
22163 // early return here skips parse_atom's tail postfix
22164 // pass, so fold casts in explicitly. For the
22165 // comparison / predicate forms nothing postfix follows,
22166 // so this is a no-op.
22167 return self
22168 .parse_row_comparison_tail(row)
22169 .and_then(|e| self.finish_postfix_casts(e));
22170 }
22171 match self.advance() {
22172 Token::RParen => Ok(e),
22173 other => Err(ParseError {
22174 message: format!("expected ')', got {other:?}"),
22175 token_pos: self.consumed_pos(),
22176 }),
22177 }
22178 }
22179 }
22180 Token::LBracket => self.parse_vector_literal_body(),
22181 Token::Extract => self.parse_extract_atom(),
22182 Token::Interval => self.parse_interval_atom(),
22183 // `LEFT` / `RIGHT` are reserved-keyword tokens because the
22184 // grammar dedicates arms for `LEFT [OUTER] JOIN` /
22185 // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
22186 // expression position calling the PG `left(string, n)` /
22187 // `right(string, n)` function; rebuild the AST as a regular
22188 // function call so the engine's apply_function dispatch picks
22189 // it up. Delegated to a #[inline(never)] helper so its locals
22190 // don't bloat this recursive `parse_atom` frame (the nesting
22191 // budget in `enter_nested` is tuned to parse_atom's size).
22192 Token::Left if matches!(self.peek(), Token::LParen) => {
22193 self.parse_lr_string_function_call("left")
22194 }
22195 Token::Right if matches!(self.peek(), Token::LParen) => {
22196 self.parse_lr_string_function_call("right")
22197 }
22198 // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
22199 // token; we match on the bare ident. NOT is a token
22200 // (consumed in the comparison rung), but `EXISTS (...)`
22201 // at the top of an expression starts here.
22202 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
22203 self.parse_exists_atom(false)
22204 }
22205 // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
22206 // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
22207 // CASE is a bare ident; we dispatch on lowercase match.
22208 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
22209 self.parse_case_atom()
22210 }
22211 // v7.37.17 (17.6 siblings) — PG typed datetime literals:
22212 // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
22213 // '…'`. Lower onto the ::cast node so the existing
22214 // runtime text→date/timestamp paths do the parsing. The
22215 // string must follow immediately, else the ident stays a
22216 // plain column reference.
22217 Token::Ident(s)
22218 if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
22219 && matches!(self.peek(), Token::String(_)) =>
22220 {
22221 let target =
22222 typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
22223 let Token::String(lit) = self.advance() else {
22224 unreachable!("peek guaranteed a string token");
22225 };
22226 Ok(Expr::Cast {
22227 expr: Box::new(Expr::Literal(Literal::String(lit))),
22228 target,
22229 })
22230 }
22231 // v7.39 (round 221) — the SQL-standard long spellings:
22232 // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
22233 // TIME ZONE '…'`. Consume the modifier and lower to the same
22234 // typed-literal cast (`timetz` / `timestamptz` for WITH).
22235 Token::Ident(s)
22236 if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
22237 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
22238 || w.eq_ignore_ascii_case("without"))
22239 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
22240 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
22241 && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
22242 {
22243 let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
22244 self.advance(); // WITH / WITHOUT
22245 self.advance(); // TIME
22246 self.advance(); // ZONE
22247 let Token::String(lit) = self.advance() else {
22248 unreachable!("guard checked a string token");
22249 };
22250 let base = s.to_ascii_lowercase();
22251 let target = match (base.as_str(), with_tz) {
22252 ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
22253 ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
22254 (_, true) => CastTarget::Timestamptz,
22255 (_, false) => CastTarget::Timestamp,
22256 };
22257 Ok(Expr::Cast {
22258 expr: Box::new(Expr::Literal(Literal::String(lit))),
22259 target,
22260 })
22261 }
22262 // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
22263 // gathers the subquery's single-column rows (in its row order)
22264 // into an array. Desugared to `array_agg` over the subquery as a
22265 // derived table; out-of-line to keep parse_atom's frame small (it
22266 // sits on the recursive nesting-budget cycle).
22267 Token::Ident(s) | Token::QuotedIdent(s)
22268 if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
22269 {
22270 self.parse_array_subquery()
22271 }
22272 // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
22273 // is not a reserved token; we match by case-insensitive
22274 // ident. The opening `[` must follow immediately. v7.39 (read01
22275 // round 105) — the body moved out-of-line so its `Vec`/loop locals
22276 // leave parse_atom's frame (which sits on the nesting-budget cycle).
22277 Token::Ident(s) | Token::QuotedIdent(s)
22278 if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
22279 {
22280 self.parse_array_literal_body()
22281 }
22282 // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
22283 // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
22284 // We special-case before the generic ident dispatch so
22285 // the AGAINST clause never reaches the function-call
22286 // loop (which would mis-read `(cols) AGAINST` as a
22287 // call with no trailing modifier). The shape is
22288 // rewritten to a Boolean OR over per-column
22289 // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
22290 // term)` so the existing FTS evaluator handles
22291 // semantics — the fulltext-GIN built at CREATE TABLE
22292 // time is currently a "real index that survives dump
22293 // round-trip"; the planner hook that actually uses
22294 // it for posting-list intersection lands in a later
22295 // sub-phase (Phase 2.2b) without touching this surface.
22296 Token::Ident(s) | Token::QuotedIdent(s)
22297 if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
22298 {
22299 self.parse_match_against_atom()
22300 }
22301 Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
22302 // v7.37.43-T4 — PG-unreserved keywords are legal column /
22303 // alias names in expression context too. `release` appears
22304 // in sentori `0003_partition_events.sql` as both a column
22305 // reference (SELECT … release …) and an INSERT column list
22306 // entry. Mirrors `expect_ident_like`'s expansion of the
22307 // identifier set.
22308 other if unreserved_keyword_text(&other).is_some() => {
22309 let s = unreserved_keyword_text(&other).unwrap();
22310 self.finish_ident_atom(s)
22311 }
22312 // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
22313 // only inside `SET` before, so `SELECT @@autocommit` — which
22314 // every MySQL connector asks at handshake — was a parse error.
22315 // MariaDB accepts the bare, `@@session.` and `@@global.`
22316 // spellings alike and answers from the session's own value.
22317 Token::SessionVar(v) => {
22318 // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
22319 // has nothing to do with a `@@` engine setting: its own
22320 // per-session namespace, and an unset one reads NULL instead
22321 // of raising. Stripping every `@` (as this did) made `@x` and
22322 // `@@x` the same node, so `SELECT @x` answered "Unknown
22323 // system variable".
22324 Ok(variable_ref_atom(&v))
22325 }
22326 other => Err(ParseError {
22327 message: format!("unexpected token {other:?} in expression"),
22328 token_pos: tok_pos,
22329 }),
22330 }
22331 // After parsing the atom, fold any postfix `::vector` casts.
22332 .and_then(|atom| self.finish_postfix_casts(atom))
22333 }
22334
22335 /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
22336 /// Both bind tighter than any binary op.
22337 /// Shared cast-target parser for postfix `::TYPE` and the
22338 /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
22339 /// If the next tokens are `( N )`, consume them and return the canonical
22340 /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
22341 /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
22342 fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
22343 if !matches!(self.peek(), Token::LParen) {
22344 return None;
22345 }
22346 self.advance(); // (
22347 let n = match self.advance() {
22348 Token::Integer(n) => n,
22349 _ => return Some(base.to_string()), // malformed → drop precision
22350 };
22351 if matches!(self.peek(), Token::RParen) {
22352 self.advance();
22353 }
22354 Some(alloc::format!("{base}({n})"))
22355 }
22356
22357 fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
22358 // r1052 — `::pg_catalog.regproc` and friends: pg_dump
22359 // schema-qualifies every cast target, and `pg_catalog.X` names
22360 // exactly the builtin type X. Consume the qualifier and let
22361 // the ordinary target parse decide.
22362 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
22363 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
22364 {
22365 self.advance();
22366 self.advance();
22367 }
22368 let target = match self.advance() {
22369 Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
22370 "int" | "integer" | "int4" => {
22371 if matches!(self.peek(), Token::LBracket)
22372 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22373 {
22374 self.advance();
22375 self.advance();
22376 CastTarget::IntArray
22377 } else {
22378 CastTarget::Int
22379 }
22380 }
22381 "bigint" | "int8" => {
22382 if matches!(self.peek(), Token::LBracket)
22383 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22384 {
22385 self.advance();
22386 self.advance();
22387 CastTarget::BigIntArray
22388 } else {
22389 CastTarget::BigInt
22390 }
22391 }
22392 "float" | "double" => CastTarget::Float,
22393 "text" => {
22394 // v7.10.11 — `::TEXT[]` widens to TextArray.
22395 if matches!(self.peek(), Token::LBracket)
22396 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22397 {
22398 self.advance();
22399 self.advance();
22400 CastTarget::TextArray
22401 } else {
22402 CastTarget::Text
22403 }
22404 }
22405 "bool" | "boolean" => CastTarget::Bool,
22406 "vector" => CastTarget::Vector,
22407 "date" => CastTarget::Date,
22408 // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22409 // seconds precision through the Named path (the engine rounds
22410 // the sub-second field); bare `::timestamp` keeps the fast arm.
22411 "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22412 Some(named) => CastTarget::Named(named),
22413 None => CastTarget::Timestamp,
22414 },
22415 "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22416 Some(named) => CastTarget::Named(named),
22417 None => CastTarget::Timestamptz,
22418 },
22419 "interval" => CastTarget::Interval,
22420 "json" => CastTarget::Json,
22421 "jsonb" => CastTarget::Jsonb,
22422 // v7.39 (round 694) — these have dedicated CastTarget
22423 // variants, so they never reached the postfix `[]` handling
22424 // further down and `::regtype[]` was a SYNTAX error at the
22425 // `]`. PG has an array type for every scalar; take the
22426 // suffix here and hand the canonical `<ty>_array` name to
22427 // the engine, the same shape every other array cast uses.
22428 "regtype" if self.peek_postfix_array_brackets() => {
22429 self.advance();
22430 self.advance();
22431 CastTarget::Named(alloc::string::String::from("regtype_array"))
22432 }
22433 "regclass" if self.peek_postfix_array_brackets() => {
22434 self.advance();
22435 self.advance();
22436 CastTarget::Named(alloc::string::String::from("regclass_array"))
22437 }
22438 "regtype" => CastTarget::RegType,
22439 "regclass" => CastTarget::RegClass,
22440 // v7.12.0 — `::tsvector` / `::tsquery`.
22441 // Engine decodes the LHS text via the PG
22442 // external form parser.
22443 // v7.39 (round 352, M8) — MySQL's own cast targets.
22444 // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22445 // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22446 // such type, so they are taken only in that dialect and
22447 // fall through to the "type does not exist" arm otherwise.
22448 "signed" | "unsigned" if self.mysql_dialect => {
22449 if matches!(self.peek(), Token::Ident(k)
22450 if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22451 {
22452 self.advance();
22453 }
22454 CastTarget::Named(s.to_ascii_lowercase())
22455 }
22456 // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22457 // in MySQL: MariaDB answers '123' where the SQL-standard
22458 // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22459 // Truncating a number to its first digit is a wrong answer
22460 // with no error, so the MySQL session gets MySQL's reading.
22461 "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22462 CastTarget::Text
22463 }
22464 "tsvector" => CastTarget::TsVector,
22465 "tsquery" => CastTarget::TsQuery,
22466 // v7.17.0 — `::uuid`. Engine decodes the LHS
22467 // text via `spg_storage::parse_uuid_str`.
22468 "uuid" => CastTarget::Uuid,
22469 // v7.18 — `::bytea`. Engine decodes the LHS
22470 // text via the PG hex form (`'\xdeadbeef'`)
22471 // or escape form (`'\\x05\\x00'`). Closes
22472 // mailrs D-pre #3 reverse-acceptance gap.
22473 "bytea" => CastTarget::Bytea,
22474 // v7.37.5 ship triage — generic typed-cast escape.
22475 // Anything the long-tail PG type ident table knows
22476 // about(network/bit/geometry/multirange/etc.)flows
22477 // through `CastTarget::Named(canonical)`; the engine
22478 // resolves via `column_type_to_data_type` and dispatches
22479 // through the typed `coerce_value` path. Truly
22480 // unrecognised idents still hit the error arm below
22481 // because the engine rejects them.
22482 other => {
22483 // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22484 // `::varchar(255)`, etc. Capture into the canonical
22485 // `name(p,s)` form so `type_name_to_data_type` can
22486 // reconstruct the `DataType::Numeric { precision,
22487 // scale }` (and similar param-carrying types).
22488 let mut name = other.to_string();
22489 // v7.39 (round 281) — `::bit varying(3)` is two
22490 // words; fold the tail in so the typmod reaches the
22491 // type resolver instead of tripping the parser.
22492 if name.eq_ignore_ascii_case("bit")
22493 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22494 {
22495 self.advance();
22496 name = alloc::string::String::from("varbit");
22497 }
22498 // v7.39 (round 613) — `::character varying` is the same
22499 // two-word shape and had no fold, so the `varying` was
22500 // left behind and the cast became a bare `character`,
22501 // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22502 // `a` where PG answers `ab`. Silently, and for a spelling
22503 // pg_dump writes.
22504 if name.eq_ignore_ascii_case("character")
22505 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22506 {
22507 self.advance();
22508 name = alloc::string::String::from("varchar");
22509 }
22510 if matches!(self.peek(), Token::LParen) {
22511 let mut buf = alloc::string::String::from("(");
22512 let mut depth = 0usize;
22513 loop {
22514 match self.advance() {
22515 Token::LParen => {
22516 depth += 1;
22517 if depth > 1 {
22518 buf.push('(');
22519 }
22520 }
22521 Token::RParen => {
22522 depth -= 1;
22523 if depth == 0 {
22524 buf.push(')');
22525 break;
22526 }
22527 buf.push(')');
22528 }
22529 Token::Comma => buf.push(','),
22530 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22531 // v7.39 (round 273) — a minus used to fall
22532 // into the catch-all below and vanish, so
22533 // `::numeric(10,-2)` reached the engine as
22534 // the text `numeric(10,2)` and silently
22535 // rounded to two DECIMALS instead of to
22536 // hundreds. A dropped token is not a
22537 // no-op when it carries a sign.
22538 Token::Minus => buf.push('-'),
22539 Token::Eof => break,
22540 _ => {}
22541 }
22542 }
22543 name.push_str(&buf);
22544 }
22545 // Optional postfix `[]` widens to the array form —
22546 // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22547 // The engine's `type_name_to_data_type` recognises
22548 // the canonical `<ty>_array` form.
22549 if matches!(self.peek(), Token::LBracket)
22550 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22551 {
22552 self.advance();
22553 self.advance();
22554 name.push_str("_array");
22555 }
22556 CastTarget::Named(name)
22557 }
22558 },
22559 Token::Interval => CastTarget::Interval,
22560 // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22561 // "char" (oid 18, SPG Char1 — distinct from bare `char`
22562 // = char(1)); other quoted names resolve like idents.
22563 Token::QuotedIdent(q) => {
22564 if q.eq_ignore_ascii_case("char") {
22565 CastTarget::Named("char1".into())
22566 } else {
22567 CastTarget::Named(q.to_ascii_lowercase())
22568 }
22569 }
22570 other => {
22571 return Err(ParseError {
22572 message: format!("expected type ident after `::`, got {other:?}"),
22573 token_pos: self.consumed_pos(),
22574 });
22575 }
22576 };
22577 // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22578 // target to its array sibling. Closed-enum arms (Bool /
22579 // SmallInt / Numeric / Float / Date / …) didn't carry the
22580 // explicit widening that Text / Int / BigInt did, so
22581 // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22582 // error. The widening here mirrors the per-arm Text /
22583 // Int / BigInt logic above + folds the new ζ-A first-class
22584 // types through `CastTarget::Named("<ty>_array")`.
22585 if matches!(self.peek(), Token::LBracket)
22586 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22587 {
22588 let widened = match &target {
22589 CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22590 CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22591 // v7.39 (round 326, V43) — the two temporal types stay
22592 // distinct. Both used to widen to `timestamptz_array`, so
22593 // `::timestamp[]` named the wrong target in its own error
22594 // message and lost the zone-less identity on the way.
22595 CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22596 CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22597 CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22598 CastTarget::Json | CastTarget::Jsonb => {
22599 Some(CastTarget::Named("jsonb_array".to_string()))
22600 }
22601 CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22602 CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22603 CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22604 CastTarget::Named(name) => {
22605 let mut a = name.clone();
22606 a.push_str("_array");
22607 Some(CastTarget::Named(a))
22608 }
22609 // Int / BigInt / Text / Vector / TsVector / TsQuery /
22610 // RegType / RegClass / TextArray / IntArray /
22611 // BigIntArray already finalised — leave as is.
22612 _ => None,
22613 };
22614 if let Some(w) = widened {
22615 self.advance();
22616 self.advance();
22617 return Ok(w);
22618 }
22619 }
22620 Ok(target)
22621 }
22622
22623 fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
22624 loop {
22625 // v7.38 (read01, T9) — composite field access `(expr).field`.
22626 // A bare `a.b` is consumed as a qualified column inside the ident
22627 // atom, so a Dot only survives to this postfix position when the
22628 // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
22629 // `.*` whole-row expansion is not handled here (projection-level).
22630 if matches!(self.peek(), Token::Dot)
22631 && matches!(
22632 self.tokens.get(self.pos + 1),
22633 Some(Token::Ident(_) | Token::QuotedIdent(_))
22634 )
22635 {
22636 self.advance(); // .
22637 let field = match self.advance() {
22638 Token::Ident(s) | Token::QuotedIdent(s) => s,
22639 other => {
22640 return Err(
22641 self.err(format!("expected a field name after '.', got {other:?}"))
22642 );
22643 }
22644 };
22645 expr = Expr::FieldAccess {
22646 base: Box::new(expr),
22647 field,
22648 };
22649 continue;
22650 }
22651 if matches!(self.peek(), Token::DoubleColon) {
22652 self.advance();
22653 // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
22654 // target set to include INTERVAL (reserved Token),
22655 // TIMESTAMPTZ, and PG catalog regtype / regclass.
22656 // mailrs follow-up H3a + H3b.
22657 let target = self.parse_cast_target()?;
22658 expr = Expr::Cast {
22659 expr: Box::new(expr),
22660 target,
22661 };
22662 continue;
22663 }
22664 // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
22665 // returns NULL for out-of-range. Multiple subscripts
22666 // chain: `a[i][j]` parses left-to-right.
22667 if matches!(self.peek(), Token::LBracket) {
22668 self.advance();
22669 // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
22670 // bare index stays a subscript.
22671 let lo = if matches!(self.peek(), Token::Colon) {
22672 None
22673 } else {
22674 Some(self.parse_expr(0)?)
22675 };
22676 if matches!(self.peek(), Token::Colon) {
22677 self.advance();
22678 let hi = if matches!(self.peek(), Token::RBracket) {
22679 None
22680 } else {
22681 Some(Box::new(self.parse_expr(0)?))
22682 };
22683 if !matches!(self.peek(), Token::RBracket) {
22684 return Err(self.err(alloc::format!(
22685 "expected ']' after array slice, got {:?}",
22686 self.peek()
22687 )));
22688 }
22689 self.advance();
22690 expr = Expr::ArraySlice {
22691 target: Box::new(expr),
22692 lo: lo.map(Box::new),
22693 hi,
22694 };
22695 continue;
22696 }
22697 let index = lo.expect("non-colon branch parsed an index");
22698 if !matches!(self.peek(), Token::RBracket) {
22699 return Err(self.err(alloc::format!(
22700 "expected ']' after array index, got {:?}",
22701 self.peek()
22702 )));
22703 }
22704 self.advance();
22705 expr = Expr::ArraySubscript {
22706 target: Box::new(expr),
22707 index: Box::new(index),
22708 };
22709 continue;
22710 }
22711 // `expr AT TIME ZONE zone` — lowers to PG's own function
22712 // form timezone(zone, expr); the scalar implements the
22713 // offset shift (named zones error there — no tzdata).
22714 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
22715 && matches!(self.tokens.get(self.pos + 1),
22716 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
22717 && matches!(self.tokens.get(self.pos + 2),
22718 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
22719 {
22720 self.advance(); // AT
22721 self.advance(); // TIME
22722 self.advance(); // ZONE
22723 // Zone at comparison precedence so AND/OR stay out.
22724 let zone = self.parse_expr(6)?;
22725 expr = Expr::FunctionCall {
22726 name: "timezone".to_string(),
22727 args: alloc::vec![zone, expr],
22728 };
22729 continue;
22730 }
22731 // `expr COLLATE "name"` — SPG's single text ordering IS
22732 // byte order, i.e. the C collation. The byte-order
22733 // spellings absorb as no-ops; a locale collation would
22734 // silently sort differently from PG, so it errors
22735 // honestly instead.
22736 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
22737 self.advance();
22738 let mut cname = match self.advance() {
22739 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22740 other => {
22741 return Err(self.err(alloc::format!(
22742 "expected collation name after COLLATE, got {other:?}"
22743 )));
22744 }
22745 };
22746 // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
22747 // is how `pg_dump` writes the default one:
22748 // `… COLLATE pg_catalog.default`. Reading a single token
22749 // left the SCHEMA as the name, so the clause was refused
22750 // as an unsupported locale collation and no dump ran.
22751 if matches!(self.peek(), Token::Dot) {
22752 self.advance();
22753 cname = match self.advance() {
22754 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22755 // `default` lexes as a KEYWORD, and it is the name
22756 // pg_dump writes — the same trap round 535 hit with
22757 // TABLE / INDEX / FULL.
22758 Token::Default => alloc::string::String::from("default"),
22759 other => {
22760 return Err(self.err(alloc::format!(
22761 "expected collation name after COLLATE, got {other:?}"
22762 )));
22763 }
22764 };
22765 }
22766 let lc = cname.to_ascii_lowercase();
22767 // v7.39 (round 371, M4 P4b) — a per-expression MySQL
22768 // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
22769 // family / `binary`) forces byte-wise, which is exactly
22770 // what `BINARY expr` does — lower onto that so every fold
22771 // site (comparison, LIKE, ORDER BY) suppresses via
22772 // `is_binary_coerced`. A `_ci` family override folds, and
22773 // under the MySQL dialect the default already folds, so it
22774 // absorbs as a no-op; likewise the C / byte-order spellings.
22775 if self.mysql_dialect && (lc.ends_with("_bin") || lc == "binary") {
22776 expr = Expr::Cast {
22777 expr: alloc::boxed::Box::new(expr),
22778 target: CastTarget::Named("binary".to_string()),
22779 };
22780 continue;
22781 }
22782 let mysql_ci = self.mysql_dialect
22783 && (lc.ends_with("_ci")
22784 || matches!(lc.as_str(), "case_insensitive" | "nocase"));
22785 // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
22786 // goes to the lowering channel, the byte-order spellings
22787 // included. Round 691 recorded only the names the old
22788 // allow-list rejected, which left `ORDER BY a COLLATE "C"`
22789 // absorbed as a no-op — and once a column could declare a
22790 // collation, absorbing the clause meant the COLUMN's
22791 // collation won where the query had asked for bytes.
22792 if self.in_order_by_key && !mysql_ci {
22793 self.order_key_collation = Some(cname);
22794 continue;
22795 }
22796 if !matches!(
22797 lc.as_str(),
22798 "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
22799 ) && !mysql_ci
22800 {
22801 // v7.38.18 — the old message read "SPG orders text
22802 // by bytes (the C collation); locale collations are
22803 // not supported yet", and both halves were false by
22804 // the time it was read. This build performs locale
22805 // collations: declared on a column or written in an
22806 // ORDER BY key, `en_US.utf8` orders `apple, client,
22807 // DateStyle, Zebra` exactly as PG 18.4 does. What it
22808 // cannot do is carry a collation on an arbitrary
22809 // expression, because there is no `Expr::Collate` to
22810 // carry it — so say that, and say where the clause
22811 // does work rather than telling the reader to drop it.
22812 return Err(self.err(alloc::format!(
22813 "COLLATE {cname:?} is not supported in this position: \
22814 SPG carries a collation on a column declaration and \
22815 on an ORDER BY key, not on an arbitrary expression. \
22816 Declare it on the column (`x text COLLATE \
22817 {cname:?}`) or move it into the ORDER BY key"
22818 )));
22819 }
22820 continue;
22821 }
22822 return Ok(expr);
22823 }
22824 }
22825
22826 /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
22827 /// the first token that is not one. Schema qualifiers collapse to the
22828 /// last part, which is what every other name path here does (SPG is
22829 /// single-schema).
22830 fn take_comma_separated_names(&mut self) -> Vec<String> {
22831 let mut out = Vec::new();
22832 while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
22833 self.advance();
22834 let mut last = n;
22835 while matches!(self.peek(), Token::Dot) {
22836 self.advance();
22837 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
22838 last = t;
22839 }
22840 }
22841 out.push(last);
22842 if matches!(self.peek(), Token::Comma) {
22843 self.advance();
22844 } else {
22845 break;
22846 }
22847 }
22848 out
22849 }
22850
22851 /// v7.39 (round 694) — is the next token pair a postfix `[]`?
22852 ///
22853 /// The general cast-target path tests this inline; the types with their
22854 /// own `CastTarget` variant need it as a guard on their match arm,
22855 /// which is what this exists for.
22856 fn peek_postfix_array_brackets(&self) -> bool {
22857 matches!(self.peek(), Token::LBracket)
22858 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22859 }
22860
22861 /// Parse the operator tail after a `(a, b, …)` row constructor
22862 /// and expand at parse time. `=` is the conjunction of element
22863 /// equalities; `<>` its negation; the order operators expand
22864 /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
22865 /// equalities. Anything else (a bare row value, a subquery
22866 /// RHS) errors honestly — SPG has no composite runtime value.
22867 fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
22868 fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
22869 let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
22870 lhs: Box::new(l.clone()),
22871 op: BinOp::Eq,
22872 rhs: Box::new(r.clone()),
22873 });
22874 let first = it.next().expect("row has at least two elements");
22875 it.fold(first, |acc, e| Expr::Binary {
22876 lhs: Box::new(acc),
22877 op: BinOp::And,
22878 rhs: Box::new(e),
22879 })
22880 }
22881 // Lexicographic (a,b) OP (c,d):
22882 // a STRICT c OR (a = c AND (b OP d)) — recursing right.
22883 fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
22884 if lhs.len() == 1 {
22885 return Expr::Binary {
22886 lhs: Box::new(lhs[0].clone()),
22887 op: last,
22888 rhs: Box::new(rhs[0].clone()),
22889 };
22890 }
22891 let head_strict = Expr::Binary {
22892 lhs: Box::new(lhs[0].clone()),
22893 op: strict,
22894 rhs: Box::new(rhs[0].clone()),
22895 };
22896 let head_eq = Expr::Binary {
22897 lhs: Box::new(lhs[0].clone()),
22898 op: BinOp::Eq,
22899 rhs: Box::new(rhs[0].clone()),
22900 };
22901 Expr::Binary {
22902 lhs: Box::new(head_strict),
22903 op: BinOp::Or,
22904 rhs: Box::new(Expr::Binary {
22905 lhs: Box::new(head_eq),
22906 op: BinOp::And,
22907 rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
22908 }),
22909 }
22910 }
22911 let negated_in = if matches!(self.peek(), Token::Not)
22912 && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
22913 {
22914 self.advance();
22915 true
22916 } else {
22917 false
22918 };
22919 if matches!(self.peek(), Token::In) {
22920 self.advance();
22921 if !matches!(self.peek(), Token::LParen) {
22922 return Err(self.err(alloc::format!(
22923 "expected '(' after row IN, got {:?}",
22924 self.peek()
22925 )));
22926 }
22927 self.advance();
22928 // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
22929 // not a list of literal rows. Row-vs-list decomposes to
22930 // OR-of-AND above, but the subquery's rows are only known at
22931 // runtime, so keep it as a RowInSubquery node.
22932 if matches!(self.peek(), Token::Select) {
22933 let inner = self.parse_select_stmt()?;
22934 if !matches!(self.peek(), Token::RParen) {
22935 return Err(self.err(alloc::format!(
22936 "expected ')' after row IN-subquery, got {:?}",
22937 self.peek()
22938 )));
22939 }
22940 self.advance();
22941 let Statement::Select(s) = inner else {
22942 unreachable!("parse_select_stmt always returns Statement::Select")
22943 };
22944 return Ok(Expr::RowInSubquery {
22945 row,
22946 subquery: Box::new(s),
22947 negated: negated_in,
22948 });
22949 }
22950 let mut alternatives: Vec<Expr> = Vec::new();
22951 loop {
22952 // Optional ROW keyword before the paren row.
22953 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22954 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22955 {
22956 self.advance();
22957 }
22958 if !matches!(self.peek(), Token::LParen) {
22959 return Err(self.err(alloc::format!(
22960 "expected '(' to open a row inside IN, got {:?}",
22961 self.peek()
22962 )));
22963 }
22964 self.advance();
22965 let mut rhs = alloc::vec![self.parse_expr(0)?];
22966 while matches!(self.peek(), Token::Comma) {
22967 self.advance();
22968 rhs.push(self.parse_expr(0)?);
22969 }
22970 if !matches!(self.peek(), Token::RParen) {
22971 return Err(self.err(alloc::format!(
22972 "expected ')' after row inside IN, got {:?}",
22973 self.peek()
22974 )));
22975 }
22976 self.advance();
22977 if rhs.len() != row.len() {
22978 return Err(self.err(alloc::format!(
22979 "row IN arity mismatch: left has {}, right has {}",
22980 row.len(),
22981 rhs.len()
22982 )));
22983 }
22984 alternatives.push(row_eq(&row, &rhs));
22985 if matches!(self.peek(), Token::Comma) {
22986 self.advance();
22987 continue;
22988 }
22989 break;
22990 }
22991 if !matches!(self.peek(), Token::RParen) {
22992 return Err(self.err(alloc::format!(
22993 "expected ')' to close row IN list, got {:?}",
22994 self.peek()
22995 )));
22996 }
22997 self.advance();
22998 let mut it = alternatives.into_iter();
22999 let first = it.next().expect("IN list has at least one row");
23000 let combined = it.fold(first, |acc, e| Expr::Binary {
23001 lhs: Box::new(acc),
23002 op: BinOp::Or,
23003 rhs: Box::new(e),
23004 });
23005 return Ok(maybe_not(combined, negated_in));
23006 }
23007 // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
23008 // two periods share at least one time point. Each pair is
23009 // normalised with least/greatest (PG accepts the endpoints
23010 // in either order), then lowered to the standard
23011 // `start1 < end2 AND start2 < end1` form.
23012 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
23013 if row.len() != 2 {
23014 return Err(self.err(alloc::format!(
23015 "OVERLAPS needs (start, end) pairs; left side has {} elements",
23016 row.len()
23017 )));
23018 }
23019 self.advance();
23020 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23021 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23022 {
23023 self.advance();
23024 }
23025 if !matches!(self.peek(), Token::LParen) {
23026 return Err(self.err(alloc::format!(
23027 "expected '(' after OVERLAPS, got {:?}",
23028 self.peek()
23029 )));
23030 }
23031 self.advance();
23032 let r0 = self.parse_expr(0)?;
23033 if !matches!(self.peek(), Token::Comma) {
23034 return Err(self.err(alloc::format!(
23035 "OVERLAPS needs (start, end) on the right, got {:?}",
23036 self.peek()
23037 )));
23038 }
23039 self.advance();
23040 let r1 = self.parse_expr(0)?;
23041 if !matches!(self.peek(), Token::RParen) {
23042 return Err(self.err(alloc::format!(
23043 "expected ')' after OVERLAPS pair, got {:?}",
23044 self.peek()
23045 )));
23046 }
23047 self.advance();
23048 let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
23049 name: String::from(name),
23050 args: alloc::vec![a.clone(), b.clone()],
23051 };
23052 let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
23053 lhs: Box::new(lhs),
23054 op: BinOp::Lt,
23055 rhs: Box::new(rhs),
23056 };
23057 return Ok(Expr::Binary {
23058 lhs: Box::new(lt(
23059 pair_fn("least", &row[0], &row[1]),
23060 pair_fn("greatest", &r0, &r1),
23061 )),
23062 op: BinOp::And,
23063 rhs: Box::new(lt(
23064 pair_fn("least", &r0, &r1),
23065 pair_fn("greatest", &row[0], &row[1]),
23066 )),
23067 });
23068 }
23069 // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
23070 // PG, `IS NULL` is true only when EVERY field is NULL, and
23071 // `IS NOT NULL` is true only when every field is non-NULL — the
23072 // latter is NOT the negation of the former (a mixed row is
23073 // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
23074 // which reproduces exactly that all-fields semantics.
23075 if matches!(self.peek(), Token::Is) {
23076 self.advance();
23077 let negated = if matches!(self.peek(), Token::Not) {
23078 self.advance();
23079 true
23080 } else {
23081 false
23082 };
23083 if !matches!(self.peek(), Token::Null) {
23084 return Err(self.err(alloc::format!(
23085 "expected NULL after row IS [NOT], got {:?}",
23086 self.peek()
23087 )));
23088 }
23089 self.advance();
23090 let mut it = row.iter().map(|e| Expr::IsNull {
23091 expr: Box::new(e.clone()),
23092 negated,
23093 });
23094 let first = it.next().expect("row has at least two elements");
23095 return Ok(it.fold(first, |acc, e| Expr::Binary {
23096 lhs: Box::new(acc),
23097 op: BinOp::And,
23098 rhs: Box::new(e),
23099 }));
23100 }
23101 let op = match self.peek() {
23102 Token::Eq => BinOp::Eq,
23103 Token::NotEq => BinOp::NotEq,
23104 Token::Lt => BinOp::Lt,
23105 Token::LtEq => BinOp::LtEq,
23106 Token::Gt => BinOp::Gt,
23107 Token::GtEq => BinOp::GtEq,
23108 // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
23109 // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
23110 // constructor value, identical to the `ROW(a, b, …)` keyword form:
23111 // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
23112 // (`::text`, `.field`) applies at the caller just as it does for the
23113 // ROW(...) node. All the comparison / predicate forms returned above.
23114 _ => {
23115 return Ok(Expr::FunctionCall {
23116 name: String::from("row"),
23117 args: row,
23118 });
23119 }
23120 };
23121 self.advance();
23122 // Optional ROW keyword before the paren row.
23123 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23124 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23125 {
23126 self.advance();
23127 }
23128 if !matches!(self.peek(), Token::LParen) {
23129 return Err(self.err(alloc::format!(
23130 "expected '(' to open the right-hand row, got {:?}",
23131 self.peek()
23132 )));
23133 }
23134 self.advance();
23135 // `(a, b) <op> (SELECT x, y)` — compare against a single-row
23136 // subquery. Kept as a node (the subquery's row is a runtime value);
23137 // the literal-RHS form below still decomposes at parse time.
23138 if matches!(self.peek(), Token::Select) {
23139 let inner = self.parse_select_stmt()?;
23140 if !matches!(self.peek(), Token::RParen) {
23141 return Err(self.err(alloc::format!(
23142 "expected ')' after row comparison subquery, got {:?}",
23143 self.peek()
23144 )));
23145 }
23146 self.advance();
23147 let Statement::Select(s) = inner else {
23148 unreachable!("parse_select_stmt always returns Statement::Select")
23149 };
23150 return Ok(Expr::RowCmpSubquery {
23151 row,
23152 op,
23153 subquery: Box::new(s),
23154 });
23155 }
23156 let mut rhs = alloc::vec![self.parse_expr(0)?];
23157 while matches!(self.peek(), Token::Comma) {
23158 self.advance();
23159 rhs.push(self.parse_expr(0)?);
23160 }
23161 if !matches!(self.peek(), Token::RParen) {
23162 return Err(self.err(alloc::format!(
23163 "expected ')' after right-hand row, got {:?}",
23164 self.peek()
23165 )));
23166 }
23167 self.advance();
23168 if rhs.len() != row.len() {
23169 // v7.39 (round 239) — PG's wording (42601).
23170 return Err(self.err("unequal number of entries in row expressions".to_string()));
23171 }
23172 Ok(match op {
23173 BinOp::Eq => row_eq(&row, &rhs),
23174 BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
23175 BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
23176 BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
23177 BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
23178 BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
23179 _ => unreachable!("op restricted above"),
23180 })
23181 }
23182
23183 /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
23184 /// escape character becomes the matcher's default backslash:
23185 /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
23186 /// → the char itself, and any pre-existing backslash escapes
23187 /// itself so it stays literal. Both operands must be string
23188 /// literals — a runtime pattern would need matcher support.
23189 fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
23190 let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
23191 (&pattern, &esc)
23192 else {
23193 return Err(
23194 "LIKE ... ESCAPE requires string-literal pattern and escape \
23195 (runtime escape characters are not supported yet)"
23196 .into(),
23197 );
23198 };
23199 // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
23200 // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
23201 // multi-character escape is an error.
23202 let esc_ch: Option<char> = {
23203 let mut ch_iter = e.chars();
23204 match (ch_iter.next(), ch_iter.next()) {
23205 (Some(c), None) => Some(c),
23206 (None, _) => None,
23207 (Some(_), Some(_)) => {
23208 return Err(alloc::format!(
23209 "ESCAPE must be a single character, got {e:?}"
23210 ));
23211 }
23212 }
23213 };
23214 let mut out = String::with_capacity(p.len() + 4);
23215 let mut chars = p.chars();
23216 while let Some(c) = chars.next() {
23217 if Some(c) == esc_ch {
23218 match chars.next() {
23219 // Escaped wildcard / escaped escape → keep the
23220 // next char literal via backslash.
23221 Some(next) => {
23222 out.push('\\');
23223 out.push(next);
23224 }
23225 None => {
23226 return Err("LIKE pattern ends with the escape character".into());
23227 }
23228 }
23229 } else if c == '\\' && esc_ch != Some('\\') {
23230 // A raw backslash is literal under a custom (or absent) escape
23231 // — escape it for the backslash-based matcher.
23232 out.push('\\');
23233 out.push('\\');
23234 } else {
23235 out.push(c);
23236 }
23237 }
23238 Ok(Expr::Literal(Literal::String(out)))
23239 }
23240
23241 /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
23242 /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
23243 /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
23244 /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
23245 /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
23246 /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
23247 /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
23248 /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
23249 /// array expression errors honestly rather than silently mismatching.
23250 fn try_like_any_all(
23251 &mut self,
23252 base: &Expr,
23253 negated: bool,
23254 case_insensitive: bool,
23255 ) -> Result<Option<Expr>, ParseError> {
23256 let is_any = match self.peek() {
23257 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
23258 Token::Ident(s)
23259 if s.eq_ignore_ascii_case("any")
23260 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
23261 {
23262 true
23263 }
23264 _ => return Ok(None),
23265 };
23266 self.advance(); // ANY / ALL
23267 self.advance(); // '('
23268 let arr = self.parse_expr(0)?;
23269 if !matches!(self.peek(), Token::RParen) {
23270 return Err(self.err(format!(
23271 "expected ')' after LIKE {} argument, got {:?}",
23272 if is_any { "ANY" } else { "ALL" },
23273 self.peek()
23274 )));
23275 }
23276 self.advance(); // ')'
23277 let Expr::Array(items) = arr else {
23278 return Err(self.err(
23279 "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
23280 ));
23281 };
23282 let mut clauses = items.into_iter().map(|p| Expr::Like {
23283 expr: Box::new(base.clone()),
23284 pattern: Box::new(p),
23285 negated,
23286 case_insensitive,
23287 });
23288 let Some(first) = clauses.next() else {
23289 // ANY(empty) = FALSE, ALL(empty) = TRUE.
23290 return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
23291 };
23292 let op = if is_any { BinOp::Or } else { BinOp::And };
23293 let combined = clauses.fold(first, |acc, c| Expr::Binary {
23294 lhs: Box::new(acc),
23295 op,
23296 rhs: Box::new(c),
23297 });
23298 Ok(Some(combined))
23299 }
23300
23301 /// `x BETWEEN low AND high` → `(x >= low) AND (x <= high)`, wrapped in
23302 /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
23303 /// `AND` is not swallowed.
23304 fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23305 self.advance(); // BETWEEN
23306 // SYMMETRIC — the bounds may arrive in either order; both
23307 // orientations OR together. ASYMMETRIC is the default and
23308 // absorbs as noise.
23309 let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
23310 {
23311 self.advance();
23312 true
23313 } else {
23314 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
23315 self.advance();
23316 }
23317 false
23318 };
23319 let low = self.parse_expr(6)?;
23320 if !matches!(self.peek(), Token::And) {
23321 return Err(self.err(format!(
23322 "expected AND after BETWEEN low bound, got {:?}",
23323 self.peek()
23324 )));
23325 }
23326 self.advance();
23327 let high = self.parse_expr(6)?;
23328 let target = Box::new(expr);
23329 let range = |lo: Expr, hi: Expr| Expr::Binary {
23330 lhs: Box::new(Expr::Binary {
23331 lhs: target.clone(),
23332 op: BinOp::GtEq,
23333 rhs: Box::new(lo),
23334 }),
23335 op: BinOp::And,
23336 rhs: Box::new(Expr::Binary {
23337 lhs: target.clone(),
23338 op: BinOp::LtEq,
23339 rhs: Box::new(hi),
23340 }),
23341 };
23342 let combined = if symmetric {
23343 Expr::Binary {
23344 lhs: Box::new(range(low.clone(), high.clone())),
23345 op: BinOp::Or,
23346 rhs: Box::new(range(high, low)),
23347 }
23348 } else {
23349 range(low, high)
23350 };
23351 Ok(maybe_not(combined, negated))
23352 }
23353
23354 /// `x IN (a, b, c)` → chained OR of equalities. Empty list collapses
23355 /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
23356 /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
23357 /// Caller already consumed the leading `WITH` ident.
23358 /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
23359 /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
23360 /// self-reference that appears more than once in a single term.
23361 fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
23362 use crate::ast::{CteBody, SelectStatement};
23363 if !cte.recursive {
23364 return Ok(());
23365 }
23366 let CteBody::Select(body) = &cte.body else {
23367 return Ok(());
23368 };
23369 // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
23370 // check the anchor and every peer term.
23371 let has_order = |s: &SelectStatement| !s.order_by.is_empty();
23372 let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
23373 if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
23374 return Err(self.err(String::from(
23375 "ORDER BY in a recursive query is not implemented",
23376 )));
23377 }
23378 if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
23379 return Err(self.err(String::from(
23380 "LIMIT in a recursive query is not implemented",
23381 )));
23382 }
23383 let self_refs = |s: &SelectStatement| -> usize {
23384 let Some(from) = &s.from else {
23385 return 0;
23386 };
23387 let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
23388 for j in &from.joins {
23389 if j.table.name.eq_ignore_ascii_case(&cte.name) {
23390 n += 1;
23391 }
23392 }
23393 n
23394 };
23395 if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
23396 return Err(self.err(alloc::format!(
23397 "recursive reference to query \"{}\" must not appear more than once",
23398 cte.name
23399 )));
23400 }
23401 // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
23402 // apply only when the body actually references itself (a non-self-
23403 // referencing CTE under WITH RECURSIVE may use any set-op shape).
23404 let anchor_refs = self_refs(body);
23405 let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
23406 if anchor_refs > 0 || union_refs {
23407 // Shape: the top level must be UNION [ALL] arms only. A self-ref
23408 // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
23409 // "does not have the form" error — SPG used to compute a value.
23410 if body.unions.is_empty()
23411 || body.unions.iter().any(|(k, _)| {
23412 !matches!(
23413 k,
23414 crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
23415 )
23416 })
23417 {
23418 return Err(self.err(alloc::format!(
23419 "recursive query \"{}\" does not have the form non-recursive-term \
23420 UNION [ALL] recursive-term",
23421 cte.name
23422 )));
23423 }
23424 if anchor_refs > 0 {
23425 return Err(self.err(alloc::format!(
23426 "recursive reference to query \"{}\" must not appear within its non-recursive term",
23427 cte.name
23428 )));
23429 }
23430 }
23431 let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23432 for (_, u) in &body.unions {
23433 if self_refs(u) == 0 {
23434 continue;
23435 }
23436 // The self-reference must not sit on the nullable side of an outer
23437 // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23438 if let Some(from) = &u.from {
23439 for (i, j) in from.joins.iter().enumerate() {
23440 let left_has_self = is_self(&from.primary)
23441 || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23442 let violated = match j.kind {
23443 crate::ast::JoinKind::Left => is_self(&j.table),
23444 crate::ast::JoinKind::Right => left_has_self,
23445 crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23446 _ => false,
23447 };
23448 if violated {
23449 return Err(self.err(alloc::format!(
23450 "recursive reference to query \"{}\" must not appear within an outer join",
23451 cte.name
23452 )));
23453 }
23454 }
23455 }
23456 // No aggregates at the top level of the recursive term (SPG used
23457 // to run them and surface a misleading downstream error).
23458 let mut items_and_having: Vec<&Expr> = Vec::new();
23459 for it in &u.items {
23460 if let crate::ast::SelectItem::Expr { expr, .. } = it {
23461 items_and_having.push(expr);
23462 }
23463 }
23464 if let Some(h) = &u.having {
23465 items_and_having.push(h);
23466 }
23467 for e in items_and_having {
23468 if expr_has_toplevel_aggregate(e) {
23469 return Err(self.err(String::from(
23470 "aggregate functions are not allowed in a recursive query's recursive term",
23471 )));
23472 }
23473 }
23474 }
23475 // A self-reference inside a sublink expression (EXISTS / IN / scalar
23476 // subquery) anywhere in the body is rejected; a plain FROM derived
23477 // table is legal in PG and untouched here.
23478 let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23479 all_terms.extend(body.unions.iter().map(|(_, u)| u));
23480 for term in all_terms {
23481 if select_has_self_ref_in_sublink(term, &cte.name) {
23482 return Err(self.err(alloc::format!(
23483 "recursive reference to query \"{}\" must not appear within a subquery",
23484 cte.name
23485 )));
23486 }
23487 }
23488 Ok(())
23489 }
23490
23491 /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23492 /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23493 /// right after parse so the engine sees a plain recursive CTE with the
23494 /// tracking columns already projected. DEPTH FIRST and CYCLE are
23495 /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23496 /// text-rendered rows can't provide, and errors honestly.
23497 fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23498 use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23499 if cte.search.is_none() && cte.cycle.is_none() {
23500 return Ok(());
23501 }
23502 let cte_name = cte.name.clone();
23503 let col_names = cte.column_overrides.clone();
23504 if col_names.is_empty() {
23505 return Err(
23506 self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23507 );
23508 }
23509 let search = cte.search.take();
23510 let cycle = cte.cycle.take();
23511 let mut extra_cols: Vec<String> = Vec::new();
23512 let col_ref = |name: &str| {
23513 Expr::Column(ColumnName {
23514 qualifier: Some(cte_name.clone()),
23515 name: name.to_string(),
23516 })
23517 };
23518 // Position of a SEARCH/CYCLE column within the CTE's column list.
23519 let pos_of = |name: &str| -> Result<usize, ParseError> {
23520 col_names
23521 .iter()
23522 .position(|c| c.eq_ignore_ascii_case(name))
23523 .ok_or_else(|| {
23524 self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23525 })
23526 };
23527 let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23528 let mut args = Vec::with_capacity(positions.len());
23529 for &p in positions {
23530 match items.get(p) {
23531 Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23532 _ => {
23533 return Err(self.err(
23534 "SEARCH/CYCLE column maps to a non-expression select item".into(),
23535 ));
23536 }
23537 }
23538 }
23539 Ok(Expr::FunctionCall {
23540 name: "row".into(),
23541 args,
23542 })
23543 };
23544 let CteBody::Select(body) = &mut cte.body else {
23545 return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23546 };
23547 if body.unions.is_empty() {
23548 return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23549 }
23550 let rec = body.unions.len() - 1; // recursive term = last UNION peer
23551
23552 if let Some(srch) = search {
23553 // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23554 // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23555 // no typed `record[]`, but element-wise array ORDER BY is correct
23556 // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23557 // exactly onto a typed array: DEPTH is the root→node path
23558 // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23559 // orders numerically (multi-digit keys included), matching PG.
23560 //
23561 // A multi-column BY would need a record[] to keep the per-node key
23562 // tuple orderable, which SPG can't express — error honestly there
23563 // rather than mis-order.
23564 if srch.by_columns.len() != 1 {
23565 return Err(self.err(
23566 "SEARCH … BY with multiple columns needs typed record[] ordering \
23567 SPG doesn't have yet; a single BY column is supported"
23568 .into(),
23569 ));
23570 }
23571 let key_pos = pos_of(&srch.by_columns[0])?;
23572 let base_key = match body.items.get(key_pos) {
23573 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23574 _ => {
23575 return Err(
23576 self.err("SEARCH BY column maps to a non-expression select item".into())
23577 );
23578 }
23579 };
23580 let rec_key = match body.unions[rec].1.items.get(key_pos) {
23581 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23582 _ => {
23583 return Err(
23584 self.err("SEARCH BY column maps to a non-expression select item".into())
23585 );
23586 }
23587 };
23588 if srch.depth_first {
23589 // base: ARRAY[key]; rec: array_append(cte.set, key).
23590 body.items.push(SelectItem::Expr {
23591 expr: Expr::Array(alloc::vec![base_key]),
23592 alias: Some(srch.set_column.clone()),
23593 });
23594 body.unions[rec].1.items.push(SelectItem::Expr {
23595 expr: Expr::FunctionCall {
23596 name: "array_append".into(),
23597 args: alloc::vec![col_ref(&srch.set_column), rec_key],
23598 },
23599 alias: Some(srch.set_column.clone()),
23600 });
23601 } else {
23602 // BREADTH: [depth, key]; depth starts at 0 and increments. The
23603 // leading depth element dominates the element-wise comparison,
23604 // so shallower rows sort first, then by key — PG's (depth, key).
23605 body.items.push(SelectItem::Expr {
23606 expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
23607 alias: Some(srch.set_column.clone()),
23608 });
23609 // rec depth = cte.set[1] + 1.
23610 let parent_depth = Expr::ArraySubscript {
23611 target: Box::new(col_ref(&srch.set_column)),
23612 index: Box::new(Expr::Literal(Literal::Integer(1))),
23613 };
23614 body.unions[rec].1.items.push(SelectItem::Expr {
23615 expr: Expr::Array(alloc::vec![
23616 Expr::Binary {
23617 lhs: Box::new(parent_depth),
23618 op: BinOp::Add,
23619 rhs: Box::new(Expr::Literal(Literal::Integer(1))),
23620 },
23621 rec_key,
23622 ]),
23623 alias: Some(srch.set_column.clone()),
23624 });
23625 }
23626 extra_cols.push(srch.set_column);
23627 }
23628
23629 if let Some(cyc) = cycle {
23630 let positions: Vec<usize> = cyc
23631 .columns
23632 .iter()
23633 .map(|c| pos_of(c))
23634 .collect::<Result<_, _>>()?;
23635 // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
23636 // cast it to text for the cycle path: membership only needs equality,
23637 // and the record text form gives SPG a TextArray path (SPG has no
23638 // typed record[] array). Cycle detection is unaffected.
23639 let base_row = Expr::Cast {
23640 expr: Box::new(row_of(&body.items, &positions)?),
23641 target: CastTarget::Text,
23642 };
23643 let rec_row = Expr::Cast {
23644 expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
23645 target: CastTarget::Text,
23646 };
23647 let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
23648 let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
23649 // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
23650 body.items.push(SelectItem::Expr {
23651 expr: Expr::Literal(dflt.clone()),
23652 alias: Some(cyc.mark_column.clone()),
23653 });
23654 body.items.push(SelectItem::Expr {
23655 expr: Expr::Array(alloc::vec![base_row]),
23656 alias: Some(cyc.path_column.clone()),
23657 });
23658 // rec mark: ROW(cols) already in the path → cycle.
23659 let hit = Expr::AnyAll {
23660 expr: Box::new(rec_row.clone()),
23661 op: BinOp::Eq,
23662 array: Box::new(col_ref(&cyc.path_column)),
23663 is_any: true,
23664 };
23665 let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
23666 Expr::Case {
23667 operand: None,
23668 branches: alloc::vec![(hit, Expr::Literal(mark))],
23669 else_branch: Some(Box::new(Expr::Literal(dflt))),
23670 }
23671 } else {
23672 hit
23673 };
23674 body.unions[rec].1.items.push(SelectItem::Expr {
23675 expr: mark_expr,
23676 alias: Some(cyc.mark_column.clone()),
23677 });
23678 // rec path: array_append(cte.path, ROW(cols)).
23679 body.unions[rec].1.items.push(SelectItem::Expr {
23680 expr: Expr::FunctionCall {
23681 name: "array_append".into(),
23682 args: alloc::vec![col_ref(&cyc.path_column), rec_row],
23683 },
23684 alias: Some(cyc.path_column.clone()),
23685 });
23686 // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
23687 let stop = Expr::Unary {
23688 op: UnOp::Not,
23689 expr: Box::new(col_ref(&cyc.mark_column)),
23690 };
23691 let w = &mut body.unions[rec].1.where_;
23692 *w = Some(match w.take() {
23693 Some(prev) => Expr::Binary {
23694 lhs: Box::new(prev),
23695 op: BinOp::And,
23696 rhs: Box::new(stop),
23697 },
23698 None => stop,
23699 });
23700 extra_cols.push(cyc.mark_column);
23701 extra_cols.push(cyc.path_column);
23702 }
23703 cte.column_overrides.extend(extra_cols);
23704 Ok(())
23705 }
23706
23707 /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
23708 /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
23709 fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
23710 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
23711 return Ok(None);
23712 }
23713 self.advance(); // SEARCH
23714 let depth_first = match self.peek() {
23715 Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
23716 Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
23717 other => {
23718 return Err(self.err(format!(
23719 "expected DEPTH or BREADTH after SEARCH, got {other:?}"
23720 )));
23721 }
23722 };
23723 self.advance();
23724 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
23725 return Err(self.err(format!(
23726 "expected FIRST after SEARCH mode, got {:?}",
23727 self.peek()
23728 )));
23729 }
23730 self.advance();
23731 if !self.peek_is_by() {
23732 return Err(self.err(format!(
23733 "expected BY after SEARCH … FIRST, got {:?}",
23734 self.peek()
23735 )));
23736 }
23737 self.advance();
23738 let mut by_columns = alloc::vec![self.expect_ident_like()?];
23739 while matches!(self.peek(), Token::Comma) {
23740 self.advance();
23741 by_columns.push(self.expect_ident_like()?);
23742 }
23743 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23744 return Err(self.err(format!(
23745 "expected SET in SEARCH clause, got {:?}",
23746 self.peek()
23747 )));
23748 }
23749 self.advance();
23750 let set_column = self.expect_ident_like()?;
23751 Ok(Some(crate::ast::SearchClause {
23752 depth_first,
23753 by_columns,
23754 set_column,
23755 }))
23756 }
23757
23758 /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
23759 /// USING pathcol`. Returns None when the next token isn't CYCLE.
23760 fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
23761 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
23762 return Ok(None);
23763 }
23764 self.advance(); // CYCLE
23765 let mut columns = alloc::vec![self.expect_ident_like()?];
23766 while matches!(self.peek(), Token::Comma) {
23767 self.advance();
23768 columns.push(self.expect_ident_like()?);
23769 }
23770 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23771 return Err(self.err(format!(
23772 "expected SET in CYCLE clause, got {:?}",
23773 self.peek()
23774 )));
23775 }
23776 self.advance();
23777 let mark_column = self.expect_ident_like()?;
23778 let mut mark_value = None;
23779 let mut default_value = None;
23780 if matches!(self.peek(), Token::To) {
23781 self.advance();
23782 mark_value = Some(self.parse_cycle_literal()?);
23783 if !matches!(self.peek(), Token::Default) {
23784 return Err(self.err(format!(
23785 "expected DEFAULT after CYCLE … TO, got {:?}",
23786 self.peek()
23787 )));
23788 }
23789 self.advance();
23790 default_value = Some(self.parse_cycle_literal()?);
23791 }
23792 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
23793 return Err(self.err(format!(
23794 "expected USING in CYCLE clause, got {:?}",
23795 self.peek()
23796 )));
23797 }
23798 self.advance();
23799 let path_column = self.expect_ident_like()?;
23800 Ok(Some(crate::ast::CycleClause {
23801 columns,
23802 mark_column,
23803 mark_value,
23804 default_value,
23805 path_column,
23806 }))
23807 }
23808
23809 /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
23810 /// literal (string / bool / number) in PG.
23811 fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
23812 match self.parse_expr(0)? {
23813 Expr::Literal(l) => Ok(l),
23814 other => Err(self.err(format!(
23815 "CYCLE mark/default value must be a literal, got {other:?}"
23816 ))),
23817 }
23818 }
23819
23820 fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
23821 // v4.22: WITH RECURSIVE — optional keyword right after WITH.
23822 // Comes through as an identifier; consume it if present and
23823 // mark every CTE in the clause as recursive (PG semantics —
23824 // the flag is per-WITH, not per-CTE).
23825 let mut recursive = false;
23826 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
23827 && s.eq_ignore_ascii_case("recursive")
23828 {
23829 self.advance();
23830 recursive = true;
23831 }
23832 let mut ctes = Vec::new();
23833 loop {
23834 let name = self.expect_ident_like()?;
23835 // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
23836 // PG uses these to rename the body's output columns; we
23837 // do the same below by overriding `columns[i].name`.
23838 let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
23839 self.advance();
23840 let mut names = Vec::new();
23841 loop {
23842 names.push(self.expect_ident_like()?);
23843 if matches!(self.peek(), Token::Comma) {
23844 self.advance();
23845 continue;
23846 }
23847 break;
23848 }
23849 if !matches!(self.peek(), Token::RParen) {
23850 return Err(self.err(format!(
23851 "expected ')' to close CTE column list, got {:?}",
23852 self.peek()
23853 )));
23854 }
23855 self.advance();
23856 names
23857 } else {
23858 Vec::new()
23859 };
23860 // AS is a reserved Token::As (used by SELECT-item / FROM
23861 // aliasing) — handle it specially rather than as a bare
23862 // ident.
23863 if !matches!(self.peek(), Token::As) {
23864 return Err(self.err(format!(
23865 "expected AS after CTE name {name:?}, got {:?}",
23866 self.peek()
23867 )));
23868 }
23869 self.advance();
23870 // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
23871 // MATERIALIZED` optimizer hints. SPG materialises every
23872 // CTE, so both spellings are accepted and absorbed.
23873 if matches!(self.peek(), Token::Not) {
23874 self.advance(); // NOT
23875 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23876 if s.eq_ignore_ascii_case("materialized"))
23877 {
23878 self.advance();
23879 } else {
23880 return Err(self.err(format!(
23881 "expected MATERIALIZED after AS NOT, got {:?}",
23882 self.peek()
23883 )));
23884 }
23885 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23886 if s.eq_ignore_ascii_case("materialized"))
23887 {
23888 self.advance();
23889 }
23890 if !matches!(self.peek(), Token::LParen) {
23891 return Err(self.err(format!(
23892 "expected '(' after AS in WITH clause, got {:?}",
23893 self.peek()
23894 )));
23895 }
23896 self.advance();
23897 // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
23898 // RETURNING) as the CTE body in addition to SELECT.
23899 // PG writable CTE semantics. UPDATE / DELETE come in as
23900 // bare Idents (lexer keeps SELECT / INSERT as reserved
23901 // tokens but treats the rest of DML as case-insensitive
23902 // idents).
23903 let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23904 let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23905 let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23906 let body = match self.peek() {
23907 Token::Select => {
23908 let inner = self.parse_select_stmt()?;
23909 let Statement::Select(s) = inner else {
23910 unreachable!("parse_select_stmt returns Select");
23911 };
23912 crate::ast::CteBody::Select(s)
23913 }
23914 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23915 // `SELECT * FROM t` this way and accepts it wherever a
23916 // SELECT goes, so the CTE body dispatch needs its own
23917 // arm: this match is keyed on the FIRST token, and
23918 // `Token::Table` fell through to a tail that then
23919 // rejected what it got. `parse_table_shorthand` has
23920 // returned a desugared SelectStatement since the
23921 // shorthand landed — only the routing was missing.
23922 // Round 868 found this by putting the shorthand in a
23923 // subquery; every earlier check used a top-level form.
23924 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23925 // `SELECT * FROM t` this way and accepts it wherever a
23926 // SELECT goes, so the CTE body dispatch needs its own
23927 // arm: this match is keyed on the FIRST token, and
23928 // `Token::Table` fell through to a tail that rejected
23929 // what it got. `parse_table_shorthand` has returned a
23930 // desugared SelectStatement since the shorthand landed —
23931 // only the routing was missing, here and in the derived
23932 // table's second-token gate. Round 868 found both by
23933 // putting the shorthand in a subquery; every earlier
23934 // check had used a top-level form.
23935 Token::Table
23936 if matches!(
23937 self.tokens.get(self.pos + 1),
23938 Some(Token::Ident(_) | Token::QuotedIdent(_))
23939 ) =>
23940 {
23941 let mut head = self.parse_table_shorthand()?;
23942 self.parse_setop_chain_into(&mut head)?;
23943 self.parse_select_tail_into(&mut head)?;
23944 crate::ast::CteBody::Select(head)
23945 }
23946 // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
23947 // WITH t(a) AS (VALUES (1), (2)) … lowers through
23948 // the shared rows helper onto a Select body.
23949 Token::Values => {
23950 self.advance(); // VALUES
23951 let mut head = self.parse_values_rows_body()?;
23952 // A VALUES seed can head a set-operation chain —
23953 // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
23954 // SELECT n+1 FROM t …). Attach any trailing
23955 // UNION / INTERSECT / EXCEPT peers so the
23956 // recursive-CTE body parses like the SELECT seed.
23957 self.parse_setop_chain_into(&mut head)?;
23958 crate::ast::CteBody::Select(head)
23959 }
23960 Token::Insert => {
23961 let inner = self.parse_one_statement()?;
23962 let Statement::Insert(s) = inner else {
23963 unreachable!("Token::Insert routes to Insert");
23964 };
23965 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23966 }
23967 _ if is_update_kw => {
23968 let inner = self.parse_one_statement()?;
23969 let Statement::Update(s) = inner else {
23970 return Err(
23971 self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
23972 );
23973 };
23974 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23975 }
23976 _ if is_delete_kw => {
23977 let inner = self.parse_one_statement()?;
23978 let Statement::Delete(s) = inner else {
23979 return Err(
23980 self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
23981 );
23982 };
23983 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23984 }
23985 // v7.39 (round 149) — PG 17 allows MERGE as a
23986 // data-modifying CTE body.
23987 _ if is_merge_kw => {
23988 let inner = self.parse_one_statement()?;
23989 let Statement::Merge(s) = inner else {
23990 return Err(
23991 self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
23992 );
23993 };
23994 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23995 }
23996 // v7.39 (round 151) — a CTE body may itself be
23997 // WITH-headed (PG grammar: PreparableStmt carries its
23998 // own with_clause). The nested statement keeps its own
23999 // ctes; the modifying-CTE-at-top-level rule is enforced
24000 // at execution.
24001 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
24002 self.advance(); // WITH
24003 match self.parse_with_cte_then_select()? {
24004 Statement::Select(s) => crate::ast::CteBody::Select(s),
24005 Statement::Insert(s) => {
24006 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24007 }
24008 Statement::Update(s) => {
24009 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24010 }
24011 Statement::Delete(s) => {
24012 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24013 }
24014 Statement::Merge(s) => {
24015 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24016 }
24017
24018 other => {
24019 return Err(self.err(format!(
24020 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24021 )));
24022 }
24023 }
24024 }
24025 other => {
24026 return Err(self.err(format!(
24027 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24028 )));
24029 }
24030 };
24031 if !matches!(self.peek(), Token::RParen) {
24032 return Err(self.err(format!(
24033 "expected ')' after CTE body, got {:?}",
24034 self.peek()
24035 )));
24036 }
24037 self.advance();
24038 // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
24039 // CTE, desugared into extra body columns by the engine.
24040 let search = self.parse_cte_search_clause()?;
24041 let cycle = self.parse_cte_cycle_clause()?;
24042 let mut cte = crate::ast::Cte {
24043 name,
24044 body,
24045 recursive,
24046 column_overrides,
24047 search,
24048 cycle,
24049 };
24050 self.validate_recursive_cte(&cte)?;
24051 self.desugar_cte_search_cycle(&mut cte)?;
24052 ctes.push(cte);
24053 if matches!(self.peek(), Token::Comma) {
24054 self.advance();
24055 continue;
24056 }
24057 break;
24058 }
24059 // v7.37.43-T4.4 — the outer body may be SELECT (classical),
24060 // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
24061 // the parsed CTEs to whichever statement the body produces.
24062 let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24063 let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24064 let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24065 match self.peek() {
24066 Token::Select => {
24067 let body_stmt = self.parse_select_stmt()?;
24068 let Statement::Select(mut body) = body_stmt else {
24069 unreachable!()
24070 };
24071 body.ctes = ctes;
24072 Ok(Statement::Select(body))
24073 }
24074 Token::Insert => {
24075 let body_stmt = self.parse_one_statement()?;
24076 let Statement::Insert(mut body) = body_stmt else {
24077 unreachable!()
24078 };
24079 body.ctes = ctes;
24080 Ok(Statement::Insert(body))
24081 }
24082 _ if outer_is_update => {
24083 let body_stmt = self.parse_one_statement()?;
24084 let Statement::Update(mut body) = body_stmt else {
24085 return Err(self.err(format!("expected UPDATE after WITH clause")));
24086 };
24087 body.ctes = ctes;
24088 Ok(Statement::Update(body))
24089 }
24090 _ if outer_is_delete => {
24091 let body_stmt = self.parse_one_statement()?;
24092 let Statement::Delete(mut body) = body_stmt else {
24093 return Err(self.err(format!("expected DELETE after WITH clause")));
24094 };
24095 body.ctes = ctes;
24096 Ok(Statement::Delete(body))
24097 }
24098 // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
24099 // WITH RECURSIVE is rejected with PG's exact message
24100 // (parse analysis, transformWithClause).
24101 _ if outer_is_merge => {
24102 if recursive {
24103 return Err(self.err(String::from(
24104 "WITH RECURSIVE is not supported for MERGE statement",
24105 )));
24106 }
24107 let body_stmt = self.parse_one_statement()?;
24108 let Statement::Merge(mut body) = body_stmt else {
24109 return Err(self.err(format!("expected MERGE after WITH clause")));
24110 };
24111 body.ctes = ctes;
24112 Ok(Statement::Merge(body))
24113 }
24114 other => Err(self.err(format!(
24115 "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
24116 ))),
24117 }
24118 }
24119
24120 /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
24121 /// already consumed the leading `EXISTS` ident via
24122 /// `self.advance()`.
24123 /// v7.13.0 — parse the rest of a `CASE … END` expression after
24124 /// the leading `CASE` ident has been consumed (mailrs round-5
24125 /// G9). Supports both the searched form
24126 /// (`CASE WHEN cond THEN val …`) and the simple form
24127 /// (`CASE operand WHEN val THEN val …`).
24128 fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
24129 // Disambiguate searched vs simple form: if the next token
24130 // is `WHEN`, we're in the searched form. Otherwise the
24131 // intervening expression is the operand.
24132 let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
24133 None
24134 } else {
24135 Some(Box::new(self.parse_expr(0)?))
24136 };
24137 let mut branches: Vec<(Expr, Expr)> = Vec::new();
24138 loop {
24139 match self.peek() {
24140 Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
24141 self.advance();
24142 let cond = self.parse_expr(0)?;
24143 match self.peek() {
24144 Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
24145 self.advance();
24146 }
24147 other => {
24148 return Err(self.err(alloc::format!(
24149 "expected THEN after CASE WHEN <expr>, got {other:?}"
24150 )));
24151 }
24152 }
24153 let value = self.parse_expr(0)?;
24154 branches.push((cond, value));
24155 }
24156 _ => break,
24157 }
24158 }
24159 if branches.is_empty() {
24160 return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
24161 }
24162 let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
24163 {
24164 self.advance();
24165 Some(Box::new(self.parse_expr(0)?))
24166 } else {
24167 None
24168 };
24169 match self.peek() {
24170 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
24171 self.advance();
24172 }
24173 other => {
24174 return Err(self.err(alloc::format!(
24175 "expected END to close CASE expression, got {other:?}"
24176 )));
24177 }
24178 }
24179 Ok(Expr::Case {
24180 operand,
24181 branches,
24182 else_branch,
24183 })
24184 }
24185
24186 /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
24187 /// query-source position (EXISTS / IN / INSERT source / CTE body /
24188 /// view body). Caller consumed the WITH keyword. Only a SELECT
24189 /// outer is grammatical here; the data-modifying-CTE-at-top-level
24190 /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
24191 /// maps correctly.
24192 fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24193 let inner = self.parse_with_cte_then_select()?;
24194 match inner {
24195 Statement::Select(s) => Ok(s),
24196 other => Err(self.err(format!(
24197 "expected SELECT after WITH in a subquery, got {other:?}"
24198 ))),
24199 }
24200 }
24201
24202 /// True when the next token is the (unquoted) WITH keyword. WITH is
24203 /// reserved in PG, so a bare `with` can never be a column reference
24204 /// in these positions; a quoted `"with"` stays an identifier.
24205 fn peek_is_with_kw(&self) -> bool {
24206 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
24207 }
24208
24209 /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
24210 /// `#[inline(never)]` keeps the large SelectStatement temporaries
24211 /// off parse_expr's recursive frame (the nesting-budget stack
24212 /// cliff — see the round-153 gate regression).
24213 #[inline(never)]
24214 fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24215 if self.peek_is_with_kw() {
24216 self.advance();
24217 self.parse_nested_with_select()
24218 } else {
24219 match self.parse_select_stmt()? {
24220 Statement::Select(s) => Ok(s),
24221 other => Err(self.err(alloc::format!(
24222 "expected SELECT inside ANY/ALL, got {other:?}"
24223 ))),
24224 }
24225 }
24226 }
24227
24228 fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
24229 if !matches!(self.peek(), Token::LParen) {
24230 return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
24231 }
24232 self.advance();
24233 // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
24234 let s = if self.peek_is_with_kw() {
24235 self.advance();
24236 self.parse_nested_with_select()?
24237 } else {
24238 let inner = self.parse_select_stmt()?;
24239 let Statement::Select(s) = inner else {
24240 unreachable!("parse_select_stmt returns Select")
24241 };
24242 s
24243 };
24244 if !matches!(self.peek(), Token::RParen) {
24245 return Err(self.err(format!(
24246 "expected ')' after EXISTS-subquery, got {:?}",
24247 self.peek()
24248 )));
24249 }
24250 self.advance();
24251 Ok(Expr::Exists {
24252 subquery: Box::new(s),
24253 negated,
24254 })
24255 }
24256
24257 fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
24258 self.advance(); // IN
24259 if !matches!(self.peek(), Token::LParen) {
24260 return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
24261 }
24262 self.advance();
24263 // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
24264 // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
24265 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
24266 let s = if self.peek_is_with_kw() {
24267 self.advance();
24268 self.parse_nested_with_select()?
24269 } else {
24270 let inner = self.parse_select_stmt()?;
24271 let Statement::Select(s) = inner else {
24272 unreachable!("parse_select_stmt always returns Statement::Select")
24273 };
24274 s
24275 };
24276 if !matches!(self.peek(), Token::RParen) {
24277 return Err(self.err(format!(
24278 "expected ')' after IN-subquery, got {:?}",
24279 self.peek()
24280 )));
24281 }
24282 self.advance();
24283 return Ok(Expr::InSubquery {
24284 expr: Box::new(expr),
24285 subquery: Box::new(s),
24286 negated,
24287 });
24288 }
24289 let mut elements = Vec::new();
24290 if !matches!(self.peek(), Token::RParen) {
24291 loop {
24292 elements.push(self.parse_expr(0)?);
24293 match self.peek() {
24294 Token::Comma => {
24295 self.advance();
24296 }
24297 Token::RParen => break,
24298 other => {
24299 return Err(
24300 self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
24301 );
24302 }
24303 }
24304 }
24305 }
24306 self.advance(); // ')'
24307 // v7.30.2 (mailrs round-25) — flat InList node instead of a
24308 // left-deep OR-Eq chain: chain depth scaled with the element
24309 // count and overflowed the stack (eval + drop are recursive).
24310 if elements.is_empty() {
24311 return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
24312 }
24313 Ok(Expr::InList {
24314 expr: Box::new(expr),
24315 list: elements,
24316 negated,
24317 })
24318 }
24319
24320 /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
24321 /// already consumed by the caller. Elements must be numeric literals
24322 /// (with optional unary `-`); any compound expression is rejected at
24323 /// parse time so the runtime never needs to evaluate inside a vector.
24324 /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
24325 /// has already consumed the `EXTRACT` token before calling us —
24326 /// we pick up at the opening `(`.
24327 /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
24328 /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
24329 /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
24330 /// per-column OR-fold of
24331 /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
24332 /// term)` so the existing FTS evaluator handles semantics.
24333 ///
24334 /// The mode modifier is accepted-and-ignored at v7.17 — all
24335 /// modes map to the same `plainto_tsquery` rewrite. Boolean-
24336 /// mode operators (`+foo -bar`) would need their own parser
24337 /// (Phase 2.2c); customers who hit them today already get a
24338 /// correct lexeme-match against the bare term, only without
24339 /// the +/- precedence the customer asked for.
24340 fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
24341 // Already at `MATCH`-consumed position; the dispatcher
24342 // confirmed the next token is `(`.
24343 if !matches!(self.peek(), Token::LParen) {
24344 return Err(self.err(alloc::format!(
24345 "expected '(' after MATCH, got {:?}",
24346 self.peek()
24347 )));
24348 }
24349 self.advance();
24350 let mut cols: Vec<Expr> = Vec::new();
24351 loop {
24352 cols.push(self.parse_expr(0)?);
24353 match self.peek() {
24354 Token::Comma => {
24355 self.advance();
24356 }
24357 Token::RParen => break,
24358 other => {
24359 return Err(self.err(alloc::format!(
24360 "expected ',' or ')' in MATCH column list, got {other:?}"
24361 )));
24362 }
24363 }
24364 }
24365 self.advance(); // ')'
24366 // Expect AGAINST.
24367 match self.peek() {
24368 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
24369 self.advance();
24370 }
24371 other => {
24372 return Err(self.err(alloc::format!(
24373 "expected AGAINST after MATCH column list, got {other:?}"
24374 )));
24375 }
24376 }
24377 if !matches!(self.peek(), Token::LParen) {
24378 return Err(self.err(alloc::format!(
24379 "expected '(' after AGAINST, got {:?}",
24380 self.peek()
24381 )));
24382 }
24383 self.advance();
24384 // Read AGAINST's argument as a single primary token —
24385 // string literal, placeholder, or column-ref ident. We
24386 // can't call `parse_expr` / `parse_unary` here because
24387 // the postfix chain inside `parse_atom` would greedily
24388 // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
24389 // and fail at "expected '(' after IN". Customers always
24390 // write a literal or bound parameter in AGAINST, so this
24391 // restriction is non-blocking; the error path explains
24392 // the limit if a more complex expression shows up.
24393 let term = match self.advance() {
24394 Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
24395 Token::Placeholder(n) => Expr::Placeholder(n),
24396 Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
24397 qualifier: None,
24398 name: s,
24399 }),
24400 other => {
24401 return Err(self.err(alloc::format!(
24402 "MATCH ... AGAINST(<term>) expects a string literal, \
24403 bound parameter, or column ref, got {other:?}"
24404 )));
24405 }
24406 };
24407 // Optional mode tail — accept-and-ignore at v7.17:
24408 // IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
24409 // IN BOOLEAN MODE
24410 // WITH QUERY EXPANSION
24411 loop {
24412 match self.peek() {
24413 // IN lexes as a reserved Token::In, not an ident,
24414 // so it gets its own arm.
24415 Token::In => {
24416 self.advance();
24417 }
24418 Token::Ident(s) | Token::QuotedIdent(s)
24419 if s.eq_ignore_ascii_case("natural")
24420 || s.eq_ignore_ascii_case("language")
24421 || s.eq_ignore_ascii_case("boolean")
24422 || s.eq_ignore_ascii_case("mode")
24423 || s.eq_ignore_ascii_case("with")
24424 || s.eq_ignore_ascii_case("query")
24425 || s.eq_ignore_ascii_case("expansion") =>
24426 {
24427 self.advance();
24428 }
24429 _ => break,
24430 }
24431 }
24432 if !matches!(self.peek(), Token::RParen) {
24433 return Err(self.err(alloc::format!(
24434 "expected ')' to close AGAINST, got {:?}",
24435 self.peek()
24436 )));
24437 }
24438 self.advance();
24439 // Build per-column `to_tsvector('simple', col) @@
24440 // plainto_tsquery('simple', term)` and OR-fold.
24441 let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24442 let plainto = Expr::FunctionCall {
24443 name: String::from("plainto_tsquery"),
24444 args: alloc::vec![simple_lit(), term.clone()],
24445 };
24446 let mut folded: Option<Expr> = None;
24447 for col in cols {
24448 let to_tsv = Expr::FunctionCall {
24449 name: String::from("to_tsvector"),
24450 args: alloc::vec![simple_lit(), col],
24451 };
24452 let leaf = Expr::Binary {
24453 lhs: Box::new(to_tsv),
24454 op: crate::ast::BinOp::TsMatch,
24455 rhs: Box::new(plainto.clone()),
24456 };
24457 folded = Some(match folded {
24458 None => leaf,
24459 Some(prev) => Expr::Binary {
24460 lhs: Box::new(prev),
24461 op: crate::ast::BinOp::Or,
24462 rhs: Box::new(leaf),
24463 },
24464 });
24465 }
24466 match folded {
24467 Some(e) => Ok(e),
24468 None => Err(self.err(String::from(
24469 "MATCH(...) AGAINST(...) requires at least one column",
24470 ))),
24471 }
24472 }
24473
24474 fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24475 if !matches!(self.peek(), Token::LParen) {
24476 return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24477 }
24478 self.advance();
24479 let field_name = self.expect_ident_like()?;
24480 let field = match field_name.to_ascii_lowercase().as_str() {
24481 // PG accepts the plural spellings (years/months/…/millenniums) as
24482 // aliases for the singular fields — its datetime unit table has both.
24483 // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24484 "year" | "years" => ExtractField::Year,
24485 "month" | "months" => ExtractField::Month,
24486 "day" | "days" => ExtractField::Day,
24487 "hour" | "hours" => ExtractField::Hour,
24488 "minute" | "minutes" => ExtractField::Minute,
24489 "second" | "seconds" => ExtractField::Second,
24490 "microsecond" | "microseconds" => ExtractField::Microsecond,
24491 "epoch" => ExtractField::Epoch,
24492 "dow" => ExtractField::Dow,
24493 "isodow" => ExtractField::Isodow,
24494 "doy" => ExtractField::Doy,
24495 "week" | "weeks" => ExtractField::Week,
24496 "isoyear" => ExtractField::Isoyear,
24497 "quarter" => ExtractField::Quarter,
24498 "decade" | "decades" => ExtractField::Decade,
24499 "century" | "centuries" => ExtractField::Century,
24500 "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24501 "julian" => ExtractField::Julian,
24502 "millisecond" | "milliseconds" => ExtractField::Millisecond,
24503 "timezone" => ExtractField::Timezone,
24504 "timezone_hour" => ExtractField::TimezoneHour,
24505 "timezone_minute" => ExtractField::TimezoneMinute,
24506 // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24507 // reports an unknown one with the source type (22023); carry the
24508 // raw name so eval can word it.
24509 other => ExtractField::Other(alloc::string::String::from(other)),
24510 };
24511 if !matches!(self.peek(), Token::From) {
24512 return Err(self.err(format!(
24513 "expected FROM after EXTRACT field, got {:?}",
24514 self.peek()
24515 )));
24516 }
24517 self.advance();
24518 let source = self.parse_expr(0)?;
24519 if !matches!(self.peek(), Token::RParen) {
24520 return Err(self.err(format!(
24521 "expected ')' to close EXTRACT, got {:?}",
24522 self.peek()
24523 )));
24524 }
24525 self.advance();
24526 Ok(Expr::Extract {
24527 field,
24528 source: Box::new(source),
24529 })
24530 }
24531
24532 /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24533 /// is already consumed; we expect a single string literal next and
24534 /// resolve it into `Literal::Interval` at parse time so the engine
24535 /// never has to re-tokenise inside the string.
24536 /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24537 /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24538 /// is the SQL-standard form and is left to the path below.
24539 fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24540 // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24541 let (offset, sign) = match self.peek() {
24542 Token::Minus => (1, "-"),
24543 _ => (0, ""),
24544 };
24545 let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24546 return None;
24547 };
24548 self.tokens
24549 .get(self.pos + offset + 1)
24550 .filter(|t| mysql_interval_unit(t).is_some())?;
24551 Some((alloc::format!("{sign}{n}"), offset + 1))
24552 }
24553
24554 /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24555 /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24556 /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24557 ///
24558 /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24559 /// this by parsing the group and then restoring `self.pos` — which could
24560 /// never have worked, because `advance()` DESTROYS the token it returns
24561 /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24562 /// inert only because both branches errored back then.
24563 fn interval_paren_is_quantity(&self) -> bool {
24564 let mut depth = 0usize;
24565 let mut saw_top_level_comma = false;
24566 let mut i = self.pos;
24567 while let Some(tok) = self.tokens.get(i) {
24568 match tok {
24569 Token::LParen => depth += 1,
24570 Token::RParen => {
24571 depth = depth.saturating_sub(1);
24572 if depth == 0 {
24573 return !saw_top_level_comma
24574 && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
24575 .is_some();
24576 }
24577 }
24578 // A comma directly inside the outermost parens means the
24579 // argument list of the INTERVAL() function.
24580 Token::Comma if depth == 1 => saw_top_level_comma = true,
24581 Token::Eof => return false,
24582 _ => {}
24583 }
24584 i += 1;
24585 }
24586 false
24587 }
24588
24589 fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
24590 // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
24591 // (the index of the last Ni ≤ N), distinct from the interval literal.
24592 // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
24593 // is decided by a non-destructive lookahead (round 422) before either
24594 // branch consumes anything. MySQL only.
24595 if self.mysql_dialect
24596 && matches!(self.peek(), Token::LParen)
24597 && !self.interval_paren_is_quantity()
24598 {
24599 self.advance(); // (
24600 let mut args = Vec::new();
24601 if !matches!(self.peek(), Token::RParen) {
24602 loop {
24603 args.push(self.parse_expr(0)?);
24604 if matches!(self.peek(), Token::Comma) {
24605 self.advance();
24606 continue;
24607 }
24608 break;
24609 }
24610 }
24611 if !matches!(self.peek(), Token::RParen) {
24612 return Err(self.err(alloc::format!(
24613 "expected ')' after INTERVAL() arguments, got {:?}",
24614 self.peek()
24615 )));
24616 }
24617 self.advance(); // )
24618 return Ok(Expr::FunctionCall {
24619 name: alloc::string::String::from("interval"),
24620 args,
24621 });
24622 }
24623 // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
24624 // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
24625 // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
24626 // writes every date arithmetic there is, and it did not parse at
24627 // all. PG rejects the unquoted form outright (`syntax error at or
24628 // near "1"`, measured), so it is taken only in the MySQL dialect —
24629 // PG's own `INTERVAL '1' DAY` is untouched below.
24630 if self.mysql_dialect
24631 && let Some((text, consume)) = self.peek_unquoted_interval_count()
24632 {
24633 for _ in 0..consume {
24634 self.advance(); // the optional `-` and the number
24635 }
24636 let Some(unit) = mysql_interval_unit(self.peek()) else {
24637 return Err(self.err(alloc::format!(
24638 "expected an interval unit after INTERVAL {text}, got {:?}",
24639 self.peek()
24640 )));
24641 };
24642 self.advance(); // the unit
24643 let (months, days, micros) = scale_mysql_interval(&text, unit)
24644 .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
24645 return Ok(Expr::Literal(Literal::Interval {
24646 months,
24647 days,
24648 micros,
24649 // The canonical rendering, so Display round-trips into a
24650 // form both dialects read back.
24651 text: alloc::format!("{text} {unit}"),
24652 }));
24653 }
24654 // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
24655 // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
24656 // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
24657 // Those cannot fold into a compile-time `Literal::Interval`, so they
24658 // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
24659 // builtin, which builds the value at run time (and yields NULL for a
24660 // NULL quantity, as MariaDB does). The literal path above still folds
24661 // the constant case — it is cheaper and round-trips through Display.
24662 //
24663 // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
24664 // MySQL's quoted spelling) keep the qualifier path below.
24665 if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
24666 let qty = self.parse_expr(0)?;
24667 let Some(unit) = mysql_interval_unit(self.peek()) else {
24668 return Err(self.err(alloc::format!(
24669 "expected an interval unit after INTERVAL <expr>, got {:?}",
24670 self.peek()
24671 )));
24672 };
24673 self.advance(); // the unit
24674 return Ok(make_interval_call(qty, unit));
24675 }
24676 let tok = self.advance();
24677 let Token::String(text) = tok else {
24678 return Err(self.err(format!(
24679 "expected string literal after INTERVAL, got {tok:?}"
24680 )));
24681 };
24682 // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
24683 // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
24684 // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
24685 // bare number means and the leading/trailing precision.
24686 let field1 = interval_field_of(self.peek());
24687 let qualifier = if let Some(f1) = field1 {
24688 self.advance();
24689 let f2 = if matches!(self.peek(), Token::To) {
24690 self.advance();
24691 let Some(f) = interval_field_of(self.peek()) else {
24692 return Err(self.err(format!(
24693 "expected an interval field after TO, got {:?}",
24694 self.peek()
24695 )));
24696 };
24697 self.advance();
24698 Some(f)
24699 } else {
24700 None
24701 };
24702 Some((f1, f2))
24703 } else {
24704 None
24705 };
24706 let (months, days, micros) = match qualifier {
24707 Some(q) => interpret_qualified_interval(&text, q),
24708 None => parse_interval_text(&text),
24709 }
24710 .ok_or_else(|| ParseError {
24711 message: format!(
24712 "cannot parse INTERVAL {text:?}; \
24713 expected `<n> <unit> [<n> <unit> ...]` with units \
24714 microsecond[s], millisecond[s], second[s], minute[s], \
24715 hour[s], day[s], week[s], month[s], year[s]"
24716 ),
24717 token_pos: self.consumed_pos(),
24718 })?;
24719 Ok(Expr::Literal(Literal::Interval {
24720 months,
24721 days,
24722 micros,
24723 text,
24724 }))
24725 }
24726
24727 /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
24728 /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
24729 /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
24730 /// than a pgvector literal.
24731 fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
24732 self.advance(); // consume `[`
24733 let mut items: Vec<Expr> = Vec::new();
24734 if !matches!(self.peek(), Token::RBracket) {
24735 loop {
24736 if matches!(self.peek(), Token::LBracket) {
24737 items.push(self.parse_array_bracket_body()?);
24738 } else {
24739 items.push(self.parse_expr(0)?);
24740 }
24741 match self.peek() {
24742 Token::Comma => {
24743 self.advance();
24744 }
24745 Token::RBracket => break,
24746 other => {
24747 return Err(self.err(alloc::format!(
24748 "expected ',' or ']' in array literal, got {other:?}"
24749 )));
24750 }
24751 }
24752 }
24753 }
24754 self.advance(); // consume `]`
24755 Ok(Expr::Array(items))
24756 }
24757
24758 fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
24759 let mut elems = Vec::new();
24760 if matches!(self.peek(), Token::RBracket) {
24761 self.advance();
24762 return Ok(Expr::Literal(Literal::Vector(elems)));
24763 }
24764 loop {
24765 let e = self.parse_expr(0)?;
24766 let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
24767 message: format!("vector element must be a numeric literal, got {e:?}"),
24768 token_pos: self.pos,
24769 })?;
24770 elems.push(x);
24771 match self.peek() {
24772 Token::Comma => {
24773 self.advance();
24774 }
24775 Token::RBracket => {
24776 self.advance();
24777 break;
24778 }
24779 other => {
24780 return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
24781 }
24782 }
24783 }
24784 Ok(Expr::Literal(Literal::Vector(elems)))
24785 }
24786
24787 /// Atom that started with an identifier: could be `t.col`, `col`, or
24788 /// `func(arg, ...)`. Detect each shape by looking at the next token.
24789 /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
24790 /// [, ...])`. Caller has already consumed `OVER`. Either clause
24791 /// is optional; an empty `()` is also legal (PG semantics).
24792 /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
24793 /// modifier between `name(args)` and `OVER (...)`. Default is
24794 /// `Respect`. Unrecognised idents leave the stream unchanged.
24795 fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
24796 let Token::Ident(s) = self.peek().clone() else {
24797 return NullTreatment::Respect;
24798 };
24799 let is_ignore = s.eq_ignore_ascii_case("ignore");
24800 let is_respect = s.eq_ignore_ascii_case("respect");
24801 if !is_ignore && !is_respect {
24802 return NullTreatment::Respect;
24803 }
24804 // Lookahead for NULLS — only consume both tokens together.
24805 // pos+1 must hold a "nulls" ident.
24806 if self.pos + 1 < self.tokens.len()
24807 && let Token::Ident(s2) = &self.tokens[self.pos + 1]
24808 && s2.eq_ignore_ascii_case("nulls")
24809 {
24810 self.advance();
24811 self.advance();
24812 return if is_ignore {
24813 NullTreatment::Ignore
24814 } else {
24815 NullTreatment::Respect
24816 };
24817 }
24818 NullTreatment::Respect
24819 }
24820
24821 /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
24822 /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
24823 /// (same shape as the `OVER` tail). Consumes the whole clause and
24824 /// returns the predicate; returns `None` when no `FILTER` follows.
24825 fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
24826 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24827 return Ok(None);
24828 };
24829 if !s.eq_ignore_ascii_case("filter") {
24830 return Ok(None);
24831 }
24832 self.advance(); // FILTER
24833 if !matches!(self.peek(), Token::LParen) {
24834 return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
24835 }
24836 self.advance(); // (
24837 if !matches!(self.peek(), Token::Where) {
24838 return Err(self.err(format!(
24839 "expected WHERE inside FILTER (...), got {:?}",
24840 self.peek()
24841 )));
24842 }
24843 self.advance(); // WHERE
24844 let cond = self.parse_expr(0)?;
24845 if !matches!(self.peek(), Token::RParen) {
24846 return Err(self.err(format!(
24847 "expected ')' to close FILTER (WHERE ...), got {:?}",
24848 self.peek()
24849 )));
24850 }
24851 self.advance(); // )
24852 Ok(Some(Box::new(cond)))
24853 }
24854
24855 /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
24856 /// the separator as the aggregate's second argument, which is the
24857 /// shape `string_agg` already takes. Returns whether one was there.
24858 fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
24859 if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
24860 return Ok(false);
24861 }
24862 self.advance();
24863 let Token::String(sep) = self.peek().clone() else {
24864 return Err(self.err(alloc::format!(
24865 "expected a string literal after SEPARATOR, got {:?}",
24866 self.peek()
24867 )));
24868 };
24869 self.advance();
24870 args.push(Expr::Literal(Literal::String(sep)));
24871 Ok(true)
24872 }
24873
24874 /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
24875 /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
24876 /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
24877 /// keys, or an empty vec when no `WITHIN GROUP` follows.
24878 fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
24879 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24880 return Ok(Vec::new());
24881 };
24882 if !s.eq_ignore_ascii_case("within") {
24883 return Ok(Vec::new());
24884 }
24885 self.advance(); // WITHIN
24886 if !matches!(self.peek(), Token::Group) {
24887 return Err(self.err(format!(
24888 "expected GROUP after WITHIN, got {:?}",
24889 self.peek()
24890 )));
24891 }
24892 self.advance(); // GROUP
24893 if !matches!(self.peek(), Token::LParen) {
24894 return Err(self.err(format!(
24895 "expected '(' after WITHIN GROUP, got {:?}",
24896 self.peek()
24897 )));
24898 }
24899 self.advance(); // (
24900 if !matches!(self.peek(), Token::Order) {
24901 return Err(self.err(format!(
24902 "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
24903 self.peek()
24904 )));
24905 }
24906 self.advance(); // ORDER
24907 if !self.peek_is_by() {
24908 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24909 }
24910 self.advance(); // BY
24911 let mut keys: Vec<OrderBy> = Vec::new();
24912 loop {
24913 // v7.39 (round 691) — save/restore, the discipline this parser
24914 // already uses around `pending_sample_preds`, so a subquery inside
24915 // a key neither inherits nor leaks the channel.
24916 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
24917 let saved_coll = self.order_key_collation.take();
24918 let parsed = self.parse_expr(0);
24919 self.in_order_by_key = saved_flag;
24920 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
24921 let expr = parsed?;
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 let nulls_first = self.parse_optional_nulls_placement()?;
24932 keys.push(OrderBy {
24933 expr,
24934 desc,
24935 nulls_first,
24936 collation,
24937 });
24938 if matches!(self.peek(), Token::Comma) {
24939 self.advance();
24940 } else {
24941 break;
24942 }
24943 }
24944 if !matches!(self.peek(), Token::RParen) {
24945 return Err(self.err(format!(
24946 "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
24947 self.peek()
24948 )));
24949 }
24950 self.advance(); // )
24951 Ok(keys)
24952 }
24953
24954 /// No frame clause is supported.
24955 #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
24956 fn parse_over_clause(
24957 &mut self,
24958 ) -> Result<
24959 (
24960 Vec<Expr>,
24961 Vec<(Expr, bool, Option<bool>)>,
24962 Option<WindowFrame>,
24963 ),
24964 ParseError,
24965 > {
24966 // `OVER w` — a named-window reference. The WINDOW clause
24967 // parses after the select list, so the name rides out as a
24968 // marker in partition_by; parse_bare_select substitutes the
24969 // definition once the clause is known.
24970 if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
24971 let name = w.clone();
24972 self.advance();
24973 return Ok((
24974 alloc::vec![Expr::Column(crate::ast::ColumnName {
24975 qualifier: Some("__named_window__".to_string()),
24976 name,
24977 })],
24978 Vec::new(),
24979 None,
24980 ));
24981 }
24982 if !matches!(self.peek(), Token::LParen) {
24983 return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
24984 }
24985 self.advance();
24986 let mut partition_by = Vec::new();
24987 let mut order_by = Vec::new();
24988 // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
24989 // window, refined in place. PG's rules (probed against 18.4) differ
24990 // from the bare `OVER w1` form, so the reference rides out under its
24991 // own marker and `substitute_named_windows` applies them. The base
24992 // name is any leading identifier that isn't a window-spec keyword.
24993 let base_window = match self.peek() {
24994 Token::Ident(s) | Token::QuotedIdent(s)
24995 if !s.eq_ignore_ascii_case("partition")
24996 && !s.eq_ignore_ascii_case("rows")
24997 && !s.eq_ignore_ascii_case("range")
24998 && !s.eq_ignore_ascii_case("groups") =>
24999 {
25000 let n = s.clone();
25001 self.advance();
25002 Some(n)
25003 }
25004 _ => None,
25005 };
25006 // PARTITION BY ?
25007 // v7.37.6-B promoted PARTITION to a reserved keyword
25008 // (Token::Partition); pre-7.37.6-B catalogs lexed it as
25009 // `Token::Ident("partition")`. Accept both so older sources
25010 // and the new lexer surface land on the same path.
25011 let is_partition_kw = match self.peek() {
25012 Token::Partition => true,
25013 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
25014 _ => false,
25015 };
25016 if is_partition_kw {
25017 self.advance();
25018 if !self.peek_is_by() {
25019 return Err(self.err(format!(
25020 "expected BY after PARTITION, got {:?}",
25021 self.peek()
25022 )));
25023 }
25024 self.advance();
25025 loop {
25026 partition_by.push(self.parse_expr(0)?);
25027 if matches!(self.peek(), Token::Comma) {
25028 self.advance();
25029 continue;
25030 }
25031 break;
25032 }
25033 }
25034 // ORDER BY ?
25035 if matches!(self.peek(), Token::Order) {
25036 self.advance();
25037 if !self.peek_is_by() {
25038 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25039 }
25040 self.advance();
25041 loop {
25042 let e = self.parse_expr(0)?;
25043 let desc = if matches!(self.peek(), Token::Desc) {
25044 self.advance();
25045 true
25046 } else if matches!(self.peek(), Token::Asc) {
25047 self.advance();
25048 false
25049 } else {
25050 false
25051 };
25052 // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
25053 let nulls_first = self.parse_optional_nulls_placement()?;
25054 order_by.push((e, desc, nulls_first));
25055 if matches!(self.peek(), Token::Comma) {
25056 self.advance();
25057 continue;
25058 }
25059 break;
25060 }
25061 }
25062 // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
25063 // Both keywords come through the lexer as identifiers; match
25064 // case-insensitively.
25065 let mut frame: Option<WindowFrame> = None;
25066 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
25067 let kind = if s.eq_ignore_ascii_case("rows") {
25068 Some(FrameKind::Rows)
25069 } else if s.eq_ignore_ascii_case("range") {
25070 Some(FrameKind::Range)
25071 } else if s.eq_ignore_ascii_case("groups") {
25072 // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
25073 Some(FrameKind::Groups)
25074 } else {
25075 None
25076 };
25077 if let Some(kind) = kind {
25078 self.advance();
25079 frame = Some(self.parse_frame_tail(kind)?);
25080 }
25081 }
25082 if !matches!(self.peek(), Token::RParen) {
25083 return Err(self.err(format!(
25084 "expected ')' to close OVER clause, got {:?}",
25085 self.peek()
25086 )));
25087 }
25088 self.advance();
25089 if let Some(base) = base_window {
25090 // A copy may refine but never override the base's partitioning
25091 // (PG rejects it outright, before looking the name up).
25092 if !partition_by.is_empty() {
25093 return Err(self.err(alloc::format!(
25094 "cannot override PARTITION BY clause of window \"{base}\""
25095 )));
25096 }
25097 partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
25098 qualifier: Some("__named_window_ref__".to_string()),
25099 name: base,
25100 })];
25101 }
25102 Ok((partition_by, order_by, frame))
25103 }
25104
25105 /// v4.20: parse the tail of an explicit frame, given the `ROWS`
25106 /// or `RANGE` keyword was just consumed. Accepts both
25107 /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
25108 /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
25109 /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
25110 fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
25111 let (start, end) = if matches!(self.peek(), Token::Between) {
25112 self.advance();
25113 let start = self.parse_frame_bound()?;
25114 if !matches!(self.peek(), Token::And) {
25115 return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
25116 }
25117 self.advance();
25118 let end = self.parse_frame_bound()?;
25119 (start, Some(end))
25120 } else {
25121 (self.parse_frame_bound()?, None)
25122 };
25123 let exclude = self.parse_frame_exclusion()?;
25124 Ok(WindowFrame {
25125 kind,
25126 start,
25127 end,
25128 exclude,
25129 })
25130 }
25131
25132 /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
25133 /// after a frame spec. NO OTHERS is the default no-op.
25134 fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
25135 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
25136 return Ok(FrameExclusion::NoOthers);
25137 }
25138 self.advance(); // EXCLUDE
25139 match self.peek() {
25140 Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
25141 self.advance();
25142 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
25143 return Err(self.err(format!(
25144 "expected ROW after EXCLUDE CURRENT, got {:?}",
25145 self.peek()
25146 )));
25147 }
25148 self.advance();
25149 Ok(FrameExclusion::CurrentRow)
25150 }
25151 // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
25152 // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
25153 // Without this arm it fell to the catch-all, whose message
25154 // self-contradictingly listed GROUP as expected.
25155 Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
25156 self.advance();
25157 Ok(FrameExclusion::Group)
25158 }
25159 Token::Group => {
25160 self.advance();
25161 Ok(FrameExclusion::Group)
25162 }
25163 Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
25164 self.advance();
25165 Ok(FrameExclusion::Ties)
25166 }
25167 Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
25168 self.advance();
25169 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
25170 return Err(self.err(format!(
25171 "expected OTHERS after EXCLUDE NO, got {:?}",
25172 self.peek()
25173 )));
25174 }
25175 self.advance();
25176 Ok(FrameExclusion::NoOthers)
25177 }
25178 other => Err(self.err(format!(
25179 "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
25180 ))),
25181 }
25182 }
25183
25184 /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
25185 /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
25186 /// `UNBOUNDED FOLLOWING`.
25187 fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
25188 // Interval-typed offset for a value-based RANGE frame over a
25189 // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
25190 // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
25191 // PRECEDING`.
25192 if let Some((months, days, micros)) = self.try_take_interval_offset()? {
25193 let dir = self.expect_ident_like()?;
25194 return if dir.eq_ignore_ascii_case("preceding") {
25195 Ok(FrameBound::IntervalPreceding {
25196 months,
25197 days,
25198 micros,
25199 })
25200 } else if dir.eq_ignore_ascii_case("following") {
25201 Ok(FrameBound::IntervalFollowing {
25202 months,
25203 days,
25204 micros,
25205 })
25206 } else {
25207 Err(self.err(format!(
25208 "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
25209 )))
25210 };
25211 }
25212 // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
25213 if let Token::Integer(n) = *self.peek() {
25214 self.advance();
25215 let n: u64 = u64::try_from(n).map_err(|_| {
25216 self.err(format!(
25217 "invalid frame offset {n} — expected non-negative integer"
25218 ))
25219 })?;
25220 let dir = self.expect_ident_like()?;
25221 return if dir.eq_ignore_ascii_case("preceding") {
25222 Ok(FrameBound::OffsetPreceding(n))
25223 } else if dir.eq_ignore_ascii_case("following") {
25224 Ok(FrameBound::OffsetFollowing(n))
25225 } else {
25226 Err(self.err(format!(
25227 "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
25228 )))
25229 };
25230 }
25231 let first = self.expect_ident_like()?;
25232 if first.eq_ignore_ascii_case("unbounded") {
25233 let dir = self.expect_ident_like()?;
25234 return if dir.eq_ignore_ascii_case("preceding") {
25235 Ok(FrameBound::UnboundedPreceding)
25236 } else if dir.eq_ignore_ascii_case("following") {
25237 Ok(FrameBound::UnboundedFollowing)
25238 } else {
25239 Err(self.err(format!(
25240 "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
25241 )))
25242 };
25243 }
25244 if first.eq_ignore_ascii_case("current") {
25245 let row = self.expect_ident_like()?;
25246 if !row.eq_ignore_ascii_case("row") {
25247 return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
25248 }
25249 return Ok(FrameBound::CurrentRow);
25250 }
25251 Err(self.err(format!(
25252 "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
25253 )))
25254 }
25255
25256 /// Detect and consume a leading interval offset in a frame bound —
25257 /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
25258 /// `(months, days, micros)`. Leaves the cursor on the trailing
25259 /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
25260 /// when the next tokens are not an interval offset.
25261 fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
25262 // Shape A — `INTERVAL '1 day'`.
25263 if matches!(self.peek(), Token::Interval) {
25264 self.advance(); // INTERVAL
25265 let atom = self.parse_interval_atom()?;
25266 if let Expr::Literal(Literal::Interval {
25267 months,
25268 days,
25269 micros,
25270 ..
25271 }) = atom
25272 {
25273 return Ok(Some((months, days, micros)));
25274 }
25275 return Err(self.err("expected an interval literal in frame offset".to_string()));
25276 }
25277 // Shape B — `'1 day'::interval`. Look ahead for the exact
25278 // string / `::` / interval-target triple before committing.
25279 if let Token::String(text) = self.peek() {
25280 let target_is_interval = match self.tokens.get(self.pos + 2) {
25281 Some(Token::Interval) => true,
25282 Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
25283 _ => false,
25284 };
25285 let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
25286 && target_is_interval;
25287 if is_cast {
25288 let text = text.clone();
25289 self.advance(); // string
25290 self.advance(); // ::
25291 self.advance(); // interval
25292 let parts = parse_interval_text(&text).ok_or_else(|| {
25293 self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
25294 })?;
25295 return Ok(Some(parts));
25296 }
25297 }
25298 Ok(None)
25299 }
25300
25301 fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
25302 if matches!(self.peek(), Token::Dot) {
25303 self.advance();
25304 let name = self.expect_ident_like()?;
25305 // v7.14.0 — schema-qualified function call
25306 // `<schema>.<fn>(args)`. PG dumps emit
25307 // `pg_catalog.set_config(...)` in the preamble. SPG
25308 // is single-namespace: drop the schema prefix and
25309 // route the dispatch on the bare function name.
25310 if matches!(self.peek(), Token::LParen) {
25311 return self.finish_ident_atom(name);
25312 }
25313 return Ok(Expr::Column(ColumnName {
25314 qualifier: Some(first),
25315 name,
25316 }));
25317 }
25318 if matches!(self.peek(), Token::LParen) {
25319 self.advance();
25320 // `COUNT(*)` — special-cased here because `*` isn't a normal
25321 // expression token. Lower-case match on `first` since the lexer
25322 // folds identifiers.
25323 if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
25324 self.advance();
25325 if !matches!(self.peek(), Token::RParen) {
25326 return Err(self.err(format!(
25327 "expected ')' after COUNT(*), got {:?}",
25328 self.peek()
25329 )));
25330 }
25331 self.advance();
25332 // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
25333 let filter = self.parse_filter_clause()?;
25334 // v4.12: COUNT(*) OVER (...) — same window tail.
25335 let null_treatment = self.parse_null_treatment_modifier();
25336 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25337 && s.eq_ignore_ascii_case("over")
25338 {
25339 self.advance();
25340 let (partition_by, order_by, frame) = self.parse_over_clause()?;
25341 return Ok(Expr::WindowFunction {
25342 name: "count_star".into(),
25343 args: Vec::new(),
25344 partition_by,
25345 order_by,
25346 frame,
25347 null_treatment,
25348 filter,
25349 });
25350 }
25351 if let Some(filter) = filter {
25352 return Ok(Expr::AggregateOrdered {
25353 call: Box::new(Expr::FunctionCall {
25354 name: "count_star".into(),
25355 args: Vec::new(),
25356 }),
25357 order_by: Vec::new(),
25358 distinct: false,
25359 filter: Some(filter),
25360 });
25361 }
25362 return Ok(Expr::FunctionCall {
25363 name: "count_star".into(),
25364 args: Vec::new(),
25365 });
25366 }
25367 // Function call. PG-style: zero-or-more comma-separated args.
25368 let mut args = Vec::new();
25369 // v7.38 (read01, T14) — named-argument notation `argname => value`.
25370 // Names are collected in lock-step with `args` and resolved to
25371 // positional order after the loop (the AST stays positional).
25372 let mut arg_names: Vec<Option<String>> = Vec::new();
25373 let mut agg_order_by: Vec<OrderBy> = Vec::new();
25374 // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
25375 // seen, so the value arguments before it can be folded.
25376 let mut saw_separator = false;
25377 // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
25378 // v7.32 (round-29) — accept the dual `ALL` quantifier too
25379 // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
25380 let agg_distinct = if matches!(self.peek(), Token::Distinct) {
25381 self.advance();
25382 true
25383 } else if matches!(self.peek(), Token::All) {
25384 self.advance();
25385 false
25386 } else {
25387 false
25388 };
25389 // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
25390 // TIMESTAMPDIFF take a bare unit keyword as the first
25391 // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
25392 // bare type keyword (DATE / TIME / DATETIME); lower them
25393 // onto string literals so the evaluator sees plain text.
25394 if ((first.eq_ignore_ascii_case("timestampadd")
25395 || first.eq_ignore_ascii_case("timestampdiff"))
25396 && matches!(self.peek(), Token::Ident(u) if matches!(
25397 u.to_ascii_lowercase().as_str(),
25398 "microsecond" | "second" | "minute" | "hour" | "day"
25399 | "week" | "month" | "quarter" | "year"
25400 )))
25401 || (first.eq_ignore_ascii_case("get_format")
25402 && matches!(self.peek(), Token::Ident(u) if matches!(
25403 u.to_ascii_lowercase().as_str(),
25404 "date" | "time" | "datetime" | "timestamp"
25405 )))
25406 {
25407 if let Token::Ident(u) = self.peek() {
25408 args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
25409 }
25410 self.advance();
25411 if matches!(self.peek(), Token::Comma) {
25412 self.advance();
25413 }
25414 }
25415 // `ROW(a, b, …)` keyword constructor. Followed by a
25416 // comparison operator or [NOT] IN it joins the paren
25417 // row-constructor machinery (fieldwise parse-time
25418 // expansion); bare, it stays a `row` call the evaluator
25419 // renders as PG record text.
25420 if first.eq_ignore_ascii_case("row") {
25421 let mut row_items = Vec::new();
25422 if !matches!(self.peek(), Token::RParen) {
25423 loop {
25424 row_items.push(self.parse_expr(0)?);
25425 match self.peek() {
25426 Token::Comma => {
25427 self.advance();
25428 }
25429 Token::RParen => break,
25430 other => {
25431 return Err(self.err(format!(
25432 "expected ',' or ')' in ROW(...), got {other:?}"
25433 )));
25434 }
25435 }
25436 }
25437 }
25438 self.advance(); // ')'
25439 let comparison_follows = matches!(
25440 self.peek(),
25441 Token::Eq
25442 | Token::NotEq
25443 | Token::Lt
25444 | Token::LtEq
25445 | Token::Gt
25446 | Token::GtEq
25447 | Token::In
25448 ) || (matches!(self.peek(), Token::Not)
25449 && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25450 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25451 if comparison_follows && !row_items.is_empty() {
25452 return self.parse_row_comparison_tail(row_items);
25453 }
25454 return Ok(Expr::FunctionCall {
25455 name: String::from("row"),
25456 args: row_items,
25457 });
25458 }
25459 // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25460 // the parse-mode keyword introduces the source text. SPG
25461 // carries XML as text, so both modes lower to __xmlparse(expr)
25462 // which validates well-formedness and returns Value::Xml.
25463 if first.eq_ignore_ascii_case("xmlparse")
25464 && matches!(self.peek(), Token::Ident(kw)
25465 if kw.eq_ignore_ascii_case("document")
25466 || kw.eq_ignore_ascii_case("content"))
25467 {
25468 let mode = match self.advance() {
25469 Token::Ident(kw) => kw.to_ascii_lowercase(),
25470 _ => unreachable!("peeked an ident"),
25471 };
25472 let src = self.parse_expr(0)?;
25473 if !matches!(self.peek(), Token::RParen) {
25474 return Err(self.err(format!(
25475 "expected ')' to close XMLPARSE, got {:?}",
25476 self.peek()
25477 )));
25478 }
25479 self.advance();
25480 return Ok(Expr::FunctionCall {
25481 name: String::from("__xmlparse"),
25482 args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25483 });
25484 }
25485 // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25486 // keyword introduces the element name (a bare or quoted
25487 // identifier), then optional content expressions. Lower to a
25488 // plain `xmlelement(name_text, content …)` call.
25489 if first.eq_ignore_ascii_case("xmlelement")
25490 && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25491 {
25492 self.advance(); // consume NAME
25493 let elem_name = match self.peek().clone() {
25494 Token::Ident(n) | Token::QuotedIdent(n) => {
25495 self.advance();
25496 n
25497 }
25498 other => {
25499 return Err(self.err(format!(
25500 "expected element name after XMLELEMENT NAME, got {other:?}"
25501 )));
25502 }
25503 };
25504 let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25505 while matches!(self.peek(), Token::Comma) {
25506 self.advance();
25507 args.push(self.parse_expr(0)?);
25508 }
25509 if !matches!(self.peek(), Token::RParen) {
25510 return Err(self.err(format!(
25511 "expected ')' to close XMLELEMENT, got {:?}",
25512 self.peek()
25513 )));
25514 }
25515 self.advance();
25516 return Ok(Expr::FunctionCall {
25517 name: String::from("xmlelement"),
25518 args,
25519 });
25520 }
25521 // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
25522 // becomes a `<name>value</name>` element; a bare column infers its
25523 // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
25524 if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
25525 let mut args: Vec<Expr> = Vec::new();
25526 loop {
25527 let val = self.parse_expr(0)?;
25528 let name = if matches!(self.peek(), Token::As) {
25529 self.advance();
25530 match self.peek().clone() {
25531 Token::Ident(n) | Token::QuotedIdent(n) => {
25532 self.advance();
25533 n
25534 }
25535 other => {
25536 return Err(self.err(format!(
25537 "expected name after AS in XMLFOREST, got {other:?}"
25538 )));
25539 }
25540 }
25541 } else if let Expr::Column(c) = &val {
25542 c.name.clone()
25543 } else {
25544 return Err(
25545 self.err("XMLFOREST element without a column name needs AS".into())
25546 );
25547 };
25548 args.push(Expr::Literal(Literal::String(name)));
25549 args.push(val);
25550 if matches!(self.peek(), Token::Comma) {
25551 self.advance();
25552 } else {
25553 break;
25554 }
25555 }
25556 if !matches!(self.peek(), Token::RParen) {
25557 return Err(self.err(format!(
25558 "expected ')' to close XMLFOREST, got {:?}",
25559 self.peek()
25560 )));
25561 }
25562 self.advance();
25563 return Ok(Expr::FunctionCall {
25564 name: String::from("xmlforest"),
25565 args,
25566 });
25567 }
25568 // SQL-standard `POSITION(sub IN str)` — lowers onto
25569 // strpos(str, sub). IN is the argument separator here,
25570 // so the needle parses with the IN-tail suppressed.
25571 if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
25572 let saved = self.suppress_in_tail;
25573 self.suppress_in_tail = true;
25574 let needle = self.parse_expr(0);
25575 self.suppress_in_tail = saved;
25576 let needle = needle?;
25577 if matches!(self.peek(), Token::In) {
25578 self.advance();
25579 let haystack = self.parse_expr(0)?;
25580 if !matches!(self.peek(), Token::RParen) {
25581 return Err(self.err(format!(
25582 "expected ')' to close POSITION, got {:?}",
25583 self.peek()
25584 )));
25585 }
25586 self.advance();
25587 return Ok(Expr::FunctionCall {
25588 name: String::from("strpos"),
25589 args: alloc::vec![haystack, needle],
25590 });
25591 }
25592 // position(sub, str) comma form (incl. bytea) —
25593 // hand the parsed first arg to the generic list.
25594 args.push(needle);
25595 if matches!(self.peek(), Token::Comma) {
25596 self.advance();
25597 }
25598 }
25599 // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
25600 // FROM str)` — lowers onto btrim / ltrim / rtrim. The
25601 // plain comma forms TRIM(str) / TRIM(str, chars) keep
25602 // riding the generic argument list below.
25603 if first.eq_ignore_ascii_case("trim") {
25604 let mode = match self.peek() {
25605 Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
25606 self.advance();
25607 Some("btrim")
25608 }
25609 Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
25610 self.advance();
25611 Some("ltrim")
25612 }
25613 Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
25614 self.advance();
25615 Some("rtrim")
25616 }
25617 _ => None,
25618 };
25619 if mode.is_some() || matches!(self.peek(), Token::From) {
25620 // TRIM([mode] FROM str) — no strip-chars.
25621 let chars = if matches!(self.peek(), Token::From) {
25622 None
25623 } else {
25624 Some(self.parse_expr(0)?)
25625 };
25626 if !matches!(self.peek(), Token::From) {
25627 return Err(self.err(format!(
25628 "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
25629 self.peek()
25630 )));
25631 }
25632 self.advance();
25633 let target = self.parse_expr(0)?;
25634 if !matches!(self.peek(), Token::RParen) {
25635 return Err(
25636 self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
25637 );
25638 }
25639 self.advance();
25640 let mut trim_args = alloc::vec![target];
25641 if let Some(c) = chars {
25642 trim_args.push(c);
25643 }
25644 return Ok(Expr::FunctionCall {
25645 name: String::from(mode.unwrap_or("btrim")),
25646 args: trim_args,
25647 });
25648 }
25649 }
25650 if !matches!(self.peek(), Token::RParen) {
25651 loop {
25652 // v7.38 (read01, T14) — `argname => value` names this arg.
25653 // v7.39 (read01 round 77) — `argname := value` is the same
25654 // thing, and it is the spelling PG's own docs lead with. It
25655 // was simply never lexed here, so every `f(x := 1)` died in
25656 // the parser regardless of what `f` was.
25657 let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
25658 (
25659 Token::Ident(n) | Token::QuotedIdent(n),
25660 Some(Token::FatArrow | Token::ColonEq),
25661 ) => {
25662 let name = n.clone();
25663 self.advance(); // name
25664 self.advance(); // => / :=
25665 Some(name)
25666 }
25667 _ => None,
25668 };
25669 // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
25670 // array's elements into a variadic call's trailing args
25671 // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
25672 // reserved, so it arrives as a bare ident before the arg.
25673 let is_variadic = this_name.is_none()
25674 && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
25675 if is_variadic {
25676 self.advance();
25677 }
25678 let arg = self.parse_expr(0)?;
25679 args.push(match &this_name {
25680 // The callee's parameter names decide the slot, and a
25681 // user function's live in the catalog. Carry the name
25682 // to eval rather than guessing here.
25683 Some(n) => Expr::NamedArg {
25684 name: n.clone(),
25685 expr: Box::new(arg),
25686 },
25687 None if is_variadic => Expr::Variadic(Box::new(arg)),
25688 None => arg,
25689 });
25690 arg_names.push(this_name);
25691 // v7.25 (round-17) — standard `CAST(expr AS type)`.
25692 // The `::` cast already worked; this lowers the
25693 // function form onto the same Expr::Cast node.
25694 if first.eq_ignore_ascii_case("cast")
25695 && args.len() == 1
25696 && matches!(self.peek(), Token::As)
25697 {
25698 self.advance();
25699 let target = self.parse_cast_target()?;
25700 if !matches!(self.peek(), Token::RParen) {
25701 return Err(self.err(format!(
25702 "expected ')' to close CAST, got {:?}",
25703 self.peek()
25704 )));
25705 }
25706 self.advance();
25707 return Ok(Expr::Cast {
25708 expr: Box::new(args.pop().expect("one arg")),
25709 target,
25710 });
25711 }
25712 // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
25713 // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
25714 // keywords; SPG's lexer makes them plain idents (so they'd be
25715 // read as column refs). Lower the keyword to the string form
25716 // the evaluator already accepts.
25717 if first.eq_ignore_ascii_case("normalize")
25718 && args.len() == 1
25719 && matches!(self.peek(), Token::Comma)
25720 {
25721 let form = match self.tokens.get(self.pos + 1) {
25722 Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
25723 let up = f.to_ascii_uppercase();
25724 matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
25725 }
25726 _ => None,
25727 };
25728 if let Some(up) = form {
25729 self.advance(); // comma
25730 self.advance(); // form keyword
25731 args.push(Expr::Literal(Literal::String(up)));
25732 }
25733 }
25734 // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
25735 // form. Desugars to the comma-list shape evaluator already
25736 // handles. Triggered after the first arg when the function
25737 // name is substring / substr and the next token is FROM
25738 // (a reserved keyword in PG; SPG also reserves it).
25739 // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
25740 // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
25741 // internal __substring_similar(str, pat, esc) call.
25742 if (first.eq_ignore_ascii_case("substring")
25743 || first.eq_ignore_ascii_case("substr"))
25744 && args.len() == 1
25745 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
25746 {
25747 self.advance(); // SIMILAR
25748 let pattern = self.parse_expr(0)?;
25749 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
25750 {
25751 return Err(self.err(format!(
25752 "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
25753 self.peek()
25754 )));
25755 }
25756 self.advance(); // ESCAPE
25757 let esc = self.parse_expr(0)?;
25758 if !matches!(self.peek(), Token::RParen) {
25759 return Err(self.err(format!(
25760 "expected ')' to close substring(... SIMILAR ...), got {:?}",
25761 self.peek()
25762 )));
25763 }
25764 self.advance();
25765 args.push(pattern);
25766 args.push(esc);
25767 return Ok(Expr::FunctionCall {
25768 name: "__substring_similar".to_string(),
25769 args,
25770 });
25771 }
25772 if (first.eq_ignore_ascii_case("substring")
25773 || first.eq_ignore_ascii_case("substr"))
25774 && args.len() == 1
25775 && matches!(self.peek(), Token::From | Token::For)
25776 {
25777 // `substring(str FROM pos [FOR len])`, or the FOR-only
25778 // `substring(str FOR len)` which PG treats as FROM 1.
25779 if matches!(self.peek(), Token::From) {
25780 self.advance();
25781 let start = self.parse_expr(0)?;
25782 args.push(start);
25783 } else {
25784 args.push(Expr::Literal(Literal::Integer(1)));
25785 }
25786 if matches!(self.peek(), Token::For) {
25787 self.advance();
25788 let length = self.parse_expr(0)?;
25789 args.push(length);
25790 }
25791 if !matches!(self.peek(), Token::RParen) {
25792 return Err(self.err(format!(
25793 "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
25794 self.peek()
25795 )));
25796 }
25797 self.advance();
25798 return Ok(Expr::FunctionCall {
25799 name: first.to_ascii_lowercase(),
25800 args,
25801 });
25802 }
25803 // PG `overlay(str PLACING repl FROM n [FOR len])`
25804 // syntactic form. Desugars to the `overlay(str,
25805 // repl, n[, len])` comma-list shape the evaluator
25806 // already implements. `PLACING` is not a reserved
25807 // token in SPG, so it arrives as a bare Ident.
25808 if first.eq_ignore_ascii_case("overlay")
25809 && args.len() == 1
25810 && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
25811 {
25812 self.advance(); // consume PLACING
25813 args.push(self.parse_expr(0)?); // replacement
25814 if !matches!(self.peek(), Token::From) {
25815 return Err(self.err(format!(
25816 "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
25817 self.peek()
25818 )));
25819 }
25820 self.advance();
25821 args.push(self.parse_expr(0)?); // start position
25822 if matches!(self.peek(), Token::For) {
25823 self.advance();
25824 args.push(self.parse_expr(0)?); // length
25825 }
25826 if !matches!(self.peek(), Token::RParen) {
25827 return Err(self.err(format!(
25828 "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
25829 self.peek()
25830 )));
25831 }
25832 self.advance();
25833 return Ok(Expr::FunctionCall {
25834 name: String::from("overlay"),
25835 args,
25836 });
25837 }
25838 // `TRIM(chars FROM str)` — the keyword-less
25839 // spelling lands here after the chars parse
25840 // (the keyword forms return earlier).
25841 if first.eq_ignore_ascii_case("trim")
25842 && args.len() == 1
25843 && matches!(self.peek(), Token::From)
25844 {
25845 self.advance();
25846 let target = self.parse_expr(0)?;
25847 if !matches!(self.peek(), Token::RParen) {
25848 return Err(self.err(format!(
25849 "expected ')' to close TRIM(chars FROM str), got {:?}",
25850 self.peek()
25851 )));
25852 }
25853 self.advance();
25854 let chars = args.pop().expect("one arg");
25855 return Ok(Expr::FunctionCall {
25856 name: String::from("btrim"),
25857 args: alloc::vec![target, chars],
25858 });
25859 }
25860 // v7.24 (round-16 A) — aggregate-internal
25861 // ordering: `array_agg(x ORDER BY y DESC NULLS
25862 // LAST)`. Keys close the argument list.
25863 if matches!(self.peek(), Token::Order) {
25864 self.advance();
25865 if !self.peek_is_by() {
25866 return Err(self.err(format!(
25867 "expected BY after ORDER in aggregate args, got {:?}",
25868 self.peek()
25869 )));
25870 }
25871 self.advance();
25872 loop {
25873 // v7.39 (round 691) — save/restore, the discipline this parser
25874 // already uses around `pending_sample_preds`, so a subquery inside
25875 // a key neither inherits nor leaks the channel.
25876 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25877 let saved_coll = self.order_key_collation.take();
25878 let parsed = self.parse_expr(0);
25879 self.in_order_by_key = saved_flag;
25880 let collation =
25881 core::mem::replace(&mut self.order_key_collation, saved_coll);
25882 let expr = parsed?;
25883 let desc = if matches!(self.peek(), Token::Desc) {
25884 self.advance();
25885 true
25886 } else if matches!(self.peek(), Token::Asc) {
25887 self.advance();
25888 false
25889 } else {
25890 false
25891 };
25892 let nulls_first = self.parse_optional_nulls_placement()?;
25893 agg_order_by.push(OrderBy {
25894 expr,
25895 desc,
25896 nulls_first,
25897 collation,
25898 });
25899 if matches!(self.peek(), Token::Comma) {
25900 self.advance();
25901 } else {
25902 break;
25903 }
25904 }
25905 // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
25906 // follow the ORDER BY inside GROUP_CONCAT.
25907 if self.consume_group_concat_separator(&mut args)? {
25908 saw_separator = true;
25909 }
25910 if !matches!(self.peek(), Token::RParen) {
25911 return Err(self.err(format!(
25912 "expected ')' after aggregate ORDER BY, got {:?}",
25913 self.peek()
25914 )));
25915 }
25916 break;
25917 }
25918 // v7.39 (round 354, M12) — …or directly after the
25919 // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
25920 // own spelling of what PG passes as string_agg's second
25921 // argument; it was a parse error, so every MySQL query
25922 // that names its own separator failed outright.
25923 if self.consume_group_concat_separator(&mut args)? {
25924 saw_separator = true;
25925 break;
25926 }
25927 match self.peek() {
25928 Token::Comma => {
25929 self.advance();
25930 }
25931 Token::RParen => break,
25932 other => {
25933 return Err(self.err(format!(
25934 "expected ',' or ')' in function args, got {other:?}"
25935 )));
25936 }
25937 }
25938 }
25939 }
25940 // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
25941 // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
25942 // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
25943 // meaning a separator — that is what the explicit SEPARATOR
25944 // tail is for. Fold them into one `concat(...)` so the
25945 // aggregate keeps its single value argument.
25946 if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
25947 let values = args.len() - usize::from(saw_separator);
25948 if values > 1 {
25949 let sep_arg = if saw_separator { args.pop() } else { None };
25950 let folded = Expr::FunctionCall {
25951 name: "concat".to_string(),
25952 args: core::mem::take(&mut args),
25953 };
25954 args.push(folded);
25955 if let Some(sep) = sep_arg {
25956 args.push(sep);
25957 }
25958 }
25959 }
25960 self.advance(); // consume ')'
25961 // v7.39 (read01 round 77) — named arguments are NOT reordered here
25962 // any more. The parser has no catalog, so it could only ever resolve
25963 // the handful of `make_*` builtins whose parameter names were baked
25964 // into a table right here — every user function got
25965 // "does not support named arguments", though the catalog has been
25966 // storing its parameter names all along. Reordering happens in eval,
25967 // in one place, for builtins and user functions alike.
25968 // v7.32 (round-29) — ordered-set aggregate tail
25969 // `name(direct_args) WITHIN GROUP (ORDER BY …)`
25970 // (percentile_cont / percentile_disc / mode). The sort spec
25971 // lands in the same `order_by` slot a decorated aggregate
25972 // uses; the executor dispatches on the function name. WITHIN
25973 // GROUP and an intra-argument ORDER BY are mutually
25974 // exclusive (PG rejects both).
25975 let within_group_order = self.parse_within_group_clause()?;
25976 if !within_group_order.is_empty() && !agg_order_by.is_empty() {
25977 return Err(self.err(
25978 "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
25979 .into(),
25980 ));
25981 }
25982 let within_group_seen = !within_group_order.is_empty();
25983 let agg_order_by = if within_group_order.is_empty() {
25984 agg_order_by
25985 } else {
25986 within_group_order
25987 };
25988 // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
25989 let filter = self.parse_filter_clause()?;
25990 // v4.12: window-function tail — `name(args) OVER (...)`.
25991 // Promotes the just-parsed FunctionCall into a
25992 // WindowFunction node carrying partition + order.
25993 // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
25994 // / `RESPECT NULLS OVER (...)` between the closing paren
25995 // and `OVER`.
25996 let null_treatment = self.parse_null_treatment_modifier();
25997 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25998 && s.eq_ignore_ascii_case("over")
25999 {
26000 self.advance();
26001 // v7.39 (round 230) — PG implements neither modifier for a
26002 // windowed call and says so (0A000). Both used to be parsed
26003 // and then silently dropped here, so `count(DISTINCT v)
26004 // OVER (…)` quietly answered the non-distinct count.
26005 if agg_distinct {
26006 return Err(
26007 self.err("DISTINCT is not implemented for window functions".to_string())
26008 );
26009 }
26010 if !agg_order_by.is_empty() {
26011 // PG separates the two shapes that land here: a
26012 // WITHIN GROUP call is an ordered-set aggregate and gets
26013 // its own message naming the aggregate; a plain
26014 // `agg(x ORDER BY y)` gets the generic one.
26015 let msg = if within_group_seen {
26016 alloc::format!("OVER is not supported for ordered-set aggregate {first}")
26017 } else {
26018 "aggregate ORDER BY is not implemented for window functions".to_string()
26019 };
26020 return Err(self.err(msg));
26021 }
26022 let (partition_by, order_by, frame) = self.parse_over_clause()?;
26023 return Ok(Expr::WindowFunction {
26024 name: first,
26025 args,
26026 partition_by,
26027 order_by,
26028 frame,
26029 null_treatment,
26030 filter,
26031 });
26032 }
26033 if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
26034 return Ok(Expr::AggregateOrdered {
26035 call: Box::new(Expr::FunctionCall { name: first, args }),
26036 order_by: agg_order_by,
26037 distinct: agg_distinct,
26038 filter,
26039 });
26040 }
26041 // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
26042 // over TIMESTAMPTZ and has no timestamp overload, so a
26043 // timestamp argument is coerced on the way in and the answer
26044 // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
26045 // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
26046 // zone`. SPG answered `timestamp without time zone`, dropping
26047 // the offset from every rendering.
26048 //
26049 // Writing the coercion PG performs makes the existing
26050 // argument-driven typing (the one `date_trunc` uses) reach the
26051 // right answer, rather than teaching the type layer a second
26052 // rule. MySQL's DATE_ADD is a different function that returns
26053 // DATE or DATETIME, so this is PG-dialect only.
26054 //
26055 // Out-of-line because this sits on the RECURSIVE descent
26056 // frame: an inline block with locals here costs every nesting
26057 // level, and the suite's deep-nesting sentinel overflowed the
26058 // 512 KiB parser stack the moment one was added (round 430's
26059 // lesson, in the same shape).
26060 if !self.mysql_dialect {
26061 lift_date_add_arg_to_timestamptz(&first, &mut args);
26062 }
26063 return Ok(Expr::FunctionCall { name: first, args });
26064 }
26065 // v7.9.20 — SQL-standard parenless keyword expressions
26066 // (PG treats these as functions called without parens).
26067 // Resolve to a synthetic FunctionCall so the engine's
26068 // eval path reuses the existing function-call routing.
26069 // mailrs G3.
26070 let lc = first.to_ascii_lowercase();
26071 if matches!(
26072 lc.as_str(),
26073 "current_date"
26074 | "current_time"
26075 | "current_timestamp"
26076 | "localtimestamp"
26077 | "localtime"
26078 // v7.37.17 (17.6 siblings) — session-identity SQL-
26079 // standard parenless keywords. current_user /
26080 // session_user / user were already caught by the
26081 // pgwire canned-response shortcut but bare-select
26082 // in the embedded engine went through Expr::Column
26083 // and errored. Adding them here so the parser
26084 // resolves to a synthetic FunctionCall that reuses
26085 // the existing eval/functions.rs dispatch.
26086 | "current_user"
26087 | "session_user"
26088 | "current_role"
26089 | "current_catalog"
26090 | "current_schema"
26091 | "current_database"
26092 // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
26093 | "system_user"
26094 ) {
26095 return Ok(Expr::FunctionCall {
26096 name: lc,
26097 args: Vec::new(),
26098 });
26099 }
26100 Ok(Expr::Column(ColumnName {
26101 qualifier: None,
26102 name: first,
26103 }))
26104 }
26105}
26106
26107/// v7.39 (round 522) — write the coercion PG's `date_add` /
26108/// `date_subtract` signature performs.
26109///
26110/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
26111/// timestamp argument is cast on the way in and the answer is
26112/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
26113/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
26114/// `timestamp without time zone`, dropping the offset from every
26115/// rendering of the result.
26116///
26117/// Writing the cast the signature implies lets the existing
26118/// argument-driven typing (the one `date_trunc` uses) reach the right
26119/// answer instead of teaching the type layer a second rule. MySQL's
26120/// DATE_ADD is a different function returning DATE or DATETIME, so the
26121/// caller applies this in PG dialect only.
26122///
26123/// A free function, and not a block at the call site, because the caller
26124/// is on the recursive-descent frame chain.
26125#[inline(never)]
26126fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
26127 if args.len() != 2
26128 || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
26129 {
26130 return;
26131 }
26132 let base = args.remove(0);
26133 args.insert(
26134 0,
26135 Expr::Cast {
26136 expr: Box::new(base),
26137 target: CastTarget::Timestamptz,
26138 },
26139 );
26140}
26141
26142/// v6.8.2 — walk an expression tree and return the first column
26143/// reference's bare name. Used by `parse_create_index_stmt_after_create`
26144/// to derive `CreateIndexStatement.column` from an expression
26145/// key (so downstream planner code resolving a primary column
26146/// position keeps working with expression indexes). Returns
26147/// `None` when the expression has no column ref at all — caller
26148/// surfaces that as a parse error.
26149fn extract_first_column(expr: &Expr) -> Option<String> {
26150 match expr {
26151 Expr::Column(cn) => Some(cn.name.clone()),
26152 Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
26153 Expr::Binary { lhs, rhs, .. } => {
26154 extract_first_column(lhs).or_else(|| extract_first_column(rhs))
26155 }
26156 Expr::Unary { expr: e, .. } => extract_first_column(e),
26157 // v7.39 (read01 round 93) — a cast wraps its operand: a common
26158 // expression-index key is `lower(col::text)`, where the column
26159 // sits under the `::text` cast inside the function arg. Without
26160 // descending here the key was rejected as "references no column".
26161 Expr::Cast { expr: e, .. } => extract_first_column(e),
26162 _ => None,
26163 }
26164}
26165
26166fn maybe_not(expr: Expr, negated: bool) -> Expr {
26167 if negated {
26168 Expr::Unary {
26169 op: UnOp::Not,
26170 expr: Box::new(expr),
26171 }
26172 } else {
26173 expr
26174 }
26175}
26176
26177/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
26178/// things in the two dialects, and SPG read all three PG's way:
26179///
26180/// | token | PG (and SPG) | MySQL, measured |
26181/// |---|---|---|
26182/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
26183/// | `&&` | inet / array overlap | **AND** |
26184/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
26185///
26186/// `1 || 0` answering the string '10' on a MySQL session is a wrong
26187/// answer with no error, which is why they are routed here rather than
26188/// left to the shared table.
26189impl Parser {
26190 fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
26191 if self.mysql_dialect {
26192 // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
26193 // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
26194 // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
26195 if let Token::Ident(w) = tok
26196 && w.eq_ignore_ascii_case("div")
26197 {
26198 return Some((BinOp::IntDiv, 8));
26199 }
26200 // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
26201 // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
26202 // the lexer; the `MOD(x, y)` function form is unaffected (MOD
26203 // there sits in operand position, not infix).
26204 if let Token::Ident(w) = tok
26205 && w.eq_ignore_ascii_case("mod")
26206 {
26207 return Some((BinOp::Mod, 8));
26208 }
26209 // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
26210 // plain ident to the lexer. Its precedence sits between OR (1)
26211 // and AND (3) — hence rung 2, the slot freed by moving AND up.
26212 if let Token::Ident(w) = tok
26213 && w.eq_ignore_ascii_case("xor")
26214 {
26215 return Some((BinOp::LogicalXor, 2));
26216 }
26217 match tok {
26218 Token::Concat => return Some((BinOp::Or, 1)),
26219 // MySQL's `&&` is logical AND, sharing AND's rung (3).
26220 Token::InetOverlap => return Some((BinOp::And, 3)),
26221 // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
26222 Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
26223 _ => {}
26224 }
26225 }
26226 binop_from(tok)
26227 }
26228}
26229
26230// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
26231// (which sits strictly between OR and AND), every level from AND upward was
26232// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
26233// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
26234// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
26235// the *relative* order of every PG operator is unchanged by the shift.
26236fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
26237 let pair = match tok {
26238 Token::Or => (BinOp::Or, 1),
26239 Token::And => (BinOp::And, 3),
26240 Token::Eq => (BinOp::Eq, 5),
26241 Token::NotEq => (BinOp::NotEq, 5),
26242 Token::Lt => (BinOp::Lt, 5),
26243 Token::LtEq => (BinOp::LtEq, 5),
26244 Token::Gt => (BinOp::Gt, 5),
26245 Token::GtEq => (BinOp::GtEq, 5),
26246 // pgvector distance ops all sit on the same rung — tighter than
26247 // comparisons (5) so `col <-> v < threshold` parses correctly.
26248 Token::L2Distance => (BinOp::L2Distance, 6),
26249 // v7.39 (read01 geo_ops.c) — geometric predicates ride the
26250 // comparison rung.
26251 Token::GeomParallel => (BinOp::GeomParallel, 5),
26252 // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
26253 // comparison rung.
26254 Token::OverLeft => (BinOp::OverLeft, 5),
26255 Token::OverRight => (BinOp::OverRight, 5),
26256 Token::GeomPerp => (BinOp::GeomPerp, 5),
26257 Token::GeomSameAs => (BinOp::GeomSameAs, 5),
26258 Token::ClosestPoint => (BinOp::ClosestPoint, 6),
26259 Token::GeomHoriz => (BinOp::GeomHoriz, 5),
26260 Token::InnerProduct => (BinOp::InnerProduct, 6),
26261 Token::CosineDistance => (BinOp::CosineDistance, 6),
26262 Token::Plus => (BinOp::Add, 7),
26263 Token::Minus => (BinOp::Sub, 7),
26264 // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
26265 // binds every "other" operator (`||`, `|`, `&`, `#`, the
26266 // pgvector distances above) BETWEEN additive (7) and the
26267 // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
26268 // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
26269 // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
26270 // ("matches PG conceptually" — the round-753 audit measured it
26271 // false; the old rung errored on `'a' || 1 + 1` with
26272 // `text + integer`). Same-level chains left-fold, as PG does.
26273 Token::Concat => (BinOp::Concat, 6),
26274 Token::Pipe => (BinOp::BitOr, 6),
26275 Token::Amp => (BinOp::BitAnd, 6),
26276 Token::Star => (BinOp::Mul, 8),
26277 Token::Slash => (BinOp::Div, 8),
26278 Token::Percent => (BinOp::Mod, 8),
26279 // v4.14: JSON path ops bind tighter than comparisons (5)
26280 // and additive (7) so `doc->'k' = 'v'` parses correctly.
26281 // Same rung as the multiplicative ops.
26282 Token::JsonGet => (BinOp::JsonGet, 8),
26283 Token::JsonGetText => (BinOp::JsonGetText, 8),
26284 Token::JsonGetPath => (BinOp::JsonGetPath, 8),
26285 Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
26286 Token::JsonContains => (BinOp::JsonContains, 8),
26287 Token::JsonPathExists => (BinOp::JsonPathExists, 8),
26288 Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
26289 Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
26290 Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
26291 Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
26292 Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
26293 // v7.12.2 — `@@` binds at the comparison rung (looser than
26294 // arithmetic, tighter than AND / OR). PG places `@@` at
26295 // the same precedence as `=` / `<`, so we follow.
26296 Token::TsMatch => (BinOp::TsMatch, 5),
26297 // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
26298 // PG places these at the comparison rung (same level as `=`),
26299 // so we follow.
26300 Token::InetContainedBy => (BinOp::InetContainedBy, 5),
26301 Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
26302 Token::InetContains => (BinOp::InetContains, 5),
26303 Token::InetContainsEq => (BinOp::InetContainsEq, 5),
26304 Token::InetOverlap => (BinOp::InetOverlap, 5),
26305 // v7.39 (round 508) — the geometric and pattern-order predicates
26306 // ride the comparison rung, as every other predicate does.
26307 Token::Intersects => (BinOp::Intersects, 5),
26308 Token::IsBelow => (BinOp::IsBelow, 5),
26309 Token::IsAbove => (BinOp::IsAbove, 5),
26310 Token::PatternLt => (BinOp::PatternLt, 5),
26311 Token::PatternLtEq => (BinOp::PatternLtEq, 5),
26312 Token::PatternGt => (BinOp::PatternGt, 5),
26313 Token::PatternGtEq => (BinOp::PatternGtEq, 5),
26314 // `@@@` is the old spelling of `@@` and means exactly it.
26315 Token::TsMatchOld => (BinOp::TsMatch, 5),
26316 _ => return None,
26317 };
26318 Some(pair)
26319}
26320
26321#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26322// `as f32` here is intentional: vector elements widen / narrow into f32 on
26323// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
26324// past ~15 decimal digits — both are acceptable for a fixed-precision
26325// pgvector column.
26326/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
26327/// implicit table alias and break trailing clauses. WITH lands
26328/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
26329/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
26330/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
26331/// / VALUES / FOR / LATERAL — all of which would otherwise be
26332/// silently swallowed by `parse_optional_alias`.
26333fn is_alias_stopword(s: &str) -> bool {
26334 matches!(
26335 s.to_ascii_lowercase().as_str(),
26336 "with"
26337 | "on"
26338 | "where"
26339 | "having"
26340 | "group"
26341 | "order"
26342 | "limit"
26343 | "offset"
26344 | "union"
26345 | "except"
26346 | "intersect"
26347 | "returning"
26348 | "set"
26349 | "values"
26350 | "for"
26351 | "window"
26352 | "tablesample"
26353 | "lateral"
26354 | "left"
26355 | "right"
26356 | "inner"
26357 | "outer"
26358 | "full"
26359 | "cross"
26360 | "join"
26361 | "natural"
26362 | "using"
26363 | "fetch"
26364 )
26365}
26366
26367fn extract_numeric_literal(e: &Expr) -> Option<f32> {
26368 match e {
26369 Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
26370 Expr::Literal(Literal::Float(x)) => Some(*x as f32),
26371 // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
26372 // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
26373 // so scale the divisor by hand instead of `f32::powi`.)
26374 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26375 let mut div = 1.0f32;
26376 for _ in 0..*scale {
26377 div *= 10.0;
26378 }
26379 Some(*unscaled as f32 / div)
26380 }
26381 Expr::Unary {
26382 op: UnOp::Neg,
26383 expr,
26384 } => extract_numeric_literal(expr).map(|x| -x),
26385 _ => None,
26386 }
26387}
26388
26389/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
26390/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
26391/// negative. Returns `None` if any pair fails to parse or no pair is found.
26392///
26393/// Recognised units (case-insensitive, optional trailing `s`):
26394/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
26395/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
26396/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
26397/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
26398/// (PG-canonical: DST and month-boundary semantics depend on this).
26399/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
26400/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
26401/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
26402#[allow(clippy::cast_possible_truncation)]
26403fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
26404 let mut months: i64 = 0;
26405 let mut days: i64 = 0;
26406 let mut micros: i64 = 0;
26407 let mut in_time = false;
26408 let mut num = alloc::string::String::new();
26409 for ch in rest.chars() {
26410 if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
26411 num.push(ch);
26412 continue;
26413 }
26414 if ch == 'T' || ch == 't' {
26415 if !num.is_empty() {
26416 return None;
26417 }
26418 in_time = true;
26419 continue;
26420 }
26421 let n: f64 = num.parse().ok()?;
26422 num.clear();
26423 match (ch, in_time) {
26424 ('Y' | 'y', false) => months += (n * 12.0) as i64,
26425 ('M', false) => months += n as i64,
26426 ('W' | 'w', false) => days += (n * 7.0) as i64,
26427 ('D' | 'd', false) => days += n as i64,
26428 ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26429 ('M', true) => micros += (n * 60_000_000.0) as i64,
26430 ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26431 _ => return None,
26432 }
26433 }
26434 if !num.is_empty() {
26435 return None;
26436 }
26437 Some((
26438 i32::try_from(months).ok()?,
26439 i32::try_from(days).ok()?,
26440 micros,
26441 ))
26442}
26443
26444/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
26445/// leading `-` negates the whole value). Rejects date-like strings.
26446fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
26447 let (neg, body) = match s.strip_prefix('-') {
26448 Some(b) => (true, b),
26449 None => (false, s),
26450 };
26451 let (y, m) = body.split_once('-')?;
26452 let years: i32 = y.parse().ok()?;
26453 let mons: i32 = m.parse().ok()?;
26454 if years < 0 || mons < 0 {
26455 return None;
26456 }
26457 let total = years.checked_mul(12)?.checked_add(mons)?;
26458 Some((if neg { -total } else { total }, 0, 0))
26459}
26460
26461/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
26462/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
26463fn parse_interval_clock(tok: &str) -> Option<i64> {
26464 let (neg, body) = match tok.strip_prefix('-') {
26465 Some(r) => (true, r),
26466 None => (false, tok.strip_prefix('+').unwrap_or(tok)),
26467 };
26468 let mut it = body.split(':');
26469 let h: i64 = it.next()?.parse().ok()?;
26470 let m: i64 = it.next()?.parse().ok()?;
26471 let s_tok = it.next().unwrap_or("0");
26472 if it.next().is_some() {
26473 return None;
26474 }
26475 let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
26476 let sec: i64 = sec.parse().ok()?;
26477 let mut f = alloc::string::String::from(frac);
26478 while f.len() < 6 {
26479 f.push('0');
26480 }
26481 f.truncate(6);
26482 let fus: i64 = f.parse().ok()?;
26483 sec.checked_mul(1_000_000)?.checked_add(fus)?
26484 } else {
26485 s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
26486 };
26487 let total = h
26488 .checked_mul(3_600_000_000)?
26489 .checked_add(m.checked_mul(60_000_000)?)?
26490 .checked_add(sec_us)?;
26491 Some(if neg { -total } else { total })
26492}
26493
26494/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
26495/// every spelling PG accepts (measured against live PG18.4, not guessed):
26496/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
26497/// Before this, the unit table matched long names only, with an ad-hoc
26498/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
26499/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
26500/// INTERVAL", and it had also grown arms for the debris that stripping leaves
26501/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
26502/// fractional) both read from this one table now.
26503fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
26504 let u = raw.to_ascii_lowercase();
26505 Some(match u.as_str() {
26506 "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
26507 "microsecond"
26508 }
26509 "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
26510 "millisecond"
26511 }
26512 "second" | "seconds" | "sec" | "secs" | "s" => "second",
26513 "minute" | "minutes" | "min" | "mins" | "m" => "minute",
26514 "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
26515 "day" | "days" | "d" => "day",
26516 "week" | "weeks" | "w" => "week",
26517 "month" | "months" | "mon" | "mons" => "month",
26518 "year" | "years" | "yr" | "yrs" | "y" => "year",
26519 "decade" | "decades" | "dec" | "decs" => "decade",
26520 "century" | "centuries" | "cent" | "c" => "century",
26521 "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
26522 _ => return None,
26523 })
26524}
26525
26526/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
26527/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
26528#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26529pub(crate) enum IntervalField {
26530 Year,
26531 Month,
26532 Day,
26533 Hour,
26534 Minute,
26535 Second,
26536}
26537
26538/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
26539/// spellings aren't standard for the qualifier position, so only the singular
26540/// forms are accepted.
26541/// v7.39 (round 350, M7) — MySQL's interval units, measured against
26542/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
26543/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
26544/// take a `'1 2'` style literal — are not read here; they stay a parse
26545/// error rather than being silently misread.)
26546/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
26547///
26548/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
26549/// to do with a `@@` engine setting, and an unset one reads NULL rather
26550/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
26551/// were the same node and `SELECT @x` answered "Unknown system variable".)
26552/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
26553/// not see a session override — measured, after `SET autocommit=0`,
26554/// `@@global.autocommit` is still 1.
26555///
26556/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
26557/// the parser's nesting budget is tuned against, and building these
26558/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
26559/// wall `parse_left_right_atom` and friends were factored out for).
26560#[inline(never)]
26561fn variable_ref_atom(raw: &str) -> Expr {
26562 let user_var = !raw.starts_with("@@");
26563 let bare = raw.trim_start_matches('@').to_ascii_lowercase();
26564 Expr::FunctionCall {
26565 name: String::from(if user_var {
26566 "__spg_user_var"
26567 } else {
26568 "__spg_session_var"
26569 }),
26570 args: alloc::vec![Expr::Literal(Literal::String(bare))],
26571 }
26572}
26573
26574fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
26575 let Token::Ident(s) = tok else { return None };
26576 Some(match () {
26577 () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
26578 () if s.eq_ignore_ascii_case("second") => "second",
26579 () if s.eq_ignore_ascii_case("minute") => "minute",
26580 () if s.eq_ignore_ascii_case("hour") => "hour",
26581 () if s.eq_ignore_ascii_case("day") => "day",
26582 () if s.eq_ignore_ascii_case("week") => "week",
26583 () if s.eq_ignore_ascii_case("month") => "month",
26584 () if s.eq_ignore_ascii_case("quarter") => "quarter",
26585 () if s.eq_ignore_ascii_case("year") => "year",
26586 () => return None,
26587 })
26588}
26589
26590/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
26591/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
26592/// which constructs the value at run time. Only the slot the unit names
26593/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
26594/// slot the builtin has (months and fractional seconds respectively).
26595fn make_interval_call(qty: Expr, unit: &str) -> Expr {
26596 let zero = || Expr::Literal(Literal::Integer(0));
26597 let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
26598 lhs: alloc::boxed::Box::new(qty.clone()),
26599 op,
26600 rhs: alloc::boxed::Box::new(by),
26601 };
26602 // (years, months, weeks, days, hours, mins, secs)
26603 let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
26604 match unit {
26605 "year" => args[0] = qty,
26606 "quarter" => {
26607 args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
26608 }
26609 "month" => args[1] = qty,
26610 "week" => args[2] = qty,
26611 "day" => args[3] = qty,
26612 "hour" => args[4] = qty,
26613 "minute" => args[5] = qty,
26614 "second" => args[6] = qty,
26615 // The builtin's seconds slot takes a fraction, so microseconds ride
26616 // it scaled down; the divisor is a NUMERIC literal so the division
26617 // stays exact rather than going through a float.
26618 "microsecond" => {
26619 args[6] = scaled(
26620 crate::ast::BinOp::Div,
26621 Expr::Literal(Literal::Numeric {
26622 unscaled: 1_000_000,
26623 scale: 0,
26624 }),
26625 );
26626 }
26627 _ => args[3] = qty,
26628 }
26629 Expr::FunctionCall {
26630 name: alloc::string::String::from("make_interval"),
26631 args,
26632 }
26633}
26634
26635/// `(count, unit)` → `(months, days, micros)`.
26636fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
26637 let n: i64 = count.trim().parse().ok()?;
26638 Some(match unit {
26639 "microsecond" => (0, 0, n),
26640 "second" => (0, 0, n.checked_mul(1_000_000)?),
26641 "minute" => (0, 0, n.checked_mul(60_000_000)?),
26642 "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
26643 "day" => (0, i32::try_from(n).ok()?, 0),
26644 "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
26645 "month" => (i32::try_from(n).ok()?, 0, 0),
26646 "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
26647 "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
26648 _ => return None,
26649 })
26650}
26651
26652fn interval_field_of(tok: &Token) -> Option<IntervalField> {
26653 let Token::Ident(s) = tok else { return None };
26654 Some(match () {
26655 () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
26656 () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
26657 () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
26658 () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
26659 () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
26660 () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
26661 () => return None,
26662 })
26663}
26664
26665/// v7.39 (read01 round 102) — interpret an interval literal under a field
26666/// qualifier. Returns `(months, days, micros)`.
26667///
26668/// * A single field applied to a bare number sets which unit the number means,
26669/// truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
26670/// SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
26671/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
26672/// * Every other range, and any literal a single field can't read as a plain
26673/// number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
26674/// interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
26675/// like PG, and the qualifier there only bounds precision.
26676fn interpret_qualified_interval(
26677 text: &str,
26678 (f1, f2): (IntervalField, Option<IntervalField>),
26679) -> Option<(i32, i32, i64)> {
26680 if let Some(f2) = f2 {
26681 if f1 == IntervalField::Year && f2 == IntervalField::Month {
26682 if let Some(m) = parse_year_month_literal(text) {
26683 return Some((m, 0, 0));
26684 }
26685 }
26686 return parse_interval_text(text);
26687 }
26688 // Single field: reinterpret a bare number; otherwise the default parse.
26689 let trimmed = text.trim();
26690 if let Ok(val) = trimmed.parse::<f64>() {
26691 // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
26692 #[allow(clippy::cast_possible_truncation)]
26693 let whole = val as i64;
26694 #[allow(clippy::cast_possible_truncation)]
26695 let secs_micros = {
26696 let m = val * 1_000_000.0;
26697 if m >= 0.0 {
26698 (m + 0.5) as i64
26699 } else {
26700 (m - 0.5) as i64
26701 }
26702 };
26703 return Some(match f1 {
26704 IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
26705 IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
26706 IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
26707 IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
26708 IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
26709 IntervalField::Second => (0, 0, secs_micros),
26710 });
26711 }
26712 parse_interval_text(text)
26713}
26714
26715/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
26716fn parse_year_month_literal(text: &str) -> Option<i32> {
26717 let t = text.trim();
26718 let (neg, body) = match t.strip_prefix('-') {
26719 Some(r) => (true, r),
26720 None => (false, t.strip_prefix('+').unwrap_or(t)),
26721 };
26722 let mut it = body.split('-');
26723 let years: i32 = it.next()?.trim().parse().ok()?;
26724 let months: i32 = match it.next() {
26725 Some(m) => m.trim().parse().ok()?,
26726 None => 0,
26727 };
26728 if it.next().is_some() {
26729 return None;
26730 }
26731 let total = years.checked_mul(12)?.checked_add(months)?;
26732 Some(if neg { -total } else { total })
26733}
26734
26735pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
26736 // v7.38.19 — the two infinities, answered as the three extreme
26737 // fields PostgreSQL itself puts on the wire for them:
26738 //
26739 // COPY (SELECT 'infinity'::interval) TO STDOUT (FORMAT binary)
26740 // … 7fffffffffffffff 7fffffff 7fffffff
26741 //
26742 // So no caller has to know the spelling — every one of them already
26743 // reads the three numbers, and `IntervalKind::from_fields` names
26744 // what they mean.
26745 //
26746 // `inf` is NOT one of them, measured: `'inf'::interval` is *invalid
26747 // input syntax* on PostgreSQL 18.4 while `'inf'::float8` is
26748 // infinity. Interval takes the full word, in any case.
26749 {
26750 let word = s.trim();
26751 let word = word.strip_prefix('@').map_or(word, str::trim);
26752 let (neg, body) = match word.strip_prefix('-') {
26753 Some(rest) => (true, rest.trim_start()),
26754 None => (false, word.strip_prefix('+').map_or(word, str::trim_start)),
26755 };
26756 if body.eq_ignore_ascii_case("infinity") {
26757 return Some(if neg {
26758 (i32::MIN, i32::MIN, i64::MIN)
26759 } else {
26760 (i32::MAX, i32::MAX, i64::MAX)
26761 });
26762 }
26763 }
26764 // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
26765 // `@` is decorative; a trailing `ago` negates the whole interval.
26766 let mut trimmed = s.trim();
26767 trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
26768 let mut negate = false;
26769 if let Some(rest) = trimmed
26770 .strip_suffix("ago")
26771 .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
26772 {
26773 negate = true;
26774 trimmed = rest.trim();
26775 }
26776 let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
26777 let (mo, d, us) = v?;
26778 if negate {
26779 Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
26780 } else {
26781 Some((mo, d, us))
26782 }
26783 };
26784 let s = trimmed;
26785 // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
26786 // are single tokens, not the `<n> <unit>` pair form handled below.
26787 if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
26788 return finish(parse_iso8601_interval(rest));
26789 }
26790 if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
26791 if let Some(iv) = parse_year_month_interval(trimmed) {
26792 return finish(Some(iv));
26793 }
26794 }
26795 // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
26796 // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
26797 // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
26798 if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
26799 if let Ok(n) = trimmed.parse::<i64>() {
26800 return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
26801 }
26802 if let Ok(f) = trimmed.parse::<f64>() {
26803 if f.is_finite() {
26804 #[allow(clippy::cast_possible_truncation)]
26805 return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
26806 }
26807 }
26808 }
26809 // v7.39 (round 243) — PG accepts the number and unit run together
26810 // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
26811 // the `<n> <unit>` pair loop below sees them as two.
26812 let raw_parts: Vec<&str> = s.split_whitespace().collect();
26813 let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
26814 for p in raw_parts {
26815 let boundary = p
26816 .char_indices()
26817 .find(|(i, c)| {
26818 *i > 0
26819 && c.is_ascii_alphabetic()
26820 && p[..*i]
26821 .chars()
26822 .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
26823 && p[..*i].chars().any(|d| d.is_ascii_digit())
26824 })
26825 .map(|(i, _)| i);
26826 match boundary {
26827 Some(i) => {
26828 parts.push(&p[..i]);
26829 parts.push(&p[i..]);
26830 }
26831 None => parts.push(p),
26832 }
26833 }
26834 // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
26835 // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
26836 // remains is the `<n> <unit>` pair form handled below.
26837 let mut clock_us: i64 = 0;
26838 let mut had_clock = false;
26839 if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
26840 clock_us = parse_interval_clock(parts[pos])?;
26841 parts.remove(pos);
26842 had_clock = true;
26843 }
26844 // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
26845 // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
26846 let mut lone_days: i32 = 0;
26847 if had_clock && parts.len() == 1 {
26848 if let Ok(n) = parts[0].parse::<i64>() {
26849 lone_days = i32::try_from(n).ok()?;
26850 parts.clear();
26851 }
26852 }
26853 if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
26854 return None;
26855 }
26856 let mut months: i32 = 0;
26857 let mut days: i32 = lone_days;
26858 let mut micros: i64 = clock_us;
26859 let mut i = 0;
26860 while i < parts.len() {
26861 let unit_stripped = canonical_interval_unit(parts[i + 1])?;
26862 if let Ok(n) = parts[i].parse::<i64>() {
26863 match unit_stripped {
26864 "microsecond" => micros = micros.checked_add(n)?,
26865 "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
26866 "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
26867 "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
26868 "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
26869 "day" => {
26870 let n32 = i32::try_from(n).ok()?;
26871 days = days.checked_add(n32)?;
26872 }
26873 "week" => {
26874 let n32 = i32::try_from(n).ok()?;
26875 days = days.checked_add(n32.checked_mul(7)?)?;
26876 }
26877 "month" => {
26878 let n32 = i32::try_from(n).ok()?;
26879 months = months.checked_add(n32)?;
26880 }
26881 "year" => {
26882 let n32 = i32::try_from(n).ok()?;
26883 months = months.checked_add(n32.checked_mul(12)?)?;
26884 }
26885 // v7.39 (read01 timestamp.c) — the larger calendar units.
26886 "decade" => {
26887 let n32 = i32::try_from(n).ok()?;
26888 months = months.checked_add(n32.checked_mul(120)?)?;
26889 }
26890 "century" => {
26891 let n32 = i32::try_from(n).ok()?;
26892 months = months.checked_add(n32.checked_mul(1200)?)?;
26893 }
26894 "millennium" => {
26895 let n32 = i32::try_from(n).ok()?;
26896 months = months.checked_add(n32.checked_mul(12000)?)?;
26897 }
26898 _ => return None,
26899 }
26900 } else if let Ok(f) = parts[i].parse::<f64>() {
26901 // Fractional units cascade down to the next-finer field the way
26902 // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
26903 // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
26904 // no_std: f64 has no trunc/fract/round methods, so do them with
26905 // casts (toward-zero) + explicit round-half-away-from-zero.
26906 #[allow(clippy::cast_possible_truncation)]
26907 fn round_i64(x: f64) -> i64 {
26908 if x >= 0.0 {
26909 (x + 0.5) as i64
26910 } else {
26911 (x - 0.5) as i64
26912 }
26913 }
26914 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26915 fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
26916 const DAY_US: f64 = 86_400_000_000.0;
26917 let whole = d as i64; // truncates toward zero
26918 let frac = d - whole as f64;
26919 *days = days.checked_add(i32::try_from(whole).ok()?)?;
26920 *micros = micros.checked_add(round_i64(frac * DAY_US))?;
26921 Some(())
26922 }
26923 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26924 match unit_stripped {
26925 "microsecond" => micros = micros.checked_add(round_i64(f))?,
26926 "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
26927 "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
26928 "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
26929 "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
26930 "day" => add_days_frac(&mut days, &mut micros, f)?,
26931 "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
26932 "month" => {
26933 let whole = f as i64;
26934 months = months.checked_add(i32::try_from(whole).ok()?)?;
26935 add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
26936 }
26937 "year" => {
26938 let m = f * 12.0;
26939 let whole = m as i64;
26940 months = months.checked_add(i32::try_from(whole).ok()?)?;
26941 add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
26942 }
26943 _ => return None,
26944 }
26945 } else {
26946 return None;
26947 }
26948 i += 2;
26949 }
26950 finish(Some((months, days, micros)))
26951}
26952
26953/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
26954/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
26955/// `interval` is intentionally absent (handled by its own parser arm).
26956/// Returns `None` for names that aren't sensible as a bare typed literal, so
26957/// the caller falls back to treating the ident as a column reference.
26958fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
26959 Some(match ident {
26960 "date" => CastTarget::Date,
26961 "timestamp" | "datetime" => CastTarget::Timestamp,
26962 "timestamptz" => CastTarget::Timestamptz,
26963 "bool" | "boolean" => CastTarget::Bool,
26964 "int" | "integer" | "int4" => CastTarget::Int,
26965 "bigint" | "int8" => CastTarget::BigInt,
26966 "float8" | "double precision" => CastTarget::Float,
26967 "uuid" => CastTarget::Uuid,
26968 "bytea" => CastTarget::Bytea,
26969 "json" => CastTarget::Json,
26970 "jsonb" => CastTarget::Jsonb,
26971 // Types without a dedicated CastTarget variant flow through the
26972 // generic Named path (engine resolves via column_type_to_data_type).
26973 "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
26974 | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
26975 | "money" | "bit" | "varbit"
26976 // Geometric types accept the `TYPE 'literal'` prefix spelling too.
26977 | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
26978 // Range / multirange types likewise.
26979 | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
26980 | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
26981 | "datemultirange" | "tsmultirange" | "tstzmultirange"
26982 // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
26983 | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
26984 CastTarget::Named(alloc::string::String::from(ident))
26985 }
26986 _ => return None,
26987 })
26988}
26989
26990/// v7.12.4 — map a bare type-name identifier (the form that
26991/// appears in a function arg list or RETURNS clause) to a
26992/// [`ColumnTypeName`]. Returns `None` for unknown / extension
26993/// types so the caller can preserve them as
26994/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
26995///
26996/// Subset of the full column-type grammar — we deliberately
26997/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
26998/// here because function-arg types in v7.12.4 are mostly the
26999/// bare form (`text`, `int`, `bytea`, …).
27000/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
27001/// than being `name TYPE`?
27002///
27003/// The multi-word spellings SQL allows for a bare argument type, each
27004/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
27005///
27006/// NOTE this list also exists in `spg-storage`, which computes the
27007/// signature key from the rendered argument text and has to reach the
27008/// same verdict. The two crates are siblings — neither depends on the
27009/// other — and each already carries its own table of type spellings
27010/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
27011/// there), so this follows the structure rather than inventing new
27012/// duplication. Recorded as V49.
27013pub fn is_multiword_type_phrase(phrase: &str) -> bool {
27014 let t = phrase.trim().to_ascii_lowercase();
27015 let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
27016 matches!(
27017 base,
27018 "double precision"
27019 | "character varying"
27020 | "bit varying"
27021 | "timestamp with time zone"
27022 | "timestamp without time zone"
27023 | "time with time zone"
27024 | "time without time zone"
27025 | "national character"
27026 | "national character varying"
27027 )
27028}
27029
27030fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
27031 Some(match ident.to_ascii_lowercase().as_str() {
27032 "smallint" | "tinyint" => ColumnTypeName::SmallInt,
27033 "int" | "integer" | "mediumint" => ColumnTypeName::Int,
27034 "bigint" => ColumnTypeName::BigInt,
27035 "float" | "double" => ColumnTypeName::Float,
27036 // v7.39 (round 269) — real is 32-bit.
27037 "real" | "float4" => ColumnTypeName::Real,
27038 "text" => ColumnTypeName::Text,
27039 "bool" | "boolean" => ColumnTypeName::Bool,
27040 "date" => ColumnTypeName::Date,
27041 "timestamp" | "datetime" => ColumnTypeName::Timestamp,
27042 "timestamptz" => ColumnTypeName::Timestamptz,
27043 "json" => ColumnTypeName::Json,
27044 "jsonb" => ColumnTypeName::Jsonb,
27045 "bytea" | "bytes" => ColumnTypeName::Bytes,
27046 "tsvector" => ColumnTypeName::TsVector,
27047 "tsquery" => ColumnTypeName::TsQuery,
27048 "uuid" => ColumnTypeName::Uuid,
27049 "interval" => ColumnTypeName::Interval,
27050 "time" => ColumnTypeName::Time,
27051 "year" => ColumnTypeName::Year,
27052 "timetz" => ColumnTypeName::TimeTz,
27053 "money" => ColumnTypeName::Money,
27054 _ => return None,
27055 })
27056}
27057
27058/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
27059/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
27060///
27061/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
27062/// / embedded SQL land in v7.12.5+):
27063///
27064/// ```text
27065/// body := [ws] block [ws]
27066/// block := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
27067/// stmt := assign | return
27068/// assign := assign_target := expr
27069/// assign_target := ( NEW | OLD ) . ident | ident
27070/// return := RETURN ( NEW | OLD | NULL | expr )
27071/// ```
27072///
27073/// `expr` is parsed by recursing into the regular `Parser` — so a
27074/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
27075/// NEW.subject || ' ' || NEW.sender)` body shape works without
27076/// the body parser knowing what `to_tsvector` is.
27077///
27078/// Errors here cause the caller to fall back to
27079/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
27080/// successful, but the executor will refuse to invoke the
27081/// function with an "unparseable body" error.
27082/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
27083/// from the crate root as `spg_sql::parse_function_body`.
27084pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
27085 parse_plpgsql_body(body)
27086}
27087
27088fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
27089 // Use the regular lexer on the body text. The trailing
27090 // `END;` may or may not have a semicolon; the lexer treats
27091 // both forms identically.
27092 let tokens = lexer::tokenize(body).map_err(|e| ParseError {
27093 message: alloc::format!("plpgsql body lex error: {e}"),
27094 token_pos: 0,
27095 })?;
27096 let mut parser = Parser::new(tokens);
27097 parser.parse_plpgsql_block()
27098}
27099
27100/// v7.39 (GUC) — the textual body of a SET value, for list joining.
27101fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
27102 match v {
27103 crate::ast::SetValue::String(s)
27104 | crate::ast::SetValue::Ident(s)
27105 | crate::ast::SetValue::Number(s) => s.clone(),
27106 crate::ast::SetValue::Default => "DEFAULT".into(),
27107 }
27108}
27109
27110/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
27111/// contains an aggregate call at ITS OWN query level (recursion stops at
27112/// sublink boundaries — a sublink's aggregates belong to the sublink).
27113/// Backs the "aggregate functions are not allowed in a recursive query's
27114/// recursive term" well-formedness check.
27115fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
27116 const AGG_NAMES: &[&str] = &[
27117 "count",
27118 "sum",
27119 "min",
27120 "max",
27121 "avg",
27122 "string_agg",
27123 "array_agg",
27124 "bool_and",
27125 "bool_or",
27126 "every",
27127 "any_value",
27128 "json_agg",
27129 "jsonb_agg",
27130 "json_object_agg",
27131 "jsonb_object_agg",
27132 "bit_and",
27133 "bit_or",
27134 "bit_xor",
27135 "var_pop",
27136 "var_samp",
27137 "variance",
27138 "stddev",
27139 "stddev_pop",
27140 "stddev_samp",
27141 "range_agg",
27142 "range_intersect_agg",
27143 "percentile_cont",
27144 "percentile_disc",
27145 "mode",
27146 "corr",
27147 "covar_pop",
27148 "covar_samp",
27149 ];
27150 match e {
27151 Expr::AggregateOrdered { .. } => true,
27152 Expr::FunctionCall { name, args } => {
27153 AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
27154 || args.iter().any(expr_has_toplevel_aggregate)
27155 }
27156 Expr::NamedArg { expr, .. }
27157 | Expr::Variadic(expr)
27158 | Expr::Unary { expr, .. }
27159 | Expr::Cast { expr, .. }
27160 | Expr::IsNull { expr, .. }
27161 | Expr::FieldAccess { base: expr, .. }
27162 | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
27163 Expr::Binary { lhs, rhs, .. } => {
27164 expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
27165 }
27166 Expr::Like { expr, pattern, .. } => {
27167 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
27168 }
27169 Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
27170 Expr::InList { expr, list, .. } => {
27171 expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
27172 }
27173 Expr::ArraySubscript { target, index } => {
27174 expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
27175 }
27176 Expr::ArraySlice { target, lo, hi } => {
27177 expr_has_toplevel_aggregate(target)
27178 || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
27179 || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
27180 }
27181 Expr::AnyAll { expr, array, .. } => {
27182 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
27183 }
27184 Expr::Case {
27185 operand,
27186 branches,
27187 else_branch,
27188 } => {
27189 operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
27190 || branches
27191 .iter()
27192 .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
27193 || else_branch
27194 .as_deref()
27195 .is_some_and(expr_has_toplevel_aggregate)
27196 }
27197 // The outer-level operands of a sublink can aggregate; the sublink's
27198 // own body cannot leak its aggregates up here.
27199 Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
27200 Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
27201 row.iter().any(expr_has_toplevel_aggregate)
27202 }
27203 _ => false,
27204 }
27205}
27206
27207/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
27208/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
27209/// named table anywhere in its subtree. A plain FROM derived table is NOT a
27210/// sublink and is legal in a recursive term, so it is not walked here.
27211fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
27212 let mut exprs: Vec<&Expr> = Vec::new();
27213 for it in &s.items {
27214 if let crate::ast::SelectItem::Expr { expr, .. } = it {
27215 exprs.push(expr);
27216 }
27217 }
27218 if let Some(w) = &s.where_ {
27219 exprs.push(w);
27220 }
27221 if let Some(h) = &s.having {
27222 exprs.push(h);
27223 }
27224 if let Some(g) = &s.group_by {
27225 exprs.extend(g.iter());
27226 }
27227 if let Some(from) = &s.from {
27228 for j in &from.joins {
27229 if let Some(on) = &j.on {
27230 exprs.push(on);
27231 }
27232 }
27233 }
27234 exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
27235}
27236
27237/// Does this expression contain a sublink whose subquery mentions `name`?
27238fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
27239 match e {
27240 Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
27241 Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
27242 Expr::InSubquery { expr, subquery, .. } => {
27243 expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
27244 }
27245 Expr::RowInSubquery { row, subquery, .. } => {
27246 row.iter().any(|x| expr_sublink_mentions(x, name))
27247 || select_mentions_table(subquery, name)
27248 }
27249 Expr::RowCmpSubquery { row, subquery, .. } => {
27250 row.iter().any(|x| expr_sublink_mentions(x, name))
27251 || select_mentions_table(subquery, name)
27252 }
27253 Expr::NamedArg { expr, .. }
27254 | Expr::Variadic(expr)
27255 | Expr::Unary { expr, .. }
27256 | Expr::Cast { expr, .. }
27257 | Expr::IsNull { expr, .. }
27258 | Expr::FieldAccess { base: expr, .. }
27259 | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
27260 Expr::Binary { lhs, rhs, .. } => {
27261 expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
27262 }
27263 Expr::Like { expr, pattern, .. } => {
27264 expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
27265 }
27266 Expr::FunctionCall { args, .. } | Expr::Array(args) => {
27267 args.iter().any(|x| expr_sublink_mentions(x, name))
27268 }
27269 Expr::InList { expr, list, .. } => {
27270 expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
27271 }
27272 Expr::ArraySubscript { target, index } => {
27273 expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
27274 }
27275 Expr::ArraySlice { target, lo, hi } => {
27276 expr_sublink_mentions(target, name)
27277 || lo
27278 .as_deref()
27279 .is_some_and(|x| expr_sublink_mentions(x, name))
27280 || hi
27281 .as_deref()
27282 .is_some_and(|x| expr_sublink_mentions(x, name))
27283 }
27284 Expr::AnyAll { expr, array, .. } => {
27285 expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
27286 }
27287 Expr::Case {
27288 operand,
27289 branches,
27290 else_branch,
27291 } => {
27292 operand
27293 .as_deref()
27294 .is_some_and(|x| expr_sublink_mentions(x, name))
27295 || branches
27296 .iter()
27297 .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
27298 || else_branch
27299 .as_deref()
27300 .is_some_and(|x| expr_sublink_mentions(x, name))
27301 }
27302 _ => false,
27303 }
27304}
27305
27306/// Does this SELECT (in full — FROM tables, derived tables, its own
27307/// sublinks, and union arms) mention the named table?
27308fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
27309 if let Some(from) = &s.from {
27310 if from.primary.name.eq_ignore_ascii_case(name) {
27311 return true;
27312 }
27313 if let Some(sub) = &from.primary.lateral_subquery
27314 && select_mentions_table(sub, name)
27315 {
27316 return true;
27317 }
27318 for j in &from.joins {
27319 if j.table.name.eq_ignore_ascii_case(name) {
27320 return true;
27321 }
27322 if let Some(sub) = &j.table.lateral_subquery
27323 && select_mentions_table(sub, name)
27324 {
27325 return true;
27326 }
27327 }
27328 }
27329 if select_has_self_ref_in_sublink(s, name) {
27330 return true;
27331 }
27332 s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
27333}
27334
27335/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
27336/// row count, the way PG evaluates one before applying it.
27337///
27338/// `None` = not a constant (a column, a subquery, a function call).
27339/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
27340/// message stands in for LIMIT / OFFSET, which the caller substitutes.
27341/// All wordings were read off live PG 18.4.
27342fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
27343 use crate::ast::{BinOp, Expr, Literal, UnOp};
27344 match e {
27345 Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
27346 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27347 Some(Ok(round_scaled_half_away(*unscaled, *scale)))
27348 }
27349 // PG coerces a string by its CONTENT, and fails on the value.
27350 Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
27351 |_| {
27352 Err(alloc::format!(
27353 "invalid input syntax for type bigint: \"{t}\""
27354 ))
27355 },
27356 |n| Ok(i128::from(n)),
27357 )),
27358 Expr::Literal(Literal::Bool(_)) => Some(Err(
27359 "argument of {L} must be type bigint, not type boolean".into(),
27360 )),
27361 Expr::Unary {
27362 op: UnOp::Neg,
27363 expr,
27364 } => match fold_limit_constant(expr)? {
27365 Ok(v) => Some(Ok(-v)),
27366 e @ Err(_) => Some(e),
27367 },
27368 Expr::Binary { lhs, op, rhs } => {
27369 let a = match fold_limit_constant(lhs)? {
27370 Ok(v) => v,
27371 e @ Err(_) => return Some(e),
27372 };
27373 let b = match fold_limit_constant(rhs)? {
27374 Ok(v) => v,
27375 e @ Err(_) => return Some(e),
27376 };
27377 let out = match op {
27378 BinOp::Add => a.checked_add(b),
27379 BinOp::Sub => a.checked_sub(b),
27380 BinOp::Mul => a.checked_mul(b),
27381 BinOp::Div if b != 0 => a.checked_div(b),
27382 BinOp::Div => return Some(Err("division by zero".into())),
27383 BinOp::Mod if b != 0 => a.checked_rem(b),
27384 BinOp::Mod => return Some(Err("division by zero".into())),
27385 _ => return None,
27386 };
27387 // PG evaluates the arithmetic in the operand's own type, so an
27388 // int-by-int product that leaves int range fails there — before
27389 // the row count is ever looked at.
27390 match out {
27391 Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
27392 Some(Err("integer out of range".into()))
27393 }
27394 Some(v) => Some(Ok(v)),
27395 None => Some(Err("integer out of range".into())),
27396 }
27397 }
27398 _ => None,
27399 }
27400}
27401
27402/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
27403/// cast, which is what makes `LIMIT 2.5` keep three rows.
27404fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
27405 if scale == 0 {
27406 return unscaled;
27407 }
27408 let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
27409 return 0;
27410 };
27411 let neg = unscaled < 0;
27412 let mag = unscaled.unsigned_abs() as i128;
27413 let rounded = (mag + div / 2) / div;
27414 if neg { -rounded } else { rounded }
27415}
27416
27417#[cfg(test)]
27418mod tests {
27419 use super::*;
27420 use alloc::string::ToString;
27421
27422 fn parse(s: &str) -> Statement {
27423 parse_statement(s).expect("parse ok")
27424 }
27425
27426 // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
27427 // `tables`, `partition`, etc. are unreserved keywords per PG's
27428 // `pg_get_keywords()` and MUST be usable as column / table /
27429 // alias names. Pre-T4 every drop-in user whose schema had one
27430 // of these as a column name (sentori events.release, mailrs
27431 // messages.index in some forks) blew the parser up at CREATE
27432 // TABLE time with "expected identifier, got Release". The
27433 // generalisation lives in `unreserved_keyword_text` + the
27434 // `expect_ident_like` and `parse_atom` arms that consult it.
27435 #[test]
27436 fn release_usable_as_column_name_in_create_table() {
27437 let stmt =
27438 parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
27439 if let Statement::CreateTable(t) = stmt {
27440 let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
27441 assert_eq!(names, alloc::vec!["id", "release", "payload"]);
27442 } else {
27443 panic!("expected CreateTable");
27444 }
27445 }
27446
27447 #[test]
27448 fn release_usable_as_column_ref_in_select_projection() {
27449 // The sentori `0003_partition_events.sql` INSERT-SELECT
27450 // walk references `release` in both column lists; the
27451 // projection-side use exercises `parse_atom`'s relaxed
27452 // identifier set.
27453 parse("SELECT id, release, payload FROM events WHERE id = 1");
27454 }
27455
27456 #[test]
27457 fn release_usable_as_column_ref_in_insert_column_list() {
27458 // INSERT INTO t (id, release, payload) VALUES (…)
27459 parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
27460 }
27461
27462 #[test]
27463 fn alter_column_drop_not_null_uses_keyword_drop_token() {
27464 // Sentori `0013_audit_tombstone.sql` issues
27465 // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
27466 // emits Token::Drop (not Ident("drop")); the parser must
27467 // accept both in the ALTER COLUMN sub-dispatch.
27468 parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
27469 }
27470
27471 #[test]
27472 fn create_index_accepts_parenthesised_expression_key() {
27473 // sentori `0040_events_bundle_idx.sql` shape — JSONB
27474 // expression index. Pre-T4 the parser bailed at the
27475 // inner `(` with "expected column ident or expression,
27476 // got LParen". The Token::LParen arm in CREATE INDEX
27477 // routes through the expression parser instead.
27478 parse(
27479 "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
27480 ON events ((payload->'bundle'->>'id'))",
27481 );
27482 }
27483
27484 // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
27485 // surface as parse errors, never stack overflows (embed hosts
27486 // abort on overflow).
27487 /// The nesting budget is a COUNT; what it has to fit inside is a
27488 /// number of BYTES, and only one of those two is stable across
27489 /// compiler versions. Round 847 measured 30,336 bytes per level
27490 /// after a toolchain move, which puts 64 levels at 1.94 MB and
27491 /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
27492 /// aborted instead of erroring, which is precisely the outcome it
27493 /// exists to rule out.
27494 ///
27495 /// So the budget is metered rather than assumed. The ceiling leaves
27496 /// the depth SPG advertises fitting in a default 2 MiB thread with
27497 /// room to spare, in the debug build, where frames are widest.
27498 #[test]
27499 fn nesting_frame_cost_stays_under_ceiling() {
27500 // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
27501 // thread keeps a margin for whatever called the parser.
27502 const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
27503
27504 frame_meter::reset();
27505 let depth = frame_meter::SAMPLE_HI + 8;
27506 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27507 parse(&sql);
27508
27509 let per_level = frame_meter::bytes_per_level();
27510 {
27511 extern crate std;
27512 std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
27513 }
27514 assert!(
27515 per_level <= CEILING,
27516 "{per_level} bytes per nesting level exceeds {CEILING}; \
27517 {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
27518 in parse_expr_inner / parse_unary rather than lowering the \
27519 depth or widening the stack.",
27520 per_level * MAX_NEST_DEPTH
27521 );
27522 }
27523
27524 #[test]
27525 fn nesting_budget_errors_cleanly() {
27526 let depth = MAX_NEST_DEPTH + 50;
27527 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27528 let err = parse_statement(&sql).expect_err("must reject");
27529 assert!(err.message.contains("nests deeper"), "{err:?}");
27530 // Within budget still parses.
27531 let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
27532 parse(&sql);
27533 }
27534
27535 #[test]
27536 fn binary_chain_budget_errors_cleanly() {
27537 let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
27538 let err = parse_statement(&sql).expect_err("must reject");
27539 assert!(err.message.contains("chained binary"), "{err:?}");
27540 // Within budget still parses (chain depth ≤ budget is safe
27541 // for recursive eval/drop on 2 MiB stacks).
27542 let sql = format!("SELECT 1{}", " + 1".repeat(200));
27543 parse(&sql);
27544 }
27545
27546 #[test]
27547 fn in_list_unaffected_by_chain_budget() {
27548 // Flat InList: 20k elements parse fine and stay flat.
27549 let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
27550 let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
27551 let Statement::Select(s) = parse(&sql) else {
27552 panic!("expected select")
27553 };
27554 let Some(Expr::InList { list, negated, .. }) = s.where_ else {
27555 panic!("expected flat InList, got {:?}", s.where_)
27556 };
27557 assert_eq!(list.len(), 20_000);
27558 assert!(!negated);
27559 }
27560
27561 fn lit_int(n: i64) -> Expr {
27562 Expr::Literal(Literal::Integer(n))
27563 }
27564
27565 fn col(name: &str) -> Expr {
27566 Expr::Column(ColumnName {
27567 qualifier: None,
27568 name: name.into(),
27569 })
27570 }
27571
27572 #[test]
27573 fn select_single_integer() {
27574 let s = parse("SELECT 1");
27575 let Statement::Select(s) = s else {
27576 panic!("expected SELECT")
27577 };
27578 assert_eq!(s.items.len(), 1);
27579 assert!(s.from.is_none());
27580 assert!(s.where_.is_none());
27581 }
27582
27583 #[test]
27584 fn select_multiple_literal_kinds() {
27585 let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
27586 let Statement::Select(s) = s else {
27587 panic!("expected SELECT")
27588 };
27589 assert_eq!(s.items.len(), 5);
27590 }
27591
27592 #[test]
27593 fn select_wildcard_from_table() {
27594 let s = parse("SELECT * FROM users");
27595 let Statement::Select(s) = s else {
27596 panic!("expected SELECT")
27597 };
27598 assert!(matches!(s.items[..], [SelectItem::Wildcard]));
27599 assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
27600 }
27601
27602 #[test]
27603 fn select_with_table_alias() {
27604 let s = parse("SELECT * FROM users AS u");
27605 let Statement::Select(s) = s else {
27606 panic!("expected SELECT")
27607 };
27608 let t = &s.from.as_ref().unwrap().primary;
27609 assert_eq!(t.name, "users");
27610 assert_eq!(t.alias.as_deref(), Some("u"));
27611 }
27612
27613 #[test]
27614 fn select_with_where_eq() {
27615 let s = parse("SELECT a FROM t WHERE a = 1");
27616 let Statement::Select(s) = s else {
27617 panic!("expected SELECT")
27618 };
27619 let w = s.where_.unwrap();
27620 assert_eq!(
27621 w,
27622 Expr::Binary {
27623 lhs: Box::new(col("a")),
27624 op: BinOp::Eq,
27625 rhs: Box::new(lit_int(1)),
27626 }
27627 );
27628 }
27629
27630 #[test]
27631 fn arithmetic_precedence() {
27632 let s = parse("SELECT 1 + 2 * 3");
27633 let Statement::Select(s) = s else {
27634 panic!("expected SELECT")
27635 };
27636 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27637 panic!("wildcard?")
27638 };
27639 assert_eq!(
27640 expr,
27641 &Expr::Binary {
27642 lhs: Box::new(lit_int(1)),
27643 op: BinOp::Add,
27644 rhs: Box::new(Expr::Binary {
27645 lhs: Box::new(lit_int(2)),
27646 op: BinOp::Mul,
27647 rhs: Box::new(lit_int(3)),
27648 }),
27649 }
27650 );
27651 }
27652
27653 #[test]
27654 fn parentheses_override_precedence() {
27655 let s = parse("SELECT (1 + 2) * 3");
27656 let Statement::Select(s) = s else {
27657 panic!("expected SELECT")
27658 };
27659 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27660 panic!()
27661 };
27662 assert_eq!(
27663 expr,
27664 &Expr::Binary {
27665 lhs: Box::new(Expr::Binary {
27666 lhs: Box::new(lit_int(1)),
27667 op: BinOp::Add,
27668 rhs: Box::new(lit_int(2)),
27669 }),
27670 op: BinOp::Mul,
27671 rhs: Box::new(lit_int(3)),
27672 }
27673 );
27674 }
27675
27676 #[test]
27677 fn not_binds_below_comparison() {
27678 // `NOT a = 1` should parse as `NOT (a = 1)`.
27679 let s = parse("SELECT NOT a = 1 FROM t");
27680 let Statement::Select(s) = s else {
27681 panic!("expected SELECT")
27682 };
27683 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27684 panic!()
27685 };
27686 assert_eq!(
27687 expr,
27688 &Expr::Unary {
27689 op: UnOp::Not,
27690 expr: Box::new(Expr::Binary {
27691 lhs: Box::new(col("a")),
27692 op: BinOp::Eq,
27693 rhs: Box::new(lit_int(1)),
27694 }),
27695 }
27696 );
27697 }
27698
27699 #[test]
27700 fn unary_minus_binds_above_multiplication() {
27701 // `-a * 2` should be `(-a) * 2`.
27702 let s = parse("SELECT -a * 2 FROM t");
27703 let Statement::Select(s) = s else {
27704 panic!("expected SELECT")
27705 };
27706 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27707 panic!()
27708 };
27709 assert_eq!(
27710 expr,
27711 &Expr::Binary {
27712 lhs: Box::new(Expr::Unary {
27713 op: UnOp::Neg,
27714 expr: Box::new(col("a")),
27715 }),
27716 op: BinOp::Mul,
27717 rhs: Box::new(lit_int(2)),
27718 }
27719 );
27720 }
27721
27722 #[test]
27723 fn qualified_column() {
27724 let s = parse("SELECT t.col FROM t");
27725 let Statement::Select(s) = s else {
27726 panic!("expected SELECT")
27727 };
27728 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27729 panic!()
27730 };
27731 assert_eq!(
27732 expr,
27733 &Expr::Column(ColumnName {
27734 qualifier: Some("t".into()),
27735 name: "col".into()
27736 })
27737 );
27738 }
27739
27740 #[test]
27741 fn select_item_alias_with_as() {
27742 let s = parse("SELECT a AS y FROM t");
27743 let Statement::Select(s) = s else {
27744 panic!("expected SELECT")
27745 };
27746 let SelectItem::Expr { alias, .. } = &s.items[0] else {
27747 panic!()
27748 };
27749 assert_eq!(alias.as_deref(), Some("y"));
27750 }
27751
27752 #[test]
27753 fn trailing_semicolon_accepted() {
27754 let s = parse("SELECT 1;");
27755 let Statement::Select(s) = s else {
27756 panic!("expected SELECT")
27757 };
27758 assert_eq!(s.items.len(), 1);
27759 }
27760
27761 #[test]
27762 fn boolean_chain_with_and_or_not() {
27763 // (NOT a) OR (b AND (NOT c))
27764 let s = parse("SELECT NOT a OR b AND NOT c FROM t");
27765 let Statement::Select(s) = s else {
27766 panic!("expected SELECT")
27767 };
27768 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27769 panic!()
27770 };
27771 let expected = Expr::Binary {
27772 lhs: Box::new(Expr::Unary {
27773 op: UnOp::Not,
27774 expr: Box::new(col("a")),
27775 }),
27776 op: BinOp::Or,
27777 rhs: Box::new(Expr::Binary {
27778 lhs: Box::new(col("b")),
27779 op: BinOp::And,
27780 rhs: Box::new(Expr::Unary {
27781 op: UnOp::Not,
27782 expr: Box::new(col("c")),
27783 }),
27784 }),
27785 };
27786 assert_eq!(expr, &expected);
27787 }
27788
27789 #[test]
27790 fn empty_input_errors() {
27791 // v7.14.0 — pg_dump preambles emit several comment-only
27792 // / blank-line statements that collapse to Statement::
27793 // Empty rather than a parse error. The old "SELECT in
27794 // message" assertion is stale; verify the new contract:
27795 // empty / whitespace / comment-only input parses to
27796 // Statement::Empty.
27797 assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
27798 assert!(matches!(
27799 parse_statement(" \n\t ").unwrap(),
27800 Statement::Empty
27801 ));
27802 // Sanity: malformed-but-non-empty still errors.
27803 assert!(parse_statement("SELECT FROM WHERE").is_err());
27804 }
27805
27806 #[test]
27807 fn unmatched_paren_errors() {
27808 assert!(parse_statement("SELECT (1 + 2").is_err());
27809 }
27810
27811 #[test]
27812 fn display_round_trip_simple_select() {
27813 let original = parse("SELECT a + 1 FROM t WHERE a > 0");
27814 let text = original.to_string();
27815 let again = parse_statement(&text).expect("re-parse");
27816 assert_eq!(original, again);
27817 }
27818
27819 // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
27820
27821 #[test]
27822 fn create_table_single_column() {
27823 let s = parse("CREATE TABLE foo (a INT)");
27824 let Statement::CreateTable(c) = s else {
27825 panic!("expected CreateTable")
27826 };
27827 assert_eq!(c.name, "foo");
27828 assert_eq!(c.columns.len(), 1);
27829 assert_eq!(c.columns[0].name, "a");
27830 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27831 assert!(c.columns[0].nullable);
27832 }
27833
27834 #[test]
27835 fn create_table_multi_column_with_not_null_mix() {
27836 let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
27837 let Statement::CreateTable(c) = s else {
27838 panic!()
27839 };
27840 assert_eq!(c.columns.len(), 4);
27841 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27842 assert!(!c.columns[0].nullable);
27843 assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
27844 assert!(c.columns[1].nullable);
27845 assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
27846 assert!(!c.columns[2].nullable);
27847 assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
27848 }
27849
27850 #[test]
27851 fn create_table_bigint_supported() {
27852 let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
27853 let Statement::CreateTable(c) = s else {
27854 panic!()
27855 };
27856 assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
27857 }
27858
27859 #[test]
27860 fn create_table_vector_default_is_f32() {
27861 let s = parse("CREATE TABLE t (v VECTOR(128))");
27862 let Statement::CreateTable(c) = s else {
27863 panic!()
27864 };
27865 assert_eq!(
27866 c.columns[0].ty,
27867 ColumnTypeName::Vector {
27868 dim: 128,
27869 encoding: VecEncoding::F32,
27870 },
27871 );
27872 }
27873
27874 #[test]
27875 fn create_table_vector_using_sq8() {
27876 // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
27877 // Case-insensitive on both `USING` and the encoding name.
27878 for sql in [
27879 "CREATE TABLE t (v VECTOR(128) USING SQ8)",
27880 "CREATE TABLE t (v VECTOR(128) using sq8)",
27881 ] {
27882 let s = parse(sql);
27883 let Statement::CreateTable(c) = s else {
27884 panic!()
27885 };
27886 assert_eq!(
27887 c.columns[0].ty,
27888 ColumnTypeName::Vector {
27889 dim: 128,
27890 encoding: VecEncoding::Sq8,
27891 },
27892 "{sql}",
27893 );
27894 }
27895 }
27896
27897 #[test]
27898 fn create_table_vector_using_unknown_errors() {
27899 // v7.16.1 — the inline `USING <encoding>` shape on
27900 // CREATE TABLE column defs was withdrawn before
27901 // v7.14.0 in favour of `CREATE INDEX … USING hnsw
27902 // (col vector_<metric>_ops)`; the parser now rejects
27903 // USING at column-list position with a clearer
27904 // "expected ',' or ')'" message. Test asserts the
27905 // current rejection, not the old "unknown vector
27906 // encoding" string.
27907 let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
27908 assert!(
27909 err.message.contains("USING")
27910 || err.message.contains("using")
27911 || err.message.contains("')'")
27912 || err.message.contains("','"),
27913 "expected USING/column-list rejection, got: {}",
27914 err.message
27915 );
27916 }
27917
27918 #[test]
27919 fn vector_using_sq8_display_roundtrips() {
27920 // The Display impl must produce text that re-parses to the
27921 // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
27922 let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
27923 let Statement::CreateTable(c) = s else {
27924 panic!()
27925 };
27926 assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
27927 }
27928
27929 #[test]
27930 fn parser_recognises_placeholders() {
27931 use crate::ast::{Expr, SelectItem, Statement};
27932 // $N in expression position parses as Expr::Placeholder(N).
27933 let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
27934 let Statement::Select(sel) = s else { panic!() };
27935 assert!(matches!(
27936 sel.items[0],
27937 SelectItem::Expr {
27938 expr: Expr::Placeholder(1),
27939 alias: None
27940 }
27941 ));
27942 // $2 + 1
27943 let SelectItem::Expr {
27944 expr: Expr::Binary { lhs, rhs, .. },
27945 ..
27946 } = &sel.items[1]
27947 else {
27948 panic!()
27949 };
27950 assert!(matches!(**lhs, Expr::Placeholder(2)));
27951 assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
27952 // WHERE x = $3
27953 let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
27954 panic!()
27955 };
27956 assert!(matches!(**rhs, Expr::Placeholder(3)));
27957 }
27958
27959 #[test]
27960 fn parser_rejects_dollar_zero() {
27961 // $0 is not valid in PG; the lexer rejects it.
27962 assert!(parse_statement("SELECT $0").is_err());
27963 }
27964
27965 #[test]
27966 fn placeholder_display_roundtrips() {
27967 // The Display impl must produce text that re-lexes to the
27968 // same Placeholder token.
27969 let s = parse("SELECT $42 FROM t");
27970 let printed = s.to_string();
27971 assert!(printed.contains("$42"));
27972 let again = parse(&printed);
27973 assert_eq!(s, again);
27974 }
27975
27976 #[test]
27977 fn alter_index_rebuild_bare() {
27978 use crate::ast::{AlterIndexTarget, Statement};
27979 let s = parse("ALTER INDEX my_idx REBUILD");
27980 let Statement::AlterIndex(a) = s else {
27981 panic!("expected AlterIndex, got {s:?}")
27982 };
27983 assert_eq!(a.name, "my_idx");
27984 assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
27985 }
27986
27987 #[test]
27988 fn alter_index_rebuild_with_encoding() {
27989 use crate::ast::{AlterIndexTarget, Statement};
27990 for (sql, want) in [
27991 (
27992 "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
27993 VecEncoding::F32,
27994 ),
27995 (
27996 "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
27997 VecEncoding::Sq8,
27998 ),
27999 (
28000 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28001 VecEncoding::F16,
28002 ),
28003 ] {
28004 let s = parse(sql);
28005 let Statement::AlterIndex(a) = s else {
28006 panic!("{sql}: expected AlterIndex")
28007 };
28008 assert_eq!(a.name, "my_idx");
28009 assert_eq!(
28010 a.target,
28011 AlterIndexTarget::Rebuild {
28012 encoding: Some(want)
28013 },
28014 "{sql}"
28015 );
28016 }
28017 }
28018
28019 #[test]
28020 fn alter_index_rebuild_unknown_encoding_errors() {
28021 let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
28022 assert!(
28023 err.message.contains("unknown vector encoding"),
28024 "got: {}",
28025 err.message
28026 );
28027 }
28028
28029 #[test]
28030 fn alter_index_rebuild_display_roundtrips() {
28031 for (input, want) in [
28032 ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
28033 (
28034 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28035 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28036 ),
28037 (
28038 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28039 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28040 ),
28041 ] {
28042 let s = parse(input);
28043 assert_eq!(s.to_string(), want);
28044 }
28045 }
28046
28047 #[test]
28048 fn create_table_unknown_type_defers_to_engine() {
28049 // v4.9 picked XML as a parse-time "unsupported column
28050 // type" probe. v7.17.0 Phase 1.4 changed the contract:
28051 // an unknown type ident parses as Text + `user_type_ref`
28052 // so CREATE TABLE can resolve user-defined enum / domain
28053 // types — rejection of truly-unknown types moved to the
28054 // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
28055 // to a first-class built-in, so this probe switched to a
28056 // synthetic name nothing in the lexer will ever recognise.
28057 let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
28058 let Statement::CreateTable(t) = stmt else {
28059 panic!("expected CreateTable");
28060 };
28061 assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
28062 }
28063
28064 #[test]
28065 fn create_table_missing_table_keyword_errors() {
28066 assert!(parse_statement("CREATE x (a INT)").is_err());
28067 }
28068
28069 // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
28070 // `PARTITION OF parent <bounds>` child parse + Display round-trip.
28071
28072 #[test]
28073 fn parse_create_table_partition_by_range() {
28074 use crate::ast::{PartitionBySpec, PartitionKindAst};
28075 let stmt = parse_statement(
28076 "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
28077 payload JSONB) PARTITION BY RANGE (ts)",
28078 )
28079 .unwrap();
28080 let Statement::CreateTable(t) = stmt else {
28081 panic!("expected CreateTable");
28082 };
28083 assert!(t.partition_of.is_none(), "parent has no partition_of");
28084 assert_eq!(t.columns.len(), 3);
28085 let by = t.partition_by.as_ref().expect("expected PARTITION BY");
28086 assert_eq!(
28087 by,
28088 &PartitionBySpec {
28089 kind: PartitionKindAst::Range,
28090 key_columns: alloc::vec!["ts".to_string()],
28091 }
28092 );
28093 // Display round-trip preserves the suffix. `quote_ident`
28094 // only adds double quotes when the ident needs escaping, so
28095 // a plain `ts` survives bare here.
28096 assert!(
28097 t.to_string().contains("PARTITION BY RANGE (ts)"),
28098 "Display lost PARTITION BY suffix: {t}"
28099 );
28100 }
28101
28102 #[test]
28103 fn parse_create_table_partition_of_range() {
28104 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
28105 let stmt = parse_statement(
28106 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
28107 FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
28108 )
28109 .unwrap();
28110 let Statement::CreateTable(t) = stmt else {
28111 panic!("expected CreateTable");
28112 };
28113 assert!(t.columns.is_empty(), "child inherits columns from parent");
28114 assert!(t.partition_by.is_none());
28115 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
28116 assert_eq!(of.parent_name, "events_partitioned");
28117 let PartitionOfSpec { bounds, .. } = of.clone();
28118 match bounds {
28119 PartitionOfBoundsAst::Range { lower, upper } => {
28120 assert!(lower.to_string().contains("2026-06-01"));
28121 assert!(upper.to_string().contains("2026-07-01"));
28122 }
28123 other => panic!("expected Range, got {other:?}"),
28124 }
28125 // Display round-trip emits the FOR VALUES tail. `quote_ident`
28126 // skips quotes when not required, so the parent name appears
28127 // bare here.
28128 let s = t.to_string();
28129 assert!(
28130 s.contains("PARTITION OF events_partitioned"),
28131 "Display lost PARTITION OF: {s}"
28132 );
28133 assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
28134 assert!(s.contains(") TO ("), "Display lost TO: {s}");
28135 }
28136
28137 #[test]
28138 fn parse_create_table_partition_of_default() {
28139 use crate::ast::PartitionOfBoundsAst;
28140 let stmt =
28141 parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
28142 .unwrap();
28143 let Statement::CreateTable(t) = stmt else {
28144 panic!("expected CreateTable");
28145 };
28146 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
28147 assert_eq!(of.parent_name, "events_partitioned");
28148 assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
28149 assert!(
28150 t.to_string()
28151 .contains("PARTITION OF events_partitioned DEFAULT"),
28152 "Display lost DEFAULT: {t}"
28153 );
28154 }
28155
28156 #[test]
28157 fn parse_create_table_partition_by_list() {
28158 // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
28159 // child with `FOR VALUES IN (lit, lit, …)`.
28160 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28161 let parent =
28162 parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
28163 .unwrap();
28164 let Statement::CreateTable(t) = parent else {
28165 panic!("expected CreateTable");
28166 };
28167 let Some(PartitionBySpec {
28168 kind,
28169 ref key_columns,
28170 }) = t.partition_by
28171 else {
28172 panic!("expected PARTITION BY");
28173 };
28174 assert_eq!(kind, PartitionKindAst::List);
28175 assert_eq!(*key_columns, vec!["region".to_string()]);
28176 assert!(t.to_string().contains("PARTITION BY LIST (region)"));
28177
28178 let child = parse_statement(
28179 "CREATE TABLE events_apac PARTITION OF events_listed \
28180 FOR VALUES IN ('jp', 'kr', 'tw')",
28181 )
28182 .unwrap();
28183 let Statement::CreateTable(c) = child else {
28184 panic!("expected CreateTable");
28185 };
28186 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28187 let PartitionOfBoundsAst::List { values } = &of.bounds else {
28188 panic!("expected List bounds, got {:?}", of.bounds);
28189 };
28190 assert_eq!(values.len(), 3);
28191 let disp = c.to_string();
28192 assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
28193 }
28194
28195 #[test]
28196 fn parse_create_table_partition_by_hash() {
28197 // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
28198 // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
28199 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28200 let parent =
28201 parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
28202 let Statement::CreateTable(t) = parent else {
28203 panic!("expected CreateTable");
28204 };
28205 let Some(PartitionBySpec {
28206 kind,
28207 ref key_columns,
28208 }) = t.partition_by
28209 else {
28210 panic!("expected PARTITION BY");
28211 };
28212 assert_eq!(kind, PartitionKindAst::Hash);
28213 assert_eq!(*key_columns, vec!["id".to_string()]);
28214 assert!(t.to_string().contains("PARTITION BY HASH (id)"));
28215
28216 let child = parse_statement(
28217 "CREATE TABLE orders_h_0 PARTITION OF orders_h \
28218 FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
28219 )
28220 .unwrap();
28221 let Statement::CreateTable(c) = child else {
28222 panic!("expected CreateTable");
28223 };
28224 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28225 let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
28226 panic!("expected Hash bounds");
28227 };
28228 assert_eq!(modulus, 4);
28229 assert_eq!(remainder, 0);
28230 let disp = c.to_string();
28231 assert!(
28232 disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
28233 "Display lost HASH bounds: {disp}"
28234 );
28235
28236 // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
28237 let bad = parse_statement(
28238 "CREATE TABLE orders_h_bad PARTITION OF orders_h \
28239 FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
28240 );
28241 let msg = format!("{}", bad.unwrap_err());
28242 assert!(
28243 msg.contains("REMAINDER") && msg.contains("MODULUS"),
28244 "expected REMAINDER/MODULUS validation error: {msg}"
28245 );
28246 }
28247
28248 #[test]
28249 fn parse_create_table_partition_of_rejects_columns() {
28250 // v7.37.6-B contract: PARTITION OF children inherit columns
28251 // from the parent; an explicit list MUST surface as a parse
28252 // error rather than getting silently ignored.
28253 let err = parse_statement(
28254 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
28255 FOR VALUES FROM ('a') TO ('b')",
28256 );
28257 assert!(err.is_err(), "expected parse error for explicit columns");
28258 let msg = format!("{}", err.unwrap_err());
28259 assert!(
28260 msg.contains("PARTITION OF") && msg.contains("column"),
28261 "error should mention PARTITION OF + columns: {msg}"
28262 );
28263 }
28264
28265 #[test]
28266 fn insert_single_value() {
28267 let s = parse("INSERT INTO foo VALUES (42)");
28268 let Statement::Insert(i) = s else {
28269 panic!("expected Insert")
28270 };
28271 assert_eq!(i.table, "foo");
28272 assert_eq!(i.rows.len(), 1);
28273 assert_eq!(i.rows[0].len(), 1);
28274 assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
28275 }
28276
28277 #[test]
28278 fn insert_multi_value_with_mixed_literals() {
28279 let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
28280 let Statement::Insert(i) = s else { panic!() };
28281 assert_eq!(i.rows.len(), 1);
28282 assert_eq!(i.rows[0].len(), 5);
28283 }
28284
28285 #[test]
28286 fn insert_missing_into_errors() {
28287 assert!(parse_statement("INSERT foo VALUES (1)").is_err());
28288 }
28289
28290 #[test]
28291 fn create_table_round_trip() {
28292 let original =
28293 parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
28294 let text = original.to_string();
28295 let again = parse_statement(&text).expect("re-parse");
28296 assert_eq!(original, again);
28297 }
28298
28299 #[test]
28300 fn insert_round_trip_with_negation_and_string() {
28301 let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
28302 let text = original.to_string();
28303 let again = parse_statement(&text).expect("re-parse");
28304 assert_eq!(original, again);
28305 }
28306
28307 #[test]
28308 fn unknown_keyword_at_statement_start_errors() {
28309 // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
28310 // the top-level dispatch still has no branch to take.
28311 let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
28312 assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
28313 }
28314
28315 // --- v0.8 CREATE INDEX --------------------------------------------------
28316
28317 #[test]
28318 fn create_index_basic() {
28319 let s = parse("CREATE INDEX idx_id ON users (id)");
28320 let Statement::CreateIndex(c) = s else {
28321 panic!("expected CreateIndex")
28322 };
28323 assert_eq!(c.name, "idx_id");
28324 assert_eq!(c.table, "users");
28325 assert_eq!(c.column, "id");
28326 }
28327
28328 #[test]
28329 fn create_index_missing_on_errors() {
28330 assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
28331 }
28332
28333 #[test]
28334 fn create_index_missing_paren_errors() {
28335 assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
28336 }
28337
28338 #[test]
28339 fn create_index_round_trip() {
28340 let original = parse("CREATE INDEX by_name ON users (name)");
28341 let again = parse_statement(&original.to_string()).unwrap();
28342 assert_eq!(original, again);
28343 }
28344
28345 // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
28346
28347 #[test]
28348 fn create_unique_index_basic() {
28349 let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
28350 let Statement::CreateIndex(c) = s else {
28351 panic!("expected CreateIndex");
28352 };
28353 assert!(c.is_unique);
28354 assert_eq!(c.column, "a");
28355 assert!(c.partial_predicate.is_none());
28356 }
28357
28358 #[test]
28359 fn create_unique_index_partial() {
28360 // mailrs's email_templates "one default per user" shape.
28361 let s = parse(
28362 "CREATE UNIQUE INDEX idx_email_templates_user_default \
28363 ON email_templates (user_address) WHERE is_default = true",
28364 );
28365 let Statement::CreateIndex(c) = s else {
28366 panic!("expected CreateIndex");
28367 };
28368 assert!(c.is_unique);
28369 assert_eq!(c.table, "email_templates");
28370 assert_eq!(c.column, "user_address");
28371 assert!(c.partial_predicate.is_some());
28372 }
28373
28374 #[test]
28375 fn create_unique_index_composite_with_predicate() {
28376 // mailrs's calendar_events instance: composite columns.
28377 let s = parse(
28378 "CREATE UNIQUE INDEX uq_calendar_events_instance \
28379 ON calendar_events (calendar_id, uid, recurrence_id) \
28380 WHERE recurrence_id IS NOT NULL",
28381 );
28382 let Statement::CreateIndex(c) = s else {
28383 panic!("expected CreateIndex");
28384 };
28385 assert!(c.is_unique);
28386 assert_eq!(c.column, "calendar_id");
28387 assert_eq!(
28388 c.extra_columns,
28389 vec!["uid".to_string(), "recurrence_id".to_string()]
28390 );
28391 assert!(c.partial_predicate.is_some());
28392 }
28393
28394 #[test]
28395 fn create_unique_index_using_btree_ok() {
28396 let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
28397 assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
28398 }
28399
28400 #[test]
28401 fn create_unique_index_using_hnsw_rejected() {
28402 let err =
28403 parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
28404 assert!(err.message.contains("UNIQUE"), "{}", err.message);
28405 }
28406
28407 #[test]
28408 fn create_unique_index_round_trip() {
28409 let original = parse(
28410 "CREATE UNIQUE INDEX uq_calendar_events_master \
28411 ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
28412 );
28413 let again = parse_statement(&original.to_string()).unwrap();
28414 assert_eq!(original, again);
28415 }
28416
28417 #[test]
28418 fn create_unique_without_index_errors() {
28419 let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
28420 // v7.39 (round 340, V56) — PG 18.4, verbatim.
28421 assert_eq!(err.message, "syntax error at or near \"TABLE\"");
28422 }
28423
28424 // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
28425
28426 #[test]
28427 fn create_table_bytea_column() {
28428 let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
28429 let Statement::CreateTable(c) = s else {
28430 panic!("expected CreateTable");
28431 };
28432 assert_eq!(c.columns.len(), 2);
28433 assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
28434 assert!(!c.columns[1].nullable);
28435 }
28436
28437 #[test]
28438 fn create_table_bytes_alias_column() {
28439 let s = parse("CREATE TABLE t (blob BYTES)");
28440 let Statement::CreateTable(c) = s else {
28441 panic!("expected CreateTable");
28442 };
28443 assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
28444 }
28445
28446 #[test]
28447 fn bytea_round_trip_display() {
28448 let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
28449 let again = parse_statement(&original.to_string()).unwrap();
28450 assert_eq!(original, again);
28451 }
28452
28453 // --- v0.9 transactions -------------------------------------------------
28454
28455 #[test]
28456 fn begin_commit_rollback_parse_as_unit_variants() {
28457 assert_eq!(parse("BEGIN"), Statement::Begin(None));
28458 assert_eq!(parse("COMMIT"), Statement::Commit);
28459 // r1066 — PG synonyms pgbench's tpcb script relies on.
28460 assert_eq!(parse("END"), Statement::Commit);
28461 assert_eq!(parse("END TRANSACTION"), Statement::Commit);
28462 assert_eq!(parse("COMMIT WORK"), Statement::Commit);
28463 assert_eq!(parse("ROLLBACK"), Statement::Rollback);
28464 // Trailing semicolons accepted too.
28465 assert_eq!(parse("BEGIN;"), Statement::Begin(None));
28466 // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
28467 // statement (with or without the WORK/TRANSACTION noise word).
28468 assert_eq!(
28469 parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
28470 Statement::Begin(Some(IsolationLevel::RepeatableRead))
28471 );
28472 assert_eq!(
28473 parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
28474 Statement::Begin(Some(IsolationLevel::Serializable))
28475 );
28476 // A non-isolation mode keeps the session default (None).
28477 assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
28478 }
28479
28480 // --- v1.2: pgvector distance ops + ::vector cast --------------------
28481
28482 #[test]
28483 fn inner_product_binop_parses() {
28484 let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
28485 let Statement::Select(s) = s else { panic!() };
28486 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28487 panic!()
28488 };
28489 assert!(matches!(
28490 expr,
28491 Expr::Binary {
28492 op: BinOp::InnerProduct,
28493 ..
28494 }
28495 ));
28496 }
28497
28498 #[test]
28499 fn cosine_distance_binop_parses() {
28500 let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
28501 let Statement::Select(s) = s else { panic!() };
28502 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28503 panic!()
28504 };
28505 assert!(matches!(
28506 expr,
28507 Expr::Binary {
28508 op: BinOp::CosineDistance,
28509 ..
28510 }
28511 ));
28512 }
28513
28514 #[test]
28515 fn vector_cast_postfix_wraps_string_literal() {
28516 let s = parse("SELECT '[1,2,3]'::vector FROM t");
28517 let Statement::Select(s) = s else { panic!() };
28518 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28519 panic!()
28520 };
28521 assert!(matches!(
28522 expr,
28523 Expr::Cast {
28524 target: CastTarget::Vector,
28525 ..
28526 }
28527 ));
28528 }
28529
28530 #[test]
28531 fn unsupported_cast_target_errors() {
28532 // v7.37.5 ship triage promoted the parser to accept every
28533 // ident as a `CastTarget::Named(canonical)`; the engine
28534 // surfaces the "unsupported cast target" error at eval
28535 // time when `type_name_to_data_type` can't resolve it.
28536 // Parser-side error now requires a NON-ident after `::`
28537 // (e.g. a punctuation token).
28538 let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
28539 assert_eq!(err.message, "syntax error at or near \",\"");
28540 }
28541
28542 #[test]
28543 fn tx_statements_round_trip() {
28544 for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
28545 let original = parse(q);
28546 let again = parse_statement(&original.to_string()).unwrap();
28547 assert_eq!(original, again);
28548 }
28549 }
28550
28551 #[test]
28552 fn interval_text_parsing_units() {
28553 // v7.37.5 β — three-field shape `(months, days, micros)` so
28554 // `'1 day'` and `'24 hours'` no longer collide (PG parity).
28555 // Single unit.
28556 assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
28557 assert_eq!(
28558 parse_interval_text("24 hours"),
28559 Some((0, 0, 86_400_000_000))
28560 );
28561 assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
28562 assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
28563 assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
28564 assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
28565 // Compound spans accumulate per-dimension.
28566 assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
28567 assert_eq!(
28568 parse_interval_text("1 day 2 hours"),
28569 Some((0, 1, 7_200_000_000))
28570 );
28571 // Negative numbers carry through per-dimension.
28572 assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
28573 // Bad shapes return None.
28574 assert_eq!(parse_interval_text(""), None);
28575 assert_eq!(parse_interval_text("garbage"), None);
28576 assert_eq!(parse_interval_text("1 fortnight"), None);
28577 // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
28578 // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
28579 assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
28580 assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
28581 assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
28582 }
28583
28584 #[test]
28585 fn interval_literal_roundtrips_via_display() {
28586 let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
28587 let s = parsed.to_string();
28588 // Display preserves the original text verbatim.
28589 assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
28590 // And re-parsing yields a structurally equal statement.
28591 let again = parse_statement(&s).unwrap();
28592 assert_eq!(parsed, again);
28593 }
28594
28595 // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
28596
28597 #[test]
28598 fn parser_recognises_create_publication_bare() {
28599 let s = parse("CREATE PUBLICATION pub_a");
28600 let Statement::CreatePublication(p) = s else {
28601 panic!("expected CreatePublication, got {s:?}")
28602 };
28603 assert_eq!(p.name, "pub_a");
28604 assert_eq!(p.scope, PublicationScope::AllTables);
28605 }
28606
28607 #[test]
28608 fn parser_recognises_create_publication_for_all_tables() {
28609 let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
28610 let Statement::CreatePublication(p) = s else {
28611 panic!("expected CreatePublication, got {s:?}")
28612 };
28613 assert_eq!(p.name, "pub_a");
28614 assert_eq!(p.scope, PublicationScope::AllTables);
28615 }
28616
28617 #[test]
28618 fn parser_recognises_drop_publication() {
28619 let s = parse("DROP PUBLICATION pub_a");
28620 let Statement::DropPublication { name, .. } = s else {
28621 panic!("expected DropPublication, got {s:?}")
28622 };
28623 assert_eq!(name, "pub_a");
28624 }
28625
28626 #[test]
28627 fn parser_recognises_for_table_list() {
28628 let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
28629 let Statement::CreatePublication(p) = s else {
28630 panic!("expected CreatePublication, got {s:?}")
28631 };
28632 assert_eq!(p.name, "pub_a");
28633 let PublicationScope::ForTables(ts) = p.scope else {
28634 panic!("expected ForTables scope")
28635 };
28636 assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
28637 }
28638
28639 #[test]
28640 fn parser_rejects_bare_for_tables_and_takes_in_schema() {
28641 // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
28642 // is rejected (`invalid publication object list`; the old
28643 // test pinned an unverifiable "PG 19 accepts both" claim);
28644 // TABLES pairs with IN SCHEMA.
28645 let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
28646 .expect_err("bare FOR TABLES must reject");
28647 assert!(
28648 alloc::format!("{err}").contains("invalid publication object list"),
28649 "got: {err}"
28650 );
28651 let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
28652 let Statement::CreatePublication(p) = s else {
28653 panic!("expected CreatePublication, got {s:?}")
28654 };
28655 let PublicationScope::TablesInSchema(schema) = p.scope else {
28656 panic!("expected TablesInSchema")
28657 };
28658 assert_eq!(schema, "public");
28659 }
28660
28661 #[test]
28662 fn parser_recognises_for_all_tables_except_list() {
28663 let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
28664 let Statement::CreatePublication(p) = s else {
28665 panic!()
28666 };
28667 let PublicationScope::AllTablesExcept(ts) = p.scope else {
28668 panic!("expected AllTablesExcept")
28669 };
28670 assert_eq!(ts, alloc::vec!["t1", "t2"]);
28671 }
28672
28673 #[test]
28674 fn parser_rejects_for_table_with_empty_list() {
28675 // `FOR TABLE` with nothing after is a parse error.
28676 let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
28677 .expect_err("must error on empty list");
28678 // No specific message asserted — the call falls through to
28679 // expect_ident_like which yields "expected identifier, got …".
28680 assert!(!err.message.is_empty());
28681 }
28682
28683 #[test]
28684 fn parser_recognises_show_publications() {
28685 // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
28686 // bare ident in this position, NOT a reserved keyword.
28687 let s = parse("SHOW PUBLICATIONS");
28688 assert!(matches!(s, Statement::ShowPublications));
28689 }
28690
28691 // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
28692
28693 #[test]
28694 fn parser_recognises_create_subscription_single_publication() {
28695 let s = parse(
28696 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
28697 );
28698 let Statement::CreateSubscription(c) = s else {
28699 panic!("expected CreateSubscription, got {s:?}")
28700 };
28701 assert_eq!(c.name, "sub_a");
28702 assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
28703 assert_eq!(c.publications, alloc::vec!["pub_a"]);
28704 }
28705
28706 #[test]
28707 fn parser_recognises_create_subscription_multi_publication() {
28708 let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
28709 let Statement::CreateSubscription(c) = s else {
28710 panic!()
28711 };
28712 assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
28713 }
28714
28715 #[test]
28716 fn parser_rejects_create_subscription_missing_connection() {
28717 let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
28718 .expect_err("must error on missing CONNECTION");
28719 assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
28720 }
28721
28722 #[test]
28723 fn parser_rejects_create_subscription_missing_publication() {
28724 let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
28725 .expect_err("must error on missing PUBLICATION");
28726 assert_eq!(err.message, "syntax error at end of input");
28727 }
28728
28729 #[test]
28730 fn parser_recognises_drop_subscription() {
28731 let s = parse("DROP SUBSCRIPTION sub_a");
28732 let Statement::DropSubscription { name, .. } = s else {
28733 panic!("expected DropSubscription, got {s:?}")
28734 };
28735 assert_eq!(name, "sub_a");
28736 }
28737
28738 #[test]
28739 fn parser_recognises_show_subscriptions() {
28740 let s = parse("SHOW SUBSCRIPTIONS");
28741 assert!(matches!(s, Statement::ShowSubscriptions));
28742 }
28743
28744 #[test]
28745 fn parser_recognises_wait_for_wal_position_no_timeout() {
28746 let s = parse("WAIT FOR WAL POSITION 12345");
28747 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28748 panic!("expected WaitForWalPosition, got {s:?}")
28749 };
28750 assert_eq!(pos, 12345);
28751 assert!(timeout_ms.is_none());
28752 }
28753
28754 #[test]
28755 fn parser_recognises_wait_for_wal_position_with_timeout() {
28756 let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
28757 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28758 panic!()
28759 };
28760 assert_eq!(pos, 67890);
28761 assert_eq!(timeout_ms, Some(5000));
28762 }
28763
28764 #[test]
28765 fn parser_rejects_wait_with_negative_position() {
28766 // The lexer treats `-` as a token; `expect_u64_literal`
28767 // only sees the Integer that follows, so the negative
28768 // arrives as a unary-minus expression at higher levels.
28769 // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
28770 // parse error one way or another.
28771 let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
28772 assert!(!err.message.is_empty());
28773 }
28774
28775 #[test]
28776 fn parser_recognises_bare_analyze() {
28777 let s = parse("ANALYZE");
28778 assert!(matches!(s, Statement::Analyze(None)));
28779 }
28780
28781 #[test]
28782 fn parser_recognises_analyze_with_table() {
28783 let s = parse("ANALYZE users");
28784 let Statement::Analyze(Some(name)) = s else {
28785 panic!("expected Analyze, got {s:?}")
28786 };
28787 assert_eq!(name, "users");
28788 }
28789
28790 #[test]
28791 fn parser_recognises_analyze_with_quoted_table() {
28792 let s = parse("ANALYZE \"Mixed Case\"");
28793 let Statement::Analyze(Some(name)) = s else {
28794 panic!()
28795 };
28796 assert_eq!(name, "Mixed Case");
28797 }
28798
28799 #[test]
28800 fn parser_rejects_analyze_with_garbage_token() {
28801 let err = parse_statement("ANALYZE 42").expect_err("must error");
28802 assert!(!err.message.is_empty());
28803 }
28804
28805 #[test]
28806 fn analyze_display_roundtrips() {
28807 for sql in ["ANALYZE", "ANALYZE users"] {
28808 let s = parse(sql);
28809 let printed = s.to_string();
28810 let again = parse_statement(&printed)
28811 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28812 assert_eq!(s, again);
28813 }
28814 }
28815
28816 #[test]
28817 fn wait_for_display_roundtrips() {
28818 for sql in [
28819 "WAIT FOR WAL POSITION 12345",
28820 "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
28821 ] {
28822 let s = parse(sql);
28823 let printed = s.to_string();
28824 let again = parse_statement(&printed)
28825 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28826 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28827 }
28828 }
28829
28830 #[test]
28831 fn subscription_ddl_display_roundtrips() {
28832 for sql in [
28833 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
28834 "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
28835 "DROP SUBSCRIPTION sub_a",
28836 "SHOW SUBSCRIPTIONS",
28837 ] {
28838 let s = parse(sql);
28839 let printed = s.to_string();
28840 let again = parse_statement(&printed)
28841 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28842 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28843 }
28844 }
28845
28846 #[test]
28847 fn parser_drop_dispatches_user_vs_publication() {
28848 // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
28849 // tokenises DROP. Both targets must still parse.
28850 let s = parse("DROP USER 'alice'");
28851 let Statement::DropUser { name, .. } = s else {
28852 panic!("expected DropUser, got {s:?}")
28853 };
28854 assert_eq!(name, "alice");
28855 // And DROP PUBLICATION lands the new variant.
28856 let s = parse("DROP PUBLICATION p1");
28857 assert!(matches!(s, Statement::DropPublication { .. }));
28858 }
28859
28860 #[test]
28861 fn publication_ddl_display_roundtrips() {
28862 // Every CREATE PUBLICATION variant must Display → parse →
28863 // same AST. v6.1.3 covers all three scope shapes.
28864 for sql in [
28865 "CREATE PUBLICATION pub_a",
28866 "CREATE PUBLICATION pub_a FOR ALL TABLES",
28867 "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
28868 "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
28869 "DROP PUBLICATION pub_a",
28870 "SHOW PUBLICATIONS",
28871 ] {
28872 let s = parse(sql);
28873 let printed = s.to_string();
28874 let again = parse_statement(&printed)
28875 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28876 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28877 }
28878 }
28879
28880 // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
28881
28882 #[test]
28883 fn create_function_returns_trigger_plpgsql_minimal() {
28884 let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
28885 let s = parse(sql);
28886 let Statement::CreateFunction(f) = s else {
28887 panic!("expected CreateFunction");
28888 };
28889 assert_eq!(f.name, "noop");
28890 assert!(!f.or_replace);
28891 assert!(f.args.is_empty());
28892 assert!(matches!(f.returns, FunctionReturn::Trigger));
28893 assert_eq!(f.language, "plpgsql");
28894 let FunctionBody::PlPgSql(block) = f.body else {
28895 panic!("expected PlPgSql body");
28896 };
28897 assert_eq!(block.statements.len(), 1);
28898 assert!(matches!(
28899 block.statements[0],
28900 PlPgSqlStmt::Return(ReturnTarget::New)
28901 ));
28902 }
28903
28904 #[test]
28905 fn create_function_or_replace_with_assignment() {
28906 // mailrs-shape trigger function: NEW.col := to_tsvector(...);
28907 // RETURN NEW.
28908 let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
28909BEGIN
28910 NEW.search_vector := to_tsvector('english', NEW.subject);
28911 RETURN NEW;
28912END;
28913$$";
28914 let s = parse(sql);
28915 let Statement::CreateFunction(f) = s else {
28916 panic!("expected CreateFunction");
28917 };
28918 assert!(f.or_replace);
28919 let FunctionBody::PlPgSql(block) = &f.body else {
28920 panic!("expected PlPgSql body");
28921 };
28922 assert_eq!(block.statements.len(), 2);
28923 // First statement: NEW.search_vector := to_tsvector(...)
28924 let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
28925 panic!("expected Assign as first stmt");
28926 };
28927 match target {
28928 AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
28929 other => panic!("expected NEW.col, got {other:?}"),
28930 }
28931 // Second statement: RETURN NEW
28932 assert!(matches!(
28933 block.statements[1],
28934 PlPgSqlStmt::Return(ReturnTarget::New)
28935 ));
28936 }
28937
28938 #[test]
28939 fn create_trigger_after_insert_or_update() {
28940 let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
28941 let s = parse(sql);
28942 let Statement::CreateTrigger(t) = s else {
28943 panic!("expected CreateTrigger");
28944 };
28945 assert_eq!(t.name, "tg");
28946 assert_eq!(t.table, "messages");
28947 assert_eq!(t.timing, TriggerTiming::After);
28948 assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
28949 assert_eq!(t.for_each, TriggerForEach::Row);
28950 assert_eq!(t.function, "update_sv");
28951 }
28952
28953 #[test]
28954 fn create_trigger_before_delete_execute_procedure_alias() {
28955 // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
28956 let sql =
28957 "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
28958 let s = parse(sql);
28959 let Statement::CreateTrigger(t) = s else {
28960 panic!("expected CreateTrigger");
28961 };
28962 assert_eq!(t.timing, TriggerTiming::Before);
28963 assert_eq!(t.events, vec![TriggerEvent::Delete]);
28964 }
28965
28966 #[test]
28967 fn drop_trigger_if_exists_round_trips() {
28968 // No parser support for DROP TRIGGER yet — added in v7.12.5
28969 // alongside the broader DROP …{IF EXISTS} cleanup. The
28970 // AST + Display impls are in place so we round-trip via
28971 // construction:
28972 let s = Statement::DropTrigger {
28973 name: "tg".into(),
28974 table: "messages".into(),
28975 if_exists: true,
28976 };
28977 assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
28978 }
28979
28980 #[test]
28981 fn trigger_ddl_display_roundtrips_through_parser() {
28982 // CREATE TRIGGER + its referenced CREATE FUNCTION must
28983 // Display → parse → same AST (modulo PL/pgSQL body
28984 // formatting which is parser-canonicalised).
28985 for sql in [
28986 "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
28987 "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
28988 ] {
28989 let s = parse(sql);
28990 let printed = s.to_string();
28991 let again = parse_statement(&printed)
28992 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28993 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28994 }
28995 }
28996}