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 // v7.39 — this used to parse the clause and throw it
3998 // away ("SPG stores UTF-8 always and orders
3999 // bytewise; accept as a no-op"). That sentence
4000 // stopped being true when collations arrived, and
4001 // once `collation_connection` began driving literal
4002 // comparison, dropping the COLLATE clause became a
4003 // silently wrong answer: `SET NAMES utf8mb4 COLLATE
4004 // utf8mb4_general_ci` reported back
4005 // `utf8mb4_0900_ai_ci` and compared as NO PAD.
4006 //
4007 // The charset name is emitted as `names` and the
4008 // ENGINE expands it, because which collation a
4009 // charset defaults to is MySQL semantics and belongs
4010 // beside the rest of them, not in the parser.
4011 let mut pairs = alloc::vec::Vec::new();
4012 if matches!(
4013 self.peek(),
4014 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4015 ) {
4016 let charset = match self.advance() {
4017 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4018 _ => unreachable!("peeked an ident-or-string"),
4019 };
4020 pairs.push((String::from("names"), crate::ast::SetValue::Ident(charset)));
4021 }
4022 // Optional `COLLATE <name>` — emitted AFTER `names`
4023 // so it overrides the charset's default, which is
4024 // what MySQL does.
4025 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
4026 {
4027 self.advance();
4028 if matches!(
4029 self.peek(),
4030 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4031 ) {
4032 let coll = match self.advance() {
4033 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4034 _ => unreachable!("peeked an ident-or-string"),
4035 };
4036 pairs.push((
4037 String::from("collation_connection"),
4038 crate::ast::SetValue::Ident(coll),
4039 ));
4040 }
4041 }
4042 if pairs.is_empty() {
4043 return Ok(Statement::Empty);
4044 }
4045 return Ok(Statement::SetParameterList(pairs));
4046 }
4047 // v7.37.17 (17.6 sibling) — PG `SET ROLE
4048 // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
4049 // uses this to switch to the object owner before
4050 // recreating tables. SPG has no role system so this
4051 // is a no-op.
4052 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
4053 {
4054 self.advance(); // ROLE
4055 // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
4056 // reset to the login identity; a name / string sets the
4057 // effective role that drives current_user + RLS.
4058 let role = match self.peek().clone() {
4059 Token::Default => {
4060 self.advance();
4061 None
4062 }
4063 Token::Ident(s) | Token::QuotedIdent(s)
4064 if s.eq_ignore_ascii_case("none") =>
4065 {
4066 self.advance();
4067 None
4068 }
4069 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4070 self.advance();
4071 Some(s)
4072 }
4073 _ => None,
4074 };
4075 return Ok(Statement::SetRole(role));
4076 }
4077 // v7.37.17 (17.6 sibling) — PG `SET SESSION
4078 // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
4079 // ISO SQL surface). pg_dump prepends this to fix
4080 // the isolation level for the restore session. SPG
4081 // defaults to READ COMMITTED and doesn't yet honor
4082 // session-set isolation across statements — accept
4083 // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
4084 // per-tx form is handled elsewhere.
4085 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
4086 {
4087 self.advance(); // CHARACTERISTICS
4088 if matches!(self.peek(), Token::As) {
4089 self.advance();
4090 }
4091 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
4092 self.advance();
4093 }
4094 // v7.39 — no longer a no-op. The note above said SPG
4095 // "doesn't yet honor session-set isolation across
4096 // statements"; it does now, through
4097 // `default_transaction_isolation`, and measured on
4098 // PG 18.6 this statement is exactly a way to set it:
4099 //
4100 // SET SESSION CHARACTERISTICS AS TRANSACTION
4101 // ISOLATION LEVEL REPEATABLE READ;
4102 // current_setting('default_transaction_isolation')
4103 // -> repeatable read
4104 //
4105 // pg_dump prepends this to fix the level for a
4106 // restore session, so accepting it and doing nothing
4107 // meant the restore ran at a level nobody chose.
4108 //
4109 // The trailing READ ONLY / [NOT] DEFERRABLE modes are
4110 // still consumed and dropped. `default_transaction_read_only`
4111 // exists in the GUC inventory but nothing enforces it,
4112 // and setting a value no code honours is the very
4113 // defect this version is about — a session told it
4114 // holds a guarantee it does not.
4115 let modes = self.parse_isolation_level_clauses()?;
4116 self.consume_until_statement_boundary();
4117 let mut pairs: alloc::vec::Vec<(
4118 alloc::string::String,
4119 crate::ast::SetValue,
4120 )> = alloc::vec::Vec::new();
4121 if let Some(level) = modes.isolation {
4122 pairs.push((
4123 alloc::string::String::from("default_transaction_isolation"),
4124 crate::ast::SetValue::String(alloc::string::String::from(
4125 level.as_pg_str(),
4126 )),
4127 ));
4128 }
4129 if let Some(ro) = modes.read_only {
4130 pairs.push((
4131 alloc::string::String::from("default_transaction_read_only"),
4132 crate::ast::SetValue::Ident(alloc::string::String::from(if ro {
4133 "on"
4134 } else {
4135 "off"
4136 })),
4137 ));
4138 }
4139 return Ok(if pairs.is_empty() {
4140 Statement::Empty
4141 } else {
4142 Statement::SetParameterList(pairs)
4143 });
4144 }
4145 // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
4146 // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
4147 // pg_dump emits this to control the deferrability of
4148 // FK / UNIQUE constraints across a bulk restore. SPG
4149 // has no deferrable-constraint machinery today; the
4150 // FK checker is strict-immediate. Accept-and-no-op
4151 // for pg_dump round-trip compatibility.
4152 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
4153 {
4154 self.advance(); // CONSTRAINTS
4155 // v7.39 (round 288) — no longer a no-op: the trailing
4156 // DEFERRED / IMMEDIATE sets the transaction's timing.
4157 // v7.39 (round 308, V29) — and the names are kept.
4158 // They used to be skipped over on the way to the
4159 // DEFERRED keyword, so a named form silently behaved
4160 // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
4161 // every deferrable constraint in the transaction.
4162 let mut names: alloc::vec::Vec<alloc::string::String> =
4163 alloc::vec::Vec::new();
4164 if matches!(self.peek(), Token::All) {
4165 self.advance();
4166 } else {
4167 loop {
4168 let mut n = self.expect_ident_like()?;
4169 // A schema-qualified name (`public.fk_a`)
4170 // identifies the same constraint; PG resolves
4171 // it by the trailing segment.
4172 while matches!(self.peek(), Token::Dot) {
4173 self.advance();
4174 n = self.expect_ident_like()?;
4175 }
4176 names.push(n);
4177 if matches!(self.peek(), Token::Comma) {
4178 self.advance();
4179 } else {
4180 break;
4181 }
4182 }
4183 }
4184 let deferred = match self.peek() {
4185 Token::Ident(s) | Token::QuotedIdent(s)
4186 if s.eq_ignore_ascii_case("deferred") =>
4187 {
4188 true
4189 }
4190 Token::Ident(s) | Token::QuotedIdent(s)
4191 if s.eq_ignore_ascii_case("immediate") =>
4192 {
4193 false
4194 }
4195 other => {
4196 return Err(self.err(alloc::format!(
4197 "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
4198 )));
4199 }
4200 };
4201 self.advance();
4202 return Ok(Statement::SetConstraints { names, deferred });
4203 }
4204 // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
4205 // { DEFAULT | '<role>' | <ident> }` (mailrs
4206 // round-10 A.1). pg_dump preamble emits the
4207 // `DEFAULT` form to reset session authorization.
4208 //
4209 // v7.39 (round 697) — this said "SPG has no role system so
4210 // this is a strict no-op". SPG has had one since round 58;
4211 // the comment outlived it, and with it the reason a name
4212 // that is not a role was accepted here. It still switches
4213 // no authorization — what it does now is refuse a role
4214 // that does not exist, as PG18 does. PG also accepts `RESET SESSION
4215 // AUTHORIZATION` (handled by the RESET parser
4216 // elsewhere). Reference:
4217 // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
4218 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
4219 {
4220 self.advance(); // AUTHORIZATION
4221 match self.peek().clone() {
4222 Token::Default => {
4223 self.advance();
4224 }
4225 Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4226 self.advance();
4227 return Ok(Statement::ValidateOnly {
4228 kind: crate::ast::ValidateOnlyKind::RoleName,
4229 names: alloc::vec![r],
4230 });
4231 }
4232 other => {
4233 return Err(self.err(alloc::format!(
4234 "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4235 )));
4236 }
4237 }
4238 return Ok(Statement::Empty);
4239 }
4240 // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4241 // ISOLATION LEVEL { READ COMMITTED | READ
4242 // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4243 // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4244 // PG-standard surface. v7.37.8 accepts the syntax
4245 // and tracks the selected level on
4246 // `Engine::current_isolation_level()`; the actual
4247 // MVCC / SSI semantics implementation lands in
4248 // the 轴 4 isolation framework (separate train).
4249 // PG itself maps READ UNCOMMITTED to READ COMMITTED
4250 // internally; SPG behaves the same (effectively
4251 // READ COMMITTED at every level today).
4252 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4253 {
4254 self.advance(); // TRANSACTION
4255 let modes = self.parse_isolation_level_clauses()?;
4256 return Ok(Statement::SetTransaction { modes });
4257 }
4258 // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4259 // alias — same accept-as-no-op as SET NAMES.
4260 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4261 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4262 {
4263 self.advance(); // CHARACTER
4264 self.advance(); // SET
4265 if matches!(
4266 self.peek(),
4267 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4268 ) {
4269 self.advance();
4270 }
4271 return Ok(Statement::Empty);
4272 }
4273 // v7.39 (GUC) — PG spells the timezone GUC as two
4274 // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4275 // where <value> is a string/ident or the LOCAL /
4276 // DEFAULT keyword (both mean "back to the default").
4277 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4278 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4279 {
4280 self.advance(); // TIME
4281 self.advance(); // ZONE
4282 let value = match self.peek().clone() {
4283 Token::Ident(s)
4284 if s.eq_ignore_ascii_case("local")
4285 || s.eq_ignore_ascii_case("default") =>
4286 {
4287 self.advance();
4288 crate::ast::SetValue::Default
4289 }
4290 Token::Default => {
4291 self.advance();
4292 crate::ast::SetValue::Default
4293 }
4294 _ => self.parse_set_value()?,
4295 };
4296 return Ok(Statement::SetParameter {
4297 name: "timezone".into(),
4298 value,
4299 local: set_local,
4300 });
4301 }
4302 // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4303 // MySQL USER-variable assignment: its own per-session
4304 // namespace, an arbitrary expression on the right, and `:=`
4305 // as a second spelling of `=`. It used to fall into the
4306 // session-PARAMETER list below, whose values are literals and
4307 // whose store nothing reads back under a `@` name — so the
4308 // assignment reported success and vanished.
4309 //
4310 // A `@@`-prefixed LHS is a real engine setting and keeps the
4311 // old path.
4312 if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4313 return self.parse_set_user_vars();
4314 }
4315 // v7.14.0 — multi-assignment form
4316 // `SET a = 1, b = 2, …`. Single-assignment is the
4317 // 1-element case. Each LHS may be a regular ident
4318 // or a SessionVar (`@VAR` / `@@VAR`).
4319 let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4320 loop {
4321 let lhs = match self.peek().clone() {
4322 Token::SessionVar(s) => {
4323 self.advance();
4324 s
4325 }
4326 Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4327 other => {
4328 return Err(self.err(format!(
4329 "expected parameter name after SET, got {other:?}"
4330 )));
4331 }
4332 };
4333 // Accept either `=` or the bare `TO` keyword.
4334 match self.peek() {
4335 Token::Eq => {
4336 self.advance();
4337 }
4338 Token::To => {
4339 self.advance();
4340 }
4341 other => {
4342 return Err(self.err(format!(
4343 "expected `=` or TO after SET {lhs}, got {other:?}"
4344 )));
4345 }
4346 }
4347 let mut value = self.parse_set_value()?;
4348 // v7.39 (GUC) — disambiguate the comma: `, name =` /
4349 // `, name TO` continues a MySQL-style multi-assign,
4350 // anything else is a PG list VALUE
4351 // (`SET search_path = myschema, public`) folded into
4352 // one comma-joined string.
4353 while matches!(self.peek(), Token::Comma) {
4354 let is_assign = matches!(
4355 self.tokens.get(self.pos + 1),
4356 Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4357 ) && matches!(
4358 self.tokens.get(self.pos + 2),
4359 Some(Token::Eq | Token::To)
4360 );
4361 if is_assign {
4362 break;
4363 }
4364 self.advance(); // comma
4365 let next = self.parse_set_value()?;
4366 let joined = alloc::format!(
4367 "{}, {}",
4368 set_value_text(&value),
4369 set_value_text(&next)
4370 );
4371 value = crate::ast::SetValue::String(joined);
4372 }
4373 pairs.push((lhs, value));
4374 if matches!(self.peek(), Token::Comma) {
4375 self.advance();
4376 continue;
4377 }
4378 break;
4379 }
4380 if pairs.len() == 1 {
4381 let (name, value) = pairs.into_iter().next().unwrap();
4382 Ok(Statement::SetParameter {
4383 name,
4384 value,
4385 local: set_local,
4386 })
4387 } else {
4388 Ok(Statement::SetParameterList(pairs))
4389 }
4390 }
4391 // v7.12.1 — `RESET <name>` / `RESET ALL`.
4392 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4393 self.advance();
4394 match self.peek().clone() {
4395 Token::All => {
4396 self.advance();
4397 Ok(Statement::ResetParameter(None))
4398 }
4399 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4400 self.advance();
4401 Ok(Statement::ResetParameter(None))
4402 }
4403 // v7.39 (RLS) — `RESET ROLE` clears the session role.
4404 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4405 self.advance();
4406 Ok(Statement::SetRole(None))
4407 }
4408 // 7.38.1 S5.2 — `RESET SESSION AUTHORIZATION`
4409 // (pg_dump's return from the owner switch).
4410 Token::Ident(s) | Token::QuotedIdent(s)
4411 if s.eq_ignore_ascii_case("session")
4412 && matches!(
4413 self.tokens.get(self.pos + 1),
4414 Some(Token::Ident(a) | Token::QuotedIdent(a))
4415 if a.eq_ignore_ascii_case("authorization")
4416 ) =>
4417 {
4418 self.advance(); // SESSION
4419 self.advance(); // AUTHORIZATION
4420 Ok(Statement::SetRole(None))
4421 }
4422 _ => {
4423 let name = self.parse_set_param_name()?;
4424 Ok(Statement::ResetParameter(Some(name)))
4425 }
4426 }
4427 }
4428 // v7.39 (round 218) — server-side cursors.
4429 Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4430 Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4431 Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4432 Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4433 self.advance();
4434 match self.peek().clone() {
4435 Token::All => {
4436 self.advance();
4437 Ok(Statement::CloseCursor { name: None })
4438 }
4439 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4440 self.advance();
4441 Ok(Statement::CloseCursor { name: None })
4442 }
4443 Token::Ident(n) | Token::QuotedIdent(n) => {
4444 self.advance();
4445 Ok(Statement::CloseCursor { name: Some(n) })
4446 }
4447 other => Err(self.err(format!(
4448 "expected cursor name or ALL after CLOSE, got {other:?}"
4449 ))),
4450 }
4451 }
4452 other => Err(self.err(format!(
4453 "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4454 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4455 ))),
4456 }
4457 }
4458
4459 /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4460 /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4461 /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4462 /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4463 fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4464 self.advance(); // DECLARE
4465 let name = match self.advance() {
4466 Token::Ident(n) | Token::QuotedIdent(n) => n,
4467 other => {
4468 return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4469 }
4470 };
4471 let mut scroll: Option<bool> = None;
4472 loop {
4473 match self.peek() {
4474 Token::Ident(s)
4475 if s.eq_ignore_ascii_case("binary")
4476 || s.eq_ignore_ascii_case("insensitive")
4477 || s.eq_ignore_ascii_case("asensitive") =>
4478 {
4479 self.advance();
4480 }
4481 Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4482 self.advance();
4483 scroll = Some(true);
4484 }
4485 Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4486 {
4487 self.advance(); // NO
4488 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4489 return Err(self.err(format!(
4490 "expected SCROLL after NO in DECLARE, got {:?}",
4491 self.peek()
4492 )));
4493 }
4494 self.advance();
4495 scroll = Some(false);
4496 }
4497 _ => break,
4498 }
4499 }
4500 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4501 return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4502 }
4503 self.advance();
4504 let mut hold = false;
4505 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4506 self.advance();
4507 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4508 return Err(self.err(format!(
4509 "expected HOLD after WITH in DECLARE, got {:?}",
4510 self.peek()
4511 )));
4512 }
4513 self.advance();
4514 hold = true;
4515 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4516 self.advance();
4517 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4518 return Err(self.err(format!(
4519 "expected HOLD after WITHOUT in DECLARE, got {:?}",
4520 self.peek()
4521 )));
4522 }
4523 self.advance();
4524 }
4525 if !matches!(self.peek(), Token::For) {
4526 return Err(self.err(format!(
4527 "expected FOR before the cursor query, got {:?}",
4528 self.peek()
4529 )));
4530 }
4531 self.advance();
4532 let query = self.parse_one_statement()?;
4533 Ok(Statement::DeclareCursor {
4534 name,
4535 scroll,
4536 hold,
4537 query: alloc::boxed::Box::new(query),
4538 })
4539 }
4540
4541 /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4542 /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4543 /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4544 fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4545 use crate::ast::CursorDirection as D;
4546 self.advance(); // FETCH / MOVE
4547 let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4548 let neg = if matches!(this.peek(), Token::Minus) {
4549 this.advance();
4550 true
4551 } else {
4552 false
4553 };
4554 match this.advance() {
4555 Token::Integer(v) => Ok(if neg { -v } else { v }),
4556 other => Err(this.err(format!("expected count, got {other:?}"))),
4557 }
4558 };
4559 let direction = match self.peek().clone() {
4560 Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4561 self.advance();
4562 D::Next
4563 }
4564 Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4565 self.advance();
4566 D::Prior
4567 }
4568 Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4569 self.advance();
4570 D::First
4571 }
4572 Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4573 self.advance();
4574 D::Last
4575 }
4576 Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4577 self.advance();
4578 D::Absolute(signed_count(self)?)
4579 }
4580 Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4581 self.advance();
4582 D::Relative(signed_count(self)?)
4583 }
4584 Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4585 self.advance();
4586 match self.peek().clone() {
4587 Token::All => {
4588 self.advance();
4589 D::All
4590 }
4591 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4592 self.advance();
4593 D::All
4594 }
4595 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4596 _ => D::Next, // bare FORWARD = FORWARD 1
4597 }
4598 }
4599 Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4600 self.advance();
4601 match self.peek().clone() {
4602 Token::All => {
4603 self.advance();
4604 D::BackwardAll
4605 }
4606 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4607 self.advance();
4608 D::BackwardAll
4609 }
4610 Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4611 _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4612 }
4613 }
4614 Token::All => {
4615 self.advance();
4616 D::All
4617 }
4618 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4619 self.advance();
4620 D::All
4621 }
4622 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4623 // Bare `FETCH <name>` — direction defaults to NEXT.
4624 _ => D::Next,
4625 };
4626 // Optional FROM / IN.
4627 if matches!(self.peek(), Token::From)
4628 || matches!(self.peek(), Token::In)
4629 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4630 {
4631 self.advance();
4632 }
4633 let name = match self.advance() {
4634 Token::Ident(n) | Token::QuotedIdent(n) => n,
4635 other => {
4636 return Err(self.err(format!("expected cursor name, got {other:?}")));
4637 }
4638 };
4639 Ok(if is_move {
4640 Statement::MoveCursor { name, direction }
4641 } else {
4642 Statement::FetchCursor { name, direction }
4643 })
4644 }
4645
4646 /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4647 /// [(kind, …)] ON <col>, … FROM <table>`.
4648 fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4649 self.advance(); // STATISTICS
4650 // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4651 let mut if_not_exists = false;
4652 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4653 && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4654 {
4655 self.advance();
4656 self.advance();
4657 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4658 self.advance();
4659 if_not_exists = true;
4660 }
4661 }
4662 let name = self.expect_ident_like()?;
4663 let mut kinds = Vec::new();
4664 if matches!(self.peek(), Token::LParen) {
4665 self.advance();
4666 loop {
4667 let k = self.expect_ident_like()?;
4668 // PG stores the single letters; accept the spelled-out
4669 // names the SQL uses and record what PG records.
4670 kinds.push(match k.to_ascii_lowercase().as_str() {
4671 "ndistinct" => String::from("d"),
4672 "dependencies" => String::from("f"),
4673 "mcv" => String::from("m"),
4674 other => {
4675 return Err(
4676 self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4677 );
4678 }
4679 });
4680 match self.advance() {
4681 Token::Comma => {}
4682 Token::RParen => break,
4683 other => {
4684 return Err(self.err(alloc::format!(
4685 "expected ',' or ')' in statistics kind list, got {other:?}"
4686 )));
4687 }
4688 }
4689 }
4690 }
4691 if !matches!(self.peek(), Token::On) {
4692 return Err(self.err(alloc::format!(
4693 "expected ON in CREATE STATISTICS, got {:?}",
4694 self.peek()
4695 )));
4696 }
4697 self.advance();
4698 let mut columns = Vec::new();
4699 loop {
4700 columns.push(self.expect_ident_like()?);
4701 if matches!(self.peek(), Token::Comma) {
4702 self.advance();
4703 } else {
4704 break;
4705 }
4706 }
4707 if !matches!(self.peek(), Token::From) {
4708 return Err(self.err(alloc::format!(
4709 "expected FROM in CREATE STATISTICS, got {:?}",
4710 self.peek()
4711 )));
4712 }
4713 self.advance();
4714 let table = self.expect_ident_like()?;
4715 Ok(Statement::CreateStatistics {
4716 name,
4717 if_not_exists,
4718 kinds,
4719 columns,
4720 table,
4721 })
4722 }
4723
4724 /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4725 /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4726 /// entered with the `TABLE` keyword still unconsumed. Extracted so
4727 /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4728 /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4729 /// forward call.
4730 fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4731 self.advance(); // TABLE
4732 let if_exists = self.consume_if_exists();
4733 let mut names: Vec<String> = Vec::new();
4734 loop {
4735 names.push(self.expect_ident_like()?);
4736 if matches!(self.peek(), Token::Comma) {
4737 self.advance();
4738 continue;
4739 }
4740 break;
4741 }
4742 if matches!(
4743 self.peek(),
4744 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4745 || s.eq_ignore_ascii_case("restrict")
4746 ) {
4747 self.advance();
4748 }
4749 Ok(Statement::DropTable { names, if_exists })
4750 }
4751
4752 fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4753 self.advance(); // STATISTICS
4754 let mut if_exists = false;
4755 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4756 && matches!(self.tokens.get(self.pos + 1),
4757 Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4758 {
4759 self.advance();
4760 self.advance();
4761 if_exists = true;
4762 }
4763 let name = self.expect_ident_like()?;
4764 Ok(Statement::DropStatistics { name, if_exists })
4765 }
4766
4767 fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4768 debug_assert!(matches!(self.peek(), Token::Create));
4769 self.advance();
4770 match self.peek() {
4771 Token::Table => self.parse_create_table_stmt_after_create(),
4772 Token::Index => self.parse_create_index_stmt_after_create(false),
4773 // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4774 // object now. It used to be consumed by the CREATE-noise
4775 // arm, so a pg_dump that declares extended statistics
4776 // restored silently without them and reflection showed
4777 // nothing.
4778 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4779 self.parse_create_statistics_after_create()
4780 }
4781 // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4782 // The `UNIQUE` modifier turns a partial index into a
4783 // partial-uniqueness invariant (only rows matching the
4784 // WHERE predicate are checked for duplicates). mailrs
4785 // K1 (3 hits: email_templates default, calendar_events
4786 // master, calendar_events instance).
4787 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4788 self.advance();
4789 if !matches!(self.peek(), Token::Index) {
4790 return Err(self.err(alloc::format!(
4791 "expected INDEX after CREATE UNIQUE, got {:?}",
4792 self.peek()
4793 )));
4794 }
4795 self.parse_create_index_stmt_after_create(true)
4796 }
4797 Token::Publication => {
4798 self.advance();
4799 self.parse_create_publication_after_keyword()
4800 }
4801 Token::Subscription => {
4802 self.advance();
4803 self.parse_create_subscription_after_keyword()
4804 }
4805 // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4806 // USER isn't a reserved keyword — we look for the bare
4807 // identifier so the lexer doesn't have to grow a token.
4808 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4809 self.advance();
4810 self.parse_create_user_after_keyword(true)
4811 }
4812 // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4813 // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4814 // the default of the LOGIN attribute.
4815 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4816 self.advance();
4817 self.parse_create_user_after_keyword(false)
4818 }
4819 // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4820 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4821 self.advance();
4822 self.parse_create_policy_after_keyword()
4823 }
4824 // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4825 // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4826 // no-op. mailrs follow-up F3.
4827 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4828 self.advance();
4829 self.parse_create_extension_after_keyword()
4830 }
4831 // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4832 // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4833 // optional; absorb it here and forward to the
4834 // per-kind parsers with the flag. OR is a reserved
4835 // keyword token.
4836 Token::Or => {
4837 self.advance();
4838 let next = self.peek();
4839 let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4840 return Err(self.err(alloc::format!(
4841 "expected REPLACE after CREATE OR, got {next:?}"
4842 )));
4843 };
4844 if !s2.eq_ignore_ascii_case("replace") {
4845 return Err(self.err(alloc::format!(
4846 "expected REPLACE after CREATE OR, got {s2:?}"
4847 )));
4848 }
4849 self.advance();
4850 self.parse_create_function_or_trigger_after_or_replace(true)
4851 }
4852 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4853 self.advance();
4854 self.parse_create_function_after_keyword(false)
4855 }
4856 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4857 self.advance();
4858 self.parse_create_trigger_after_keyword(false)
4859 }
4860 // v7.39 (round 139) — CREATE RULE …
4861 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4862 self.advance();
4863 self.parse_create_rule_after_keyword(false)
4864 }
4865 // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4866 // trigger is a row-level AFTER trigger that additionally carries
4867 // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4868 // path already tolerates and skips those clauses, so consuming the
4869 // CONSTRAINT keyword and reusing it makes the statement parse and the
4870 // trigger fire. (The deferral timing itself is not yet honoured —
4871 // SPG fires it as a plain AFTER trigger, which is correct behaviour
4872 // for every non-deferred use.)
4873 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4874 self.advance();
4875 if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4876 if t.eq_ignore_ascii_case("trigger"))
4877 {
4878 return Err(self.err(alloc::format!(
4879 "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4880 self.peek()
4881 )));
4882 }
4883 self.advance();
4884 self.parse_create_trigger_after_keyword(false)
4885 }
4886 // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4887 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4888 self.advance();
4889 self.parse_create_sequence_after_keyword(false)
4890 }
4891 // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4892 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4893 self.advance();
4894 self.parse_create_view_after_keyword(false, false, false)
4895 }
4896 // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4897 // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4898 // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4899 // appear (in any order) between `CREATE` and `VIEW` in
4900 // every mysqldump-emitted view. Pre-2.6 the parser
4901 // rejected the prefix and the customer's whole view
4902 // backup failed on the first view. The hints are pure
4903 // planner / permission metadata; SPG's view-rewrite
4904 // path is semantically equivalent for all three
4905 // algorithms in v7.17 (TEMPTABLE differs only in
4906 // perf for huge views — out of v7.17 scope), and
4907 // DEFINER / SQL SECURITY are pure single-user
4908 // permissioning that SPG ignores by design.
4909 Token::Ident(s) | Token::QuotedIdent(s)
4910 if s.eq_ignore_ascii_case("algorithm")
4911 || s.eq_ignore_ascii_case("definer")
4912 || s.eq_ignore_ascii_case("sql") =>
4913 {
4914 self.consume_mysql_view_prefix()?;
4915 // After absorbing ALGORITHM / DEFINER / SQL SECURITY
4916 // (in any order, in any combination), the next
4917 // keyword must be VIEW. mysqldump never emits these
4918 // prefixes on non-view statements.
4919 let next = self.peek().clone();
4920 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
4921 if s2.eq_ignore_ascii_case("view"))
4922 {
4923 self.advance();
4924 self.parse_create_view_after_keyword(false, false, false)
4925 } else {
4926 Err(self.err(alloc::format!(
4927 "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
4928 )))
4929 }
4930 }
4931 // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
4932 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
4933 self.advance();
4934 self.parse_create_type_after_keyword()
4935 }
4936 // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
4937 // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
4938 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
4939 self.advance();
4940 self.parse_create_domain_after_keyword()
4941 }
4942 // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
4943 // name [AUTHORIZATION user]. Real catalog registry
4944 // (was silent-no-op'd pre-v7.17).
4945 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
4946 self.advance();
4947 let if_not_exists = self.parse_if_not_exists();
4948 let name = self.expect_ident_like()?;
4949 // Optional `AUTHORIZATION <user>` trailer — accepted,
4950 // ignored (single-user catalog).
4951 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4952 if s.eq_ignore_ascii_case("authorization"))
4953 {
4954 self.advance();
4955 let _ = self.expect_ident_like()?;
4956 }
4957 Ok(Statement::CreateSchema { name, if_not_exists })
4958 }
4959 // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
4960 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
4961 self.advance();
4962 let next = self.peek().clone();
4963 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4964 {
4965 self.advance();
4966 self.parse_create_materialized_view_after_keyword()
4967 } else {
4968 Err(self.err(alloc::format!(
4969 "expected VIEW after CREATE MATERIALIZED, got {next:?}"
4970 )))
4971 }
4972 }
4973 // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
4974 // no-op below), an UNLOGGED table is a real, fully-usable table in
4975 // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
4976 // durability optimisation is a follow-up), so a dump / app that
4977 // declares UNLOGGED tables works instead of failing to parse.
4978 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
4979 self.advance(); // UNLOGGED
4980 if matches!(self.peek(), Token::Table) {
4981 self.parse_create_table_stmt_after_create()
4982 } else {
4983 Err(self.err(format!(
4984 "expected TABLE after CREATE UNLOGGED, got {:?}",
4985 self.peek()
4986 )))
4987 }
4988 }
4989 Token::Ident(s) | Token::QuotedIdent(s)
4990 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
4991 {
4992 self.advance();
4993 // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
4994 let next = self.peek().clone();
4995 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
4996 {
4997 self.advance();
4998 self.parse_create_sequence_after_keyword(true)
4999 } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5000 {
5001 self.advance();
5002 self.parse_create_view_after_keyword(false, false, true)
5003 } else {
5004 // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
5005 // consumed and answered OK while creating nothing, so
5006 // every statement that touched the table afterwards failed
5007 // with "table not found" — the DDL itself lied. It is a
5008 // real CREATE TABLE now, marked temporary so the executor
5009 // puts it in the session's own namespace. An optional
5010 // TABLE keyword may or may not be present (`CREATE TEMP t`
5011 // is not legal, but the keyword is consumed by the
5012 // CREATE TABLE parser itself).
5013 let stmt = self.parse_create_table_stmt_after_create()?;
5014 match stmt {
5015 Statement::CreateTable(mut c) => {
5016 c.temporary = true;
5017 Ok(Statement::CreateTable(c))
5018 }
5019 // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
5020 // CTAS node, which needs the same session namespace.
5021 Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
5022 m.temporary = true;
5023 Ok(Statement::CreateMaterializedView(m))
5024 }
5025 other => Ok(other),
5026 }
5027 }
5028 }
5029 // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
5030 // BEGIN <body> END`. The body may reference `@var`
5031 // session variables, SET statements, internal `;`
5032 // terminators, etc. SPG has no procedure runtime, so
5033 // consume the whole `CREATE PROCEDURE … END` block as
5034 // a no-op so mysqldump scripts that include stored
5035 // routines load through. The matching-END consumer
5036 // tracks BEGIN/END nesting depth to handle nested
5037 // BEGIN blocks correctly.
5038 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
5039 self.consume_mysql_routine_body();
5040 Ok(Statement::Empty)
5041 }
5042 // v7.14.0 — pg_dump / mysqldump emit
5043 // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
5044 // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
5045 // SPG is single-schema / single-database; these have
5046 // no behavioural effect, so consume + return Empty.
5047 // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
5048 // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
5049 // moved up to real parser branches. DATABASE / ROLE /
5050 // POLICY / OPERATOR stay no-op forever
5051 // (single-database, hardcoded roles).
5052 Token::Ident(s) | Token::QuotedIdent(s)
5053 if matches!(
5054 s.to_ascii_lowercase().as_str(),
5055 "database"
5056 | "role"
5057 | "operator"
5058 | "cast"
5059 | "aggregate"
5060 | "language"
5061 | "collation"
5062 | "conversion"
5063 // v7.17.0 Phase 8 (audit N6) — rarely-
5064 // emitted pg_dump shapes that should
5065 // load through without a parser error.
5066 // SPG has no planner statistics catalog,
5067 // no event-trigger hooks, no foreign-
5068 // data-wrapper infrastructure; consume
5069 // + return Empty.
5070 | "statistics"
5071 | "event"
5072 // v7.37.17 (17.6 siblings) — additional CREATE
5073 // targets pg_dump / operator install scripts
5074 // may emit that SPG has no matching machinery
5075 // for. Consume + Empty-return.
5076 | "text"
5077 | "tablespace"
5078 | "access"
5079 | "large"
5080 ) =>
5081 {
5082 // DATABASE is the one member of this list PG refuses
5083 // inside a transaction block; the rest (ROLE, CAST,
5084 // TABLESPACE, …) it runs there quite happily, so only
5085 // this one is named. Still a no-op otherwise — SPG is
5086 // single-database.
5087 let is_database = s.eq_ignore_ascii_case("database");
5088 // The name is the first token after DATABASE, past an
5089 // `IF NOT EXISTS`.
5090 let name = if is_database {
5091 self.scan_database_name()
5092 } else {
5093 None
5094 };
5095 let collation = if is_database {
5096 self.scan_database_collation_until_boundary()
5097 } else {
5098 self.consume_until_statement_boundary();
5099 None
5100 };
5101 if is_database {
5102 return Ok(Statement::NoOpPreventedInTransaction {
5103 what: String::from("CREATE DATABASE"),
5104 collation,
5105 name,
5106 });
5107 }
5108 Ok(Statement::Empty)
5109 }
5110 // v7.39 (round 706) — the foreign-data family leaves the silent
5111 // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
5112 // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
5113 // FDW machinery), but the ENGINE now warns, so a restore log
5114 // says what will not function instead of reporting success.
5115 Token::Ident(s) | Token::QuotedIdent(s)
5116 if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
5117 {
5118 self.consume_until_statement_boundary();
5119 Ok(Statement::ValidateOnly {
5120 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
5121 names: Vec::new(),
5122 })
5123 }
5124 other => Err(self.err(format!(
5125 "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
5126 ))),
5127 }
5128 }
5129
5130 /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
5131 /// keyword decides whether we parse a function or trigger
5132 /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
5133 /// PROCEDURE) — those land in later releases.
5134 fn parse_create_function_or_trigger_after_or_replace(
5135 &mut self,
5136 or_replace: bool,
5137 ) -> Result<Statement, ParseError> {
5138 let tok = self.peek();
5139 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5140 return Err(self.err(alloc::format!(
5141 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
5142 )));
5143 };
5144 if s.eq_ignore_ascii_case("function") {
5145 self.advance();
5146 self.parse_create_function_after_keyword(or_replace)
5147 } else if s.eq_ignore_ascii_case("trigger") {
5148 self.advance();
5149 self.parse_create_trigger_after_keyword(or_replace)
5150 } else if s.eq_ignore_ascii_case("rule") {
5151 // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
5152 self.advance();
5153 self.parse_create_rule_after_keyword(or_replace)
5154 } else if s.eq_ignore_ascii_case("view") {
5155 // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
5156 self.advance();
5157 self.parse_create_view_after_keyword(or_replace, false, false)
5158 } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
5159 // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
5160 self.advance();
5161 let nxt = self.peek().clone();
5162 if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
5163 {
5164 self.advance();
5165 self.parse_create_view_after_keyword(or_replace, false, true)
5166 } else {
5167 Err(self.err(alloc::format!(
5168 "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
5169 )))
5170 }
5171 } else {
5172 Err(self.err(alloc::format!(
5173 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
5174 )))
5175 }
5176 }
5177
5178 /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
5179 /// SPG doesn't have a registry; pgvector / similar are
5180 /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
5181 /// the syntax lets dual-target schemas keep the line.
5182 fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
5183 // Optional `IF NOT EXISTS`.
5184 self.consume_if_not_exists();
5185 let name = self.expect_ident_like()?;
5186 // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
5187 // CASCADE / FROM '<v>' clauses; we don't model them.
5188 loop {
5189 match self.peek() {
5190 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
5191 self.advance();
5192 continue;
5193 }
5194 Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
5195 self.advance();
5196 let _ = self.expect_ident_like()?;
5197 continue;
5198 }
5199 Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
5200 self.advance();
5201 // String or ident literal.
5202 let _ = self.advance();
5203 continue;
5204 }
5205 Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
5206 self.advance();
5207 let _ = self.advance();
5208 continue;
5209 }
5210 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
5211 self.advance();
5212 continue;
5213 }
5214 _ => break,
5215 }
5216 }
5217 // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
5218 // nosuch` reported success and `pg_extension` then did not list it,
5219 // which is the accept-and-do-nothing shape F31 exists to find.
5220 Ok(Statement::ValidateOnly {
5221 kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
5222 names: alloc::vec![name],
5223 })
5224 }
5225
5226 /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
5227 /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
5228 /// already been consumed by the caller. Grammar accepted:
5229 ///
5230 /// name `(` arg-list `)`
5231 /// `RETURNS` return-type
5232 /// [ `LANGUAGE` ident ]
5233 /// `AS` $$ body $$
5234 /// [ `LANGUAGE` ident ]
5235 ///
5236 /// Either `LANGUAGE` position is allowed; PG accepts both.
5237 fn parse_create_function_after_keyword(
5238 &mut self,
5239 or_replace: bool,
5240 ) -> Result<Statement, ParseError> {
5241 let name = self.expect_ident_like()?;
5242 // Argument list. v7.12.4 commonly sees the empty `()`
5243 // (trigger functions); typed args parse and round-trip
5244 // but the executor only invokes nullary functions.
5245 if !matches!(self.peek(), Token::LParen) {
5246 return Err(self.err(alloc::format!(
5247 "expected '(' after function name {name:?}, got {:?}",
5248 self.peek()
5249 )));
5250 }
5251 self.advance();
5252 let args = self.parse_function_arg_list()?;
5253 // RETURNS clause.
5254 let tok = self.peek();
5255 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5256 return Err(self.err(alloc::format!(
5257 "expected RETURNS after function arg list, got {tok:?}"
5258 )));
5259 };
5260 if !s.eq_ignore_ascii_case("returns") {
5261 return Err(self.err(alloc::format!(
5262 "expected RETURNS after function arg list, got {s:?}"
5263 )));
5264 }
5265 self.advance();
5266 let returns = self.parse_function_return()?;
5267 // Optional LANGUAGE clause (PG also accepts after AS — we'll
5268 // re-check after the body too).
5269 let mut language: Option<String> = self.parse_optional_language()?;
5270 // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5271 // either side of the body and in any order, interleaved with
5272 // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5273 // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5274 // PG's own pg_dump output did not restore.
5275 let mut attrs = FunctionAttrs::default();
5276 loop {
5277 let before = self.pos;
5278 self.parse_function_attrs_into(&mut attrs)?;
5279 if language.is_none() {
5280 language = self.parse_optional_language()?;
5281 }
5282 if self.pos == before {
5283 break;
5284 }
5285 }
5286 // `AS` followed by a $$-quoted body (lexer already
5287 // collapses both `$$…$$` and `$tag$…$tag$` to a single
5288 // Token::String). AS is a reserved keyword (Token::As).
5289 if !matches!(self.peek(), Token::As) {
5290 return Err(self.err(alloc::format!(
5291 "expected AS before function body, got {:?}",
5292 self.peek()
5293 )));
5294 }
5295 self.advance();
5296 let body_text = match self.peek() {
5297 Token::String(s) => {
5298 let body = s.clone();
5299 self.advance();
5300 body
5301 }
5302 other => {
5303 return Err(self.err(alloc::format!(
5304 "expected $$-quoted function body after AS, got {other:?}"
5305 )));
5306 }
5307 };
5308 // Trailing clauses — PG's other accepted position for both the
5309 // LANGUAGE and the attributes.
5310 loop {
5311 let before = self.pos;
5312 self.parse_function_attrs_into(&mut attrs)?;
5313 if language.is_none() {
5314 language = self.parse_optional_language()?;
5315 }
5316 if self.pos == before {
5317 break;
5318 }
5319 }
5320 let language = language.unwrap_or_else(|| String::from("sql"));
5321 // PL/pgSQL bodies get structure-parsed. Other languages
5322 // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5323 // recognise) round-trip as Raw text — the executor errors
5324 // when invoked with a clear unsupported message.
5325 let body = if language.eq_ignore_ascii_case("plpgsql") {
5326 match parse_plpgsql_body(&body_text) {
5327 Ok(block) => FunctionBody::PlPgSql(block),
5328 // Best-effort: if the body parser doesn't yet
5329 // support a construct used inside, fall back to
5330 // raw — keeps `CREATE FUNCTION` itself working
5331 // (catalogue accepts), executor errors on
5332 // invocation only.
5333 Err(_) => FunctionBody::Raw(body_text),
5334 }
5335 } else {
5336 FunctionBody::Raw(body_text)
5337 };
5338 Ok(Statement::CreateFunction(CreateFunctionStatement {
5339 name,
5340 or_replace,
5341 args,
5342 returns,
5343 language,
5344 body,
5345 attrs,
5346 }))
5347 }
5348
5349 /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5350 /// attribute clauses into `attrs`, stopping at the first token that
5351 /// is not one. Measured against PG 18.4, which accepts them in any
5352 /// order and on either side of the body.
5353 fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5354 loop {
5355 let word = match self.peek() {
5356 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5357 // NOT LEAKPROOF — NOT is a reserved keyword token.
5358 Token::Not
5359 if matches!(
5360 self.tokens.get(self.pos + 1),
5361 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5362 ) =>
5363 {
5364 self.advance();
5365 self.advance();
5366 attrs.leakproof = false;
5367 continue;
5368 }
5369 _ => return Ok(()),
5370 };
5371 match word.as_str() {
5372 "immutable" => {
5373 self.advance();
5374 attrs.volatility = FunctionVolatility::Immutable;
5375 }
5376 "stable" => {
5377 self.advance();
5378 attrs.volatility = FunctionVolatility::Stable;
5379 }
5380 "volatile" => {
5381 self.advance();
5382 attrs.volatility = FunctionVolatility::Volatile;
5383 }
5384 "strict" => {
5385 self.advance();
5386 attrs.strict = true;
5387 }
5388 "leakproof" => {
5389 self.advance();
5390 attrs.leakproof = true;
5391 }
5392 // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5393 // spelled-out forms of STRICT and its opposite.
5394 "returns" | "called" => {
5395 let strict = word == "returns";
5396 let mut probe = self.pos + 1;
5397 if strict {
5398 // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5399 // is not ours.
5400 match self.tokens.get(probe) {
5401 Some(Token::Null) => probe += 1,
5402 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5403 _ => return Ok(()),
5404 }
5405 }
5406 let ok = matches!(self.tokens.get(probe), Some(Token::On))
5407 || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5408 if !ok {
5409 return Ok(());
5410 }
5411 probe += 1;
5412 match self.tokens.get(probe) {
5413 Some(Token::Null) => probe += 1,
5414 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5415 _ => return Ok(()),
5416 }
5417 match self.tokens.get(probe) {
5418 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5419 _ => return Ok(()),
5420 }
5421 self.pos = probe;
5422 attrs.strict = strict;
5423 }
5424 "security" | "external" => {
5425 // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5426 let mut probe = self.pos + 1;
5427 if word == "external" {
5428 match self.tokens.get(probe) {
5429 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5430 probe += 1;
5431 }
5432 _ => return Ok(()),
5433 }
5434 }
5435 let definer = match self.tokens.get(probe) {
5436 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5437 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5438 _ => return Ok(()),
5439 };
5440 self.pos = probe + 1;
5441 attrs.security_definer = definer;
5442 }
5443 "parallel" => {
5444 let level = match self.tokens.get(self.pos + 1) {
5445 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5446 FunctionParallel::Safe
5447 }
5448 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5449 FunctionParallel::Restricted
5450 }
5451 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5452 FunctionParallel::Unsafe
5453 }
5454 _ => return Ok(()),
5455 };
5456 self.pos += 2;
5457 attrs.parallel = level;
5458 }
5459 "cost" | "rows" => {
5460 let Some(n) = self.peek_number_at(self.pos + 1) else {
5461 return Ok(());
5462 };
5463 self.pos += 2;
5464 if word == "cost" {
5465 attrs.cost = Some(n);
5466 } else {
5467 attrs.rows = Some(n);
5468 }
5469 }
5470 _ => return Ok(()),
5471 }
5472 }
5473 }
5474
5475 /// The numeric literal at `idx`, if there is one.
5476 fn peek_number_at(&self, idx: usize) -> Option<f64> {
5477 match self.tokens.get(idx)? {
5478 Token::Integer(n) => Some(*n as f64),
5479 Token::Float(f) => Some(*f),
5480 Token::Numeric(t) => t.parse::<f64>().ok(),
5481 _ => None,
5482 }
5483 }
5484
5485 /// Closing `)`-terminated argument list. v7.12.4 commonly
5486 /// sees the empty `()`; typed args round-trip but the
5487 /// executor (yet) doesn't invoke them.
5488 /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5489 /// it away, which is what PG does with one on a function parameter.
5490 fn skip_type_modifier(&mut self) {
5491 if !matches!(self.peek(), Token::LParen) {
5492 return;
5493 }
5494 // Only a numeric modifier — anything else is not one, and eating
5495 // it would swallow real grammar.
5496 let mut i = self.pos + 1;
5497 let mut seen_number = false;
5498 loop {
5499 match self.tokens.get(i) {
5500 Some(Token::Integer(_)) => seen_number = true,
5501 Some(Token::Comma) => {}
5502 Some(Token::RParen) => break,
5503 _ => return,
5504 }
5505 i += 1;
5506 }
5507 if !seen_number {
5508 return;
5509 }
5510 while self.pos <= i {
5511 self.advance();
5512 }
5513 }
5514
5515 fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5516 let mut args: Vec<FunctionArg> = Vec::new();
5517 if matches!(self.peek(), Token::RParen) {
5518 self.advance();
5519 return Ok(args);
5520 }
5521 loop {
5522 // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5523 // a reserved token; OUT / INOUT are bare idents.
5524 let mode = if matches!(self.peek(), Token::In) {
5525 self.advance();
5526 FunctionArgMode::In
5527 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5528 {
5529 self.advance();
5530 FunctionArgMode::Out
5531 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5532 {
5533 self.advance();
5534 FunctionArgMode::InOut
5535 } else {
5536 FunctionArgMode::In
5537 };
5538 // Optional name. The next token is either a name
5539 // (followed by a type ident) or the type itself.
5540 // Disambiguate by peeking ahead: if the token after
5541 // the next ident is also an ident, we treat the
5542 // first as the name.
5543 // v7.39 (round 315, V19) — take EVERY ident-like word up to
5544 // the comma or paren, then decide. Reading at most two of
5545 // them could not spell `x double precision` at all, and
5546 // silently mis-read the bare `double precision` as a
5547 // parameter named "double" — which is what made the same
5548 // signature key two different ways.
5549 let (name, ty_token) = {
5550 let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5551 while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5552 words.push(self.expect_ident_like()?);
5553 }
5554 // v7.39 (round 344) — a length / precision modifier on the
5555 // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5556 // accepts it and DROPS it — `pg_get_function_arguments`
5557 // reports plain `character varying` / `numeric`, measured on
5558 // 18.4 — but SPG raised `syntax error at or near "("`,
5559 // because the modifier's parens were never consumed.
5560 self.skip_type_modifier();
5561 // r1049 — `f(v bigint[])`. The array suffix parsed in
5562 // the column position, the cast position and (r1038)
5563 // the RETURNS position, but not here: the fifth
5564 // member of the same family, reported by sentori as
5565 // presumably the same code. It is now.
5566 let array_suffix = self.consume_array_suffix();
5567 let whole = words.join(" ");
5568 let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5569 {
5570 (Some(words[0].clone()), words[1..].join(" "))
5571 } else {
5572 (None, whole)
5573 };
5574 ty_token.push_str(&array_suffix);
5575 (name, ty_token)
5576 };
5577 // Type — try to map to ColumnTypeName, else Raw.
5578 let ty = match map_type_ident_to_column_type_name(&ty_token) {
5579 Some(t) => FunctionArgType::Typed(t),
5580 None => FunctionArgType::Raw(ty_token),
5581 };
5582 args.push(FunctionArg { mode, name, ty });
5583 match self.peek() {
5584 Token::Comma => {
5585 self.advance();
5586 continue;
5587 }
5588 Token::RParen => {
5589 self.advance();
5590 return Ok(args);
5591 }
5592 other => {
5593 return Err(self.err(alloc::format!(
5594 "expected , or ) in function arg list, got {other:?}"
5595 )));
5596 }
5597 }
5598 }
5599 }
5600
5601 fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5602 // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5603 // function whose row shape is named inline.
5604 if matches!(self.peek(), Token::Table)
5605 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5606 {
5607 self.advance(); // TABLE
5608 self.advance(); // (
5609 let mut cols: Vec<String> = Vec::new();
5610 loop {
5611 let cname = self.expect_ident_like()?;
5612 let mut ty: Vec<String> = Vec::new();
5613 loop {
5614 match self.peek() {
5615 Token::Comma | Token::RParen | Token::Eof => break,
5616 _ => {}
5617 }
5618 match self.advance() {
5619 Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5620 other => {
5621 if let Some(w) = unreserved_keyword_text(&other) {
5622 ty.push(w);
5623 }
5624 }
5625 }
5626 }
5627 cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5628 if matches!(self.peek(), Token::Comma) {
5629 self.advance();
5630 } else {
5631 break;
5632 }
5633 }
5634 if matches!(self.peek(), Token::RParen) {
5635 self.advance();
5636 }
5637 return Ok(FunctionReturn::Other(alloc::format!(
5638 "TABLE({})",
5639 cols.join(", ")
5640 )));
5641 }
5642 let ident = self.expect_ident_like()?;
5643 // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5644 if ident.eq_ignore_ascii_case("setof") {
5645 let inner = self.expect_ident_like()?;
5646 let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5647 return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5648 }
5649 if ident.eq_ignore_ascii_case("trigger") {
5650 return Ok(FunctionReturn::Trigger);
5651 }
5652 if ident.eq_ignore_ascii_case("void") {
5653 return Ok(FunctionReturn::Void);
5654 }
5655 // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5656 // RETURN position did not, so the `[` was a syntax error and the
5657 // whole migration stopped. sentori worked around it by returning
5658 // zero-padded text.
5659 let suffix = self.consume_array_suffix();
5660 if !suffix.is_empty() {
5661 return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5662 }
5663 match map_type_ident_to_column_type_name(&ident) {
5664 Some(t) => Ok(FunctionReturn::Type(t)),
5665 None => Ok(FunctionReturn::Other(ident)),
5666 }
5667 }
5668
5669 /// Consume any `[]` / `[N]` array markers after a type name and give
5670 /// back their text. Empty when there are none.
5671 fn consume_array_suffix(&mut self) -> String {
5672 let mut out = String::new();
5673 while matches!(self.peek(), Token::LBracket) {
5674 self.advance();
5675 // `[N]` is accepted and, as in PG, the length is not enforced.
5676 if let Token::Integer(n) = self.peek().clone() {
5677 self.advance();
5678 out.push_str(&alloc::format!("[{n}]"));
5679 } else {
5680 out.push_str("[]");
5681 }
5682 if matches!(self.peek(), Token::RBracket) {
5683 self.advance();
5684 }
5685 }
5686 out
5687 }
5688
5689 fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5690 match self.peek() {
5691 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5692 self.advance();
5693 let lang = self.expect_ident_like()?;
5694 Ok(Some(lang.to_ascii_lowercase()))
5695 }
5696 _ => Ok(None),
5697 }
5698 }
5699
5700 /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5701 /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5702 /// (expr)]*`. The `DOMAIN` keyword has already been
5703 /// consumed. PG allows the trailing constraints in any
5704 /// order; we approximate with a small loop.
5705 fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5706 let name = self.expect_ident_like()?;
5707 // Optional `AS`.
5708 if matches!(self.peek(), Token::As) {
5709 self.advance();
5710 }
5711 // v7.39 (round 259) — keep the raw type NAME when the base is not
5712 // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5713 // parent domain.
5714 let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _) =
5715 self.parse_type_with_implied_flags()?;
5716 let mut default: Option<Expr> = None;
5717 let mut not_null = false;
5718 let mut checks: Vec<Expr> = Vec::new();
5719 loop {
5720 match self.peek() {
5721 Token::Default => {
5722 if default.is_some() {
5723 return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5724 }
5725 self.advance();
5726 default = Some(self.parse_expr(0)?);
5727 }
5728 Token::Not => {
5729 self.advance();
5730 if !matches!(self.peek(), Token::Null) {
5731 return Err(self.err(alloc::format!(
5732 "expected NULL after NOT in DOMAIN, got {:?}",
5733 self.peek()
5734 )));
5735 }
5736 self.advance();
5737 not_null = true;
5738 }
5739 Token::Null => {
5740 self.advance();
5741 // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5742 // is the default-nullable marker (PG accepts it),
5743 // but AFTER a NOT NULL it is a conflict PG refuses
5744 // (`conflicting NULL/NOT NULL constraints`,
5745 // PG18-measured); the old arm no-opped both ways.
5746 if not_null {
5747 return Err(self.err(alloc::string::String::from(
5748 "conflicting NULL/NOT NULL constraints",
5749 )));
5750 }
5751 }
5752 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5753 self.advance();
5754 if !matches!(self.peek(), Token::LParen) {
5755 return Err(self.err(alloc::format!(
5756 "expected '(' after CHECK in DOMAIN, got {:?}",
5757 self.peek()
5758 )));
5759 }
5760 self.advance();
5761 let expr = self.parse_expr(0)?;
5762 if !matches!(self.peek(), Token::RParen) {
5763 return Err(self.err(alloc::format!(
5764 "expected ')' after CHECK expr, got {:?}",
5765 self.peek()
5766 )));
5767 }
5768 self.advance();
5769 checks.push(expr);
5770 }
5771 // CONSTRAINT <name> CHECK (…) — PG accepts a name
5772 // prefix on the constraint; we drop the name and
5773 // recurse into the constraint parsing.
5774 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5775 self.advance();
5776 let _ = self.expect_ident_like()?;
5777 }
5778 _ => break,
5779 }
5780 }
5781 Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5782 name,
5783 base_type,
5784 base_domain: base_user_ref,
5785 default,
5786 not_null,
5787 checks,
5788 }))
5789 }
5790
5791 /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5792 /// ('a', 'b', …)`. The `TYPE` keyword has already been
5793 /// consumed.
5794 fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5795 let name = self.expect_ident_like()?;
5796 // Required `AS`.
5797 if !matches!(self.peek(), Token::As) {
5798 return Err(self.err(alloc::format!(
5799 "expected AS after CREATE TYPE {name:?}, got {:?}",
5800 self.peek()
5801 )));
5802 }
5803 self.advance();
5804 // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5805 // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5806 // on the next token: `(` = composite, ident `ENUM` = enum.
5807 if matches!(self.peek(), Token::LParen) {
5808 self.advance();
5809 let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5810 let mut field_user_types: Vec<Option<String>> = Vec::new();
5811 // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5812 // is legal PG (an attribute-less composite; measured — the old
5813 // e2e note claimed PG requires at least one attribute).
5814 if matches!(self.peek(), Token::RParen) {
5815 self.advance();
5816 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5817 name,
5818 kind: crate::ast::TypeKind::Composite {
5819 fields,
5820 field_user_types,
5821 },
5822 }));
5823 }
5824 loop {
5825 let field_name = self.expect_ident_like()?;
5826 // v7.39 (round 264) — keep the raw type name when it is not
5827 // a builtin: that is how a NESTED composite field records
5828 // which composite it holds.
5829 let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _) =
5830 self.parse_type_with_implied_flags()?;
5831 fields.push((field_name, field_type));
5832 field_user_types.push(field_user_ref);
5833 if matches!(self.peek(), Token::Comma) {
5834 self.advance();
5835 continue;
5836 }
5837 if matches!(self.peek(), Token::RParen) {
5838 self.advance();
5839 break;
5840 }
5841 return Err(self.err(alloc::format!(
5842 "expected , or ) in composite field list, got {:?}",
5843 self.peek()
5844 )));
5845 }
5846 if fields.is_empty() {
5847 return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5848 }
5849 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5850 name,
5851 kind: crate::ast::TypeKind::Composite {
5852 fields,
5853 field_user_types,
5854 },
5855 }));
5856 }
5857 // Required `ENUM` ident.
5858 let kind_ident = match self.peek().clone() {
5859 Token::Ident(s) | Token::QuotedIdent(s) => s,
5860 other => {
5861 return Err(self.err(alloc::format!(
5862 "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5863 )));
5864 }
5865 };
5866 if !kind_ident.eq_ignore_ascii_case("enum") {
5867 return Err(self.err(alloc::format!(
5868 "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5869 )));
5870 }
5871 self.advance();
5872 if !matches!(self.peek(), Token::LParen) {
5873 return Err(self.err(alloc::format!(
5874 "expected '(' after ENUM, got {:?}",
5875 self.peek()
5876 )));
5877 }
5878 self.advance();
5879 let mut labels: Vec<String> = Vec::new();
5880 loop {
5881 match self.peek().clone() {
5882 Token::String(s) => {
5883 self.advance();
5884 labels.push(s);
5885 }
5886 other => {
5887 return Err(
5888 self.err(alloc::format!("expected enum label string, got {other:?}"))
5889 );
5890 }
5891 }
5892 if matches!(self.peek(), Token::Comma) {
5893 self.advance();
5894 continue;
5895 }
5896 if matches!(self.peek(), Token::RParen) {
5897 self.advance();
5898 break;
5899 }
5900 return Err(self.err(alloc::format!(
5901 "expected , or ) in ENUM label list, got {:?}",
5902 self.peek()
5903 )));
5904 }
5905 if labels.is_empty() {
5906 return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5907 }
5908 Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5909 name,
5910 kind: crate::ast::TypeKind::Enum { labels },
5911 }))
5912 }
5913
5914 /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
5915 /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
5916 /// The `CREATE MATERIALIZED VIEW` keywords have already been
5917 /// consumed.
5918 fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
5919 let if_not_exists = self.parse_if_not_exists();
5920 let name = self.expect_ident_like()?;
5921 let mut columns: Vec<String> = Vec::new();
5922 if matches!(self.peek(), Token::LParen) {
5923 self.advance();
5924 loop {
5925 let c = self.expect_ident_like()?;
5926 columns.push(c);
5927 if matches!(self.peek(), Token::Comma) {
5928 self.advance();
5929 continue;
5930 }
5931 if matches!(self.peek(), Token::RParen) {
5932 self.advance();
5933 break;
5934 }
5935 return Err(self.err(alloc::format!(
5936 "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
5937 self.peek()
5938 )));
5939 }
5940 }
5941 if !matches!(self.peek(), Token::As) {
5942 return Err(self.err(alloc::format!(
5943 "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
5944 self.peek()
5945 )));
5946 }
5947 self.advance();
5948 // v7.39 (round 151) — a WITH-headed body is legal (read-only
5949 // CTEs only; the engine rejects data-modifying ones with PG's
5950 // message). A trailing `WITH [NO] DATA` can't START the body,
5951 // so WITH here heads the query.
5952 let body = if self.peek_is_with_kw() {
5953 self.advance();
5954 self.parse_nested_with_select()?
5955 } else {
5956 let body_stmt = self.parse_select_stmt()?;
5957 let Statement::Select(body) = body_stmt else {
5958 return Err(self.err(alloc::format!(
5959 "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
5960 )));
5961 };
5962 body
5963 };
5964 // Optional trailing `WITH [NO] DATA`.
5965 let with_data = self.parse_optional_with_data(true)?;
5966 Ok(Statement::CreateMaterializedView(
5967 crate::ast::CreateMaterializedViewStatement {
5968 temporary: false,
5969 name,
5970 if_not_exists,
5971 columns,
5972 body,
5973 with_data,
5974 as_plain_table: false,
5975 },
5976 ))
5977 }
5978
5979 /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
5980 /// `default_when_absent` is what to return if the tail is
5981 /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
5982 /// WITH DATA).
5983 fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
5984 let save = self.pos;
5985 // `WITH` is an Ident (not reserved in the lexer).
5986 let is_with = match self.peek() {
5987 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
5988 _ => false,
5989 };
5990 if !is_with {
5991 return Ok(default_when_absent);
5992 }
5993 self.advance();
5994 // Optional `NO`.
5995 let mut with_data = true;
5996 let is_no = match self.peek() {
5997 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
5998 _ => false,
5999 };
6000 if is_no {
6001 self.advance();
6002 with_data = false;
6003 }
6004 // Required `DATA` ident.
6005 let is_data = match self.peek() {
6006 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
6007 _ => false,
6008 };
6009 if is_data {
6010 self.advance();
6011 Ok(with_data)
6012 } else {
6013 // Caller's WITH wasn't WITH-DATA — rewind so the outer
6014 // parser can interpret it.
6015 self.pos = save;
6016 Ok(default_when_absent)
6017 }
6018 }
6019
6020 /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
6021 /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
6022 /// All keyword prefixes have already been consumed; the flags
6023 /// say which were present.
6024 fn parse_create_view_after_keyword(
6025 &mut self,
6026 or_replace: bool,
6027 _materialized_unused: bool,
6028 temporary: bool,
6029 ) -> Result<Statement, ParseError> {
6030 let if_not_exists = self.parse_if_not_exists();
6031 let name = self.expect_ident_like()?;
6032 // Optional `(col, col, …)` rename list.
6033 let mut columns: Vec<String> = Vec::new();
6034 if matches!(self.peek(), Token::LParen) {
6035 self.advance();
6036 loop {
6037 let c = self.expect_ident_like()?;
6038 columns.push(c);
6039 if matches!(self.peek(), Token::Comma) {
6040 self.advance();
6041 continue;
6042 }
6043 if matches!(self.peek(), Token::RParen) {
6044 self.advance();
6045 break;
6046 }
6047 return Err(self.err(alloc::format!(
6048 "expected , or ) in VIEW column list, got {:?}",
6049 self.peek()
6050 )));
6051 }
6052 }
6053 // Required `AS`.
6054 if !matches!(self.peek(), Token::As) {
6055 return Err(self.err(alloc::format!(
6056 "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
6057 self.peek()
6058 )));
6059 }
6060 self.advance();
6061 // Body: a regular SELECT statement. v7.39 (round 151) — a
6062 // WITH-headed body is legal too (read-only CTEs only; the
6063 // engine rejects data-modifying ones with PG's message).
6064 // Disambiguation vs `WITH CHECK OPTION`: a body can't START
6065 // with the check-option clause, so WITH here heads the query.
6066 let body = if self.peek_is_with_kw() {
6067 self.advance();
6068 self.parse_nested_with_select()?
6069 } else {
6070 let body_stmt = self.parse_select_stmt()?;
6071 let Statement::Select(body) = body_stmt else {
6072 return Err(self.err(alloc::format!(
6073 "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
6074 )));
6075 };
6076 body
6077 };
6078 // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
6079 // The SELECT parser stops before a trailing WITH, so it lands here.
6080 let check_option = if matches!(self.peek(),
6081 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
6082 {
6083 self.advance(); // WITH
6084 let opt = match self.peek() {
6085 Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
6086 self.advance();
6087 crate::ast::ViewCheckOption::Local
6088 }
6089 Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
6090 self.advance();
6091 crate::ast::ViewCheckOption::Cascaded
6092 }
6093 // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
6094 _ => crate::ast::ViewCheckOption::Cascaded,
6095 };
6096 if !matches!(self.peek(),
6097 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
6098 {
6099 return Err(self.err(alloc::format!(
6100 "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
6101 self.peek()
6102 )));
6103 }
6104 self.advance(); // CHECK
6105 if !matches!(self.peek(),
6106 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
6107 {
6108 return Err(self.err(alloc::format!(
6109 "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
6110 self.peek()
6111 )));
6112 }
6113 self.advance(); // OPTION
6114 Some(opt)
6115 } else {
6116 None
6117 };
6118 Ok(Statement::CreateView(crate::ast::CreateViewStatement {
6119 name,
6120 or_replace,
6121 if_not_exists,
6122 temporary,
6123 columns,
6124 body,
6125 check_option,
6126 }))
6127 }
6128
6129 /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
6130 /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
6131 /// consumed; `temporary` carries whether TEMPORARY was seen.
6132 fn parse_create_sequence_after_keyword(
6133 &mut self,
6134 temporary: bool,
6135 ) -> Result<Statement, ParseError> {
6136 let if_not_exists = self.parse_if_not_exists();
6137 let name = self.expect_ident_like()?;
6138 // Optional `AS data_type`.
6139 let data_type = if matches!(self.peek(), Token::As) {
6140 self.advance();
6141 Some(self.parse_sequence_data_type()?)
6142 } else {
6143 None
6144 };
6145 let options = self.parse_sequence_options(/* allow_restart = */ false)?;
6146 Ok(Statement::CreateSequence(
6147 crate::ast::CreateSequenceStatement {
6148 name,
6149 if_not_exists,
6150 temporary,
6151 data_type,
6152 options,
6153 },
6154 ))
6155 }
6156
6157 /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
6158 /// already been consumed; this is reached after `SEQUENCE`.
6159 /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
6160 fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
6161 use crate::ast::AlterDomainAction as A;
6162 let name = self.expect_ident_like()?;
6163 // DROP / SET / ADD lex as reserved keyword tokens, not idents.
6164 let kw = match self.peek() {
6165 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6166 Token::Drop => alloc::string::String::from("drop"),
6167 Token::Default => alloc::string::String::from("default"),
6168 other => {
6169 return Err(self.err(alloc::format!(
6170 "expected an ALTER DOMAIN action, got {other:?}"
6171 )));
6172 }
6173 };
6174 let action = match kw.as_str() {
6175 "add" => {
6176 self.advance();
6177 let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
6178 {
6179 self.advance();
6180 Some(self.expect_ident_like()?)
6181 } else {
6182 None
6183 };
6184 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
6185 return Err(self.err(alloc::format!(
6186 "ALTER DOMAIN ADD supports CHECK only, got {:?}",
6187 self.peek()
6188 )));
6189 }
6190 self.advance();
6191 if !matches!(self.peek(), Token::LParen) {
6192 return Err(self.err("expected '(' after CHECK".into()));
6193 }
6194 self.advance();
6195 let check = self.parse_expr(0)?;
6196 if !matches!(self.peek(), Token::RParen) {
6197 return Err(self.err("expected ')' after CHECK expression".into()));
6198 }
6199 self.advance();
6200 A::AddConstraint { name: cname, check }
6201 }
6202 "drop" => {
6203 self.advance();
6204 match self.peek() {
6205 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
6206 self.advance();
6207 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
6208 {
6209 self.advance();
6210 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
6211 {
6212 return Err(self.err("expected EXISTS after IF".into()));
6213 }
6214 self.advance();
6215 true
6216 } else {
6217 false
6218 };
6219 let cn = self.expect_ident_like()?;
6220 A::DropConstraint {
6221 name: cn,
6222 if_exists,
6223 }
6224 }
6225 Token::Default => {
6226 self.advance();
6227 A::DropDefault
6228 }
6229 Token::Not => {
6230 self.advance();
6231 if !matches!(self.peek(), Token::Null) {
6232 return Err(self.err("expected NULL after NOT".into()));
6233 }
6234 self.advance();
6235 A::DropNotNull
6236 }
6237 other => {
6238 return Err(self.err(alloc::format!(
6239 "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
6240 )));
6241 }
6242 }
6243 }
6244 "set" => {
6245 self.advance();
6246 match self.peek() {
6247 Token::Default => {
6248 self.advance();
6249 A::SetDefault(self.parse_expr(0)?)
6250 }
6251 Token::Not => {
6252 self.advance();
6253 if !matches!(self.peek(), Token::Null) {
6254 return Err(self.err("expected NULL after NOT".into()));
6255 }
6256 self.advance();
6257 A::SetNotNull
6258 }
6259 other => {
6260 return Err(self.err(alloc::format!(
6261 "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6262 )));
6263 }
6264 }
6265 }
6266 "rename" => {
6267 self.advance();
6268 if !matches!(self.peek(), Token::To) {
6269 return Err(self.err("expected TO after RENAME".into()));
6270 }
6271 self.advance();
6272 A::RenameTo(self.expect_ident_like()?)
6273 }
6274 other => {
6275 return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6276 }
6277 };
6278 Ok(Statement::AlterDomain { name, action })
6279 }
6280
6281 fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6282 let if_exists = self.parse_if_exists();
6283 let name = self.expect_ident_like()?;
6284 // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6285 // the option list (PG allows only one or the other).
6286 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6287 self.advance();
6288 if matches!(self.peek(), Token::To) {
6289 self.advance();
6290 } else {
6291 self.expect_keyword_ident("to")?;
6292 }
6293 let new = self.expect_ident_like()?;
6294 return Ok(Statement::AlterSequence(
6295 crate::ast::AlterSequenceStatement {
6296 name,
6297 if_exists,
6298 options: crate::ast::SequenceOptions::default(),
6299 rename_to: Some(new),
6300 },
6301 ));
6302 }
6303 let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6304 Ok(Statement::AlterSequence(
6305 crate::ast::AlterSequenceStatement {
6306 name,
6307 if_exists,
6308 options,
6309 rename_to: None,
6310 },
6311 ))
6312 }
6313
6314 fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6315 let kw = self.expect_ident_like()?;
6316 match kw.to_ascii_lowercase().as_str() {
6317 "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6318 "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6319 "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6320 other => Err(self.err(alloc::format!(
6321 "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6322 ))),
6323 }
6324 }
6325
6326 fn parse_sequence_options(
6327 &mut self,
6328 allow_restart: bool,
6329 ) -> Result<crate::ast::SequenceOptions, ParseError> {
6330 use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6331 let mut opts = SequenceOptions::default();
6332 #[allow(clippy::while_let_loop)]
6333 loop {
6334 // Match an ident; stop at any non-ident token (sentinel,
6335 // semicolon, end of statement).
6336 let kw_lc = match self.peek() {
6337 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6338 _ => break,
6339 };
6340 match kw_lc.as_str() {
6341 "increment" => {
6342 self.advance();
6343 // Optional BY.
6344 if self.peek_is_by() {
6345 self.advance();
6346 }
6347 opts.increment = Some(self.expect_signed_int()?);
6348 }
6349 "minvalue" => {
6350 self.advance();
6351 opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6352 }
6353 "maxvalue" => {
6354 self.advance();
6355 opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6356 }
6357 "no" => {
6358 self.advance();
6359 let what = self.expect_ident_like()?;
6360 match what.to_ascii_lowercase().as_str() {
6361 "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6362 "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6363 "cycle" => opts.cycle = Some(false),
6364 other => {
6365 return Err(self.err(alloc::format!(
6366 "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6367 )));
6368 }
6369 }
6370 }
6371 "start" => {
6372 self.advance();
6373 // Optional WITH.
6374 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6375 if s.eq_ignore_ascii_case("with"))
6376 {
6377 self.advance();
6378 }
6379 opts.start = Some(self.expect_signed_int()?);
6380 }
6381 "restart" if allow_restart => {
6382 self.advance();
6383 // Optional WITH n; bare RESTART means restart at START.
6384 let mut with_val: Option<i64> = None;
6385 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6386 if s.eq_ignore_ascii_case("with"))
6387 {
6388 self.advance();
6389 with_val = Some(self.expect_signed_int()?);
6390 } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6391 with_val = Some(self.expect_signed_int()?);
6392 }
6393 opts.restart = Some(with_val);
6394 }
6395 "cache" => {
6396 self.advance();
6397 opts.cache = Some(self.expect_signed_int()?);
6398 }
6399 "cycle" => {
6400 self.advance();
6401 opts.cycle = Some(true);
6402 }
6403 "owned" => {
6404 self.advance();
6405 match self.peek() {
6406 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6407 self.advance();
6408 }
6409 other => {
6410 return Err(
6411 self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6412 );
6413 }
6414 }
6415 // OWNED BY {NONE | tab.col}. Read just one ident
6416 // (NOT expect_ident_like which would auto-strip
6417 // a schema prefix and consume the `.col` we need).
6418 let first = match self.advance() {
6419 Token::Ident(s) | Token::QuotedIdent(s) => s,
6420 other => {
6421 return Err(self.err(alloc::format!(
6422 "expected identifier or NONE after OWNED BY, got {other:?}"
6423 )));
6424 }
6425 };
6426 if first.eq_ignore_ascii_case("none") {
6427 opts.owned_by = Some(SequenceOwnedBy::None);
6428 } else if matches!(self.peek(), Token::Dot) {
6429 self.advance();
6430 let second = match self.advance() {
6431 Token::Ident(s) | Token::QuotedIdent(s) => s,
6432 other => {
6433 return Err(self.err(alloc::format!(
6434 "expected column name after OWNED BY {first}., got {other:?}"
6435 )));
6436 }
6437 };
6438 // v7.17 dump-compat fix — pg_dump emits
6439 // OWNED BY clauses as
6440 // `schema.table.column` (three segments).
6441 // If a third `.<ident>` follows, treat the
6442 // first ident as schema (drop it; SPG is
6443 // single-schema) and the middle / last
6444 // pair as table.column. Otherwise it's
6445 // the two-segment form table.column.
6446 if matches!(self.peek(), Token::Dot) {
6447 self.advance();
6448 let third = match self.advance() {
6449 Token::Ident(s) | Token::QuotedIdent(s) => s,
6450 other => {
6451 return Err(self.err(alloc::format!(
6452 "expected column name after OWNED BY {first}.{second}., got {other:?}"
6453 )));
6454 }
6455 };
6456 let _ = first; // schema prefix discarded
6457 opts.owned_by = Some(SequenceOwnedBy::Column {
6458 table: second,
6459 column: third,
6460 });
6461 } else {
6462 opts.owned_by = Some(SequenceOwnedBy::Column {
6463 table: first,
6464 column: second,
6465 });
6466 }
6467 } else {
6468 return Err(self.err(alloc::format!(
6469 "expected table.column or NONE after OWNED BY, got {first:?}"
6470 )));
6471 }
6472 }
6473 _ => break,
6474 }
6475 }
6476 Ok(opts)
6477 }
6478
6479 fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6480 let neg = if matches!(self.peek(), Token::Minus) {
6481 self.advance();
6482 true
6483 } else {
6484 false
6485 };
6486 match self.peek() {
6487 Token::Integer(n) => {
6488 let v = *n;
6489 self.advance();
6490 Ok(if neg { -v } else { v })
6491 }
6492 other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6493 }
6494 }
6495
6496 /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6497 /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6498 /// clause is fully accepted and discarded — SPG always runs
6499 /// constraint checks immediately (single-writer model). The
6500 /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6501 /// in either order (per the SQL spec they're independent),
6502 /// though pg_dump always emits them in the canonical
6503 /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6504 /// Stops at the first token that isn't part of the clause.
6505 fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6506 self.consume_deferrable_clauses_timed().map(|_| ())
6507 }
6508
6509 /// v7.39 (round 288) — the same scan, but reporting what it saw:
6510 /// `(deferrable, initially_deferred)`. The clauses were parsed and
6511 /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6512 /// NOT DEFERRABLE and a circular-FK migration could not load.
6513 fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6514 let mut deferrable = false;
6515 let mut initially_deferred = false;
6516 loop {
6517 // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6518 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6519 self.advance();
6520 deferrable = true;
6521 if self.consume_optional_initially_clause()? {
6522 initially_deferred = true;
6523 }
6524 continue;
6525 }
6526 // `NOT DEFERRABLE` — already worked pre-3.1.
6527 if matches!(self.peek(), Token::Not) {
6528 let look = self.tokens.get(self.pos + 1);
6529 if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6530 self.advance(); // NOT
6531 self.advance(); // DEFERRABLE
6532 deferrable = false;
6533 initially_deferred = false;
6534 let _ = self.consume_optional_initially_clause()?;
6535 continue;
6536 }
6537 break;
6538 }
6539 // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6540 // accepts this without a leading [NOT] DEFERRABLE
6541 // (the timing keyword alone). pg_dump occasionally
6542 // emits it on FK constraints that inherit timing.
6543 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6544 if self.consume_optional_initially_clause()? {
6545 initially_deferred = true;
6546 // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6547 deferrable = true;
6548 }
6549 continue;
6550 }
6551 break;
6552 }
6553 Ok((deferrable, initially_deferred))
6554 }
6555
6556 /// Helper for [`consume_optional_deferrable_clauses`]. When the
6557 /// next token is `INITIALLY`, consume it plus the required
6558 /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6559 /// Returns true when the timing seen was `DEFERRED`.
6560 fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6561 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6562 return Ok(false);
6563 }
6564 self.advance(); // INITIALLY
6565 match self.advance() {
6566 Token::Ident(s)
6567 if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6568 {
6569 Ok(s.eq_ignore_ascii_case("deferred"))
6570 }
6571 other => Err(self.err(alloc::format!(
6572 "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6573 ))),
6574 }
6575 }
6576
6577 /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6578 /// in its entirety so the parser returns Empty without
6579 /// touching the runtime. The CREATE+PROCEDURE keywords are
6580 /// already consumed; this swallows everything from the
6581 /// procedure name through the matching `END`, including
6582 /// nested `BEGIN`/`END` blocks, internal `;` terminators
6583 /// (DELIMITER `//` makes the script splitter forward the
6584 /// whole block as one statement), `@var` session-variable
6585 /// references, and the trailing terminator.
6586 ///
6587 /// Tracks nesting depth so:
6588 /// BEGIN
6589 /// IF cond THEN
6590 /// BEGIN ... END;
6591 /// END IF;
6592 /// END
6593 /// terminates at the outer END.
6594 fn consume_mysql_routine_body(&mut self) {
6595 // Outer skeleton: name, (...), optional clauses, BEGIN
6596 // <body> END [;]. Scan for the first BEGIN — anything
6597 // before it is signature decoration we don't care about.
6598 // Once inside BEGIN, count up on BEGIN, down on END.
6599 let mut depth: i32 = 0;
6600 let mut started = false;
6601 loop {
6602 match self.peek().clone() {
6603 Token::Begin => {
6604 self.advance();
6605 depth += 1;
6606 started = true;
6607 }
6608 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6609 self.advance();
6610 if started {
6611 depth -= 1;
6612 if depth <= 0 {
6613 // Optional trailing ident (`END IF`,
6614 // `END LOOP`, `END WHILE`, `END CASE`,
6615 // `END label_name`) — eat the next
6616 // ident if present so we don't
6617 // mistake `END IF;` for the outer
6618 // close.
6619 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6620 // If the next token is one of the
6621 // PL/SQL block-closer keywords,
6622 // the END belongs to an inner
6623 // block; bump depth back up.
6624 let is_inner_close = matches!(
6625 self.peek(),
6626 Token::Ident(s) | Token::QuotedIdent(s)
6627 if matches!(
6628 s.to_ascii_lowercase().as_str(),
6629 "if" | "loop" | "while" | "case" | "repeat"
6630 )
6631 );
6632 if is_inner_close {
6633 self.advance();
6634 depth += 1;
6635 continue;
6636 }
6637 }
6638 // Eat optional trailing `;`.
6639 if matches!(self.peek(), Token::Semicolon) {
6640 self.advance();
6641 }
6642 return;
6643 }
6644 }
6645 }
6646 Token::Eof => return,
6647 _ => {
6648 self.advance();
6649 }
6650 }
6651 }
6652 }
6653
6654 /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6655 /// that appear between `CREATE` and `VIEW` in mysqldump output:
6656 ///
6657 /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6658 /// * `DEFINER = <user>` (user may be a quoted string, a bare
6659 /// ident, or `ident @ ident-or-quoted-string` host form)
6660 /// * `SQL SECURITY {DEFINER|INVOKER}`
6661 ///
6662 /// Each clause may appear at most once but in any order.
6663 /// The hints are pure planner / permission metadata that
6664 /// SPG's view-rewrite engine handles uniformly; we accept
6665 /// and discard. Returns `Ok(())` once a non-clause token is
6666 /// peeked (the caller then checks for the `VIEW` keyword).
6667 fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6668 loop {
6669 match self.peek().clone() {
6670 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6671 self.advance(); // ALGORITHM
6672 // Optional `=`. MySQL spec requires it but be
6673 // generous.
6674 if matches!(self.peek(), Token::Eq) {
6675 self.advance();
6676 }
6677 // UNDEFINED / MERGE / TEMPTABLE — accept any
6678 // bare ident; unknown values still parse so
6679 // future MySQL versions don't break.
6680 if matches!(
6681 self.peek(),
6682 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6683 ) {
6684 self.advance();
6685 }
6686 }
6687 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6688 self.advance(); // DEFINER
6689 if matches!(self.peek(), Token::Eq) {
6690 self.advance();
6691 }
6692 // User: quoted string, ident, OR ident @ host
6693 // (host may itself be quoted or bare).
6694 match self.peek().clone() {
6695 Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6696 self.advance();
6697 // Optional `@host`.
6698 if matches!(self.peek(), Token::At) {
6699 self.advance();
6700 if matches!(
6701 self.peek(),
6702 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6703 ) {
6704 self.advance();
6705 }
6706 }
6707 }
6708 _ => {}
6709 }
6710 }
6711 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6712 // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6713 // when followed by SECURITY — the dispatcher must
6714 // not consume a bare `SQL` token (it's not a
6715 // legal CREATE prefix on its own).
6716 let save = self.pos;
6717 self.advance(); // SQL
6718 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6719 if s2.eq_ignore_ascii_case("security"))
6720 {
6721 self.advance(); // SECURITY
6722 // DEFINER / INVOKER trailing ident.
6723 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6724 self.advance();
6725 }
6726 } else {
6727 // Not a SQL SECURITY clause — roll back and
6728 // bail; the caller will error out cleanly.
6729 self.pos = save;
6730 return Ok(());
6731 }
6732 }
6733 _ => return Ok(()),
6734 }
6735 }
6736 }
6737
6738 fn parse_if_not_exists(&mut self) -> bool {
6739 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6740 {
6741 let save = self.pos;
6742 self.advance();
6743 if matches!(self.peek(), Token::Not) {
6744 self.advance();
6745 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6746 {
6747 self.advance();
6748 return true;
6749 }
6750 }
6751 self.pos = save;
6752 }
6753 false
6754 }
6755
6756 fn parse_if_exists(&mut self) -> bool {
6757 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6758 {
6759 let save = self.pos;
6760 self.advance();
6761 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6762 {
6763 self.advance();
6764 return true;
6765 }
6766 self.pos = save;
6767 }
6768 false
6769 }
6770
6771 /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6772 /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6773 /// been consumed.
6774 fn parse_create_trigger_after_keyword(
6775 &mut self,
6776 or_replace: bool,
6777 ) -> Result<Statement, ParseError> {
6778 let name = self.expect_ident_like()?;
6779 let timing = {
6780 let ident = self.expect_ident_like()?;
6781 if ident.eq_ignore_ascii_case("before") {
6782 TriggerTiming::Before
6783 } else if ident.eq_ignore_ascii_case("after") {
6784 TriggerTiming::After
6785 } else if ident.eq_ignore_ascii_case("instead") {
6786 let next = self.expect_ident_like()?;
6787 if !next.eq_ignore_ascii_case("of") {
6788 return Err(self.err(alloc::format!(
6789 "expected OF after INSTEAD in trigger timing, got {next:?}"
6790 )));
6791 }
6792 TriggerTiming::InsteadOf
6793 } else {
6794 return Err(self.err(alloc::format!(
6795 "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6796 )));
6797 }
6798 };
6799 // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6800 // OR is a reserved keyword token (Token::Or), not an Ident.
6801 // v7.13.0 — after an UPDATE event we may optionally see
6802 // `OF col, col, …` (mailrs round-5 G7). Columns are
6803 // captured into `update_columns` once across the whole
6804 // events list; multiple `UPDATE OF` clauses are rejected.
6805 let mut events: Vec<TriggerEvent> = Vec::new();
6806 let mut update_columns: Vec<String> = Vec::new();
6807 let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6808 events.push(first_ev);
6809 if !first_cols.is_empty() {
6810 update_columns = first_cols;
6811 }
6812 while matches!(self.peek(), Token::Or) {
6813 self.advance();
6814 let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6815 events.push(ev);
6816 if !cols.is_empty() {
6817 if !update_columns.is_empty() {
6818 return Err(
6819 self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6820 );
6821 }
6822 update_columns = cols;
6823 }
6824 }
6825 // ON <table>
6826 let tok = self.peek();
6827 let Token::On = tok else {
6828 return Err(self.err(alloc::format!(
6829 "expected ON after trigger events, got {tok:?}"
6830 )));
6831 };
6832 self.advance();
6833 let table = self.expect_ident_like()?;
6834 // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6835 // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6836 // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6837 // the trigger as a plain AFTER trigger (correct for every non-deferred
6838 // use; deferral timing is not yet honoured).
6839 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6840 if s.eq_ignore_ascii_case("from"))
6841 {
6842 self.advance();
6843 let _reftable = self.expect_ident_like()?;
6844 }
6845 self.consume_optional_deferrable_clauses()?;
6846 // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6847 // keyword (Token::For); EACH / ROW / STATEMENT are bare
6848 // idents.
6849 if !matches!(self.peek(), Token::For) {
6850 return Err(self.err(alloc::format!(
6851 "expected FOR EACH ROW / STATEMENT, got {:?}",
6852 self.peek()
6853 )));
6854 }
6855 self.advance();
6856 let for_each = {
6857 let e = self.expect_ident_like()?;
6858 if !e.eq_ignore_ascii_case("each") {
6859 return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6860 }
6861 let unit = self.expect_ident_like()?;
6862 if unit.eq_ignore_ascii_case("row") {
6863 TriggerForEach::Row
6864 } else if unit.eq_ignore_ascii_case("statement") {
6865 TriggerForEach::Statement
6866 } else {
6867 return Err(self.err(alloc::format!(
6868 "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6869 )));
6870 }
6871 };
6872 // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6873 let when_condition = if matches!(self.peek(),
6874 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6875 {
6876 self.advance();
6877 Some(self.parse_paren_expr("WHEN")?)
6878 } else {
6879 None
6880 };
6881 // EXECUTE FUNCTION/PROCEDURE name(...)
6882 let exec = self.expect_ident_like()?;
6883 if !exec.eq_ignore_ascii_case("execute") {
6884 return Err(self.err(alloc::format!(
6885 "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6886 )));
6887 }
6888 let fn_or_proc = self.expect_ident_like()?;
6889 if !(fn_or_proc.eq_ignore_ascii_case("function")
6890 || fn_or_proc.eq_ignore_ascii_case("procedure"))
6891 {
6892 return Err(self.err(alloc::format!(
6893 "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6894 )));
6895 }
6896 let function = self.expect_ident_like()?;
6897 // Optional empty arg list `()`.
6898 if matches!(self.peek(), Token::LParen) {
6899 self.advance();
6900 if !matches!(self.peek(), Token::RParen) {
6901 return Err(self.err(alloc::format!(
6902 "v7.12.4 trigger function calls take no args; got {:?}",
6903 self.peek()
6904 )));
6905 }
6906 self.advance();
6907 }
6908 Ok(Statement::CreateTrigger(CreateTriggerStatement {
6909 name,
6910 or_replace,
6911 timing,
6912 events,
6913 table,
6914 for_each,
6915 function,
6916 update_columns,
6917 when_condition,
6918 }))
6919 }
6920
6921 /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
6922 /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
6923 fn parse_create_rule_after_keyword(
6924 &mut self,
6925 or_replace: bool,
6926 ) -> Result<Statement, ParseError> {
6927 let name = self.expect_ident_like()?;
6928 if !matches!(self.peek(), Token::As) {
6929 return Err(self.err(alloc::format!(
6930 "expected AS in CREATE RULE, got {:?}",
6931 self.peek()
6932 )));
6933 }
6934 self.advance();
6935 if !matches!(self.peek(), Token::On) {
6936 return Err(self.err(alloc::format!(
6937 "expected ON in CREATE RULE, got {:?}",
6938 self.peek()
6939 )));
6940 }
6941 self.advance();
6942 let event = self.parse_rule_event()?;
6943 if !matches!(self.peek(), Token::To)
6944 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
6945 {
6946 return Err(self.err(alloc::format!(
6947 "expected TO after rule event, got {:?}",
6948 self.peek()
6949 )));
6950 }
6951 self.advance();
6952 let table = self.expect_ident_like()?;
6953 // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
6954 let when_condition = if matches!(self.peek(), Token::Where) {
6955 self.advance();
6956 Some(self.parse_expr(0)?)
6957 } else {
6958 None
6959 };
6960 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
6961 {
6962 return Err(self.err(alloc::format!(
6963 "expected DO in CREATE RULE, got {:?}",
6964 self.peek()
6965 )));
6966 }
6967 self.advance();
6968 // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
6969 let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
6970 {
6971 self.advance();
6972 true
6973 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
6974 self.advance();
6975 false
6976 } else {
6977 false
6978 };
6979 // `NOTHING` | `( cmd; … )` | `cmd`.
6980 let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
6981 {
6982 self.advance();
6983 Vec::new()
6984 } else if matches!(self.peek(), Token::LParen) {
6985 self.advance();
6986 let mut cmds = Vec::new();
6987 loop {
6988 cmds.push(self.parse_one_statement()?);
6989 if matches!(self.peek(), Token::Semicolon) {
6990 self.advance();
6991 if matches!(self.peek(), Token::RParen) {
6992 break;
6993 }
6994 continue;
6995 }
6996 break;
6997 }
6998 if !matches!(self.peek(), Token::RParen) {
6999 return Err(self.err(alloc::format!(
7000 "expected ) closing the CREATE RULE command list, got {:?}",
7001 self.peek()
7002 )));
7003 }
7004 self.advance();
7005 cmds
7006 } else {
7007 alloc::vec![self.parse_one_statement()?]
7008 };
7009 Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
7010 name,
7011 or_replace,
7012 event,
7013 table,
7014 instead,
7015 when_condition,
7016 commands,
7017 }))
7018 }
7019
7020 /// v7.39 (round 139) — a rule event keyword → uppercase string.
7021 fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
7022 if matches!(self.peek(), Token::Insert) {
7023 self.advance();
7024 return Ok(alloc::string::String::from("INSERT"));
7025 }
7026 if matches!(self.peek(), Token::Select) {
7027 self.advance();
7028 return Ok(alloc::string::String::from("SELECT"));
7029 }
7030 match self.peek() {
7031 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7032 self.advance();
7033 Ok(alloc::string::String::from("UPDATE"))
7034 }
7035 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7036 self.advance();
7037 Ok(alloc::string::String::from("DELETE"))
7038 }
7039 other => Err(self.err(alloc::format!(
7040 "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
7041 ))),
7042 }
7043 }
7044
7045 /// v7.13.0 — parse one trigger event, then optionally consume
7046 /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
7047 /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
7048 fn parse_trigger_event_with_optional_of(
7049 &mut self,
7050 ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
7051 let ev = self.parse_trigger_event()?;
7052 if !matches!(ev, TriggerEvent::Update) {
7053 return Ok((ev, Vec::new()));
7054 }
7055 // `OF` is a bare ident.
7056 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
7057 return Ok((ev, Vec::new()));
7058 }
7059 self.advance(); // OF
7060 let mut cols: Vec<String> = Vec::new();
7061 loop {
7062 cols.push(self.expect_ident_like()?);
7063 if matches!(self.peek(), Token::Comma) {
7064 self.advance();
7065 continue;
7066 }
7067 break;
7068 }
7069 if cols.is_empty() {
7070 return Err(
7071 self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
7072 );
7073 }
7074 Ok((ev, cols))
7075 }
7076
7077 /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
7078 /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
7079 /// before `BEGIN`, and IF / RAISE / embedded SQL statements
7080 /// inside the body.
7081 /// Called by [`parse_plpgsql_body`] after the body's tokens
7082 /// have been lexed into this temporary parser.
7083 pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
7084 // v7.12.6 — optional DECLARE prelude.
7085 let declarations = if matches!(
7086 self.peek(),
7087 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
7088 ) {
7089 self.advance();
7090 self.parse_plpgsql_declare_block()?
7091 } else {
7092 Vec::new()
7093 };
7094 // BEGIN keyword (PL/pgSQL — distinct from the SQL
7095 // `BEGIN` transaction-start, but we can reuse the
7096 // reserved Token::Begin since the body is a separate
7097 // lex/parse context).
7098 if !matches!(self.peek(), Token::Begin) {
7099 return Err(self.err(alloc::format!(
7100 "expected BEGIN at start of plpgsql block, got {:?}",
7101 self.peek()
7102 )));
7103 }
7104 self.advance();
7105 let statements = self.parse_plpgsql_stmt_list_until_end()?;
7106 // v7.37.20 (20.10) — optional EXCEPTION clause between the
7107 // body's last statement and the trailing END. When present
7108 // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
7109 // arms terminated by END.
7110 let exception_handlers = if matches!(
7111 self.peek(),
7112 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
7113 ) {
7114 self.advance();
7115 self.parse_plpgsql_exception_handlers()?
7116 } else {
7117 Vec::new()
7118 };
7119 Ok(PlPgSqlBlock {
7120 declarations,
7121 statements,
7122 exception_handlers,
7123 })
7124 }
7125
7126 /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
7127 /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
7128 fn parse_plpgsql_exception_handlers(
7129 &mut self,
7130 ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
7131 let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
7132 loop {
7133 // Stop at END — the block-level trailing END LOOP / END;
7134 // is handled by the caller.
7135 if matches!(
7136 self.peek(),
7137 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
7138 ) {
7139 return Ok(out);
7140 }
7141 // WHEN <cond> [OR <cond>]* THEN <body>
7142 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7143 {
7144 return Err(self.err(alloc::format!(
7145 "expected WHEN or END inside EXCEPTION clause, got {:?}",
7146 self.peek()
7147 )));
7148 }
7149 self.advance();
7150 let mut conditions: Vec<String> = Vec::new();
7151 conditions.push(self.expect_ident_like()?);
7152 while matches!(self.peek(), Token::Or) {
7153 self.advance();
7154 conditions.push(self.expect_ident_like()?);
7155 }
7156 let then_kw = self.expect_ident_like()?;
7157 if !then_kw.eq_ignore_ascii_case("then") {
7158 return Err(self.err(alloc::format!(
7159 "expected THEN after WHEN condition list, got {then_kw:?}"
7160 )));
7161 }
7162 let body = self.parse_plpgsql_stmt_list_until_end()?;
7163 out.push(crate::ast::ExceptionHandler { conditions, body });
7164 }
7165 }
7166
7167 /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
7168 /// prelude. Caller has already consumed `DECLARE`. We stop
7169 /// reading entries when we hit `BEGIN`.
7170 fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
7171 let mut out: Vec<PlPgSqlDeclare> = Vec::new();
7172 loop {
7173 if matches!(self.peek(), Token::Begin) {
7174 return Ok(out);
7175 }
7176 let name = self.expect_ident_like()?;
7177 // v7.37.20 (20.7) — type inference: if the next token is
7178 // `:=` or `=` (no explicit type), infer from the default
7179 // expression. Otherwise the ident that follows is the
7180 // declared type.
7181 //
7182 // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
7183 // (PG-standard). SPG parse-accepts and treats identically
7184 // to inference — the eventual runtime value determines
7185 // the local's type, which is faithful to how SPG handles
7186 // untyped locals today (see 20.7). Full compile-time
7187 // catalog lookup queues with v7.40 PL/pgSQL epic.
7188 let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
7189 // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
7190 // downstream declaration walker to type the local by
7191 // the runtime type of the default expression.
7192 FunctionArgType::Raw("_infer_".into())
7193 } else {
7194 let ty_token = self.expect_ident_like()?;
7195 // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
7196 // consume optional `.<ident>` qualifier + `%<KW>`
7197 // suffix. Both qualifier and suffix map to _infer_.
7198 if matches!(self.peek(), Token::Dot) {
7199 self.advance();
7200 let _ = self.expect_ident_like()?;
7201 }
7202 if matches!(self.peek(), Token::Percent) {
7203 self.advance();
7204 // Consume the trailing TYPE / ROWTYPE ident.
7205 let _ = self.expect_ident_like()?;
7206 FunctionArgType::Raw("_infer_".into())
7207 } else {
7208 match map_type_ident_to_column_type_name(&ty_token) {
7209 Some(t) => FunctionArgType::Typed(t),
7210 None => FunctionArgType::Raw(ty_token),
7211 }
7212 }
7213 };
7214 let default = match self.peek() {
7215 Token::ColonEq => {
7216 self.advance();
7217 Some(self.parse_expr(0)?)
7218 }
7219 Token::Eq => {
7220 // PL/pgSQL also accepts `=` for the
7221 // DECLARE default (PG treats them the same
7222 // in this position).
7223 self.advance();
7224 Some(self.parse_expr(0)?)
7225 }
7226 _ => None,
7227 };
7228 // Mandatory `;` between declarations.
7229 if !matches!(self.peek(), Token::Semicolon) {
7230 return Err(self.err(alloc::format!(
7231 "expected ; after DECLARE entry for {name:?}, got {:?}",
7232 self.peek()
7233 )));
7234 }
7235 self.advance();
7236 out.push(PlPgSqlDeclare { name, ty, default });
7237 }
7238 }
7239
7240 /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
7241 /// the terminating `END;` (or `END IF;` etc — handled by the
7242 /// per-construct sub-parsers). Used by both the outer block
7243 /// and the IF/ELSE branch bodies.
7244 fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
7245 let mut statements: Vec<PlPgSqlStmt> = Vec::new();
7246 loop {
7247 // Allow trailing semicolons + END.
7248 while matches!(self.peek(), Token::Semicolon) {
7249 self.advance();
7250 }
7251 // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7252 if matches!(
7253 self.peek(),
7254 Token::Ident(s) | Token::QuotedIdent(s)
7255 if s.eq_ignore_ascii_case("end")
7256 || s.eq_ignore_ascii_case("else")
7257 || s.eq_ignore_ascii_case("elsif")
7258 || s.eq_ignore_ascii_case("elseif")
7259 || s.eq_ignore_ascii_case("exception")
7260 || s.eq_ignore_ascii_case("when")
7261 ) {
7262 return Ok(statements);
7263 }
7264 // Otherwise: one statement, then expect `;` or
7265 // a block-terminator keyword.
7266 let stmt = self.parse_plpgsql_stmt()?;
7267 statements.push(stmt);
7268 match self.peek() {
7269 Token::Semicolon => {
7270 self.advance();
7271 }
7272 Token::Ident(s) | Token::QuotedIdent(s)
7273 if s.eq_ignore_ascii_case("end")
7274 || s.eq_ignore_ascii_case("else")
7275 || s.eq_ignore_ascii_case("elsif")
7276 || s.eq_ignore_ascii_case("elseif")
7277 || s.eq_ignore_ascii_case("exception")
7278 || s.eq_ignore_ascii_case("when") =>
7279 {
7280 // Final statement of the block without `;`.
7281 }
7282 other => {
7283 return Err(self.err(alloc::format!(
7284 "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7285 )));
7286 }
7287 }
7288 }
7289 }
7290
7291 fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7292 // RETURN keyword?
7293 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7294 {
7295 self.advance();
7296 return self.parse_plpgsql_return();
7297 }
7298 // v7.12.6 — IF block.
7299 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7300 {
7301 self.advance();
7302 return self.parse_plpgsql_if();
7303 }
7304 // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7305 // Detected by peeking that token pos+3 is Ident("execute").
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!(
7313 self.tokens.get(self.pos + 3),
7314 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7315 )
7316 {
7317 self.advance(); // FOR
7318 let var = self.expect_ident_like()?;
7319 self.advance(); // IN
7320 self.advance(); // EXECUTE
7321 // Prescan for LOOP at paren depth 0 so parse_expr stops
7322 // before the LOOP keyword (same trick as the bare-SELECT
7323 // ForQuery arm).
7324 let mut depth: i32 = 0;
7325 let mut loop_pos: Option<usize> = None;
7326 let mut scan = self.pos;
7327 while scan < self.tokens.len() {
7328 match self.tokens.get(scan) {
7329 Some(Token::LParen) => depth += 1,
7330 Some(Token::RParen) => depth -= 1,
7331 Some(Token::Ident(s) | Token::QuotedIdent(s))
7332 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7333 {
7334 loop_pos = Some(scan);
7335 break;
7336 }
7337 _ => {}
7338 }
7339 scan += 1;
7340 }
7341 let loop_pos = loop_pos.ok_or_else(|| {
7342 self.err(alloc::format!(
7343 "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7344 ))
7345 })?;
7346 let saved_loop = self.tokens[loop_pos].clone();
7347 self.tokens[loop_pos] = Token::Semicolon;
7348 let expr_result = self.parse_expr(0);
7349 self.tokens[loop_pos] = saved_loop;
7350 let sql_expr = expr_result?;
7351 let loop_kw = self.expect_ident_like()?;
7352 if !loop_kw.eq_ignore_ascii_case("loop") {
7353 return Err(self.err(alloc::format!(
7354 "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7355 )));
7356 }
7357 let body = self.parse_plpgsql_stmt_list_until_end()?;
7358 let end_kw = self.expect_ident_like()?;
7359 if !end_kw.eq_ignore_ascii_case("end") {
7360 return Err(self.err(alloc::format!(
7361 "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7362 )));
7363 }
7364 let loop_kw2 = self.expect_ident_like()?;
7365 if !loop_kw2.eq_ignore_ascii_case("loop") {
7366 return Err(self.err(alloc::format!(
7367 "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7368 )));
7369 }
7370 return Ok(PlPgSqlStmt::ForExecute {
7371 var,
7372 sql_expr,
7373 body,
7374 });
7375 }
7376 // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7377 //
7378 // Two syntactic forms:
7379 // FOR var IN SELECT ... ORDER BY ... LOOP ...
7380 // FOR var IN (SELECT ...) LOOP ...
7381 //
7382 // Bare-SELECT form: to keep parse_select_stmt from swallowing
7383 // the trailing `LOOP` keyword as a table alias, we prescan
7384 // forward to find LOOP at paren depth 0, splice a fake
7385 // Semicolon at that position (so SELECT parses cleanly),
7386 // then re-splice LOOP back in.
7387 //
7388 // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7389 // LOOP directly — no scan required.
7390 if matches!(self.peek(), Token::For)
7391 && matches!(
7392 self.tokens.get(self.pos + 1),
7393 Some(Token::Ident(_) | Token::QuotedIdent(_))
7394 )
7395 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7396 && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7397 || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7398 {
7399 self.advance(); // FOR
7400 let var = self.expect_ident_like()?;
7401 // IN
7402 self.advance();
7403 let query = if matches!(self.peek(), Token::LParen) {
7404 // Paren-wrapped SELECT.
7405 self.advance();
7406 let inner = self.parse_select_stmt()?;
7407 let Statement::Select(q) = inner else {
7408 return Err(self.err(alloc::format!(
7409 "expected SELECT inside (…), got {:?}",
7410 self.peek()
7411 )));
7412 };
7413 if !matches!(self.peek(), Token::RParen) {
7414 return Err(self.err(alloc::format!(
7415 "expected ')' after FOR-IN-SELECT body, got {:?}",
7416 self.peek()
7417 )));
7418 }
7419 self.advance();
7420 q
7421 } else {
7422 // Bare SELECT: prescan to find the LOOP boundary.
7423 let mut depth: i32 = 0;
7424 let mut loop_pos: Option<usize> = None;
7425 let mut scan = self.pos;
7426 while scan < self.tokens.len() {
7427 match self.tokens.get(scan) {
7428 Some(Token::LParen) => depth += 1,
7429 Some(Token::RParen) => depth -= 1,
7430 Some(Token::Ident(s) | Token::QuotedIdent(s))
7431 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7432 {
7433 loop_pos = Some(scan);
7434 break;
7435 }
7436 _ => {}
7437 }
7438 scan += 1;
7439 }
7440 let loop_pos = loop_pos.ok_or_else(|| {
7441 self.err(alloc::format!(
7442 "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7443 ))
7444 })?;
7445 // Swap the LOOP token with a synthetic Semicolon so
7446 // parse_select_stmt stops there, then restore afterward.
7447 let saved_loop = self.tokens[loop_pos].clone();
7448 self.tokens[loop_pos] = Token::Semicolon;
7449 let parse_result = self.parse_select_stmt();
7450 self.tokens[loop_pos] = saved_loop;
7451 let inner = parse_result?;
7452 let Statement::Select(q) = inner else {
7453 return Err(self.err(alloc::format!(
7454 "expected SELECT after FOR <var> IN, got {:?}",
7455 self.peek()
7456 )));
7457 };
7458 q
7459 };
7460 let loop_kw = self.expect_ident_like()?;
7461 if !loop_kw.eq_ignore_ascii_case("loop") {
7462 return Err(self.err(alloc::format!(
7463 "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7464 )));
7465 }
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 FOR IN SELECT body, got {end_kw:?}"
7471 )));
7472 }
7473 let loop_kw2 = self.expect_ident_like()?;
7474 if !loop_kw2.eq_ignore_ascii_case("loop") {
7475 return Err(self.err(alloc::format!(
7476 "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7477 )));
7478 }
7479 return Ok(PlPgSqlStmt::ForQuery {
7480 var,
7481 query: Box::new(query),
7482 body,
7483 });
7484 }
7485 // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7486 // FOR is a reserved keyword token (Token::For).
7487 if matches!(self.peek(), Token::For)
7488 && matches!(
7489 self.tokens.get(self.pos + 1),
7490 Some(Token::Ident(_) | Token::QuotedIdent(_))
7491 )
7492 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7493 {
7494 self.advance(); // FOR
7495 let var = self.expect_ident_like()?;
7496 if !matches!(self.peek(), Token::In) {
7497 return Err(self.err(alloc::format!(
7498 "expected IN after FOR <var>, got {:?}",
7499 self.peek()
7500 )));
7501 }
7502 self.advance();
7503 let reverse = matches!(
7504 self.peek(),
7505 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7506 );
7507 if reverse {
7508 self.advance();
7509 }
7510 let start = self.parse_expr(0)?;
7511 if !matches!(self.peek(), Token::DotDot) {
7512 return Err(self.err(alloc::format!(
7513 "expected '..' between FOR loop bounds, got {:?}",
7514 self.peek()
7515 )));
7516 }
7517 self.advance();
7518 let end = self.parse_expr(0)?;
7519 let loop_kw = self.expect_ident_like()?;
7520 if !loop_kw.eq_ignore_ascii_case("loop") {
7521 return Err(self.err(alloc::format!(
7522 "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7523 )));
7524 }
7525 let body = self.parse_plpgsql_stmt_list_until_end()?;
7526 let end_kw = self.expect_ident_like()?;
7527 if !end_kw.eq_ignore_ascii_case("end") {
7528 return Err(self.err(alloc::format!(
7529 "expected END LOOP after FOR body, got {end_kw:?}"
7530 )));
7531 }
7532 let loop_kw2 = self.expect_ident_like()?;
7533 if !loop_kw2.eq_ignore_ascii_case("loop") {
7534 return Err(self.err(alloc::format!(
7535 "expected END LOOP after FOR body, got END {loop_kw2:?}"
7536 )));
7537 }
7538 return Ok(PlPgSqlStmt::ForRange {
7539 var,
7540 start,
7541 end,
7542 reverse,
7543 body,
7544 });
7545 }
7546 // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7547 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7548 {
7549 self.advance();
7550 let body = self.parse_plpgsql_stmt_list_until_end()?;
7551 let end_kw = self.expect_ident_like()?;
7552 if !end_kw.eq_ignore_ascii_case("end") {
7553 return Err(self.err(alloc::format!(
7554 "expected END LOOP after LOOP body, got {end_kw:?}"
7555 )));
7556 }
7557 let loop_kw = self.expect_ident_like()?;
7558 if !loop_kw.eq_ignore_ascii_case("loop") {
7559 return Err(self.err(alloc::format!(
7560 "expected END LOOP after LOOP body, got END {loop_kw:?}"
7561 )));
7562 }
7563 return Ok(PlPgSqlStmt::Loop { body });
7564 }
7565 // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7566 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7567 {
7568 self.advance();
7569 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7570 {
7571 self.advance();
7572 Some(self.parse_expr(0)?)
7573 } else {
7574 None
7575 };
7576 return Ok(PlPgSqlStmt::Exit { when });
7577 }
7578 // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7579 // already-parsed Statement or a runtime-computed SQL string.
7580 // The disambiguator vs the extended-query-protocol `EXECUTE
7581 // <stmt_name>` (which is a top-level Statement, not a
7582 // plpgsql line) is that inside a DO block / trigger body the
7583 // EXECUTE keyword ALWAYS refers to dynamic SQL.
7584 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7585 {
7586 self.advance();
7587 let sql = self.parse_expr(0)?;
7588 return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7589 }
7590 // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7591 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7592 {
7593 self.advance();
7594 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7595 {
7596 self.advance();
7597 Some(self.parse_expr(0)?)
7598 } else {
7599 None
7600 };
7601 return Ok(PlPgSqlStmt::Continue { when });
7602 }
7603 // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7604 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7605 {
7606 self.advance();
7607 let condition = self.parse_expr(0)?;
7608 let loop_kw = self.expect_ident_like()?;
7609 if !loop_kw.eq_ignore_ascii_case("loop") {
7610 return Err(self.err(alloc::format!(
7611 "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7612 )));
7613 }
7614 let body = self.parse_plpgsql_stmt_list_until_end()?;
7615 // Expect END LOOP.
7616 let end_kw = self.expect_ident_like()?;
7617 if !end_kw.eq_ignore_ascii_case("end") {
7618 return Err(self.err(alloc::format!(
7619 "expected END LOOP after WHILE body, got {end_kw:?}"
7620 )));
7621 }
7622 let loop_kw2 = self.expect_ident_like()?;
7623 if !loop_kw2.eq_ignore_ascii_case("loop") {
7624 return Err(self.err(alloc::format!(
7625 "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7626 )));
7627 }
7628 return Ok(PlPgSqlStmt::While { condition, body });
7629 }
7630 // v7.12.6 — RAISE.
7631 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7632 {
7633 self.advance();
7634 return self.parse_plpgsql_raise();
7635 }
7636 // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7637 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7638 {
7639 self.advance();
7640 let condition = self.parse_expr(0)?;
7641 let message = if matches!(self.peek(), Token::Comma) {
7642 self.advance();
7643 Some(self.parse_expr(0)?)
7644 } else {
7645 None
7646 };
7647 return Ok(PlPgSqlStmt::Assert { condition, message });
7648 }
7649 // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7650 // "PERFORM is equivalent to SELECT but discards the
7651 // result." Side effects (function calls, RAISE inside
7652 // SQL functions, etc.) still execute. We desugar to
7653 // `SELECT <body>` and wrap in EmbeddedSql so the engine's
7654 // existing embedded-statement path handles execution +
7655 // result-discard cleanly. The result is naturally
7656 // discarded because EmbeddedSql doesn't propagate row
7657 // sets back to the plpgsql interpreter.
7658 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7659 {
7660 self.advance();
7661 // Splice a synthetic Token::Select into the stream at
7662 // the current position so parse_select_stmt parses the
7663 // remainder as a normal SELECT body. Token-stream
7664 // surgery mirrors the try_parse_plpgsql_select_into
7665 // pattern used for SELECT … INTO desugaring.
7666 self.tokens.insert(self.pos, Token::Select);
7667 let select = self.parse_select_stmt()?;
7668 let Statement::Select(s) = select else {
7669 return Err(self.err(alloc::format!(
7670 "expected SELECT body after PERFORM, got {:?}",
7671 self.peek()
7672 )));
7673 };
7674 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7675 }
7676 // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7677 // plpgsql-specific shape (mailrs round-10 migrate-042).
7678 // PG's SELECT INTO at top-level SQL would CREATE a new
7679 // table; inside plpgsql it ASSIGNS the query result to
7680 // a local variable. We detect the INTO at paren-depth
7681 // 0 between SELECT and the statement boundary; if
7682 // found, split the token stream into "pre-INTO
7683 // projection" + "var" + "post-INTO FROM/WHERE…" and
7684 // rebuild as a SelectInto with a regular SELECT body
7685 // (no INTO clause).
7686 if matches!(self.peek(), Token::Select)
7687 && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7688 {
7689 return Ok(PlPgSqlStmt::SelectInto {
7690 var: var_name,
7691 body: Box::new(select_body),
7692 });
7693 }
7694 // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7695 // SELECT can appear directly inside a trigger body; we
7696 // recurse into the regular Statement parser, which will
7697 // stop at the trailing `;` (which our caller then
7698 // consumes).
7699 // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7700 // also embed ALTER / CREATE / DROP statements; route
7701 // those through the same parser so the DO body parses
7702 // cleanly.
7703 if matches!(self.peek(), Token::Insert)
7704 || matches!(self.peek(), Token::Select)
7705 || matches!(self.peek(), Token::Create)
7706 || matches!(self.peek(), Token::Drop)
7707 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7708 if s.eq_ignore_ascii_case("update")
7709 || s.eq_ignore_ascii_case("delete")
7710 || s.eq_ignore_ascii_case("alter"))
7711 {
7712 let stmt = self.parse_one_statement()?;
7713 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7714 }
7715 // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7716 // followed by `:=` and an expression.
7717 let target = self.parse_plpgsql_assign_target()?;
7718 // PL/pgSQL assignment uses `:=`. The lexer represents
7719 // this as a colon followed by `=`; check both shapes.
7720 match self.peek() {
7721 Token::ColonEq => {
7722 self.advance();
7723 }
7724 Token::Colon => {
7725 self.advance();
7726 if !matches!(self.peek(), Token::Eq) {
7727 return Err(self.err(alloc::format!(
7728 "expected := after plpgsql assign target, got `:` then {:?}",
7729 self.peek()
7730 )));
7731 }
7732 self.advance();
7733 }
7734 other => {
7735 return Err(self.err(alloc::format!(
7736 "expected := after plpgsql assign target, got {other:?}"
7737 )));
7738 }
7739 }
7740 let value = self.parse_expr(0)?;
7741 Ok(PlPgSqlStmt::Assign { target, value })
7742 }
7743
7744 /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7745 /// [ELSE body] END IF`. `IF` keyword already consumed.
7746 fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7747 let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7748 let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7749 loop {
7750 // <expr> THEN
7751 let cond = self.parse_expr(0)?;
7752 let then_kw = self.expect_ident_like()?;
7753 if !then_kw.eq_ignore_ascii_case("then") {
7754 return Err(self.err(alloc::format!(
7755 "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7756 )));
7757 }
7758 let body = self.parse_plpgsql_stmt_list_until_end()?;
7759 branches.push((cond, body));
7760 // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7761 match self.peek() {
7762 Token::Ident(s) | Token::QuotedIdent(s)
7763 if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7764 {
7765 self.advance();
7766 continue;
7767 }
7768 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7769 self.advance();
7770 else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7771 break;
7772 }
7773 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7774 break;
7775 }
7776 other => {
7777 return Err(self.err(alloc::format!(
7778 "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7779 )));
7780 }
7781 }
7782 }
7783 // Expect `END IF` (the END keyword is the one we're
7784 // looking at right now).
7785 let end_kw = self.expect_ident_like()?;
7786 if !end_kw.eq_ignore_ascii_case("end") {
7787 return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7788 }
7789 let if_kw = self.expect_ident_like()?;
7790 if !if_kw.eq_ignore_ascii_case("if") {
7791 return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7792 }
7793 Ok(PlPgSqlStmt::If {
7794 branches,
7795 else_branch,
7796 })
7797 }
7798
7799 /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7800 /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7801 /// is already consumed.
7802 fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7803 let lvl_ident = self.expect_ident_like()?;
7804 let level = match lvl_ident.to_ascii_lowercase().as_str() {
7805 "notice" => RaiseLevel::Notice,
7806 "warning" => RaiseLevel::Warning,
7807 "info" => RaiseLevel::Info,
7808 "log" => RaiseLevel::Log,
7809 "debug" => RaiseLevel::Debug,
7810 "exception" => RaiseLevel::Exception,
7811 other => {
7812 return Err(self.err(alloc::format!(
7813 "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7814 )));
7815 }
7816 };
7817 // Message: required for v7.12.6. PG accepts a bare
7818 // RAISE-rethrow form (no message), reserved for future
7819 // RAISE-no-args support.
7820 let Token::String(msg) = self.peek() else {
7821 return Err(self.err(alloc::format!(
7822 "expected RAISE message string, got {:?}",
7823 self.peek()
7824 )));
7825 };
7826 let message = msg.clone();
7827 self.advance();
7828 // Optional comma-separated args (PG `%` format substitution).
7829 let mut args: Vec<Expr> = Vec::new();
7830 while matches!(self.peek(), Token::Comma) {
7831 self.advance();
7832 args.push(self.parse_expr(0)?);
7833 }
7834 Ok(PlPgSqlStmt::Raise {
7835 level,
7836 message,
7837 args,
7838 })
7839 }
7840
7841 /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7842 /// <projection> INTO <var> [FROM …]` (mailrs round-10
7843 /// migrate-042). Returns `(rebuilt_select_without_into,
7844 /// var_name)` when the pattern matches; `None` for
7845 /// regular SELECTs (those go through the embedded-SQL
7846 /// path). Token-stream surgery so the rebuilt SELECT
7847 /// parses through the regular `parse_select_stmt`.
7848 #[allow(clippy::too_many_lines)]
7849 fn try_parse_plpgsql_select_into(
7850 &mut self,
7851 ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7852 // Scan forward from `self.pos + 1` (past Token::Select)
7853 // for Token::Into at paren-depth 0, stopping at the
7854 // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7855 // end the plpgsql statement.
7856 let start = self.pos;
7857 let mut into_pos: Option<usize> = None;
7858 let mut depth: i32 = 0;
7859 let mut i = start + 1;
7860 while i < self.tokens.len() {
7861 match &self.tokens[i] {
7862 Token::LParen => depth += 1,
7863 Token::RParen => depth -= 1,
7864 Token::Semicolon if depth == 0 => break,
7865 Token::Ident(s)
7866 if depth == 0
7867 && (s.eq_ignore_ascii_case("end")
7868 || s.eq_ignore_ascii_case("else")
7869 || s.eq_ignore_ascii_case("elsif")) =>
7870 {
7871 break;
7872 }
7873 Token::Into if depth == 0 => {
7874 into_pos = Some(i);
7875 break;
7876 }
7877 _ => {}
7878 }
7879 i += 1;
7880 }
7881 let Some(into_at) = into_pos else {
7882 return Ok(None);
7883 };
7884 // The token immediately after INTO must be the target
7885 // var ident; anything else (e.g. INSERT INTO table)
7886 // ruled out by the depth-0 check above. Capture it.
7887 let var = match self.tokens.get(into_at + 1) {
7888 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7889 other => {
7890 return Err(self.err(alloc::format!(
7891 "expected variable name after SELECT … INTO, got {other:?}"
7892 )));
7893 }
7894 };
7895 // Find the end of the plpgsql SELECT INTO statement —
7896 // same boundary rules as the depth-0 scan above.
7897 let mut end = into_at + 2;
7898 let mut depth2: i32 = 0;
7899 while end < self.tokens.len() {
7900 match &self.tokens[end] {
7901 Token::LParen => depth2 += 1,
7902 Token::RParen => depth2 -= 1,
7903 Token::Semicolon if depth2 == 0 => break,
7904 Token::Ident(s)
7905 if depth2 == 0
7906 && (s.eq_ignore_ascii_case("end")
7907 || s.eq_ignore_ascii_case("else")
7908 || s.eq_ignore_ascii_case("elsif")) =>
7909 {
7910 break;
7911 }
7912 _ => {}
7913 }
7914 end += 1;
7915 }
7916 // Rebuild a token stream that represents the SELECT
7917 // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
7918 // post-var tokens up to statement end]. Run the
7919 // regular `parse_select_stmt` against it.
7920 let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
7921 for j in start..into_at {
7922 rebuilt.push(self.tokens[j].clone());
7923 }
7924 for j in (into_at + 2)..end {
7925 rebuilt.push(self.tokens[j].clone());
7926 }
7927 rebuilt.push(Token::Eof);
7928 let saved_pos = self.pos;
7929 let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
7930 self.pos = 0;
7931 // parse_select_stmt → parse_bare_select consumes Token::Select itself.
7932 if !matches!(self.peek(), Token::Select) {
7933 self.tokens = saved_tokens;
7934 self.pos = saved_pos;
7935 return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
7936 }
7937 let sel = self.parse_select_stmt();
7938 self.tokens = saved_tokens;
7939 self.pos = end;
7940 let sel = sel?;
7941 let Statement::Select(body) = sel else {
7942 return Err(self.err(alloc::format!(
7943 "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
7944 )));
7945 };
7946 Ok(Some((body, var)))
7947 }
7948
7949 fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
7950 // v7.16.1 — read the head token DIRECTLY rather than
7951 // via `expect_ident_like`. The v7.14.0 schema-qualifier
7952 // strip (`public.t` → `t`) inside `expect_ident_like`
7953 // greedily consumes any `ident . ident` pair, which
7954 // silently turned every `NEW.col := …` /
7955 // `OLD.col := …` plpgsql assignment into a Local("col")
7956 // assignment — the head "new"/"old" was eaten as if it
7957 // were a schema name and the Dot was consumed too, so
7958 // this function's own `peek() == Token::Dot` check
7959 // below never fired. Every BEFORE trigger that rewrote
7960 // a NEW cell was a silent no-op for two major releases
7961 // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
7962 // gate failures were investigated as v7.16.1 backlog.
7963 let head = match self.advance() {
7964 Token::Ident(s) | Token::QuotedIdent(s) => s,
7965 other => {
7966 return Err(self.err(alloc::format!(
7967 "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
7968 )));
7969 }
7970 };
7971 if matches!(self.peek(), Token::Dot) {
7972 self.advance();
7973 let col = self.expect_ident_like()?;
7974 if head.eq_ignore_ascii_case("new") {
7975 return Ok(AssignTarget::NewColumn(col));
7976 }
7977 if head.eq_ignore_ascii_case("old") {
7978 return Ok(AssignTarget::OldColumn(col));
7979 }
7980 return Err(self.err(alloc::format!(
7981 "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
7982 got {head:?}.<col>"
7983 )));
7984 }
7985 Ok(AssignTarget::Local(head))
7986 }
7987
7988 fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7989 // RETURN NEW / OLD / NULL — bare-ident forms.
7990 match self.peek() {
7991 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
7992 self.advance();
7993 return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
7994 }
7995 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
7996 self.advance();
7997 return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
7998 }
7999 Token::Null => {
8000 self.advance();
8001 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8002 }
8003 // Bare `RETURN;` (no value) — treated as `RETURN NULL`
8004 // per PL/pgSQL convention.
8005 Token::Semicolon => {
8006 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8007 }
8008 _ => {}
8009 }
8010 // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
8011 // EXECUTE <expr>. In a DO block context RETURN QUERY has no
8012 // caller-visible effect (blocks don't return sets), so we
8013 // desugar it identically to PERFORM: parse the SELECT (or
8014 // EXECUTE dynamic) as embedded SQL that runs for side
8015 // effects and discards the result. RETURN NEXT <expr>
8016 // (single-row accumulator) queues with v7.40 SETOF function
8017 // infrastructure.
8018 // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
8019 // and keep going.
8020 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
8021 {
8022 self.advance();
8023 let e = self.parse_expr(0)?;
8024 return Ok(PlPgSqlStmt::ReturnNext(e));
8025 }
8026 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
8027 {
8028 self.advance();
8029 // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
8030 // rows go to the set, like the static form. It used to desugar to a
8031 // bare ExecuteDynamic, whose result was DISCARDED.
8032 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
8033 {
8034 self.advance();
8035 let sql = self.parse_expr(0)?;
8036 return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
8037 }
8038 // Bare RETURN QUERY <select>. If the current token is
8039 // not already SELECT (e.g., the user wrote `RETURN QUERY
8040 // <projection> FROM ...` in a shorthand — rare but PG
8041 // accepts a bare projection here), splice one in. Same
8042 // trick as PERFORM.
8043 if !matches!(self.peek(), Token::Select) {
8044 self.tokens.insert(self.pos, Token::Select);
8045 }
8046 let select = self.parse_select_stmt()?;
8047 let Statement::Select(s) = select else {
8048 return Err(self.err(alloc::format!(
8049 "expected SELECT body after RETURN QUERY, got {:?}",
8050 self.peek()
8051 )));
8052 };
8053 // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
8054 // to an embedded side-effect SELECT whose rows were DISCARDED, which
8055 // in a SETOF function is the entire answer thrown away.
8056 return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
8057 }
8058 // Fall through: parse a full expression.
8059 let e = self.parse_expr(0)?;
8060 Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
8061 }
8062
8063 fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
8064 // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
8065 // are ident-shaped (the parser keys off case-insensitive
8066 // match — same shape used by the top-level Update / Delete
8067 // dispatchers at parse_one_statement).
8068 if matches!(self.peek(), Token::Insert) {
8069 self.advance();
8070 return Ok(TriggerEvent::Insert);
8071 }
8072 match self.peek() {
8073 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
8074 self.advance();
8075 Ok(TriggerEvent::Update)
8076 }
8077 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
8078 self.advance();
8079 Ok(TriggerEvent::Delete)
8080 }
8081 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
8082 self.advance();
8083 Ok(TriggerEvent::Truncate)
8084 }
8085 other => Err(self.err(alloc::format!(
8086 "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
8087 ))),
8088 }
8089 }
8090
8091 /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
8092 /// - (no clause) → implicit `FOR ALL TABLES`
8093 /// - `FOR ALL TABLES`
8094 /// - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
8095 /// - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
8096 /// accepted as an SPG lenience. PG18-measured (round 753): PG
8097 /// REJECTS the bare plural (`invalid publication object list`,
8098 /// TABLES only pairs with IN SCHEMA); the old note claimed an
8099 /// unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
8100 fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
8101 let name = self.expect_ident_or_string()?;
8102 // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
8103 // shape so existing publications keep parsing identically.
8104 let scope = if matches!(self.peek(), Token::For) {
8105 self.advance();
8106 if matches!(self.peek(), Token::All) {
8107 self.advance();
8108 if !matches!(self.peek(), Token::Tables) {
8109 return Err(self.err(format!(
8110 "expected TABLES after FOR ALL, got {:?}",
8111 self.peek()
8112 )));
8113 }
8114 self.advance();
8115 if matches!(self.peek(), Token::Except) {
8116 self.advance();
8117 let tables = self.parse_publication_table_list()?;
8118 PublicationScope::AllTablesExcept(tables)
8119 } else {
8120 PublicationScope::AllTables
8121 }
8122 } else if matches!(self.peek(), Token::Table) {
8123 self.advance();
8124 let tables = self.parse_publication_table_list()?;
8125 PublicationScope::ForTables(tables)
8126 } else if matches!(self.peek(), Token::Tables) {
8127 // v7.39 (round 754, F31-B5) — PG18-measured: the bare
8128 // plural (`FOR TABLES t`) is REJECTED (`invalid
8129 // publication object list`); TABLES only pairs with
8130 // `IN SCHEMA`. The old arm accepted it on an
8131 // unverifiable "PG 19 accepts both" claim.
8132 self.advance();
8133 if !matches!(self.peek(), Token::In) {
8134 return Err(self.err(alloc::string::String::from(
8135 "invalid publication object list",
8136 )));
8137 }
8138 self.advance();
8139 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
8140 return Err(self.err(format!(
8141 "expected SCHEMA after FOR TABLES IN, got {:?}",
8142 self.peek()
8143 )));
8144 }
8145 self.advance();
8146 let schema = self.expect_ident_or_string()?;
8147 PublicationScope::TablesInSchema(schema)
8148 } else {
8149 return Err(self.err(format!(
8150 "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
8151 self.peek()
8152 )));
8153 }
8154 } else {
8155 PublicationScope::AllTables
8156 };
8157 Ok(Statement::CreatePublication(CreatePublicationStatement {
8158 name,
8159 scope,
8160 }))
8161 }
8162
8163 /// v6.1.3 — Comma-separated identifier list for the publication
8164 /// FOR-clause. Requires at least one entry; empty list is a
8165 /// parse error (PG behaviour). Quoted idents are accepted; the
8166 /// names round-trip through `Display` as `quote_ident(name)`.
8167 ///
8168 /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
8169 /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
8170 /// pg_dump output. SPG's publication state today is per-table
8171 /// only (matching the pre-PG-15 surface); the col list + WHERE
8172 /// are parsed so dumps load through and the table name reaches
8173 /// `PublicationScope::ForTables`, but the filter is not enforced
8174 /// at publish time. Re-open when a customer dogfood gate
8175 /// requires per-row-filter or column-subset publish semantics
8176 /// (which gates on persistent slot state landing first, 21.12).
8177 fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
8178 let first = self.parse_publication_table_entry()?;
8179 let mut out = alloc::vec![first];
8180 while matches!(self.peek(), Token::Comma) {
8181 self.advance();
8182 out.push(self.parse_publication_table_entry()?);
8183 }
8184 Ok(out)
8185 }
8186
8187 /// One table entry inside a FOR TABLE clause:
8188 /// tab_name [ (col, col, …) ] [ WHERE (predicate) ]
8189 /// Returns just the table name; the column list + WHERE predicate
8190 /// are consumed and discarded per the parse-accept-discard
8191 /// commitment above.
8192 fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
8193 let name = self.expect_ident_like()?;
8194 // Optional column list — `(col, col, …)`.
8195 if matches!(self.peek(), Token::LParen) {
8196 self.advance();
8197 // Empty parens are a PG error too; require ≥ 1 column.
8198 let _ = self.expect_ident_like()?;
8199 while matches!(self.peek(), Token::Comma) {
8200 self.advance();
8201 let _ = self.expect_ident_like()?;
8202 }
8203 if !matches!(self.peek(), Token::RParen) {
8204 return Err(self.err(alloc::format!(
8205 "expected ')' to close publication column list, got {:?}",
8206 self.peek()
8207 )));
8208 }
8209 self.advance();
8210 }
8211 // Optional row filter — `WHERE (predicate)`.
8212 if matches!(self.peek(), Token::Where) {
8213 self.advance();
8214 if !matches!(self.peek(), Token::LParen) {
8215 return Err(self.err(alloc::format!(
8216 "expected '(' after WHERE in publication row filter, got {:?}",
8217 self.peek()
8218 )));
8219 }
8220 self.advance();
8221 let _ = self.parse_expr(0)?;
8222 if !matches!(self.peek(), Token::RParen) {
8223 return Err(self.err(alloc::format!(
8224 "expected ')' to close publication WHERE filter, got {:?}",
8225 self.peek()
8226 )));
8227 }
8228 self.advance();
8229 }
8230 Ok(name)
8231 }
8232
8233 /// v6.1.4 — `CREATE SUBSCRIPTION <name>
8234 /// CONNECTION '<conn>'
8235 /// PUBLICATION <pub> [, <pub> ...]`.
8236 ///
8237 /// The clause order is fixed (CONNECTION first, then
8238 /// PUBLICATION) to match PG. No WITH-options accepted in
8239 /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
8240 fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
8241 let name = self.expect_ident_or_string()?;
8242 if !matches!(self.peek(), Token::Connection) {
8243 return Err(self.err(format!(
8244 "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
8245 self.peek()
8246 )));
8247 }
8248 self.advance();
8249 let conn_str = self.expect_string_literal()?;
8250 if !matches!(self.peek(), Token::Publication) {
8251 return Err(self.err(format!(
8252 "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8253 self.peek()
8254 )));
8255 }
8256 self.advance();
8257 // Reuse the publication FOR-list parser shape: at least one
8258 // identifier, comma-separated.
8259 let first = self.expect_ident_like()?;
8260 let mut publications = alloc::vec![first];
8261 while matches!(self.peek(), Token::Comma) {
8262 self.advance();
8263 publications.push(self.expect_ident_like()?);
8264 }
8265 Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8266 name,
8267 conn_str,
8268 publications,
8269 }))
8270 }
8271
8272 /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8273 /// All keywords after `WAIT` are bare idents in v6.1.x; no
8274 /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8275 /// that fit `u64`.
8276 /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8277 /// qualifier is a *namespace* the app owns (`app.user_id`,
8278 /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8279 /// to discard. So parse the raw segments here instead of
8280 /// `expect_ident_like`, which strips a leading `schema.` qualifier
8281 /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8282 /// a single segment and round-trip unchanged.
8283 fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8284 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8285 loop {
8286 let seg = match self.advance() {
8287 Token::Ident(s) | Token::QuotedIdent(s) => s,
8288 other if unreserved_keyword_text(&other).is_some() => {
8289 unreserved_keyword_text(&other).unwrap()
8290 }
8291 other => {
8292 return Err(ParseError {
8293 message: format!("expected parameter name, got {other:?}"),
8294 token_pos: self.consumed_pos(),
8295 });
8296 }
8297 };
8298 parts.push(seg);
8299 if matches!(self.peek(), Token::Dot) {
8300 self.advance();
8301 continue;
8302 }
8303 break;
8304 }
8305 Ok(parts.join(".").to_ascii_lowercase())
8306 }
8307
8308 fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8309 Self::parse_set_value_inner(self)
8310 }
8311
8312 fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8313 match self.advance() {
8314 Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8315 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8316 Ok(crate::ast::SetValue::Default)
8317 }
8318 Token::Ident(s) | Token::QuotedIdent(s) => {
8319 let mut accum = s;
8320 while matches!(self.peek(), Token::Dot) {
8321 self.advance();
8322 let next = self.expect_ident_like()?;
8323 accum.push('.');
8324 accum.push_str(&next);
8325 }
8326 Ok(crate::ast::SetValue::Ident(accum))
8327 }
8328 Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8329 Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8330 // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8331 // spellings that lex as keyword tokens, not idents:
8332 // `SET standard_conforming_strings = on` is in every
8333 // pg_dump preamble (`off` already lexes as an ident).
8334 // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8335 // DEFAULT lexes as its keyword token, so the ident arm above
8336 // never saw it and the everyday reset form was a syntax error.
8337 Token::Default => Ok(crate::ast::SetValue::Default),
8338 Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8339 Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8340 Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8341 // v7.14.0 — MySQL session/user variable RHS
8342 // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8343 // Wrap as Ident so the SET handler can record it; the
8344 // engine treats `@VAR` / `@@VAR` values as opaque
8345 // strings.
8346 Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8347 // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8348 // is the common MySQL preamble shape. Allow a `+` or
8349 // `-` prefix on negative numerics for parity with PG
8350 // (some param defaults are negative).
8351 Token::Minus => match self.advance() {
8352 Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8353 Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8354 other => Err(self.err(format!(
8355 "expected numeric after `-` in SET value, got {other:?}"
8356 ))),
8357 },
8358 other => Err(self.err(format!(
8359 "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8360 ))),
8361 }
8362 }
8363
8364 /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8365 /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8366 /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8367 /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8368 /// present). Modes are comma-separated per PG; SPG also
8369 /// accepts space-separated for tolerance. READ ONLY / WRITE
8370 /// / DEFERRABLE are parsed-and-ignored (recorded for future
8371 /// surface but not behaviorally honoured today).
8372 /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8373 /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8374 /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8375 /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8376 /// session default rather than forcing READ COMMITTED.
8377 fn parse_isolation_level_clauses(
8378 &mut self,
8379 ) -> Result<crate::ast::TransactionModes, ParseError> {
8380 let mut level = IsolationLevel::default();
8381 let mut have_level = false;
8382 // v7.39 — READ ONLY / READ WRITE used to be consumed and dropped,
8383 // so `BEGIN READ ONLY` opened an ordinary read-write transaction.
8384 let mut read_only: Option<bool> = None;
8385 loop {
8386 // ISOLATION LEVEL …
8387 let saw_isolation =
8388 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8389 if saw_isolation {
8390 self.advance(); // ISOLATION
8391 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8392 return Err(self.err(alloc::format!(
8393 "expected LEVEL after ISOLATION, got {:?}",
8394 self.peek()
8395 )));
8396 }
8397 self.advance(); // LEVEL
8398 // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8399 let w1 = self
8400 .expect_ident_like()
8401 .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8402 let lc = w1.to_ascii_lowercase();
8403 level = match lc.as_str() {
8404 "serializable" => IsolationLevel::Serializable,
8405 "repeatable" => {
8406 // Expect READ
8407 let w2 = self
8408 .expect_ident_like()
8409 .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8410 if !w2.eq_ignore_ascii_case("read") {
8411 return Err(self.err(alloc::format!(
8412 "expected READ after REPEATABLE, got {w2:?}"
8413 )));
8414 }
8415 IsolationLevel::RepeatableRead
8416 }
8417 "read" => {
8418 let w2 = self
8419 .expect_ident_like()
8420 .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8421 match w2.to_ascii_lowercase().as_str() {
8422 "committed" => IsolationLevel::ReadCommitted,
8423 "uncommitted" => IsolationLevel::ReadUncommitted,
8424 other => {
8425 return Err(self.err(alloc::format!(
8426 "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8427 )));
8428 }
8429 }
8430 }
8431 other => {
8432 return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8433 }
8434 };
8435 have_level = true;
8436 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8437 // v7.39 — READ ONLY | READ WRITE. The comment here used to
8438 // read "parsed, not behaviorally honoured", and it was
8439 // accurate: the clause was thrown away, so `BEGIN READ ONLY`
8440 // opened an ordinary read-write transaction and accepted
8441 // every write in it.
8442 self.advance();
8443 match self.peek().clone() {
8444 Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8445 self.advance();
8446 read_only = Some(true);
8447 }
8448 Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8449 self.advance();
8450 read_only = Some(false);
8451 }
8452 other => {
8453 return Err(self.err(alloc::format!(
8454 "expected ONLY or WRITE after READ, got {other:?}"
8455 )));
8456 }
8457 }
8458 } else if matches!(self.peek(), Token::Not) {
8459 // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8460 self.advance();
8461 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8462 return Err(self.err(alloc::format!(
8463 "expected DEFERRABLE after NOT, got {:?}",
8464 self.peek()
8465 )));
8466 }
8467 self.advance();
8468 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8469 {
8470 self.advance();
8471 } else {
8472 break;
8473 }
8474 // Optional comma between modes.
8475 if matches!(self.peek(), Token::Comma) {
8476 self.advance();
8477 }
8478 }
8479 Ok(crate::ast::TransactionModes {
8480 isolation: have_level.then_some(level),
8481 read_only,
8482 })
8483 }
8484
8485 fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8486 // FOR is a v6.1.2-reserved keyword (Token::For). The
8487 // other two are bare idents — they've never needed lexer
8488 // support and we keep it that way.
8489 if !matches!(self.peek(), Token::For) {
8490 return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8491 }
8492 self.advance();
8493 self.expect_keyword_ident("wal")?;
8494 self.expect_keyword_ident("position")?;
8495 let pos = self.expect_u64_literal()?;
8496 let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8497 {
8498 self.advance();
8499 self.expect_keyword_ident("timeout")?;
8500 Some(self.expect_u64_literal()?)
8501 } else {
8502 None
8503 };
8504 Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8505 }
8506
8507 /// v6.1.7 helper — consume a `Token::Integer` and check it
8508 /// fits `u64`. WAL positions and millisecond timeouts are
8509 /// non-negative.
8510 fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8511 match self.advance() {
8512 Token::Integer(n) if n >= 0 => Ok(n as u64),
8513 Token::Integer(n) => Err(ParseError {
8514 message: format!("expected non-negative integer, got {n}"),
8515 token_pos: self.consumed_pos(),
8516 }),
8517 other => Err(ParseError {
8518 message: format!("expected integer literal, got {other:?}"),
8519 token_pos: self.consumed_pos(),
8520 }),
8521 }
8522 }
8523
8524 /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8525 /// ROLE '<role>' (defaults to readonly). All string slots accept
8526 /// either a quoted ident or a quoted string literal.
8527 /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8528 /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8529 ///
8530 /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8531 /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8532 /// wire role) still parses — it is a different axis from the PG attributes.
8533 /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8534 /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8535 /// or RESET, so the plain attribute forms keep their old path.
8536 fn peeks_db_role_setting(&self) -> bool {
8537 let mut i = self.pos + 1; // past the object's name
8538 let word = |p: usize| -> Option<String> {
8539 match self.tokens.get(p) {
8540 Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8541 Some(Token::In) => Some(String::from("in")),
8542 _ => None,
8543 }
8544 };
8545 if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8546 i += 3; // IN DATABASE <name>
8547 }
8548 matches!(word(i).as_deref(), Some("set" | "reset"))
8549 }
8550
8551 fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8552 use crate::ast::SetDbRoleSettingStatement;
8553 // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8554 // identifier, so the ordinary name reader refuses it. Same trap
8555 // as TABLE / INDEX / FULL / DEFAULT before it.
8556 let name = if matches!(self.peek(), Token::All) {
8557 self.advance();
8558 String::from("all")
8559 } else {
8560 self.expect_ident_or_string()?
8561 };
8562 // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8563 let all = name.eq_ignore_ascii_case("all");
8564 let (mut database, mut role) = if is_database {
8565 (Some(name), None)
8566 } else if all {
8567 (None, None)
8568 } else {
8569 (None, Some(name))
8570 };
8571 if matches!(self.peek(), Token::In) {
8572 self.advance();
8573 self.advance(); // DATABASE
8574 database = Some(self.expect_ident_or_string()?);
8575 }
8576 let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8577 self.advance(); // SET | RESET
8578 if resetting && matches!(self.peek(), Token::All) {
8579 self.advance();
8580 self.consume_until_statement_boundary();
8581 return Ok(Statement::SetDbRoleSetting(Box::new(
8582 SetDbRoleSettingStatement {
8583 database,
8584 role,
8585 param: None,
8586 value: None,
8587 },
8588 )));
8589 }
8590 let param = self.expect_ident_like()?;
8591 let value = if resetting {
8592 None
8593 } else {
8594 // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8595 // KEYWORD, so the ident-only check missed it and consumed
8596 // the word itself as the value — the same trap as ALL, one
8597 // clause over.
8598 if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8599 self.advance();
8600 }
8601 Some(self.take_guc_value())
8602 };
8603 self.consume_until_statement_boundary();
8604 Ok(Statement::SetDbRoleSetting(Box::new(
8605 SetDbRoleSettingStatement {
8606 database,
8607 role,
8608 param: Some(param),
8609 value,
8610 },
8611 )))
8612 }
8613
8614 /// The remainder of a `SET <p> = …` clause as PG renders it back:
8615 /// a quoted literal loses its quotes, a bare word or number does not.
8616 fn take_guc_value(&mut self) -> String {
8617 match self.advance() {
8618 Token::String(s) => s,
8619 Token::Integer(n) => format!("{n}"),
8620 Token::Float(f) => format!("{f}"),
8621 Token::Ident(s) | Token::QuotedIdent(s) => s,
8622 other => format!("{other:?}"),
8623 }
8624 }
8625
8626 fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8627 let name = self.expect_ident_or_string()?;
8628 if self.peek_keyword_ident("with") {
8629 self.advance();
8630 }
8631 let mut password = String::new();
8632 let mut role = String::new();
8633 let mut login: Option<bool> = None;
8634 let mut inherit: Option<bool> = None;
8635 let mut superuser: Option<bool> = None;
8636 // Not a `while let`: the pattern would borrow `self` across the
8637 // body, which calls `self.advance()` / `self.expect_*` (&mut).
8638 #[allow(clippy::while_let_loop)]
8639 loop {
8640 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8641 break;
8642 };
8643 match w.to_ascii_lowercase().as_str() {
8644 "password" => {
8645 self.advance();
8646 password = self.expect_string_literal()?;
8647 }
8648 // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8649 // is the same slot.
8650 "encrypted" => {
8651 self.advance();
8652 self.expect_keyword_ident("password")?;
8653 password = self.expect_string_literal()?;
8654 }
8655 "login" => {
8656 self.advance();
8657 login = Some(true);
8658 }
8659 "nologin" => {
8660 self.advance();
8661 login = Some(false);
8662 }
8663 "inherit" => {
8664 self.advance();
8665 inherit = Some(true);
8666 }
8667 "noinherit" => {
8668 self.advance();
8669 inherit = Some(false);
8670 }
8671 "superuser" => {
8672 self.advance();
8673 superuser = Some(true);
8674 }
8675 "nosuperuser" => {
8676 self.advance();
8677 superuser = Some(false);
8678 }
8679 // SPG's own coarse wire role: `ROLE 'readwrite'`.
8680 "role" => {
8681 self.advance();
8682 role = self.expect_string_literal()?;
8683 }
8684 // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8685 // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8686 // accepted and ignored so a pg_dump role block restores. They
8687 // gate capabilities SPG does not have.
8688 "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8689 | "noreplication" | "bypassrls" | "nobypassrls" => {
8690 self.advance();
8691 }
8692 "connection" => {
8693 self.advance();
8694 self.expect_keyword_ident("limit")?;
8695 self.advance(); // the number
8696 }
8697 "valid" => {
8698 self.advance();
8699 self.expect_keyword_ident("until")?;
8700 self.expect_string_literal()?;
8701 }
8702 _ => break,
8703 }
8704 }
8705 if role.is_empty() {
8706 role = "readonly".to_string();
8707 }
8708 Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8709 name,
8710 password,
8711 role,
8712 login,
8713 inherit,
8714 superuser,
8715 is_user,
8716 }))
8717 }
8718
8719 /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8720 /// consumed the USING / WITH CHECK keyword.
8721 fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8722 if !matches!(self.peek(), Token::LParen) {
8723 return Err(self.err(alloc::format!(
8724 "expected '(' after {clause}, got {:?}",
8725 self.peek()
8726 )));
8727 }
8728 self.advance();
8729 let e = self.parse_expr(0)?;
8730 if !matches!(self.peek(), Token::RParen) {
8731 return Err(self.err(alloc::format!(
8732 "expected ')' to close {clause}, got {:?}",
8733 self.peek()
8734 )));
8735 }
8736 self.advance();
8737 Ok(e)
8738 }
8739
8740 /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8741 fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8742 let mut roles = Vec::new();
8743 loop {
8744 roles.push(self.expect_ident_like()?);
8745 if matches!(self.peek(), Token::Comma) {
8746 self.advance();
8747 } else {
8748 break;
8749 }
8750 }
8751 Ok(roles)
8752 }
8753
8754 /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8755 /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8756 /// `CREATE POLICY`.
8757 fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8758 use crate::ast::PolicyCmd;
8759 let name = self.expect_ident_like()?;
8760 if !matches!(self.peek(), Token::On) {
8761 return Err(self.err(alloc::format!(
8762 "expected ON after CREATE POLICY name, got {:?}",
8763 self.peek()
8764 )));
8765 }
8766 self.advance();
8767 let table = self.expect_ident_like()?;
8768
8769 let mut permissive = true;
8770 if matches!(self.peek(), Token::As) {
8771 self.advance();
8772 let w = self.expect_ident_like()?;
8773 permissive = if w.eq_ignore_ascii_case("permissive") {
8774 true
8775 } else if w.eq_ignore_ascii_case("restrictive") {
8776 false
8777 } else {
8778 return Err(self.err(alloc::format!(
8779 "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8780 )));
8781 };
8782 }
8783
8784 let mut cmd = PolicyCmd::All;
8785 if matches!(self.peek(), Token::For) {
8786 self.advance();
8787 cmd = self.parse_policy_cmd()?;
8788 }
8789
8790 let mut roles = Vec::new();
8791 if matches!(self.peek(), Token::To) {
8792 self.advance();
8793 roles = self.parse_policy_roles()?;
8794 }
8795
8796 let mut using = None;
8797 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8798 {
8799 self.advance();
8800 using = Some(self.parse_paren_expr("USING")?);
8801 }
8802
8803 let mut with_check = None;
8804 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8805 {
8806 self.advance();
8807 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8808 {
8809 return Err(self.err(alloc::format!(
8810 "expected CHECK after WITH, got {:?}",
8811 self.peek()
8812 )));
8813 }
8814 self.advance();
8815 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8816 }
8817
8818 // Clause-per-command matrix (PG wording).
8819 match cmd {
8820 PolicyCmd::Insert => {
8821 if using.is_some() {
8822 return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8823 }
8824 }
8825 PolicyCmd::Select | PolicyCmd::Delete => {
8826 if with_check.is_some() {
8827 return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8828 }
8829 }
8830 PolicyCmd::Update | PolicyCmd::All => {}
8831 }
8832
8833 Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8834 name,
8835 table,
8836 permissive,
8837 cmd,
8838 roles,
8839 using,
8840 with_check,
8841 }))
8842 }
8843
8844 /// v7.39 (RLS) — the command word after `FOR`.
8845 fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8846 use crate::ast::PolicyCmd;
8847 match self.peek().clone() {
8848 Token::All => {
8849 self.advance();
8850 Ok(PolicyCmd::All)
8851 }
8852 Token::Select => {
8853 self.advance();
8854 Ok(PolicyCmd::Select)
8855 }
8856 Token::Insert => {
8857 self.advance();
8858 Ok(PolicyCmd::Insert)
8859 }
8860 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8861 self.advance();
8862 Ok(PolicyCmd::Update)
8863 }
8864 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8865 self.advance();
8866 Ok(PolicyCmd::Delete)
8867 }
8868 other => Err(self.err(alloc::format!(
8869 "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8870 ))),
8871 }
8872 }
8873
8874 /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8875 /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8876 fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8877 let name = self.expect_ident_like()?;
8878 if !matches!(self.peek(), Token::On) {
8879 return Err(self.err(alloc::format!(
8880 "expected ON after ALTER POLICY name, got {:?}",
8881 self.peek()
8882 )));
8883 }
8884 self.advance();
8885 let table = self.expect_ident_like()?;
8886
8887 // RENAME TO new
8888 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8889 {
8890 self.advance();
8891 if !matches!(self.peek(), Token::To) {
8892 return Err(self.err(alloc::format!(
8893 "expected TO after RENAME, got {:?}",
8894 self.peek()
8895 )));
8896 }
8897 self.advance();
8898 let new = self.expect_ident_like()?;
8899 return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8900 name,
8901 table,
8902 rename_to: Some(new),
8903 roles: None,
8904 using: None,
8905 with_check: None,
8906 }));
8907 }
8908
8909 let mut roles = None;
8910 if matches!(self.peek(), Token::To) {
8911 self.advance();
8912 roles = Some(self.parse_policy_roles()?);
8913 }
8914 let mut using = None;
8915 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8916 {
8917 self.advance();
8918 using = Some(self.parse_paren_expr("USING")?);
8919 }
8920 let mut with_check = None;
8921 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8922 {
8923 self.advance();
8924 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8925 {
8926 return Err(self.err(alloc::format!(
8927 "expected CHECK after WITH, got {:?}",
8928 self.peek()
8929 )));
8930 }
8931 self.advance();
8932 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8933 }
8934 Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8935 name,
8936 table,
8937 rename_to: None,
8938 roles,
8939 using,
8940 with_check,
8941 }))
8942 }
8943
8944 /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
8945 /// `DROP POLICY`.
8946 fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8947 let if_exists = self.consume_if_exists();
8948 let name = self.expect_ident_like()?;
8949 if !matches!(self.peek(), Token::On) {
8950 return Err(self.err(alloc::format!(
8951 "expected ON after DROP POLICY name, got {:?}",
8952 self.peek()
8953 )));
8954 }
8955 self.advance();
8956 let table = self.expect_ident_like()?;
8957 Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
8958 name,
8959 table,
8960 if_exists,
8961 }))
8962 }
8963}
8964fn wrap_from_leaves(
8965 e: &mut Expr,
8966 names: &[String],
8967 make: &dyn Fn(Expr) -> Expr,
8968 refs: &dyn Fn(&Expr) -> bool,
8969) {
8970 if let Expr::Column(c) = e {
8971 if c.qualifier
8972 .as_deref()
8973 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
8974 {
8975 let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
8976 *e = make(taken);
8977 }
8978 return;
8979 }
8980 match e {
8981 Expr::Binary { lhs, rhs, .. } => {
8982 wrap_from_leaves(lhs, names, make, refs);
8983 wrap_from_leaves(rhs, names, make, refs);
8984 }
8985 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
8986 wrap_from_leaves(expr, names, make, refs)
8987 }
8988 Expr::FunctionCall { args, .. } => {
8989 for a in args.iter_mut() {
8990 wrap_from_leaves(a, names, make, refs);
8991 }
8992 }
8993 Expr::Case {
8994 operand,
8995 branches,
8996 else_branch,
8997 } => {
8998 if let Some(o) = operand.as_deref_mut() {
8999 wrap_from_leaves(o, names, make, refs);
9000 }
9001 for (w, t) in branches.iter_mut() {
9002 wrap_from_leaves(w, names, make, refs);
9003 wrap_from_leaves(t, names, make, refs);
9004 }
9005 if let Some(el) = else_branch.as_deref_mut() {
9006 wrap_from_leaves(el, names, make, refs);
9007 }
9008 }
9009 // Compound variants the walk doesn't decompose: keep the
9010 // pre-D.30 behavior — wrap the whole sub-expr if it touches
9011 // a source table, so nothing regresses.
9012 other => {
9013 if refs(other) {
9014 let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
9015 *other = make(taken);
9016 }
9017 }
9018 }
9019}
9020
9021/// v7.39 (round 241) — does this expression reference any of the FROM /
9022/// USING table names (shared by the UPDATE…FROM and DELETE…USING
9023/// lowerings)?
9024fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
9025 match e {
9026 Expr::Column(c) => c
9027 .qualifier
9028 .as_deref()
9029 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9030 Expr::Binary { lhs, rhs, .. } => {
9031 expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
9032 }
9033 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
9034 Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
9035 Expr::Case {
9036 operand,
9037 branches,
9038 else_branch,
9039 } => {
9040 operand
9041 .as_deref()
9042 .is_some_and(|o| expr_refs_tables(o, names))
9043 || branches
9044 .iter()
9045 .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
9046 || else_branch
9047 .as_deref()
9048 .is_some_and(|el| expr_refs_tables(el, names))
9049 }
9050 _ => false,
9051 }
9052}
9053
9054impl Parser {
9055 /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
9056 /// Caller already consumed the leading `UPDATE` ident.
9057 /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
9058 /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
9059 /// after the target name has been read. `JOIN` is a reserved token;
9060 /// the qualifiers are bare idents.
9061 fn peek_is_update_join_start(&self) -> bool {
9062 match self.peek() {
9063 // JOIN and its qualifiers are reserved lexer tokens (the grammar
9064 // dedicates arms to `LEFT [OUTER] JOIN` and friends).
9065 Token::Join
9066 | Token::Inner
9067 | Token::Left
9068 | Token::Right
9069 | Token::Cross
9070 | Token::Full => true,
9071 // NATURAL / STRAIGHT_JOIN arrive as bare idents.
9072 Token::Ident(s) | Token::QuotedIdent(s) => {
9073 matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
9074 }
9075 _ => false,
9076 }
9077 }
9078
9079 /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
9080 /// USER-variable assignment. Its own per-session namespace, an arbitrary
9081 /// expression on the right, and `:=` as a second spelling of `=`.
9082 ///
9083 /// Out-of-line (`inline(never)`): the statement-parse frame it is called
9084 /// from sits on the nesting recursion chain (a CTE body, a subquery),
9085 /// and holding this loop's `Vec` + `String` locals there overflowed the
9086 /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
9087 #[inline(never)]
9088 fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
9089 let mut assigns: Vec<(String, Expr)> = Vec::new();
9090 let mut settings: Vec<(String, Expr)> = Vec::new();
9091 loop {
9092 // v7.39 (round 554) — a plain NAME here is a session
9093 // setting, not a user variable. mysqldump writes the two in
9094 // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
9095 // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
9096 // changes it — and this refused the mixture outright, so no
9097 // dump could be restored past its preamble.
9098 if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
9099 self.advance();
9100 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9101 return Err(self.err(alloc::format!(
9102 "expected `=` after {name}, got {:?}",
9103 self.peek()
9104 )));
9105 }
9106 self.advance();
9107 let value = self.parse_expr(0)?;
9108 settings.push((name.to_ascii_lowercase(), value));
9109 if matches!(self.peek(), Token::Comma) {
9110 self.advance();
9111 continue;
9112 }
9113 break;
9114 }
9115 let Token::SessionVar(raw) = self.peek().clone() else {
9116 return Err(self.err(alloc::format!(
9117 "expected a user variable after SET, got {:?}",
9118 self.peek()
9119 )));
9120 };
9121 if raw.starts_with("@@") {
9122 return Err(self.err(alloc::string::String::from(
9123 "cannot mix `@@` settings with `@` user variables in one SET",
9124 )));
9125 }
9126 self.advance();
9127 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9128 return Err(self.err(alloc::format!(
9129 "expected `=` or `:=` after {raw}, got {:?}",
9130 self.peek()
9131 )));
9132 }
9133 self.advance();
9134 let value = self.parse_expr(0)?;
9135 assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
9136 if matches!(self.peek(), Token::Comma) {
9137 self.advance();
9138 continue;
9139 }
9140 break;
9141 }
9142 Ok(Statement::SetUserVars(assigns, settings))
9143 }
9144
9145 fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
9146 // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
9147 // NAMED `only` until now, which failed on `relation "only" does
9148 // not exist`. The lookahead is what keeps a table actually
9149 // called `only` working: the keyword is only a keyword when a
9150 // TABLE NAME follows it — and `SET` arrives as an identifier
9151 // here, so `UPDATE only SET a = 2` would otherwise take `SET`
9152 // for the table and die on the `=`. Measured by the pin.
9153 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9154 if s.eq_ignore_ascii_case("only"))
9155 && matches!(
9156 self.tokens.get(self.pos + 1),
9157 Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
9158 );
9159 if only {
9160 self.advance();
9161 }
9162 let table = self.expect_ident_like()?;
9163 // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
9164 // bare spelling; a bare identifier that is the SET keyword itself
9165 // is the clause, not an alias.
9166 // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
9167 // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
9168 // multi-table form, and swallowing `LEFT` as `a`'s alias made the
9169 // following JOIN a syntax error.
9170 let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
9171 let alias = if matches!(self.peek(), Token::As) {
9172 self.advance();
9173 Some(self.expect_ident_like()?)
9174 } else {
9175 match self.peek() {
9176 Token::Ident(s) | Token::QuotedIdent(s)
9177 if !s.eq_ignore_ascii_case("set") && !starts_join =>
9178 {
9179 let a = s.clone();
9180 self.advance();
9181 Some(a)
9182 }
9183 _ => None,
9184 }
9185 };
9186 // v7.39 (round 420) — MySQL's multi-table UPDATE:
9187 // UPDATE a, b SET a.v = b.v WHERE a.id = b.id
9188 // UPDATE a JOIN b ON a.id = b.id SET a.v = b.v + 1
9189 // UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
9190 // The FIRST table is the mutation target and the rest are sources —
9191 // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
9192 // SPG already lowers onto correlated subqueries. So rewind, let
9193 // `parse_from_clause` read the whole list (it handles aliases, comma
9194 // lists, and every JOIN form), then peel the target off the front.
9195 let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
9196 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9197 {
9198 // NOTE: `advance()` destroys the tokens it returns
9199 // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
9200 // is NOT possible — the tail is read forward, once, through the
9201 // same grammar `parse_from_clause` uses after its primary.
9202 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9203 let mut joins = self.parse_from_joins(&target_qual)?;
9204 if joins.is_empty() {
9205 return Err(self.err(alloc::string::String::from(
9206 "multi-table UPDATE needs at least one source table",
9207 )));
9208 }
9209 let head = joins.remove(0);
9210 // A LEFT join keeps every target row (the unmatched ones see NULL
9211 // on the source side), so it must NOT get the EXISTS row filter
9212 // the inner / comma forms use.
9213 let outer = matches!(head.kind, crate::ast::JoinKind::Left);
9214 let src = FromClause {
9215 primary: head.table,
9216 joins,
9217 };
9218 (Some(src), head.on, outer)
9219 } else {
9220 (None, None, false)
9221 };
9222 self.expect_keyword_ident("set")?;
9223 let mut assignments = Vec::new();
9224 loop {
9225 // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
9226 // …)` — the parenthesized multi-assignment. Expressions
9227 // assign positionally; a subquery RHS clones per column
9228 // keeping only the Nth projection item.
9229 if matches!(self.peek(), Token::LParen) {
9230 self.advance();
9231 let mut cols = alloc::vec![self.expect_ident_like()?];
9232 while matches!(self.peek(), Token::Comma) {
9233 self.advance();
9234 cols.push(self.expect_ident_like()?);
9235 }
9236 if !matches!(self.peek(), Token::RParen) {
9237 return Err(self.err(format!(
9238 "expected ')' after SET column list, got {:?}",
9239 self.peek()
9240 )));
9241 }
9242 self.advance();
9243 if !matches!(self.peek(), Token::Eq) {
9244 return Err(self.err(format!(
9245 "expected `=` after SET column list, got {:?}",
9246 self.peek()
9247 )));
9248 }
9249 self.advance();
9250 if !matches!(self.peek(), Token::LParen) {
9251 return Err(self.err(format!(
9252 "expected '(' after SET (…) =, got {:?}",
9253 self.peek()
9254 )));
9255 }
9256 self.advance();
9257 if matches!(self.peek(), Token::Select) {
9258 let inner = match self.parse_select_stmt()? {
9259 Statement::Select(s) => s,
9260 other => {
9261 return Err(self.err(alloc::format!(
9262 "expected SELECT in SET (…) = (SELECT …), got {other:?}"
9263 )));
9264 }
9265 };
9266 if !matches!(self.peek(), Token::RParen) {
9267 return Err(self.err(format!(
9268 "expected ')' after SET subquery, got {:?}",
9269 self.peek()
9270 )));
9271 }
9272 self.advance();
9273 if inner.items.len() != cols.len() {
9274 return Err(self.err(alloc::format!(
9275 "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9276 cols.len(),
9277 inner.items.len()
9278 )));
9279 }
9280 for (i, col) in cols.into_iter().enumerate() {
9281 let mut sub = inner.clone();
9282 sub.items = alloc::vec![sub.items[i].clone()];
9283 assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9284 }
9285 } else {
9286 let mut exprs = alloc::vec![self.parse_expr(0)?];
9287 while matches!(self.peek(), Token::Comma) {
9288 self.advance();
9289 exprs.push(self.parse_expr(0)?);
9290 }
9291 if !matches!(self.peek(), Token::RParen) {
9292 return Err(self.err(format!(
9293 "expected ')' after SET row values, got {:?}",
9294 self.peek()
9295 )));
9296 }
9297 self.advance();
9298 if exprs.len() != cols.len() {
9299 return Err(self.err(alloc::format!(
9300 "SET (…) = (…) arity mismatch: {} columns, {} values",
9301 cols.len(),
9302 exprs.len()
9303 )));
9304 }
9305 for (col, e) in cols.into_iter().zip(exprs) {
9306 assignments.push((col, e));
9307 }
9308 }
9309 if matches!(self.peek(), Token::Comma) {
9310 self.advance();
9311 continue;
9312 }
9313 break;
9314 }
9315 // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9316 // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9317 // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9318 // `public.` dump qualifiers), so the qualifier has to be read off
9319 // the token stream first — otherwise `SET b.v = 888` would write
9320 // to the TARGET table's `v` while naming a source table, a
9321 // silent-wrong. A qualifier naming a SOURCE table means a
9322 // multi-TARGET update — mutating two tables in one statement —
9323 // which SPG does not model, so it is refused loudly.
9324 let set_qual: Option<String> = if mysql_from.is_some()
9325 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9326 {
9327 match self.peek() {
9328 Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9329 _ => None,
9330 }
9331 } else {
9332 None
9333 };
9334 let col = self.expect_ident_like()?;
9335 if let Some(q) = set_qual {
9336 let names_target = q.eq_ignore_ascii_case(&table)
9337 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9338 if !names_target {
9339 return Err(self.err(alloc::format!(
9340 "multi-table UPDATE can only assign to its first table \
9341 ({table}); `{q}.{col}` targets another table"
9342 )));
9343 }
9344 }
9345 // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9346 // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9347 // `__column_default` marker lowering just below). PG assigns to the
9348 // i-th (1-based) element, NULL-padding when i exceeds the length.
9349 if matches!(self.peek(), Token::LBracket) {
9350 self.advance();
9351 let index = self.parse_expr(0)?;
9352 // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9353 // (and the open `arr[lo:]`), lowered to
9354 // `__array_assign_slice`. Only the single-subscript form
9355 // parsed before, so a slice assignment was a syntax error.
9356 let mut slice_hi: Option<Option<Expr>> = None;
9357 if matches!(self.peek(), Token::Colon) {
9358 self.advance();
9359 slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9360 None
9361 } else {
9362 Some(self.parse_expr(0)?)
9363 });
9364 }
9365 if !matches!(self.peek(), Token::RBracket) {
9366 return Err(self.err(format!(
9367 "expected `]` after array subscript in UPDATE SET, got {:?}",
9368 self.peek()
9369 )));
9370 }
9371 self.advance();
9372 if !matches!(self.peek(), Token::Eq) {
9373 return Err(self.err(format!(
9374 "expected `=` after array subscript in UPDATE SET, got {:?}",
9375 self.peek()
9376 )));
9377 }
9378 self.advance();
9379 let value = self.parse_expr(0)?;
9380 // PG merges several subscript writes to the same column into one
9381 // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9382 // assignment to `col` rather than each overwriting the original.
9383 let existing = assignments.iter().position(|(c, _)| c == &col);
9384 let base = match existing {
9385 Some(i) => assignments[i].1.clone(),
9386 None => Expr::Column(ColumnName {
9387 qualifier: None,
9388 name: col.clone(),
9389 }),
9390 };
9391 let assigned = match slice_hi {
9392 None => Expr::FunctionCall {
9393 name: "__array_assign".to_string(),
9394 args: alloc::vec![base, index, value],
9395 },
9396 Some(hi) => Expr::FunctionCall {
9397 name: "__array_assign_slice".to_string(),
9398 args: alloc::vec![
9399 base,
9400 index,
9401 hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9402 value,
9403 ],
9404 },
9405 };
9406 match existing {
9407 Some(i) => assignments[i].1 = assigned,
9408 None => assignments.push((col, assigned)),
9409 }
9410 if matches!(self.peek(), Token::Comma) {
9411 self.advance();
9412 continue;
9413 }
9414 break;
9415 }
9416 if !matches!(self.peek(), Token::Eq) {
9417 return Err(self.err(format!(
9418 "expected `=` after column name in UPDATE SET, got {:?}",
9419 self.peek()
9420 )));
9421 }
9422 self.advance();
9423 // `SET col = DEFAULT` — the column's declared default;
9424 // rides out as a marker call the update executor
9425 // resolves against the schema.
9426 let value = if matches!(self.peek(), Token::Default) {
9427 self.advance();
9428 Expr::FunctionCall {
9429 name: "__column_default".to_string(),
9430 args: Vec::new(),
9431 }
9432 } else {
9433 self.parse_expr(0)?
9434 };
9435 assignments.push((col, value));
9436 if matches!(self.peek(), Token::Comma) {
9437 self.advance();
9438 continue;
9439 }
9440 break;
9441 }
9442 // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9443 // update. Lowers onto the correlated-subquery machinery:
9444 // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9445 // and each assignment that references a FROM-list table
9446 // wraps into a correlated scalar subquery
9447 // (SELECT expr FROM src WHERE cond). Equivalent for the
9448 // unique-join shape (the overwhelmingly common one); a
9449 // multi-match, which PG resolves by arbitrary pick,
9450 // surfaces as a scalar-subquery cardinality error instead
9451 // of a silent arbitrary result.
9452 // v7.39 (round 420) — the MySQL multi-table form supplies the source
9453 // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9454 // the SAME lowering below. Both spellings together is not legal in
9455 // either dialect.
9456 let from_clause = if let Some(fc) = mysql_from {
9457 if matches!(self.peek(), Token::From) {
9458 return Err(self.err(alloc::string::String::from(
9459 "multi-table UPDATE already names its sources; drop the FROM clause",
9460 )));
9461 }
9462 Some(fc)
9463 } else if matches!(self.peek(), Token::From) {
9464 self.advance();
9465 Some(self.parse_from_clause()?)
9466 } else {
9467 None
9468 };
9469 let where_ = if matches!(self.peek(), Token::Where) {
9470 self.advance();
9471 Some(self.parse_expr(0)?)
9472 } else {
9473 None
9474 };
9475 // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9476 // and the TARGET-row filter are NOT the same predicate once a LEFT
9477 // join is involved:
9478 // * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9479 // one conjunction, and the whole thing filters target rows via
9480 // EXISTS.
9481 // * LEFT join: only the ON predicate belongs inside the source
9482 // subquery. The WHERE still filters TARGET rows (with source
9483 // columns read through the correlated subquery, which yields NULL
9484 // for an unmatched row — exactly LEFT-join semantics).
9485 // Round 420 folded ON into WHERE unconditionally and then dropped the
9486 // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9487 // WHERE a.id > 1` updated EVERY row.
9488 let sub_where = match (mysql_on.clone(), where_.clone()) {
9489 _ if mysql_outer => mysql_on.clone(),
9490 (Some(on), Some(w)) => Some(Expr::Binary {
9491 lhs: Box::new(on),
9492 op: crate::ast::BinOp::And,
9493 rhs: Box::new(w),
9494 }),
9495 (Some(on), None) => Some(on),
9496 (None, w) => w,
9497 };
9498 // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9499 // has no such clause on UPDATE, so this is accepted only under the
9500 // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9501 let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9502 let mut returning = self.parse_optional_returning()?;
9503 // v7.39 (round 533) — kept for the engine, which can resolve the
9504 // UNQUALIFIED leaves this lowering has to leave alone.
9505 let from_sources = from_clause.as_ref().map(|fc| {
9506 alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9507 from: fc.clone(),
9508 sub_where: sub_where.clone(),
9509 })
9510 });
9511 let (assignments, where_) = if let Some(fc) = from_clause {
9512 let names: Vec<String> = core::iter::once(&fc.primary)
9513 .chain(fc.joins.iter().map(|j| &j.table))
9514 .flat_map(|t| {
9515 t.alias
9516 .clone()
9517 .into_iter()
9518 .chain(core::iter::once(t.name.clone()))
9519 })
9520 .collect();
9521 let refs_list = |e: &Expr| -> bool {
9522 fn walk(e: &Expr, names: &[String]) -> bool {
9523 match e {
9524 Expr::Column(c) => c
9525 .qualifier
9526 .as_deref()
9527 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9528 Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9529 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9530 Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9531 Expr::Case {
9532 operand,
9533 branches,
9534 else_branch,
9535 } => {
9536 operand.as_deref().is_some_and(|o| walk(o, names))
9537 || branches
9538 .iter()
9539 .any(|(w, t)| walk(w, names) || walk(t, names))
9540 || else_branch.as_deref().is_some_and(|el| walk(el, names))
9541 }
9542 _ => false,
9543 }
9544 }
9545 walk(e, &names)
9546 };
9547 let sub_select = |items: Vec<SelectItem>| SelectStatement {
9548 locking: None,
9549 ctes: Vec::new(),
9550 distinct: false,
9551 distinct_on: Vec::new(),
9552 items,
9553 from: Some(fc.clone()),
9554 where_: sub_where.clone(),
9555 group_by: None,
9556 group_by_all: false,
9557 having: None,
9558 unions: Vec::new(),
9559 order_by: Vec::new(),
9560 limit: None,
9561 offset: None,
9562 limit_with_ties: false,
9563 window_check_exprs: Vec::new(),
9564 };
9565 // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9566 // assignment RHS with a correlated scalar subquery, instead of
9567 // wrapping the whole RHS. Wrapping the whole expr moved a target-
9568 // column reference (`SET v = v + u.bonus`, where `v` is the target
9569 // table's column) inside a subquery whose FROM only has the source
9570 // table, so the unqualified `v` resolved against the source and
9571 // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9572 // context — where they belong — fixes it; only the source columns
9573 // (`u.bonus`) become subqueries. A whole-expr fallback covers
9574 // compound variants the leaf-walk doesn't decompose.
9575 let make_subq = |inner: Expr| {
9576 Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9577 expr: inner,
9578 alias: None,
9579 }])))
9580 };
9581 let assignments = assignments
9582 .into_iter()
9583 .map(|(col, mut expr)| {
9584 wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9585 (col, expr)
9586 })
9587 .collect();
9588 let exists = Expr::Exists {
9589 subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9590 expr: Expr::Literal(Literal::Integer(1)),
9591 alias: None,
9592 }])),
9593 negated: false,
9594 };
9595 // v7.39 (round 241) — RETURNING may reference the FROM-list
9596 // tables too (`RETURNING emp.id, dept.name`); the same
9597 // leaf-to-correlated-subquery lowering the assignments get.
9598 // Without it the qualifier died at eval with "unknown table
9599 // qualifier". (RETURNING was parsed before this block — the
9600 // lowering is a pure AST transformation.)
9601 if let Some(items) = returning.as_mut() {
9602 for item in items.iter_mut() {
9603 if let SelectItem::Expr { expr, .. } = item {
9604 wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9605 }
9606 }
9607 }
9608 // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9609 // EVERY matching target row: it gets no EXISTS filter, but the
9610 // caller's WHERE still applies, with source columns read through
9611 // the correlated subquery (NULL when unmatched — LEFT-join
9612 // semantics). `sub_where` above already excluded the WHERE from
9613 // the source subquery for this case.
9614 if mysql_outer {
9615 let mut outer = where_;
9616 if let Some(w) = outer.as_mut() {
9617 wrap_from_leaves(w, &names, &make_subq, &refs_list);
9618 }
9619 (assignments, outer)
9620 } else {
9621 (assignments, Some(exists))
9622 }
9623 } else {
9624 (assignments, where_)
9625 };
9626 Ok(Statement::Update(crate::ast::UpdateStatement {
9627 ctes: Vec::new(),
9628 table,
9629 only,
9630 alias,
9631 assignments,
9632 from_sources,
9633 where_,
9634 order_limit: update_order_limit,
9635 returning,
9636 }))
9637 }
9638
9639 /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9640 /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9641 /// clause and its meaning are identical, so both call this rather than
9642 /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9643 /// legal. PG has no such clause on either statement, so it is read only
9644 /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9645 /// errors.
9646 ///
9647 /// `#[inline(never)]`: its locals would otherwise land on the statement-
9648 /// parsing recursion frame, which is what tipped the 512 KiB nesting
9649 /// stack in round 430.
9650 #[inline(never)]
9651 fn parse_mysql_dml_order_limit(
9652 &mut self,
9653 what: &str,
9654 ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9655 if !self.mysql_dialect {
9656 return Ok(None);
9657 }
9658 let order_by = self.parse_order_by_keys()?;
9659 let limit = if matches!(self.peek(), Token::Limit) {
9660 self.advance();
9661 let tok = self.advance();
9662 let Token::Integer(n) = tok else {
9663 return Err(self.err(alloc::format!(
9664 "expected integer after {what} LIMIT, got {tok:?}"
9665 )));
9666 };
9667 // MySQL rejects the `LIMIT offset, count` form here — only a
9668 // single row count is legal on a DML statement.
9669 if matches!(self.peek(), Token::Comma) {
9670 return Err(self.err(alloc::format!(
9671 "{what} LIMIT takes a row count, not an offset"
9672 )));
9673 }
9674 let n = u32::try_from(n)
9675 .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9676 Some(n)
9677 } else {
9678 None
9679 };
9680 if order_by.is_empty() && limit.is_none() {
9681 return Ok(None);
9682 }
9683 Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9684 order_by,
9685 limit,
9686 })))
9687 }
9688
9689 /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9690 /// the leading `DELETE` ident.
9691 fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9692 // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9693 // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9694 // USING a, b WHERE …` — the third MySQL spelling — needs no special
9695 // parse here; it reaches the existing USING path with the target
9696 // repeated in the list, which the source-list peel below handles.)
9697 // More than one name is a multi-TARGET delete, which SPG does not
9698 // model; it is refused rather than half-applied.
9699 let mysql_pre_target: Option<String> =
9700 if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9701 let first = self.expect_ident_like()?;
9702 if matches!(self.peek(), Token::Comma) {
9703 return Err(self.err(alloc::format!(
9704 "multi-table DELETE can only delete from one table; \
9705 `DELETE {first}, …` names several"
9706 )));
9707 }
9708 Some(first)
9709 } else {
9710 None
9711 };
9712 if !matches!(self.peek(), Token::From) {
9713 return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9714 }
9715 self.advance();
9716 // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9717 // lookahead as the UPDATE spelling.
9718 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9719 if s.eq_ignore_ascii_case("only"))
9720 && matches!(
9721 self.tokens.get(self.pos + 1),
9722 Some(Token::Ident(_) | Token::QuotedIdent(_))
9723 );
9724 if only {
9725 self.advance();
9726 }
9727 let table = self.expect_ident_like()?;
9728 // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9729 // spelling must not swallow the clause keywords that can follow
9730 // the target.
9731 let alias = if matches!(self.peek(), Token::As) {
9732 self.advance();
9733 Some(self.expect_ident_like()?)
9734 } else {
9735 match self.peek() {
9736 Token::Ident(s) | Token::QuotedIdent(s)
9737 if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9738 {
9739 let a = s.clone();
9740 self.advance();
9741 Some(a)
9742 }
9743 _ => None,
9744 }
9745 };
9746 // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9747 // through the SAME join grammar the FROM clause uses (see the
9748 // `advance()`-destroys-tokens note on `parse_from_joins`).
9749 let mut mysql_on: Option<Expr> = None;
9750 let mut mysql_outer = false;
9751 let mysql_using = if mysql_pre_target.is_some()
9752 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9753 {
9754 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9755 let mut joins = self.parse_from_joins(&target_qual)?;
9756 if joins.is_empty() {
9757 return Err(self.err(alloc::string::String::from(
9758 "multi-table DELETE needs at least one source table",
9759 )));
9760 }
9761 let head = joins.remove(0);
9762 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9763 mysql_on = head.on;
9764 Some(FromClause {
9765 primary: head.table,
9766 joins,
9767 })
9768 } else {
9769 None
9770 };
9771 // The pre-FROM target must be the table the FROM names (or its
9772 // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9773 // is not the scan target.
9774 if let Some(t) = &mysql_pre_target {
9775 let names_target = t.eq_ignore_ascii_case(&table)
9776 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9777 if !names_target {
9778 return Err(self.err(alloc::format!(
9779 "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9780 )));
9781 }
9782 }
9783 // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9784 // delete. Same lowering as UPDATE … FROM: the WHERE
9785 // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9786 // target row by the correlated machinery.
9787 let using_clause = if let Some(fc) = mysql_using {
9788 Some(fc)
9789 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9790 self.advance();
9791 let mut fc = self.parse_from_clause()?;
9792 // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9793 // repeats the TARGET as the first USING entry (PG's spelling
9794 // lists only the extra sources). Peel it so the source subquery
9795 // does not re-scan — and shadow — the target table.
9796 let primary_is_target =
9797 fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9798 if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9799 let head = fc.joins.remove(0);
9800 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9801 mysql_on = head.on;
9802 fc = FromClause {
9803 primary: head.table,
9804 joins: fc.joins,
9805 };
9806 }
9807 Some(fc)
9808 } else {
9809 None
9810 };
9811 let where_ = if matches!(self.peek(), Token::Where) {
9812 self.advance();
9813 Some(self.parse_expr(0)?)
9814 } else {
9815 None
9816 };
9817 // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9818 // read before RETURNING (MariaDB's own extension trails the LIMIT).
9819 let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9820 let mut returning = self.parse_optional_returning()?;
9821 let where_ = if let Some(fc) = using_clause {
9822 // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9823 // a USING-table reference in RETURNING becomes a correlated
9824 // scalar subquery over the USING list.
9825 let names: Vec<String> = core::iter::once(&fc.primary)
9826 .chain(fc.joins.iter().map(|j| &j.table))
9827 .flat_map(|t| {
9828 t.alias
9829 .clone()
9830 .into_iter()
9831 .chain(core::iter::once(t.name.clone()))
9832 })
9833 .collect();
9834 // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9835 // join filters the SOURCE subquery on the ON predicate alone and
9836 // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9837 // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9838 // rows); every other form folds ON and WHERE into one EXISTS.
9839 let sub_where = match (mysql_on.clone(), where_.clone()) {
9840 _ if mysql_outer => mysql_on.clone(),
9841 (Some(on), Some(w)) => Some(Expr::Binary {
9842 lhs: Box::new(on),
9843 op: crate::ast::BinOp::And,
9844 rhs: Box::new(w),
9845 }),
9846 (Some(on), None) => Some(on),
9847 (None, w) => w,
9848 };
9849 let exists_where = sub_where.clone();
9850 let sub_fc = fc.clone();
9851 let make_subq = move |leaf: Expr| -> Expr {
9852 Expr::ScalarSubquery(Box::new(SelectStatement {
9853 locking: None,
9854 ctes: Vec::new(),
9855 distinct: false,
9856 distinct_on: Vec::new(),
9857 items: alloc::vec![SelectItem::Expr {
9858 expr: leaf,
9859 alias: None,
9860 }],
9861 from: Some(sub_fc.clone()),
9862 where_: sub_where.clone(),
9863 group_by: None,
9864 group_by_all: false,
9865 having: None,
9866 unions: Vec::new(),
9867 order_by: Vec::new(),
9868 limit: None,
9869 offset: None,
9870 limit_with_ties: false,
9871 window_check_exprs: Vec::new(),
9872 }))
9873 };
9874 let refs = |e: &Expr| expr_refs_tables(e, &names);
9875 if let Some(items) = returning.as_mut() {
9876 for item in items.iter_mut() {
9877 if let SelectItem::Expr { expr, .. } = item {
9878 wrap_from_leaves(expr, &names, &make_subq, &refs);
9879 }
9880 }
9881 }
9882 // A LEFT join deletes the target rows the WHERE selects, reading
9883 // source columns through the correlated subquery (NULL when
9884 // unmatched); no EXISTS row filter.
9885 if mysql_outer {
9886 let mut outer = where_;
9887 if let Some(w) = outer.as_mut() {
9888 wrap_from_leaves(w, &names, &make_subq, &refs);
9889 }
9890 outer
9891 } else {
9892 Some(Expr::Exists {
9893 subquery: Box::new(SelectStatement {
9894 locking: None,
9895 ctes: Vec::new(),
9896 distinct: false,
9897 distinct_on: Vec::new(),
9898 items: alloc::vec![SelectItem::Expr {
9899 expr: Expr::Literal(Literal::Integer(1)),
9900 alias: None,
9901 }],
9902 from: Some(fc),
9903 where_: exists_where,
9904 group_by: None,
9905 group_by_all: false,
9906 having: None,
9907 unions: Vec::new(),
9908 order_by: Vec::new(),
9909 limit: None,
9910 offset: None,
9911 limit_with_ties: false,
9912 window_check_exprs: Vec::new(),
9913 }),
9914 negated: false,
9915 })
9916 }
9917 } else {
9918 where_
9919 };
9920 Ok(Statement::Delete(crate::ast::DeleteStatement {
9921 ctes: Vec::new(),
9922 table,
9923 only,
9924 alias,
9925 where_,
9926 order_limit: delete_order_limit,
9927 returning,
9928 }))
9929 }
9930
9931 /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
9932 /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
9933 /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
9934 /// keyword. v7.17 surface:
9935 /// * source: table reference (subquery source is a follow-up)
9936 /// * actions: UPDATE SET / DELETE / DO NOTHING (matched);
9937 /// INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
9938 /// * AND-conditioned WHEN clauses; clauses tried in declaration
9939 /// order
9940 fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
9941 // INTO
9942 let is_into_kw = matches!(self.peek(), Token::Into)
9943 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
9944 if !is_into_kw {
9945 return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
9946 }
9947 self.advance();
9948 let target = self.expect_ident_like()?;
9949 // Optional alias — bare ident before USING.
9950 let target_alias = match self.peek() {
9951 Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
9952 Some(self.expect_ident_like()?)
9953 }
9954 _ => None,
9955 };
9956 // USING
9957 let is_using_kw = matches!(
9958 self.peek(),
9959 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
9960 );
9961 if !is_using_kw {
9962 return Err(self.err(format!(
9963 "expected USING after MERGE INTO target, got {:?}",
9964 self.peek()
9965 )));
9966 }
9967 self.advance();
9968 // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
9969 // <table> [alias]`. PG requires an alias after a subquery source.
9970 let (source, source_select) = if matches!(self.peek(), Token::LParen) {
9971 self.advance(); // (
9972 // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
9973 // constant-SELECT lowering the derived-table parser uses
9974 // (PG deletes through this form; it was a parse error).
9975 let inner = if matches!(self.peek(), Token::Values) {
9976 self.advance(); // VALUES
9977 Statement::Select(self.parse_values_rows_body()?)
9978 } else {
9979 self.parse_select_stmt()?
9980 };
9981 match self.advance() {
9982 Token::RParen => {}
9983 other => {
9984 return Err(self.err(format!(
9985 "expected ')' after MERGE USING subquery, got {other:?}"
9986 )));
9987 }
9988 }
9989 let Statement::Select(sub) = inner else {
9990 return Err(self.err("MERGE USING subquery must be a SELECT".into()));
9991 };
9992 (String::new(), Some(Box::new(sub)))
9993 } else {
9994 (self.expect_ident_like()?, None)
9995 };
9996 let source_alias = match self.peek() {
9997 Token::Ident(s) | Token::QuotedIdent(s)
9998 if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
9999 {
10000 Some(self.expect_ident_like()?)
10001 }
10002 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
10003 self.advance(); // AS
10004 Some(self.expect_ident_like()?)
10005 }
10006 _ => None,
10007 };
10008 // v7.39 (round 768, F31-D5) — optional positional column-alias
10009 // list after the source alias (`s(id, v)`).
10010 let mut source_column_aliases: Vec<String> = Vec::new();
10011 if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
10012 self.advance();
10013 loop {
10014 source_column_aliases.push(self.expect_ident_like()?);
10015 match self.peek() {
10016 Token::Comma => {
10017 self.advance();
10018 }
10019 Token::RParen => {
10020 self.advance();
10021 break;
10022 }
10023 other => {
10024 return Err(self.err(format!(
10025 "expected ',' or ')' in MERGE source column list, got {other:?}"
10026 )));
10027 }
10028 }
10029 }
10030 }
10031 if source_select.is_some() && source_alias.is_none() {
10032 return Err(self.err("MERGE USING (subquery) requires an alias".into()));
10033 }
10034 // ON
10035 if !matches!(self.peek(), Token::On) {
10036 return Err(self.err(format!(
10037 "expected ON after MERGE … USING source, got {:?}",
10038 self.peek()
10039 )));
10040 }
10041 self.advance();
10042 let on = self.parse_expr(0)?;
10043 // One or more WHEN clauses.
10044 let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
10045 loop {
10046 let is_when_kw = matches!(
10047 self.peek(),
10048 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
10049 );
10050 if !is_when_kw {
10051 break;
10052 }
10053 self.advance(); // WHEN
10054 // [NOT] MATCHED
10055 let matched = if matches!(self.peek(), Token::Not) {
10056 self.advance();
10057 crate::ast::MergeMatched::NotMatched
10058 } else {
10059 crate::ast::MergeMatched::Matched
10060 };
10061 let is_matched_kw = matches!(
10062 self.peek(),
10063 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
10064 );
10065 if !is_matched_kw {
10066 return Err(self.err(format!(
10067 "expected MATCHED in WHEN clause, got {:?}",
10068 self.peek()
10069 )));
10070 }
10071 self.advance();
10072 // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
10073 // BY TARGET is the default (a synonym); BY SOURCE flips the clause
10074 // to fire for target rows no source row matches.
10075 let mut matched = matched;
10076 if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
10077 self.advance();
10078 match self.peek() {
10079 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
10080 self.advance();
10081 matched = crate::ast::MergeMatched::NotMatchedBySource;
10082 }
10083 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
10084 self.advance();
10085 }
10086 other => {
10087 return Err(self.err(format!(
10088 "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
10089 )));
10090 }
10091 }
10092 }
10093 // Optional AND <expr>
10094 let condition = if matches!(self.peek(), Token::And) {
10095 self.advance();
10096 Some(self.parse_expr(0)?)
10097 } else {
10098 None
10099 };
10100 // THEN
10101 let is_then_kw = matches!(
10102 self.peek(),
10103 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
10104 );
10105 if !is_then_kw {
10106 return Err(self.err(format!(
10107 "expected THEN in WHEN clause, got {:?}",
10108 self.peek()
10109 )));
10110 }
10111 self.advance();
10112 // Action: INSERT / UPDATE / DELETE / DO NOTHING
10113 let action = match self.peek().clone() {
10114 Token::Insert => {
10115 self.advance();
10116 // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
10117 // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
10118 // VALUES (…)` omits it and fills every column in declaration
10119 // order. PG accepts this; SPG used to require the list.
10120 let mut columns: Vec<String> = Vec::new();
10121 if matches!(self.peek(), Token::LParen) {
10122 self.advance();
10123 loop {
10124 columns.push(self.expect_ident_like()?);
10125 if matches!(self.peek(), Token::Comma) {
10126 self.advance();
10127 continue;
10128 }
10129 break;
10130 }
10131 if !matches!(self.peek(), Token::RParen) {
10132 return Err(self.err(format!(
10133 "expected ')' after INSERT column list, got {:?}",
10134 self.peek()
10135 )));
10136 }
10137 self.advance();
10138 }
10139 // VALUES (...)
10140 if !matches!(self.peek(), Token::Values) {
10141 return Err(self.err(format!(
10142 "expected VALUES in MERGE INSERT, got {:?}",
10143 self.peek()
10144 )));
10145 }
10146 self.advance();
10147 if !matches!(self.peek(), Token::LParen) {
10148 return Err(self.err(format!(
10149 "expected '(' after VALUES in MERGE INSERT, got {:?}",
10150 self.peek()
10151 )));
10152 }
10153 self.advance();
10154 let mut values: Vec<crate::ast::Expr> = Vec::new();
10155 loop {
10156 values.push(self.parse_expr(0)?);
10157 if matches!(self.peek(), Token::Comma) {
10158 self.advance();
10159 continue;
10160 }
10161 break;
10162 }
10163 if !matches!(self.peek(), Token::RParen) {
10164 return Err(self.err(format!(
10165 "expected ')' after MERGE INSERT values, got {:?}",
10166 self.peek()
10167 )));
10168 }
10169 self.advance();
10170 // Empty column list = positional into every column, so the
10171 // count is checked against the table arity at execution.
10172 if !columns.is_empty() && columns.len() != values.len() {
10173 return Err(self.err(format!(
10174 "MERGE INSERT column count ({}) ≠ value count ({})",
10175 columns.len(),
10176 values.len()
10177 )));
10178 }
10179 crate::ast::MergeAction::Insert { columns, values }
10180 }
10181 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
10182 self.advance();
10183 // SET
10184 let is_set_kw = matches!(
10185 self.peek(),
10186 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
10187 );
10188 if !is_set_kw {
10189 return Err(self.err(format!(
10190 "expected SET after UPDATE in MERGE, got {:?}",
10191 self.peek()
10192 )));
10193 }
10194 self.advance();
10195 let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
10196 loop {
10197 let col = self.expect_ident_like()?;
10198 if !matches!(self.peek(), Token::Eq) {
10199 return Err(self.err(format!(
10200 "expected '=' in MERGE UPDATE assignment, got {:?}",
10201 self.peek()
10202 )));
10203 }
10204 self.advance();
10205 let expr = self.parse_expr(0)?;
10206 assignments.push((col, expr));
10207 if matches!(self.peek(), Token::Comma) {
10208 self.advance();
10209 continue;
10210 }
10211 break;
10212 }
10213 crate::ast::MergeAction::Update { assignments }
10214 }
10215 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
10216 self.advance();
10217 crate::ast::MergeAction::Delete
10218 }
10219 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
10220 self.advance();
10221 let is_nothing_kw = matches!(
10222 self.peek(),
10223 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
10224 );
10225 if !is_nothing_kw {
10226 return Err(self.err(format!(
10227 "expected NOTHING after DO in MERGE clause, got {:?}",
10228 self.peek()
10229 )));
10230 }
10231 self.advance();
10232 crate::ast::MergeAction::DoNothing
10233 }
10234 other => {
10235 return Err(self.err(format!(
10236 "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
10237 )));
10238 }
10239 };
10240 // PG's grammar simply has no INSERT production under BY SOURCE
10241 // (a target row already exists there) — same syntax error.
10242 if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
10243 && matches!(action, crate::ast::MergeAction::Insert { .. })
10244 {
10245 return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
10246 }
10247 clauses.push(crate::ast::MergeWhenClause {
10248 matched,
10249 condition,
10250 action,
10251 });
10252 }
10253 if clauses.is_empty() {
10254 return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
10255 }
10256 // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
10257 // unconditional (no `AND`) WHEN of the same match kind: it could
10258 // never fire. Check per match kind in clause order.
10259 let mut seen_unconditional_matched = false;
10260 let mut seen_unconditional_not_matched = false;
10261 let mut seen_unconditional_by_source = false;
10262 for c in &clauses {
10263 let seen = match c.matched {
10264 crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10265 crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10266 crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10267 };
10268 if *seen {
10269 return Err(self.err(String::from(
10270 "unreachable WHEN clause specified after unconditional WHEN clause",
10271 )));
10272 }
10273 if c.condition.is_none() {
10274 *seen = true;
10275 }
10276 }
10277 // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10278 let returning = self.parse_optional_returning()?;
10279 Ok(Statement::Merge(crate::ast::MergeStatement {
10280 // Attached by `parse_with_cte_then_select` when the MERGE
10281 // heads a WITH clause (round 149).
10282 ctes: Vec::new(),
10283 target,
10284 target_alias,
10285 source,
10286 source_alias,
10287 source_select,
10288 source_column_aliases,
10289 on,
10290 clauses,
10291 returning,
10292 }))
10293 }
10294
10295 /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10296 /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10297 /// as SELECT, so `RETURNING *`, `RETURNING col`,
10298 /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10299 fn parse_optional_returning(
10300 &mut self,
10301 ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10302 let is_returning_kw = matches!(
10303 self.peek(),
10304 Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10305 );
10306 if !is_returning_kw {
10307 return Ok(None);
10308 }
10309 self.advance();
10310 let mut items = Vec::new();
10311 loop {
10312 items.push(self.parse_select_item()?);
10313 if matches!(self.peek(), Token::Comma) {
10314 self.advance();
10315 continue;
10316 }
10317 break;
10318 }
10319 Ok(Some(items))
10320 }
10321
10322 /// v6.0.4 — parse the tail of an ALTER statement after the
10323 /// leading `ALTER` keyword has been consumed. Only one form is
10324 /// supported in v6.0.4:
10325 ///
10326 /// ```text
10327 /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10328 /// ```
10329 fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10330 // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10331 // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10332 // exclusion) is accepted by stripping the `ONLY` keyword
10333 // before the table parse.
10334 // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10335 // and the long PG-dump tail are accepted as no-ops.
10336 match self.advance() {
10337 Token::Index => {}
10338 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10339 // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10340 // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10341 Token::Table => {
10342 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10343 self.advance();
10344 }
10345 return self.parse_alter_table_after_keyword();
10346 }
10347 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10348 return self.parse_alter_policy_after_keyword();
10349 }
10350 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10351 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10352 self.advance();
10353 }
10354 return self.parse_alter_table_after_keyword();
10355 }
10356 // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10357 // of the silent-noop tail.
10358 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10359 return self.parse_alter_sequence_after_keyword();
10360 }
10361 // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10362 // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10363 // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10364 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10365 // NB: the match arm consumed `TYPE` via self.advance(); the
10366 // cursor is now at the type name — do NOT advance again.
10367 let type_name = self.expect_ident_like()?;
10368 let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10369 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10370 if is_add_value {
10371 self.advance(); // ADD
10372 self.advance(); // VALUE
10373 // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10374 // IF/EXISTS as identifiers.
10375 let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10376 {
10377 let n1 = self.tokens.get(self.pos + 1);
10378 let n2 = self.tokens.get(self.pos + 2);
10379 if matches!(n1, Some(Token::Not))
10380 && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10381 {
10382 self.advance();
10383 self.advance();
10384 self.advance();
10385 true
10386 } else {
10387 false
10388 }
10389 } else {
10390 false
10391 };
10392 let label = self.expect_string_literal()?;
10393 let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10394 {
10395 let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10396 self.advance();
10397 let anchor = self.expect_string_literal()?;
10398 Some((is_before, anchor))
10399 } else {
10400 None
10401 };
10402 return Ok(Statement::AlterTypeAddValue {
10403 type_name,
10404 label,
10405 if_not_exists,
10406 position,
10407 });
10408 }
10409 // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10410 // Used to fall into the no-op tail below: accepted, silently
10411 // ignored. `RENAME TO <newtype>` keeps falling through.
10412 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10413 && matches!(
10414 self.tokens.get(self.pos + 1),
10415 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10416 )
10417 {
10418 self.advance(); // RENAME
10419 self.advance(); // VALUE
10420 let old = self.expect_string_literal()?;
10421 if matches!(self.peek(), Token::To) {
10422 self.advance();
10423 } else {
10424 self.expect_keyword_ident("to")?;
10425 }
10426 let new = self.expect_string_literal()?;
10427 return Ok(Statement::AlterTypeRenameValue {
10428 type_name,
10429 old,
10430 new,
10431 });
10432 }
10433 // Other ALTER TYPE forms — the ACTION stays a no-op
10434 // (pg_dump tail), but v7.39 (round 708) the NAME is
10435 // validated: `ALTER TYPE nosuch RENAME TO x` reported
10436 // success for a type that does not exist.
10437 self.consume_until_statement_boundary();
10438 return Ok(Statement::ValidateOnly {
10439 kind: crate::ast::ValidateOnlyKind::TypeName,
10440 names: alloc::vec![type_name],
10441 });
10442 }
10443 // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10444 // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10445 // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10446 // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10447 // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10448 // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10449 // pg_dump no-op list below: every form used to report success
10450 // and change nothing, which is worse than refusing outright
10451 // (a migration dropping a constraint kept rejecting data).
10452 // NOTE: the enclosing `match self.advance()` already consumed
10453 // the DOMAIN keyword, so the name is next.
10454 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10455 return self.parse_alter_domain_after_keyword();
10456 }
10457 // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10458 // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10459 // used to fall into the pg_dump no-op tail below, so a DBA
10460 // setting a per-role default was told it worked and nothing
10461 // happened. Intercepted here, BEFORE that tail.
10462 // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10463 // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10464 // interception below exists: swallowed with the no-op tail, an
10465 // unknown parameter name was ACCEPTED where PG18 answers
10466 // `unrecognized configuration parameter`. SPG applies nothing
10467 // either way — there is no postgresql.auto.conf — but it now
10468 // says so about a name it does not know.
10469 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10470 // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10471 // already consumed here. An extra advance eats the SET and
10472 // the parameter name is never seen — which is exactly the
10473 // bug a panic in this branch disproved: the branch WAS on
10474 // the path, the reading of it was wrong.
10475 let mut parameter = None;
10476 // SET <name> … | RESET <name> | RESET ALL
10477 if matches!(self.peek(), Token::Ident(k)
10478 if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10479 {
10480 self.advance();
10481 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10482 && !n.eq_ignore_ascii_case("all")
10483 {
10484 self.advance();
10485 // A dotted GUC (`plpgsql.check_asserts`) is two
10486 // tokens; keep the whole name.
10487 let mut full = n;
10488 while matches!(self.peek(), Token::Dot) {
10489 self.advance();
10490 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10491 full.push('.');
10492 full.push_str(&t);
10493 }
10494 }
10495 parameter = Some(full);
10496 }
10497 }
10498 self.consume_until_statement_boundary();
10499 return Ok(Statement::AlterSystem { parameter });
10500 }
10501 Token::Ident(s) | Token::QuotedIdent(s)
10502 if matches!(
10503 s.to_ascii_lowercase().as_str(),
10504 "role" | "user" | "database"
10505 ) && self.peeks_db_role_setting() =>
10506 {
10507 let is_database = s.eq_ignore_ascii_case("database");
10508 return self.parse_db_role_setting(is_database);
10509 }
10510 // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10511 // (the non-SET forms; SET/RESET took the branch above). The
10512 // attributes still no-op — recorded, and the ignored PASSWORD
10513 // is ledgered as its own follow-up — but the ROLE is validated:
10514 // any name was accepted for a role that does not exist.
10515 Token::Ident(s) | Token::QuotedIdent(s)
10516 if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10517 {
10518 // NB: the enclosing `match self.advance()` already consumed
10519 // ROLE/USER — the round-695 trap, hit again in this round's
10520 // first draft (the name was eaten and WITH parsed as the
10521 // role). The cursor is at the name.
10522 let name = self.expect_ident_or_string()?;
10523 // v7.39 (round 750) — scan the attribute tail for
10524 // PASSWORD. Everything else stays a recorded no-op, but
10525 // a dropped credential rotation is a SECURITY bug:
10526 // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10527 // changed nothing, so the old password kept working.
10528 // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10529 // NULL` clears the credential.
10530 let mut password: Option<Option<String>> = None;
10531 loop {
10532 match self.peek() {
10533 Token::Semicolon | Token::Eof => break,
10534 Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10535 self.advance();
10536 match self.advance() {
10537 Token::String(p) => password = Some(Some(p)),
10538 Token::Null => password = Some(None),
10539 Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10540 password = Some(None);
10541 }
10542 other => {
10543 return Err(self.err(alloc::format!(
10544 "expected password string or NULL after PASSWORD, got {other:?}"
10545 )));
10546 }
10547 }
10548 }
10549 _ => {
10550 self.advance();
10551 }
10552 }
10553 }
10554 if name.eq_ignore_ascii_case("all") {
10555 // `ALTER ROLE ALL …` names every role; nothing to check.
10556 return Ok(Statement::Empty);
10557 }
10558 if let Some(pw) = password {
10559 return Ok(Statement::AlterRolePassword { name, password: pw });
10560 }
10561 return Ok(Statement::ValidateOnly {
10562 kind: crate::ast::ValidateOnlyKind::RoleName,
10563 names: alloc::vec![name],
10564 });
10565 }
10566 // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10567 // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10568 // list far enough to validate the NAME; the actions still no-op.
10569 // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10570 // models none of them and their dumps are rare.)
10571 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10572 let name = self.expect_ident_or_string()?;
10573 self.consume_until_statement_boundary();
10574 return Ok(Statement::ValidateOnly {
10575 kind: crate::ast::ValidateOnlyKind::CollationName,
10576 names: alloc::vec![name],
10577 });
10578 }
10579 Token::Ident(s) | Token::QuotedIdent(s)
10580 if s.eq_ignore_ascii_case("text")
10581 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10582 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10583 {
10584 self.advance(); // SEARCH
10585 self.advance(); // CONFIGURATION
10586 let name = self.expect_ident_like()?;
10587 self.consume_until_statement_boundary();
10588 return Ok(Statement::ValidateOnly {
10589 kind: crate::ast::ValidateOnlyKind::TsConfigName,
10590 names: alloc::vec![name],
10591 });
10592 }
10593 Token::Ident(s) | Token::QuotedIdent(s)
10594 if s.eq_ignore_ascii_case("event")
10595 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10596 {
10597 self.advance(); // TRIGGER
10598 let name = self.expect_ident_like()?;
10599 self.consume_until_statement_boundary();
10600 return Ok(Statement::ValidateOnly {
10601 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10602 names: alloc::vec![name],
10603 });
10604 }
10605 Token::Ident(s) | Token::QuotedIdent(s)
10606 if s.eq_ignore_ascii_case("large")
10607 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10608 {
10609 self.advance(); // OBJECT
10610 let oid = match self.advance() {
10611 Token::Integer(n) => alloc::format!("{n}"),
10612 other => {
10613 return Err(
10614 self.err(alloc::format!("expected large object oid, got {other:?}"))
10615 );
10616 }
10617 };
10618 self.consume_until_statement_boundary();
10619 return Ok(Statement::ValidateOnly {
10620 kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10621 names: alloc::vec![oid],
10622 });
10623 }
10624 // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10625 // argument-list parse as DROP AGGREGATE (round 707); the
10626 // action no-ops, the existence check is real.
10627 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10628 // Same round-695 trap as above: AGGREGATE is already
10629 // consumed; the cursor is at the name.
10630 let name = self.expect_ident_like()?;
10631 let mut names = alloc::vec![name];
10632 if matches!(self.peek(), Token::LParen) {
10633 self.advance();
10634 loop {
10635 match self.peek().clone() {
10636 Token::RParen => {
10637 self.advance();
10638 break;
10639 }
10640 Token::Star => {
10641 self.advance();
10642 names.push(String::from("*"));
10643 }
10644 Token::Comma => {
10645 self.advance();
10646 }
10647 _ => {
10648 let mut t = self.expect_ident_like()?;
10649 while let Token::Ident(nx) = self.peek() {
10650 let nx = nx.clone();
10651 self.advance();
10652 t.push(' ');
10653 t.push_str(&nx);
10654 }
10655 names.push(t);
10656 }
10657 }
10658 }
10659 }
10660 self.consume_until_statement_boundary();
10661 return Ok(Statement::ValidateOnly {
10662 kind: crate::ast::ValidateOnlyKind::AggregateName,
10663 names,
10664 });
10665 }
10666 Token::Ident(s) | Token::QuotedIdent(s)
10667 if matches!(
10668 s.to_ascii_lowercase().as_str(),
10669 "view"
10670 | "function"
10671 | "database"
10672 | "schema"
10673 | "owner"
10674 | "default"
10675 | "extension"
10676 | "materialized"
10677 | "publication"
10678 | "subscription"
10679 // v7.37.17 (17.6 siblings) — additional ALTER
10680 // targets pg_dump / pg_dumpall / operator DB
10681 // migration scripts commonly emit. SPG has
10682 // no matching machinery for any of these; the
10683 // parser accepts + Empty-returns so pg_dump
10684 // tail statements don't stall.
10685 | "tablespace"
10686 | "language"
10687 | "operator"
10688 | "conversion"
10689 | "statistics"
10690 | "server"
10691 | "foreign"
10692 // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10693 // / TEMPLATE (CONFIGURATION intercepted above).
10694 | "text"
10695 ) =>
10696 {
10697 self.consume_until_statement_boundary();
10698 return Ok(Statement::Empty);
10699 }
10700 other => {
10701 return Err(self.err(format!(
10702 "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10703 after ALTER, got {other:?}"
10704 )));
10705 }
10706 }
10707 // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10708 // (mailrs migrate-042 ships these). The presence of an
10709 // IF EXISTS makes the subsequent name lookup tolerate
10710 // a missing index — engine returns CommandOk no-op.
10711 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10712 let next = self.tokens.get(self.pos + 1);
10713 if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10714 self.advance();
10715 self.advance();
10716 true
10717 } else {
10718 false
10719 }
10720 } else {
10721 false
10722 };
10723 let name = self.expect_ident_like()?;
10724 // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10725 // Detect BEFORE the REBUILD path so the existing REBUILD
10726 // arm stays untouched.
10727 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10728 self.advance();
10729 if matches!(self.peek(), Token::To) {
10730 self.advance();
10731 } else {
10732 self.expect_keyword_ident("to")?;
10733 }
10734 let new = self.expect_ident_like()?;
10735 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10736 name,
10737 target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10738 }));
10739 }
10740 // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10741 // A syntax error before; the index is validated, the params no-op.
10742 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10743 || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10744 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10745 {
10746 self.consume_until_statement_boundary();
10747 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10748 name,
10749 target: crate::ast::AlterIndexTarget::StorageParams,
10750 }));
10751 }
10752 // REBUILD
10753 self.expect_keyword_ident("rebuild")?;
10754 // Optional: WITH (encoding = <enc>)
10755 let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10756 self.advance();
10757 if !matches!(self.peek(), Token::LParen) {
10758 return Err(self.err(format!(
10759 "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10760 self.peek()
10761 )));
10762 }
10763 self.advance();
10764 self.expect_keyword_ident("encoding")?;
10765 if !matches!(self.peek(), Token::Eq) {
10766 return Err(self.err(format!(
10767 "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10768 self.peek()
10769 )));
10770 }
10771 self.advance();
10772 let enc_ident = match self.advance() {
10773 Token::Ident(s) | Token::QuotedIdent(s) => s,
10774 other => {
10775 return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10776 }
10777 };
10778 let enc = match enc_ident.to_ascii_lowercase().as_str() {
10779 "f32" => VecEncoding::F32,
10780 "sq8" => VecEncoding::Sq8,
10781 "half" => VecEncoding::F16,
10782 other => {
10783 return Err(self.err(format!(
10784 "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10785 )));
10786 }
10787 };
10788 if !matches!(self.peek(), Token::RParen) {
10789 return Err(self.err(format!(
10790 "expected ')' after encoding value, got {:?}",
10791 self.peek()
10792 )));
10793 }
10794 self.advance();
10795 Some(enc)
10796 } else {
10797 None
10798 };
10799 Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10800 name,
10801 target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10802 }))
10803 }
10804
10805 /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10806 /// only `SET` form currently supported; future v6.7.x can add
10807 /// more SET subjects without changing the dispatch shape.
10808 /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10809 /// subactions. Single-subaction shape stays a 1-element vec.
10810 fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10811 let table_name = self.expect_ident_like()?;
10812 let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10813 loop {
10814 let subaction = self.parse_alter_table_subaction()?;
10815 // ADD COLUMN with inline REFERENCES emits both an
10816 // AddColumn and an AddForeignKey subaction; the
10817 // helper returns 1 or 2 items.
10818 targets.extend(subaction);
10819 if matches!(self.peek(), Token::Comma) {
10820 self.advance();
10821 continue;
10822 }
10823 break;
10824 }
10825 Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10826 name: table_name,
10827 targets,
10828 }))
10829 }
10830
10831 /// Parse one ALTER TABLE subaction. Returns a Vec because
10832 /// inline `REFERENCES` on `ADD COLUMN` produces both an
10833 /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10834 fn parse_alter_table_subaction(
10835 &mut self,
10836 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10837 match self.peek() {
10838 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10839 self.advance();
10840 // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10841 // storage parameters: paren-prefixed; consume.
10842 if matches!(self.peek(), Token::LParen) {
10843 self.consume_until_statement_boundary();
10844 return Ok(Vec::new());
10845 }
10846 let setting = self.expect_ident_like()?;
10847 if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10848 if !matches!(self.peek(), Token::Eq) {
10849 return Err(self.err(alloc::format!(
10850 "expected '=' after hot_tier_bytes, got {:?}",
10851 self.peek()
10852 )));
10853 }
10854 self.advance();
10855 let n = self.expect_u64_literal()?;
10856 return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10857 }
10858 // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10859 // accept-and-no-op for ALTER TABLE SET <subject>
10860 // forms that pg_dump emits but SPG either treats
10861 // as N/A (single-tenant, single-owner, no shared
10862 // tablespaces) or accepts the dump-side declaration
10863 // without runtime effect:
10864 // SET SCHEMA <name> (18.11)
10865 // SET TABLESPACE <name> (18.8)
10866 // SET LOGGED / UNLOGGED (18.7 alt-form)
10867 // SET WITHOUT CLUSTER (18.13)
10868 // SET WITHOUT OIDS (PG legacy)
10869 // SET (option = value, …) (storage parameters)
10870 // SET REPLICA IDENTITY {…} (18.14)
10871 if setting.eq_ignore_ascii_case("schema")
10872 || setting.eq_ignore_ascii_case("tablespace")
10873 || setting.eq_ignore_ascii_case("logged")
10874 || setting.eq_ignore_ascii_case("unlogged")
10875 || setting.eq_ignore_ascii_case("without")
10876 {
10877 self.consume_until_statement_boundary();
10878 return Ok(Vec::new());
10879 }
10880 if setting.eq_ignore_ascii_case("replica") {
10881 // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10882 self.consume_until_statement_boundary();
10883 return Ok(Vec::new());
10884 }
10885 // SET (option=value, …) — storage parameters.
10886 if matches!(self.peek(), Token::LParen) {
10887 self.consume_until_statement_boundary();
10888 return Ok(Vec::new());
10889 }
10890 Err(self.err(alloc::format!(
10891 "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10892 hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10893 WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10894 )))
10895 }
10896 // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10897 // not ignored: round 645 gave SPG the inheritance the
10898 // v7.37.18 no-op said it did not have.
10899 Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10900 self.advance();
10901 let parent = self.expect_ident_like()?;
10902 self.consume_until_statement_boundary();
10903 Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10904 parent,
10905 detach: false
10906 }])
10907 }
10908 // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10909 // LEVEL SECURITY`, which has its own RLS arm below — without
10910 // the guard this swallowed NO FORCE as a no-op.
10911 Token::Ident(s)
10912 if s.eq_ignore_ascii_case("no")
10913 && !matches!(
10914 self.tokens.get(self.pos + 1),
10915 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10916 ) =>
10917 {
10918 self.advance();
10919 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
10920 if k.eq_ignore_ascii_case("inherit"))
10921 {
10922 self.advance();
10923 let parent = self.expect_ident_like()?;
10924 self.consume_until_statement_boundary();
10925 return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10926 parent,
10927 detach: true
10928 }]);
10929 }
10930 self.consume_until_statement_boundary();
10931 Ok(Vec::new())
10932 }
10933 // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
10934 // single-owner, so there is still nothing to record.
10935 //
10936 // v7.39 (round 652) — but the name now reaches the engine,
10937 // which refuses a role that does not exist as PG does. The
10938 // no-op was swallowing the whole statement, so a dump naming
10939 // a role this server never heard of restored clean and left
10940 // the table owned by whoever ran the restore.
10941 Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
10942 self.advance();
10943 if matches!(self.peek(), Token::To) {
10944 self.advance();
10945 }
10946 let role = self.expect_ident_like()?;
10947 Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
10948 role
10949 }])
10950 }
10951 // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
10952 // PG sets a hint; SPG doesn't have clustered storage, so the
10953 // hint itself stays a no-op.
10954 //
10955 // v7.39 (round 652) — the index name is checked now. PG
10956 // errors on one that does not exist, and swallowing that let
10957 // a typo'd CLUSTER ON pass silently.
10958 Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
10959 self.advance();
10960 // `ON` is a reserved token, not an ident.
10961 if !matches!(self.peek(), Token::On) {
10962 return Err(self.err(alloc::format!(
10963 "expected ON after CLUSTER, got {:?}",
10964 self.peek()
10965 )));
10966 }
10967 self.advance();
10968 let index = self.expect_ident_like()?;
10969 Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
10970 index: Some(index)
10971 }])
10972 }
10973 // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
10974 // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
10975 // what a logical decoder puts in the old-tuple image; SPG's
10976 // replication is SQL-text, so there is nothing to record.
10977 // Accept-and-no-op (it used to be a parse error).
10978 Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
10979 self.advance();
10980 // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
10981 // validates the index; DEFAULT / FULL / NOTHING stay no-op.
10982 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
10983 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
10984 {
10985 self.advance(); // IDENTITY
10986 self.advance(); // USING
10987 if matches!(self.peek(), Token::Index)
10988 || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
10989 {
10990 self.advance();
10991 }
10992 let index = self.expect_ident_like()?;
10993 self.consume_until_statement_boundary();
10994 return Ok(alloc::vec![
10995 crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
10996 ]);
10997 }
10998 self.consume_until_statement_boundary();
10999 Ok(Vec::new())
11000 }
11001 // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
11002 //
11003 // v7.39 (round 652) — it used to consume the statement and
11004 // return nothing, on the stated theory that SPG validated at
11005 // ADD CONSTRAINT time so there was never anything left to
11006 // validate. Measured against PG18, ADD CONSTRAINT did not
11007 // scan the existing rows at all — the comment described a
11008 // property SPG did not have, which is why nobody looked. Both
11009 // halves are real now: ADD scans unless told NOT VALID, and
11010 // this scans what NOT VALID skipped.
11011 Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
11012 self.advance();
11013 self.expect_keyword_ident("constraint")?;
11014 let name = self.expect_ident_like()?;
11015 Ok(alloc::vec![
11016 crate::ast::AlterTableTarget::ValidateConstraint { name }
11017 ])
11018 }
11019 // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
11020 // SET (option = value, …). PG uses it to clear per-table
11021 // storage params like fillfactor or autovacuum_*. SPG
11022 // engine-manages those parameters; accept-and-no-op.
11023 Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
11024 self.advance();
11025 self.consume_until_statement_boundary();
11026 Ok(Vec::new())
11027 }
11028 // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
11029 // type-of binding (PG 9.0+). SPG composite types
11030 // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
11031 // TABLE OF is rare and inverse of CREATE TABLE OF.
11032 // Accept-and-no-op until a customer dump round-trips it.
11033 Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
11034 self.advance();
11035 // v7.39 (round 710) — the type name is validated now.
11036 let type_name = self.expect_ident_like()?;
11037 self.consume_until_statement_boundary();
11038 Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
11039 type_name
11040 }])
11041 }
11042 // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
11043 // (reserved keyword) rather than Token::Ident("not"),
11044 // so it needs its own arm. Accept-and-no-op same as OF.
11045 Token::Not => {
11046 self.advance();
11047 self.consume_until_statement_boundary();
11048 Ok(Vec::new())
11049 }
11050 // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
11051 Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
11052 self.advance();
11053 self.expect_row_level_security()?;
11054 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11055 enabled: None,
11056 force: Some(true),
11057 }])
11058 }
11059 // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
11060 Token::Ident(s)
11061 if s.eq_ignore_ascii_case("no")
11062 && matches!(
11063 self.tokens.get(self.pos + 1),
11064 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11065 ) =>
11066 {
11067 self.advance(); // NO
11068 self.advance(); // FORCE
11069 self.expect_row_level_security()?;
11070 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11071 enabled: None,
11072 force: Some(false),
11073 }])
11074 }
11075 // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
11076 // (sets relrowsecurity). The guard requires the next token to be
11077 // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
11078 Token::Ident(s)
11079 if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
11080 && matches!(
11081 self.tokens.get(self.pos + 1),
11082 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
11083 ) =>
11084 {
11085 let enabled = s.eq_ignore_ascii_case("enable");
11086 self.advance(); // ENABLE/DISABLE
11087 self.expect_row_level_security()?;
11088 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11089 enabled: Some(enabled),
11090 force: None,
11091 }])
11092 }
11093 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11094 self.advance();
11095 // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
11096 // {INDEX|KEY} [name] (cols)`, which every ORM migration
11097 // emits. The same grammar CREATE TABLE already accepts
11098 // inline (`KEY idx (a)`, prefix lengths and all), so it goes
11099 // through the SAME parser — an ALTER-only copy would be a
11100 // second place for the two to drift.
11101 if self.peek_mysql_inline_key_start() {
11102 return Ok(match self.parse_mysql_inline_key()? {
11103 Some(c) => {
11104 alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
11105 }
11106 // FULLTEXT / SPATIAL parse and are accepted as a
11107 // no-op here exactly as they are inline.
11108 None => Vec::new(),
11109 });
11110 }
11111 // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
11112 // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
11113 // PRIMARY KEY this way; mysqldump emits both.
11114 // Peek-only dispatch (no advance) — `advance()`
11115 // destructively replaces consumed tokens with Eof,
11116 // so saved-pos restore would land on Eofs.
11117 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
11118 {
11119 // The next-but-one ident is the constraint
11120 // name; the one after THAT is the kind.
11121 let kind_pos = self.pos + 2;
11122 let kind = self.tokens.get(kind_pos).cloned();
11123 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
11124 {
11125 let fk = self.parse_table_level_fk()?;
11126 return Ok(alloc::vec![
11127 crate::ast::AlterTableTarget::AddForeignKey(fk)
11128 ]);
11129 }
11130 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
11131 {
11132 self.advance(); // CONSTRAINT
11133 // v7.39 (read01 round 48) — keep the name; the engine
11134 // stores it now instead of dropping it on the floor.
11135 let con_name = self.expect_ident_like()?;
11136 self.advance(); // PRIMARY
11137 self.expect_keyword_ident("key")?;
11138 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11139 // v7.39 (round 711) — the ALTER form carries the
11140 // timing too (pg_dump writes it here).
11141 let (deferrable, initially_deferred) =
11142 self.consume_deferrable_clauses_timed()?;
11143 return Ok(alloc::vec![
11144 crate::ast::AlterTableTarget::AddTableConstraint(
11145 crate::ast::TableConstraint::PrimaryKey {
11146 name: Some(con_name),
11147 columns: cols,
11148 deferrable,
11149 initially_deferred,
11150 }
11151 )
11152 ]);
11153 }
11154 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
11155 {
11156 self.advance(); // CONSTRAINT
11157 // v7.39 (read01 round 48) — keep the name.
11158 let con_name = self.expect_ident_like()?;
11159 // v7.22 (mailrs round-13 gap 6) — delegate so
11160 // the optional `NULLS [NOT] DISTINCT` modifier
11161 // parses here too (pg_dump emits the ALTER
11162 // form; semantics enforced by the engine
11163 // since v7.13).
11164 let mut uc = self.parse_table_level_unique()?;
11165 if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
11166 *name = Some(con_name);
11167 }
11168 return Ok(alloc::vec![
11169 crate::ast::AlterTableTarget::AddTableConstraint(uc)
11170 ]);
11171 }
11172 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
11173 {
11174 self.advance(); // CONSTRAINT
11175 // v7.39 (read01 round 48) — keep the name.
11176 let con_name = self.expect_ident_like()?;
11177 self.advance(); // CHECK
11178 if !matches!(self.peek(), Token::LParen) {
11179 return Err(self.err(alloc::format!(
11180 "expected '(' after CHECK, got {:?}", self.peek()
11181 )));
11182 }
11183 self.advance();
11184 let expr = self.parse_expr(0)?;
11185 if matches!(self.peek(), Token::RParen) {
11186 self.advance();
11187 }
11188 let not_valid = self.parse_not_valid_suffix();
11189 return Ok(alloc::vec![
11190 crate::ast::AlterTableTarget::AddTableConstraint(
11191 crate::ast::TableConstraint::Check {
11192 name: Some(con_name),
11193 expr,
11194 not_valid,
11195 }
11196 )
11197 ]);
11198 }
11199 // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
11200 // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
11201 // exclusion constraints via this ALTER form.
11202 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
11203 {
11204 self.advance(); // CONSTRAINT
11205 let con_name = self.expect_ident_like()?;
11206 let mut ex = self.parse_table_level_exclude()?;
11207 if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
11208 *name = Some(con_name);
11209 }
11210 return Ok(alloc::vec![
11211 crate::ast::AlterTableTarget::AddTableConstraint(ex)
11212 ]);
11213 }
11214 // Unknown kind — fall through to FK path which
11215 // produces a descriptive parse error.
11216 }
11217 let is_fk = matches!(
11218 self.peek(),
11219 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
11220 || s.eq_ignore_ascii_case("foreign")
11221 );
11222 if is_fk {
11223 let fk = self.parse_table_level_fk()?;
11224 return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
11225 }
11226 // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
11227 // (no CONSTRAINT prefix) — same dispatch.
11228 match self.peek().clone() {
11229 Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
11230 self.advance();
11231 self.expect_keyword_ident("key")?;
11232 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11233 let (deferrable, initially_deferred) =
11234 self.consume_deferrable_clauses_timed()?;
11235 return Ok(alloc::vec![
11236 crate::ast::AlterTableTarget::AddTableConstraint(
11237 crate::ast::TableConstraint::PrimaryKey {
11238 name: None,
11239 columns: cols,
11240 deferrable,
11241 initially_deferred,
11242 }
11243 )
11244 ]);
11245 }
11246 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
11247 // v7.22 — delegate (NULLS [NOT] DISTINCT).
11248 let uc = self.parse_table_level_unique()?;
11249 return Ok(alloc::vec![
11250 crate::ast::AlterTableTarget::AddTableConstraint(uc)
11251 ]);
11252 }
11253 // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
11254 // prefix). The other three bare forms were here and
11255 // this one was not, so it fell through to the column
11256 // path and came back as "unexpected reserved keyword
11257 // 'check' at start of column definition".
11258 _ if self.peek_table_level_check_start() => {
11259 let chk = self.parse_table_level_check()?;
11260 let not_valid = self.parse_not_valid_suffix();
11261 let crate::ast::TableConstraint::Check { expr, .. } = chk else {
11262 unreachable!("parse_table_level_check returns Check")
11263 };
11264 return Ok(alloc::vec![
11265 crate::ast::AlterTableTarget::AddTableConstraint(
11266 crate::ast::TableConstraint::Check {
11267 name: None,
11268 expr,
11269 not_valid,
11270 }
11271 )
11272 ]);
11273 }
11274 // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11275 Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11276 let ex = self.parse_table_level_exclude()?;
11277 return Ok(alloc::vec![
11278 crate::ast::AlterTableTarget::AddTableConstraint(ex)
11279 ]);
11280 }
11281 _ => {}
11282 }
11283 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11284 self.advance();
11285 }
11286 let mut if_not_exists = false;
11287 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11288 self.advance();
11289 if !matches!(self.peek(), Token::Not) {
11290 return Err(self.err(alloc::format!(
11291 "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11292 self.peek()
11293 )));
11294 }
11295 self.advance();
11296 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11297 return Err(self.err(alloc::format!(
11298 "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11299 self.peek()
11300 )));
11301 }
11302 self.advance();
11303 if_not_exists = true;
11304 }
11305 // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11306 // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11307 // returns ColumnDef + an optional inline FK.
11308 let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11309 let col_name = column.name.clone();
11310 let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11311 column,
11312 if_not_exists,
11313 }];
11314 if let Some(mut fk) = col_level_fk {
11315 if fk.columns.is_empty() {
11316 fk.columns.push(col_name);
11317 }
11318 out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11319 }
11320 Ok(out)
11321 }
11322 Token::Drop => {
11323 self.advance();
11324 // v7.13.3 — dispatch on the next token. mailrs round-7
11325 // S8 closed DROP COLUMN; round-6 S7 closed
11326 // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11327 // RESTRICT modifiers.
11328 // DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11329 // DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11330 let subject = match self.peek() {
11331 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11332 self.advance();
11333 "constraint"
11334 }
11335 Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11336 self.advance();
11337 "column"
11338 }
11339 // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11340 // `INDEX` lexes as the reserved Token::Index, so it is
11341 // unambiguous. `KEY` is a plain ident, and PG allows a
11342 // column literally named "key", so only read it as the
11343 // keyword when a name follows it.
11344 Token::Index => {
11345 self.advance();
11346 "index"
11347 }
11348 Token::Ident(s)
11349 if s.eq_ignore_ascii_case("key")
11350 && matches!(
11351 self.tokens.get(self.pos + 1),
11352 Some(Token::Ident(_) | Token::QuotedIdent(_))
11353 ) =>
11354 {
11355 self.advance();
11356 "index"
11357 }
11358 // PG-canonical bare `DROP <col>` without COLUMN
11359 // keyword is also valid; treat any other ident
11360 // as the column name.
11361 Token::Ident(_) | Token::QuotedIdent(_) => "column",
11362 other => {
11363 return Err(self.err(alloc::format!(
11364 "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11365 )));
11366 }
11367 };
11368 let mut if_exists = false;
11369 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11370 let n1 = self.tokens.get(self.pos + 1);
11371 if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11372 self.advance();
11373 self.advance();
11374 if_exists = true;
11375 }
11376 }
11377 let name = self.expect_ident_like()?;
11378 let mut cascade = false;
11379 if matches!(
11380 self.peek(),
11381 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11382 || s.eq_ignore_ascii_case("restrict")
11383 ) {
11384 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11385 {
11386 cascade = true;
11387 }
11388 self.advance();
11389 }
11390 if subject == "index" {
11391 Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11392 name,
11393 if_exists,
11394 }])
11395 } else if subject == "constraint" {
11396 Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11397 name,
11398 if_exists,
11399 }])
11400 } else {
11401 Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11402 column: name,
11403 if_exists,
11404 cascade,
11405 }])
11406 }
11407 }
11408 Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11409 self.advance();
11410 // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11411 // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11412 // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11413 // immediately; accept-and-no-op.
11414 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11415 self.advance();
11416 self.consume_until_statement_boundary();
11417 return Ok(Vec::new());
11418 }
11419 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11420 self.advance();
11421 }
11422 let col_name = self.expect_ident_like()?;
11423 match self.peek() {
11424 Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11425 self.advance();
11426 }
11427 // v7.14.0 — pg_dump emits BIGSERIAL via
11428 // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11429 // nextval('seq')` (the sequence is created
11430 // separately). SPG's BIGSERIAL already uses
11431 // AUTO_INCREMENT; accept SET DEFAULT / DROP
11432 // DEFAULT / SET NOT NULL / DROP NOT NULL as
11433 // engine no-ops by consuming the tail.
11434 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11435 // v7.22 (round-13 T2) — `SET DEFAULT
11436 // nextval('…')` is how pg_dump spells a
11437 // SERIAL column (plain integer in CREATE
11438 // TABLE + this ALTER). It used to be
11439 // swallowed as a no-op, which silently
11440 // STRIPPED auto-increment from imported
11441 // schemas — the first post-import INSERT
11442 // without an explicit id then violated NOT
11443 // NULL. Lower it to the auto-increment
11444 // marker instead.
11445 let is_default_nextval =
11446 matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11447 && matches!(
11448 self.tokens.get(self.pos + 2),
11449 Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11450 );
11451 if is_default_nextval {
11452 let seq_name = self.scan_sequence_name_until_boundary();
11453 return Ok(alloc::vec![
11454 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11455 column: col_name,
11456 seq_name,
11457 }
11458 ]);
11459 }
11460 // v7.37.18 (18.1 + 18.2) — proper lowering.
11461 self.advance(); // consume "set"
11462 match self.peek().clone() {
11463 Token::Default => {
11464 self.advance();
11465 let default_expr = self.parse_expr(0)?;
11466 return Ok(alloc::vec![
11467 crate::ast::AlterTableTarget::AlterColumnSetDefault {
11468 column: col_name,
11469 default_expr,
11470 }
11471 ]);
11472 }
11473 Token::Not => {
11474 self.advance();
11475 if !matches!(self.peek(), Token::Null) {
11476 return Err(self.err(alloc::format!(
11477 "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11478 self.peek()
11479 )));
11480 }
11481 self.advance();
11482 return Ok(alloc::vec![
11483 crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11484 column: col_name,
11485 }
11486 ]);
11487 }
11488 // `SET EXPRESSION AS (expr)` (PG 17) — change a
11489 // stored generated column's expression and
11490 // recompute existing rows.
11491 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11492 self.advance(); // EXPRESSION
11493 if matches!(self.peek(), Token::As) {
11494 self.advance();
11495 }
11496 let expr = self.parse_expr(0)?;
11497 return Ok(alloc::vec![
11498 crate::ast::AlterTableTarget::AlterColumnSetExpression {
11499 column: col_name,
11500 expr,
11501 }
11502 ]);
11503 }
11504 other => {
11505 // Other SET subjects (STATISTICS,
11506 // STORAGE, COMPRESSION, …) stay no-ops —
11507 // storage hints with no SPG semantics.
11508 let _ = other;
11509 self.consume_until_statement_boundary();
11510 return Ok(Vec::new());
11511 }
11512 }
11513 }
11514 Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11515 self.advance(); // consume "drop"
11516 return self.parse_alter_column_drop_tail(col_name);
11517 }
11518 Token::Drop => {
11519 self.advance(); // consume Drop token
11520 return self.parse_alter_column_drop_tail(col_name);
11521 }
11522 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11523 // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11524 // GENERATED { ALWAYS | BY DEFAULT } AS
11525 // IDENTITY ( … )`: pg_dump's spelling for
11526 // identity columns. Same auto-increment
11527 // lowering as the nextval default; the
11528 // sequence options inside the parens are
11529 // no-ops under SPG's max+1 semantics.
11530 let is_generated = matches!(
11531 self.tokens.get(self.pos + 1),
11532 Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11533 );
11534 if !is_generated {
11535 return Err(self.err(alloc::format!(
11536 "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11537 self.tokens.get(self.pos + 1)
11538 )));
11539 }
11540 let seq_name = self.scan_sequence_name_until_boundary();
11541 return Ok(alloc::vec![
11542 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11543 column: col_name,
11544 seq_name,
11545 }
11546 ]);
11547 }
11548 // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11549 // column: floor the next allocated value at n (bare
11550 // RESTART = restart from the start value, 1).
11551 Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11552 self.advance();
11553 let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11554 {
11555 self.advance();
11556 let neg = if matches!(self.peek(), Token::Minus) {
11557 self.advance();
11558 true
11559 } else {
11560 false
11561 };
11562 match self.advance() {
11563 Token::Integer(v) => Some(if neg { -v } else { v }),
11564 other => {
11565 return Err(self.err(alloc::format!(
11566 "expected integer after RESTART WITH, got {other:?}"
11567 )));
11568 }
11569 }
11570 } else {
11571 None
11572 };
11573 return Ok(alloc::vec![
11574 crate::ast::AlterTableTarget::AlterColumnRestart {
11575 column: col_name,
11576 with,
11577 }
11578 ]);
11579 }
11580 other => {
11581 return Err(self.err(alloc::format!(
11582 "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11583 )));
11584 }
11585 }
11586 // v7.39 (round 713) — the type parser has consumed a
11587 // trailing `COLLATE <name>` since Phase 2.5, and
11588 // `parse_column_type_name` discarded it: `ALTER COLUMN t
11589 // TYPE text COLLATE "C"` parsed clean and changed
11590 // nothing. Keep the clause; the engine re-collates.
11591 let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _) =
11592 self.parse_type_with_implied_flags()?;
11593 let collation = if coll_explicit {
11594 coll_name.map(|n| (coll, n))
11595 } else {
11596 None
11597 };
11598 let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11599 {
11600 self.advance();
11601 Some(self.parse_expr(0)?)
11602 } else {
11603 None
11604 };
11605 Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11606 column: col_name,
11607 new_type,
11608 using,
11609 collation,
11610 }])
11611 }
11612 // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11613 // PG also supports `RENAME TO new_table` for table-name
11614 // rename; that surface is deferred (pg_dump never emits
11615 // it). If the first post-RENAME ident is `TO`, the user
11616 // is asking for table rename — error with a clear
11617 // message rather than misparsing `TO` as a column name.
11618 Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11619 self.advance();
11620 // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11621 // table-name rename (mailrs round-10 A.5 — used
11622 // by migrate-042's `RENAME TO email_contacts`).
11623 // `TO` lexes as Token::To.
11624 if matches!(self.peek(), Token::To)
11625 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11626 {
11627 self.advance();
11628 let new = self.expect_ident_like()?;
11629 return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11630 new,
11631 }]);
11632 }
11633 // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11634 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11635 self.advance();
11636 let old = self.expect_ident_like()?;
11637 if matches!(self.peek(), Token::To) {
11638 self.advance();
11639 } else {
11640 self.expect_keyword_ident("to")?;
11641 }
11642 let new = self.expect_ident_like()?;
11643 return Ok(alloc::vec![
11644 crate::ast::AlterTableTarget::RenameConstraint { old, new }
11645 ]);
11646 }
11647 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11648 self.advance();
11649 }
11650 let old = self.expect_ident_like()?;
11651 // `TO` is a reserved keyword token; accept both
11652 // Token::To and Token::Ident("to") for consistency.
11653 if matches!(self.peek(), Token::To) {
11654 self.advance();
11655 } else {
11656 self.expect_keyword_ident("to")?;
11657 }
11658 let new = self.expect_ident_like()?;
11659 Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11660 old,
11661 new,
11662 }])
11663 }
11664 // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11665 // { ALL | <name> }`. pg_dump --disable-triggers wraps
11666 // every data block with these. Real disable semantics —
11667 // not no-op — because reload correctness assumes the
11668 // triggers don't fire (rows already carry their
11669 // computed values from prod).
11670 Token::Ident(s)
11671 if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11672 {
11673 let enabled = s.eq_ignore_ascii_case("enable");
11674 self.advance();
11675 // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11676 // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11677 // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11678 // pg_dump output) — anything else falls through to
11679 // the catch-all error below.
11680 // v7.22 (round-13 T3) — mysqldump wraps every data
11681 // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11682 // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11683 // maintains indexes incrementally — engine no-op.
11684 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11685 self.advance();
11686 return Ok(Vec::new());
11687 }
11688 // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11689 // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11690 // to gate triggers on session_replication_role; SPG
11691 // has no replica role, so the prefix is consumed and
11692 // treated identically to the plain ENABLE/DISABLE
11693 // TRIGGER form.
11694 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11695 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11696 {
11697 self.advance();
11698 }
11699 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11700 return Err(self.err(alloc::format!(
11701 "expected TRIGGER after {}, got {:?}",
11702 if enabled { "ENABLE" } else { "DISABLE" },
11703 self.peek()
11704 )));
11705 }
11706 self.advance();
11707 // `ALL` lexes as Token::All (reserved); also
11708 // accept Token::Ident("all") for symmetry.
11709 // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11710 // TRIGGER selectors. USER (= all user triggers) is
11711 // semantically ALL here; REPLICA / ALWAYS gate on
11712 // session_replication_role which SPG doesn't track.
11713 // All map to TriggerSelector::All.
11714 let which = if matches!(self.peek(), Token::All)
11715 || matches!(self.peek(), Token::Ident(s)
11716 if s.eq_ignore_ascii_case("all")
11717 || s.eq_ignore_ascii_case("user")
11718 || s.eq_ignore_ascii_case("replica")
11719 || s.eq_ignore_ascii_case("always"))
11720 {
11721 self.advance();
11722 crate::ast::TriggerSelector::All
11723 } else {
11724 let name = self.expect_ident_like()?;
11725 crate::ast::TriggerSelector::Named(name)
11726 };
11727 Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11728 which,
11729 enabled,
11730 }])
11731 }
11732 // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11733 Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11734 self.advance();
11735 if !matches!(self.peek(), Token::Partition)
11736 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11737 if s.eq_ignore_ascii_case("partition"))
11738 {
11739 return Err(self.err(alloc::format!(
11740 "expected PARTITION after ATTACH, got {:?}",
11741 self.peek()
11742 )));
11743 }
11744 self.advance();
11745 let child = self.expect_ident_like()?;
11746 let bounds = self.parse_partition_bounds_tail()?;
11747 Ok(alloc::vec![
11748 crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11749 ])
11750 }
11751 // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11752 Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11753 self.advance();
11754 if !matches!(self.peek(), Token::Partition)
11755 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11756 if s.eq_ignore_ascii_case("partition"))
11757 {
11758 return Err(self.err(alloc::format!(
11759 "expected PARTITION after DETACH, got {:?}",
11760 self.peek()
11761 )));
11762 }
11763 self.advance();
11764 let child = self.expect_ident_like()?;
11765 let mut concurrently = false;
11766 let mut finalize = false;
11767 loop {
11768 match self.peek().clone() {
11769 Token::Ident(s) | Token::QuotedIdent(s)
11770 if s.eq_ignore_ascii_case("concurrently") =>
11771 {
11772 self.advance();
11773 concurrently = true;
11774 }
11775 Token::Ident(s) | Token::QuotedIdent(s)
11776 if s.eq_ignore_ascii_case("finalize") =>
11777 {
11778 self.advance();
11779 finalize = true;
11780 }
11781 _ => break,
11782 }
11783 }
11784 Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11785 child,
11786 concurrently,
11787 finalize,
11788 }])
11789 }
11790 other => Err(self.err(alloc::format!(
11791 "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11792 ))),
11793 }
11794 }
11795
11796 /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11797 /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11798 /// TABLE … ATTACH PARTITION. Shares the same grammar as
11799 /// `parse_partition_of_tail`'s bounds branch.
11800 /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11801 /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11802 /// lowering each to the respective AlterTableTarget. Any
11803 /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11804 /// no-op via consume_until_statement_boundary.
11805 fn parse_alter_column_drop_tail(
11806 &mut self,
11807 col_name: String,
11808 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11809 match self.peek().clone() {
11810 Token::Default => {
11811 self.advance();
11812 Ok(alloc::vec![
11813 crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11814 ])
11815 }
11816 Token::Not => {
11817 self.advance();
11818 if !matches!(self.peek(), Token::Null) {
11819 return Err(self.err(alloc::format!(
11820 "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11821 self.peek()
11822 )));
11823 }
11824 self.advance();
11825 Ok(alloc::vec![
11826 crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11827 ])
11828 }
11829 // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11830 // generated column into a plain column.
11831 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11832 self.advance();
11833 // v7.39 (round 187, U10) — IF EXISTS was consumed but
11834 // dropped, so the engine still errored on a plain
11835 // column; PG's semantics are NOTICE + skip.
11836 let mut if_exists = false;
11837 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11838 self.advance();
11839 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11840 self.advance();
11841 if_exists = true;
11842 }
11843 }
11844 Ok(alloc::vec![
11845 crate::ast::AlterTableTarget::AlterColumnDropExpression {
11846 column: col_name,
11847 if_exists,
11848 }
11849 ])
11850 }
11851 // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11852 // identity column into a plain column.
11853 Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11854 self.advance();
11855 let mut if_exists = false;
11856 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11857 self.advance();
11858 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11859 self.advance();
11860 if_exists = true;
11861 }
11862 }
11863 Ok(alloc::vec![
11864 crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11865 column: col_name,
11866 if_exists,
11867 }
11868 ])
11869 }
11870 _ => {
11871 self.consume_until_statement_boundary();
11872 Ok(Vec::new())
11873 }
11874 }
11875 }
11876
11877 /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11878 /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11879 /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11880 /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11881 fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11882 let mut opts = crate::ast::CopyOptions::default();
11883 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11884 return Ok(opts);
11885 }
11886 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11887 self.advance();
11888 }
11889 if matches!(self.peek(), Token::LParen) {
11890 self.advance();
11891 loop {
11892 self.parse_one_copy_option(&mut opts)?;
11893 match self.peek() {
11894 Token::Comma => {
11895 self.advance();
11896 }
11897 Token::RParen => {
11898 self.advance();
11899 break;
11900 }
11901 other => {
11902 return Err(self.err(alloc::format!(
11903 "expected ',' or ')' in COPY options, got {other:?}"
11904 )));
11905 }
11906 }
11907 }
11908 } else {
11909 while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11910 self.parse_one_copy_option(&mut opts)?;
11911 }
11912 }
11913 if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11914 return Err(self.err(alloc::format!(
11915 "unexpected token after COPY options: {:?}",
11916 self.peek()
11917 )));
11918 }
11919 Ok(opts)
11920 }
11921
11922 fn parse_one_copy_option(
11923 &mut self,
11924 opts: &mut crate::ast::CopyOptions,
11925 ) -> Result<(), ParseError> {
11926 use crate::ast::CopyFormat;
11927 // The option keyword. NULL lexes as its own token; the rest are
11928 // bare identifiers.
11929 let kw = match self.advance() {
11930 Token::Null => alloc::string::String::from("NULL"),
11931 Token::Ident(s) => s.to_uppercase(),
11932 other => {
11933 return Err(self.err(alloc::format!(
11934 "expected a COPY option keyword, got {other:?}"
11935 )));
11936 }
11937 };
11938 match kw.as_str() {
11939 "FORMAT" => {
11940 let fmt = self.expect_ident_like()?;
11941 match fmt.to_ascii_uppercase().as_str() {
11942 "CSV" => opts.format = CopyFormat::Csv,
11943 "TEXT" => opts.format = CopyFormat::Text,
11944 other => {
11945 return Err(self.err(alloc::format!(
11946 "COPY format \"{}\" not recognized",
11947 other.to_ascii_lowercase()
11948 )));
11949 }
11950 }
11951 }
11952 // Legacy bare format keywords.
11953 "CSV" => opts.format = CopyFormat::Csv,
11954 "TEXT" => opts.format = CopyFormat::Text,
11955 "HEADER" => {
11956 opts.header = match self.peek() {
11957 Token::True => {
11958 self.advance();
11959 true
11960 }
11961 Token::False => {
11962 self.advance();
11963 false
11964 }
11965 Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
11966 self.advance();
11967 true
11968 }
11969 Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
11970 self.advance();
11971 false
11972 }
11973 // Bare HEADER (no boolean) means HEADER true.
11974 _ => true,
11975 };
11976 }
11977 // r1066 (7.38 S5.1) — pgbench 14+ loads with
11978 // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
11979 // vacuum bookkeeping on a freshly created/truncated
11980 // table; SPG's per-statement visibility makes it a
11981 // faithful no-op, and rejecting it aborted `pgbench -i`
11982 // against the drop-in. Accept ON/OFF/bare, change nothing.
11983 "FREEZE" => match self.peek() {
11984 Token::True | Token::False => {
11985 self.advance();
11986 }
11987 Token::Ident(s)
11988 if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
11989 {
11990 self.advance();
11991 }
11992 _ => {}
11993 },
11994 "DELIMITER" | "QUOTE" | "ESCAPE" => {
11995 let s = match self.advance() {
11996 Token::String(s) => s,
11997 other => {
11998 return Err(self.err(alloc::format!(
11999 "COPY {kw} expects a single-character string, got {other:?}"
12000 )));
12001 }
12002 };
12003 // v7.39 (round 247) — PG's wording (0A000), keyword in
12004 // lowercase: "COPY delimiter must be a single one-byte
12005 // character".
12006 let one_byte_err = || {
12007 self.err(alloc::format!(
12008 "COPY {} must be a single one-byte character",
12009 kw.to_ascii_lowercase()
12010 ))
12011 };
12012 let mut chars = s.chars();
12013 let c = chars.next().ok_or_else(one_byte_err)?;
12014 if chars.next().is_some() || c.len_utf8() != 1 {
12015 return Err(one_byte_err());
12016 }
12017 match kw.as_str() {
12018 "DELIMITER" => opts.delimiter = Some(c),
12019 "QUOTE" => opts.quote = Some(c),
12020 _ => opts.escape = Some(c),
12021 }
12022 }
12023 // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
12024 "FORCE_QUOTE" => {
12025 if matches!(self.peek(), Token::Star) {
12026 self.advance();
12027 opts.force_quote = Some(Vec::new());
12028 } else {
12029 if !matches!(self.peek(), Token::LParen) {
12030 return Err(self.err(alloc::format!(
12031 "expected '(' or '*' after FORCE_QUOTE, got {:?}",
12032 self.peek()
12033 )));
12034 }
12035 self.advance();
12036 let mut cols = Vec::new();
12037 loop {
12038 cols.push(self.expect_ident_like()?);
12039 match self.peek() {
12040 Token::Comma => {
12041 self.advance();
12042 }
12043 Token::RParen => {
12044 self.advance();
12045 break;
12046 }
12047 other => {
12048 return Err(self.err(alloc::format!(
12049 "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
12050 )));
12051 }
12052 }
12053 }
12054 opts.force_quote = Some(cols);
12055 }
12056 }
12057 "NULL" => {
12058 opts.null_str = Some(match self.advance() {
12059 Token::String(s) => s,
12060 other => {
12061 return Err(self.err(alloc::format!(
12062 "COPY NULL expects a quoted string, got {other:?}"
12063 )));
12064 }
12065 });
12066 }
12067 // v7.39 (round 265) — the two CSV FROM-side column lists. Same
12068 // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
12069 // FORCE_NULL too.
12070 "FORCE_NOT_NULL" | "FORCE_NULL" => {
12071 let cols = self.parse_copy_column_list(&kw)?;
12072 if kw == "FORCE_NOT_NULL" {
12073 opts.force_not_null = Some(cols);
12074 } else {
12075 opts.force_null = Some(cols);
12076 }
12077 }
12078 other => {
12079 // PG's wording, lowercased option name.
12080 return Err(self.err(alloc::format!(
12081 "option \"{}\" not recognized",
12082 other.to_ascii_lowercase()
12083 )));
12084 }
12085 }
12086 Ok(())
12087 }
12088
12089 /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
12090 /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
12091 /// is the `*` spelling.
12092 fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
12093 if matches!(self.peek(), Token::Star) {
12094 self.advance();
12095 return Ok(Vec::new());
12096 }
12097 if !matches!(self.peek(), Token::LParen) {
12098 return Err(self.err(alloc::format!(
12099 "expected '(' or '*' after {kw}, got {:?}",
12100 self.peek()
12101 )));
12102 }
12103 self.advance();
12104 let mut cols = Vec::new();
12105 loop {
12106 cols.push(self.expect_ident_like()?);
12107 match self.peek() {
12108 Token::Comma => {
12109 self.advance();
12110 }
12111 Token::RParen => {
12112 self.advance();
12113 break;
12114 }
12115 other => {
12116 return Err(self.err(alloc::format!(
12117 "expected ',' or ')' in {kw} list, got {other:?}"
12118 )));
12119 }
12120 }
12121 }
12122 Ok(cols)
12123 }
12124
12125 fn parse_partition_bounds_tail(
12126 &mut self,
12127 ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
12128 use crate::ast::PartitionOfBoundsAst;
12129 match self.peek() {
12130 Token::Default => {
12131 self.advance();
12132 Ok(PartitionOfBoundsAst::Default)
12133 }
12134 Token::For => {
12135 self.advance();
12136 if !matches!(self.peek(), Token::Values) {
12137 return Err(
12138 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
12139 );
12140 }
12141 self.advance();
12142 let want_with = matches!(
12143 self.peek(),
12144 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
12145 );
12146 if want_with {
12147 self.advance();
12148 if !matches!(self.peek(), Token::LParen) {
12149 return Err(self.err(format!(
12150 "expected '(' after FOR VALUES WITH, got {:?}",
12151 self.peek()
12152 )));
12153 }
12154 self.advance();
12155 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
12156 loop {
12157 let key = self.expect_ident_like()?;
12158 let n = match self.peek().clone() {
12159 Token::Integer(v) if u32::try_from(v).is_ok() => {
12160 self.advance();
12161 v as u32
12162 }
12163 other => {
12164 return Err(self.err(format!(
12165 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
12166 )));
12167 }
12168 };
12169 match key.to_ascii_uppercase().as_str() {
12170 "MODULUS" => modulus = Some(n),
12171 "REMAINDER" => remainder = Some(n),
12172 other => {
12173 return Err(self.err(format!(
12174 "FOR VALUES WITH: unknown key {other:?}; \
12175 expected MODULUS or REMAINDER"
12176 )));
12177 }
12178 }
12179 match self.peek() {
12180 Token::Comma => {
12181 self.advance();
12182 }
12183 Token::RParen => {
12184 self.advance();
12185 break;
12186 }
12187 other => {
12188 return Err(self.err(format!(
12189 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
12190 )));
12191 }
12192 }
12193 }
12194 let modulus = modulus
12195 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
12196 let remainder = remainder.ok_or_else(|| {
12197 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
12198 })?;
12199 if modulus == 0 {
12200 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
12201 }
12202 if remainder >= modulus {
12203 return Err(self.err(format!(
12204 "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
12205 )));
12206 }
12207 return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
12208 }
12209 match self.peek() {
12210 Token::From => {
12211 self.advance();
12212 let lower = Box::new(self.parse_partition_bound_expr()?);
12213 if !matches!(self.peek(), Token::To) {
12214 return Err(self.err(format!(
12215 "expected TO after FROM (...), got {:?}",
12216 self.peek()
12217 )));
12218 }
12219 self.advance();
12220 let upper = Box::new(self.parse_partition_bound_expr()?);
12221 Ok(PartitionOfBoundsAst::Range { lower, upper })
12222 }
12223 Token::In => {
12224 self.advance();
12225 if !matches!(self.peek(), Token::LParen) {
12226 return Err(self.err(format!(
12227 "expected '(' after FOR VALUES IN, got {:?}",
12228 self.peek()
12229 )));
12230 }
12231 self.advance();
12232 let mut values = Vec::new();
12233 loop {
12234 values.push(self.parse_expr(0)?);
12235 match self.peek() {
12236 Token::Comma => {
12237 self.advance();
12238 }
12239 Token::RParen => {
12240 self.advance();
12241 break;
12242 }
12243 other => {
12244 return Err(self.err(format!(
12245 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
12246 )));
12247 }
12248 }
12249 }
12250 if values.is_empty() {
12251 return Err(
12252 self.err("FOR VALUES IN requires at least one literal".to_string())
12253 );
12254 }
12255 Ok(PartitionOfBoundsAst::List { values })
12256 }
12257 other => Err(self.err(format!(
12258 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
12259 ))),
12260 }
12261 }
12262 other => Err(self.err(format!(
12263 "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
12264 ))),
12265 }
12266 }
12267
12268 /// v7.16.2 — peek for `information_schema.<tbl>` /
12269 /// `pg_catalog.<tbl>` triples and, if matched, consume all
12270 /// three tokens + return a synthetic table name the engine's
12271 /// SELECT path recognises as a virtual view. Returns `None`
12272 /// when the head doesn't look like a meta-qualified name.
12273 /// Used by `parse_table_ref` to bypass the
12274 /// `expect_ident_like` schema-strip for these specific PG
12275 /// meta schemas (mailrs round-10 A.3).
12276 fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12277 // Extract the schema name. Must be a plain ident token.
12278 let schema = match self.tokens.get(self.pos) {
12279 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12280 _ => return None,
12281 };
12282 // Dot.
12283 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12284 return None;
12285 }
12286 // The table-side ident may lex as a reserved keyword
12287 // (e.g. `Token::Tables`). Tolerate the common ones via a
12288 // helper that reads the trailing token's underlying name.
12289 let tbl = match self.tokens.get(self.pos + 2)? {
12290 Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12291 Token::Tables => "tables".to_string(),
12292 // Other PG meta table names that may collide with
12293 // reserved keywords land here as needed.
12294 _ => return None,
12295 };
12296 // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12297 // names so the synthetic name doesn't double-prefix
12298 // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12299 let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12300 ("__spg_info_", tbl.to_ascii_lowercase())
12301 } else if schema.eq_ignore_ascii_case("pg_catalog") {
12302 // v7.39 (round 541) — only the catalogs SPG actually
12303 // synthesises are rewritten, which is what the BARE path
12304 // has always checked. Anything else keeps its own name and
12305 // takes the ordinary route: `pg_stat_activity` and friends
12306 // resolve through meta_view_result, and a name that is no
12307 // catalog at all gets PG's "relation does not exist"
12308 // instead of a message about a view SPG cannot materialise.
12309 let lowered = tbl.to_ascii_lowercase();
12310 if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12311 self.advance(); // schema
12312 self.advance(); // dot
12313 self.advance(); // tbl
12314 return Some((lowered.clone(), lowered));
12315 }
12316 let bare = lowered
12317 .strip_prefix("pg_")
12318 .map(alloc::string::String::from)
12319 .unwrap_or(lowered);
12320 ("__spg_pg_", bare)
12321 } else if schema.eq_ignore_ascii_case("mysql") {
12322 // v7.17.0 Phase 3.P0-65 — MySQL system schema
12323 // (`mysql.user`, `mysql.db`). Same synthetic-name
12324 // shape as pg_catalog.
12325 ("__spg_mysql_", tbl.to_ascii_lowercase())
12326 } else {
12327 return None;
12328 };
12329 self.advance(); // schema
12330 self.advance(); // dot
12331 self.advance(); // tbl
12332 Some((
12333 alloc::format!("{prefix}{normalised}"),
12334 tbl.to_ascii_lowercase(),
12335 ))
12336 }
12337
12338 /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12339 /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12340 /// implicit front of every search_path, so a bare reference to a
12341 /// known catalog table always means the catalog table. Only the
12342 /// names the engine actually synthesises are recognised — any
12343 /// other `pg_*` ident stays a user table (mailrs embed round-12).
12344 fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12345 // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12346 // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12347 // `pg_catalog` at the front of every search_path. (pg_stat_activity
12348 // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12349 // through the meta_view_result path instead, and already resolve
12350 // bare — they must NOT be listed here or the __spg_ rewrite would
12351 // mis-target them.)
12352 const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12353 let name = match self.tokens.get(self.pos) {
12354 Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12355 _ => return None,
12356 };
12357 // A following dot means this ident is a schema qualifier,
12358 // not a table name — let the qualified path handle it.
12359 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12360 return None;
12361 }
12362 if !PG_META_TABLES.contains(&name.as_str()) {
12363 return None;
12364 }
12365 self.advance();
12366 let bare = name.strip_prefix("pg_").unwrap_or(&name);
12367 Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12368 }
12369
12370 /// Consume a bare ident if its lowercase matches `kw`, else err.
12371 /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12372 /// Peeks only; the caller advances.
12373 fn peek_keyword_ident(&self, kw: &str) -> bool {
12374 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12375 }
12376
12377 fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12378 match self.advance() {
12379 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12380 other => Err(ParseError {
12381 message: format!("expected {kw:?}, got {other:?}"),
12382 token_pos: self.consumed_pos(),
12383 }),
12384 }
12385 }
12386
12387 /// Accept either a quoted identifier (`"foo"`) or a quoted string
12388 /// literal (`'foo'`) — same shape used by CREATE USER for the
12389 /// username slot.
12390 fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12391 match self.advance() {
12392 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12393 other => Err(ParseError {
12394 message: format!("expected identifier or string, got {other:?}"),
12395 token_pos: self.consumed_pos(),
12396 }),
12397 }
12398 }
12399
12400 fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12401 match self.advance() {
12402 Token::String(s) => Ok(s),
12403 other => Err(ParseError {
12404 message: format!("expected quoted string, got {other:?}"),
12405 token_pos: self.consumed_pos(),
12406 }),
12407 }
12408 }
12409
12410 fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12411 // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12412 // subqueries recurse through here without passing
12413 // parse_expr; share the same nesting budget.
12414 self.enter_nested()?;
12415 let r = self.parse_select_stmt_inner();
12416 self.nest_depth -= 1;
12417 r
12418 }
12419
12420 fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12421 // Caller dispatches on Token::Select; the inner helper handles
12422 // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12423 // get a fresh bare-select parse and may not have their own ORDER
12424 // BY / LIMIT.
12425 let mut head = self.parse_bare_select()?;
12426 let into = self.pending_select_into.take();
12427 self.parse_setop_chain_into(&mut head)?;
12428 self.parse_select_tail_into(&mut head)?;
12429 // v7.38.19 — `SELECT … INTO t` lowers to the SAME node as
12430 // `CREATE TABLE t AS SELECT …`, which is what a comment in
12431 // `ast.rs` has claimed since v7.38 and what only CTAS actually
12432 // did. The tail (ORDER BY / LIMIT) is parsed first so it belongs
12433 // to the body, as it does in PostgreSQL.
12434 if let Some((name, temporary)) = into {
12435 return Ok(Statement::CreateMaterializedView(
12436 crate::ast::CreateMaterializedViewStatement {
12437 temporary,
12438 name,
12439 if_not_exists: false,
12440 columns: Vec::new(),
12441 body: head,
12442 with_data: true,
12443 as_plain_table: true,
12444 },
12445 ));
12446 }
12447 Ok(Statement::Select(head))
12448 }
12449
12450 /// v7.37.17 (17.6 siblings) — the three SQL set operations
12451 /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12452 /// token), and INTERSECT [ALL] (a bare ident — it was never
12453 /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12454 /// tighter than UNION / EXCEPT — the executor folds the chain
12455 /// left-to-right, which is already correct for LEADING
12456 /// intersects; an INTERSECT pair that FOLLOWS a union/except
12457 /// pair nests into that previous peer, so A UNION B INTERSECT C
12458 /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12459 /// groups.
12460 fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12461 // A parenthesized group arrives with its own (already
12462 // regrouped) unions on `head`; only the pairs THIS chain
12463 // appends participate in the precedence regroup below —
12464 // nesting an outer INTERSECT into a group-internal peer
12465 // would dissolve the explicit grouping.
12466 let boundary = head.unions.len();
12467 loop {
12468 let base = match self.peek() {
12469 Token::Union => UnionKind::Distinct,
12470 Token::Except => UnionKind::Except,
12471 Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12472 _ => break,
12473 };
12474 self.advance();
12475 let kind = if matches!(self.peek(), Token::All) {
12476 self.advance();
12477 match base {
12478 UnionKind::Distinct => UnionKind::All,
12479 UnionKind::Except => UnionKind::ExceptAll,
12480 _ => UnionKind::IntersectAll,
12481 }
12482 } else {
12483 base
12484 };
12485 let peer = self.parse_bare_select()?;
12486 head.unions.push((kind, peer));
12487 }
12488 let mut pairs = core::mem::take(&mut head.unions);
12489 let tail = pairs.split_off(boundary);
12490 let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12491 for (kind, peer) in tail {
12492 let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12493 // An intersect nests into the previous element of THIS
12494 // chain only; with no new previous element it stays at
12495 // the outer level (the left fold applies it to the
12496 // whole head, group included).
12497 match (
12498 is_intersect,
12499 regrouped.len() > boundary,
12500 regrouped.last_mut(),
12501 ) {
12502 (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12503 _ => regrouped.push((kind, peer)),
12504 }
12505 }
12506 head.unions = regrouped;
12507 Ok(())
12508 }
12509
12510 /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12511 /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12512 /// the top-level bare VALUES statement reuses it verbatim.
12513 /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12514 /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12515 /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12516 /// where the grouping-set universe is still in scope.
12517 fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12518 if !matches!(self.peek(), Token::Order) {
12519 return Ok(Vec::new());
12520 }
12521 self.advance();
12522 if !self.peek_is_by() {
12523 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12524 }
12525 self.advance();
12526 let mut keys = Vec::new();
12527 loop {
12528 // v7.39 (round 691) — save/restore, the discipline this parser
12529 // already uses around `pending_sample_preds`, so a subquery inside
12530 // a key neither inherits nor leaks the channel.
12531 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12532 let saved_coll = self.order_key_collation.take();
12533 let parsed = self.parse_expr(0);
12534 self.in_order_by_key = saved_flag;
12535 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12536 let expr = parsed?;
12537 let desc = if matches!(self.peek(), Token::Desc) {
12538 self.advance();
12539 true
12540 } else if matches!(self.peek(), Token::Asc) {
12541 self.advance();
12542 false
12543 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12544 // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12545 // one ordering per type, so the btree comparison operators map
12546 // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12547 // would need a custom operator class — honest error.
12548 self.advance();
12549 match self.advance() {
12550 Token::Lt | Token::LtEq => false,
12551 Token::Gt | Token::GtEq => true,
12552 other => {
12553 return Err(self.err(alloc::format!(
12554 "ORDER BY USING supports the btree comparison \
12555 operators (< <= > >=); got {other:?}"
12556 )));
12557 }
12558 }
12559 } else {
12560 false
12561 };
12562 // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12563 let nulls_first = self.parse_optional_nulls_placement()?;
12564 keys.push(OrderBy {
12565 expr,
12566 desc,
12567 nulls_first,
12568 collation,
12569 });
12570 if matches!(self.peek(), Token::Comma) {
12571 self.advance();
12572 } else {
12573 break;
12574 }
12575 }
12576 Ok(keys)
12577 }
12578
12579 fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12580 // v7.39 (round 135) — a grouping-set query may have already parsed +
12581 // rewritten its ORDER BY (to reference synthetic grouping columns); if
12582 // no ORDER BY token is present, keep that pre-set order_by rather than
12583 // clobbering it with an empty list.
12584 let parsed_keys = self.parse_order_by_keys()?;
12585 head.order_by = if parsed_keys.is_empty() {
12586 core::mem::take(&mut head.order_by)
12587 } else {
12588 parsed_keys
12589 };
12590 // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12591 // order. PG's grammar takes a limit clause and an offset clause
12592 // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12593 // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12594 // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12595 // spelling died on `expected end of input, got Limit`.
12596 //
12597 // Each may appear at most once, and LIMIT and FETCH FIRST are
12598 // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12599 // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12600 // A second one is left unconsumed here, which the caller reports
12601 // as trailing input rather than silently taking the last.
12602 let mut saw_limit = false;
12603 let mut saw_offset = false;
12604 loop {
12605 if !saw_limit && matches!(self.peek(), Token::Limit) {
12606 self.advance();
12607 // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12608 // PG synonyms for "no limit". Treat both as None
12609 // (no head.limit set) so the engine's existing
12610 // unlimited-result path takes over. Reject was the
12611 // pre-5.1 behaviour and broke pg_dump-flavoured
12612 // tooling that occasionally emits LIMIT NULL.
12613 if self.consume_limit_unbounded_sentinel() {
12614 head.limit = None;
12615 } else {
12616 let first = self.parse_limit_expr("LIMIT")?;
12617 // MySQL `LIMIT offset, count` — the first number is
12618 // the offset when a comma follows.
12619 if matches!(self.peek(), Token::Comma) {
12620 self.advance();
12621 let count = self.parse_limit_expr("LIMIT")?;
12622 head.offset = Some(first);
12623 saw_offset = true;
12624 head.limit = Some(count);
12625 } else {
12626 head.limit = Some(first);
12627 }
12628 }
12629 saw_limit = true;
12630 continue;
12631 }
12632 if !saw_offset && matches!(self.peek(), Token::Offset) {
12633 self.advance();
12634 // PG also accepts an optional `ROW` / `ROWS` trailer
12635 // after the offset value (`OFFSET 10 ROWS`). The
12636 // FETCH-FIRST branch below relies on the same.
12637 let off = self.parse_limit_expr("OFFSET")?;
12638 self.consume_optional_rows_keyword();
12639 head.offset = Some(off);
12640 saw_offset = true;
12641 continue;
12642 }
12643 // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12644 // the SQL-standard alias for LIMIT. PG accepts both
12645 // spellings interchangeably; pg_dump emits FETCH FIRST in
12646 // newer versions. We map it onto `head.limit` so the
12647 // engine path is unified.
12648 if !saw_limit
12649 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12650 if s.eq_ignore_ascii_case("fetch"))
12651 {
12652 self.advance(); // FETCH
12653 // `FIRST` or `NEXT` (both legal per SQL standard).
12654 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12655 if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12656 {
12657 self.advance();
12658 }
12659 // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12660 // implicit 1 — but we always consume one if present).
12661 let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12662 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12663 {
12664 // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12665 crate::ast::LimitExpr::Literal(1)
12666 } else {
12667 self.parse_limit_expr("FETCH FIRST")?
12668 };
12669 // Eat `ROW` / `ROWS` if not already consumed above.
12670 self.consume_optional_rows_keyword();
12671 // Optional `ONLY` (the spec form) — or the SQL:2008
12672 // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12673 // now honours WITH TIES by extending past the LIMIT
12674 // truncation point through every row that shares the
12675 // last-kept row's ORDER BY key.
12676 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12677 if s.eq_ignore_ascii_case("only"))
12678 {
12679 self.advance();
12680 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12681 if s.eq_ignore_ascii_case("with"))
12682 {
12683 self.advance(); // WITH
12684 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12685 if s.eq_ignore_ascii_case("ties"))
12686 {
12687 self.advance();
12688 head.limit_with_ties = true;
12689 }
12690 }
12691 head.limit = Some(count);
12692 saw_limit = true;
12693 continue;
12694 }
12695 break;
12696 }
12697 // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12698 // FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12699 // [ OF table_name [, …] ]
12700 // [ NOWAIT | SKIP LOCKED ]
12701 // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12702 // FOR SHARE OF t2`). SPG is a single-writer engine — every
12703 // SELECT already returns a consistent snapshot — so these
12704 // are accept-and-discard: the parser absorbs them so
12705 // mailrs / Rails / Django code paths that emit `SELECT
12706 // … FOR UPDATE` for advisory pessimistic locking load
12707 // without a parser error. The on-disk locking model is
12708 // unchanged; callers that rely on FOR UPDATE for read-
12709 // through-write ordering still get the right answer
12710 // because SPG serialises writes anyway.
12711 head.locking = self
12712 .consume_optional_for_lock_clauses()
12713 .map(alloc::boxed::Box::new);
12714 Ok(())
12715 }
12716
12717 /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12718 /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12719 /// LOCKED ]` trailers. Each clause is fully accepted and
12720 /// discarded — SPG's single-writer model already satisfies the
12721 /// callers' implicit ordering requirement. Stops at the first
12722 /// token that isn't `FOR`.
12723 fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12724 // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12725 // not discarded. PG keeps the strongest of several clauses; the
12726 // policy of the last one wins, which is what this loop records.
12727 let mut seen: Option<crate::ast::LockingClause> = None;
12728 while matches!(self.peek(), Token::For) {
12729 // v7.37.14 (A2.5-stub) — record that this query asked
12730 // for a row lock the parser is about to silently
12731 // discard. Operators surface the count via
12732 // `spg_sql::silent_for_update_count()` so they can
12733 // gauge how much of the workload depends on advisory
12734 // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12735 // before v7.37.15's per-row tuple locking lands.
12736 crate::record_silent_for_update_clause();
12737 self.advance(); // FOR
12738 // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12739 // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12740 let mut no_key = false;
12741 let mut key = false;
12742 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12743 if s.eq_ignore_ascii_case("no"))
12744 {
12745 self.advance(); // NO
12746 no_key = true;
12747 // The next ident should be KEY but be generous;
12748 // anything followed by UPDATE/SHARE is accepted.
12749 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12750 if s.eq_ignore_ascii_case("key"))
12751 {
12752 self.advance(); // KEY
12753 }
12754 }
12755 // `KEY` prefix (PG `FOR KEY SHARE`).
12756 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12757 if s.eq_ignore_ascii_case("key"))
12758 {
12759 self.advance(); // KEY
12760 key = true;
12761 }
12762 // Lock-strength keyword: UPDATE / SHARE. Required, but
12763 // we're lenient — an unexpected token here just bails
12764 // (we already consumed FOR; caller's downstream
12765 // dispatch will error if anything actually depends on
12766 // the trailing tokens).
12767 let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12768 if s.eq_ignore_ascii_case("update"));
12769 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12770 if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12771 {
12772 self.advance();
12773 use crate::ast::LockStrength as LS;
12774 let strength = match (is_update, no_key, key) {
12775 (true, true, _) => LS::NoKeyUpdate,
12776 (true, _, _) => LS::Update,
12777 (false, _, true) => LS::KeyShare,
12778 (false, _, _) => LS::Share,
12779 };
12780 seen = Some(crate::ast::LockingClause {
12781 strength,
12782 of_tables: alloc::vec::Vec::new(),
12783 policy: crate::ast::LockWait::Wait,
12784 });
12785 } else {
12786 // FOR by itself (or `FOR KEY` with nothing after) —
12787 // give up on the lock-clause path. We've already
12788 // advanced past FOR; further attempts to parse
12789 // here would clobber state.
12790 return seen;
12791 }
12792 // Optional `OF tbl[, tbl …]`. mailrs emits this when
12793 // joining and locking only a subset of tables.
12794 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12795 if s.eq_ignore_ascii_case("of"))
12796 {
12797 self.advance(); // OF
12798 #[allow(clippy::while_let_loop)]
12799 loop {
12800 match self.peek() {
12801 Token::Ident(_) | Token::QuotedIdent(_) => {
12802 // v7.39 (round 294) — the name is CAPTURED now: PG
12803 // validates it against the FROM clause, and an
12804 // uncaptured list silently means "lock everything".
12805 let mut nm = match self.advance() {
12806 Token::Ident(n) | Token::QuotedIdent(n) => n,
12807 _ => alloc::string::String::new(),
12808 };
12809 // Optional schema-qualified `schema.table`.
12810 if matches!(self.peek(), Token::Dot) {
12811 self.advance();
12812 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12813 {
12814 self.advance();
12815 nm = n;
12816 }
12817 }
12818 if let Some(c) = seen.as_mut() {
12819 c.of_tables.push(nm);
12820 }
12821 }
12822 _ => break,
12823 }
12824 if matches!(self.peek(), Token::Comma) {
12825 self.advance();
12826 } else {
12827 break;
12828 }
12829 }
12830 }
12831 // Optional `NOWAIT` | `SKIP LOCKED`.
12832 match self.peek().clone() {
12833 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12834 self.advance();
12835 if let Some(c) = seen.as_mut() {
12836 c.policy = crate::ast::LockWait::NoWait;
12837 }
12838 }
12839 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12840 self.advance(); // SKIP
12841 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12842 if s.eq_ignore_ascii_case("locked"))
12843 {
12844 self.advance(); // LOCKED
12845 if let Some(c) = seen.as_mut() {
12846 c.policy = crate::ast::LockWait::SkipLocked;
12847 }
12848 }
12849 }
12850 _ => {}
12851 }
12852 // Loop: PG allows multiple FOR clauses chained.
12853 }
12854 seen
12855 }
12856
12857 /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12858 /// Bind value gets resolved during prepared-statement Execute;
12859 /// the Pratt expression parser would over-accept here (e.g.
12860 /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12861 /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12862 /// sentinel tokens (PG synonyms for "no limit"). Returns true
12863 /// when one was consumed; caller skips the regular
12864 /// limit-value parse and leaves `head.limit` at None.
12865 fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12866 if matches!(self.peek(), Token::Null) {
12867 self.advance();
12868 return true;
12869 }
12870 if matches!(self.peek(), Token::All) {
12871 self.advance();
12872 return true;
12873 }
12874 false
12875 }
12876
12877 /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12878 /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12879 /// SQL-standard shape. No-op when missing.
12880 fn consume_optional_rows_keyword(&mut self) {
12881 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12882 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12883 {
12884 self.advance();
12885 }
12886 }
12887
12888 /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12889 ///
12890 /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12891 /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12892 /// constant, which is why that spelling keeps the token path below.
12893 ///
12894 /// Constants are folded here rather than carried into the tree: the
12895 /// 15+ execution paths that read the row count go through
12896 /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12897 /// means "no limit". A clause the engine could not resolve would
12898 /// therefore return the WHOLE table instead of failing. Folding at
12899 /// parse time keeps that impossible; a non-constant clause is still
12900 /// a clean error (recorded residual — closing it wants a resolution
12901 /// pre-pass on the simple-query path, where `substitute_placeholders`
12902 /// does not run).
12903 fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12904 // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12905 // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12906 // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12907 // ONLY` both work (its grammar takes a c_expr). Both measured
12908 // against PG 18.4 in round 305.
12909 if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12910 return self.parse_limit_constant(label);
12911 }
12912 // One pass, no rewind: `advance()` takes each token by
12913 // `mem::replace`, so a consumed token reads back as Eof and this
12914 // parser cannot backtrack. Everything — bare literal included —
12915 // is therefore folded from the parsed expression rather than
12916 // re-read from the token stream.
12917 let start = self.pos;
12918 let e = self.parse_expr(0)?;
12919 if let crate::ast::Expr::Placeholder(n) = e {
12920 return Ok(crate::ast::LimitExpr::Placeholder(n));
12921 }
12922 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12923 match fold_limit_constant(&e) {
12924 Some(Ok(v)) if v < 0 => Err(ParseError {
12925 message: alloc::format!("{neg_label} must not be negative"),
12926 token_pos: start,
12927 }),
12928 Some(Ok(v)) => u32::try_from(v)
12929 .map(crate::ast::LimitExpr::Literal)
12930 .map_err(|_| ParseError {
12931 message: alloc::format!("{label} value too large: {v}"),
12932 token_pos: start,
12933 }),
12934 Some(Err(message)) => Err(ParseError {
12935 message: message.replace("{L}", neg_label),
12936 token_pos: start,
12937 }),
12938 // v7.39 (round 305, V23) — not foldable at parse time
12939 // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
12940 // expression; the engine evaluates it once before dispatch.
12941 None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
12942 }
12943 }
12944
12945 fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12946 // v7.39 (round 239) — PG's row-count clause takes a bigint with its
12947 // coercion rules, not just an integer token: a NUMERIC rounds half
12948 // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
12949 // refused with PG's wording ("LIMIT must not be negative", 2201W /
12950 // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
12951 // content, failing as an input-syntax error on the value. General
12952 // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
12953 // they need an Expr-carrying LimitExpr variant.
12954 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12955 let err_at = |message: alloc::string::String, pos: usize| ParseError {
12956 message,
12957 token_pos: pos,
12958 };
12959 match self.advance() {
12960 Token::Integer(n) if n >= 0 => u32::try_from(n)
12961 .map(crate::ast::LimitExpr::Literal)
12962 .map_err(|_| ParseError {
12963 message: alloc::format!("{label} value too large: {n}"),
12964 token_pos: self.consumed_pos(),
12965 }),
12966 Token::Integer(_) => Err(err_at(
12967 alloc::format!("{neg_label} must not be negative"),
12968 self.pos.saturating_sub(1),
12969 )),
12970 Token::Numeric(t) => {
12971 let pos = self.pos.saturating_sub(1);
12972 let v: f64 = t.parse().map_err(|_| {
12973 err_at(
12974 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12975 pos,
12976 )
12977 })?;
12978 if v < 0.0 {
12979 return Err(err_at(
12980 alloc::format!("{neg_label} must not be negative"),
12981 pos,
12982 ));
12983 }
12984 // Round half away from zero — PG's numeric→bigint cast.
12985 // (no_std: no f64::round; v is non-negative, so truncating
12986 // v + 0.5 is the same thing.)
12987 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
12988 let rounded = (v + 0.5) as u64;
12989 u32::try_from(rounded)
12990 .map(crate::ast::LimitExpr::Literal)
12991 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
12992 }
12993 Token::Minus => {
12994 let pos = self.pos.saturating_sub(1);
12995 match self.peek() {
12996 Token::Integer(_) | Token::Numeric(_) => {
12997 self.advance();
12998 Err(err_at(
12999 alloc::format!("{neg_label} must not be negative"),
13000 pos,
13001 ))
13002 }
13003 other => Err(err_at(
13004 alloc::format!(
13005 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13006 ),
13007 pos,
13008 )),
13009 }
13010 }
13011 Token::String(t) => {
13012 let pos = self.pos.saturating_sub(1);
13013 match t.trim().parse::<i64>() {
13014 Ok(n) if n < 0 => Err(err_at(
13015 alloc::format!("{neg_label} must not be negative"),
13016 pos,
13017 )),
13018 Ok(n) => u32::try_from(n)
13019 .map(crate::ast::LimitExpr::Literal)
13020 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
13021 Err(_) => Err(err_at(
13022 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13023 pos,
13024 )),
13025 }
13026 }
13027 Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
13028 other => Err(ParseError {
13029 message: alloc::format!(
13030 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13031 ),
13032 token_pos: self.consumed_pos(),
13033 }),
13034 }
13035 }
13036
13037 /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
13038 /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
13039 /// `unions` empty and `order_by` / `limit` `None`; the top-level
13040 /// `parse_select_stmt` is responsible for filling those in.
13041 /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
13042 /// call in the expression tree to the per-set integer bitmask
13043 /// (PG semantics: one bit per argument, MSB first; 1 = the key
13044 /// is dropped in this grouping set). Runs during the ROLLUP /
13045 /// CUBE / GROUPING SETS expansion, where the set is known.
13046 /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
13047 /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
13048 fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
13049 if let Expr::FunctionCall { name, .. } = expr
13050 && name.eq_ignore_ascii_case("grouping")
13051 {
13052 if !out.iter().any(|e| e == expr) {
13053 out.push(expr.clone());
13054 }
13055 return;
13056 }
13057 match expr {
13058 Expr::Binary { lhs, rhs, .. } => {
13059 Self::collect_grouping_calls(lhs, out);
13060 Self::collect_grouping_calls(rhs, out);
13061 }
13062 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13063 Self::collect_grouping_calls(expr, out)
13064 }
13065 Expr::FunctionCall { args, .. } => {
13066 for a in args {
13067 Self::collect_grouping_calls(a, out);
13068 }
13069 }
13070 Expr::Case {
13071 operand,
13072 branches,
13073 else_branch,
13074 } => {
13075 if let Some(o) = operand {
13076 Self::collect_grouping_calls(o, out);
13077 }
13078 for (c, v) in branches {
13079 Self::collect_grouping_calls(c, out);
13080 Self::collect_grouping_calls(v, out);
13081 }
13082 if let Some(x) = else_branch {
13083 Self::collect_grouping_calls(x, out);
13084 }
13085 }
13086 _ => {}
13087 }
13088 }
13089
13090 /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
13091 /// `grp_exprs[k]` with a reference to the synthetic ordering column
13092 /// `__grp_ord_k` (injected per grouping-set branch).
13093 fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
13094 if let Expr::FunctionCall { name, .. } = expr
13095 && name.eq_ignore_ascii_case("grouping")
13096 {
13097 if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
13098 *expr = Expr::Column(crate::ast::ColumnName {
13099 qualifier: None,
13100 name: alloc::format!("__grp_ord_{k}"),
13101 });
13102 }
13103 return;
13104 }
13105 match expr {
13106 Expr::Binary { lhs, rhs, .. } => {
13107 Self::rewrite_grouping_to_col(lhs, grp_exprs);
13108 Self::rewrite_grouping_to_col(rhs, grp_exprs);
13109 }
13110 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13111 Self::rewrite_grouping_to_col(expr, grp_exprs)
13112 }
13113 Expr::FunctionCall { args, .. } => {
13114 for a in args {
13115 Self::rewrite_grouping_to_col(a, grp_exprs);
13116 }
13117 }
13118 Expr::Case {
13119 operand,
13120 branches,
13121 else_branch,
13122 } => {
13123 if let Some(o) = operand {
13124 Self::rewrite_grouping_to_col(o, grp_exprs);
13125 }
13126 for (c, v) in branches {
13127 Self::rewrite_grouping_to_col(c, grp_exprs);
13128 Self::rewrite_grouping_to_col(v, grp_exprs);
13129 }
13130 if let Some(x) = else_branch {
13131 Self::rewrite_grouping_to_col(x, grp_exprs);
13132 }
13133 }
13134 _ => {}
13135 }
13136 }
13137
13138 /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
13139 /// as the list of key sets it contributes. A bare expression is one
13140 /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
13141 /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
13142 /// the concatenation of its items' sets, where an item is itself an
13143 /// element, a parenthesized key list, or the empty set `()`. A
13144 /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
13145 /// move together.
13146 fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
13147 let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
13148 // ROLLUP ( … ) / CUBE ( … )
13149 if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
13150 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
13151 {
13152 let is_cube = is_kw(self.peek(), "cube");
13153 self.advance(); // ROLLUP / CUBE
13154 self.advance(); // (
13155 let mut units: Vec<Vec<Expr>> = Vec::new();
13156 loop {
13157 if matches!(self.peek(), Token::LParen) {
13158 // Composite unit: (a, b) rolls up as one.
13159 self.advance();
13160 let mut unit = Vec::new();
13161 if !matches!(self.peek(), Token::RParen) {
13162 loop {
13163 unit.push(self.parse_expr(0)?);
13164 match self.peek() {
13165 Token::Comma => {
13166 self.advance();
13167 }
13168 Token::RParen => break,
13169 other => {
13170 return Err(self.err(format!(
13171 "expected ',' or ')' in grouping unit, got {other:?}"
13172 )));
13173 }
13174 }
13175 }
13176 }
13177 self.advance(); // )
13178 units.push(unit);
13179 } else {
13180 units.push(alloc::vec![self.parse_expr(0)?]);
13181 }
13182 match self.peek() {
13183 Token::Comma => {
13184 self.advance();
13185 }
13186 Token::RParen => break,
13187 other => {
13188 return Err(self.err(format!(
13189 "expected ',' or ')' in grouping list, got {other:?}"
13190 )));
13191 }
13192 }
13193 }
13194 self.advance(); // )
13195 let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
13196 units
13197 .iter()
13198 .zip(unit_sel.iter())
13199 .filter(|(_, keep)| **keep)
13200 .flat_map(|(u, _)| u.iter().cloned())
13201 .collect()
13202 };
13203 let n = units.len();
13204 if is_cube {
13205 let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
13206 .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
13207 .collect();
13208 subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
13209 return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
13210 }
13211 return Ok((0..=n)
13212 .rev()
13213 .map(|keep| {
13214 let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
13215 flatten(&sel)
13216 })
13217 .collect());
13218 }
13219 // GROUPING SETS ( item [, item]* )
13220 if is_kw(self.peek(), "grouping")
13221 && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
13222 {
13223 self.advance(); // GROUPING
13224 self.advance(); // SETS
13225 if !matches!(self.peek(), Token::LParen) {
13226 return Err(self.err(format!(
13227 "expected '(' after GROUPING SETS, got {:?}",
13228 self.peek()
13229 )));
13230 }
13231 self.advance(); // outer (
13232 let mut sets: Vec<Vec<Expr>> = Vec::new();
13233 loop {
13234 if matches!(self.peek(), Token::LParen) {
13235 // A parenthesized key list (or the empty set).
13236 self.advance();
13237 let mut set = Vec::new();
13238 if !matches!(self.peek(), Token::RParen) {
13239 loop {
13240 set.push(self.parse_expr(0)?);
13241 match self.peek() {
13242 Token::Comma => {
13243 self.advance();
13244 }
13245 Token::RParen => break,
13246 other => {
13247 return Err(self.err(format!(
13248 "expected ',' or ')' in grouping set, got {other:?}"
13249 )));
13250 }
13251 }
13252 }
13253 }
13254 self.advance(); // )
13255 sets.push(set);
13256 } else {
13257 // A nested element: ROLLUP/CUBE/GROUPING SETS or a
13258 // bare expression.
13259 sets.extend(self.parse_grouping_element()?);
13260 }
13261 match self.peek() {
13262 Token::Comma => {
13263 self.advance();
13264 }
13265 Token::RParen => break,
13266 other => {
13267 return Err(self.err(format!(
13268 "expected ',' or ')' after a grouping set, got {other:?}"
13269 )));
13270 }
13271 }
13272 }
13273 self.advance(); // outer )
13274 return Ok(sets);
13275 }
13276 Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
13277 }
13278
13279 fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
13280 // v7.38 (read01) — a reference to a key that is dropped in this grouping
13281 // set evaluates to NULL, at any depth. Previously only a *top-level*
13282 // select item equal to a dropped key was nullified, so a key nested in
13283 // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13284 // column and failed to resolve against the set's synthetic schema.
13285 if dropped.iter().any(|d| d == expr) {
13286 *expr = Expr::Literal(Literal::Null);
13287 return;
13288 }
13289 if let Expr::FunctionCall { name, args } = expr
13290 && name.eq_ignore_ascii_case("grouping")
13291 {
13292 let mut mask: i64 = 0;
13293 for a in args.iter() {
13294 mask <<= 1;
13295 if dropped.iter().any(|d| d == a) {
13296 mask |= 1;
13297 }
13298 }
13299 // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13300 // literal: a bare integer in a select item is indistinguishable
13301 // from a positional reference once `ORDER BY 1` substitutes the
13302 // item back in, and the round-232 position check then read the
13303 // mask value as an out-of-range position. The cast changes
13304 // nothing semantically (grouping() is integer).
13305 *expr = Expr::Cast {
13306 expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13307 target: crate::ast::CastTarget::Int,
13308 };
13309 return;
13310 }
13311 // Generic recursion over the common expression shapes the
13312 // SELECT list uses; anything without child expressions is
13313 // left alone.
13314 match expr {
13315 Expr::FunctionCall { args, .. } => {
13316 for a in args {
13317 Self::substitute_grouping_calls(a, dropped);
13318 }
13319 }
13320 Expr::Binary { lhs, rhs, .. } => {
13321 Self::substitute_grouping_calls(lhs, dropped);
13322 Self::substitute_grouping_calls(rhs, dropped);
13323 }
13324 Expr::Unary { expr: inner, .. } => {
13325 Self::substitute_grouping_calls(inner, dropped);
13326 }
13327 Expr::Cast { expr: inner, .. } => {
13328 Self::substitute_grouping_calls(inner, dropped);
13329 }
13330 Expr::Case {
13331 operand,
13332 branches,
13333 else_branch,
13334 } => {
13335 if let Some(op) = operand {
13336 Self::substitute_grouping_calls(op, dropped);
13337 }
13338 for (w, t) in branches {
13339 Self::substitute_grouping_calls(w, dropped);
13340 Self::substitute_grouping_calls(t, dropped);
13341 }
13342 if let Some(e) = else_branch {
13343 Self::substitute_grouping_calls(e, dropped);
13344 }
13345 }
13346 // v7.38 (read01) — recurse into the remaining child-bearing shapes
13347 // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13348 // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13349 // …` is the canonical rollup-total label idiom).
13350 Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13351 Expr::Like { expr, pattern, .. } => {
13352 Self::substitute_grouping_calls(expr, dropped);
13353 Self::substitute_grouping_calls(pattern, dropped);
13354 }
13355 Expr::InList { expr, list, .. } => {
13356 Self::substitute_grouping_calls(expr, dropped);
13357 for item in list {
13358 Self::substitute_grouping_calls(item, dropped);
13359 }
13360 }
13361 Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13362 Expr::Array(items) => {
13363 for item in items {
13364 Self::substitute_grouping_calls(item, dropped);
13365 }
13366 }
13367 Expr::ArraySubscript { target, index } => {
13368 Self::substitute_grouping_calls(target, dropped);
13369 Self::substitute_grouping_calls(index, dropped);
13370 }
13371 Expr::ArraySlice { target, lo, hi } => {
13372 Self::substitute_grouping_calls(target, dropped);
13373 if let Some(lo) = lo {
13374 Self::substitute_grouping_calls(lo, dropped);
13375 }
13376 if let Some(hi) = hi {
13377 Self::substitute_grouping_calls(hi, dropped);
13378 }
13379 }
13380 Expr::AnyAll { expr, array, .. } => {
13381 Self::substitute_grouping_calls(expr, dropped);
13382 Self::substitute_grouping_calls(array, dropped);
13383 }
13384 _ => {}
13385 }
13386 }
13387
13388 fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13389 // v7.37.17 (17.6 siblings) — parenthesized set-operation
13390 // group: `( <select chain> )` usable anywhere a query block
13391 // is (head or peer of an outer chain). The group's own
13392 // unions ride the returned SelectStatement; the executor's
13393 // nested-peer recursion runs them.
13394 if matches!(self.peek(), Token::LParen)
13395 && matches!(
13396 self.tokens.get(self.pos + 1),
13397 Some(Token::Select | Token::LParen | Token::Values)
13398 )
13399 {
13400 self.advance(); // (
13401 self.enter_nested()?;
13402 // v7.37 D.20 — a group whose head is a VALUES list:
13403 // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13404 // otherwise recurse into a nested SELECT/group head.
13405 let mut head = (if matches!(self.peek(), Token::Values) {
13406 self.advance(); // VALUES
13407 self.parse_values_rows_body()
13408 } else {
13409 self.parse_bare_select()
13410 })
13411 .and_then(|mut h| {
13412 self.parse_setop_chain_into(&mut h)?;
13413 Ok(h)
13414 });
13415 self.nest_depth -= 1;
13416 let mut head = match &mut head {
13417 Ok(h) => core::mem::take(h),
13418 Err(_) => return head,
13419 };
13420 // v7.37.17 (17.6 siblings) — group-internal tail:
13421 // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13422 // group head, then wrap the group as a derived table
13423 // (SELECT * FROM (group)) so the outer chain / outer
13424 // tail can't clobber the group's own ordering or limit.
13425 let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13426 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13427 if s.eq_ignore_ascii_case("fetch"));
13428 if has_tail {
13429 self.parse_select_tail_into(&mut head)?;
13430 head = SelectStatement {
13431 locking: None,
13432 ctes: Vec::new(),
13433 distinct: false,
13434 distinct_on: Vec::new(),
13435 items: alloc::vec![SelectItem::Wildcard],
13436 from: Some(FromClause {
13437 primary: TableRef {
13438 name: "subquery".to_string(),
13439 alias: None,
13440 only: false,
13441 as_of_segment: None,
13442 unnest_expr: None,
13443 unnest_column_aliases: Vec::new(),
13444 with_ordinality: false,
13445 generate_series_args: None,
13446 lateral_subquery: Some(Box::new(head)),
13447 jsonb_each_text_arg: None,
13448 table_fn_call: None,
13449 rows_from: None,
13450 json_table: None,
13451 scalar_fn_item: false,
13452 },
13453 joins: Vec::new(),
13454 }),
13455 where_: None,
13456 group_by: None,
13457 group_by_all: false,
13458 having: None,
13459 unions: Vec::new(),
13460 order_by: Vec::new(),
13461 limit: None,
13462 offset: None,
13463 limit_with_ties: false,
13464 window_check_exprs: Vec::new(),
13465 };
13466 }
13467 if !matches!(self.peek(), Token::RParen) {
13468 return Err(self.err(format!(
13469 "expected ')' after parenthesized query group, got {:?}",
13470 self.peek()
13471 )));
13472 }
13473 self.advance();
13474 return Ok(head);
13475 }
13476 // `TABLE name` shorthand as a query block — valid anywhere
13477 // a SELECT head is (set-op peers included).
13478 if matches!(self.peek(), Token::Table)
13479 && matches!(
13480 self.tokens.get(self.pos + 1),
13481 Some(Token::Ident(_) | Token::QuotedIdent(_))
13482 )
13483 {
13484 return self.parse_table_shorthand();
13485 }
13486 if !matches!(self.peek(), Token::Select) {
13487 return Err(self.err(format!(
13488 "expected SELECT to start a query block, got {:?}",
13489 self.peek()
13490 )));
13491 }
13492 self.advance();
13493 let distinct = if matches!(self.peek(), Token::Distinct) {
13494 self.advance();
13495 true
13496 } else {
13497 false
13498 };
13499 // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13500 // keep the first row (per ORDER BY) of each group the
13501 // expressions define. Django's .distinct('field') shape.
13502 let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13503 self.advance(); // ON
13504 if !matches!(self.peek(), Token::LParen) {
13505 return Err(self.err(format!(
13506 "expected '(' after DISTINCT ON, got {:?}",
13507 self.peek()
13508 )));
13509 }
13510 self.advance();
13511 let mut exprs = Vec::new();
13512 loop {
13513 exprs.push(self.parse_expr(0)?);
13514 match self.peek() {
13515 Token::Comma => {
13516 self.advance();
13517 }
13518 Token::RParen => break,
13519 other => {
13520 return Err(self.err(format!(
13521 "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13522 )));
13523 }
13524 }
13525 }
13526 self.advance(); // )
13527 exprs
13528 } else {
13529 Vec::new()
13530 };
13531 let mut items = self.parse_select_list()?;
13532 // v7.38.19 — `SELECT … INTO <table>`, PostgreSQL's other spelling
13533 // of CTAS. It sits exactly here in PG's grammar, right after the
13534 // target list.
13535 //
13536 // A comment in `ast.rs` has said since v7.38 that CTAS and
13537 // `SELECT INTO` lower to the same node. Only CTAS ever did:
13538 // `SELECT i INTO t FROM src` answered `syntax error at or near
13539 // "INTO"`, which the differential found while measuring what
13540 // PostgreSQL tags each of the five materialising forms with. A
13541 // comment describing a capability the code does not have is the
13542 // defect this version has been finding all day, and this is the
13543 // one it found in the parser.
13544 //
13545 // `INTO` is captured rather than consumed here: the name has to
13546 // travel out of a function that returns a `SelectStatement`, and
13547 // the caller lowers the whole thing to the CTAS node.
13548 if matches!(self.peek(), Token::Into) {
13549 self.advance();
13550 // `TEMP` / `TEMPORARY` / `UNLOGGED` / `TABLE` are modifiers on
13551 // the target, not part of its name. SPG has one storage
13552 // class, so `UNLOGGED` is accepted and means nothing, which
13553 // is what it already means on `CREATE TABLE`.
13554 let mut temporary = false;
13555 loop {
13556 match self.peek().clone() {
13557 Token::Ident(w) | Token::QuotedIdent(w)
13558 if w.eq_ignore_ascii_case("temp")
13559 || w.eq_ignore_ascii_case("temporary") =>
13560 {
13561 temporary = true;
13562 self.advance();
13563 }
13564 Token::Ident(w) | Token::QuotedIdent(w)
13565 if w.eq_ignore_ascii_case("unlogged") =>
13566 {
13567 self.advance();
13568 }
13569 Token::Table => {
13570 self.advance();
13571 }
13572 _ => break,
13573 }
13574 }
13575 let name = match self.peek().clone() {
13576 Token::Ident(w) | Token::QuotedIdent(w) => {
13577 self.advance();
13578 w
13579 }
13580 other => {
13581 return Err(self.err(alloc::format!(
13582 "expected a table name after SELECT … INTO, got {other:?}"
13583 )));
13584 }
13585 };
13586 self.pending_select_into = Some((name, temporary));
13587 }
13588 // Scope the TABLESAMPLE lowering channel to this SELECT:
13589 // stash whatever an enclosing select accumulated, collect
13590 // our own FROM's predicates, restore after the combine.
13591 let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13592 let mut from = if matches!(self.peek(), Token::From) {
13593 self.advance();
13594 Some(self.parse_from_clause()?)
13595 } else {
13596 None
13597 };
13598 // v7.37 D.22 — a set-returning function in the projection with no FROM
13599 // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13600 // rows. Move the first SRF projection item to a FROM-position derived
13601 // table and replace it in the projection with a reference to its output
13602 // column; sibling scalar columns repeat per SRF row. PG names the output
13603 // column after the function (or its AS alias). Reuses the FROM-SRF
13604 // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13605 // works via the targetlist-SRF path.
13606 // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13607 // `SELECT * FROM f(args)` — the record's fields become the columns, which
13608 // is exactly what the function's own row shape already is. Anywhere else
13609 // (per outer row, or beside other items) it would need a real record-typed
13610 // projection, so it says so rather than answering something else.
13611 if let [
13612 SelectItem::Expr {
13613 expr: Expr::FunctionCall { name, args },
13614 ..
13615 },
13616 ] = items.as_slice()
13617 && name == "__record_expand"
13618 {
13619 let Some(Expr::FunctionCall {
13620 name: inner_name,
13621 args: inner_args,
13622 }) = args.first()
13623 else {
13624 return Err(self.err(
13625 "(<expr>).* expands a function's record — it needs a function call".into(),
13626 ));
13627 };
13628 if from.is_some() {
13629 return Err(self.err(
13630 "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13631 .into(),
13632 ));
13633 }
13634 let fn_ref = TableRef {
13635 name: inner_name.clone(),
13636 alias: None,
13637 only: false,
13638 as_of_segment: None,
13639 unnest_expr: None,
13640 unnest_column_aliases: Vec::new(),
13641 with_ordinality: false,
13642 generate_series_args: None,
13643 lateral_subquery: None,
13644 jsonb_each_text_arg: None,
13645 table_fn_call: Some(Box::new((
13646 inner_name.to_ascii_lowercase(),
13647 inner_args.clone(),
13648 ))),
13649 rows_from: None,
13650 json_table: None,
13651 scalar_fn_item: false,
13652 };
13653 items = alloc::vec![SelectItem::Wildcard];
13654 from = Some(FromClause {
13655 primary: fn_ref,
13656 joins: Vec::new(),
13657 });
13658 }
13659 // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13660 // FROM, keeps its marker: the ENGINE lowers it, because naming the
13661 // record's fields takes the catalog. It becomes a LATERAL of the same
13662 // function plus one item per declared column — the machinery rounds 65
13663 // and 69 already built.
13664 // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13665 // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13666 // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13667 // express, since the lifted one becomes a scan and the other would
13668 // expand per its rows (a cross product, not a zip). So when the
13669 // projection holds more than one top-level function call, the lift steps
13670 // aside and the engine's target-list expansion takes the whole list.
13671 let fn_call_items = items
13672 .iter()
13673 .filter(|it| {
13674 matches!(
13675 it,
13676 SelectItem::Expr {
13677 expr: Expr::FunctionCall { .. },
13678 ..
13679 }
13680 )
13681 })
13682 .count();
13683 if from.is_none() && fn_call_items <= 1 {
13684 let mut found: Option<(usize, TableRef, String)> = None;
13685 for (i, item) in items.iter().enumerate() {
13686 if let SelectItem::Expr {
13687 expr: Expr::FunctionCall { name, args },
13688 alias,
13689 } = item
13690 {
13691 let lname = name.to_ascii_lowercase();
13692 let colname = alias.clone().unwrap_or_else(|| lname.clone());
13693 let (unnest, gs) = match lname.as_str() {
13694 "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13695 "generate_series" if (2..=3).contains(&args.len()) => {
13696 (None, Some(args.clone()))
13697 }
13698 // v7.38 (read01) — generate_subscripts(arr, dim) in a
13699 // no-FROM projection yields the 1-based subscripts, i.e.
13700 // generate_series(1, array_length(arr, dim)); an invalid
13701 // dimension makes array_length NULL → 0 rows, as in PG.
13702 "generate_subscripts" if args.len() == 2 => (
13703 None,
13704 Some(alloc::vec![
13705 Expr::Literal(Literal::Integer(1)),
13706 Expr::FunctionCall {
13707 name: "array_length".to_string(),
13708 args: args.clone(),
13709 },
13710 ]),
13711 ),
13712 // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13713 // in a no-FROM projection unnest their *_to_array form.
13714 "string_to_table" | "regexp_split_to_table" => {
13715 let array_fn = if lname == "string_to_table" {
13716 "string_to_array"
13717 } else {
13718 "regexp_split_to_array"
13719 };
13720 (
13721 Some(Box::new(Expr::FunctionCall {
13722 name: array_fn.to_string(),
13723 args: args.clone(),
13724 })),
13725 None,
13726 )
13727 }
13728 // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13729 // a no-FROM projection expand per element. The scalar form
13730 // returns the elements as a TEXT array, so unnest over the
13731 // same call materialises one row each (same rewrite the
13732 // FROM-clause form uses).
13733 "jsonb_array_elements"
13734 | "json_array_elements"
13735 | "jsonb_array_elements_text"
13736 | "json_array_elements_text"
13737 if args.len() == 1 =>
13738 {
13739 (
13740 Some(Box::new(Expr::FunctionCall {
13741 name: lname.clone(),
13742 args: args.clone(),
13743 })),
13744 None,
13745 )
13746 }
13747 // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13748 // in a no-FROM projection expands per match (scalar form
13749 // returns the matches as a TEXT array → unnest).
13750 "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13751 Some(Box::new(Expr::FunctionCall {
13752 name: lname.clone(),
13753 args: args.clone(),
13754 })),
13755 None,
13756 ),
13757 _ => continue,
13758 };
13759 found = Some((
13760 i,
13761 TableRef {
13762 name: colname.clone(),
13763 alias: Some(colname.clone()),
13764 only: false,
13765 as_of_segment: None,
13766 unnest_expr: unnest,
13767 unnest_column_aliases: alloc::vec![colname.clone()],
13768 with_ordinality: false,
13769 generate_series_args: gs,
13770 lateral_subquery: None,
13771 jsonb_each_text_arg: None,
13772 table_fn_call: None,
13773 rows_from: None,
13774 json_table: None,
13775 scalar_fn_item: false,
13776 },
13777 colname,
13778 ));
13779 break;
13780 }
13781 }
13782 if let Some((idx, tref, colname)) = found {
13783 from = Some(FromClause {
13784 primary: tref,
13785 joins: Vec::new(),
13786 });
13787 items[idx] = SelectItem::Expr {
13788 expr: Expr::Column(ColumnName {
13789 qualifier: None,
13790 name: colname.clone(),
13791 }),
13792 alias: Some(colname),
13793 };
13794 }
13795 }
13796 let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13797 let where_ = if matches!(self.peek(), Token::Where) {
13798 self.advance();
13799 Some(self.parse_expr(0)?)
13800 } else {
13801 None
13802 };
13803 let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13804 Some(match acc {
13805 Some(w) => Expr::Binary {
13806 lhs: Box::new(pred),
13807 op: crate::ast::BinOp::And,
13808 rhs: Box::new(w),
13809 },
13810 None => pred,
13811 })
13812 });
13813 self.pending_sample_preds = enclosing_sample_preds;
13814 let mut group_by_all = false;
13815 // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13816 // share one expansion: `grouping_sets` lists the key subsets
13817 // (first = primary, assigned to stmt.group_by; the rest
13818 // become UNION ALL peers), `grouping_universe` is the full
13819 // key list used to compute each peer's dropped keys.
13820 let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13821 let mut grouping_universe: Vec<Expr> = Vec::new();
13822 // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13823 // A BOOL, not the key list: this frame is the statement parser's, and
13824 // round 430 measured that a `Vec` local here is enough on its own to
13825 // tip the 512 KiB nesting guard. The keys are recoverable from
13826 // `grouping_universe`, which a rollup fills with exactly them.
13827 let mut mysql_rollup = false;
13828 let group_by = if matches!(self.peek(), Token::Group) {
13829 self.advance();
13830 if !self.peek_is_by() {
13831 return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13832 }
13833 self.advance();
13834 // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13835 // every non-aggregate SELECT-list item later.
13836 if matches!(self.peek(), Token::All) {
13837 self.advance();
13838 group_by_all = true;
13839 None
13840 } else {
13841 // v7.39 (round 242) — PG's general grouping-element grammar:
13842 // GROUP BY [DISTINCT] element [, element]*, where an element
13843 // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13844 // SETS (…) — mixed freely. Each element yields a list of
13845 // key sets; the query's grouping sets are the CARTESIAN
13846 // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13847 // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13848 // content. ROLLUP/CUBE members may be composite
13849 // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13850 // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13851 // parser handled only a lone ROLLUP/CUBE/GS as the whole
13852 // clause.
13853 let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13854 self.advance();
13855 true
13856 } else {
13857 false
13858 };
13859 let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13860 loop {
13861 element_sets.push(self.parse_grouping_element()?);
13862 if matches!(self.peek(), Token::Comma) {
13863 self.advance();
13864 } else {
13865 break;
13866 }
13867 }
13868 let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13869 for el in &element_sets {
13870 let mut next: Vec<Vec<Expr>> = Vec::new();
13871 for base in &total {
13872 for set in el {
13873 let mut merged = base.clone();
13874 for k in set {
13875 if !merged.iter().any(|m| m == k) {
13876 merged.push(k.clone());
13877 }
13878 }
13879 next.push(merged);
13880 }
13881 }
13882 total = next;
13883 }
13884 // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13885 // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13886 // The keys and the aggregates come out identical; the ROW
13887 // ORDER does not, and that is the part a report depends on.
13888 // MySQL interleaves each group's subtotal right after its
13889 // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13890 // where the union-of-grouping-sets expansion emits every
13891 // leaf first and then every subtotal. MariaDB REFUSES an
13892 // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13893 // order itself — measured on MariaDB 11 and MySQL 9.7, which
13894 // agree on the order and disagree only on whether ORDER BY
13895 // is allowed (MySQL allows it; SPG allows it too, since
13896 // refusing would break the clients that can write it).
13897 if self.mysql_dialect
13898 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13899 && matches!(
13900 self.tokens.get(self.pos + 1),
13901 Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13902 )
13903 {
13904 self.advance(); // WITH
13905 self.advance(); // ROLLUP
13906 let keys = total.into_iter().next().unwrap_or_default();
13907 mysql_rollup = true;
13908 // n+1 prefixes, largest first — the same expansion
13909 // `ROLLUP (…)` produces.
13910 total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13911 }
13912 if distinct_sets {
13913 let mut seen: Vec<Vec<String>> = Vec::new();
13914 total.retain(|set| {
13915 let mut key: Vec<String> =
13916 set.iter().map(|e| alloc::format!("{e}")).collect();
13917 key.sort();
13918 if seen.contains(&key) {
13919 false
13920 } else {
13921 seen.push(key);
13922 true
13923 }
13924 });
13925 }
13926 if total.len() > 1 {
13927 let mut universe: Vec<Expr> = Vec::new();
13928 for set in &total {
13929 for k in set {
13930 if !universe.iter().any(|u| u == k) {
13931 universe.push(k.clone());
13932 }
13933 }
13934 }
13935 grouping_universe = universe;
13936 let primary = total[0].clone();
13937 grouping_sets = total;
13938 Some(primary)
13939 } else {
13940 // One set (a plain GROUP BY list, or a single-set
13941 // spelling like GROUPING SETS ((a, b))). An EMPTY
13942 // single set — GROUPING SETS (()) — stays
13943 // `Some(vec![])`: the grand-total group, which must
13944 // run the aggregate path.
13945 Some(total.into_iter().next().unwrap_or_default())
13946 }
13947 }
13948 } else {
13949 None
13950 };
13951 let having = if matches!(self.peek(), Token::Having) {
13952 self.advance();
13953 Some(self.parse_expr(0)?)
13954 } else {
13955 None
13956 };
13957 // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
13958 // OVER w parsed to a marker above; inline each definition
13959 // into the referencing WindowFunction nodes.
13960 let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
13961 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
13962 self.advance();
13963 loop {
13964 let wname = self.expect_ident_like()?;
13965 if !matches!(self.peek(), Token::As) {
13966 return Err(self.err(format!(
13967 "expected AS after WINDOW {wname}, got {:?}",
13968 self.peek()
13969 )));
13970 }
13971 self.advance();
13972 // v7.39 (round 229) — PG rejects a redefinition outright.
13973 if window_defs
13974 .iter()
13975 .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
13976 {
13977 return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
13978 }
13979 let def = self.parse_over_clause()?;
13980 // A definition may itself copy an earlier one
13981 // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
13982 // so resolve it against the defs already in scope. Same
13983 // copy rules as an `OVER (w1 …)` in the select list.
13984 let mut probe = Expr::WindowFunction {
13985 name: String::new(),
13986 args: Vec::new(),
13987 partition_by: def.0,
13988 order_by: def.1,
13989 frame: def.2,
13990 null_treatment: crate::ast::NullTreatment::Respect,
13991 filter: None,
13992 };
13993 Self::substitute_named_windows(&mut probe, &window_defs)
13994 .map_err(|m| self.err(m))?;
13995 let Expr::WindowFunction {
13996 partition_by,
13997 order_by,
13998 frame,
13999 ..
14000 } = probe
14001 else {
14002 unreachable!("probe is a WindowFunction")
14003 };
14004 window_defs.push((wname, (partition_by, order_by, frame)));
14005 if matches!(self.peek(), Token::Comma) {
14006 self.advance();
14007 continue;
14008 }
14009 break;
14010 }
14011 }
14012 // v7.39 (round 705) — which definitions did anything reference?
14013 // The ones nothing did used to be dropped here, unexamined, so
14014 // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
14015 // definition whether referenced or not. Their key expressions ride
14016 // out on the statement for the engine to resolve.
14017 let mut window_refs: Vec<String> = Vec::new();
14018 if !window_defs.is_empty() {
14019 for it in &items {
14020 if let SelectItem::Expr { expr, .. } = it {
14021 Self::collect_named_window_refs(expr, &mut window_refs);
14022 }
14023 }
14024 }
14025 let window_check_exprs: Vec<Expr> = window_defs
14026 .iter()
14027 .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
14028 .flat_map(|(_, (partition, order, _))| {
14029 partition
14030 .iter()
14031 .cloned()
14032 .chain(order.iter().map(|(e, _, _)| e.clone()))
14033 })
14034 .collect();
14035 if !window_defs.is_empty()
14036 || items
14037 .iter()
14038 .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
14039 {
14040 for it in &mut items {
14041 if let SelectItem::Expr { expr, .. } = it {
14042 Self::substitute_named_windows(expr, &window_defs)
14043 .map_err(|m| self.err(m))?;
14044 }
14045 }
14046 }
14047 // `GROUP BY 1` — positional keys substitute with the Nth
14048 // select item's expression (same contract ORDER BY has had
14049 // since v6.x). Out-of-range positions error.
14050 let group_by = match group_by {
14051 Some(mut keys) => {
14052 for k in &mut keys {
14053 if let Expr::Literal(Literal::Integer(n)) = k {
14054 let idx = *n;
14055 if idx < 1 || idx as usize > items.len() {
14056 return Err(self.err(alloc::format!(
14057 "GROUP BY position {idx} is not in select list"
14058 )));
14059 }
14060 match &items[(idx - 1) as usize] {
14061 SelectItem::Expr { expr, .. } => *k = expr.clone(),
14062 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
14063 return Err(self.err(alloc::format!(
14064 "GROUP BY position {idx} references a wildcard item"
14065 )));
14066 }
14067 }
14068 }
14069 }
14070 Some(keys)
14071 }
14072 None => None,
14073 };
14074 let mut stmt = SelectStatement {
14075 locking: None,
14076 ctes: Vec::new(),
14077 distinct,
14078 distinct_on,
14079 items,
14080 from,
14081 where_,
14082 group_by,
14083 group_by_all,
14084 having,
14085 unions: Vec::new(),
14086 order_by: Vec::new(),
14087 limit: None,
14088 offset: None,
14089 limit_with_ties: false,
14090 window_check_exprs,
14091 };
14092 // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
14093 // first set is the primary (already on stmt.group_by); each
14094 // further set becomes a UNION ALL peer with its dropped
14095 // keys (universe minus the set) replaced by NULL literals
14096 // in the peer's items and group_by. PG-legal: non-grouped
14097 // select items must be group keys or aggregates, so a
14098 // dropped key's occurrences in the projection are exactly
14099 // the ones to nullify.
14100 // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
14101 // over a plain GROUP BY (every argument must be a group key; the
14102 // mask is then 0) and rejects anything else with 42803. SPG's
14103 // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
14104 // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
14105 // function `grouping`".
14106 if grouping_sets.len() <= 1 {
14107 let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
14108 let mut calls: Vec<Expr> = Vec::new();
14109 for item in &stmt.items {
14110 if let SelectItem::Expr { expr, .. } = item {
14111 Self::collect_grouping_calls(expr, &mut calls);
14112 }
14113 }
14114 if let Some(h) = &stmt.having {
14115 Self::collect_grouping_calls(h, &mut calls);
14116 }
14117 for call in &calls {
14118 let Expr::FunctionCall { args, .. } = call else {
14119 continue;
14120 };
14121 for a in args {
14122 if !keys.iter().any(|k| k == a) {
14123 return Err(self.err(
14124 "arguments to GROUPING must be grouping expressions of the associated query level"
14125 .to_string(),
14126 ));
14127 }
14128 }
14129 }
14130 if !calls.is_empty() {
14131 for item in &mut stmt.items {
14132 if let SelectItem::Expr { expr, .. } = item {
14133 Self::substitute_grouping_calls(expr, &[]);
14134 }
14135 }
14136 if let Some(h) = &mut stmt.having {
14137 Self::substitute_grouping_calls(h, &[]);
14138 }
14139 }
14140 }
14141 if grouping_sets.len() > 1 {
14142 // The primary set's own dropped keys nullify in the
14143 // HEAD's projection too (GROUPING SETS's first set may
14144 // omit keys other sets use).
14145 let primary = grouping_sets[0].clone();
14146 let head_dropped: Vec<Expr> = grouping_universe
14147 .iter()
14148 .filter(|u| !primary.iter().any(|k| k == *u))
14149 .cloned()
14150 .collect();
14151 for set in grouping_sets.iter().skip(1) {
14152 let mut peer = stmt.clone();
14153 peer.unions = Vec::new();
14154 let dropped: Vec<&Expr> = grouping_universe
14155 .iter()
14156 .filter(|u| !set.iter().any(|k| k == *u))
14157 .collect();
14158 // Empty set = grand-total group: `Some(vec![])` forces
14159 // the aggregate path (one collapsed row) instead of a
14160 // per-row passthrough. See the primary-set note above.
14161 peer.group_by = Some(set.clone());
14162 let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
14163 for item in &mut peer.items {
14164 if let SelectItem::Expr { expr, alias } = item {
14165 if dropped.iter().any(|d| *d == expr) {
14166 // v7.39 — keep the dropped key's name on the
14167 // NULL literal so the UNION output column
14168 // (and any top-level ORDER BY on it) still
14169 // resolves.
14170 if alias.is_none()
14171 && let Expr::Column(c) = &expr
14172 {
14173 *alias = Some(c.name.clone());
14174 }
14175 *expr = Expr::Literal(Literal::Null);
14176 } else {
14177 Self::substitute_grouping_calls(expr, &dropped_owned);
14178 }
14179 }
14180 }
14181 if let Some(h) = &mut peer.having {
14182 Self::substitute_grouping_calls(h, &dropped_owned);
14183 }
14184 stmt.unions.push((UnionKind::All, peer));
14185 }
14186 for item in &mut stmt.items {
14187 if let SelectItem::Expr { expr, alias } = item {
14188 if head_dropped.iter().any(|d| d == expr) {
14189 if alias.is_none()
14190 && let Expr::Column(c) = &expr
14191 {
14192 *alias = Some(c.name.clone());
14193 }
14194 *expr = Expr::Literal(Literal::Null);
14195 } else {
14196 Self::substitute_grouping_calls(expr, &head_dropped);
14197 }
14198 }
14199 }
14200 if let Some(h) = &mut stmt.having {
14201 Self::substitute_grouping_calls(h, &head_dropped);
14202 }
14203 // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
14204 // (while `grouping_universe` / the per-branch sets are in scope). For
14205 // each grouping() call in it, inject a per-branch hidden column
14206 // `__grp_ord_K` carrying that branch's mask into the head + every
14207 // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
14208 // preserves this pre-set order_by; the engine strips `__grp_ord_*`
14209 // from the final output. A standalone grouping-set query has ORDER BY
14210 // (not an explicit set-op) next, so consuming it here is safe.
14211 // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
14212 // rollup carries the hierarchical order: sort by the grouping
14213 // keys with the rolled-up NULLs last, which is exactly the
14214 // interleaving both oracles emit. A client's own ORDER BY wins,
14215 // which is what MySQL does (MariaDB refuses to let one be
14216 // written at all).
14217 // The synthesised keys have to travel the SAME path a written
14218 // ORDER BY does: the block below is what turns a `grouping()`
14219 // call into the per-branch `__grp_ord_K` column the engine can
14220 // actually sort on. Bypassing it left a bare `grouping(text)`
14221 // for the evaluator to reject.
14222 let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
14223 self.parse_order_by_keys()?
14224 } else if mysql_rollup {
14225 Self::mysql_rollup_order(&grouping_universe)
14226 } else {
14227 Vec::new()
14228 };
14229 if !synthesised_or_parsed.is_empty() {
14230 let mut order_keys = synthesised_or_parsed;
14231 let mut grp_exprs: Vec<Expr> = Vec::new();
14232 for ob in &order_keys {
14233 Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
14234 }
14235 for (k, gexpr) in grp_exprs.iter().enumerate() {
14236 let colname = alloc::format!("__grp_ord_{k}");
14237 // Head branch (primary set) uses `head_dropped`.
14238 let mut he = gexpr.clone();
14239 Self::substitute_grouping_calls(&mut he, &head_dropped);
14240 stmt.items.push(SelectItem::Expr {
14241 expr: he,
14242 alias: Some(colname.clone()),
14243 });
14244 // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
14245 for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14246 let set = &grouping_sets[i + 1];
14247 let dropped: Vec<Expr> = grouping_universe
14248 .iter()
14249 .filter(|u| !set.iter().any(|k| k == *u))
14250 .cloned()
14251 .collect();
14252 let mut pe = gexpr.clone();
14253 Self::substitute_grouping_calls(&mut pe, &dropped);
14254 peer.items.push(SelectItem::Expr {
14255 expr: pe,
14256 alias: Some(colname.clone()),
14257 });
14258 }
14259 }
14260 for ob in &mut order_keys {
14261 Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
14262 }
14263 stmt.order_by = order_keys;
14264 }
14265 }
14266 Ok(stmt)
14267 }
14268
14269 /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
14270 /// as ORDER BY keys.
14271 ///
14272 /// Per key: the rollup marker, then the key. Sorting on the key alone
14273 /// is not enough, and a table with a NULL in it says why — MariaDB puts
14274 /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
14275 /// the ROLLUP-introduced NULL last, and both print as NULL.
14276 /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
14277 /// real group including the data-NULL one, 1 only for the row the
14278 /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
14279 /// rolls up to NULL|2, a|1, b|3, NULL|6.
14280 ///
14281 /// `#[inline(never)]`: its locals must not join the statement parser's
14282 /// frame, which round 430 measured sitting against the nesting guard.
14283 #[inline(never)]
14284 fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
14285 let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
14286 for e in keys {
14287 out.push(OrderBy {
14288 expr: Expr::FunctionCall {
14289 name: "grouping".into(),
14290 args: alloc::vec![e.clone()],
14291 },
14292 desc: false,
14293 nulls_first: None,
14294 collation: None,
14295 });
14296 out.push(OrderBy {
14297 expr: e.clone(),
14298 desc: false,
14299 // MySQL orders NULL first on an ascending key.
14300 nulls_first: Some(true),
14301 collation: None,
14302 });
14303 }
14304 out
14305 }
14306
14307 /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
14308 /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
14309 #[inline(never)]
14310 fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
14311 use crate::ast::MaintainKind;
14312 self.skip_paren_option_list();
14313 let kind = match self.peek() {
14314 // `TABLE` and `INDEX` lex as keywords, not identifiers.
14315 Token::Table | Token::Index => {
14316 self.advance();
14317 MaintainKind::ReindexRelation
14318 }
14319 Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
14320 "index" | "table" => {
14321 self.advance();
14322 MaintainKind::ReindexRelation
14323 }
14324 "schema" => {
14325 self.advance();
14326 MaintainKind::ReindexSchema
14327 }
14328 "system" | "database" => {
14329 self.advance();
14330 MaintainKind::Whole
14331 }
14332 // PG requires the object type; anything else is the
14333 // caller's problem, not something to swallow.
14334 _ => MaintainKind::ReindexRelation,
14335 },
14336 _ => MaintainKind::Whole,
14337 };
14338 // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
14339 // allows the plain form, so the modifier is recorded rather than
14340 // skipped. It still has no effect on how the reindex runs.
14341 let mut concurrently = false;
14342 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14343 self.advance();
14344 concurrently = true;
14345 }
14346 let target = self.take_optional_maintain_name();
14347 self.consume_until_statement_boundary();
14348 Ok(Statement::Maintain {
14349 kind,
14350 concurrently,
14351 target,
14352 })
14353 }
14354
14355 /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14356 /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14357 #[inline(never)]
14358 fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14359 use crate::ast::MaintainKind;
14360 self.skip_paren_option_list();
14361 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14362 self.advance();
14363 }
14364 let target = self.take_optional_maintain_name();
14365 self.consume_until_statement_boundary();
14366 Ok(Statement::Maintain {
14367 kind: if target.is_some() {
14368 MaintainKind::ClusterRelation
14369 } else {
14370 MaintainKind::Whole
14371 },
14372 // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14373 // transaction block quite happily (measured).
14374 concurrently: false,
14375 target,
14376 })
14377 }
14378
14379 /// The next token as a relation / schema name, when there is one.
14380 fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14381 match self.peek() {
14382 Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14383 Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14384 _ => None,
14385 },
14386 _ => None,
14387 }
14388 }
14389
14390 /// A parenthesised option list, absorbed.
14391 fn skip_paren_option_list(&mut self) {
14392 if !matches!(self.peek(), Token::LParen) {
14393 return;
14394 }
14395 let mut depth = 0usize;
14396 loop {
14397 match self.advance() {
14398 Token::LParen => depth += 1,
14399 Token::RParen => {
14400 depth -= 1;
14401 if depth == 0 {
14402 return;
14403 }
14404 }
14405 Token::Eof => return,
14406 _ => {}
14407 }
14408 }
14409 }
14410
14411 /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14412 /// column list.
14413 ///
14414 /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14415 /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14416 /// / ALL. The three that describe physical storage have no meaning
14417 /// here, so they parse and change nothing rather than making a
14418 /// dump that mentions them fail to load.
14419 ///
14420 /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14421 /// parse chain the nesting sentinel is tuned against.
14422 #[inline(never)]
14423 fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14424 self.advance(); // LIKE
14425 let source = self.expect_ident_like()?;
14426 let mut options = crate::ast::LikeOptions::default();
14427 loop {
14428 let including = match self.peek() {
14429 Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14430 Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14431 _ => break,
14432 };
14433 self.advance();
14434 // `ALL` lexes as its own keyword, not an identifier.
14435 let opt = if matches!(self.peek(), Token::All) {
14436 self.advance();
14437 alloc::string::String::from("all")
14438 } else {
14439 self.expect_ident_like()?
14440 };
14441 let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14442 o.defaults = on;
14443 o.constraints = on;
14444 o.identity = on;
14445 o.generated = on;
14446 o.indexes = on;
14447 o.comments = on;
14448 };
14449 match opt.to_ascii_lowercase().as_str() {
14450 "all" => set(&mut options, including),
14451 "defaults" => options.defaults = including,
14452 "constraints" => options.constraints = including,
14453 "identity" => options.identity = including,
14454 "generated" => options.generated = including,
14455 "indexes" => options.indexes = including,
14456 "comments" => options.comments = including,
14457 // No storage model to copy into.
14458 "storage" | "statistics" | "compression" => {}
14459 other => {
14460 return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14461 }
14462 }
14463 }
14464 Ok(crate::ast::LikeSpec {
14465 source,
14466 at,
14467 options,
14468 })
14469 }
14470
14471 fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14472 // Caller already consumed CREATE; we're sitting on TABLE.
14473 debug_assert!(matches!(self.peek(), Token::Table));
14474 self.advance();
14475 let if_not_exists = self.consume_if_not_exists();
14476 let name = self.expect_ident_like()?;
14477 // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14478 // child shape has no column list; the child inherits its
14479 // columns from the parent at engine-DDL time. Detect it
14480 // before the `(` requirement below.
14481 if matches!(self.peek(), Token::Partition)
14482 && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14483 {
14484 self.advance(); // PARTITION
14485 self.advance(); // of
14486 let partition_of = self.parse_partition_of_tail()?;
14487 return Ok(Statement::CreateTable(CreateTableStatement {
14488 temporary: false,
14489 name,
14490 engine: None,
14491 columns: Vec::new(),
14492 like_specs: Vec::new(),
14493 inherits: Vec::new(),
14494 if_not_exists,
14495 foreign_keys: Vec::new(),
14496 table_constraints: Vec::new(),
14497 partition_by: None,
14498 partition_of: Some(partition_of),
14499 }));
14500 }
14501 // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14502 // the materialized-view materialisation path (run the SELECT, infer the
14503 // column types, create + populate the table) but marks the node so the
14504 // executor creates a plain table without a mat-view registry entry.
14505 if matches!(self.peek(), Token::As) {
14506 self.advance();
14507 let body_stmt = self.parse_select_stmt()?;
14508 let Statement::Select(body) = body_stmt else {
14509 return Err(self.err(format!(
14510 "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14511 )));
14512 };
14513 let with_data = self.parse_optional_with_data(true)?;
14514 return Ok(Statement::CreateMaterializedView(
14515 crate::ast::CreateMaterializedViewStatement {
14516 temporary: false,
14517 name,
14518 if_not_exists,
14519 columns: Vec::new(),
14520 body,
14521 with_data,
14522 as_plain_table: true,
14523 },
14524 ));
14525 }
14526 if !matches!(self.peek(), Token::LParen) {
14527 return Err(self.err(format!(
14528 "expected '(' after table name, got {:?}",
14529 self.peek()
14530 )));
14531 }
14532 self.advance();
14533 let mut columns = Vec::new();
14534 let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14535 let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14536 let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14537 loop {
14538 // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14539 // column list. It is how a child that adds nothing of its own is
14540 // written, and this loop demanded at least one entry: `syntax
14541 // error at or near ")"`. The child takes the parent's columns,
14542 // which the INHERITS clause already arranges.
14543 if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14544 self.advance();
14545 break;
14546 }
14547 // v7.6.0 / v7.9.18 — distinguish table-level constraint
14548 // clauses from column definitions. Constraints start
14549 // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14550 // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14551 // a column.
14552 if self.peek_table_level_pk_start() {
14553 table_constraints.push(self.parse_table_level_primary_key()?);
14554 } else if matches!(self.peek(), Token::Like) {
14555 // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14556 // <opt> ]*`. The source table's shape lives in the catalog,
14557 // so this records the clause and the engine expands it.
14558 like_specs.push(self.parse_create_table_like(columns.len())?);
14559 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14560 // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14561 table_constraints.push(self.parse_table_level_exclude()?);
14562 } else if self.peek_table_level_unique_start() {
14563 table_constraints.push(self.parse_table_level_unique()?);
14564 } else if self.peek_table_level_check_start() {
14565 // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14566 table_constraints.push(self.parse_table_level_check()?);
14567 } else if self.peek_mysql_inline_key_start() {
14568 // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14569 // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14570 // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14571 // inside the column list. Skip name + paren list;
14572 // for UNIQUE KEY, register as a UC.
14573 if let Some(uc) = self.parse_mysql_inline_key()? {
14574 table_constraints.push(uc);
14575 }
14576 } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14577 // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14578 // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14579 // CHECK is named, and the named-CONSTRAINT arm used
14580 // to accept FOREIGN KEY only. The name is accepted
14581 // and discarded — same handling as every other SPG
14582 // constraint name.
14583 self.advance(); // CONSTRAINT
14584 // v7.39 (read01 round 48) — the name is kept now: the schema
14585 // stores it, so DROP / RENAME CONSTRAINT can find it.
14586 let con_name = self.expect_ident_like()?;
14587 let mut tc = match kind {
14588 NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14589 NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14590 NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14591 NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14592 };
14593 match &mut tc {
14594 crate::ast::TableConstraint::Check { name, .. }
14595 | crate::ast::TableConstraint::Unique { name, .. }
14596 | crate::ast::TableConstraint::PrimaryKey { name, .. }
14597 | crate::ast::TableConstraint::Exclude { name, .. } => {
14598 *name = Some(con_name);
14599 }
14600 _ => {}
14601 }
14602 table_constraints.push(tc);
14603 } else if self.peek_constraint_or_fk_start() {
14604 foreign_keys.push(self.parse_table_level_fk()?);
14605 } else {
14606 let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14607 // v7.13.0 — fold inline UNIQUE / CHECK column
14608 // constraints into table-level entries so the
14609 // engine path stays uniform.
14610 if col.is_unique {
14611 table_constraints.push(crate::ast::TableConstraint::Unique {
14612 name: None,
14613 columns: alloc::vec![col.name.clone()],
14614 nulls_not_distinct: col.unique_nulls_not_distinct,
14615 deferrable: col.constraint_deferrable,
14616 initially_deferred: col.constraint_initially_deferred,
14617 });
14618 }
14619 if let Some(check_expr) = col.check.clone() {
14620 table_constraints.push(crate::ast::TableConstraint::Check {
14621 name: None,
14622 expr: check_expr,
14623 not_valid: false,
14624 });
14625 }
14626 columns.push(col);
14627 if let Some(fk) = col_level_fk {
14628 foreign_keys.push(fk);
14629 }
14630 }
14631 match self.peek() {
14632 Token::Comma => {
14633 self.advance();
14634 }
14635 Token::RParen => {
14636 self.advance();
14637 break;
14638 }
14639 other => {
14640 return Err(
14641 self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14642 );
14643 }
14644 }
14645 }
14646 // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14647 // `CREATE TABLE k (LIKE t)` is a complete definition even though
14648 // nothing is written between the parentheses.
14649 // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14650 // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14651 // empty parentheses were a parse error in their own right — quite apart
14652 // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14653 // SPG does not have (filed separately).
14654 let _ = &like_specs;
14655 // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14656 // It sits between the column list and the MySQL table options,
14657 // and it was a syntax error until this round.
14658 let mut inherits: Vec<String> = Vec::new();
14659 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14660 if k.eq_ignore_ascii_case("inherits"))
14661 {
14662 self.advance();
14663 if !matches!(self.peek(), Token::LParen) {
14664 return Err(self.err(alloc::format!(
14665 "expected ( after INHERITS, got {:?}",
14666 self.peek()
14667 )));
14668 }
14669 self.advance();
14670 loop {
14671 inherits.push(self.expect_ident_like()?);
14672 if matches!(self.peek(), Token::Comma) {
14673 self.advance();
14674 continue;
14675 }
14676 break;
14677 }
14678 if !matches!(self.peek(), Token::RParen) {
14679 return Err(self.err(alloc::format!(
14680 "expected ) closing INHERITS, got {:?}",
14681 self.peek()
14682 )));
14683 }
14684 self.advance();
14685 }
14686 // v7.14.0 — consume MySQL/MariaDB table options after the
14687 // closing `)`. mysqldump emits things like
14688 // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14689 // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14690 // SPG accepts all forms as no-ops (each option is
14691 // `<ident> [=] <ident-or-string>` separated by whitespace).
14692 let engine = self.consume_mysql_table_options();
14693 // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14694 // SPG has no per-table reloptions, so accept and ignore them so a
14695 // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14696 self.consume_with_reloptions();
14697 // v7.37.6-B — declarative-partition-parent suffix
14698 // (`PARTITION BY RANGE (key_col)`) sits after the column
14699 // list + MySQL table-options. v7.37.6-B only accepts RANGE
14700 // and locks the key column at one ident; the engine then
14701 // verifies the column type is TIMESTAMPTZ.
14702 let partition_by = if matches!(self.peek(), Token::Partition) {
14703 self.advance(); // PARTITION
14704 if !self.peek_is_by() {
14705 return Err(self.err(format!(
14706 "expected BY after PARTITION, got {:?}",
14707 self.peek()
14708 )));
14709 }
14710 self.advance();
14711 Some(self.parse_partition_by_tail()?)
14712 } else {
14713 None
14714 };
14715 Ok(Statement::CreateTable(CreateTableStatement {
14716 temporary: false,
14717 name,
14718 engine,
14719 columns,
14720 like_specs,
14721 inherits,
14722 if_not_exists,
14723 foreign_keys,
14724 table_constraints,
14725 partition_by,
14726 partition_of: None,
14727 }))
14728 }
14729
14730 /// v7.37.6-B — case-insensitive ident match helper for the
14731 /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14732 /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14733 /// didn't burn a global keyword slot for each (see the
14734 /// `Token::Partition` doc-comment in `lexer.rs`).
14735 fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14736 matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14737 }
14738
14739 /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14740 /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14741 fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14742 use crate::ast::{PartitionBySpec, PartitionKindAst};
14743 let kind = match self.peek() {
14744 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14745 self.advance();
14746 PartitionKindAst::Range
14747 }
14748 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14749 self.advance();
14750 PartitionKindAst::List
14751 }
14752 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14753 self.advance();
14754 PartitionKindAst::Hash
14755 }
14756 other => {
14757 return Err(self.err(format!(
14758 "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14759 )));
14760 }
14761 };
14762 if !matches!(self.peek(), Token::LParen) {
14763 return Err(self.err(format!(
14764 "expected '(' after PARTITION BY <strategy>, got {:?}",
14765 self.peek()
14766 )));
14767 }
14768 self.advance();
14769 let mut key_columns = Vec::new();
14770 loop {
14771 key_columns.push(self.expect_ident_like()?);
14772 match self.peek() {
14773 Token::Comma => {
14774 self.advance();
14775 }
14776 Token::RParen => {
14777 self.advance();
14778 break;
14779 }
14780 other => {
14781 return Err(self.err(format!(
14782 "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14783 )));
14784 }
14785 }
14786 }
14787 if key_columns.is_empty() {
14788 return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14789 }
14790 Ok(PartitionBySpec { kind, key_columns })
14791 }
14792
14793 /// v7.37.6-B — after `PARTITION OF`, expect
14794 /// <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14795 /// or
14796 /// <parent> DEFAULT
14797 fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14798 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14799 let parent_name = self.expect_ident_like()?;
14800 // v7.37.6-B rejects an explicit column list — the child
14801 // inherits from the parent. mailrs round-7 taught us that
14802 // CREATE TABLE-side schema reconciliation hides drift, so
14803 // we surface this as a parse error rather than silently
14804 // ignoring user columns.
14805 if matches!(self.peek(), Token::LParen) {
14806 return Err(self.err(
14807 "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14808 at v7.37.6-B; the child inherits its columns from the parent"
14809 .to_string(),
14810 ));
14811 }
14812 let bounds = match self.peek() {
14813 Token::Default => {
14814 self.advance();
14815 PartitionOfBoundsAst::Default
14816 }
14817 Token::For => {
14818 self.advance();
14819 if !matches!(self.peek(), Token::Values) {
14820 return Err(
14821 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14822 );
14823 }
14824 self.advance();
14825 // WITH is not a reserved Token in the lexer — it lexes
14826 // as Token::Ident("with"). Disambiguate manually.
14827 let want_with = matches!(
14828 self.peek(),
14829 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14830 );
14831 if want_with {
14832 self.advance();
14833 if !matches!(self.peek(), Token::LParen) {
14834 return Err(self.err(format!(
14835 "expected '(' after FOR VALUES WITH, got {:?}",
14836 self.peek()
14837 )));
14838 }
14839 self.advance();
14840 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14841 loop {
14842 let key = self.expect_ident_like()?;
14843 let n = match self.peek().clone() {
14844 Token::Integer(v) if u32::try_from(v).is_ok() => {
14845 self.advance();
14846 v as u32
14847 }
14848 other => {
14849 return Err(self.err(format!(
14850 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14851 )));
14852 }
14853 };
14854 match key.to_ascii_uppercase().as_str() {
14855 "MODULUS" => modulus = Some(n),
14856 "REMAINDER" => remainder = Some(n),
14857 other => {
14858 return Err(self.err(format!(
14859 "FOR VALUES WITH: unknown key {other:?}; \
14860 expected MODULUS or REMAINDER"
14861 )));
14862 }
14863 }
14864 match self.peek() {
14865 Token::Comma => {
14866 self.advance();
14867 }
14868 Token::RParen => {
14869 self.advance();
14870 break;
14871 }
14872 other => {
14873 return Err(self.err(format!(
14874 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14875 )));
14876 }
14877 }
14878 }
14879 let modulus = modulus
14880 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14881 let remainder = remainder.ok_or_else(|| {
14882 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14883 })?;
14884 if modulus == 0 {
14885 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14886 }
14887 if remainder >= modulus {
14888 return Err(self.err(format!(
14889 "FOR VALUES WITH: REMAINDER ({remainder}) \
14890 must be < MODULUS ({modulus})"
14891 )));
14892 }
14893 PartitionOfBoundsAst::Hash { modulus, remainder }
14894 } else {
14895 match self.peek() {
14896 Token::From => {
14897 self.advance();
14898 let lower = Box::new(self.parse_partition_bound_expr()?);
14899 if !matches!(self.peek(), Token::To) {
14900 return Err(self.err(format!(
14901 "expected TO after FROM (...), got {:?}",
14902 self.peek()
14903 )));
14904 }
14905 self.advance();
14906 let upper = Box::new(self.parse_partition_bound_expr()?);
14907 PartitionOfBoundsAst::Range { lower, upper }
14908 }
14909 // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14910 Token::In => {
14911 self.advance();
14912 if !matches!(self.peek(), Token::LParen) {
14913 return Err(self.err(format!(
14914 "expected '(' after FOR VALUES IN, got {:?}",
14915 self.peek()
14916 )));
14917 }
14918 self.advance();
14919 let mut values = Vec::new();
14920 loop {
14921 values.push(self.parse_expr(0)?);
14922 match self.peek() {
14923 Token::Comma => {
14924 self.advance();
14925 }
14926 Token::RParen => {
14927 self.advance();
14928 break;
14929 }
14930 other => {
14931 return Err(self.err(format!(
14932 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
14933 )));
14934 }
14935 }
14936 }
14937 if values.is_empty() {
14938 return Err(self.err(
14939 "FOR VALUES IN requires at least one literal".to_string(),
14940 ));
14941 }
14942 PartitionOfBoundsAst::List { values }
14943 }
14944 other => {
14945 return Err(self.err(format!(
14946 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
14947 )));
14948 }
14949 }
14950 }
14951 }
14952 other => {
14953 return Err(self.err(format!(
14954 "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
14955 )));
14956 }
14957 };
14958 Ok(PartitionOfSpec {
14959 parent_name,
14960 bounds,
14961 })
14962 }
14963
14964 /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
14965 /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
14966 /// markers (no-arg builtins) so the engine resolves them
14967 /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
14968 fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
14969 if !matches!(self.peek(), Token::LParen) {
14970 return Err(self.err(format!(
14971 "expected '(' before partition bound, got {:?}",
14972 self.peek()
14973 )));
14974 }
14975 self.advance();
14976 let expr = match self.peek() {
14977 Token::Ident(s) | Token::QuotedIdent(s)
14978 if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
14979 {
14980 let name = s.to_ascii_uppercase();
14981 self.advance();
14982 crate::ast::Expr::FunctionCall {
14983 name,
14984 args: Vec::new(),
14985 }
14986 }
14987 _ => self.parse_expr(0)?,
14988 };
14989 if !matches!(self.peek(), Token::RParen) {
14990 return Err(self.err(format!(
14991 "expected ')' after partition bound, got {:?}",
14992 self.peek()
14993 )));
14994 }
14995 self.advance();
14996 Ok(expr)
14997 }
14998
14999 /// v7.14.0 — true when the next tokens look like an inline
15000 /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
15001 /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
15002 /// — each followed by an optional name + `(...)`. Critical:
15003 /// a column NAMED `key` / `index` (PG accepts as ident) must
15004 /// NOT be mistaken for the KEY constraint shape. We disambig
15005 /// by requiring the keyword to be followed by either `(` or
15006 /// `<ident> (`.
15007 fn peek_mysql_inline_key_start(&self) -> bool {
15008 let cur = self.peek();
15009 // Shapes:
15010 // KEY (cols)
15011 // KEY name (cols)
15012 // INDEX (cols)
15013 // INDEX name (cols)
15014 // UNIQUE KEY [name] (cols)
15015 // UNIQUE INDEX [name] (cols)
15016 // FULLTEXT [KEY|INDEX] [name] (cols)
15017 // SPATIAL [KEY|INDEX] [name] (cols)
15018 let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
15019 // tokens at skip = the position AFTER the index-form
15020 // keywords (KEY/INDEX) have been consumed.
15021 match self.tokens.get(skip) {
15022 Some(Token::LParen) => true,
15023 Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
15024 matches!(self.tokens.get(skip + 1), Some(Token::LParen))
15025 }
15026 _ => false,
15027 }
15028 };
15029 // `INDEX` lexes as Token::Index (reserved), not as
15030 // Token::Ident("index"). Both shapes count as a KEY/INDEX
15031 // start; the peek helper below handles either.
15032 let is_key_or_index_tok = |t: &Token| -> bool {
15033 matches!(t, Token::Index)
15034 || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
15035 };
15036 match cur {
15037 Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
15038 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15039 after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
15040 }
15041 Token::Ident(s)
15042 if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
15043 {
15044 let nxt = self.tokens.get(self.pos + 1);
15045 let after_after = if nxt.is_some_and(is_key_or_index_tok) {
15046 self.pos + 2
15047 } else {
15048 self.pos + 1
15049 };
15050 after_keyword_followed_by_paren_or_ident_paren(after_after)
15051 }
15052 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
15053 let nxt = self.tokens.get(self.pos + 1);
15054 if !nxt.is_some_and(is_key_or_index_tok) {
15055 return false;
15056 }
15057 after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
15058 }
15059 _ => false,
15060 }
15061 }
15062
15063 /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
15064 /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
15065 /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
15066 /// returns Some(TableConstraint::Index) so the engine builds
15067 /// a real BTree index on the leading column (mysqldump
15068 /// `KEY idx_posts_author (author_id)` shape).
15069 /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
15070 /// (the storage layer has no matching AM).
15071 fn parse_mysql_inline_key(
15072 &mut self,
15073 ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
15074 // Detect UNIQUE prefix.
15075 let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
15076 {
15077 self.advance();
15078 true
15079 } else {
15080 false
15081 };
15082 // Consume FULLTEXT / SPATIAL prefix and record which one
15083 // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
15084 // dedicated TableConstraint variant so the engine can
15085 // build a tsvector-GIN; SPATIAL still has no matching
15086 // AM, so it falls back to accept-as-no-op.
15087 let mut is_fulltext = false;
15088 let mut is_spatial = false;
15089 if let Token::Ident(s) = self.peek().clone() {
15090 if s.eq_ignore_ascii_case("fulltext") {
15091 self.advance();
15092 is_fulltext = true;
15093 } else if s.eq_ignore_ascii_case("spatial") {
15094 self.advance();
15095 is_spatial = true;
15096 }
15097 }
15098 // KEY / INDEX keyword. `INDEX` lexes as Token::Index
15099 // (reserved); accept either token shape.
15100 match self.peek() {
15101 Token::Index => {
15102 self.advance();
15103 }
15104 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15105 self.advance();
15106 }
15107 other => {
15108 return Err(self.err(alloc::format!(
15109 "expected KEY/INDEX in inline index declaration, got {other:?}"
15110 )));
15111 }
15112 }
15113 // Optional index name (an ident before the `(`).
15114 // v7.15.0 — capture the name when present so the engine
15115 // builds the secondary index under the user's chosen
15116 // name (matches mysqldump's `KEY idx_x (col)` shape).
15117 let mut idx_name: Option<String> = None;
15118 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
15119 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
15120 {
15121 if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
15122 idx_name = Some(s);
15123 }
15124 }
15125 // Optional `USING BTREE` / `USING HASH` (MySQL).
15126 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15127 self.advance();
15128 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15129 self.advance();
15130 }
15131 }
15132 // Required column list `(col [, col]*)`.
15133 if !matches!(self.peek(), Token::LParen) {
15134 return Err(self.err(alloc::format!(
15135 "expected '(' in inline KEY/INDEX, got {:?}",
15136 self.peek()
15137 )));
15138 }
15139 self.advance();
15140 let mut cols: Vec<String> = Vec::new();
15141 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
15142 self.advance();
15143 cols.push(s);
15144 // Skip optional `(length)` per-column prefix.
15145 if matches!(self.peek(), Token::LParen) {
15146 let mut depth = 1usize;
15147 self.advance();
15148 while depth > 0 {
15149 match self.peek() {
15150 Token::LParen => depth += 1,
15151 Token::RParen => depth -= 1,
15152 Token::Eof => break,
15153 _ => {}
15154 }
15155 self.advance();
15156 }
15157 }
15158 // Skip optional ASC / DESC.
15159 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
15160 || matches!(self.peek(), Token::Asc | Token::Desc)
15161 {
15162 self.advance();
15163 }
15164 if matches!(self.peek(), Token::Comma) {
15165 self.advance();
15166 continue;
15167 }
15168 break;
15169 }
15170 if matches!(self.peek(), Token::RParen) {
15171 self.advance();
15172 }
15173 // Trailing options on the inline index — comment / etc.
15174 // Skip until comma or `)`.
15175 while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
15176 self.advance();
15177 }
15178 if cols.is_empty() {
15179 return Ok(None);
15180 }
15181 if is_unique {
15182 // Carry the captured idx_name on UNIQUE too so future
15183 // engine work can name the underlying BTree
15184 // accordingly; today the unique-constraint installer
15185 // synthesises the name itself, but Display round-trip
15186 // benefits from preserving it.
15187 Ok(Some(crate::ast::TableConstraint::Unique {
15188 name: idx_name,
15189 columns: cols,
15190 nulls_not_distinct: false,
15191 // MySQL inline UNIQUE KEY has no deferral vocabulary.
15192 deferrable: false,
15193 initially_deferred: false,
15194 }))
15195 } else if is_fulltext {
15196 // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
15197 // routes through `TableConstraint::FulltextIndex`;
15198 // the engine builds a tsvector-GIN over each named
15199 // column so MATCH AGAINST gets a real inverted
15200 // index instead of a silently-dropped declaration.
15201 Ok(Some(crate::ast::TableConstraint::FulltextIndex {
15202 name: idx_name,
15203 columns: cols,
15204 }))
15205 } else if is_spatial {
15206 // SPG has no native SPATIAL AM. Accept-as-no-op
15207 // (declaration is parsed, but no index is built).
15208 Ok(None)
15209 } else {
15210 // v7.15.0 — plain KEY / INDEX builds a real BTree
15211 // secondary index.
15212 Ok(Some(crate::ast::TableConstraint::Index {
15213 name: idx_name,
15214 columns: cols,
15215 }))
15216 }
15217 }
15218
15219 /// v7.14.0 — consume MySQL/MariaDB table-options tail after
15220 /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
15221 /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
15222 /// (in any order, separated by whitespace).
15223 /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
15224 /// storage-parameter clause on CREATE TABLE. SPG has no per-table
15225 /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
15226 /// bare ident here, and only the parenthesised form is reloptions (so this
15227 /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
15228 fn consume_with_reloptions(&mut self) {
15229 let is_with = matches!(
15230 self.peek(),
15231 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15232 );
15233 if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
15234 return;
15235 }
15236 self.advance(); // WITH
15237 self.advance(); // (
15238 let mut depth = 1u32;
15239 while depth > 0 && !matches!(self.peek(), Token::Eof) {
15240 match self.peek() {
15241 Token::LParen => depth += 1,
15242 Token::RParen => depth -= 1,
15243 _ => {}
15244 }
15245 self.advance();
15246 }
15247 }
15248
15249 /// v7.39 — returns the `ENGINE=` name, which used to be consumed and
15250 /// dropped with everything else here. The rest of the MySQL table
15251 /// options genuinely have no meaning for SPG's storage; the engine
15252 /// name does, because MySQL REFUSES one it does not know and a dump
15253 /// with a typo in it should not quietly become a table.
15254 fn consume_mysql_table_options(&mut self) -> Option<alloc::string::String> {
15255 let mut engine: Option<alloc::string::String> = None;
15256 loop {
15257 // Heuristic: a table option is an ident (or `DEFAULT`
15258 // reserved keyword) followed by `=` and an
15259 // ident / string / integer.
15260 let name_lc = match self.peek().clone() {
15261 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15262 Token::Default => alloc::string::String::from("default"),
15263 _ => break,
15264 };
15265 let known = matches!(
15266 name_lc.as_str(),
15267 "engine"
15268 | "default"
15269 | "charset"
15270 | "collate"
15271 | "auto_increment"
15272 | "row_format"
15273 | "comment"
15274 | "pack_keys"
15275 | "stats_persistent"
15276 | "stats_auto_recalc"
15277 | "stats_sample_pages"
15278 | "key_block_size"
15279 | "tablespace"
15280 | "min_rows"
15281 | "max_rows"
15282 | "checksum"
15283 | "delay_key_write"
15284 | "insert_method"
15285 | "data"
15286 | "index"
15287 | "encryption"
15288 | "compression"
15289 );
15290 if !known {
15291 break;
15292 }
15293 self.advance(); // option name
15294 // `DEFAULT` optional prefix is followed by `CHARSET` /
15295 // `COLLATE`; consume the next ident too.
15296 if name_lc == "default" {
15297 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15298 self.advance();
15299 }
15300 }
15301 if matches!(self.peek(), Token::Eq) {
15302 self.advance();
15303 }
15304 match self.peek().clone() {
15305 Token::Ident(v) | Token::QuotedIdent(v) | Token::String(v) => {
15306 if name_lc == "engine" {
15307 engine = Some(v);
15308 }
15309 self.advance();
15310 }
15311 Token::Integer(_) => {
15312 self.advance();
15313 }
15314 _ => {}
15315 }
15316 }
15317 engine
15318 }
15319
15320 /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
15321 /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
15322 /// sure (otherwise a column literally named `primary` would
15323 /// be mistaken).
15324 fn peek_table_level_pk_start(&self) -> bool {
15325 let cur = self.peek();
15326 let nxt = self.tokens.get(self.pos + 1);
15327 let nxt2 = self.tokens.get(self.pos + 2);
15328 let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
15329 let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
15330 let is_lparen = matches!(nxt2, Some(Token::LParen));
15331 is_primary && is_key && is_lparen
15332 }
15333
15334 /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
15335 /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
15336 /// (mailrs round-5 G10).
15337 fn peek_table_level_unique_start(&self) -> bool {
15338 let cur = self.peek();
15339 let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
15340 if !is_unique {
15341 return false;
15342 }
15343 let n1 = self.tokens.get(self.pos + 1);
15344 // Plain `UNIQUE (…)`.
15345 if matches!(n1, Some(Token::LParen)) {
15346 return true;
15347 }
15348 // `UNIQUE NULLS [NOT] DISTINCT (…)`.
15349 let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
15350 if !is_nulls {
15351 return false;
15352 }
15353 let n2 = self.tokens.get(self.pos + 2);
15354 let n3 = self.tokens.get(self.pos + 3);
15355 let n4 = self.tokens.get(self.pos + 4);
15356 // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15357 if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15358 return true;
15359 }
15360 // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15361 if matches!(n2, Some(Token::Not))
15362 && matches!(n3, Some(Token::Distinct))
15363 && matches!(n4, Some(Token::LParen))
15364 {
15365 return true;
15366 }
15367 false
15368 }
15369
15370 fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15371 self.advance(); // PRIMARY
15372 self.advance(); // KEY
15373 let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15374 // v7.39 (round 711) — the trailer's values are CARRIED now; round
15375 // 621 consumed and dropped them (the storing half of F08).
15376 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15377 Ok(crate::ast::TableConstraint::PrimaryKey {
15378 name: None,
15379 columns,
15380 deferrable,
15381 initially_deferred,
15382 })
15383 }
15384
15385 fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15386 self.advance(); // UNIQUE
15387 // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15388 // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15389 // is `NULLS DISTINCT` per the SQL standard.
15390 let mut nulls_not_distinct = false;
15391 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15392 let n1 = self.tokens.get(self.pos + 1);
15393 let n2 = self.tokens.get(self.pos + 2);
15394 let is_not = matches!(n1, Some(Token::Not));
15395 let is_distinct = matches!(n2, Some(Token::Distinct));
15396 if is_not && is_distinct {
15397 self.advance(); // NULLS
15398 self.advance(); // NOT
15399 self.advance(); // DISTINCT
15400 nulls_not_distinct = true;
15401 } else if matches!(n1, Some(Token::Distinct)) {
15402 self.advance(); // NULLS
15403 self.advance(); // DISTINCT
15404 }
15405 }
15406 let columns = self.parse_paren_ident_list("UNIQUE")?;
15407 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15408 Ok(crate::ast::TableConstraint::Unique {
15409 name: None,
15410 columns,
15411 nulls_not_distinct,
15412 deferrable,
15413 initially_deferred,
15414 })
15415 }
15416
15417 /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15418 /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15419 /// expression.
15420 /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15421 /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15422 /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15423 /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15424 /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15425 /// commit: `NOT` starts no other suffix here, but reading both
15426 /// tokens before advancing keeps the caller's error message intact
15427 /// if someone writes `NOT NULL` by mistake.
15428 fn parse_not_valid_suffix(&mut self) -> bool {
15429 if !matches!(self.peek(), Token::Not) {
15430 return false;
15431 }
15432 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15433 {
15434 return false;
15435 }
15436 self.advance();
15437 self.advance();
15438 true
15439 }
15440
15441 fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15442 self.advance(); // EXCLUDE
15443 // Optional `USING <method>`.
15444 let mut method = None;
15445 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15446 self.advance();
15447 method = Some(match self.advance() {
15448 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15449 other => {
15450 return Err(self.err(alloc::format!(
15451 "expected index method after USING, got {other:?}"
15452 )));
15453 }
15454 });
15455 }
15456 if !matches!(self.peek(), Token::LParen) {
15457 return Err(self.err(alloc::format!(
15458 "expected '(' after EXCLUDE, got {:?}",
15459 self.peek()
15460 )));
15461 }
15462 self.advance();
15463 let mut elements: Vec<(String, String)> = Vec::new();
15464 loop {
15465 let col = match self.advance() {
15466 Token::Ident(s) | Token::QuotedIdent(s) => s,
15467 other => {
15468 return Err(self.err(alloc::format!(
15469 "expected column name in EXCLUDE, got {other:?}"
15470 )));
15471 }
15472 };
15473 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15474 return Err(self.err(alloc::format!(
15475 "expected WITH after EXCLUDE column, got {:?}",
15476 self.peek()
15477 )));
15478 }
15479 self.advance();
15480 let op = match self.advance() {
15481 Token::InetOverlap => String::from("&&"),
15482 Token::Intersects => String::from("?#"),
15483 Token::IsBelow => String::from("<^"),
15484 Token::IsAbove => String::from(">^"),
15485 Token::PatternLt => String::from("~<~"),
15486 Token::PatternLtEq => String::from("~<=~"),
15487 Token::PatternGt => String::from("~>~"),
15488 Token::PatternGtEq => String::from("~>=~"),
15489 Token::TsMatchOld => String::from("@@@"),
15490 Token::Eq => String::from("="),
15491 Token::JsonContains => String::from("@>"),
15492 Token::JsonContainedBy => String::from("<@"),
15493 Token::OverLeft => String::from("&<"),
15494 Token::OverRight => String::from("&>"),
15495 other => {
15496 return Err(self.err(alloc::format!(
15497 "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15498 )));
15499 }
15500 };
15501 elements.push((col, op));
15502 if matches!(self.peek(), Token::Comma) {
15503 self.advance();
15504 continue;
15505 }
15506 break;
15507 }
15508 if !matches!(self.peek(), Token::RParen) {
15509 return Err(self.err(alloc::format!(
15510 "expected ')' to close EXCLUDE, got {:?}",
15511 self.peek()
15512 )));
15513 }
15514 self.advance();
15515 Ok(crate::ast::TableConstraint::Exclude {
15516 name: None,
15517 method,
15518 elements,
15519 })
15520 }
15521
15522 fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15523 self.advance(); // CHECK
15524 if !matches!(self.peek(), Token::LParen) {
15525 return Err(self.err(alloc::format!(
15526 "expected '(' after CHECK, got {:?}",
15527 self.peek()
15528 )));
15529 }
15530 self.advance();
15531 let expr = self.parse_expr(0)?;
15532 if !matches!(self.peek(), Token::RParen) {
15533 return Err(self.err(alloc::format!(
15534 "expected ')' to close CHECK predicate, got {:?}",
15535 self.peek()
15536 )));
15537 }
15538 self.advance();
15539 // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15540 // are no existing rows for PG to skip, so it rejects the suffix.
15541 Ok(crate::ast::TableConstraint::Check {
15542 name: None,
15543 expr,
15544 not_valid: false,
15545 })
15546 }
15547
15548 /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15549 fn peek_table_level_check_start(&self) -> bool {
15550 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15551 }
15552
15553 /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15554 /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15555 /// on the dedicated FK path (`parse_table_level_fk` consumes its
15556 /// own CONSTRAINT prefix).
15557 fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15558 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15559 return None;
15560 }
15561 // tokens[pos+1] is the constraint name (any ident-like);
15562 // tokens[pos+2] is the kind keyword.
15563 match self.tokens.get(self.pos + 2) {
15564 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15565 Some(NamedTableConstraintKind::Check)
15566 }
15567 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15568 Some(NamedTableConstraintKind::Unique)
15569 }
15570 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15571 Some(NamedTableConstraintKind::PrimaryKey)
15572 }
15573 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15574 Some(NamedTableConstraintKind::Exclude)
15575 }
15576 _ => None,
15577 }
15578 }
15579
15580 fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15581 if !matches!(self.peek(), Token::LParen) {
15582 return Err(self.err(alloc::format!(
15583 "expected '(' after {ctx}, got {:?}",
15584 self.peek()
15585 )));
15586 }
15587 self.advance();
15588 let mut out = Vec::new();
15589 loop {
15590 out.push(self.expect_ident_like()?);
15591 match self.peek() {
15592 Token::Comma => {
15593 self.advance();
15594 }
15595 Token::RParen => {
15596 self.advance();
15597 break;
15598 }
15599 other => {
15600 return Err(self.err(alloc::format!(
15601 "expected ',' or ')' in {ctx} list, got {other:?}"
15602 )));
15603 }
15604 }
15605 }
15606 if out.is_empty() {
15607 return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15608 }
15609 Ok(out)
15610 }
15611
15612 /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15613 /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15614 /// table-level FK; a column def never starts with either keyword
15615 /// (column names are not in this reserved set).
15616 fn peek_constraint_or_fk_start(&self) -> bool {
15617 let is_constraint_kw = matches!(
15618 self.peek(),
15619 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15620 );
15621 let is_foreign_kw = matches!(
15622 self.peek(),
15623 Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15624 );
15625 is_constraint_kw || is_foreign_kw
15626 }
15627
15628 /// v7.6.0 — parse a table-level FK clause:
15629 /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15630 /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15631 fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15632 let mut name: Option<String> = None;
15633 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15634 self.advance();
15635 name = Some(self.expect_ident_like()?);
15636 }
15637 // `FOREIGN`
15638 match self.advance() {
15639 Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15640 other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15641 }
15642 // `KEY`
15643 match self.advance() {
15644 Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15645 other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15646 }
15647 // `(col, col, ...)`
15648 if !matches!(self.peek(), Token::LParen) {
15649 return Err(self.err(format!(
15650 "expected '(' after FOREIGN KEY, got {:?}",
15651 self.peek()
15652 )));
15653 }
15654 self.advance();
15655 let mut columns = Vec::new();
15656 loop {
15657 columns.push(self.expect_ident_like()?);
15658 match self.peek() {
15659 Token::Comma => {
15660 self.advance();
15661 }
15662 Token::RParen => {
15663 self.advance();
15664 break;
15665 }
15666 other => {
15667 return Err(self.err(format!(
15668 "expected ',' or ')' in FK column list, got {other:?}"
15669 )));
15670 }
15671 }
15672 }
15673 if columns.is_empty() {
15674 return Err(self.err("FOREIGN KEY requires at least one column".into()));
15675 }
15676 let (
15677 parent_table,
15678 parent_columns,
15679 on_delete,
15680 on_update,
15681 match_type,
15682 deferrable,
15683 initially_deferred,
15684 ) = self.parse_references_tail(columns.len())?;
15685 Ok(ForeignKeyConstraint {
15686 name,
15687 columns,
15688 parent_table,
15689 parent_columns,
15690 on_delete,
15691 on_update,
15692 match_type,
15693 deferrable,
15694 initially_deferred,
15695 })
15696 }
15697
15698 /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15699 /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15700 /// the local column count, used to default the parent column
15701 /// list when omitted (SQL spec: parent's PK is implied).
15702 fn parse_references_tail(
15703 &mut self,
15704 expected_arity: usize,
15705 ) -> Result<
15706 (
15707 String,
15708 Vec<String>,
15709 FkAction,
15710 FkAction,
15711 crate::ast::MatchType,
15712 // v7.39 (round 288) — deferrable, initially_deferred.
15713 bool,
15714 bool,
15715 ),
15716 ParseError,
15717 > {
15718 match self.advance() {
15719 Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15720 other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15721 }
15722 let parent_table = self.expect_ident_like()?;
15723 let mut parent_columns: Vec<String> = Vec::new();
15724 if matches!(self.peek(), Token::LParen) {
15725 self.advance();
15726 loop {
15727 parent_columns.push(self.expect_ident_like()?);
15728 match self.peek() {
15729 Token::Comma => {
15730 self.advance();
15731 }
15732 Token::RParen => {
15733 self.advance();
15734 break;
15735 }
15736 other => {
15737 return Err(self.err(format!(
15738 "expected ',' or ')' in REFERENCES column list, got {other:?}"
15739 )));
15740 }
15741 }
15742 }
15743 }
15744 if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15745 return Err(self.err(format!(
15746 "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15747 expected_arity,
15748 parent_columns.len()
15749 )));
15750 }
15751 // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15752 // it between the referenced column list and the ON / DEFERRABLE
15753 // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15754 // is skipped when any referencing column is NULL), so SIMPLE —
15755 // the default, and the only spelling pg_dump emits — is accepted
15756 // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15757 // mixed-NULL rule, which is not wired yet; reject them honestly
15758 // rather than silently applying SIMPLE (PG itself errors on
15759 // MATCH PARTIAL as "not yet implemented").
15760 let mut match_type = crate::ast::MatchType::Simple;
15761 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15762 self.advance();
15763 // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15764 // SIMPLE / PARTIAL arrive as bare identifiers.
15765 let kind = match self.advance() {
15766 Token::Full => "FULL".to_string(),
15767 Token::Ident(s) => s.to_uppercase(),
15768 other => {
15769 return Err(self.err(format!(
15770 "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15771 )));
15772 }
15773 };
15774 match kind.as_str() {
15775 "SIMPLE" => {} // Default — match_type stays Simple.
15776 // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15777 // when ALL referencing columns are NULL; a mixed-NULL key errors.
15778 "FULL" => match_type = crate::ast::MatchType::Full,
15779 "PARTIAL" => {
15780 return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15781 }
15782 _ => {
15783 return Err(self.err(format!(
15784 "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15785 )));
15786 }
15787 }
15788 }
15789 // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15790 // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15791 // <action>` / `ON UPDATE <action>` in either order. PG /
15792 // pg_dump emits the timing clause AFTER the ON clauses
15793 // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15794 // but the SQL spec allows either order. We loop over
15795 // every possible trailer and dispatch on the next token,
15796 // stopping when nothing matches. Phase 3.1 changes the
15797 // bare DEFERRABLE form from hard-error to accept-as-
15798 // immediate; SPG is single-writer with no deferred-
15799 // constraint window so the runtime semantics are always
15800 // immediate even when INITIALLY DEFERRED is requested.
15801 // PG's default referential action (no ON DELETE / ON UPDATE
15802 // clause) is NO ACTION, not RESTRICT — the two enforce
15803 // identically in SPG (single-writer, no deferred window; see the
15804 // shared match arm in constraints.rs) but information_schema.
15805 // referential_constraints must report NO ACTION to match PG.
15806 let mut on_delete = FkAction::NoAction;
15807 let mut on_update = FkAction::NoAction;
15808 let mut seen_on_delete = false;
15809 let mut seen_on_update = false;
15810 let mut deferrable = false;
15811 let mut initially_deferred = false;
15812 loop {
15813 // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15814 let before = self.pos;
15815 let (d, idef) = self.consume_deferrable_clauses_timed()?;
15816 if self.pos != before {
15817 deferrable = d;
15818 initially_deferred = idef;
15819 continue;
15820 }
15821 // ON DELETE / ON UPDATE.
15822 if !matches!(self.peek(), Token::On) {
15823 break;
15824 }
15825 self.advance();
15826 let which = self.advance();
15827 let action = self.parse_fk_action()?;
15828 match which {
15829 Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15830 if seen_on_delete {
15831 return Err(self.err("ON DELETE specified twice".into()));
15832 }
15833 seen_on_delete = true;
15834 on_delete = action;
15835 }
15836 Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15837 if seen_on_update {
15838 return Err(self.err("ON UPDATE specified twice".into()));
15839 }
15840 seen_on_update = true;
15841 on_update = action;
15842 }
15843 other => {
15844 return Err(
15845 self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15846 );
15847 }
15848 }
15849 }
15850 Ok((
15851 parent_table,
15852 parent_columns,
15853 on_delete,
15854 on_update,
15855 match_type,
15856 deferrable,
15857 initially_deferred,
15858 ))
15859 }
15860
15861 /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15862 /// NO ACTION`.
15863 fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15864 match self.advance() {
15865 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15866 Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15867 Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15868 Token::Null => Ok(FkAction::SetNull),
15869 Token::Default => Ok(FkAction::SetDefault),
15870 other => Err(self.err(format!(
15871 "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15872 ))),
15873 },
15874 Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15875 Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15876 other => Err(self.err(format!(
15877 "expected ACTION after NO in FK action, got {other:?}"
15878 ))),
15879 },
15880 other => Err(self.err(format!(
15881 "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15882 ))),
15883 }
15884 }
15885
15886 /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15887 /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15888 fn consume_if_not_exists(&mut self) -> bool {
15889 // `IF` arrives as a bare Ident (we don't reserve it because it
15890 // also appears mid-expression in PG, though we don't support
15891 // those forms yet).
15892 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15893 if !looks_like_if {
15894 return false;
15895 }
15896 // Peek one ahead before committing: only consume IF when it's
15897 // actually `IF NOT EXISTS`.
15898 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
15899 return false;
15900 }
15901 if !matches!(
15902 self.tokens.get(self.pos + 2),
15903 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15904 ) {
15905 return false;
15906 }
15907 self.advance(); // IF
15908 self.advance(); // NOT
15909 self.advance(); // EXISTS
15910 true
15911 }
15912
15913 /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
15914 /// Consumes IF EXISTS as a pair; returns false otherwise
15915 /// without consuming any tokens.
15916 /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
15917 /// ENABLE/DISABLE/FORCE/NO FORCE.
15918 fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
15919 for kw in ["row", "level", "security"] {
15920 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
15921 {
15922 return Err(self.err(alloc::format!(
15923 "expected {} in ROW LEVEL SECURITY, got {:?}",
15924 kw.to_ascii_uppercase(),
15925 self.peek()
15926 )));
15927 }
15928 self.advance();
15929 }
15930 Ok(())
15931 }
15932
15933 fn consume_if_exists(&mut self) -> bool {
15934 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15935 if !looks_like_if {
15936 return false;
15937 }
15938 if !matches!(
15939 self.tokens.get(self.pos + 1),
15940 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15941 ) {
15942 return false;
15943 }
15944 self.advance(); // IF
15945 self.advance(); // EXISTS
15946 true
15947 }
15948
15949 /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
15950 /// qualifiers after an index column ref. ASC / DESC are
15951 /// reserved tokens; NULLS / FIRST / LAST are bare idents.
15952 /// We accept and discard them since single-column BTree
15953 /// stores rows in natural key order today.
15954 /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
15955 /// ORDER BY key. Returns None when absent.
15956 fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
15957 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15958 return Ok(None);
15959 }
15960 self.advance();
15961 match self.advance() {
15962 Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
15963 Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
15964 other => Err(self.err(alloc::format!(
15965 "expected FIRST or LAST after NULLS, got {other:?}"
15966 ))),
15967 }
15968 }
15969
15970 /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
15971 /// rather than discarded.
15972 ///
15973 /// SPG's index does not scan in a direction — column ordering is
15974 /// intrinsic to the storage — but `pg_indexes.indexdef` is a
15975 /// reproduction of the DDL, and dropping the clause meant
15976 /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
15977 /// dump lost it, and a schema diff saw drift on every run.
15978 fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
15979 let mut order = crate::ast::IndexColumnOrder::default();
15980 loop {
15981 match self.peek() {
15982 Token::Asc => {
15983 self.advance();
15984 }
15985 Token::Desc => {
15986 order.descending = true;
15987 self.advance();
15988 }
15989 Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
15990 let look = self.tokens.get(self.pos + 1);
15991 if matches!(
15992 look,
15993 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
15994 || k.eq_ignore_ascii_case("last")
15995 ) {
15996 self.advance();
15997 order.nulls_first = Some(matches!(
15998 self.advance(),
15999 Token::Ident(k) if k.eq_ignore_ascii_case("first")
16000 ));
16001 } else {
16002 break;
16003 }
16004 }
16005 _ => break,
16006 }
16007 }
16008 order
16009 }
16010
16011 fn parse_create_index_stmt_after_create(
16012 &mut self,
16013 is_unique: bool,
16014 ) -> Result<Statement, ParseError> {
16015 // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
16016 debug_assert!(matches!(self.peek(), Token::Index));
16017 self.advance();
16018 // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
16019 // SPG's CREATE INDEX is synchronous end-to-end today (real
16020 // CONCURRENTLY variant with restartable scans queues with
16021 // v7.39 indexes epic), so the modifier has no runtime effect
16022 // — same accept-and-no-op shape as v7.37.16.5 DETACH
16023 // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
16024 // VIEW CONCURRENTLY.
16025 let mut concurrently = false;
16026 if matches!(
16027 self.peek(),
16028 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
16029 ) {
16030 self.advance();
16031 concurrently = true;
16032 }
16033 let if_not_exists = self.consume_if_not_exists();
16034 // v7.39 (read01 round 93) — the index name is optional (PG since
16035 // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
16036 // When the token after `[IF NOT EXISTS]` is already `ON`, no name
16037 // was given; leave it empty and the engine derives a PG-style
16038 // `<table>_<cols>_idx` name at CREATE time (with collision counter).
16039 let name = if matches!(self.peek(), Token::On) {
16040 String::new()
16041 } else {
16042 self.expect_ident_like()?
16043 };
16044 if !matches!(self.peek(), Token::On) {
16045 return Err(self.err(format!(
16046 "expected ON after CREATE INDEX <name>, got {:?}",
16047 self.peek()
16048 )));
16049 }
16050 self.advance();
16051 let table = self.expect_ident_like()?;
16052 // Optional `USING <method>` — only recognised method in v2.0 is
16053 // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
16054 // ident `using` (we don't promote it to a reserved keyword
16055 // because it isn't reserved anywhere else in our SQL surface).
16056 let mut method_name: Option<String> = None;
16057 let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
16058 self.advance();
16059 let m = self.expect_ident_like()?;
16060 method_name = Some(m.to_ascii_lowercase());
16061 match m.to_ascii_lowercase().as_str() {
16062 "hnsw" => IndexMethod::Hnsw,
16063 "btree" => IndexMethod::BTree,
16064 "brin" => IndexMethod::Brin,
16065 // v7.12.3 — real GIN inverted index over `tsvector`.
16066 // v7.9.26b's `USING gin` → BTree silent fallback is
16067 // gone; the engine validates that the indexed column
16068 // is `tsvector` at CREATE INDEX time.
16069 "gin" => IndexMethod::Gin,
16070 // v7.9.26b — PG `pg_dump` emits `USING gist` /
16071 // `USING spgist` / `USING hash` for their built-in
16072 // AMs that SPG doesn't have a matching
16073 // implementation for; degrade to BTree on the
16074 // leading column so the schema loads + the index
16075 // catalogue stays consistent. Operator pays the
16076 // planner cost only for the queries that would have
16077 // used the specialised AM.
16078 "gist" | "spgist" | "hash" => IndexMethod::BTree,
16079 // v7.11.3 — pgvector ships both `ivfflat` and
16080 // `hnsw`. Customers shouldn't have to choose
16081 // their on-disk index method based on what SPG
16082 // implements; accept `ivfflat` as a synonym for
16083 // `hnsw` so PG schemas using either method drop
16084 // in. The vector distance op (`<->` / `<#>` /
16085 // `<=>`) at query time still picks the metric.
16086 "ivfflat" => IndexMethod::Hnsw,
16087 other => {
16088 return Err(self.err(alloc::format!(
16089 "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
16090 )));
16091 }
16092 }
16093 } else {
16094 IndexMethod::BTree
16095 };
16096 if !matches!(self.peek(), Token::LParen) {
16097 return Err(self.err(format!(
16098 "expected '(' before indexed column, got {:?}",
16099 self.peek()
16100 )));
16101 }
16102 self.advance();
16103 // v6.8.2 — accept either a bare column ident (legacy) or
16104 // an expression `fn(col, …)` for expression indexes.
16105 // Distinguish by peeking the token *after* the current
16106 // ident: `ident )` is the legacy column-only path;
16107 // anything else triggers the Pratt expression parser.
16108 // (`advance()` uses `mem::replace` to nil out the current
16109 // slot, so we can't save+rewind cleanly — peek-ahead via
16110 // direct index avoids the mutation.)
16111 let mut opclass: Option<String> = None;
16112 let mut key_collation: Option<String> = None;
16113 let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
16114 // Single column with `)` immediately after — fast path.
16115 // v7.9.29 — also: bare column followed by `,` (the
16116 // multi-column form `(a, b, c)`). Without this branch
16117 // the leading ident gets pulled into `parse_expr`
16118 // which then sets `expression = Some(Column(a))` and
16119 // breaks Display round-trip on the multi-column shape.
16120 Token::Ident(s) | Token::QuotedIdent(s)
16121 if matches!(
16122 self.tokens.get(self.pos + 1),
16123 Some(Token::RParen | Token::Comma)
16124 ) =>
16125 {
16126 self.advance();
16127 (s, None)
16128 }
16129 // v7.9.22 — single column followed by a pgvector
16130 // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
16131 // v7.15.0 — capture the opclass instead of discarding
16132 // it so the engine can dispatch (e.g. `gin_trgm_ops`
16133 // → real trigram-shingle GIN over a TEXT column).
16134 // Vector/HNSW opclasses still take their distance
16135 // metric from the query operator (`<->` / `<#>` /
16136 // `<=>`), so for those callers the opclass stays
16137 // informational.
16138 // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
16139 // opclass: `(embedding public.vector_cosine_ops)`. Strip
16140 // the schema and dispatch on the bare opclass, the same
16141 // treatment table/type names get.
16142 Token::Ident(s) | Token::QuotedIdent(s)
16143 if matches!(
16144 self.tokens.get(self.pos + 1),
16145 Some(Token::Ident(_) | Token::QuotedIdent(_))
16146 ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
16147 && matches!(
16148 self.tokens.get(self.pos + 3),
16149 Some(Token::Ident(op) | Token::QuotedIdent(op))
16150 if is_vector_opclass_name(op)
16151 ) =>
16152 {
16153 self.advance(); // column name
16154 self.advance(); // schema qualifier
16155 self.advance(); // dot
16156 let op_tok = self.advance();
16157 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16158 opclass = Some(op.to_ascii_lowercase());
16159 }
16160 (s, None)
16161 }
16162 // r1038 — an operator class is recognised by its POSITION, not
16163 // by a list of names. It used to be `is_vector_opclass_name`,
16164 // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
16165 // sentori's migration wrote — was a syntax error while
16166 // `USING gin (doc)` parsed. Anything sitting between a column
16167 // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
16168 // two bare identifiers in a row are not valid there otherwise.
16169 Token::Ident(s) | Token::QuotedIdent(s)
16170 if matches!(
16171 self.tokens.get(self.pos + 1),
16172 Some(Token::Ident(op) | Token::QuotedIdent(op))
16173 if is_vector_opclass_name(op) || Self::opclass_position_follows(
16174 self.tokens.get(self.pos + 2)
16175 )
16176 ) =>
16177 {
16178 self.advance(); // column name
16179 // Capture the opclass token, lower-cased for
16180 // case-insensitive engine dispatch.
16181 let op_tok = self.advance();
16182 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16183 opclass = Some(op.to_ascii_lowercase());
16184 }
16185 (s, None)
16186 }
16187 Token::Ident(_) | Token::QuotedIdent(_) => {
16188 // v7.39 (round 538) — an explicit COLLATE on the key,
16189 // read by LOOKAHEAD because `parse_expr` absorbs the
16190 // clause as a no-op (SPG orders text by bytes, which is
16191 // the C collation, so it changes nothing to honour). PG
16192 // still PRINTS it: an explicitly written `"C"` and the
16193 // collation a column inherits are different collation
16194 // OBJECTS even where they sort identically, which is why
16195 // `(a COLLATE "C")` shows on a C-collation database too.
16196 if matches!(
16197 self.tokens.get(self.pos + 1),
16198 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
16199 ) {
16200 key_collation = match self.tokens.get(self.pos + 2) {
16201 Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
16202 Some(n.clone())
16203 }
16204 _ => None,
16205 };
16206 }
16207 let key_expr = self.parse_expr(0)?;
16208 let primary = extract_first_column(&key_expr).ok_or_else(|| {
16209 self.err("expression index key must reference at least one column".into())
16210 })?;
16211 (primary, Some(key_expr))
16212 }
16213 // v7.37.43-T4 — parenthesised expression index key
16214 // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
16215 // PG's CREATE INDEX requires the expression to be in
16216 // its own parens to disambiguate function calls from
16217 // column lists, so this `LParen` is the inner open-paren
16218 // of an expression key. parse_expr handles the recursive
16219 // descent and consumes the matching `RParen`.
16220 Token::LParen => {
16221 let key_expr = self.parse_expr(0)?;
16222 let primary = extract_first_column(&key_expr).ok_or_else(|| {
16223 self.err("expression index key must reference at least one column".into())
16224 })?;
16225 (primary, Some(key_expr))
16226 }
16227 other => {
16228 return Err(self.err(format!(
16229 "expected column ident or expression, got {other:?}"
16230 )));
16231 }
16232 };
16233 // v7.9.14 — accept extra comma-separated columns inside
16234 // the index key parens (`CREATE INDEX … (a, b, c)`).
16235 // mailrs F2. Each extra column may carry an optional
16236 // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
16237 // — parsed and discarded; SPG doesn't honour direction
16238 // on a BTree index today (column ordering is intrinsic
16239 // to the storage). v7.10 will widen to genuine composite
16240 // index keys.
16241 let mut extra_columns: Vec<String> = Vec::new();
16242 // The leading column may also have ASC/DESC after it — and that
16243 // one is the column SPG indexes, so its clause is kept.
16244 let key_order = self.consume_optional_index_column_qualifiers();
16245 while matches!(self.peek(), Token::Comma) {
16246 self.advance();
16247 let extra = self.expect_ident_like()?;
16248 let _ = self.consume_optional_index_column_qualifiers();
16249 extra_columns.push(extra);
16250 }
16251 if !matches!(self.peek(), Token::RParen) {
16252 return Err(self.err(format!(
16253 "expected ')' after indexed column / expression, got {:?}",
16254 self.peek()
16255 )));
16256 }
16257 self.advance();
16258 // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
16259 // index-only-scan annotation. Bare ident (not a reserved
16260 // keyword) so we test by case-insensitive string match.
16261 let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
16262 {
16263 self.advance();
16264 if !matches!(self.peek(), Token::LParen) {
16265 return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
16266 }
16267 self.advance();
16268 let mut cols = Vec::new();
16269 loop {
16270 cols.push(self.expect_ident_like()?);
16271 match self.peek() {
16272 Token::Comma => {
16273 self.advance();
16274 }
16275 Token::RParen => {
16276 self.advance();
16277 break;
16278 }
16279 other => {
16280 return Err(self.err(format!(
16281 "expected ',' or ')' in INCLUDE list, got {other:?}"
16282 )));
16283 }
16284 }
16285 }
16286 cols
16287 } else {
16288 Vec::new()
16289 };
16290 // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
16291 // storage parameters. pgvector emits `WITH (lists = N)` for
16292 // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
16293 // SPG's HNSW picks its own parameters today (tunable via
16294 // env vars), so the WITH clause is informational and dropped.
16295 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
16296 self.advance();
16297 if !matches!(self.peek(), Token::LParen) {
16298 return Err(self.err(format!(
16299 "expected '(' after WITH in CREATE INDEX, got {:?}",
16300 self.peek()
16301 )));
16302 }
16303 self.advance();
16304 loop {
16305 if matches!(self.peek(), Token::RParen) {
16306 self.advance();
16307 break;
16308 }
16309 // Drain `key = value` or bare `key` tokens.
16310 let _ = self.advance(); // key
16311 if matches!(self.peek(), Token::Eq) {
16312 self.advance();
16313 let _ = self.advance(); // value (int / string / ident)
16314 }
16315 match self.peek() {
16316 Token::Comma => {
16317 self.advance();
16318 }
16319 Token::RParen => {
16320 self.advance();
16321 break;
16322 }
16323 other => {
16324 return Err(self.err(format!(
16325 "expected ',' or ')' in WITH (…) clause, got {other:?}"
16326 )));
16327 }
16328 }
16329 }
16330 }
16331 // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
16332 // which sits between the key list and the WHERE clause.
16333 let mut nulls_not_distinct = false;
16334 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16335 let n1 = self.tokens.get(self.pos + 1);
16336 let n2 = self.tokens.get(self.pos + 2);
16337 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
16338 self.advance(); // NULLS
16339 self.advance(); // NOT
16340 self.advance(); // DISTINCT
16341 nulls_not_distinct = true;
16342 } else if matches!(n1, Some(Token::Distinct)) {
16343 self.advance(); // NULLS
16344 self.advance(); // DISTINCT
16345 }
16346 }
16347 // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
16348 let partial_predicate = if matches!(self.peek(), Token::Where) {
16349 self.advance();
16350 Some(self.parse_expr(0)?)
16351 } else {
16352 None
16353 };
16354 // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16355 // sense: uniqueness over an ANN structure has no clean
16356 // semantics. Reject early. (BRIN UNIQUE is similarly
16357 // meaningless — block both.)
16358 if is_unique && !matches!(method, IndexMethod::BTree) {
16359 return Err(self.err(alloc::format!(
16360 "UNIQUE is only supported on BTree indexes, got USING {:?}",
16361 method
16362 )));
16363 }
16364 Ok(Statement::CreateIndex(CreateIndexStatement {
16365 concurrently,
16366 name,
16367 key_order,
16368 key_collation,
16369 table,
16370 column,
16371 nulls_not_distinct,
16372 method,
16373 if_not_exists,
16374 included_columns,
16375 partial_predicate,
16376 extra_columns: extra_columns.clone(),
16377 expression,
16378 is_unique,
16379 opclass,
16380 method_name,
16381 }))
16382 }
16383
16384 /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16385 /// column-level `REFERENCES ...` clause. The trailing FK is
16386 /// normalised into table-level shape (single-element columns +
16387 /// parent_columns) so the engine sees one uniform constraint list.
16388 fn parse_column_def_with_fk(
16389 &mut self,
16390 ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16391 let col = self.parse_column_def()?;
16392 // v7.39 (round 308, V29) — an explicitly named inline FK:
16393 // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16394 // loop leaves this spelling intact precisely so the name can be
16395 // kept here; PG reports it in violation messages and matches it
16396 // in `SET CONSTRAINTS`.
16397 let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16398 {
16399 self.advance();
16400 Some(self.expect_ident_like()?)
16401 } else {
16402 None
16403 };
16404 // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16405 let inline_references = matches!(
16406 self.peek(),
16407 Token::Ident(s) if s.eq_ignore_ascii_case("references")
16408 );
16409 if !inline_references {
16410 return Ok((col, None));
16411 }
16412 let (
16413 parent_table,
16414 parent_columns,
16415 on_delete,
16416 on_update,
16417 match_type,
16418 deferrable,
16419 initially_deferred,
16420 ) = self.parse_references_tail(1)?;
16421 let fk = ForeignKeyConstraint {
16422 name: declared_name,
16423 columns: vec![col.name.clone()],
16424 parent_table,
16425 parent_columns,
16426 on_delete,
16427 on_update,
16428 match_type,
16429 deferrable,
16430 initially_deferred,
16431 };
16432 Ok((col, Some(fk)))
16433 }
16434
16435 /// v7.13.0 — parse a column type (consuming the type ident and
16436 /// any trailing parameters / `[]`), without surrounding column
16437 /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16438 /// Returns the resolved `ColumnTypeName` plus implied
16439 /// `(auto_increment, not_null)` flags from PG SERIAL family
16440 /// shorthands — callers that don't expect those (ALTER COLUMN
16441 /// TYPE) can discard them.
16442 fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16443 let (ty, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16444 Ok(ty)
16445 }
16446
16447 #[allow(clippy::type_complexity)]
16448 fn parse_type_with_implied_flags(
16449 &mut self,
16450 ) -> Result<
16451 (
16452 ColumnTypeName,
16453 bool,
16454 bool,
16455 Option<String>,
16456 Collation,
16457 // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16458 bool,
16459 // v7.39 (round 676) — the collation NAME as written, which the
16460 // `Collation` enum above cannot carry.
16461 Option<String>,
16462 bool,
16463 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16464 // list captured at type-parse time. None for all
16465 // non-ENUM types.
16466 Option<Vec<String>>,
16467 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16468 // list. Distinct from ENUM (subset semantics).
16469 Option<Vec<String>>,
16470 // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16471 // width, lost when the type collapses to SmallInt / Int.
16472 Option<MysqlIntWidth>,
16473 // v7.39 (round 424) — declared fractional-seconds precision of a
16474 // MySQL temporal column (bare spelling = 0). None under PG.
16475 Option<u8>,
16476 ),
16477 ParseError,
16478 > {
16479 let mut ty_ident = match self.advance() {
16480 Token::Ident(s) => s,
16481 // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16482 // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16483 // '<span>'` literal grammar. As a column type it lands
16484 // here directly; downstream resolution still uses the
16485 // canonical lowercase string.
16486 Token::Interval => "interval".to_string(),
16487 other => {
16488 return Err(ParseError {
16489 message: format!("expected column type, got {other:?}"),
16490 token_pos: self.consumed_pos(),
16491 });
16492 }
16493 };
16494 // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16495 // pg_dump qualifies extension types (`public.vector(1024)`).
16496 // SPG is single-namespace; drop the schema and resolve the
16497 // bare type — same treatment table names already get.
16498 while matches!(self.peek(), Token::Dot) {
16499 self.advance();
16500 ty_ident = self.expect_ident_like()?;
16501 }
16502 let mut implied_auto_increment = false;
16503 let mut implied_not_null = false;
16504 let mut user_type_ref: Option<String> = None;
16505 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16506 // value list, captured here and bubbled up through the
16507 // ColumnDef so the engine can attach it to the column
16508 // schema (and validate INSERT cells against it).
16509 let mut inline_enum_variants: Option<Vec<String>> = None;
16510 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16511 let mut inline_set_variants: Option<Vec<String>> = None;
16512 // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16513 // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16514 // collapses to SmallInt / Int. Only under the MySQL dialect.
16515 let mut mysql_int_width: Option<MysqlIntWidth> = None;
16516 // v7.39 (round 424) — the declared fractional-seconds precision of a
16517 // MySQL temporal column. Set by the temporal arms below; stays None
16518 // for PG (whose temporal columns keep full microseconds).
16519 let mut mysql_fsp: Option<u8> = None;
16520 let mut ty = match ty_ident.as_str() {
16521 // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16522 "smallserial" | "serial2" => {
16523 implied_auto_increment = true;
16524 implied_not_null = true;
16525 ColumnTypeName::SmallInt
16526 }
16527 "serial" | "serial4" => {
16528 implied_auto_increment = true;
16529 implied_not_null = true;
16530 ColumnTypeName::Int
16531 }
16532 "bigserial" | "serial8" => {
16533 implied_auto_increment = true;
16534 implied_not_null = true;
16535 ColumnTypeName::BigInt
16536 }
16537 // MySQL flavours we accept by aliasing to the closest SPG
16538 // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16539 // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16540 // 24-bit) → INT. UNSIGNED modifiers are consumed below
16541 // without semantic effect.
16542 // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16543 // PG's internal type names; pg_dump and hand-written PG schemas
16544 // use them interchangeably with smallint / int / bigint (the cast
16545 // path already accepted them, only the column grammar didn't).
16546 "smallint" | "int2" => {
16547 // v7.14.0 — MySQL display-width on integers
16548 // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16549 // parenthesised number is purely cosmetic — it
16550 // doesn't change storage. Accept + discard.
16551 self.consume_optional_paren_size();
16552 ColumnTypeName::SmallInt
16553 }
16554 // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16555 // canonical encoding for BOOLEAN. Every MySQL driver
16556 // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16557 // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16558 // 4.3 SPG classified TINYINT(1) as SmallInt, which
16559 // gave the customer i16-shaped values where the app
16560 // expected bool — a Tier-A silent type drift on
16561 // mysqldump restores. Now: `TINYINT(1)` → Bool;
16562 // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16563 // stay SmallInt (the legacy width-agnostic path).
16564 "tinyint" => {
16565 let width = self.peek_optional_paren_size_value();
16566 self.consume_optional_paren_size();
16567 if width == Some(1) {
16568 ColumnTypeName::Bool
16569 } else {
16570 // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16571 // lost width so the write path can enforce -128..127.
16572 if self.mysql_dialect {
16573 mysql_int_width = Some(MysqlIntWidth::Tiny);
16574 }
16575 ColumnTypeName::SmallInt
16576 }
16577 }
16578 "mediumint" => {
16579 self.consume_optional_paren_size();
16580 // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16581 if self.mysql_dialect {
16582 mysql_int_width = Some(MysqlIntWidth::Medium);
16583 }
16584 ColumnTypeName::Int
16585 }
16586 "int" | "integer" | "int4" => {
16587 self.consume_optional_paren_size();
16588 ColumnTypeName::Int
16589 }
16590 "bigint" | "int8" => {
16591 self.consume_optional_paren_size();
16592 ColumnTypeName::BigInt
16593 }
16594 // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16595 // (mailrs round-5 G6). Consume the optional `PRECISION`
16596 // tail when the type keyword was `double` / `DOUBLE`.
16597 //
16598 // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16599 // FLOAT". `FLOAT(p)` picks the width the way PG does:
16600 // p in 1..=24 is real, 25..=53 is double precision, and
16601 // anything else is an error.
16602 "float" | "double" | "real" => {
16603 if ty_ident.eq_ignore_ascii_case("double")
16604 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16605 {
16606 self.advance();
16607 }
16608 if ty_ident.eq_ignore_ascii_case("real") {
16609 // v7.39 (round 274) — the two dialects genuinely
16610 // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16611 // synonym for DOUBLE (8-byte). Round 269 made REAL
16612 // 32-bit globally and thereby narrowed the stored
16613 // precision of every MySQL REAL column.
16614 if self.mysql_dialect {
16615 ColumnTypeName::Float
16616 } else {
16617 ColumnTypeName::Real
16618 }
16619 } else if ty_ident.eq_ignore_ascii_case("float")
16620 && self.mysql_dialect
16621 && matches!(self.peek(), Token::LParen)
16622 && self.peek_paren_has_comma()
16623 {
16624 // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16625 // display form (`FLOAT(10,2)`), which PG has no
16626 // equivalent of. It was `syntax error at or near ","`,
16627 // so the whole CREATE failed. The digits are a display
16628 // hint only; SPG stores the full double.
16629 self.consume_optional_paren_size();
16630 ColumnTypeName::Float
16631 } else if ty_ident.eq_ignore_ascii_case("float")
16632 && matches!(self.peek(), Token::LParen)
16633 {
16634 // PG words the two bounds differently, and
16635 // parse_paren_size already rejects a zero.
16636 let p = self.parse_paren_size("FLOAT")?;
16637 if p > 53 {
16638 return Err(self.err(String::from(
16639 "precision for type float must be less than 54 bits",
16640 )));
16641 }
16642 if p <= 24 {
16643 ColumnTypeName::Real
16644 } else {
16645 ColumnTypeName::Float
16646 }
16647 } else {
16648 ColumnTypeName::Float
16649 }
16650 }
16651 // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16652 "float4" => ColumnTypeName::Real,
16653 "float8" => ColumnTypeName::Float,
16654 "text" => ColumnTypeName::Text,
16655 // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16656 // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16657 // real MySQL schema and NONE of them existed: the CREATE
16658 // failed outright with `type "blob" does not exist`, so the
16659 // table was never made. The sizes differ only in MySQL's
16660 // maximum length, which SPG does not cap, so they collapse
16661 // onto TEXT and BYTEA the way the unsized spellings do.
16662 "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16663 "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16664 // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16665 // enforce, consumed so the declaration parses.
16666 "varbinary" | "binary" => {
16667 self.consume_optional_paren_size();
16668 ColumnTypeName::Bytes
16669 }
16670 "name" => ColumnTypeName::Name,
16671 "xid" => ColumnTypeName::Xid,
16672 "oid" => ColumnTypeName::Oid,
16673 "xid8" => ColumnTypeName::Xid8,
16674 "bool" | "boolean" => ColumnTypeName::Bool,
16675 // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16676 // an unbounded `character varying`, which the arm below has always
16677 // read as text. Only the short spelling demanded a length, so
16678 // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16679 // there is — failed on `VARCHAR type requires (N)` while the long
16680 // spelling of the same thing was accepted. The same asymmetry
16681 // round 613 closed on the CAST side, here on the DDL side.
16682 "varchar" => {
16683 if matches!(self.peek(), Token::LParen) {
16684 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16685 } else {
16686 ColumnTypeName::Text
16687 }
16688 }
16689 // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16690 // `character` below (SQL standard).
16691 "char" => {
16692 if matches!(self.peek(), Token::LParen) {
16693 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16694 } else {
16695 ColumnTypeName::Char(1)
16696 }
16697 }
16698 // pg_dump's canonical spellings: `character varying(n)` = varchar,
16699 // `character(n)` = char, bare `character` = char(1). Unbounded
16700 // `character varying` maps to text.
16701 "character" => {
16702 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16703 self.advance();
16704 if matches!(self.peek(), Token::LParen) {
16705 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16706 } else {
16707 ColumnTypeName::Text
16708 }
16709 } else if matches!(self.peek(), Token::LParen) {
16710 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16711 } else {
16712 ColumnTypeName::Char(1)
16713 }
16714 }
16715 "vector" => {
16716 let dim = self.parse_paren_size("VECTOR")?;
16717 let encoding = self.parse_optional_vector_encoding()?;
16718 ColumnTypeName::Vector { dim, encoding }
16719 }
16720 // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16721 // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16722 // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16723 // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16724 // DECIMAL(10,2))` — how nearly every money column is written,
16725 // in either dialect — was a syntax error and the table was
16726 // never created. `FIXED` is MySQL's alias alone, so it is
16727 // taken only in that dialect.
16728 "numeric" | "decimal" | "dec" => {
16729 let (precision, scale) = self.parse_optional_numeric_params()?;
16730 ColumnTypeName::Numeric(precision, scale)
16731 }
16732 "fixed" if self.mysql_dialect => {
16733 let (precision, scale) = self.parse_optional_numeric_params()?;
16734 ColumnTypeName::Numeric(precision, scale)
16735 }
16736 "date" => ColumnTypeName::Date,
16737 // MySQL's `DATETIME` is the same domain as standard
16738 // `TIMESTAMP` — accept both spellings.
16739 "timestamp" | "datetime" => {
16740 // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16741 // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16742 // TIME ZONE` clause, so consume it first.
16743 // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16744 // (it truncates on write and pads on render), so capture it;
16745 // a bare spelling means precision 0 there. PG stores µs always
16746 // and keeps `None`.
16747 let n = self.take_optional_paren_size();
16748 if self.mysql_dialect {
16749 mysql_fsp = Some(n.unwrap_or(0).min(6));
16750 }
16751 // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16752 // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16753 // the full form. SPG canonicalises:
16754 // - WITH TIME ZONE → Timestamptz
16755 // - WITHOUT TIME ZONE → Timestamp
16756 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16757 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16758 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16759 {
16760 self.advance(); // WITH
16761 self.advance(); // TIME
16762 self.advance(); // ZONE
16763 ColumnTypeName::Timestamptz
16764 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16765 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16766 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16767 {
16768 self.advance(); // WITHOUT
16769 self.advance(); // TIME
16770 self.advance(); // ZONE
16771 ColumnTypeName::Timestamp
16772 } else {
16773 // A second `(precision)` cannot legally follow, but the
16774 // old grammar tolerated it; keep that tolerance.
16775 self.consume_optional_paren_size();
16776 ColumnTypeName::Timestamp
16777 }
16778 }
16779 // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16780 // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16781 // only PG-wire OID differs.
16782 "timestamptz" => {
16783 self.consume_optional_paren_size();
16784 ColumnTypeName::Timestamptz
16785 }
16786 // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16787 // validation. We accept the JSONB spelling too because
16788 // most PG clients default to it; SPG doesn't distinguish
16789 // the two (no path-operator perf advantage to model).
16790 "json" => ColumnTypeName::Json,
16791 "jsonb" => ColumnTypeName::Jsonb,
16792 // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16793 // surface here. Same storage shape; mapping happens at
16794 // the engine side via the ColumnTypeName → DataType
16795 // resolver. Literal forms are handled at coerce_value
16796 // time so the lexer stays untouched.
16797 "bytea" | "bytes" => ColumnTypeName::Bytes,
16798 // v7.17.0 Phase 7 — PG network address types
16799 // v7.17.0 had a Text-backed fallback here for
16800 // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16801 // each to a first-class type; the keywords are
16802 // bound below in the ζ-A block.
16803 // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16804 // The actual `to_tsvector` / `@@` / `ts_rank` surface
16805 // arrives in v7.12.1+; the type itself loads here so
16806 // mailrs's `scripts/init-schema.sql` runs unmodified.
16807 "tsvector" => ColumnTypeName::TsVector,
16808 "tsquery" => ColumnTypeName::TsQuery,
16809 // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
16810 // surface for Django / Rails / Hibernate's default
16811 // PK pattern.
16812 "uuid" => ColumnTypeName::Uuid,
16813 // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
16814 // Storage = three-field {months, days, micros}, catalog
16815 // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
16816 // line `INTERVAL` was parser-rejected at CREATE TABLE.
16817 "interval" => {
16818 // pg_dump emits field-qualified forms like `INTERVAL DAY TO
16819 // SECOND` and an optional `(p)` precision. SPG stores the full
16820 // {months,days,micros}; consume + ignore the qualifier/precision.
16821 while matches!(self.peek(), Token::To)
16822 || matches!(self.peek(), Token::Ident(s) if matches!(
16823 s.to_ascii_lowercase().as_str(),
16824 "year" | "month" | "day" | "hour" | "minute" | "second"
16825 ))
16826 {
16827 self.advance();
16828 }
16829 self.consume_optional_paren_size();
16830 ColumnTypeName::Interval
16831 }
16832 // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
16833 // i64 microseconds since 00:00:00. Wire OID 1083.
16834 // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
16835 "time" => {
16836 // v7.39 (round 424) — MySQL TIME carries a semantic
16837 // fractional-seconds precision, bare meaning 0.
16838 let n = self.take_optional_paren_size();
16839 if self.mysql_dialect {
16840 mysql_fsp = Some(n.unwrap_or(0).min(6));
16841 }
16842 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16843 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16844 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16845 {
16846 self.advance();
16847 self.advance();
16848 self.advance();
16849 ColumnTypeName::TimeTz
16850 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16851 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16852 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16853 {
16854 self.advance();
16855 self.advance();
16856 self.advance();
16857 ColumnTypeName::Time
16858 } else {
16859 ColumnTypeName::Time
16860 }
16861 }
16862 // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
16863 // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
16864 "year" => ColumnTypeName::Year,
16865 // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
16866 // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
16867 "timetz" => ColumnTypeName::TimeTz,
16868 // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
16869 // Wire OID 790.
16870 "money" => ColumnTypeName::Money,
16871 // v7.17.0 Phase 3.P0-38 — PG range types.
16872 "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
16873 "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
16874 "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
16875 "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
16876 "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
16877 "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
16878 // v7.37.5 δ — PG 14+ multirange keywords.
16879 "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
16880 "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
16881 "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
16882 "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
16883 "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
16884 "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
16885 // v7.37.5 ε — PG geometry scalar keywords.
16886 "point" => ColumnTypeName::Point,
16887 "lseg" => ColumnTypeName::Lseg,
16888 "path" => ColumnTypeName::Path,
16889 "box" => ColumnTypeName::PgBox,
16890 "polygon" => ColumnTypeName::Polygon,
16891 "line" => ColumnTypeName::Line,
16892 "circle" => ColumnTypeName::Circle,
16893 // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
16894 "inet" => ColumnTypeName::Inet,
16895 "cidr" => ColumnTypeName::Cidr,
16896 "macaddr" => ColumnTypeName::Macaddr,
16897 "macaddr8" => ColumnTypeName::Macaddr8,
16898 // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
16899 // width in the value, so the optional `(N)` typmod is accepted and
16900 // ignored (the column stores whatever width it's given).
16901 "bit" => {
16902 let varying = matches!(
16903 self.peek(),
16904 Token::Ident(k) if k.eq_ignore_ascii_case("varying")
16905 );
16906 if varying {
16907 self.advance();
16908 }
16909 // v7.39 (round 281) — the length used to be parsed and
16910 // dropped, so `bit(3)` accepted a five-bit string.
16911 let n = if matches!(self.peek(), Token::LParen) {
16912 self.parse_paren_size("BIT")?
16913 } else {
16914 0
16915 };
16916 if varying {
16917 ColumnTypeName::BitVarying(n)
16918 } else {
16919 ColumnTypeName::Bit(n)
16920 }
16921 }
16922 "varbit" => {
16923 let n = if matches!(self.peek(), Token::LParen) {
16924 self.parse_paren_size("VARBIT")?
16925 } else {
16926 0
16927 };
16928 ColumnTypeName::BitVarying(n)
16929 }
16930 "xml" => ColumnTypeName::Xml,
16931 // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
16932 "hstore" => ColumnTypeName::Hstore,
16933 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
16934 // `ENUM('a','b','c')`. Storage is TEXT; the value
16935 // list lands on `inline_enum_variants` for the
16936 // engine to validate INSERT cells against. Empty
16937 // value list is a parse error (matches MySQL).
16938 "enum" => {
16939 // Expect the opening `(`.
16940 if !matches!(self.peek(), Token::LParen) {
16941 return Err(self.err(alloc::format!(
16942 "expected '(' after ENUM, got {:?}",
16943 self.peek()
16944 )));
16945 }
16946 self.advance();
16947 let mut variants: Vec<String> = Vec::new();
16948 loop {
16949 match self.advance() {
16950 Token::String(s) => variants.push(s),
16951 other => {
16952 return Err(self.err(alloc::format!(
16953 "ENUM(...) expects string literal variants, got {other:?}"
16954 )));
16955 }
16956 }
16957 match self.peek() {
16958 Token::Comma => {
16959 self.advance();
16960 continue;
16961 }
16962 Token::RParen => {
16963 self.advance();
16964 break;
16965 }
16966 other => {
16967 return Err(self.err(alloc::format!(
16968 "expected ',' or ')' in ENUM(...), got {other:?}"
16969 )));
16970 }
16971 }
16972 }
16973 if variants.is_empty() {
16974 return Err(self.err("ENUM(...) must declare at least one variant".into()));
16975 }
16976 inline_enum_variants = Some(variants);
16977 // Storage is plain TEXT; the variant list lives on
16978 // the ColumnSchema side.
16979 ColumnTypeName::Text
16980 }
16981 // v7.17.0 Phase 3.P0-37 — MySQL inline SET
16982 // `SET('a','b','c')`. Same parse shape as ENUM;
16983 // semantics differ (subset rather than pick-one).
16984 "set" => {
16985 if !matches!(self.peek(), Token::LParen) {
16986 return Err(self.err(alloc::format!(
16987 "expected '(' after SET, got {:?}",
16988 self.peek()
16989 )));
16990 }
16991 self.advance();
16992 let mut variants: Vec<String> = Vec::new();
16993 loop {
16994 match self.advance() {
16995 Token::String(s) => variants.push(s),
16996 other => {
16997 return Err(self.err(alloc::format!(
16998 "SET(...) expects string literal variants, got {other:?}"
16999 )));
17000 }
17001 }
17002 match self.peek() {
17003 Token::Comma => {
17004 self.advance();
17005 continue;
17006 }
17007 Token::RParen => {
17008 self.advance();
17009 break;
17010 }
17011 other => {
17012 return Err(self.err(alloc::format!(
17013 "expected ',' or ')' in SET(...), got {other:?}"
17014 )));
17015 }
17016 }
17017 }
17018 if variants.is_empty() {
17019 return Err(self.err("SET(...) must declare at least one variant".into()));
17020 }
17021 inline_set_variants = Some(variants);
17022 ColumnTypeName::Text
17023 }
17024 _other => {
17025 // v7.17.0 Phase 1.4 — unknown ident → defer
17026 // resolution to the engine. Stored as Text in
17027 // ColumnTypeName + the original name carried as
17028 // `user_type_ref` so CREATE TABLE can look up
17029 // user-defined enum / domain types.
17030 user_type_ref = Some(ty_ident.clone());
17031 ColumnTypeName::Text
17032 }
17033 };
17034 // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
17035 // right after the type keyword. Pre-4.4 SPG consumed +
17036 // discarded the keyword, leaving a customer column
17037 // declared `id INT UNSIGNED NOT NULL` silently accepting
17038 // negative values — a Tier-A correctness drift where
17039 // application invariants (auto-increment-IDs never
17040 // negative) silently broke on cutover. Now: capture as
17041 // a column flag, persist on the schema, enforce at
17042 // INSERT / UPDATE time.
17043 let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
17044 {
17045 self.advance();
17046 true
17047 } else {
17048 false
17049 };
17050 // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
17051 // `<type> COLLATE <name>` post-fixes on text columns. SPG
17052 // stores text as UTF-8 always so CHARACTER SET is still a
17053 // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
17054 // name: it gets classified into a `Collation` variant the
17055 // engine consults at WHERE-eval time. PG `default` /
17056 // `pg_catalog.default` / `C` / `POSIX` collations all
17057 // resolve to `Binary` (the prior behaviour); `_ci` /
17058 // `case_insensitive` / `nocase` shift to CaseInsensitive.
17059 // The schema-qualifier form (`pg_catalog.default`) lexes
17060 // as `Ident '.' Ident` — peek for the `.` and consume both
17061 // halves so it's treated as one collation name. PG's
17062 // `IDENT.IDENT` collation form (which can appear here) is
17063 // resolved by Collation::from_collation_name on the bare
17064 // identifier after the dot.
17065 let mut collation = Collation::Binary;
17066 // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
17067 // clause was written. The engine needs this to tell an explicit
17068 // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
17069 // clause at all: both resolve to `Collation::Binary`, but under the
17070 // MySQL dialect the latter takes the folding default collation.
17071 let mut collation_explicit = false;
17072 let mut collation_name: Option<alloc::string::String> = None;
17073 loop {
17074 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
17075 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
17076 {
17077 self.advance(); // CHARACTER
17078 self.advance(); // SET
17079 if matches!(
17080 self.peek(),
17081 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
17082 ) {
17083 self.advance();
17084 }
17085 continue;
17086 }
17087 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
17088 self.advance(); // COLLATE
17089 // Accept Ident / QuotedIdent / String AND the
17090 // keyword-tokenised `Default` (PG `pg_catalog.default`
17091 // and bare `DEFAULT` collation names — `default` is a
17092 // reserved word so the lexer hands back Token::Default
17093 // not Token::Ident).
17094 let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
17095 match this.peek().clone() {
17096 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
17097 this.advance();
17098 Some(s)
17099 }
17100 Token::Default => {
17101 this.advance();
17102 Some(alloc::string::String::from("default"))
17103 }
17104 _ => None,
17105 }
17106 };
17107 let raw = if let Some(head) = read_collation_atom(self) {
17108 // Schema-qualified PG form: `pg_catalog.default`.
17109 if matches!(self.peek(), Token::Dot) {
17110 self.advance();
17111 let tail = read_collation_atom(self).unwrap_or_default();
17112 alloc::format!("{head}.{tail}")
17113 } else {
17114 head
17115 }
17116 } else {
17117 alloc::string::String::new()
17118 };
17119 if !raw.is_empty() {
17120 collation_explicit = true;
17121 // v7.39 (round 676) — keep the name too. The enum below
17122 // folds C / POSIX / en_US / default into one value, and
17123 // `pg_attribute.attcollation` has to tell them apart.
17124 // The schema qualifier goes: PG's `pg_catalog.default`
17125 // and a bare `default` name the same collation.
17126 // v7.39 (round 679) — strip a SCHEMA qualifier, not an
17127 // encoding suffix. Round 676 used `rsplit('.')` for
17128 // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
17129 // PG writes `pg_catalog.default` (qualifier) and
17130 // `en_US.utf8` (locale + encoding) with the same
17131 // separator. Only `pg_catalog.` is a qualifier, and it
17132 // is the only one PG's own dumps emit.
17133 let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
17134 let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
17135 collation_name = Some(alloc::string::String::from(bare));
17136 let parsed = Collation::from_collation_name(&raw);
17137 // Last COLLATE clause wins, but `Binary` from a
17138 // bare keyword like `default` should not
17139 // silently downgrade a stronger one set earlier
17140 // on the same column. v7.17 only ships one
17141 // non-Binary variant so a simple OR is enough.
17142 if parsed != Collation::Binary {
17143 collation = parsed;
17144 }
17145 }
17146 continue;
17147 }
17148 break;
17149 }
17150 // v7.10.10 — postfix `[]` widens the base type to its array
17151 // type. PG accepts `TYPE[]` after any base type and so does
17152 // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
17153 // all through; the old "only TEXT[]" note was stale).
17154 if matches!(self.peek(), Token::LBracket) {
17155 self.advance();
17156 if !matches!(self.peek(), Token::RBracket) {
17157 return Err(self.err(alloc::format!(
17158 "TEXT[] takes no dimension; got {:?}",
17159 self.peek()
17160 )));
17161 }
17162 self.advance();
17163 // v7.11.13 — widened to INT[] and BIGINT[] in addition
17164 // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
17165 // still error here.
17166 ty = match ty {
17167 ColumnTypeName::Text => ColumnTypeName::TextArray,
17168 ColumnTypeName::Int => ColumnTypeName::IntArray,
17169 ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
17170 // v7.37.5 β-P4 — INTERVAL[] via the same postfix
17171 // `[]` grammar. Wire OID 1187.
17172 ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
17173 // v7.37.5 γ — full PG array-of-scalar family.
17174 ColumnTypeName::Bool => ColumnTypeName::BoolArray,
17175 ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
17176 ColumnTypeName::Float => ColumnTypeName::FloatArray,
17177 // NUMERIC(p, s) loses its precision params at the
17178 // array level (matches PG: `NUMERIC[]` is untyped,
17179 // per-element precision flows through values).
17180 ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
17181 ColumnTypeName::Date => ColumnTypeName::DateArray,
17182 ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
17183 ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
17184 ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
17185 ColumnTypeName::Json => ColumnTypeName::JsonArray,
17186 ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
17187 ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
17188 // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
17189 // the array level (matches PG semantics where the
17190 // element precision is per-row, not column-wide).
17191 ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
17192 ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
17193 // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
17194 // follow-up.
17195 ColumnTypeName::Money => ColumnTypeName::MoneyArray,
17196 other => {
17197 return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
17198 }
17199 };
17200 // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
17201 // for INT/TEXT/BIGINT. Anything else is an error.
17202 if matches!(self.peek(), Token::LBracket) {
17203 self.advance();
17204 if !matches!(self.peek(), Token::RBracket) {
17205 return Err(self.err(alloc::format!(
17206 "TYPE[][] second dimension takes no size; got {:?}",
17207 self.peek()
17208 )));
17209 }
17210 self.advance();
17211 ty = match ty {
17212 ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
17213 ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
17214 ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
17215 // v7.39 (read01 round 75) — bool[][].
17216 ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
17217 other => {
17218 return Err(self.err(alloc::format!(
17219 "v7.17 2D arrays support INT[][] / BIGINT[][] / \
17220 TEXT[][] only; got {other:?}"
17221 )));
17222 }
17223 };
17224 }
17225 }
17226 Ok((
17227 ty,
17228 implied_auto_increment,
17229 implied_not_null,
17230 user_type_ref,
17231 collation,
17232 collation_explicit,
17233 collation_name,
17234 is_unsigned,
17235 inline_enum_variants,
17236 inline_set_variants,
17237 mysql_int_width,
17238 mysql_fsp,
17239 ))
17240 }
17241
17242 fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
17243 // v7.20 — PG reserves the table-constraint keywords, so a
17244 // BARE `UNIQUE` / `PRIMARY` / … in column position is a
17245 // malformed constraint clause (e.g. `UNIQUE a` missing its
17246 // parens), not a column named "unique". Since v7.17's
17247 // unknown-type leniency (`user_type_ref`) such a clause
17248 // would otherwise parse as a column with a user-defined
17249 // type — silently accepting invalid DDL. Quoted
17250 // identifiers ("unique" / `unique`) remain valid names.
17251 if let Token::Ident(s) = self.peek()
17252 && [
17253 "unique",
17254 "primary",
17255 "foreign",
17256 "constraint",
17257 "check",
17258 "references",
17259 "exclude",
17260 ]
17261 .iter()
17262 .any(|kw| s.eq_ignore_ascii_case(kw))
17263 {
17264 return Err(self.err(alloc::format!(
17265 "unexpected reserved keyword '{s}' at start of column definition \
17266 (malformed table constraint?)"
17267 )));
17268 }
17269 let name = self.expect_ident_like()?;
17270 let (
17271 ty,
17272 implied_auto_increment,
17273 implied_not_null,
17274 user_type_ref,
17275 collation,
17276 collation_explicit,
17277 collation_name,
17278 is_unsigned,
17279 inline_enum_variants,
17280 inline_set_variants,
17281 mysql_int_width,
17282 mysql_fsp,
17283 ) = self.parse_type_with_implied_flags()?;
17284 // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
17285 // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
17286 // each at most once.
17287 let mut default: Option<Expr> = None;
17288 let mut nullable = !implied_not_null;
17289 let mut nullability_seen = implied_not_null;
17290 let mut auto_increment = implied_auto_increment;
17291 let mut is_primary_key = false;
17292 let mut is_unique = false;
17293 let mut unique_nulls_not_distinct = false;
17294 let mut constraint_deferrable = false;
17295 let mut constraint_initially_deferred = false;
17296 let mut check: Option<Expr> = None;
17297 let mut on_update_runtime: Option<Expr> = None;
17298 let mut generated_stored_expr: Option<Box<Expr>> = None;
17299 let mut identity_always = false;
17300 loop {
17301 // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
17302 // not-null constraints by name and pg_dump emits them
17303 // inline: `id bigint CONSTRAINT contacts_id_not_null1
17304 // NOT NULL`. Accept and discard the name; whatever
17305 // constraint follows is parsed by the arms below.
17306 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
17307 // v7.39 (round 308, V29) — a name on an inline
17308 // REFERENCES belongs to the FOREIGN KEY, and the caller
17309 // (`parse_column_def_with_fk`) is what builds it, so
17310 // leave the whole clause for it. Dropping the name here
17311 // is what made `CONSTRAINT fk_a REFERENCES …` come back
17312 // as the synthesised `c_pid_fkey` — which then could
17313 // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
17314 // `advance()` takes tokens by `mem::replace`, so there
17315 // is no rewinding once consumed.
17316 if matches!(
17317 self.tokens.get(self.pos + 2),
17318 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
17319 ) {
17320 break;
17321 }
17322 self.advance();
17323 let _name = self.expect_ident_like()?;
17324 continue;
17325 }
17326 // v7.39 (round 379) — MySQL's SHORT generated-column form
17327 // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
17328 // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
17329 // below), but hand-written schemas and app migrations use this.
17330 // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
17331 // SPG computes-and-stores either way, like the long form.
17332 if matches!(self.peek(), Token::As) {
17333 self.advance();
17334 if !matches!(self.peek(), Token::LParen) {
17335 return Err(self.err(alloc::format!(
17336 "expected '(' after AS in a generated column, got {:?}",
17337 self.peek()
17338 )));
17339 }
17340 self.advance();
17341 let expr = self.parse_expr(0)?;
17342 if !matches!(self.peek(), Token::RParen) {
17343 return Err(self.err(alloc::format!(
17344 "expected ')' after AS (<expr>), got {:?}",
17345 self.peek()
17346 )));
17347 }
17348 self.advance();
17349 if matches!(self.peek(), Token::Ident(s)
17350 if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
17351 {
17352 self.advance();
17353 }
17354 generated_stored_expr = Some(alloc::boxed::Box::new(expr));
17355 continue;
17356 }
17357 // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
17358 // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
17359 // the modern replacement for SERIAL in hand-written
17360 // schemas). Both flavours map onto the auto-increment
17361 // machinery — SPG's serial semantics ≈ BY DEFAULT;
17362 // ALWAYS's reject-explicit-values nuance is documented
17363 // leniency. Generated EXPRESSION columns
17364 // (`AS (expr) STORED`) are not supported: error loudly
17365 // instead of silently storing NULLs.
17366 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
17367 self.advance();
17368 let mut saw_generated_always = false;
17369 match self.peek().clone() {
17370 Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
17371 self.advance();
17372 saw_generated_always = true;
17373 }
17374 Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
17375 self.advance();
17376 if !matches!(self.peek(), Token::Default) {
17377 return Err(self.err(alloc::format!(
17378 "expected DEFAULT after GENERATED BY, got {:?}",
17379 self.peek()
17380 )));
17381 }
17382 self.advance();
17383 }
17384 other => {
17385 return Err(self.err(alloc::format!(
17386 "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
17387 )));
17388 }
17389 }
17390 if !matches!(self.peek(), Token::As) {
17391 return Err(self.err(alloc::format!(
17392 "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
17393 self.peek()
17394 )));
17395 }
17396 self.advance();
17397 // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
17398 // ( <expr> ) STORED` stored computed-column. The
17399 // expression is captured for the engine to recompute
17400 // on every INSERT / UPDATE. v7.37.7 accepts the
17401 // STORED keyword only; PG also has VIRTUAL, which
17402 // v7.37.7 carves out (sentori only uses STORED).
17403 if matches!(self.peek(), Token::LParen) {
17404 self.advance();
17405 let expr = self.parse_expr(0)?;
17406 if !matches!(self.peek(), Token::RParen) {
17407 return Err(self.err(alloc::format!(
17408 "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
17409 self.peek()
17410 )));
17411 }
17412 self.advance();
17413 let stored = match self.peek() {
17414 Token::Ident(s) | Token::QuotedIdent(s)
17415 if s.eq_ignore_ascii_case("stored") =>
17416 {
17417 self.advance();
17418 true
17419 }
17420 // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
17421 // generated columns. SPG computes them on write and
17422 // persists like STORED; the two are observably
17423 // identical for query results (the value, recompute
17424 // on base-column change, and NOT NULL enforcement all
17425 // match), so a PG 18 schema/dump using VIRTUAL loads
17426 // and behaves correctly. The compute-on-read storage
17427 // saving is an invisible internal difference.
17428 Token::Ident(s) | Token::QuotedIdent(s)
17429 if s.eq_ignore_ascii_case("virtual") =>
17430 {
17431 self.advance();
17432 false
17433 }
17434 other => {
17435 return Err(self.err(alloc::format!(
17436 "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
17437 got {other:?}"
17438 )));
17439 }
17440 };
17441 let _ = stored; // STORED / VIRTUAL both compute-and-store.
17442 generated_stored_expr = Some(Box::new(expr));
17443 continue;
17444 }
17445 self.expect_keyword_ident("identity")?;
17446 // Optional `(START WITH 1 INCREMENT BY 1 …)` —
17447 // consume the balanced parens and discard (SPG's
17448 // auto-increment is max+1-scan based).
17449 if matches!(self.peek(), Token::LParen) {
17450 let mut depth = 0usize;
17451 loop {
17452 match self.advance() {
17453 Token::LParen => depth += 1,
17454 Token::RParen => {
17455 depth -= 1;
17456 if depth == 0 {
17457 break;
17458 }
17459 }
17460 Token::Eof => {
17461 return Err(self.err(
17462 "unterminated sequence-options parens after IDENTITY".into(),
17463 ));
17464 }
17465 _ => {}
17466 }
17467 }
17468 }
17469 auto_increment = true;
17470 // v7.38 (read01) — remember the ALWAYS flavour so the engine
17471 // can reject explicit non-DEFAULT INSERT values (unless
17472 // OVERRIDING SYSTEM VALUE) the way PG does.
17473 identity_always = saw_generated_always;
17474 // PG identity columns are implicitly NOT NULL.
17475 nullable = false;
17476 continue;
17477 }
17478 // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
17479 // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
17480 // is accepted today. The "ON" token is an Ident
17481 // (not reserved) — peek before consuming.
17482 if matches!(self.peek(), Token::On)
17483 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17484 {
17485 self.advance(); // ON
17486 self.advance(); // update
17487 // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17488 let next = self.peek().clone();
17489 match next {
17490 Token::Ident(s) | Token::QuotedIdent(s)
17491 if s.eq_ignore_ascii_case("current_timestamp") =>
17492 {
17493 self.advance();
17494 // Optional `(N)` precision.
17495 if matches!(self.peek(), Token::LParen) {
17496 self.advance();
17497 if !matches!(self.peek(), Token::Integer(_)) {
17498 return Err(self.err(alloc::format!(
17499 "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17500 self.peek()
17501 )));
17502 }
17503 self.advance();
17504 if !matches!(self.peek(), Token::RParen) {
17505 return Err(self.err(alloc::format!(
17506 "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17507 self.peek()
17508 )));
17509 }
17510 self.advance();
17511 }
17512 on_update_runtime = Some(Expr::FunctionCall {
17513 name: "now".into(),
17514 args: Vec::new(),
17515 });
17516 continue;
17517 }
17518 other => {
17519 return Err(self.err(alloc::format!(
17520 "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17521 )));
17522 }
17523 }
17524 }
17525 if matches!(self.peek(), Token::Default) {
17526 if default.is_some() {
17527 return Err(self.err("DEFAULT specified twice".into()));
17528 }
17529 self.advance();
17530 default = Some(self.parse_expr(0)?);
17531 continue;
17532 }
17533 // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17534 // token with NOT NULL and sits EARLIER in the loop than the
17535 // deferrability arm, so without the lookahead it was reported as
17536 // "NOT NULL specified twice" (or "expected NULL after NOT").
17537 if matches!(self.peek(), Token::Not)
17538 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17539 {
17540 // NOT DEFERRABLE — explicit immediate; nothing to carry.
17541 self.consume_optional_deferrable_clauses()?;
17542 continue;
17543 }
17544 if matches!(self.peek(), Token::Not) {
17545 if nullability_seen {
17546 return Err(self.err("NOT NULL specified twice".into()));
17547 }
17548 self.advance();
17549 if !matches!(self.peek(), Token::Null) {
17550 return Err(self.err(format!(
17551 "expected NULL after NOT in column def, got {:?}",
17552 self.peek()
17553 )));
17554 }
17555 self.advance();
17556 nullable = false;
17557 nullability_seen = true;
17558 continue;
17559 }
17560 // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17561 // "this column is nullable" marker (the default in
17562 // standard SQL anyway). mysqldump emits it routinely
17563 // (`col TYPE NULL DEFAULT NULL` for nullable
17564 // timestamps etc). Accept + no-op.
17565 if matches!(self.peek(), Token::Null) {
17566 if nullability_seen && !nullable {
17567 // v7.39 (round 761, F31 tranche 2 #31) — PG's
17568 // sentence, PG18-measured (the table name is the
17569 // caller's; the column half is exact).
17570 return Err(self.err(alloc::format!(
17571 "conflicting NULL/NOT NULL declarations for column \"{name}\""
17572 )));
17573 }
17574 self.advance();
17575 nullable = true;
17576 nullability_seen = true;
17577 continue;
17578 }
17579 // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17580 // arrives as a bare Ident. Match either, case-insensitive.
17581 if let Token::Ident(s) = self.peek()
17582 && (s.eq_ignore_ascii_case("auto_increment")
17583 || s.eq_ignore_ascii_case("autoincrement"))
17584 {
17585 if auto_increment {
17586 return Err(self.err("AUTO_INCREMENT specified twice".into()));
17587 }
17588 self.advance();
17589 auto_increment = true;
17590 continue;
17591 }
17592 // v7.9.13 — inline `PRIMARY KEY` column constraint
17593 // (mailrs F1). Implies `NOT NULL`. The engine creates
17594 // a BTree index for the PK column at CREATE TABLE time
17595 // so FK parent-side index lookups resolve.
17596 // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17597 // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17598 // spelling was a parse error, so a pg_dump carrying one stopped
17599 // mid-restore. The clauses are consumed by the same helper the FK
17600 // path has used since round 288 and recorded nowhere: SPG enforces
17601 // the constraint IMMEDIATELY either way, which fails earlier than
17602 // PG inside a transaction that violates-then-repairs — a refusal,
17603 // not a wrong answer. True deferral is the open remainder of F08.
17604 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17605 || (matches!(self.peek(), Token::Not)
17606 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
17607 {
17608 // v7.39 (round 711) — CARRIED now (the storing half of
17609 // F08); round 621 only consumed.
17610 let (d, idef) = self.consume_deferrable_clauses_timed()?;
17611 constraint_deferrable |= d;
17612 constraint_initially_deferred |= idef;
17613 continue;
17614 }
17615 if let Token::Ident(s) = self.peek()
17616 && s.eq_ignore_ascii_case("primary")
17617 {
17618 if is_primary_key {
17619 return Err(self.err("PRIMARY KEY specified twice".into()));
17620 }
17621 // Peek-ahead for the required `KEY` token.
17622 let next = self.tokens.get(self.pos + 1);
17623 let next_is_key = matches!(
17624 next,
17625 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17626 );
17627 if !next_is_key {
17628 return Err(self.err(format!(
17629 "expected KEY after PRIMARY in column def, got {:?}",
17630 next
17631 )));
17632 }
17633 self.advance(); // PRIMARY
17634 self.advance(); // KEY
17635 is_primary_key = true;
17636 if nullability_seen && nullable {
17637 return Err(self.err(
17638 "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17639 ));
17640 }
17641 nullable = false;
17642 nullability_seen = true;
17643 continue;
17644 }
17645 // v7.13.0 — inline `UNIQUE` column constraint
17646 // (mailrs round-5 G2). Fold into a single-column
17647 // table-level UNIQUE at CREATE TABLE post-process time.
17648 if let Token::Ident(s) = self.peek()
17649 && s.eq_ignore_ascii_case("unique")
17650 {
17651 if is_unique {
17652 return Err(self.err("UNIQUE specified twice".into()));
17653 }
17654 self.advance();
17655 is_unique = true;
17656 // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17657 // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17658 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17659 let n1 = self.tokens.get(self.pos + 1);
17660 let n2 = self.tokens.get(self.pos + 2);
17661 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17662 self.advance(); // NULLS
17663 self.advance(); // NOT
17664 self.advance(); // DISTINCT
17665 unique_nulls_not_distinct = true;
17666 } else if matches!(n1, Some(Token::Distinct)) {
17667 self.advance(); // NULLS
17668 self.advance(); // DISTINCT
17669 }
17670 }
17671 continue;
17672 }
17673 // v7.13.0 — inline `CHECK (<expr>)` column constraint
17674 // (mailrs round-5 G3). PG semantics: column-level
17675 // CHECK is equivalent to a table-level CHECK. Multiple
17676 // inline CHECKs on the same column AND together.
17677 if let Token::Ident(s) = self.peek()
17678 && s.eq_ignore_ascii_case("check")
17679 {
17680 self.advance();
17681 if !matches!(self.peek(), Token::LParen) {
17682 return Err(self.err(alloc::format!(
17683 "expected '(' after CHECK in column def, got {:?}",
17684 self.peek()
17685 )));
17686 }
17687 self.advance();
17688 let pred = self.parse_expr(0)?;
17689 if !matches!(self.peek(), Token::RParen) {
17690 return Err(self.err(alloc::format!(
17691 "expected ')' to close CHECK predicate, got {:?}",
17692 self.peek()
17693 )));
17694 }
17695 self.advance();
17696 check = Some(match check.take() {
17697 Some(prev) => Expr::Binary {
17698 op: BinOp::And,
17699 lhs: Box::new(prev),
17700 rhs: Box::new(pred),
17701 },
17702 None => pred,
17703 });
17704 continue;
17705 }
17706 break;
17707 }
17708 Ok(ColumnDef {
17709 name,
17710 ty,
17711 nullable,
17712 default,
17713 auto_increment,
17714 is_primary_key,
17715 is_unique,
17716 unique_nulls_not_distinct,
17717 constraint_deferrable,
17718 constraint_initially_deferred,
17719 check,
17720 user_type_ref,
17721 on_update_runtime,
17722 collation,
17723 collation_explicit,
17724 collation_name,
17725 is_unsigned,
17726 inline_enum_variants,
17727 inline_set_variants,
17728 generated_stored_expr,
17729 identity_always,
17730 mysql_int_width,
17731 mysql_fsp,
17732 })
17733 }
17734
17735 /// `NUMERIC` may appear without parameters, with one (precision
17736 /// only, scale=0), or with both. Returns `(precision, scale)` with
17737 /// 0 = unspecified for the bare form.
17738 fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17739 if !matches!(self.peek(), Token::LParen) {
17740 // Bare `NUMERIC` — PG treats this as "unlimited precision";
17741 // we surface it as precision=0 to mean "unconstrained" so
17742 // the engine doesn't need a separate variant.
17743 return Ok((0, 0));
17744 }
17745 self.advance();
17746 // v7.39 (round 272) — PG's declared precision runs to 1000, and
17747 // it words the out-of-range case with the value it saw. SPG
17748 // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17749 // accepts failed to parse at all; values wider than i128 are
17750 // carried by the arbitrary-precision form.
17751 let precision = match self.advance() {
17752 Token::Integer(n) if (1..=1000).contains(&n) => {
17753 u16::try_from(n).expect("range-checked")
17754 }
17755 Token::Integer(n) => {
17756 return Err(ParseError {
17757 message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17758 token_pos: self.consumed_pos(),
17759 });
17760 }
17761 other => {
17762 return Err(ParseError {
17763 message: format!(
17764 "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17765 ),
17766 token_pos: self.consumed_pos(),
17767 });
17768 }
17769 };
17770 // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17771 // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17772 // then overflows). A negative scale rounds to tens / hundreds / …
17773 let scale = if matches!(self.peek(), Token::Comma) {
17774 self.advance();
17775 let neg = if matches!(self.peek(), Token::Minus) {
17776 self.advance();
17777 true
17778 } else {
17779 false
17780 };
17781 match self.advance() {
17782 Token::Integer(n) => {
17783 let signed = if neg { -n } else { n };
17784 if !(-1000..=1000).contains(&signed) {
17785 return Err(ParseError {
17786 message: format!(
17787 "NUMERIC scale {signed} must be between -1000 and 1000"
17788 ),
17789 token_pos: self.consumed_pos(),
17790 });
17791 }
17792 i16::try_from(signed).expect("range-checked")
17793 }
17794 other => {
17795 return Err(ParseError {
17796 message: format!("NUMERIC scale must be an integer, got {other:?}"),
17797 token_pos: self.consumed_pos(),
17798 });
17799 }
17800 }
17801 } else {
17802 0
17803 };
17804 if !matches!(self.peek(), Token::RParen) {
17805 return Err(self.err(format!(
17806 "expected ')' to close NUMERIC params, got {:?}",
17807 self.peek()
17808 )));
17809 }
17810 self.advance();
17811 Ok((precision, scale))
17812 }
17813
17814 /// Parse `(N)` where `N` is a positive integer literal — used by the
17815 /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
17816 /// for the error message.
17817 /// v6.0.1: parse the optional `USING <encoding>` clause that
17818 /// follows `VECTOR(N)` in a column definition. Missing clause
17819 /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
17820 /// ident → `ParseError` listing the encodings recognised today.
17821 fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
17822 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
17823 return Ok(VecEncoding::F32);
17824 }
17825 // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
17826 // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
17827 // consume the token when the very next token is a known
17828 // vector-encoding keyword (SQ8 / HALF). Otherwise leave
17829 // `USING` for the caller — it's the rewrite-expression form.
17830 let n1 = self.tokens.get(self.pos + 1);
17831 let next_is_encoding = matches!(
17832 n1,
17833 Some(Token::Ident(s))
17834 if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
17835 );
17836 if !next_is_encoding {
17837 return Ok(VecEncoding::F32);
17838 }
17839 self.advance();
17840 let enc_ident = match self.advance() {
17841 Token::Ident(s) => s,
17842 other => {
17843 return Err(self.err(format!(
17844 "expected vector encoding after USING, got {other:?}"
17845 )));
17846 }
17847 };
17848 match enc_ident.to_ascii_lowercase().as_str() {
17849 "sq8" => Ok(VecEncoding::Sq8),
17850 // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
17851 // binary16 per-element storage.
17852 "half" => Ok(VecEncoding::F16),
17853 other => Err(self.err(format!(
17854 "unknown vector encoding {other:?}; supported: SQ8, HALF"
17855 ))),
17856 }
17857 }
17858
17859 /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
17860 /// without consuming it. Returns `Some(N)` when the next
17861 /// tokens are `( <int> )`; None otherwise. Used by the
17862 /// TINYINT classifier to decide whether to map to Bool or
17863 /// SmallInt.
17864 fn peek_optional_paren_size_value(&self) -> Option<i64> {
17865 if !matches!(self.peek(), Token::LParen) {
17866 return None;
17867 }
17868 let next = self.tokens.get(self.pos + 1)?;
17869 let n = match next {
17870 Token::Integer(n) => *n,
17871 _ => return None,
17872 };
17873 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17874 return None;
17875 }
17876 Some(n)
17877 }
17878
17879 /// v7.14.0 — consume an optional MySQL display-width
17880 /// parenthesised number after an integer type, returning
17881 /// nothing. `TINYINT(1)` etc.
17882 /// v7.39 (round 360) — does the parenthesised group ahead contain a
17883 /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
17884 fn peek_paren_has_comma(&self) -> bool {
17885 let mut i = self.pos + 1;
17886 let mut depth = 1usize;
17887 while depth > 0 {
17888 match self.tokens.get(i) {
17889 Some(Token::LParen) => depth += 1,
17890 Some(Token::RParen) => depth -= 1,
17891 Some(Token::Comma) if depth == 1 => return true,
17892 None | Some(Token::Eof) => return false,
17893 _ => {}
17894 }
17895 i += 1;
17896 }
17897 false
17898 }
17899
17900 /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
17901 /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
17902 /// fractional-seconds precision that drives write truncation and render
17903 /// padding, where `consume_optional_paren_size` throws it away.
17904 /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
17905 fn take_optional_paren_size(&mut self) -> Option<u8> {
17906 let Some(Token::Integer(n)) = self
17907 .tokens
17908 .get(self.pos + 1)
17909 .filter(|_| matches!(self.peek(), Token::LParen))
17910 .cloned()
17911 else {
17912 self.consume_optional_paren_size();
17913 return None;
17914 };
17915 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17916 self.consume_optional_paren_size();
17917 return None;
17918 }
17919 self.consume_optional_paren_size();
17920 u8::try_from(n).ok()
17921 }
17922
17923 fn consume_optional_paren_size(&mut self) {
17924 if !matches!(self.peek(), Token::LParen) {
17925 return;
17926 }
17927 self.advance();
17928 // Skip until matching RParen (allow nested or any tokens).
17929 let mut depth = 1usize;
17930 while depth > 0 {
17931 match self.peek() {
17932 Token::LParen => depth += 1,
17933 Token::RParen => depth -= 1,
17934 Token::Eof => return,
17935 _ => {}
17936 }
17937 self.advance();
17938 }
17939 }
17940
17941 fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
17942 if !matches!(self.peek(), Token::LParen) {
17943 return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
17944 }
17945 self.advance();
17946 let n = match self.advance() {
17947 Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
17948 message: format!("{label} size too large: {n}"),
17949 token_pos: self.consumed_pos(),
17950 })?,
17951 other => {
17952 return Err(ParseError {
17953 message: format!("expected positive integer {label} size, got {other:?}"),
17954 token_pos: self.consumed_pos(),
17955 });
17956 }
17957 };
17958 if !matches!(self.peek(), Token::RParen) {
17959 return Err(self.err(format!(
17960 "expected ')' after {label} size, got {:?}",
17961 self.peek()
17962 )));
17963 }
17964 self.advance();
17965 Ok(n)
17966 }
17967
17968 /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
17969 /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
17970 /// key, like MySQL) whose action skips conflicting rows.
17971 /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
17972 /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
17973 /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
17974 /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
17975 /// common bulk-upsert spellings —
17976 /// INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
17977 /// REPLACE INTO t SELECT …
17978 /// — were a parse error / a duplicate-key failure respectively.
17979 ///
17980 /// Precedence: an explicitly written clause beats a statement-level flag.
17981 /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
17982 /// implicit `REPLACE` and `IGNORE` lowerings.
17983 fn parse_insert_conflict_clause(
17984 &mut self,
17985 replace: bool,
17986 ignore: bool,
17987 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17988 if let Some(c) = self.parse_optional_on_duplicate_key()? {
17989 return Ok(Some(c));
17990 }
17991 if let Some(c) = self.parse_optional_on_conflict()? {
17992 return Ok(Some(c));
17993 }
17994 if replace {
17995 // REPLACE INTO = delete-then-insert, which PG spells as
17996 // `ON CONFLICT DO UPDATE SET` over every column; the engine
17997 // reads an empty assignment list as "take the incoming row".
17998 return Ok(Some(crate::ast::OnConflictClause {
17999 target_columns: Vec::new(),
18000 index_where: None,
18001 constraint_name: None,
18002 mysql_lowered: true,
18003 action: crate::ast::OnConflictAction::Update {
18004 assignments: Vec::new(),
18005 where_: None,
18006 },
18007 }));
18008 }
18009 if ignore {
18010 return Ok(Some(Self::insert_ignore_clause()));
18011 }
18012 Ok(None)
18013 }
18014
18015 /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
18016 /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
18017 /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
18018 /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
18019 fn parse_optional_on_duplicate_key(
18020 &mut self,
18021 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18022 if !(matches!(self.peek(), Token::On)
18023 && matches!(self.tokens.get(self.pos + 1),
18024 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
18025 {
18026 return Ok(None);
18027 }
18028 self.advance(); // ON
18029 self.advance(); // DUPLICATE
18030 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
18031 return Err(self.err(format!(
18032 "expected KEY after ON DUPLICATE, got {:?}",
18033 self.peek()
18034 )));
18035 }
18036 self.advance();
18037 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
18038 return Err(self.err(format!(
18039 "expected UPDATE after ON DUPLICATE KEY, got {:?}",
18040 self.peek()
18041 )));
18042 }
18043 self.advance();
18044 let mut assignments: Vec<(String, Expr)> = Vec::new();
18045 loop {
18046 let col = self.expect_ident_like()?;
18047 if !matches!(self.peek(), Token::Eq) {
18048 return Err(self.err(format!(
18049 "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
18050 self.peek()
18051 )));
18052 }
18053 self.advance();
18054 let mut expr = self.parse_expr(0)?;
18055 Self::rewrite_mysql_values_refs(&mut expr);
18056 assignments.push((col, expr));
18057 if matches!(self.peek(), Token::Comma) {
18058 self.advance();
18059 continue;
18060 }
18061 break;
18062 }
18063 Ok(Some(crate::ast::OnConflictClause {
18064 target_columns: Vec::new(),
18065 index_where: None,
18066 constraint_name: None,
18067 mysql_lowered: true,
18068 action: crate::ast::OnConflictAction::Update {
18069 assignments,
18070 where_: None,
18071 },
18072 }))
18073 }
18074
18075 fn insert_ignore_clause() -> crate::ast::OnConflictClause {
18076 crate::ast::OnConflictClause {
18077 target_columns: Vec::new(),
18078 index_where: None,
18079 constraint_name: None,
18080 mysql_lowered: true,
18081 action: crate::ast::OnConflictAction::Nothing,
18082 }
18083 }
18084
18085 fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
18086 debug_assert!(
18087 matches!(self.peek(), Token::Insert)
18088 || (replace
18089 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
18090 );
18091 self.advance();
18092 // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
18093 // would raise a duplicate-key error instead of failing the statement,
18094 // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
18095 // plain ident to the lexer; only the MySQL dialect accepts it here.
18096 let ignore = self.mysql_dialect
18097 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
18098 if ignore {
18099 self.advance();
18100 }
18101 if !matches!(self.peek(), Token::Into) {
18102 return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
18103 }
18104 self.advance();
18105 let table = self.expect_ident_like()?;
18106 // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
18107 // grammar requires the AS keyword here (a bare identifier would be
18108 // ambiguous with a column list). The alias is what the ON CONFLICT
18109 // DO UPDATE expressions refer to the target row by.
18110 let alias = if matches!(self.peek(), Token::As) {
18111 self.advance();
18112 Some(self.expect_ident_like()?)
18113 } else {
18114 None
18115 };
18116 // v7.39 (round 428) — MySQL's SET-form INSERT:
18117 // INSERT INTO t SET a = 1, b = 'x'
18118 // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
18119 // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
18120 // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
18121 // measured). So it lowers to the column list + one VALUES row and
18122 // rejoins the ordinary path, which already handles every one of
18123 // those. PG has no such spelling, hence the dialect gate.
18124 if self.mysql_dialect
18125 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
18126 {
18127 self.advance(); // SET
18128 let mut names = Vec::new();
18129 let mut values = Vec::new();
18130 loop {
18131 names.push(self.expect_ident_like()?);
18132 if !matches!(self.peek(), Token::Eq) {
18133 return Err(self.err(alloc::format!(
18134 "expected '=' in INSERT … SET, got {:?}",
18135 self.peek()
18136 )));
18137 }
18138 self.advance();
18139 // `SET a = DEFAULT` rides the same `__column_default` marker
18140 // the VALUES-row and UPDATE-SET paths use; the INSERT
18141 // executor resolves it against the target column.
18142 if matches!(self.peek(), Token::Default) {
18143 self.advance();
18144 values.push(Expr::FunctionCall {
18145 name: "__column_default".to_string(),
18146 args: Vec::new(),
18147 });
18148 } else {
18149 values.push(self.parse_expr(0)?);
18150 }
18151 if matches!(self.peek(), Token::Comma) {
18152 self.advance();
18153 continue;
18154 }
18155 break;
18156 }
18157 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18158 let returning = self.parse_optional_returning()?;
18159 return Ok(Statement::Insert(InsertStatement {
18160 ctes: Vec::new(),
18161 table,
18162 alias,
18163 columns: Some(names),
18164 rows: alloc::vec![values],
18165 select_source: None,
18166 // MySQL's SET form has no `OVERRIDING …` clause (that is
18167 // PG's identity-column spelling).
18168 overriding: Overriding::None,
18169 mysql_ignore: ignore,
18170 on_conflict,
18171 returning,
18172 }));
18173 }
18174 // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
18175 // v7.39 (round 151) — a SELECT or WITH right after the paren is
18176 // a parenthesized query source instead (PG select_with_parens:
18177 // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
18178 // both keywords are reserved in PG, so no column list can start
18179 // with them.
18180 let columns = if matches!(self.peek(), Token::LParen) {
18181 self.advance();
18182 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18183 let select_stmt = if self.peek_is_with_kw() {
18184 self.advance();
18185 self.parse_nested_with_select()?
18186 } else {
18187 match self.parse_select_stmt()? {
18188 Statement::Select(s) => s,
18189 other => {
18190 return Err(self.err(alloc::format!(
18191 "expected SELECT in parenthesized INSERT source, got {other:?}"
18192 )));
18193 }
18194 }
18195 };
18196 if !matches!(self.peek(), Token::RParen) {
18197 return Err(self.err(format!(
18198 "expected ')' after parenthesized INSERT source, got {:?}",
18199 self.peek()
18200 )));
18201 }
18202 self.advance();
18203 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18204 let returning = self.parse_optional_returning()?;
18205 return Ok(Statement::Insert(InsertStatement {
18206 ctes: Vec::new(),
18207 table,
18208 alias: alias.clone(),
18209 columns: None,
18210 rows: Vec::new(),
18211 select_source: Some(Box::new(select_stmt)),
18212 on_conflict,
18213 returning,
18214 overriding: Overriding::None,
18215 mysql_ignore: ignore,
18216 }));
18217 }
18218 let mut names = Vec::new();
18219 loop {
18220 names.push(self.expect_ident_like()?);
18221 match self.peek() {
18222 Token::Comma => {
18223 self.advance();
18224 }
18225 Token::RParen => {
18226 self.advance();
18227 break;
18228 }
18229 other => {
18230 return Err(self.err(format!(
18231 "expected ',' or ')' in INSERT column list, got {other:?}"
18232 )));
18233 }
18234 }
18235 }
18236 Some(names)
18237 } else {
18238 None
18239 };
18240 // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
18241 // OVERRIDING SYSTEM VALUE for its identity columns. The clause
18242 // is captured on the statement so the engine can apply PG's
18243 // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
18244 let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
18245 {
18246 self.advance();
18247 let which = self.expect_ident_like()?;
18248 let ov = if which.eq_ignore_ascii_case("system") {
18249 Overriding::System
18250 } else if which.eq_ignore_ascii_case("user") {
18251 Overriding::User
18252 } else {
18253 return Err(self.err(format!(
18254 "expected SYSTEM or USER after OVERRIDING, got {which:?}"
18255 )));
18256 };
18257 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
18258 return Err(self.err(format!(
18259 "expected VALUE after OVERRIDING {}, got {:?}",
18260 which.to_ascii_uppercase(),
18261 self.peek()
18262 )));
18263 }
18264 self.advance();
18265 ov
18266 } else {
18267 Overriding::None
18268 };
18269 // `INSERT INTO t DEFAULT VALUES` — a single row made
18270 // entirely of column defaults. Lower to the permuted
18271 // column-list path with an empty list: every schema column
18272 // is unmapped, so the engine fills each from its default
18273 // (serials advance, plain defaults evaluate, the rest NULL).
18274 if matches!(self.peek(), Token::Default) {
18275 self.advance();
18276 if !matches!(self.peek(), Token::Values) {
18277 return Err(self.err(format!(
18278 "expected VALUES after DEFAULT in INSERT, got {:?}",
18279 self.peek()
18280 )));
18281 }
18282 self.advance();
18283 if columns.is_some() {
18284 return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
18285 }
18286 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18287 let returning = self.parse_optional_returning()?;
18288 return Ok(Statement::Insert(InsertStatement {
18289 ctes: Vec::new(),
18290 table,
18291 alias: alias.clone(),
18292 columns: Some(Vec::new()),
18293 rows: alloc::vec![Vec::new()],
18294 select_source: None,
18295 on_conflict,
18296 returning,
18297 overriding,
18298 mysql_ignore: ignore,
18299 }));
18300 }
18301 // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
18302 // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
18303 // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
18304 // SELECT …`) heads the SOURCE select, as in PG (the statement's
18305 // own WITH comes before INSERT).
18306 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18307 let select_stmt = if self.peek_is_with_kw() {
18308 self.advance();
18309 self.parse_nested_with_select()?
18310 } else {
18311 match self.parse_select_stmt()? {
18312 Statement::Select(s) => s,
18313 other => {
18314 return Err(self.err(alloc::format!(
18315 "expected SELECT after INSERT INTO ... target, got {other:?}"
18316 )));
18317 }
18318 }
18319 };
18320 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18321 let returning = self.parse_optional_returning()?;
18322 return Ok(Statement::Insert(InsertStatement {
18323 ctes: Vec::new(),
18324 table,
18325 alias: alias.clone(),
18326 columns,
18327 rows: Vec::new(),
18328 select_source: Some(Box::new(select_stmt)),
18329 on_conflict,
18330 returning,
18331 overriding,
18332 mysql_ignore: ignore,
18333 }));
18334 }
18335 if !matches!(self.peek(), Token::Values) {
18336 return Err(self.err(format!(
18337 "expected VALUES or SELECT after table name, got {:?}",
18338 self.peek()
18339 )));
18340 }
18341 self.advance();
18342 if !matches!(self.peek(), Token::LParen) {
18343 return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
18344 }
18345 let mut rows = Vec::new();
18346 loop {
18347 // Each iteration consumes one `(expr, expr, …)` tuple.
18348 if !matches!(self.peek(), Token::LParen) {
18349 return Err(self.err(format!(
18350 "expected '(' for next VALUES tuple, got {:?}",
18351 self.peek()
18352 )));
18353 }
18354 self.advance();
18355 let mut tuple = Vec::new();
18356 loop {
18357 // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
18358 // the column's declared default for that slot. Rides out as the
18359 // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
18360 // path uses; the INSERT executor resolves it per target column.
18361 if matches!(self.peek(), Token::Default) {
18362 self.advance();
18363 tuple.push(Expr::FunctionCall {
18364 name: "__column_default".to_string(),
18365 args: Vec::new(),
18366 });
18367 } else {
18368 tuple.push(self.parse_expr(0)?);
18369 }
18370 match self.peek() {
18371 Token::Comma => {
18372 self.advance();
18373 }
18374 Token::RParen => {
18375 self.advance();
18376 break;
18377 }
18378 other => {
18379 return Err(self.err(format!(
18380 "expected ',' or ')' in VALUES tuple, got {other:?}"
18381 )));
18382 }
18383 }
18384 }
18385 if tuple.is_empty() {
18386 return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
18387 }
18388 rows.push(tuple);
18389 // Continue with comma-separated tuples.
18390 if matches!(self.peek(), Token::Comma) {
18391 self.advance();
18392 } else {
18393 break;
18394 }
18395 }
18396 // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
18397 // to ON CONFLICT DO UPDATE with an empty conflict target
18398 // (the engine picks the table's first unique index, which
18399 // matches MySQL's any-unique-key behaviour for the common
18400 // single-key case). `VALUES(col)` in the assignments is
18401 // MySQL's spelling of EXCLUDED.col.
18402 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18403 let returning = self.parse_optional_returning()?;
18404 Ok(Statement::Insert(InsertStatement {
18405 ctes: Vec::new(),
18406 table,
18407 alias,
18408 columns,
18409 rows,
18410 select_source: None,
18411 on_conflict,
18412 returning,
18413 overriding,
18414 mysql_ignore: ignore,
18415 }))
18416 }
18417
18418 /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
18419 /// the incoming row's value — exactly PG's EXCLUDED.col.
18420 fn rewrite_mysql_values_refs(e: &mut Expr) {
18421 match e {
18422 Expr::FunctionCall { name, args }
18423 if name.eq_ignore_ascii_case("values")
18424 && args.len() == 1
18425 && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
18426 {
18427 let Expr::Column(c) = &args[0] else {
18428 unreachable!("guarded above");
18429 };
18430 *e = Expr::Column(crate::ast::ColumnName {
18431 qualifier: Some("EXCLUDED".to_string()),
18432 name: c.name.clone(),
18433 });
18434 }
18435 Expr::FunctionCall { args, .. } => {
18436 for a in args {
18437 Self::rewrite_mysql_values_refs(a);
18438 }
18439 }
18440 Expr::Binary { lhs, rhs, .. } => {
18441 Self::rewrite_mysql_values_refs(lhs);
18442 Self::rewrite_mysql_values_refs(rhs);
18443 }
18444 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
18445 Self::rewrite_mysql_values_refs(expr);
18446 }
18447 Expr::Case {
18448 operand,
18449 branches,
18450 else_branch,
18451 } => {
18452 if let Some(op) = operand {
18453 Self::rewrite_mysql_values_refs(op);
18454 }
18455 for (w, t) in branches {
18456 Self::rewrite_mysql_values_refs(w);
18457 Self::rewrite_mysql_values_refs(t);
18458 }
18459 if let Some(el) = else_branch {
18460 Self::rewrite_mysql_values_refs(el);
18461 }
18462 }
18463 _ => {}
18464 }
18465 }
18466
18467 /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
18468 /// clause sitting between the INSERT body and the trailing
18469 /// RETURNING. All keywords come in as bare idents; `ON` is
18470 /// a reserved Token though.
18471 fn parse_optional_on_conflict(
18472 &mut self,
18473 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18474 if !matches!(self.peek(), Token::On) {
18475 return Ok(None);
18476 }
18477 // Peek further: we want exactly "ON CONFLICT ...". If the
18478 // next ident isn't "conflict", let some other parser handle.
18479 let next_is_conflict = matches!(
18480 self.tokens.get(self.pos + 1),
18481 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18482 );
18483 if !next_is_conflict {
18484 return Ok(None);
18485 }
18486 self.advance(); // ON
18487 self.advance(); // CONFLICT
18488 // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18489 // the constraint instead of listing columns (the pg_dump
18490 // form); the engine resolves it.
18491 let mut constraint_name: Option<String> = None;
18492 if matches!(self.peek(), Token::On) {
18493 self.advance(); // ON
18494 match self.advance() {
18495 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18496 }
18497 other => {
18498 return Err(self.err(alloc::format!(
18499 "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18500 )));
18501 }
18502 }
18503 constraint_name = Some(self.expect_ident_like()?);
18504 }
18505 // Optional `(col [, col]*)` target list.
18506 let mut target_columns: Vec<String> = Vec::new();
18507 if matches!(self.peek(), Token::LParen) {
18508 self.advance();
18509 loop {
18510 target_columns.push(self.expect_ident_like()?);
18511 match self.peek() {
18512 Token::Comma => {
18513 self.advance();
18514 }
18515 Token::RParen => {
18516 self.advance();
18517 break;
18518 }
18519 other => {
18520 return Err(self.err(alloc::format!(
18521 "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18522 )));
18523 }
18524 }
18525 }
18526 }
18527 // v7.39 (round 240) — optional index predicate after the target
18528 // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18529 // PARTIAL unique index; SPG's arbiters are full indexes, which
18530 // satisfy any predicate, so it is parsed and carried but not
18531 // consulted (recorded residual: partial-unique-index arbiters).
18532 let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18533 self.advance();
18534 Some(self.parse_expr(0)?)
18535 } else {
18536 None
18537 };
18538 // Required `DO`.
18539 match self.advance() {
18540 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18541 other => {
18542 return Err(self.err(alloc::format!(
18543 "expected DO after ON CONFLICT [(…)], got {other:?}"
18544 )));
18545 }
18546 }
18547 // Action: NOTHING | UPDATE SET …
18548 let action = match self.advance() {
18549 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18550 crate::ast::OnConflictAction::Nothing
18551 }
18552 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18553 self.parse_on_conflict_update_action()?
18554 }
18555 other => {
18556 return Err(self.err(alloc::format!(
18557 "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18558 )));
18559 }
18560 };
18561 Ok(Some(crate::ast::OnConflictClause {
18562 target_columns,
18563 index_where,
18564 constraint_name,
18565 mysql_lowered: false,
18566 action,
18567 }))
18568 }
18569
18570 /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18571 /// `SET col = expr [, …] [WHERE cond]`. Caller already
18572 /// consumed `UPDATE`.
18573 fn parse_on_conflict_update_action(
18574 &mut self,
18575 ) -> Result<crate::ast::OnConflictAction, ParseError> {
18576 // `SET`
18577 match self.advance() {
18578 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18579 other => {
18580 return Err(self.err(alloc::format!(
18581 "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18582 )));
18583 }
18584 }
18585 let mut assignments: Vec<(String, Expr)> = Vec::new();
18586 loop {
18587 let col = self.expect_ident_like()?;
18588 if !matches!(self.peek(), Token::Eq) {
18589 return Err(self.err(alloc::format!(
18590 "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18591 self.peek()
18592 )));
18593 }
18594 self.advance();
18595 let value = self.parse_expr(0)?;
18596 assignments.push((col, value));
18597 if matches!(self.peek(), Token::Comma) {
18598 self.advance();
18599 continue;
18600 }
18601 break;
18602 }
18603 let where_ = if matches!(self.peek(), Token::Where) {
18604 self.advance();
18605 Some(self.parse_expr(0)?)
18606 } else {
18607 None
18608 };
18609 Ok(crate::ast::OnConflictAction::Update {
18610 assignments,
18611 where_,
18612 })
18613 }
18614
18615 fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18616 let mut items = Vec::new();
18617 // v7.39 (round 341, V66) — PG's target list may be EMPTY
18618 // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18619 // answers one zero-column row per row of t, and a bare `SELECT`
18620 // answers a single zero-column row. SPG required at least one
18621 // item, so both were syntax errors. Recognised by the token that
18622 // follows — nothing that can start an expression appears here.
18623 if self.select_list_is_empty_here() {
18624 return Ok(items);
18625 }
18626 loop {
18627 items.push(self.parse_select_item()?);
18628 if matches!(self.peek(), Token::Comma) {
18629 self.advance();
18630 } else {
18631 break;
18632 }
18633 }
18634 Ok(items)
18635 }
18636
18637 /// Is the target list empty at this point — i.e. does the next token
18638 /// end the SELECT's item list rather than start an item?
18639 fn select_list_is_empty_here(&self) -> bool {
18640 match self.peek() {
18641 Token::From
18642 | Token::Where
18643 | Token::Group
18644 | Token::Having
18645 | Token::Order
18646 | Token::Limit
18647 | Token::Offset
18648 | Token::Semicolon
18649 | Token::RParen
18650 | Token::Union
18651 | Token::Except
18652 | Token::Eof => true,
18653 // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18654 // with unreserved keywords, so they arrive as plain idents.
18655 Token::Ident(s) => {
18656 s.eq_ignore_ascii_case("fetch")
18657 || s.eq_ignore_ascii_case("window")
18658 || s.eq_ignore_ascii_case("intersect")
18659 }
18660 _ => false,
18661 }
18662 }
18663
18664 fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18665 if matches!(self.peek(), Token::Star) {
18666 self.advance();
18667 return Ok(SelectItem::Wildcard);
18668 }
18669 // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18670 // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18671 // choke on the `*` ("expected identifier, got Star"). The lookahead is
18672 // `<ident> . *` with nothing binding tighter.
18673 if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18674 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18675 && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18676 {
18677 self.advance(); // qualifier
18678 self.advance(); // .
18679 self.advance(); // *
18680 return Ok(SelectItem::QualifiedWildcard(q));
18681 }
18682 }
18683 let start_tok = self.pos;
18684 let expr = self.parse_expr(0)?;
18685 let end_tok = self.consumed_pos();
18686 // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18687 // multi-column function returns into columns. Marked here and lowered in
18688 // `parse_bare_select`, where the FROM clause is in hand.
18689 if matches!(self.peek(), Token::Dot)
18690 && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18691 {
18692 self.advance(); // .
18693 self.advance(); // *
18694 return Ok(SelectItem::Expr {
18695 expr: Expr::FunctionCall {
18696 name: "__record_expand".to_string(),
18697 args: alloc::vec![expr],
18698 },
18699 alias: None,
18700 });
18701 }
18702 let alias = match self.parse_optional_alias()? {
18703 Some(a) => Some(a),
18704 None => self.mysql_item_label(&expr, start_tok, end_tok),
18705 };
18706 Ok(SelectItem::Expr { expr, alias })
18707 }
18708
18709 /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18710 /// carries no `AS`, filled in here so every downstream path reports it
18711 /// without knowing the rule. `None` leaves the item un-aliased, which is
18712 /// what a PG session always gets.
18713 ///
18714 /// Measured against MariaDB 11, three rules and no more:
18715 ///
18716 /// | item | label | why |
18717 /// |------------------|------------|------------------------------|
18718 /// | `lbl.a` | `a` | a column reports its name |
18719 /// | `'it''s'` | `it's` | a string reports its VALUE |
18720 /// | `a + b` | `a + b` | anything else, source text |
18721 ///
18722 /// The third is why this lives in the parser at all: the label is the
18723 /// text the client WROTE, down to the spacing, so it cannot be printed
18724 /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18725 ///
18726 /// Comments survive, and that is right: through a `mariadb` CLI both
18727 /// servers answer `a + b` for `SELECT a /* c */ + b`, but that is the
18728 /// CLIENT stripping the comment before it sends. Asked over the raw
18729 /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18730 /// produces.
18731 fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18732 if !self.mysql_dialect {
18733 return None;
18734 }
18735 match expr {
18736 // A column already reports its own name downstream; naming it
18737 // again here would only re-state the qualifier the label drops.
18738 Expr::Column(_) => None,
18739 Expr::Literal(Literal::String(v)) => Some(v.clone()),
18740 _ => self.source_span(start_tok, end_tok).map(str::to_string),
18741 }
18742 }
18743
18744 /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18745 /// consumed VALUES keyword. Each row lowers to a constant SELECT
18746 /// with PG's default column1..columnN names; subsequent rows
18747 /// chain as UNION ALL peers. Shared by the FROM-position
18748 /// `( VALUES … )` arm and the top-level bare VALUES statement.
18749 fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
18750 let mut row_selects: Vec<SelectStatement> = Vec::new();
18751 loop {
18752 if !matches!(self.peek(), Token::LParen) {
18753 return Err(self.err(alloc::format!(
18754 "expected '(' to start a VALUES row, got {:?}",
18755 self.peek()
18756 )));
18757 }
18758 self.advance(); // (
18759 let mut items: Vec<SelectItem> = Vec::new();
18760 loop {
18761 let expr = self.parse_expr(0)?;
18762 items.push(SelectItem::Expr {
18763 expr,
18764 alias: Some(alloc::format!("column{}", items.len() + 1)),
18765 });
18766 match self.peek() {
18767 Token::Comma => {
18768 self.advance();
18769 }
18770 Token::RParen => break,
18771 other => {
18772 return Err(self.err(alloc::format!(
18773 "expected ',' or ')' in VALUES row, got {other:?}"
18774 )));
18775 }
18776 }
18777 }
18778 self.advance(); // )
18779 row_selects.push(SelectStatement {
18780 locking: None,
18781 ctes: Vec::new(),
18782 distinct: false,
18783 distinct_on: Vec::new(),
18784 items,
18785 from: None,
18786 where_: None,
18787 group_by: None,
18788 group_by_all: false,
18789 having: None,
18790 unions: Vec::new(),
18791 order_by: Vec::new(),
18792 limit: None,
18793 offset: None,
18794 limit_with_ties: false,
18795 window_check_exprs: Vec::new(),
18796 });
18797 if matches!(self.peek(), Token::Comma) {
18798 self.advance();
18799 continue;
18800 }
18801 break;
18802 }
18803 let mut head = row_selects.remove(0);
18804 head.unions = row_selects
18805 .into_iter()
18806 .map(|s| (UnionKind::All, s))
18807 .collect();
18808 Ok(head)
18809 }
18810
18811 fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
18812 // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
18813 // children. It was read as a table NAMED `only`, so the query
18814 // failed on `relation "only" does not exist`.
18815 //
18816 // v7.39 (round 644) — and it is no longer a no-op. Round 621
18817 // absorbed the keyword, reasoning that SPG's children are
18818 // separate relations a plain scan does not descend into, so ONLY
18819 // already described the scan. That stopped being true when a
18820 // partition parent started unioning its children: measured,
18821 // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
18822 // where PG answers 0. The flag is carried now.
18823 let mut only = false;
18824 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
18825 && matches!(
18826 self.tokens.get(self.pos + 1),
18827 Some(Token::Ident(_) | Token::QuotedIdent(_))
18828 )
18829 {
18830 only = true;
18831 self.advance();
18832 }
18833 // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
18834 // for these SRFs the keyword is noise at parse time: the
18835 // join executor already substitutes outer-column references
18836 // into unnest_expr / generate_series_args per outer row
18837 // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
18838 // licences the correlation even without the keyword. Absorb
18839 // it and fall through to the SRF arms below.
18840 // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
18841 // just the four builtin SRFs: a user set-returning function on a JOIN's
18842 // right side is the whole point of LATERAL. The keyword stays noise at
18843 // parse time — the join executor substitutes the outer row into the
18844 // call's arguments per outer row.
18845 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18846 && matches!(
18847 self.tokens.get(self.pos + 1),
18848 // The json_each family has its OWN `LATERAL …` arm below, which
18849 // needs to see the keyword — absorbing it here would send those
18850 // calls down the generic table-function channel instead.
18851 Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
18852 )
18853 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18854 {
18855 self.advance(); // LATERAL
18856 }
18857 // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
18858 // set-returning function whose argument may reference a
18859 // preceding FROM item. We rewrite this to
18860 // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
18861 // AS __srf__) AS <alias>` so the existing LATERAL subquery
18862 // executor handles per-outer-row evaluation and the
18863 // SRF-primary jsonb_each_text path handles the inner
18864 // materialisation. Sentori 0067 backfill is the dogfood
18865 // shape.
18866 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18867 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
18868 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18869 {
18870 self.advance(); // LATERAL
18871 let each_fn = match self.peek() {
18872 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18873 _ => unreachable!(),
18874 };
18875 self.advance(); // jsonb_each[_text] / json_each[_text]
18876 self.advance(); // (
18877 let arg = self.parse_expr(0)?;
18878 if !matches!(self.peek(), Token::RParen) {
18879 return Err(self.err(alloc::format!(
18880 "expected ')' after LATERAL {each_fn}() argument, got {:?}",
18881 self.peek()
18882 )));
18883 }
18884 self.advance();
18885 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18886 let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18887 // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
18888 // FROM jsonb_each_text(<arg>) AS __srf__
18889 // PG's `AS kv(key, value)` column-alias list maps
18890 // positions to names; default to (key, value) when
18891 // omitted (matching the SRF's natural column names).
18892 let srf_alias = "__srf__".to_string();
18893 let key_alias = column_aliases
18894 .first()
18895 .cloned()
18896 .unwrap_or_else(|| "key".to_string());
18897 let value_alias = column_aliases
18898 .get(1)
18899 .cloned()
18900 .unwrap_or_else(|| "value".to_string());
18901 let inner_select = crate::ast::SelectStatement {
18902 locking: None,
18903 ctes: Vec::new(),
18904 distinct: false,
18905 distinct_on: Vec::new(),
18906 items: alloc::vec![
18907 crate::ast::SelectItem::Expr {
18908 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18909 qualifier: Some(srf_alias.clone()),
18910 name: "key".to_string(),
18911 }),
18912 alias: Some(key_alias),
18913 },
18914 crate::ast::SelectItem::Expr {
18915 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18916 qualifier: Some(srf_alias.clone()),
18917 name: "value".to_string(),
18918 }),
18919 alias: Some(value_alias),
18920 },
18921 ],
18922 from: Some(crate::ast::FromClause {
18923 primary: TableRef {
18924 name: srf_alias.clone(),
18925 alias: Some(srf_alias.clone()),
18926 only: false,
18927 as_of_segment: None,
18928 unnest_expr: None,
18929 unnest_column_aliases: Vec::new(),
18930 with_ordinality: false,
18931 generate_series_args: None,
18932 lateral_subquery: None,
18933 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18934 table_fn_call: None,
18935 rows_from: None,
18936 json_table: None,
18937 scalar_fn_item: false,
18938 },
18939 joins: Vec::new(),
18940 }),
18941 where_: None,
18942 group_by: None,
18943 group_by_all: false,
18944 having: None,
18945 unions: Vec::new(),
18946 order_by: Vec::new(),
18947 limit: None,
18948 offset: None,
18949 limit_with_ties: false,
18950 window_check_exprs: Vec::new(),
18951 };
18952 return Ok(TableRef {
18953 name: alias.clone(),
18954 alias: Some(alias),
18955 only: false,
18956 as_of_segment: None,
18957 unnest_expr: None,
18958 unnest_column_aliases: Vec::new(),
18959 with_ordinality: false,
18960 generate_series_args: None,
18961 lateral_subquery: Some(Box::new(inner_select)),
18962 jsonb_each_text_arg: None,
18963 table_fn_call: None,
18964 rows_from: None,
18965 json_table: None,
18966 scalar_fn_item: false,
18967 });
18968 }
18969 // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
18970 // without an explicit `LATERAL` keyword is the same shape
18971 // PG accepts (SRF naturally licences lateral correlation).
18972 // We mirror the LATERAL rewrite when the argument syntactic-
18973 // ally references an outer column (Column { qualifier:
18974 // Some(_), … }). For simplicity we apply the rewrite
18975 // whenever the SRF directly follows JOIN/CROSS JOIN/comma
18976 // in the FROM-list — caller-side join parsing positions
18977 // this peek correctly.
18978 // (Implementation note: detection lives below; the LATERAL
18979 // branch above already covers the explicit form; the bare
18980 // form falls through to the plain SRF arm and the engine
18981 // treats it as a constant-arg SRF if no outer reference is
18982 // present.)
18983 // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
18984 // table. Detect at the head so it claims precedence over
18985 // every other table-ref shape (unnest / generate_series /
18986 // bare ident); the lateral subquery itself follows the
18987 // regular SELECT grammar.
18988 // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
18989 // t(cols)`. Each row lowers to a constant SELECT with PG's
18990 // default column1..columnN names; subsequent rows chain as
18991 // UNION ALL peers. The result rides the derived-table
18992 // lateral_subquery channel — zero executor work.
18993 if matches!(self.peek(), Token::LParen)
18994 && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
18995 {
18996 self.advance(); // (
18997 self.advance(); // VALUES
18998 let head = self.parse_values_rows_body()?;
18999 if !matches!(self.peek(), Token::RParen) {
19000 return Err(self.err(alloc::format!(
19001 "expected ')' after VALUES list, got {:?}",
19002 self.peek()
19003 )));
19004 }
19005 self.advance();
19006 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19007 let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
19008 return Ok(TableRef {
19009 name,
19010 alias: alias_ident,
19011 only: false,
19012 as_of_segment: None,
19013 unnest_expr: None,
19014 unnest_column_aliases: column_aliases,
19015 with_ordinality: false,
19016 generate_series_args: None,
19017 lateral_subquery: Some(Box::new(head)),
19018 jsonb_each_text_arg: None,
19019 table_fn_call: None,
19020 rows_from: None,
19021 json_table: None,
19022 scalar_fn_item: false,
19023 });
19024 }
19025 // v7.37.17 (17.6 siblings) — plain derived table:
19026 // `FROM ( SELECT … ) [AS] alias`. Rides the same
19027 // lateral_subquery channel the explicit LATERAL form uses —
19028 // an uncorrelated inner SELECT executes identically. The
19029 // inner parse carries UNION tails (they live on
19030 // SelectStatement.unions).
19031 // v7.37 D.20 — the derived-table inner may itself be a
19032 // parenthesized set-operation group (`FROM ((SELECT…) UNION
19033 // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
19034 // bare `(SELECT …)`. parse_one_statement already routes a leading
19035 // `(` set-op group (its LParen arm) and a leading WITH
19036 // (parse_with_cte_then_select), so widen the second-token gate to
19037 // Select | LParen | WITH.
19038 // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
19039 // PG's spelling of `SELECT * FROM t` and is accepted wherever a
19040 // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
19041 // has existed since the shorthand landed and `parse_bare_select`
19042 // already routes it ("valid anywhere a SELECT head is"); what was
19043 // missing is this second-token gate, and the CTE body's dispatch
19044 // below. Round 868 found both by putting the shorthand in a
19045 // subquery — the top-level forms had been the only ones tested.
19046 if matches!(self.peek(), Token::LParen)
19047 && (matches!(
19048 self.tokens.get(self.pos + 1),
19049 Some(Token::Select | Token::LParen | Token::Table)
19050 ) || matches!(self.tokens.get(self.pos + 1),
19051 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
19052 {
19053 self.advance(); // (
19054 let inner = match self.parse_one_statement()? {
19055 Statement::Select(s) => s,
19056 other => {
19057 return Err(self.err(alloc::format!(
19058 "expected SELECT inside derived table ( … ), got {other:?}"
19059 )));
19060 }
19061 };
19062 if !matches!(self.peek(), Token::RParen) {
19063 return Err(self.err(alloc::format!(
19064 "expected ')' after derived-table subquery, got {:?}",
19065 self.peek()
19066 )));
19067 }
19068 self.advance();
19069 // `AS t(a, b)` column-alias list rides the
19070 // unnest_column_aliases field (same positional-rename
19071 // contract the unnest SRFs use).
19072 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19073 let name = alias_ident
19074 .clone()
19075 .unwrap_or_else(|| "subquery".to_string());
19076 return Ok(TableRef {
19077 name,
19078 alias: alias_ident,
19079 only: false,
19080 as_of_segment: None,
19081 unnest_expr: None,
19082 unnest_column_aliases: column_aliases,
19083 with_ordinality: false,
19084 generate_series_args: None,
19085 lateral_subquery: Some(Box::new(inner)),
19086 jsonb_each_text_arg: None,
19087 table_fn_call: None,
19088 rows_from: None,
19089 json_table: None,
19090 scalar_fn_item: false,
19091 });
19092 }
19093 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19094 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19095 {
19096 self.advance(); // LATERAL
19097 self.advance(); // (
19098 // Parse the inner SELECT.
19099 let inner = match self.parse_one_statement()? {
19100 Statement::Select(s) => s,
19101 other => {
19102 return Err(self.err(alloc::format!(
19103 "expected SELECT inside LATERAL ( … ), got {other:?}"
19104 )));
19105 }
19106 };
19107 if !matches!(self.peek(), Token::RParen) {
19108 return Err(self.err(alloc::format!(
19109 "expected ')' after LATERAL subquery, got {:?}",
19110 self.peek()
19111 )));
19112 }
19113 self.advance();
19114 // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
19115 // `(VALUES …) t(g)` derived table round-trips through view-body
19116 // Display, which renders on the lateral_subquery channel).
19117 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19118 let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
19119 return Ok(TableRef {
19120 name,
19121 alias: alias_ident,
19122 only: false,
19123 as_of_segment: None,
19124 unnest_expr: None,
19125 unnest_column_aliases: column_aliases,
19126 with_ordinality: false,
19127 generate_series_args: None,
19128 lateral_subquery: Some(Box::new(inner)),
19129 jsonb_each_text_arg: None,
19130 table_fn_call: None,
19131 rows_from: None,
19132 json_table: None,
19133 scalar_fn_item: false,
19134 });
19135 }
19136 // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
19137 // function as a FROM item. Emits one row per (key, value)
19138 // pair in the JSONB object argument as TEXT columns. May
19139 // be wrapped in CROSS JOIN LATERAL when the argument
19140 // references a preceding FROM item (sentori migration
19141 // 0067 backfill shape: `CROSS JOIN LATERAL
19142 // jsonb_each_text(t.json_col) AS kv(key, value)`).
19143 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
19144 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19145 {
19146 let each_fn = match self.peek() {
19147 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19148 _ => unreachable!(),
19149 };
19150 self.advance(); // jsonb_each[_text] / json_each[_text]
19151 self.advance(); // (
19152 let arg = self.parse_expr(0)?;
19153 if !matches!(self.peek(), Token::RParen) {
19154 return Err(self.err(alloc::format!(
19155 "expected ')' after {each_fn}() argument, got {:?}",
19156 self.peek()
19157 )));
19158 }
19159 self.advance();
19160 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19161 let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19162 return Ok(TableRef {
19163 name,
19164 alias: alias_ident,
19165 only: false,
19166 as_of_segment: None,
19167 unnest_expr: None,
19168 // `AS t(k, v)` renames key/value positionally, same as the
19169 // LATERAL-position form already does.
19170 unnest_column_aliases: column_aliases,
19171 with_ordinality: false,
19172 generate_series_args: None,
19173 lateral_subquery: None,
19174 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19175 table_fn_call: None,
19176 rows_from: None,
19177 json_table: None,
19178 scalar_fn_item: false,
19179 });
19180 }
19181 // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
19182 // (+ json_ variants) — record-returning JSON functions with a
19183 // column-definition list. Desugar to a derived table that
19184 // projects each declared column from the JSON via `->>` + a cast,
19185 // over `jsonb_array_elements(J)` for the *set (per-element) form.
19186 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
19187 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19188 {
19189 return self.parse_json_to_record_from();
19190 }
19191 // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
19192 // row is a text[] of capture groups, so it cannot desugar to unnest
19193 // (that would flatten the array). Wrap it as a derived table
19194 // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
19195 // SRF path already emits one text[] row per match. PG names the column
19196 // `regexp_matches`; an `AS a(col)` alias overrides it.
19197 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19198 if s.eq_ignore_ascii_case("regexp_matches"))
19199 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19200 {
19201 self.advance(); // fn name
19202 self.advance(); // (
19203 let mut fn_args: Vec<Expr> = Vec::new();
19204 loop {
19205 fn_args.push(self.parse_expr(0)?);
19206 if matches!(self.peek(), Token::Comma) {
19207 self.advance();
19208 continue;
19209 }
19210 break;
19211 }
19212 if !matches!(self.peek(), Token::RParen) {
19213 return Err(self.err(alloc::format!(
19214 "expected ')' after regexp_matches() arguments, got {:?}",
19215 self.peek()
19216 )));
19217 }
19218 self.advance();
19219 // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
19220 // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
19221 // it, so it died on the `with` token while every other table function
19222 // accepted it.
19223 let with_ordinality = self.absorb_with_ordinality();
19224 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19225 let table_alias = alias_ident
19226 .clone()
19227 .unwrap_or_else(|| "regexp_matches".to_string());
19228 // PG names a single-column function's output column after the ALIAS
19229 // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
19230 // `m` reads as that column and not as a whole-row composite. Naming
19231 // it after the function regardless made `SELECT m[1] FROM … AS m`
19232 // subscript a record.
19233 let col_name = column_aliases
19234 .first()
19235 .cloned()
19236 .or_else(|| alias_ident.clone())
19237 .unwrap_or_else(|| "regexp_matches".to_string());
19238 let inner = crate::ast::SelectStatement {
19239 locking: None,
19240 ctes: Vec::new(),
19241 distinct: false,
19242 distinct_on: Vec::new(),
19243 items: alloc::vec![SelectItem::Expr {
19244 expr: Expr::FunctionCall {
19245 name: "regexp_matches".to_string(),
19246 args: fn_args,
19247 },
19248 alias: Some(col_name),
19249 }],
19250 from: None,
19251 where_: None,
19252 group_by: None,
19253 group_by_all: false,
19254 having: None,
19255 unions: Vec::new(),
19256 order_by: Vec::new(),
19257 limit: None,
19258 offset: None,
19259 limit_with_ties: false,
19260 window_check_exprs: Vec::new(),
19261 };
19262 return Ok(TableRef {
19263 name: table_alias.clone(),
19264 alias: Some(table_alias),
19265 only: false,
19266 as_of_segment: None,
19267 unnest_expr: None,
19268 unnest_column_aliases: column_aliases,
19269 with_ordinality,
19270 generate_series_args: None,
19271 lateral_subquery: Some(Box::new(inner)),
19272 jsonb_each_text_arg: None,
19273 table_fn_call: None,
19274 rows_from: None,
19275 json_table: None,
19276 // regexp_matches returns text[], a base type: `SELECT m FROM
19277 // regexp_matches(…) AS m` is the array, not a composite wrapping it.
19278 scalar_fn_item: true,
19279 });
19280 }
19281 // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
19282 // / json_ variants as a FROM item. Rewritten into
19283 // `unnest(<same fn>(<expr>))`: the scalar form returns the
19284 // elements as a TEXT array, and the existing unnest SRF path
19285 // materialises one row per element. PG's natural column name
19286 // is `value`; an `AS a(col)` column-alias list overrides it.
19287 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19288 if s.eq_ignore_ascii_case("jsonb_array_elements")
19289 || s.eq_ignore_ascii_case("json_array_elements")
19290 || s.eq_ignore_ascii_case("jsonb_array_elements_text")
19291 || s.eq_ignore_ascii_case("json_array_elements_text")
19292 || s.eq_ignore_ascii_case("jsonb_object_keys")
19293 || s.eq_ignore_ascii_case("json_object_keys")
19294 || s.eq_ignore_ascii_case("jsonb_path_query")
19295 || s.eq_ignore_ascii_case("json_path_query")
19296 || s.eq_ignore_ascii_case("generate_subscripts")
19297 || s.eq_ignore_ascii_case("string_to_table")
19298 || s.eq_ignore_ascii_case("regexp_split_to_table"))
19299 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19300 {
19301 let fn_name = match self.peek() {
19302 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19303 _ => unreachable!(),
19304 };
19305 self.advance(); // fn name
19306 self.advance(); // (
19307 let mut fn_args: Vec<Expr> = Vec::new();
19308 loop {
19309 fn_args.push(self.parse_expr(0)?);
19310 if matches!(self.peek(), Token::Comma) {
19311 self.advance();
19312 continue;
19313 }
19314 break;
19315 }
19316 if !matches!(self.peek(), Token::RParen) {
19317 return Err(self.err(alloc::format!(
19318 "expected ')' after {fn_name}() arguments, got {:?}",
19319 self.peek()
19320 )));
19321 }
19322 self.advance();
19323 let with_ordinality = self.absorb_with_ordinality();
19324 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19325 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
19326 // PG's natural column name: the array-elements SRFs
19327 // declare an OUT parameter `value`; jsonb_object_keys
19328 // and generate_subscripts have none, so the column is
19329 // named after the function. A bare table alias on a
19330 // single-column SRF renames the column too (PG: `FROM
19331 // generate_subscripts(a, 1) AS s` projects column s) —
19332 // except for the OUT-parameter SRFs, whose column stays
19333 // `value` under a bare alias.
19334 let natural_col = if fn_name.ends_with("_array_elements")
19335 || fn_name.ends_with("_array_elements_text")
19336 {
19337 "value".to_string()
19338 } else {
19339 alias_ident.clone().unwrap_or_else(|| fn_name.clone())
19340 };
19341 let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
19342 // Keep any further entries — the second names the
19343 // ordinality column under WITH ORDINALITY.
19344 srf_cols.extend(column_aliases.into_iter().skip(1));
19345 // The *_to_table SRFs are row-streams over the existing
19346 // *_to_array scalars — map the call target; the display
19347 // name (alias / column defaults) keeps the SRF spelling.
19348 let call_name = match fn_name.as_str() {
19349 "string_to_table" => "string_to_array".to_string(),
19350 "regexp_split_to_table" => "regexp_split_to_array".to_string(),
19351 _ => fn_name,
19352 };
19353 // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
19354 // preceding FROM item (bare or qualified column) is correlated;
19355 // route it through the per-outer-row lateral channel.
19356 let expr = crate::ast::Expr::FunctionCall {
19357 name: call_name,
19358 args: fn_args,
19359 };
19360 let correlated = Self::expr_has_any_column(&expr);
19361 let tref = TableRef {
19362 name,
19363 alias: alias_ident,
19364 only: false,
19365 as_of_segment: None,
19366 unnest_expr: Some(Box::new(expr)),
19367 unnest_column_aliases: srf_cols,
19368 with_ordinality,
19369 generate_series_args: None,
19370 lateral_subquery: None,
19371 jsonb_each_text_arg: None,
19372 table_fn_call: None,
19373 rows_from: None,
19374 json_table: None,
19375 // Each of these returns a BASE type (jsonb / text / int), so the item's
19376 // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
19377 // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
19378 scalar_fn_item: !with_ordinality,
19379 };
19380 return Ok(if correlated {
19381 Self::wrap_correlated_srf(tref)
19382 } else {
19383 tref
19384 });
19385 }
19386 // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
19387 // explicit parallel-zip syntax. Each entry lowers to its
19388 // array-returning scalar form (unnest(x) → x itself; the
19389 // FROM-SRF rewrite family → their scalar array calls) and
19390 // the list rides the multi-arg unnest zip channel:
19391 // NULL-padded to the longest, WITH ORDINALITY appends the
19392 // counter. generate_series has no scalar array form and
19393 // errors honestly.
19394 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
19395 && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
19396 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19397 {
19398 self.advance(); // ROWS
19399 self.advance(); // FROM
19400 self.advance(); // (
19401 let mut entries: Vec<Expr> = Vec::new();
19402 // v7.39 (read01 round 74) — the generic channel, filled in parallel.
19403 // Used only when some entry has no array form.
19404 let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
19405 loop {
19406 let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
19407 if !matches!(self.peek(), Token::LParen) {
19408 return Err(self.err(alloc::format!(
19409 "expected '(' after {fn_name} in ROWS FROM, got {:?}",
19410 self.peek()
19411 )));
19412 }
19413 self.advance();
19414 let mut fn_args: Vec<Expr> = Vec::new();
19415 if !matches!(self.peek(), Token::RParen) {
19416 loop {
19417 fn_args.push(self.parse_expr(0)?);
19418 if matches!(self.peek(), Token::Comma) {
19419 self.advance();
19420 continue;
19421 }
19422 break;
19423 }
19424 }
19425 if !matches!(self.peek(), Token::RParen) {
19426 return Err(self.err(alloc::format!(
19427 "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
19428 self.peek()
19429 )));
19430 }
19431 self.advance();
19432 let entry = match fn_name.as_str() {
19433 "unnest" => {
19434 if fn_args.len() != 1 {
19435 return Err(
19436 self.err("unnest inside ROWS FROM takes exactly one array".into())
19437 );
19438 }
19439 fn_args.pop().expect("len checked")
19440 }
19441 "jsonb_array_elements"
19442 | "json_array_elements"
19443 | "jsonb_array_elements_text"
19444 | "json_array_elements_text"
19445 | "jsonb_object_keys"
19446 | "json_object_keys"
19447 | "generate_subscripts" => crate::ast::Expr::FunctionCall {
19448 name: fn_name,
19449 args: fn_args,
19450 },
19451 "string_to_table" => crate::ast::Expr::FunctionCall {
19452 name: "string_to_array".to_string(),
19453 args: fn_args,
19454 },
19455 "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
19456 name: "regexp_split_to_array".to_string(),
19457 args: fn_args,
19458 },
19459 // v7.39 (read01 round 74) — an SRF with no array form
19460 // (`generate_series`, a user `RETURNS SETOF` function) has no
19461 // scalar expression to zip, so the WHOLE list switches to the
19462 // rows_from channel, which runs each function and zips the
19463 // rows themselves. The all-array case keeps the old lowering:
19464 // it is well-trodden and this must not disturb it.
19465 _ => {
19466 generic.push((fn_name, fn_args));
19467 if matches!(self.peek(), Token::Comma) {
19468 self.advance();
19469 continue;
19470 }
19471 break;
19472 }
19473 };
19474 generic.push((
19475 // The array-able entries carry their lowered expr along, so a
19476 // MIXED list still works: the engine sees the scalar array
19477 // form and unnests it.
19478 "__array".to_string(),
19479 alloc::vec![entry.clone()],
19480 ));
19481 entries.push(entry);
19482 if matches!(self.peek(), Token::Comma) {
19483 self.advance();
19484 continue;
19485 }
19486 break;
19487 }
19488 if !matches!(self.peek(), Token::RParen) {
19489 return Err(self.err(alloc::format!(
19490 "expected ')' to close ROWS FROM, got {:?}",
19491 self.peek()
19492 )));
19493 }
19494 self.advance();
19495 let with_ordinality = self.absorb_with_ordinality();
19496 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19497 let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19498 // v7.39 (read01 round 74) — some entry had no array form, so the whole
19499 // list rides the generic channel.
19500 if generic.iter().any(|(n, _)| n != "__array") {
19501 let correlated = generic
19502 .iter()
19503 .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19504 let tref = TableRef {
19505 name,
19506 alias: alias_ident,
19507 only: false,
19508 as_of_segment: None,
19509 unnest_expr: None,
19510 unnest_column_aliases,
19511 with_ordinality,
19512 generate_series_args: None,
19513 lateral_subquery: None,
19514 jsonb_each_text_arg: None,
19515 table_fn_call: None,
19516 rows_from: Some(generic),
19517 json_table: None,
19518 scalar_fn_item: false,
19519 };
19520 return Ok(if correlated {
19521 Self::wrap_correlated_srf(tref)
19522 } else {
19523 tref
19524 });
19525 }
19526 let correlated = entries.iter().any(Self::expr_has_any_column);
19527 let expr = if entries.len() == 1 {
19528 entries.pop().expect("len checked")
19529 } else {
19530 crate::ast::Expr::FunctionCall {
19531 name: "__unnest_zip".to_string(),
19532 args: entries,
19533 }
19534 };
19535 let tref = TableRef {
19536 name,
19537 alias: alias_ident,
19538 only: false,
19539 as_of_segment: None,
19540 unnest_expr: Some(Box::new(expr)),
19541 unnest_column_aliases,
19542 with_ordinality,
19543 generate_series_args: None,
19544 lateral_subquery: None,
19545 jsonb_each_text_arg: None,
19546 table_fn_call: None,
19547 rows_from: None,
19548 json_table: None,
19549 scalar_fn_item: false,
19550 };
19551 return Ok(if correlated {
19552 Self::wrap_correlated_srf(tref)
19553 } else {
19554 tref
19555 });
19556 }
19557 // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19558 // source. Detect at the head before the bare-ident fallback;
19559 // unnest is not a reserved token.
19560 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19561 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19562 {
19563 self.advance(); // unnest
19564 self.advance(); // (
19565 let mut srf_args = alloc::vec![self.parse_expr(0)?];
19566 while matches!(self.peek(), Token::Comma) {
19567 self.advance();
19568 srf_args.push(self.parse_expr(0)?);
19569 }
19570 if !matches!(self.peek(), Token::RParen) {
19571 return Err(self.err(alloc::format!(
19572 "expected ')' after unnest() argument, got {:?}",
19573 self.peek()
19574 )));
19575 }
19576 self.advance();
19577 // Multi-arg unnest(a, b, …) zips the arrays in
19578 // parallel, NULL-padding to the longest (PG's ROWS
19579 // FROM shorthand). Lower onto the unnest channel as an
19580 // internal marker call the executors unpack.
19581 let expr = if srf_args.len() == 1 {
19582 srf_args.pop().expect("len checked")
19583 } else {
19584 crate::ast::Expr::FunctionCall {
19585 name: "__unnest_zip".to_string(),
19586 args: srf_args,
19587 }
19588 };
19589 let with_ordinality = self.absorb_with_ordinality();
19590 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19591 let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19592 let correlated = Self::expr_has_any_column(&expr);
19593 let tref = TableRef {
19594 name,
19595 alias: alias_ident,
19596 only: false,
19597 as_of_segment: None,
19598 unnest_expr: Some(Box::new(expr)),
19599 unnest_column_aliases,
19600 with_ordinality,
19601 generate_series_args: None,
19602 lateral_subquery: None,
19603 jsonb_each_text_arg: None,
19604 table_fn_call: None,
19605 rows_from: None,
19606 json_table: None,
19607 scalar_fn_item: false,
19608 };
19609 return Ok(if correlated {
19610 Self::wrap_correlated_srf(tref)
19611 } else {
19612 tref
19613 });
19614 }
19615 // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19616 // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19617 // generic table-fn arg parser can't read), so it is intercepted
19618 // here BEFORE the generic dispatch. The doc expr may reference
19619 // outer columns (implicit LATERAL) — same correlated-wrap rule.
19620 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19621 if s.eq_ignore_ascii_case("json_table"))
19622 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19623 {
19624 let tref = self.parse_json_table_ref()?;
19625 let correlated = tref
19626 .json_table
19627 .as_deref()
19628 .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19629 return Ok(if correlated {
19630 Self::wrap_correlated_srf(tref)
19631 } else {
19632 tref
19633 });
19634 }
19635 // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19636 // functions dispatched by name (`pg_partition_tree('t')`,
19637 // `pg_partition_ancestors('t')`). Same head-detection shape as
19638 // unnest; the engine executor owns the row shape per function.
19639 // v7.39 (read01 round 65) — and a USER function in FROM position
19640 // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19641 // (generate_series / unnest / the json_each family) keep it — their arms
19642 // sit further down, so they are excluded here by name rather than by
19643 // ordering. Anything else that is an ident followed by `(` is a table
19644 // function; the engine executor decides whether it is a builtin, a
19645 // set-returning user function, or an error.
19646 // 7.38.1 S5.1 — pg_dump spells its table functions
19647 // schema-qualified (`pg_catalog.pg_options_to_table(...)`);
19648 // strip the pg_catalog prefix here so the same head-detection
19649 // fires. Only pg_catalog: a user schema's `s.f(x)` keeps its
19650 // meaning.
19651 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
19652 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
19653 && matches!(
19654 self.tokens.get(self.pos + 2),
19655 Some(Token::Ident(_) | Token::QuotedIdent(_))
19656 )
19657 && matches!(self.tokens.get(self.pos + 3), Some(Token::LParen))
19658 {
19659 self.advance(); // pg_catalog
19660 self.advance(); // .
19661 }
19662 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19663 if !s.eq_ignore_ascii_case("generate_series")
19664 && !s.eq_ignore_ascii_case("unnest")
19665 && !is_json_each_name(s))
19666 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19667 {
19668 // Body out-of-line — this parse sits on the FROM/subquery
19669 // recursion chain (debug frame-cliff discipline).
19670 // v7.39 (read01 round 69) — a call whose arguments reference an outer
19671 // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19672 // outer row, so it rides the lateral channel. Same rule the unnest
19673 // arm uses.
19674 let tref = self.parse_table_fn_ref()?;
19675 let correlated = tref
19676 .table_fn_call
19677 .as_deref()
19678 .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19679 return Ok(if correlated {
19680 Self::wrap_correlated_srf(tref)
19681 } else {
19682 tref
19683 });
19684 }
19685 // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19686 // [, step])` set-returning source. Same shape as unnest:
19687 // detect at the head, parse the comma-separated arg list,
19688 // dispatch downstream through the engine's set-returning
19689 // path. Supports integer triplets (mailrs's `WITH row_no AS
19690 // (SELECT * FROM generate_series(1, N))` pattern) and
19691 // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19692 // date-range iteration pattern, which pre-3.10 had no
19693 // direct equivalent in SPG).
19694 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19695 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19696 {
19697 self.advance(); // generate_series
19698 self.advance(); // (
19699 let mut args: Vec<Expr> = Vec::new();
19700 loop {
19701 args.push(self.parse_expr(0)?);
19702 if matches!(self.peek(), Token::Comma) {
19703 self.advance();
19704 continue;
19705 }
19706 break;
19707 }
19708 if !matches!(self.peek(), Token::RParen) {
19709 return Err(self.err(alloc::format!(
19710 "expected ')' after generate_series() arguments, got {:?}",
19711 self.peek()
19712 )));
19713 }
19714 self.advance();
19715 if args.len() < 2 || args.len() > 3 {
19716 return Err(self.err(alloc::format!(
19717 "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19718 args.len()
19719 )));
19720 }
19721 let with_ordinality = self.absorb_with_ordinality();
19722 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19723 let name = alias_ident
19724 .clone()
19725 .unwrap_or_else(|| "generate_series".to_string());
19726 let correlated = args.iter().any(Self::expr_has_any_column);
19727 let tref = TableRef {
19728 name,
19729 alias: alias_ident,
19730 only: false,
19731 as_of_segment: None,
19732 unnest_expr: None,
19733 unnest_column_aliases: column_aliases,
19734 with_ordinality,
19735 generate_series_args: Some(args),
19736 lateral_subquery: None,
19737 jsonb_each_text_arg: None,
19738 table_fn_call: None,
19739 rows_from: None,
19740 json_table: None,
19741 scalar_fn_item: false,
19742 };
19743 return Ok(if correlated {
19744 Self::wrap_correlated_srf(tref)
19745 } else {
19746 tref
19747 });
19748 }
19749 // v7.16.2 — preserve information_schema / pg_catalog
19750 // qualifiers (mailrs round-10 A.3). The generic
19751 // `expect_ident_like` strip silently drops the schema;
19752 // we want the engine to recognise these PG meta tables
19753 // and synthesise rows from the live catalog. Produce a
19754 // synthetic name (`__spg_info_columns` etc.) so the
19755 // engine's SELECT-side router can dispatch without
19756 // clashing with any user-defined `columns` table.
19757 let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
19758 (synth, Some(orig))
19759 } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
19760 (synth, Some(orig))
19761 } else {
19762 (self.expect_ident_like()?, None)
19763 };
19764 // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
19765 // time-travel clause. Parse BEFORE the alias so the
19766 // alias can still ride at the tail (`tbl AS OF SEGMENT
19767 // '5' alias`). `AS` is a reserved keyword token, while
19768 // `OF` and `SEGMENT` are bare idents.
19769 let as_of_segment = if matches!(self.peek(), Token::As)
19770 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
19771 {
19772 self.advance(); // AS
19773 self.advance(); // OF
19774 let kw = match self.peek().clone() {
19775 Token::Ident(s) | Token::QuotedIdent(s) => s,
19776 other => {
19777 return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
19778 }
19779 };
19780 if !kw.eq_ignore_ascii_case("segment") {
19781 return Err(self.err(format!(
19782 "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
19783 )));
19784 }
19785 self.advance();
19786 // Segment id literal — accept either a string or
19787 // integer for operator ergonomics.
19788 let id = match self.advance() {
19789 Token::String(s) => s
19790 .parse::<u32>()
19791 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19792 Token::Integer(n) => u32::try_from(n)
19793 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19794 other => {
19795 return Err(self.err(format!(
19796 "expected segment id literal after AS OF SEGMENT, got {other:?}"
19797 )));
19798 }
19799 };
19800 Some(id)
19801 } else {
19802 None
19803 };
19804 // TABLESAMPLE is not a reserved token — keep the bare-ident
19805 // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
19806 let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
19807 {
19808 None
19809 } else {
19810 self.parse_optional_alias()?
19811 };
19812 // r1052 — a catalog name rewritten to its synthetic form keeps
19813 // the WRITTEN name as the relation's alias, so `pg_cast.oid`
19814 // still binds after `pg_cast` became `__spg_pg_cast`. PG
19815 // semantics: the visible name of `pg_catalog.pg_cast` IS
19816 // `pg_cast`. Without this, every table-name-qualified column
19817 // on a synthesised catalog answered "missing FROM-clause
19818 // entry" — which is the wall pg_dump hit on its first
19819 // pg_proc/pg_cast query.
19820 let alias = match (&alias, &meta_original) {
19821 (None, Some(orig)) if *orig != name => Some(orig.clone()),
19822 _ => alias,
19823 };
19824 // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
19825 // (PG grammar). BERNOULLI lowers to a per-row
19826 // `random() < p/100` conjunct on the enclosing SELECT's
19827 // WHERE — exact row-level Bernoulli semantics. SYSTEM
19828 // shares the lowering: SPG has no page structure to
19829 // sample, and the row-level form returns the same expected
19830 // fraction. REPEATABLE(seed) promises a deterministic
19831 // sample SPG cannot honour yet — honest error rather than
19832 // a silently ignored seed.
19833 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
19834 self.advance();
19835 let method = self.expect_ident_like()?;
19836 if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
19837 return Err(self.err(alloc::format!(
19838 "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
19839 )));
19840 }
19841 if !matches!(self.peek(), Token::LParen) {
19842 return Err(self.err(alloc::format!(
19843 "expected '(' after TABLESAMPLE {}, got {:?}",
19844 method.to_ascii_uppercase(),
19845 self.peek()
19846 )));
19847 }
19848 self.advance();
19849 let percent = self.parse_expr(0)?;
19850 if !matches!(self.peek(), Token::RParen) {
19851 return Err(self.err(alloc::format!(
19852 "expected ')' after TABLESAMPLE percentage, got {:?}",
19853 self.peek()
19854 )));
19855 }
19856 self.advance();
19857 // REPEATABLE(seed) → a deterministic per-row draw seeded by
19858 // `seed`, so the sample is stable across repeats and rescans.
19859 // Non-REPEATABLE keeps the non-deterministic `random()` draw.
19860 let mut sample_seed: Option<Expr> = None;
19861 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
19862 self.advance();
19863 if !matches!(self.peek(), Token::LParen) {
19864 return Err(self.err(alloc::format!(
19865 "expected '(' after REPEATABLE, got {:?}",
19866 self.peek()
19867 )));
19868 }
19869 self.advance();
19870 let seed = self.parse_expr(0)?;
19871 if !matches!(self.peek(), Token::RParen) {
19872 return Err(self.err(alloc::format!(
19873 "expected ')' after REPEATABLE seed, got {:?}",
19874 self.peek()
19875 )));
19876 }
19877 self.advance();
19878 sample_seed = Some(seed);
19879 }
19880 let draw = match sample_seed {
19881 Some(seed) => Expr::FunctionCall {
19882 name: "__tsm_fract".to_string(),
19883 args: alloc::vec![seed],
19884 },
19885 None => Expr::FunctionCall {
19886 name: "random".to_string(),
19887 args: Vec::new(),
19888 },
19889 };
19890 self.pending_sample_preds.push(Expr::Binary {
19891 lhs: Box::new(draw),
19892 op: crate::ast::BinOp::Lt,
19893 rhs: Box::new(Expr::Binary {
19894 lhs: Box::new(percent),
19895 op: crate::ast::BinOp::Div,
19896 rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
19897 }),
19898 });
19899 }
19900 Ok(TableRef {
19901 name,
19902 alias,
19903 only,
19904 as_of_segment,
19905 unnest_expr: None,
19906 unnest_column_aliases: Vec::new(),
19907 with_ordinality: false,
19908 generate_series_args: None,
19909 lateral_subquery: None,
19910 jsonb_each_text_arg: None,
19911 table_fn_call: None,
19912 rows_from: None,
19913 json_table: None,
19914 scalar_fn_item: false,
19915 })
19916 }
19917
19918 /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
19919 /// but also accepts `AS alias(col [, col, …])` — the
19920 /// PG-standard table-function column-list form. The column
19921 /// list is only honoured when paired with `UNNEST(...)` in
19922 /// the parent; other call sites currently discard it.
19923 /// True when the expression tree contains a qualified column
19924 /// reference (`t.col`) — the syntactic marker that an SRF
19925 /// argument correlates with a preceding FROM item.
19926 fn expr_has_qualified_column(e: &Expr) -> bool {
19927 match e {
19928 Expr::Column(c) => c.qualifier.is_some(),
19929 Expr::Binary { lhs, rhs, .. } => {
19930 Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
19931 }
19932 Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
19933 Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
19934 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
19935 Expr::Case {
19936 operand,
19937 branches,
19938 else_branch,
19939 } => {
19940 operand
19941 .as_deref()
19942 .is_some_and(Self::expr_has_qualified_column)
19943 || branches.iter().any(|(w, t)| {
19944 Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
19945 })
19946 || else_branch
19947 .as_deref()
19948 .is_some_and(Self::expr_has_qualified_column)
19949 }
19950 _ => false,
19951 }
19952 }
19953
19954 /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
19955 /// counts a bare (unqualified) column. A set-returning function has no
19956 /// input columns of its own, so ANY column in its arguments is an outer
19957 /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
19958 fn expr_has_any_column(e: &Expr) -> bool {
19959 match e {
19960 Expr::Column(_) => true,
19961 Expr::Binary { lhs, rhs, .. } => {
19962 Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
19963 }
19964 Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
19965 Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
19966 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
19967 // v7.39 (round 759, F31-B8b) — a column INSIDE an array
19968 // constructor or subscript fell to the `_ => false` arm, so
19969 // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
19970 // channel and the eager peer eval answered `column "x" does
19971 // not exist` (the substitution walker already recurses both
19972 // shapes; only this detector was blind to them).
19973 Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
19974 Expr::ArraySubscript { target, index } => {
19975 Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
19976 }
19977 Expr::Case {
19978 operand,
19979 branches,
19980 else_branch,
19981 } => {
19982 operand.as_deref().is_some_and(Self::expr_has_any_column)
19983 || branches
19984 .iter()
19985 .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
19986 || else_branch
19987 .as_deref()
19988 .is_some_and(Self::expr_has_any_column)
19989 }
19990 _ => false,
19991 }
19992 }
19993
19994 /// Wrap a correlated SRF table ref (`unnest(t.col)` /
19995 /// `generate_series(1, t.n)`) into the lateral_subquery
19996 /// channel: `SELECT * FROM <srf>` executes per outer row with
19997 /// outer references substituted (v7.37.43-T4.5 machinery).
19998 /// Uncorrelated SRFs stay on their plain channels.
19999 fn wrap_correlated_srf(srf: TableRef) -> TableRef {
20000 let name = srf.name.clone();
20001 let alias = srf.alias.clone();
20002 let inner = crate::ast::SelectStatement {
20003 locking: None,
20004 ctes: Vec::new(),
20005 distinct: false,
20006 distinct_on: Vec::new(),
20007 items: alloc::vec![crate::ast::SelectItem::Wildcard],
20008 from: Some(crate::ast::FromClause {
20009 primary: srf,
20010 joins: Vec::new(),
20011 }),
20012 where_: None,
20013 group_by: None,
20014 group_by_all: false,
20015 having: None,
20016 unions: Vec::new(),
20017 order_by: Vec::new(),
20018 limit: None,
20019 offset: None,
20020 limit_with_ties: false,
20021 window_check_exprs: Vec::new(),
20022 };
20023 TableRef {
20024 name,
20025 alias,
20026 only: false,
20027 as_of_segment: None,
20028 unnest_expr: None,
20029 unnest_column_aliases: Vec::new(),
20030 with_ordinality: false,
20031 generate_series_args: None,
20032 lateral_subquery: Some(Box::new(inner)),
20033 jsonb_each_text_arg: None,
20034 table_fn_call: None,
20035 rows_from: None,
20036 json_table: None,
20037 scalar_fn_item: false,
20038 }
20039 }
20040
20041 /// True when the expression tree contains an unresolved
20042 /// `OVER w` marker (see parse_over_clause).
20043 fn expr_has_named_window(e: &Expr) -> bool {
20044 match e {
20045 Expr::WindowFunction { partition_by, .. } => matches!(
20046 partition_by.as_slice(),
20047 [Expr::Column(c)] if matches!(
20048 c.qualifier.as_deref(),
20049 Some("__named_window__") | Some("__named_window_ref__")
20050 )
20051 ),
20052 Expr::Binary { lhs, rhs, .. } => {
20053 Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
20054 }
20055 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
20056 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
20057 Expr::Case {
20058 operand,
20059 branches,
20060 else_branch,
20061 } => {
20062 operand.as_deref().is_some_and(Self::expr_has_named_window)
20063 || branches.iter().any(|(w, t)| {
20064 Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
20065 })
20066 || else_branch
20067 .as_deref()
20068 .is_some_and(Self::expr_has_named_window)
20069 }
20070 _ => false,
20071 }
20072 }
20073
20074 /// v7.39 (round 705) — the NAMES the expression references through the
20075 /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
20076 /// definitions nothing referenced. Traversal mirrors
20077 /// `expr_has_named_window` above.
20078 fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
20079 match e {
20080 Expr::WindowFunction { partition_by, .. } => {
20081 if let [Expr::Column(c)] = partition_by.as_slice()
20082 && matches!(
20083 c.qualifier.as_deref(),
20084 Some("__named_window__") | Some("__named_window_ref__")
20085 )
20086 {
20087 into.push(c.name.clone());
20088 }
20089 }
20090 Expr::Binary { lhs, rhs, .. } => {
20091 Self::collect_named_window_refs(lhs, into);
20092 Self::collect_named_window_refs(rhs, into);
20093 }
20094 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20095 Self::collect_named_window_refs(expr, into);
20096 }
20097 Expr::FunctionCall { args, .. } => {
20098 for a in args {
20099 Self::collect_named_window_refs(a, into);
20100 }
20101 }
20102 Expr::Case {
20103 operand,
20104 branches,
20105 else_branch,
20106 } => {
20107 if let Some(o) = operand.as_deref() {
20108 Self::collect_named_window_refs(o, into);
20109 }
20110 for (w, t) in branches {
20111 Self::collect_named_window_refs(w, into);
20112 Self::collect_named_window_refs(t, into);
20113 }
20114 if let Some(eb) = else_branch.as_deref() {
20115 Self::collect_named_window_refs(eb, into);
20116 }
20117 }
20118 _ => {}
20119 }
20120 }
20121
20122 /// Inline named-window definitions into the `OVER w` markers.
20123 /// An unknown name errors (PG: window "w" does not exist).
20124 #[allow(clippy::type_complexity)]
20125 fn substitute_named_windows(
20126 e: &mut Expr,
20127 defs: &[(
20128 String,
20129 (
20130 Vec<Expr>,
20131 Vec<(Expr, bool, Option<bool>)>,
20132 Option<WindowFrame>,
20133 ),
20134 )],
20135 ) -> Result<(), String> {
20136 match e {
20137 Expr::WindowFunction {
20138 partition_by,
20139 order_by,
20140 frame,
20141 ..
20142 } => {
20143 // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
20144 // from the bare `OVER w1` (a plain reference).
20145 let named = match partition_by.as_slice() {
20146 [Expr::Column(c)] => match c.qualifier.as_deref() {
20147 Some("__named_window__") => Some((c.name.clone(), false)),
20148 Some("__named_window_ref__") => Some((c.name.clone(), true)),
20149 _ => None,
20150 },
20151 _ => None,
20152 };
20153 if let Some((wname, is_copy)) = named {
20154 let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
20155 else {
20156 return Err(alloc::format!("window {wname:?} does not exist"));
20157 };
20158 if !is_copy {
20159 *partition_by = def.0.clone();
20160 *order_by = def.1.clone();
20161 *frame = def.2.clone();
20162 return Ok(());
20163 }
20164 // v7.39 (round 229) — PG's copy rules, probed against
20165 // 18.4: a copy inherits the partitioning, may supply an
20166 // ordering only when the base has none, and may not copy
20167 // a base that already carries a frame (its own frame
20168 // would be ambiguous with the inherited one).
20169 if !def.1.is_empty() && !order_by.is_empty() {
20170 return Err(alloc::format!(
20171 "cannot override ORDER BY clause of window \"{wname}\""
20172 ));
20173 }
20174 if def.2.is_some() {
20175 return Err(alloc::format!(
20176 "cannot copy window \"{wname}\" because it has a frame clause"
20177 ));
20178 }
20179 *partition_by = def.0.clone();
20180 if order_by.is_empty() {
20181 *order_by = def.1.clone();
20182 }
20183 }
20184 Ok(())
20185 }
20186 Expr::Binary { lhs, rhs, .. } => {
20187 Self::substitute_named_windows(lhs, defs)?;
20188 Self::substitute_named_windows(rhs, defs)
20189 }
20190 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20191 Self::substitute_named_windows(expr, defs)
20192 }
20193 Expr::FunctionCall { args, .. } => {
20194 for a in args {
20195 Self::substitute_named_windows(a, defs)?;
20196 }
20197 Ok(())
20198 }
20199 Expr::Case {
20200 operand,
20201 branches,
20202 else_branch,
20203 } => {
20204 if let Some(op) = operand {
20205 Self::substitute_named_windows(op, defs)?;
20206 }
20207 for (w, t) in branches {
20208 Self::substitute_named_windows(w, defs)?;
20209 Self::substitute_named_windows(t, defs)?;
20210 }
20211 if let Some(el) = else_branch {
20212 Self::substitute_named_windows(el, defs)?;
20213 }
20214 Ok(())
20215 }
20216 _ => Ok(()),
20217 }
20218 }
20219
20220 /// SQL-standard `TABLE name` shorthand — builds the equivalent
20221 /// `SELECT * FROM name` head. Callers own set-op chain / tail
20222 /// composition.
20223 fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
20224 debug_assert!(matches!(self.peek(), Token::Table));
20225 self.advance(); // TABLE
20226 let tname = self.expect_ident_like()?;
20227 Ok(SelectStatement {
20228 locking: None,
20229 ctes: Vec::new(),
20230 distinct: false,
20231 distinct_on: Vec::new(),
20232 items: alloc::vec![SelectItem::Wildcard],
20233 from: Some(FromClause {
20234 primary: TableRef {
20235 name: tname,
20236 alias: None,
20237 only: false,
20238 as_of_segment: None,
20239 unnest_expr: None,
20240 unnest_column_aliases: Vec::new(),
20241 with_ordinality: false,
20242 generate_series_args: None,
20243 lateral_subquery: None,
20244 jsonb_each_text_arg: None,
20245 table_fn_call: None,
20246 rows_from: None,
20247 json_table: None,
20248 scalar_fn_item: false,
20249 },
20250 joins: Vec::new(),
20251 }),
20252 where_: None,
20253 group_by: None,
20254 group_by_all: false,
20255 having: None,
20256 unions: Vec::new(),
20257 order_by: Vec::new(),
20258 limit: None,
20259 offset: None,
20260 limit_with_ties: false,
20261 window_check_exprs: Vec::new(),
20262 })
20263 }
20264
20265 /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
20266 /// variants) → a derived table that reads each declared column out of
20267 /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
20268 /// `jsonb_array_elements(J)` (one row per element, column `value`);
20269 /// the scalar *record form projects a single row straight off `J`.
20270 /// Rides the existing lateral-subquery channel, so no new executor or
20271 /// AST is needed.
20272 fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
20273 use crate::ast::{
20274 BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
20275 };
20276 let fn_name = match self.peek() {
20277 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20278 _ => unreachable!("caller guarded is_json_to_record_name"),
20279 };
20280 self.advance(); // fn name
20281 self.advance(); // (
20282 let mut arg = self.parse_expr(0)?;
20283 // populate_record(base, json): the base only carries the record
20284 // type here — the JSON argument is the second expression.
20285 let mut base: Option<Expr> = None;
20286 if matches!(self.peek(), Token::Comma) {
20287 self.advance();
20288 base = Some(arg);
20289 arg = self.parse_expr(0)?;
20290 }
20291 if !matches!(self.peek(), Token::RParen) {
20292 return Err(self.err(alloc::format!(
20293 "expected ')' after {fn_name}() argument, got {:?}",
20294 self.peek()
20295 )));
20296 }
20297 self.advance(); // )
20298 let is_set = fn_name.ends_with("recordset");
20299 // `[AS] alias ( col type [, …] )` column-definition list.
20300 if matches!(self.peek(), Token::As) {
20301 self.advance();
20302 }
20303 let alias_opt = match self.peek() {
20304 Token::Ident(s) | Token::QuotedIdent(s) => {
20305 let a = s.clone();
20306 self.advance();
20307 Some(a)
20308 }
20309 _ => None,
20310 };
20311 // v7.39 (read01 round 76) — the populate family's canonical PG
20312 // spelling carries no column list at all: the row shape comes from
20313 // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
20314 // j)`). The parser has no catalog, so hand the two arguments to the
20315 // engine's table-function channel, which does. Only `*_to_record*`
20316 // (whose base is bare `record`) genuinely requires the list.
20317 if !matches!(self.peek(), Token::LParen) {
20318 if let Some(base_expr) = base {
20319 let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
20320 return Ok(TableRef {
20321 name: alias.clone(),
20322 alias: Some(alias),
20323 only: false,
20324 as_of_segment: None,
20325 unnest_expr: None,
20326 unnest_column_aliases: Vec::new(),
20327 with_ordinality: false,
20328 generate_series_args: None,
20329 lateral_subquery: None,
20330 jsonb_each_text_arg: None,
20331 table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
20332 rows_from: None,
20333 json_table: None,
20334 scalar_fn_item: false,
20335 });
20336 }
20337 return Err(self.err(alloc::format!(
20338 "expected '(' to start the {fn_name} column-definition list, got {:?}",
20339 self.peek()
20340 )));
20341 }
20342 let Some(alias) = alias_opt else {
20343 return Err(self.err(alloc::format!(
20344 "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
20345 )));
20346 };
20347 self.advance(); // (
20348 let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
20349 loop {
20350 let col = self.expect_ident_like()?;
20351 let ty = self.parse_cast_target()?;
20352 coldefs.push((col, ty));
20353 if matches!(self.peek(), Token::Comma) {
20354 self.advance();
20355 continue;
20356 }
20357 if matches!(self.peek(), Token::RParen) {
20358 self.advance();
20359 break;
20360 }
20361 return Err(self.err(alloc::format!(
20362 "expected ',' or ')' in {fn_name} column list, got {:?}",
20363 self.peek()
20364 )));
20365 }
20366 if coldefs.is_empty() {
20367 return Err(self.err(alloc::format!(
20368 "{fn_name} column-definition list must declare at least one column"
20369 )));
20370 }
20371 // Per column: (base ->> 'col')::type AS col. The base is the
20372 // per-element `value` column for the *set form, or the argument
20373 // itself for the scalar record form.
20374 let items: Vec<SelectItem> = coldefs
20375 .into_iter()
20376 .map(|(col, ty)| {
20377 let base = if is_set {
20378 Expr::Column(ColumnName {
20379 qualifier: None,
20380 name: "value".to_string(),
20381 })
20382 } else {
20383 arg.clone()
20384 };
20385 SelectItem::Expr {
20386 expr: Expr::Cast {
20387 expr: Box::new(Expr::Binary {
20388 lhs: Box::new(base),
20389 op: BinOp::JsonGetText,
20390 rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
20391 }),
20392 target: ty,
20393 },
20394 alias: Some(col),
20395 }
20396 })
20397 .collect();
20398 let from = if is_set {
20399 let elem_fn = if fn_name.starts_with("jsonb") {
20400 "jsonb_array_elements"
20401 } else {
20402 "json_array_elements"
20403 };
20404 Some(FromClause {
20405 primary: TableRef {
20406 name: "value".to_string(),
20407 alias: None,
20408 only: false,
20409 as_of_segment: None,
20410 unnest_expr: Some(Box::new(Expr::FunctionCall {
20411 name: elem_fn.to_string(),
20412 args: alloc::vec![arg],
20413 })),
20414 unnest_column_aliases: alloc::vec!["value".to_string()],
20415 with_ordinality: false,
20416 generate_series_args: None,
20417 lateral_subquery: None,
20418 jsonb_each_text_arg: None,
20419 table_fn_call: None,
20420 rows_from: None,
20421 json_table: None,
20422 scalar_fn_item: false,
20423 },
20424 joins: Vec::new(),
20425 })
20426 } else {
20427 None
20428 };
20429 let inner = SelectStatement {
20430 locking: None,
20431 ctes: Vec::new(),
20432 distinct: false,
20433 distinct_on: Vec::new(),
20434 items,
20435 from,
20436 where_: None,
20437 group_by: None,
20438 group_by_all: false,
20439 having: None,
20440 unions: Vec::new(),
20441 order_by: Vec::new(),
20442 limit: None,
20443 offset: None,
20444 limit_with_ties: false,
20445 window_check_exprs: Vec::new(),
20446 };
20447 Ok(TableRef {
20448 name: alias.clone(),
20449 alias: Some(alias),
20450 only: false,
20451 as_of_segment: None,
20452 unnest_expr: None,
20453 unnest_column_aliases: Vec::new(),
20454 with_ordinality: false,
20455 generate_series_args: None,
20456 lateral_subquery: Some(Box::new(inner)),
20457 jsonb_each_text_arg: None,
20458 table_fn_call: None,
20459 rows_from: None,
20460 json_table: None,
20461 scalar_fn_item: false,
20462 })
20463 }
20464
20465 /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
20466 /// Returns true when the clause was present. `WITH` alone (a
20467 /// CTE can never start here) is not enough — the ORDINALITY
20468 /// ident must follow, so a stray WITH still errors downstream.
20469 fn absorb_with_ordinality(&mut self) -> bool {
20470 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
20471 && matches!(self.tokens.get(self.pos + 1),
20472 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
20473 {
20474 self.advance();
20475 self.advance();
20476 true
20477 } else {
20478 false
20479 }
20480 }
20481
20482 /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
20483 /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
20484 /// Out-of-line: the caller sits on the FROM recursion chain.
20485 #[inline(never)]
20486 fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
20487 let fn_name = match self.advance() {
20488 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20489 _ => unreachable!("caller peeked an ident"),
20490 };
20491 self.advance(); // (
20492 let mut args: Vec<Expr> = Vec::new();
20493 if !matches!(self.peek(), Token::RParen) {
20494 loop {
20495 args.push(self.parse_expr(0)?);
20496 if matches!(self.peek(), Token::Comma) {
20497 self.advance();
20498 continue;
20499 }
20500 break;
20501 }
20502 }
20503 if !matches!(self.peek(), Token::RParen) {
20504 return Err(self.err(alloc::format!(
20505 "expected ')' after {fn_name}() arguments, got {:?}",
20506 self.peek()
20507 )));
20508 }
20509 self.advance();
20510 // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20511 // counter column rides after the function's own, and the alias list
20512 // names it.
20513 let with_ordinality = self.absorb_with_ordinality();
20514 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20515 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20516 Ok(TableRef {
20517 name,
20518 alias: alias_ident,
20519 only: false,
20520 as_of_segment: None,
20521 unnest_expr: None,
20522 unnest_column_aliases,
20523 with_ordinality,
20524 generate_series_args: None,
20525 lateral_subquery: None,
20526 jsonb_each_text_arg: None,
20527 table_fn_call: Some(Box::new((fn_name, args))),
20528 rows_from: None,
20529 json_table: None,
20530 scalar_fn_item: false,
20531 })
20532 }
20533
20534 /// v7.39 (round 205, JSON_TABLE) — parse
20535 /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20536 /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20537 /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20538 #[inline(never)]
20539 fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20540 self.advance(); // json_table
20541 self.advance(); // (
20542 let doc = Box::new(self.parse_expr(0)?);
20543 self.expect_comma_json_table()?;
20544 let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20545 // Optional `PASSING <expr> AS <name> [, …]`.
20546 let mut passing: Vec<(String, Expr)> = Vec::new();
20547 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20548 self.advance();
20549 loop {
20550 let e = self.parse_expr(0)?;
20551 if !matches!(self.peek(), Token::As) {
20552 return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20553 }
20554 self.advance();
20555 let vname = match self.advance() {
20556 Token::Ident(s) | Token::QuotedIdent(s) => s,
20557 other => {
20558 return Err(self.err(alloc::format!(
20559 "expected PASSING variable name, got {other:?}"
20560 )));
20561 }
20562 };
20563 passing.push((vname, e));
20564 if matches!(self.peek(), Token::Comma) {
20565 self.advance();
20566 continue;
20567 }
20568 break;
20569 }
20570 }
20571 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20572 return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20573 }
20574 self.advance();
20575 let columns = self.parse_json_table_columns()?;
20576 if !matches!(self.peek(), Token::RParen) {
20577 return Err(self.err(alloc::format!(
20578 "expected ')' to close JSON_TABLE, got {:?}",
20579 self.peek()
20580 )));
20581 }
20582 self.advance();
20583 let alias_ident = self.parse_optional_alias()?;
20584 let name = alias_ident
20585 .clone()
20586 .unwrap_or_else(|| String::from("json_table"));
20587 Ok(TableRef {
20588 name,
20589 alias: alias_ident,
20590 only: false,
20591 as_of_segment: None,
20592 unnest_expr: None,
20593 unnest_column_aliases: Vec::new(),
20594 with_ordinality: false,
20595 generate_series_args: None,
20596 lateral_subquery: None,
20597 jsonb_each_text_arg: None,
20598 table_fn_call: None,
20599 rows_from: None,
20600 json_table: Some(Box::new(crate::ast::JsonTable {
20601 doc,
20602 row_path,
20603 columns,
20604 passing,
20605 })),
20606 scalar_fn_item: false,
20607 })
20608 }
20609
20610 fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20611 if !matches!(self.peek(), Token::Comma) {
20612 return Err(self.err(alloc::format!(
20613 "expected ',' after JSON_TABLE document, got {:?}",
20614 self.peek()
20615 )));
20616 }
20617 self.advance();
20618 Ok(())
20619 }
20620
20621 fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20622 match self.advance() {
20623 Token::String(s) => Ok(s),
20624 other => Err(self.err(alloc::format!(
20625 "expected {what} string literal, got {other:?}"
20626 ))),
20627 }
20628 }
20629
20630 /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20631 #[inline(never)]
20632 fn parse_json_table_columns(
20633 &mut self,
20634 ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20635 if !matches!(self.peek(), Token::LParen) {
20636 return Err(self.err("expected '(' after COLUMNS".into()));
20637 }
20638 self.advance();
20639 let mut cols = Vec::new();
20640 loop {
20641 cols.push(self.parse_json_table_one_column()?);
20642 if matches!(self.peek(), Token::Comma) {
20643 self.advance();
20644 continue;
20645 }
20646 break;
20647 }
20648 if !matches!(self.peek(), Token::RParen) {
20649 return Err(self.err(alloc::format!(
20650 "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20651 self.peek()
20652 )));
20653 }
20654 self.advance();
20655 Ok(cols)
20656 }
20657
20658 fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20659 use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20660 // NESTED [PATH] '<p>' COLUMNS (...)
20661 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20662 self.advance();
20663 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20664 self.advance();
20665 }
20666 let path = self.parse_json_string_literal("NESTED PATH")?;
20667 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20668 return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20669 }
20670 self.advance();
20671 let columns = self.parse_json_table_columns()?;
20672 return Ok(JsonTableColumn::Nested { path, columns });
20673 }
20674 // <name> ...
20675 let name = match self.advance() {
20676 Token::Ident(s) | Token::QuotedIdent(s) => s,
20677 other => {
20678 return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20679 }
20680 };
20681 // <name> FOR ORDINALITY
20682 if matches!(self.peek(), Token::For) {
20683 self.advance();
20684 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20685 return Err(self.err("expected ORDINALITY after FOR".into()));
20686 }
20687 self.advance();
20688 return Ok(JsonTableColumn::Ordinality { name });
20689 }
20690 // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20691 let ty = self.parse_column_type_name()?;
20692 let mut format_json = false;
20693 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20694 self.advance();
20695 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20696 return Err(self.err("expected JSON after FORMAT".into()));
20697 }
20698 self.advance();
20699 format_json = true;
20700 }
20701 let mut exists = false;
20702 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20703 self.advance();
20704 exists = true;
20705 }
20706 let mut path = alloc::format!("$.{name}");
20707 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20708 self.advance();
20709 path = self.parse_json_string_literal("column PATH")?;
20710 }
20711 if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20712 // `FORMAT JSON` after PATH (alternate placement).
20713 self.advance();
20714 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20715 self.advance();
20716 }
20717 format_json = true;
20718 }
20719 let mut wrapper = false;
20720 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20721 self.advance();
20722 // optional CONDITIONAL/UNCONDITIONAL
20723 if matches!(self.peek(), Token::Ident(s)
20724 if s.eq_ignore_ascii_case("unconditional")
20725 || s.eq_ignore_ascii_case("conditional"))
20726 {
20727 self.advance();
20728 }
20729 if !matches!(self.peek(), Token::Ident(s)
20730 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20731 {
20732 return Err(self.err("expected WRAPPER after WITH".into()));
20733 }
20734 self.advance();
20735 // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20736 if matches!(self.peek(), Token::Ident(s)
20737 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20738 {
20739 self.advance();
20740 }
20741 wrapper = true;
20742 }
20743 // ON EMPTY / ON ERROR clauses (two, in any order).
20744 let mut on_empty = JsonTableOnBehavior::Null;
20745 let mut on_error = JsonTableOnBehavior::Null;
20746 for _ in 0..2 {
20747 let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20748 {
20749 self.advance();
20750 Some(JsonTableOnBehavior::Error)
20751 } else if matches!(self.peek(), Token::Null) {
20752 self.advance();
20753 Some(JsonTableOnBehavior::Null)
20754 } else if matches!(self.peek(), Token::Default) {
20755 self.advance();
20756 Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
20757 } else {
20758 None
20759 };
20760 let Some(behavior) = behavior else { break };
20761 // `ON {EMPTY|ERROR}`
20762 if !matches!(self.peek(), Token::On) {
20763 return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
20764 }
20765 self.advance();
20766 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
20767 self.advance();
20768 on_empty = behavior;
20769 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
20770 self.advance();
20771 on_error = behavior;
20772 } else {
20773 return Err(self.err("expected EMPTY or ERROR after ON".into()));
20774 }
20775 }
20776 Ok(JsonTableColumn::Regular {
20777 name,
20778 ty,
20779 path,
20780 exists,
20781 format_json,
20782 wrapper,
20783 on_empty,
20784 on_error,
20785 })
20786 }
20787
20788 fn parse_optional_alias_with_columns(
20789 &mut self,
20790 ) -> Result<(Option<String>, Vec<String>), ParseError> {
20791 let alias = self.parse_optional_alias()?;
20792 if alias.is_none() {
20793 return Ok((None, Vec::new()));
20794 }
20795 let mut cols: Vec<String> = Vec::new();
20796 if matches!(self.peek(), Token::LParen) {
20797 self.advance();
20798 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
20799 self.advance();
20800 cols.push(s);
20801 if matches!(self.peek(), Token::Comma) {
20802 self.advance();
20803 continue;
20804 }
20805 break;
20806 }
20807 if matches!(self.peek(), Token::RParen) {
20808 self.advance();
20809 }
20810 }
20811 Ok((alias, cols))
20812 }
20813
20814 /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
20815 /// whose keyword token was already consumed and whose `(` is the
20816 /// current token. Factored out of `parse_atom` (and marked
20817 /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
20818 /// recursive `parse_atom` frame — inlining them there enlarges the
20819 /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
20820 /// against, risking an overflow before the budget triggers.
20821 #[inline(never)]
20822 fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
20823 self.advance(); // (
20824 let mut args = Vec::new();
20825 if !matches!(self.peek(), Token::RParen) {
20826 loop {
20827 args.push(self.parse_expr(0)?);
20828 match self.peek() {
20829 Token::Comma => {
20830 self.advance();
20831 }
20832 Token::RParen => break,
20833 other => {
20834 return Err(self.err(alloc::format!(
20835 "expected ',' or ')' in {name}() args, got {other:?}"
20836 )));
20837 }
20838 }
20839 }
20840 }
20841 self.advance(); // )
20842 Ok(Expr::FunctionCall {
20843 name: name.into(),
20844 args,
20845 })
20846 }
20847
20848 /// FROM-clause: a primary table reference plus zero-or-more joined
20849 /// peers expressed via either `, <table>` (cross-product, no ON) or
20850 /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
20851 /// v1.10 keeps the join list flat (left-associative nested-loop
20852 /// semantics).
20853 fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
20854 let primary = self.parse_table_ref()?;
20855 let primary_qual = primary
20856 .alias
20857 .clone()
20858 .unwrap_or_else(|| primary.name.clone());
20859 let joins = self.parse_from_joins(&primary_qual)?;
20860 Ok(FromClause { primary, joins })
20861 }
20862
20863 /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
20864 /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
20865 /// SAME grammar after its target table has already been consumed.
20866 /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
20867 /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
20868 /// be parsed forward, once.)
20869 /// `left_primary_qual` is the qualifier (alias, else name) of whatever
20870 /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
20871 /// target in the MySQL multi-table form. It only feeds the `USING (…)`
20872 /// desugaring, which needs a name for the left side of each equality.
20873 fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
20874 let mut joins = Vec::new();
20875 loop {
20876 // `, <table>` — cross-product with no ON.
20877 if matches!(self.peek(), Token::Comma) {
20878 self.advance();
20879 let table = self.parse_table_ref()?;
20880 joins.push(FromJoin {
20881 kind: JoinKind::Cross,
20882 table,
20883 on: None,
20884 using_cols: None,
20885 natural: false,
20886 });
20887 continue;
20888 }
20889 // v7.37.16 — optional leading `NATURAL` before the join
20890 // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
20891 // not a lexer keyword (it arrives as a bare Ident), so match
20892 // it case-insensitively here. When present, no ON/USING
20893 // clause is allowed — the common columns are resolved at
20894 // execution time.
20895 let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
20896 if natural {
20897 self.advance();
20898 }
20899 // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
20900 // CROSS JOIN, and bare JOIN (defaults to INNER).
20901 let kind =
20902 match self.peek() {
20903 Token::Inner => {
20904 self.advance();
20905 if !matches!(self.peek(), Token::Join) {
20906 return Err(self
20907 .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
20908 }
20909 self.advance();
20910 JoinKind::Inner
20911 }
20912 Token::Left => {
20913 self.advance();
20914 if matches!(self.peek(), Token::Outer) {
20915 self.advance();
20916 }
20917 if !matches!(self.peek(), Token::Join) {
20918 return Err(self.err(format!(
20919 "expected JOIN after LEFT [OUTER], got {:?}",
20920 self.peek()
20921 )));
20922 }
20923 self.advance();
20924 JoinKind::Left
20925 }
20926 Token::Cross => {
20927 self.advance();
20928 if !matches!(self.peek(), Token::Join) {
20929 return Err(self
20930 .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
20931 }
20932 self.advance();
20933 JoinKind::Cross
20934 }
20935 // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
20936 Token::Right => {
20937 self.advance();
20938 if matches!(self.peek(), Token::Outer) {
20939 self.advance();
20940 }
20941 if !matches!(self.peek(), Token::Join) {
20942 return Err(self.err(format!(
20943 "expected JOIN after RIGHT [OUTER], got {:?}",
20944 self.peek()
20945 )));
20946 }
20947 self.advance();
20948 JoinKind::Right
20949 }
20950 // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
20951 Token::Full => {
20952 self.advance();
20953 if matches!(self.peek(), Token::Outer) {
20954 self.advance();
20955 }
20956 if !matches!(self.peek(), Token::Join) {
20957 return Err(self.err(format!(
20958 "expected JOIN after FULL [OUTER], got {:?}",
20959 self.peek()
20960 )));
20961 }
20962 self.advance();
20963 JoinKind::FullOuter
20964 }
20965 Token::Join => {
20966 self.advance();
20967 JoinKind::Inner
20968 }
20969 _ => break,
20970 };
20971 let table = self.parse_table_ref()?;
20972 // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
20973 // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
20974 // where prev_table is the most-recent left-side table
20975 // (the previous join's table if any, else the FROM primary).
20976 // PG semantics around column merging are richer (USING'd
20977 // cols become deduplicated single output columns); for
20978 // sugar purposes the predicate-only form covers the
20979 // baseline corpus shape and chained `… JOIN x USING (k)
20980 // JOIN y USING (k)` calls.
20981 // v7.37.16 — NATURAL joins carry no ON/USING clause; the
20982 // common columns resolve at execution time.
20983 if natural {
20984 joins.push(FromJoin {
20985 kind,
20986 table,
20987 on: None,
20988 using_cols: None,
20989 natural: true,
20990 });
20991 continue;
20992 }
20993 let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
20994 // v7.37.16 — capture the USING column list (in addition to
20995 // the ON desugar below) so the executor can perform PG's
20996 // column-merge on the output side.
20997 let mut using_cols: Option<Vec<String>> = None;
20998 let on = if matches!(self.peek(), Token::On) {
20999 self.advance();
21000 Some(self.parse_expr(0)?)
21001 } else if using_match {
21002 self.advance();
21003 if !matches!(self.peek(), Token::LParen) {
21004 return Err(
21005 self.err(format!("expected '(' after USING, got {:?}", self.peek()))
21006 );
21007 }
21008 self.advance();
21009 let mut cols: Vec<String> = Vec::new();
21010 loop {
21011 match self.peek().clone() {
21012 Token::Ident(s) | Token::QuotedIdent(s) => {
21013 self.advance();
21014 cols.push(s);
21015 }
21016 other => {
21017 return Err(self.err(format!(
21018 "expected column name inside USING (…), got {other:?}"
21019 )));
21020 }
21021 }
21022 match self.peek() {
21023 Token::Comma => {
21024 self.advance();
21025 continue;
21026 }
21027 Token::RParen => {
21028 self.advance();
21029 break;
21030 }
21031 other => {
21032 return Err(self.err(format!(
21033 "expected ',' or ')' inside USING (…), got {other:?}"
21034 )));
21035 }
21036 }
21037 }
21038 if cols.is_empty() {
21039 return Err(self.err("USING (…) requires at least one column".to_string()));
21040 }
21041 using_cols = Some(cols.clone());
21042 // Pick the left-side alias: prev join's table if any,
21043 // else FROM primary. Use alias when present, else
21044 // table name (PG-equivalent qualifier).
21045 let left_qual: String = joins
21046 .last()
21047 .map(|j| {
21048 j.table
21049 .alias
21050 .clone()
21051 .unwrap_or_else(|| j.table.name.clone())
21052 })
21053 .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
21054 let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
21055 let mut iter = cols.into_iter().map(|c| Expr::Binary {
21056 lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21057 qualifier: Some(left_qual.clone()),
21058 name: c.clone(),
21059 })),
21060 op: crate::ast::BinOp::Eq,
21061 rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21062 qualifier: Some(right_qual.clone()),
21063 name: c,
21064 })),
21065 });
21066 let first = iter.next().expect("at least one col");
21067 Some(iter.fold(first, |acc, pred| Expr::Binary {
21068 lhs: alloc::boxed::Box::new(acc),
21069 op: crate::ast::BinOp::And,
21070 rhs: alloc::boxed::Box::new(pred),
21071 }))
21072 } else if kind == JoinKind::Cross {
21073 None
21074 } else {
21075 return Err(self.err(format!(
21076 "expected ON or USING after {:?} JOIN, got {:?}",
21077 kind,
21078 self.peek()
21079 )));
21080 };
21081 joins.push(FromJoin {
21082 kind,
21083 table,
21084 on,
21085 using_cols,
21086 natural: false,
21087 });
21088 }
21089 Ok(joins)
21090 }
21091
21092 /// Optional alias after an expression or table:
21093 /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
21094 /// accepted (PG-style implicit alias). Returns `None` if the next token
21095 /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
21096 fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
21097 if matches!(self.peek(), Token::As) {
21098 self.advance();
21099 // v7.39 (round 340, V56) — after AS the next token MUST be an
21100 // identifier. This used to return None and "let the caller
21101 // surface the error on the next expectation", but when AS is
21102 // the LAST token there is no next expectation: `SELECT 1 AS`
21103 // parsed clean and silently dropped the alias. PG rejects it.
21104 if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
21105 return self.expect_ident_like().map(Some);
21106 }
21107 return Err(self.err(alloc::format!(
21108 "expected an alias after AS, got {:?}",
21109 self.peek()
21110 )));
21111 }
21112 // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
21113 // grammar reserves a long list of follow-keywords from the
21114 // alias slot. SPG's bareword approximation: skip a small
21115 // set of idents that would otherwise be swallowed as the
21116 // table alias and break trailing clauses like CREATE
21117 // MATERIALIZED VIEW … WITH [NO] DATA or future ON
21118 // CONFLICT WHERE shapes.
21119 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
21120 if is_alias_stopword(s) {
21121 return Ok(None);
21122 }
21123 return Ok(self.expect_ident_like().ok());
21124 }
21125 Ok(None)
21126 }
21127
21128 /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
21129 fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21130 // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
21131 // error beats a stack overflow (an overflow aborts the
21132 // embedding host process).
21133 self.enter_nested()?;
21134 let r = self.parse_expr_inner(min_prec);
21135 self.nest_depth -= 1;
21136 r
21137 }
21138
21139 /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
21140 /// When the upcoming tokens form one, return the underlying
21141 /// operator token and the position just past the closing paren
21142 /// so the binary loop can dispatch on the plain operator.
21143 fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
21144 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
21145 return None;
21146 }
21147 if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
21148 return None;
21149 }
21150 let mut i = self.pos + 2;
21151 // Optional schema qualifier (pg_catalog.<op> etc.).
21152 if matches!(self.tokens.get(i), Some(Token::Ident(_)))
21153 && matches!(self.tokens.get(i + 1), Some(Token::Dot))
21154 {
21155 i += 2;
21156 }
21157 let op_tok = self.tokens.get(i)?.clone();
21158 if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
21159 return None;
21160 }
21161 Some((i + 2, op_tok))
21162 }
21163
21164 /// PG operator symbols that lower onto function calls in
21165 /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
21166 /// family → regexp_like, comparison rung), `^@` (starts_with,
21167 /// comparison rung), `^` (power, tighter than `*`), `#`
21168 /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
21169 /// subset of the OR bits so the subtraction never borrows).
21170 fn try_symbol_operator(
21171 &mut self,
21172 lhs: &Expr,
21173 min_prec: u8,
21174 ) -> Result<Option<Expr>, ParseError> {
21175 enum Sym {
21176 Regex { ci: bool, negated: bool },
21177 Like { ci: bool, negated: bool },
21178 StartsWith,
21179 Power,
21180 Xor,
21181 RangeAdjacent,
21182 }
21183 // v7.39 (IS-precedence knife) — the low-precedence postfix
21184 // predicates ride this existing leaf call (zero new frame slots
21185 // on the nesting chain).
21186 if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
21187 return Ok(Some(e));
21188 }
21189 let (sym, prec): (Sym, u8) = match self.peek() {
21190 Token::Tilde => (
21191 Sym::Regex {
21192 ci: false,
21193 negated: false,
21194 },
21195 5,
21196 ),
21197 Token::TildeStar => (
21198 Sym::Regex {
21199 ci: true,
21200 negated: false,
21201 },
21202 5,
21203 ),
21204 Token::NotTilde => (
21205 Sym::Regex {
21206 ci: false,
21207 negated: true,
21208 },
21209 5,
21210 ),
21211 Token::NotTildeStar => (
21212 Sym::Regex {
21213 ci: true,
21214 negated: true,
21215 },
21216 5,
21217 ),
21218 // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
21219 Token::DoubleTilde => (
21220 Sym::Like {
21221 ci: false,
21222 negated: false,
21223 },
21224 5,
21225 ),
21226 Token::DoubleTildeStar => (
21227 Sym::Like {
21228 ci: true,
21229 negated: false,
21230 },
21231 5,
21232 ),
21233 Token::NotDoubleTilde => (
21234 Sym::Like {
21235 ci: false,
21236 negated: true,
21237 },
21238 5,
21239 ),
21240 Token::NotDoubleTildeStar => (
21241 Sym::Like {
21242 ci: true,
21243 negated: true,
21244 },
21245 5,
21246 ),
21247 Token::CaretAt => (Sym::StartsWith, 5),
21248 // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
21249 // tighter than `* / & |`, which the prec-9 rung preserves —
21250 // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
21251 Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
21252 Token::Caret => (Sym::Power, 9),
21253 // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
21254 // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
21255 Token::Hash => (Sym::Xor, 6),
21256 Token::Adjacent => (Sym::RangeAdjacent, 5),
21257 _ => return Ok(None),
21258 };
21259 if prec < min_prec {
21260 return Ok(None);
21261 }
21262 self.advance();
21263 let rhs = self.parse_expr(prec + 1)?;
21264 let out = match sym {
21265 Sym::Regex { ci, negated } => {
21266 let mut args = alloc::vec![lhs.clone(), rhs];
21267 if ci {
21268 args.push(Expr::Literal(Literal::String(String::from("i"))));
21269 }
21270 maybe_not(
21271 Expr::FunctionCall {
21272 name: String::from("regexp_like"),
21273 args,
21274 },
21275 negated,
21276 )
21277 }
21278 Sym::Like { ci, negated } => Expr::Like {
21279 expr: alloc::boxed::Box::new(lhs.clone()),
21280 pattern: alloc::boxed::Box::new(rhs),
21281 negated,
21282 case_insensitive: ci,
21283 },
21284 Sym::StartsWith => Expr::FunctionCall {
21285 name: String::from("starts_with"),
21286 args: alloc::vec![lhs.clone(), rhs],
21287 },
21288 Sym::Power => Expr::FunctionCall {
21289 name: String::from("power"),
21290 args: alloc::vec![lhs.clone(), rhs],
21291 },
21292 // `#` bitwise XOR — a real operator now (was desugared to
21293 // `(a|b)-(a&b)`, algebraically identical for integers but
21294 // undefined for bit strings; the direct op handles both).
21295 Sym::Xor => Expr::Binary {
21296 lhs: Box::new(lhs.clone()),
21297 op: BinOp::BitXor,
21298 rhs: Box::new(rhs),
21299 },
21300 // range `-|-` "is adjacent to" — lowered to a catalog function.
21301 Sym::RangeAdjacent => Expr::FunctionCall {
21302 name: String::from("range_adjacent"),
21303 args: alloc::vec![lhs.clone(), rhs],
21304 },
21305 };
21306 Ok(Some(out))
21307 }
21308
21309 /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
21310 /// predicates, moved out of the tight postfix-cast loop: PG binds
21311 /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
21312 /// looser than EVERY binary operator (only NOT/AND/OR are looser),
21313 /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
21314 /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
21315 /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
21316 /// when nothing at this position belongs to the family. Out-of-line
21317 /// (`inline(never)`): the caller sits on the per-nesting-level frame
21318 /// chain that MAX_NEST_DEPTH is tuned against.
21319 #[inline(never)]
21320 fn parse_postfix_predicate(
21321 &mut self,
21322 lhs: &Expr,
21323 min_prec: u8,
21324 ) -> Result<Option<Expr>, ParseError> {
21325 // Reached through try_symbol_operator (an existing leaf call of
21326 // the binary loop) so NO new stack slots land on the per-nesting
21327 // frame chain; the lhs clones only when a predicate actually
21328 // consumes it.
21329 match self.peek() {
21330 // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
21331 // comparison family rung 5 (each +1 from the pre-XOR ladder).
21332 Token::Is if min_prec <= 4 => {}
21333 Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
21334 Token::Not
21335 if min_prec <= 5
21336 && matches!(
21337 self.tokens.get(self.pos + 1),
21338 Some(Token::Between | Token::In | Token::Like)
21339 ) => {}
21340 Token::Not | Token::Ident(_)
21341 if min_prec <= 5
21342 && (matches!(self.peek(), Token::Ident(s)
21343 if s.eq_ignore_ascii_case("ilike")
21344 || (self.mysql_dialect
21345 && (s.eq_ignore_ascii_case("regexp")
21346 || s.eq_ignore_ascii_case("rlike")))
21347 || (s.eq_ignore_ascii_case("similar")
21348 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
21349 || (matches!(self.peek(), Token::Not)
21350 && matches!(self.tokens.get(self.pos + 1),
21351 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21352 || (self.mysql_dialect
21353 && (s.eq_ignore_ascii_case("regexp")
21354 || s.eq_ignore_ascii_case("rlike")))
21355 || s.eq_ignore_ascii_case("similar")))) => {}
21356 _ => return Ok(None),
21357 }
21358 let mut expr = lhs.clone();
21359 // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
21360 // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
21361 if min_prec <= 4 {
21362 if matches!(self.peek(), Token::Is) {
21363 self.advance();
21364 let negated = if matches!(self.peek(), Token::Not) {
21365 self.advance();
21366 true
21367 } else {
21368 false
21369 };
21370 // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
21371 // mailrs pg_dump.
21372 if matches!(self.peek(), Token::Distinct) {
21373 self.advance();
21374 if !matches!(self.peek(), Token::From) {
21375 return Err(self.err(format!(
21376 "expected FROM after IS{} DISTINCT, got {:?}",
21377 if negated { " NOT" } else { "" },
21378 self.peek()
21379 )));
21380 }
21381 self.advance();
21382 // Right-hand side: parse at the same precedence
21383 // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
21384 // groups as `x IS DISTINCT FROM (a + b)`.
21385 let rhs = self.parse_expr(5)?;
21386 let op = if negated {
21387 BinOp::IsNotDistinctFrom
21388 } else {
21389 BinOp::IsDistinctFrom
21390 };
21391 expr = Expr::Binary {
21392 op,
21393 lhs: Box::new(expr),
21394 rhs: Box::new(rhs),
21395 };
21396 {
21397 return Ok(Some(expr));
21398 }
21399 }
21400 // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
21401 // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
21402 // Lowers onto pg_is_json(x, kind); NOT wraps the
21403 // call in a logical negation.
21404 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21405 if s.eq_ignore_ascii_case("json"))
21406 {
21407 self.advance(); // JSON
21408 let kind = match self.peek() {
21409 Token::Ident(s) | Token::QuotedIdent(s)
21410 if matches!(
21411 s.to_ascii_lowercase().as_str(),
21412 "value" | "object" | "array" | "scalar"
21413 ) =>
21414 {
21415 let k = s.to_ascii_lowercase();
21416 self.advance();
21417 k
21418 }
21419 _ => "value".to_string(),
21420 };
21421 let call = Expr::FunctionCall {
21422 name: "pg_is_json".to_string(),
21423 args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
21424 };
21425 expr = if negated {
21426 Expr::Unary {
21427 op: UnOp::Not,
21428 expr: Box::new(call),
21429 }
21430 } else {
21431 call
21432 };
21433 {
21434 return Ok(Some(expr));
21435 }
21436 }
21437 // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
21438 // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
21439 // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
21440 {
21441 let form_kw = match self.peek() {
21442 Token::Ident(s) | Token::QuotedIdent(s)
21443 if matches!(
21444 s.to_ascii_uppercase().as_str(),
21445 "NFC" | "NFD" | "NFKC" | "NFKD"
21446 ) && matches!(
21447 self.tokens.get(self.pos + 1),
21448 Some(Token::Ident(n) | Token::QuotedIdent(n))
21449 if n.eq_ignore_ascii_case("normalized")
21450 ) =>
21451 {
21452 Some(s.to_ascii_uppercase())
21453 }
21454 _ => None,
21455 };
21456 let bare_normalized = form_kw.is_none()
21457 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21458 if s.eq_ignore_ascii_case("normalized"));
21459 if form_kw.is_some() || bare_normalized {
21460 if form_kw.is_some() {
21461 self.advance(); // form keyword
21462 }
21463 self.advance(); // NORMALIZED
21464 let mut args = alloc::vec![expr];
21465 if let Some(f) = form_kw {
21466 args.push(Expr::Literal(Literal::String(f)));
21467 }
21468 let call = Expr::FunctionCall {
21469 name: "is_normalized".to_string(),
21470 args,
21471 };
21472 expr = if negated {
21473 Expr::Unary {
21474 op: UnOp::Not,
21475 expr: Box::new(call),
21476 }
21477 } else {
21478 call
21479 };
21480 {
21481 return Ok(Some(expr));
21482 }
21483 }
21484 }
21485 // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
21486 // three-valued boolean tests. IS TRUE/FALSE never
21487 // return NULL, so they lower to CASE forms whose
21488 // ELSE catches the NULL branch; IS UNKNOWN on a
21489 // boolean is exactly IS NULL.
21490 if matches!(self.peek(), Token::True | Token::False)
21491 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
21492 {
21493 let tok = self.advance();
21494 let test = match tok {
21495 Token::True => Some(true),
21496 Token::False => Some(false),
21497 _ => None, // UNKNOWN
21498 };
21499 // v7.39 (round 328, V45) — kept as what the user
21500 // wrote. These used to be lowered here into `CASE` /
21501 // `IS NULL`; the semantics were right but the AST no
21502 // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
21503 // was echoed back as
21504 // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
21505 expr = Expr::BoolTest {
21506 expr: Box::new(expr),
21507 value: test,
21508 negated,
21509 };
21510 {
21511 return Ok(Some(expr));
21512 }
21513 }
21514 if !matches!(self.peek(), Token::Null) {
21515 return Err(self.err(format!(
21516 "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21517 if negated { " NOT" } else { "" },
21518 self.peek()
21519 )));
21520 }
21521 self.advance();
21522 expr = Expr::IsNull {
21523 expr: Box::new(expr),
21524 negated,
21525 };
21526 {
21527 return Ok(Some(expr));
21528 }
21529 }
21530 }
21531 // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21532 if min_prec <= 5 {
21533 // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21534 // Look one token ahead so a stray `NOT` not followed by any of
21535 // these flows through to the early return below untouched.
21536 let negated = if matches!(self.peek(), Token::Not) {
21537 let next = self.tokens.get(self.pos + 1);
21538 matches!(next, Some(Token::Between | Token::In | Token::Like))
21539 || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21540 || (self.mysql_dialect
21541 && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21542 || s.eq_ignore_ascii_case("similar"))
21543 } else {
21544 false
21545 };
21546 if negated {
21547 self.advance();
21548 }
21549 if matches!(self.peek(), Token::Between) {
21550 expr = self.parse_between_tail(expr, negated)?;
21551 {
21552 return Ok(Some(expr));
21553 }
21554 }
21555 if matches!(self.peek(), Token::In) {
21556 if self.suppress_in_tail && !negated {
21557 // POSITION(sub IN str) — IN belongs to the
21558 // enclosing function syntax; stop here.
21559 {
21560 return Ok(None);
21561 }
21562 }
21563 expr = self.parse_in_tail(expr, negated)?;
21564 {
21565 return Ok(Some(expr));
21566 }
21567 }
21568 // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21569 // lowers onto the internal __similar_to(expr, pat[, esc]) call
21570 // (the SQL→regex transform runs inside, in the backtracking-
21571 // friendly shape SPG's matcher needs).
21572 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21573 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21574 {
21575 self.advance(); // SIMILAR
21576 self.advance(); // TO
21577 let pattern = self.parse_expr(6)?;
21578 let mut args = alloc::vec![expr, pattern];
21579 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21580 self.advance();
21581 args.push(self.parse_expr(6)?);
21582 }
21583 let call = Expr::FunctionCall {
21584 name: "__similar_to".to_string(),
21585 args,
21586 };
21587 expr = maybe_not(call, negated);
21588 {
21589 return Ok(Some(expr));
21590 }
21591 }
21592 if matches!(self.peek(), Token::Like) {
21593 self.advance();
21594 // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21595 if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21596 expr = q;
21597 {
21598 return Ok(Some(expr));
21599 }
21600 }
21601 // Pattern at the same precedence as other comparison RHSes —
21602 // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21603 let mut pattern = self.parse_expr(6)?;
21604 // `ESCAPE 'c'` — rewrite a literal pattern to the
21605 // default backslash escape at parse time. Custom
21606 // escapes on non-literal patterns would need
21607 // matcher support; error honestly.
21608 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21609 self.advance();
21610 let esc = self.parse_expr(6)?;
21611 pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21612 }
21613 expr = Expr::Like {
21614 expr: Box::new(expr),
21615 pattern: Box::new(pattern),
21616 negated,
21617 case_insensitive: false,
21618 };
21619 {
21620 return Ok(Some(expr));
21621 }
21622 }
21623 // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21624 // keyword reaches us as a plain identifier.
21625 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21626 self.advance();
21627 if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21628 expr = q;
21629 {
21630 return Ok(Some(expr));
21631 }
21632 }
21633 let pattern = self.parse_expr(6)?;
21634 expr = Expr::Like {
21635 expr: Box::new(expr),
21636 pattern: Box::new(pattern),
21637 negated,
21638 case_insensitive: true,
21639 };
21640 {
21641 return Ok(Some(expr));
21642 }
21643 }
21644 // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21645 // operator (RLIKE is the alias). It is a keyword, not `~`, and
21646 // matches case-insensitively under the default collation, so it
21647 // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21648 // `~*` operator uses, wrapped in NOT when negated.
21649 if self.mysql_dialect
21650 && matches!(self.peek(), Token::Ident(s)
21651 if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21652 {
21653 self.advance();
21654 let pattern = self.parse_expr(6)?;
21655 let call = Expr::FunctionCall {
21656 name: String::from("regexp_like"),
21657 args: alloc::vec![
21658 expr,
21659 pattern,
21660 Expr::Literal(Literal::String(String::from("i"))),
21661 ],
21662 };
21663 return Ok(Some(maybe_not(call, negated)));
21664 }
21665 }
21666 let _ = expr;
21667 Ok(None)
21668 }
21669
21670 fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21671 let mut lhs = self.parse_unary()?;
21672 let mut chain_len = 0usize;
21673 loop {
21674 // OPERATOR([schema.]op) reduces to its underlying
21675 // operator token before the normal dispatch.
21676 let explicit = self.peek_explicit_operator();
21677 let dispatch = match &explicit {
21678 Some((_, tok)) => self.binop_here(tok),
21679 None => self.binop_here(self.peek()),
21680 };
21681 let Some((op, prec)) = dispatch else {
21682 // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21683 // of the symbol family. `binop_here` answers None for them
21684 // because they lower onto function calls rather than a
21685 // BinOp, and the fallback below reads `self.peek()` — the
21686 // word OPERATOR, not the operator. `pg_dump` writes every
21687 // catalog predicate this way, so its first query failed
21688 // and no dump ran:
21689 //
21690 // AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21691 //
21692 // Collapsing the wrapper to the operator it names puts the
21693 // token where the fallback already looks.
21694 if let Some((next, op_tok)) = explicit {
21695 self.tokens.splice(self.pos..next, [op_tok]);
21696 }
21697 if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21698 lhs = e;
21699 chain_len += 1;
21700 if chain_len > MAX_BINARY_CHAIN {
21701 return Err(self.err(alloc::format!(
21702 "more than {MAX_BINARY_CHAIN} chained binary operators"
21703 )));
21704 }
21705 continue;
21706 }
21707 break;
21708 };
21709 if prec < min_prec {
21710 break;
21711 }
21712 // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21713 // iteratively but evaluates and drops recursively;
21714 // depth beyond the budget overflows worker stacks.
21715 chain_len += 1;
21716 if chain_len > MAX_BINARY_CHAIN {
21717 return Err(self.err(alloc::format!(
21718 "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21719 )));
21720 }
21721 match explicit {
21722 Some((end_pos, _)) => self.pos = end_pos,
21723 None => {
21724 self.advance();
21725 }
21726 }
21727 // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21728 // ANY is a bare ident; ALL is a reserved Token. Both
21729 // require an immediate `(` to disambiguate from
21730 // identifier columns named `any` / `all`.
21731 let any_kind = match self.peek() {
21732 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21733 Some(false)
21734 }
21735 Token::Ident(s) | Token::QuotedIdent(s)
21736 if (s.eq_ignore_ascii_case("any")
21737 || s.eq_ignore_ascii_case("some")
21738 || s.eq_ignore_ascii_case("all"))
21739 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21740 {
21741 Some(!s.eq_ignore_ascii_case("all"))
21742 }
21743 _ => None,
21744 };
21745 if let Some(is_any) = any_kind {
21746 lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21747 continue;
21748 }
21749 let rhs = self.parse_expr(prec + 1)?;
21750 lhs = Expr::Binary {
21751 lhs: Box::new(lhs),
21752 op,
21753 rhs: Box::new(rhs),
21754 };
21755 }
21756 Ok(lhs)
21757 }
21758
21759 /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
21760 /// and the array form.
21761 ///
21762 /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
21763 /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
21764 /// this block's `Expr` temporaries and four `format!` sites slots in
21765 /// that frame on every level of `((((1))))`, which never reaches it.
21766 #[inline(never)]
21767 fn parse_any_all_rhs(
21768 &mut self,
21769 lhs: Expr,
21770 op: BinOp,
21771 is_any: bool,
21772 ) -> Result<Expr, ParseError> {
21773 self.advance(); // ident
21774 self.advance(); // (
21775 // `x op ANY (SELECT …)` — the quantified-subquery
21776 // form. `= ANY` is exactly IN; the other operators
21777 // lower onto EXISTS over the subquery as a derived
21778 // table, comparing against its single projection
21779 // aliased __v (x's columns resolve correlated).
21780 // ALL is the negated-EXISTS complement; a NULL
21781 // element makes PG return NULL where this lowering
21782 // returns true — the NOT NULL column case (the
21783 // practical one) is exact.
21784 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
21785 // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
21786 // legal PG too (round-151 sibling). Out-of-line
21787 // (#[inline(never)] helper) — this sits on
21788 // parse_expr's recursive frame and the two-armed
21789 // SELECT temporary blew the nesting-budget stack.
21790 let mut sub = self.parse_any_all_select_body()?;
21791 if !matches!(self.peek(), Token::RParen) {
21792 return Err(self.err(alloc::format!(
21793 "expected ')' after ANY/ALL subquery, got {:?}",
21794 self.peek()
21795 )));
21796 }
21797 self.advance();
21798 if sub.items.len() != 1 {
21799 return Err(self.err(alloc::format!(
21800 "ANY/ALL subquery must return one column, got {}",
21801 sub.items.len()
21802 )));
21803 }
21804 if is_any && matches!(op, BinOp::Eq) {
21805 return Ok(Expr::InSubquery {
21806 expr: Box::new(lhs),
21807 subquery: Box::new(sub),
21808 negated: false,
21809 });
21810 }
21811 // The engine's subquery resolvers materialise
21812 // the single-column result into an ARRAY the
21813 // existing AnyAll three-valued eval consumes.
21814 return Ok(Expr::AnyAll {
21815 expr: Box::new(lhs),
21816 op,
21817 array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
21818 is_any,
21819 });
21820 }
21821 let arr = self.parse_expr(0)?;
21822 if !matches!(self.peek(), Token::RParen) {
21823 return Err(self.err(alloc::format!(
21824 "expected ')' after ANY/ALL argument, got {:?}",
21825 self.peek()
21826 )));
21827 }
21828 self.advance();
21829 Ok(Expr::AnyAll {
21830 expr: Box::new(lhs),
21831 op,
21832 array: Box::new(arr),
21833 is_any,
21834 })
21835 }
21836
21837 /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
21838 /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
21839 #[inline(never)]
21840 fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
21841 self.advance();
21842 let e = self.parse_expr(9)?;
21843 Ok(build_center_call(e))
21844 }
21845
21846 /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
21847 /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
21848 /// unary minus.
21849 ///
21850 /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
21851 /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
21852 /// the Expr-sized local stays out of that frame.
21853 #[inline(never)]
21854 fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21855 self.advance();
21856 let e = self.parse_expr(9)?;
21857 Ok(Expr::FunctionCall {
21858 name: alloc::string::String::from(name),
21859 args: alloc::vec![e],
21860 })
21861 }
21862
21863 /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
21864 /// (horizontal). Out-of-line from `parse_unary` (frame budget).
21865 #[inline(never)]
21866 fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
21867 self.advance();
21868 let e = self.parse_expr(9)?;
21869 Ok(Expr::FunctionCall {
21870 name: alloc::string::String::from(if vertical {
21871 "isvertical"
21872 } else {
21873 "ishorizontal"
21874 }),
21875 args: alloc::vec![e],
21876 })
21877 }
21878
21879 /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
21880 /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
21881 /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
21882 #[inline(never)]
21883 fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
21884 self.advance();
21885 let e = self.parse_expr(9)?;
21886 Ok(Expr::Cast {
21887 expr: Box::new(e),
21888 target: CastTarget::Named("binary".to_string()),
21889 })
21890 }
21891
21892 /// The prefix operators that share one shape: take the token, parse
21893 /// an operand at `prec`, wrap it.
21894 ///
21895 /// `#[inline(never)]`, and one function instead of five arms, for the
21896 /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
21897 /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
21898 /// debug build gives EVERY arm's locals a slot in the frame, whichever
21899 /// arm runs. `((((1))))` reaches none of these arms and was carrying
21900 /// five `Expr`-sized locals per level for them anyway.
21901 #[inline(never)]
21902 fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
21903 self.advance();
21904 let e = self.parse_expr(prec)?;
21905 Ok(Expr::Unary {
21906 op,
21907 expr: Box::new(e),
21908 })
21909 }
21910
21911 /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
21912 /// and separate from it because of the literal folding below and the
21913 /// `format!` temporaries that folding needs.
21914 #[inline(never)]
21915 fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
21916 self.advance();
21917 // v7.39 (round 549) — fold the sign into an integer literal that
21918 // only fits once it is negative.
21919 //
21920 // `9223372036854775808` is one past i64::MAX, so the lexer hands
21921 // it over as a NUMERIC and `-` on a numeric stays numeric. PG
21922 // folds the sign first, so `-9223372036854775808` is a bigint
21923 // there — and `-9223372036854775808 - 1` raises "bigint out of
21924 // range" where SPG quietly answered -9223372036854775809, a value
21925 // no bigint can hold. The arithmetic itself was already checked;
21926 // only the literal's type was wrong.
21927 if let Token::Numeric(lit) = self.peek()
21928 && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
21929 {
21930 self.advance();
21931 return Ok(Expr::Literal(Literal::Integer(folded)));
21932 }
21933 // Unary minus binds tighter than `*`/`/` (now at prec 7 after
21934 // `<->` slotted into 5 and arithmetic shifted up).
21935 let e = self.parse_expr(9)?;
21936 Ok(Expr::Unary {
21937 op: UnOp::Neg,
21938 expr: Box::new(e),
21939 })
21940 }
21941
21942 /// tsquery `!!` prefix negation, lowered to the catalog function.
21943 /// Binds like unary minus. Out-of-line for the frame reason on
21944 /// `parse_unary_op`.
21945 #[inline(never)]
21946 fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
21947 self.advance();
21948 let e = self.parse_expr(9)?;
21949 Ok(Expr::FunctionCall {
21950 name: String::from("tsquery_not"),
21951 args: alloc::vec![e],
21952 })
21953 }
21954
21955 fn parse_unary(&mut self) -> Result<Expr, ParseError> {
21956 match self.peek() {
21957 // NOT binds tighter than AND / XOR / OR but looser than
21958 // comparisons — its operand takes everything ≥ the comparison
21959 // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
21960 // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
21961 // was rung 3, behaviour-identical when 3 was unused; AND now
21962 // occupies 3, so this must be 4 to keep NOT tighter than AND.)
21963 Token::Not => self.parse_unary_op(UnOp::Not, 4),
21964 // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
21965 // The body is out-of-line: `parse_unary` is one of the three
21966 // frames the parser's MAX_NEST_DEPTH is tuned against, and an
21967 // inline arm here overflowed the native stack in
21968 // `nesting_budget_errors_cleanly` — the guard test caught it,
21969 // exactly as the eval-side cliff did in rounds 346 and 351.
21970 Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
21971 self.parse_binary_prefix()
21972 }
21973 // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
21974 // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
21975 // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
21976 Token::Bang => self.parse_unary_op(UnOp::Not, 9),
21977 Token::Minus => self.parse_prefix_minus(),
21978 // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
21979 // worked only because the lexer reads it as one signed literal;
21980 // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
21981 // PG18 and MariaDB take all of them. Binds like unary minus.
21982 Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
21983 // Bitwise NOT binds like unary minus.
21984 Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
21985 // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
21986 // "center of" operator; desugars to center(x). The whole arm
21987 // is out-of-line: parse_unary sits on the per-nesting-level
21988 // frame chain that MAX_NEST_DEPTH is tuned against, so no
21989 // Expr-sized local may live in this frame.
21990 Token::TsMatch => self.parse_prefix_center(),
21991 // v7.39 (round 508) — the prefix operators that are named
21992 // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
21993 // is length. Out-of-line for the same nesting-frame reason as
21994 // parse_prefix_center — parse_unary sits on the recursive cycle
21995 // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
21996 // live in this frame.
21997 Token::At => self.parse_prefix_call("abs"),
21998 Token::Hash => self.parse_prefix_call("npoints"),
21999 Token::AtMinusAt => self.parse_prefix_call("length"),
22000 // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
22001 // "is horizontal" (lseg / line); desugars to the existing
22002 // isvertical()/ishorizontal() functions. Out-of-line for the
22003 // same nesting-frame reason as parse_prefix_center.
22004 Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
22005 Token::GeomHoriz => self.parse_prefix_geom_axis(false),
22006 Token::DoubleBang => self.parse_prefix_tsquery_not(),
22007 _ => self.parse_atom(),
22008 }
22009 }
22010
22011 /// Parse a parenthesised scalar subquery body after the caller has consumed
22012 /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
22013 /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
22014 /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
22015 /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
22016 /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
22017 /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
22018 /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
22019 /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
22020 /// which sits on the recursive nesting-budget cycle (a few extra bytes there
22021 /// tips the deep-nesting test into a stack overflow).
22022 #[inline(never)]
22023 fn array_subquery_ahead(&self) -> bool {
22024 if !matches!(self.peek(), Token::LParen) {
22025 return false;
22026 }
22027 matches!(
22028 self.tokens.get(self.pos + 1),
22029 Some(Token::Select | Token::Values)
22030 ) || matches!(
22031 self.tokens.get(self.pos + 1),
22032 Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
22033 )
22034 }
22035
22036 /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
22037 /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
22038 /// locals stay off parse_atom's recursive frame (round 105).
22039 #[inline(never)]
22040 fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
22041 self.advance(); // consume `[`
22042 let mut items: Vec<Expr> = Vec::new();
22043 if !matches!(self.peek(), Token::RBracket) {
22044 loop {
22045 // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
22046 // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
22047 if matches!(self.peek(), Token::LBracket) {
22048 items.push(self.parse_array_bracket_body()?);
22049 } else {
22050 items.push(self.parse_expr(0)?);
22051 }
22052 match self.peek() {
22053 Token::Comma => {
22054 self.advance();
22055 }
22056 Token::RBracket => break,
22057 other => {
22058 return Err(self.err(alloc::format!(
22059 "expected ',' or ']' in ARRAY literal, got {other:?}"
22060 )));
22061 }
22062 }
22063 }
22064 }
22065 self.advance(); // consume `]`
22066 Ok(Expr::Array(items))
22067 }
22068
22069 /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
22070 /// is already consumed; the current token is `(`. Desugars to a scalar
22071 /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
22072 /// the subquery's single-column rows in order — reusing the existing
22073 /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
22074 /// keeps the large `Statement` local off parse_atom's recursive frame.
22075 #[inline(never)]
22076 fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
22077 self.advance(); // consume `(`
22078 let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
22079 if w.eq_ignore_ascii_case("with"));
22080 let sub = if is_with {
22081 self.advance(); // WITH
22082 self.parse_with_cte_then_select()?
22083 } else {
22084 self.parse_select_stmt()?
22085 };
22086 if !matches!(self.peek(), Token::RParen) {
22087 return Err(self.err(alloc::format!(
22088 "expected ')' to close ARRAY(subquery), got {:?}",
22089 self.peek()
22090 )));
22091 }
22092 self.advance(); // consume `)`
22093 // Reuse the parser to build the array_agg wrapper from the subquery's
22094 // canonical text — avoids hand-constructing the derived-table AST.
22095 let wrapper = alloc::format!(
22096 "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
22097 );
22098 let stmt = parse_statement(&wrapper)
22099 .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
22100 let Statement::Select(sel) = stmt else {
22101 return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
22102 };
22103 Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
22104 }
22105
22106 #[inline(never)]
22107 fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
22108 let inner = if is_with {
22109 self.advance(); // WITH
22110 self.parse_with_cte_then_select()?
22111 } else {
22112 self.parse_select_stmt()?
22113 };
22114 match self.advance() {
22115 Token::RParen => {
22116 let Statement::Select(s) = inner else {
22117 return Err(ParseError {
22118 message: "scalar subquery body must be a SELECT".into(),
22119 token_pos: self.consumed_pos(),
22120 });
22121 };
22122 Ok(Expr::ScalarSubquery(Box::new(s)))
22123 }
22124 other => Err(ParseError {
22125 message: format!("expected ')' after scalar subquery, got {other:?}"),
22126 token_pos: self.consumed_pos(),
22127 }),
22128 }
22129 }
22130
22131 /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
22132 /// literals. The lexer splits them into an ident + string; recombine
22133 /// here. Out-of-line and returning `Option` so `parse_atom` — the
22134 /// recursive frame the 768 KiB stack budget is tuned against — pays no
22135 /// frame for the `body` / `bits` strings and their char loops (the
22136 /// round-367 frame cliff, M20).
22137 #[inline(never)]
22138 fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
22139 let is_hex = match self.peek() {
22140 Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
22141 Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
22142 _ => return None,
22143 };
22144 if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
22145 return None;
22146 }
22147 self.advance();
22148 let Token::String(body) = self.advance() else {
22149 unreachable!("guarded above");
22150 };
22151 // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
22152 // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
22153 // (hex pairs, even count required — MariaDB errors on an odd
22154 // count); `b'1010'` packs its bits big-endian, left-padded to a
22155 // byte. Lower both onto the bytea cast.
22156 if self.mysql_dialect {
22157 if is_hex {
22158 if body.len() % 2 == 1 {
22159 return Some(Err(self.err(alloc::format!(
22160 "invalid hex string literal X'{body}': odd digit count"
22161 ))));
22162 }
22163 for c in body.chars() {
22164 if !c.is_ascii_hexdigit() {
22165 return Some(Err(
22166 self.err(alloc::format!("invalid hexadecimal digit {c:?} in X'…'"))
22167 ));
22168 }
22169 }
22170 return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
22171 }
22172 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22173 return Some(Err(
22174 self.err(alloc::format!("invalid binary digit {bad:?} in b'…'"))
22175 ));
22176 }
22177 return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
22178 }
22179 let bits = if is_hex {
22180 let mut out = String::with_capacity(body.len() * 4);
22181 for c in body.chars() {
22182 let Some(d) = c.to_digit(16) else {
22183 return Some(Err(self.err(alloc::format!(
22184 "invalid hexadecimal digit {c:?} in X'…' bit string"
22185 ))));
22186 };
22187 out.push_str(&alloc::format!("{d:04b}"));
22188 }
22189 out
22190 } else {
22191 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22192 return Some(Err(self.err(alloc::format!(
22193 "invalid binary digit {bad:?} in B'…' bit string"
22194 ))));
22195 }
22196 body
22197 };
22198 // Route through the postfix-cast loop so a chained cast like
22199 // `B'1010'::int` attaches onto the implicit `::bit` cast instead
22200 // of erroring at the `::`.
22201 // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
22202 // literal keeps its exact length, while an explicit `::bit` cast is
22203 // bit(1) with pad/truncate semantics (PG).
22204 Some(self.finish_postfix_casts(Expr::Cast {
22205 expr: Box::new(Expr::Literal(Literal::String(bits))),
22206 target: CastTarget::Named("__bit_literal".to_string()),
22207 }))
22208 }
22209
22210 fn parse_atom(&mut self) -> Result<Expr, ParseError> {
22211 if let Some(res) = self.try_parse_bit_string_literal() {
22212 return res;
22213 }
22214 let tok_pos = self.pos;
22215 match self.advance() {
22216 Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
22217 Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
22218 // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
22219 // carrying the source mantissa + scale so no precision is lost. A
22220 // literal too wide for i128 falls back to double precision.
22221 // Out-of-line (#[inline(never)]) — this arm sits on the
22222 // parse_expr recursion chain; its expansion locals must not
22223 // widen the recursive frame (debug frame-cliff discipline).
22224 Token::Numeric(s) => match numeric_token_to_literal(s) {
22225 Ok(lit) => Ok(Expr::Literal(lit)),
22226 Err(msg) => Err(self.err(msg)),
22227 },
22228 Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
22229 // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
22230 // (the lexer only emits this token in the MySQL dialect). Lower
22231 // onto the existing bytea cast; out-of-line to keep this arm off
22232 // the parse recursion frame.
22233 Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
22234 Token::True => Ok(Expr::Literal(Literal::Bool(true))),
22235 Token::False => Ok(Expr::Literal(Literal::Bool(false))),
22236 Token::Null => Ok(Expr::Literal(Literal::Null)),
22237 // v6.1.1 — `$N` placeholder. The actual Value lookup
22238 // happens in the engine eval path against the prepared-
22239 // statement bind buffer.
22240 Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
22241 Token::LParen => {
22242 // v4.10: `(SELECT ...)` in expression position is a
22243 // scalar subquery; otherwise it's a parenthesised
22244 // expression. Peek for SELECT keyword to dispatch.
22245 // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
22246 // lexes as Ident("with") (not a reserved token). The subquery body
22247 // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
22248 // so its large `Statement` local stays out of parse_atom's stack
22249 // frame — parse_atom is on the recursive `((…))` cycle and the
22250 // nesting budget is tuned to its frame size).
22251 let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22252 if s.eq_ignore_ascii_case("with"));
22253 if matches!(self.peek(), Token::Select) || is_with {
22254 self.parse_paren_scalar_subquery(is_with)
22255 } else {
22256 let e = self.parse_expr(0)?;
22257 // `(a, b, …)` — a row constructor. Valid only
22258 // in front of a comparison operator or [NOT]
22259 // IN; both expand at parse time (lexicographic
22260 // comparison / OR'd row equalities).
22261 if matches!(self.peek(), Token::Comma) {
22262 let mut row = alloc::vec![e];
22263 while matches!(self.peek(), Token::Comma) {
22264 self.advance();
22265 row.push(self.parse_expr(0)?);
22266 }
22267 if !matches!(self.peek(), Token::RParen) {
22268 return Err(self.err(alloc::format!(
22269 "expected ')' after row constructor, got {:?}",
22270 self.peek()
22271 )));
22272 }
22273 self.advance();
22274 // A bare `(a, b, …)` row constructor can carry postfix
22275 // (`::text`, `.field`) just like `ROW(a, b, …)`; the
22276 // early return here skips parse_atom's tail postfix
22277 // pass, so fold casts in explicitly. For the
22278 // comparison / predicate forms nothing postfix follows,
22279 // so this is a no-op.
22280 return self
22281 .parse_row_comparison_tail(row)
22282 .and_then(|e| self.finish_postfix_casts(e));
22283 }
22284 match self.advance() {
22285 Token::RParen => Ok(e),
22286 other => Err(ParseError {
22287 message: format!("expected ')', got {other:?}"),
22288 token_pos: self.consumed_pos(),
22289 }),
22290 }
22291 }
22292 }
22293 Token::LBracket => self.parse_vector_literal_body(),
22294 Token::Extract => self.parse_extract_atom(),
22295 Token::Interval => self.parse_interval_atom(),
22296 // `LEFT` / `RIGHT` are reserved-keyword tokens because the
22297 // grammar dedicates arms for `LEFT [OUTER] JOIN` /
22298 // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
22299 // expression position calling the PG `left(string, n)` /
22300 // `right(string, n)` function; rebuild the AST as a regular
22301 // function call so the engine's apply_function dispatch picks
22302 // it up. Delegated to a #[inline(never)] helper so its locals
22303 // don't bloat this recursive `parse_atom` frame (the nesting
22304 // budget in `enter_nested` is tuned to parse_atom's size).
22305 Token::Left if matches!(self.peek(), Token::LParen) => {
22306 self.parse_lr_string_function_call("left")
22307 }
22308 Token::Right if matches!(self.peek(), Token::LParen) => {
22309 self.parse_lr_string_function_call("right")
22310 }
22311 // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
22312 // token; we match on the bare ident. NOT is a token
22313 // (consumed in the comparison rung), but `EXISTS (...)`
22314 // at the top of an expression starts here.
22315 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
22316 self.parse_exists_atom(false)
22317 }
22318 // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
22319 // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
22320 // CASE is a bare ident; we dispatch on lowercase match.
22321 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
22322 self.parse_case_atom()
22323 }
22324 // v7.37.17 (17.6 siblings) — PG typed datetime literals:
22325 // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
22326 // '…'`. Lower onto the ::cast node so the existing
22327 // runtime text→date/timestamp paths do the parsing. The
22328 // string must follow immediately, else the ident stays a
22329 // plain column reference.
22330 Token::Ident(s)
22331 if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
22332 && matches!(self.peek(), Token::String(_)) =>
22333 {
22334 let target =
22335 typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
22336 let Token::String(lit) = self.advance() else {
22337 unreachable!("peek guaranteed a string token");
22338 };
22339 Ok(Expr::Cast {
22340 expr: Box::new(Expr::Literal(Literal::String(lit))),
22341 target,
22342 })
22343 }
22344 // v7.39 (round 221) — the SQL-standard long spellings:
22345 // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
22346 // TIME ZONE '…'`. Consume the modifier and lower to the same
22347 // typed-literal cast (`timetz` / `timestamptz` for WITH).
22348 Token::Ident(s)
22349 if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
22350 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
22351 || w.eq_ignore_ascii_case("without"))
22352 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
22353 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
22354 && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
22355 {
22356 let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
22357 self.advance(); // WITH / WITHOUT
22358 self.advance(); // TIME
22359 self.advance(); // ZONE
22360 let Token::String(lit) = self.advance() else {
22361 unreachable!("guard checked a string token");
22362 };
22363 let base = s.to_ascii_lowercase();
22364 let target = match (base.as_str(), with_tz) {
22365 ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
22366 ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
22367 (_, true) => CastTarget::Timestamptz,
22368 (_, false) => CastTarget::Timestamp,
22369 };
22370 Ok(Expr::Cast {
22371 expr: Box::new(Expr::Literal(Literal::String(lit))),
22372 target,
22373 })
22374 }
22375 // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
22376 // gathers the subquery's single-column rows (in its row order)
22377 // into an array. Desugared to `array_agg` over the subquery as a
22378 // derived table; out-of-line to keep parse_atom's frame small (it
22379 // sits on the recursive nesting-budget cycle).
22380 Token::Ident(s) | Token::QuotedIdent(s)
22381 if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
22382 {
22383 self.parse_array_subquery()
22384 }
22385 // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
22386 // is not a reserved token; we match by case-insensitive
22387 // ident. The opening `[` must follow immediately. v7.39 (read01
22388 // round 105) — the body moved out-of-line so its `Vec`/loop locals
22389 // leave parse_atom's frame (which sits on the nesting-budget cycle).
22390 Token::Ident(s) | Token::QuotedIdent(s)
22391 if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
22392 {
22393 self.parse_array_literal_body()
22394 }
22395 // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
22396 // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
22397 // We special-case before the generic ident dispatch so
22398 // the AGAINST clause never reaches the function-call
22399 // loop (which would mis-read `(cols) AGAINST` as a
22400 // call with no trailing modifier). The shape is
22401 // rewritten to a Boolean OR over per-column
22402 // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
22403 // term)` so the existing FTS evaluator handles
22404 // semantics — the fulltext-GIN built at CREATE TABLE
22405 // time is currently a "real index that survives dump
22406 // round-trip"; the planner hook that actually uses
22407 // it for posting-list intersection lands in a later
22408 // sub-phase (Phase 2.2b) without touching this surface.
22409 Token::Ident(s) | Token::QuotedIdent(s)
22410 if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
22411 {
22412 self.parse_match_against_atom()
22413 }
22414 Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
22415 // v7.37.43-T4 — PG-unreserved keywords are legal column /
22416 // alias names in expression context too. `release` appears
22417 // in sentori `0003_partition_events.sql` as both a column
22418 // reference (SELECT … release …) and an INSERT column list
22419 // entry. Mirrors `expect_ident_like`'s expansion of the
22420 // identifier set.
22421 other if unreserved_keyword_text(&other).is_some() => {
22422 let s = unreserved_keyword_text(&other).unwrap();
22423 self.finish_ident_atom(s)
22424 }
22425 // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
22426 // only inside `SET` before, so `SELECT @@autocommit` — which
22427 // every MySQL connector asks at handshake — was a parse error.
22428 // MariaDB accepts the bare, `@@session.` and `@@global.`
22429 // spellings alike and answers from the session's own value.
22430 Token::SessionVar(v) => {
22431 // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
22432 // has nothing to do with a `@@` engine setting: its own
22433 // per-session namespace, and an unset one reads NULL instead
22434 // of raising. Stripping every `@` (as this did) made `@x` and
22435 // `@@x` the same node, so `SELECT @x` answered "Unknown
22436 // system variable".
22437 Ok(variable_ref_atom(&v))
22438 }
22439 other => Err(ParseError {
22440 message: format!("unexpected token {other:?} in expression"),
22441 token_pos: tok_pos,
22442 }),
22443 }
22444 // After parsing the atom, fold any postfix `::vector` casts.
22445 .and_then(|atom| self.finish_postfix_casts(atom))
22446 }
22447
22448 /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
22449 /// Both bind tighter than any binary op.
22450 /// Shared cast-target parser for postfix `::TYPE` and the
22451 /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
22452 /// If the next tokens are `( N )`, consume them and return the canonical
22453 /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
22454 /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
22455 fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
22456 if !matches!(self.peek(), Token::LParen) {
22457 return None;
22458 }
22459 self.advance(); // (
22460 let n = match self.advance() {
22461 Token::Integer(n) => n,
22462 _ => return Some(base.to_string()), // malformed → drop precision
22463 };
22464 if matches!(self.peek(), Token::RParen) {
22465 self.advance();
22466 }
22467 Some(alloc::format!("{base}({n})"))
22468 }
22469
22470 fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
22471 // r1052 — `::pg_catalog.regproc` and friends: pg_dump
22472 // schema-qualifies every cast target, and `pg_catalog.X` names
22473 // exactly the builtin type X. Consume the qualifier and let
22474 // the ordinary target parse decide.
22475 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
22476 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
22477 {
22478 self.advance();
22479 self.advance();
22480 }
22481 let target = match self.advance() {
22482 Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
22483 "int" | "integer" | "int4" => {
22484 if matches!(self.peek(), Token::LBracket)
22485 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22486 {
22487 self.advance();
22488 self.advance();
22489 CastTarget::IntArray
22490 } else {
22491 CastTarget::Int
22492 }
22493 }
22494 "bigint" | "int8" => {
22495 if matches!(self.peek(), Token::LBracket)
22496 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22497 {
22498 self.advance();
22499 self.advance();
22500 CastTarget::BigIntArray
22501 } else {
22502 CastTarget::BigInt
22503 }
22504 }
22505 "float" | "double" => CastTarget::Float,
22506 "text" => {
22507 // v7.10.11 — `::TEXT[]` widens to TextArray.
22508 if matches!(self.peek(), Token::LBracket)
22509 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22510 {
22511 self.advance();
22512 self.advance();
22513 CastTarget::TextArray
22514 } else {
22515 CastTarget::Text
22516 }
22517 }
22518 "bool" | "boolean" => CastTarget::Bool,
22519 "vector" => CastTarget::Vector,
22520 "date" => CastTarget::Date,
22521 // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22522 // seconds precision through the Named path (the engine rounds
22523 // the sub-second field); bare `::timestamp` keeps the fast arm.
22524 "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22525 Some(named) => CastTarget::Named(named),
22526 None => CastTarget::Timestamp,
22527 },
22528 "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22529 Some(named) => CastTarget::Named(named),
22530 None => CastTarget::Timestamptz,
22531 },
22532 "interval" => CastTarget::Interval,
22533 "json" => CastTarget::Json,
22534 "jsonb" => CastTarget::Jsonb,
22535 // v7.39 (round 694) — these have dedicated CastTarget
22536 // variants, so they never reached the postfix `[]` handling
22537 // further down and `::regtype[]` was a SYNTAX error at the
22538 // `]`. PG has an array type for every scalar; take the
22539 // suffix here and hand the canonical `<ty>_array` name to
22540 // the engine, the same shape every other array cast uses.
22541 "regtype" if self.peek_postfix_array_brackets() => {
22542 self.advance();
22543 self.advance();
22544 CastTarget::Named(alloc::string::String::from("regtype_array"))
22545 }
22546 "regclass" if self.peek_postfix_array_brackets() => {
22547 self.advance();
22548 self.advance();
22549 CastTarget::Named(alloc::string::String::from("regclass_array"))
22550 }
22551 "regtype" => CastTarget::RegType,
22552 "regclass" => CastTarget::RegClass,
22553 // v7.12.0 — `::tsvector` / `::tsquery`.
22554 // Engine decodes the LHS text via the PG
22555 // external form parser.
22556 // v7.39 (round 352, M8) — MySQL's own cast targets.
22557 // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22558 // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22559 // such type, so they are taken only in that dialect and
22560 // fall through to the "type does not exist" arm otherwise.
22561 "signed" | "unsigned" if self.mysql_dialect => {
22562 if matches!(self.peek(), Token::Ident(k)
22563 if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22564 {
22565 self.advance();
22566 }
22567 CastTarget::Named(s.to_ascii_lowercase())
22568 }
22569 // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22570 // in MySQL: MariaDB answers '123' where the SQL-standard
22571 // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22572 // Truncating a number to its first digit is a wrong answer
22573 // with no error, so the MySQL session gets MySQL's reading.
22574 "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22575 CastTarget::Text
22576 }
22577 "tsvector" => CastTarget::TsVector,
22578 "tsquery" => CastTarget::TsQuery,
22579 // v7.17.0 — `::uuid`. Engine decodes the LHS
22580 // text via `spg_storage::parse_uuid_str`.
22581 "uuid" => CastTarget::Uuid,
22582 // v7.18 — `::bytea`. Engine decodes the LHS
22583 // text via the PG hex form (`'\xdeadbeef'`)
22584 // or escape form (`'\\x05\\x00'`). Closes
22585 // mailrs D-pre #3 reverse-acceptance gap.
22586 "bytea" => CastTarget::Bytea,
22587 // v7.37.5 ship triage — generic typed-cast escape.
22588 // Anything the long-tail PG type ident table knows
22589 // about(network/bit/geometry/multirange/etc.)flows
22590 // through `CastTarget::Named(canonical)`; the engine
22591 // resolves via `column_type_to_data_type` and dispatches
22592 // through the typed `coerce_value` path. Truly
22593 // unrecognised idents still hit the error arm below
22594 // because the engine rejects them.
22595 other => {
22596 // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22597 // `::varchar(255)`, etc. Capture into the canonical
22598 // `name(p,s)` form so `type_name_to_data_type` can
22599 // reconstruct the `DataType::Numeric { precision,
22600 // scale }` (and similar param-carrying types).
22601 let mut name = other.to_string();
22602 // v7.39 (round 281) — `::bit varying(3)` is two
22603 // words; fold the tail in so the typmod reaches the
22604 // type resolver instead of tripping the parser.
22605 if name.eq_ignore_ascii_case("bit")
22606 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22607 {
22608 self.advance();
22609 name = alloc::string::String::from("varbit");
22610 }
22611 // v7.39 (round 613) — `::character varying` is the same
22612 // two-word shape and had no fold, so the `varying` was
22613 // left behind and the cast became a bare `character`,
22614 // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22615 // `a` where PG answers `ab`. Silently, and for a spelling
22616 // pg_dump writes.
22617 if name.eq_ignore_ascii_case("character")
22618 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22619 {
22620 self.advance();
22621 name = alloc::string::String::from("varchar");
22622 }
22623 if matches!(self.peek(), Token::LParen) {
22624 let mut buf = alloc::string::String::from("(");
22625 let mut depth = 0usize;
22626 loop {
22627 match self.advance() {
22628 Token::LParen => {
22629 depth += 1;
22630 if depth > 1 {
22631 buf.push('(');
22632 }
22633 }
22634 Token::RParen => {
22635 depth -= 1;
22636 if depth == 0 {
22637 buf.push(')');
22638 break;
22639 }
22640 buf.push(')');
22641 }
22642 Token::Comma => buf.push(','),
22643 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22644 // v7.39 (round 273) — a minus used to fall
22645 // into the catch-all below and vanish, so
22646 // `::numeric(10,-2)` reached the engine as
22647 // the text `numeric(10,2)` and silently
22648 // rounded to two DECIMALS instead of to
22649 // hundreds. A dropped token is not a
22650 // no-op when it carries a sign.
22651 Token::Minus => buf.push('-'),
22652 Token::Eof => break,
22653 _ => {}
22654 }
22655 }
22656 name.push_str(&buf);
22657 }
22658 // Optional postfix `[]` widens to the array form —
22659 // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22660 // The engine's `type_name_to_data_type` recognises
22661 // the canonical `<ty>_array` form.
22662 if matches!(self.peek(), Token::LBracket)
22663 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22664 {
22665 self.advance();
22666 self.advance();
22667 name.push_str("_array");
22668 }
22669 CastTarget::Named(name)
22670 }
22671 },
22672 Token::Interval => CastTarget::Interval,
22673 // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22674 // "char" (oid 18, SPG Char1 — distinct from bare `char`
22675 // = char(1)); other quoted names resolve like idents.
22676 Token::QuotedIdent(q) => {
22677 if q.eq_ignore_ascii_case("char") {
22678 CastTarget::Named("char1".into())
22679 } else {
22680 CastTarget::Named(q.to_ascii_lowercase())
22681 }
22682 }
22683 other => {
22684 return Err(ParseError {
22685 message: format!("expected type ident after `::`, got {other:?}"),
22686 token_pos: self.consumed_pos(),
22687 });
22688 }
22689 };
22690 // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22691 // target to its array sibling. Closed-enum arms (Bool /
22692 // SmallInt / Numeric / Float / Date / …) didn't carry the
22693 // explicit widening that Text / Int / BigInt did, so
22694 // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22695 // error. The widening here mirrors the per-arm Text /
22696 // Int / BigInt logic above + folds the new ζ-A first-class
22697 // types through `CastTarget::Named("<ty>_array")`.
22698 if matches!(self.peek(), Token::LBracket)
22699 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22700 {
22701 let widened = match &target {
22702 CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22703 CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22704 // v7.39 (round 326, V43) — the two temporal types stay
22705 // distinct. Both used to widen to `timestamptz_array`, so
22706 // `::timestamp[]` named the wrong target in its own error
22707 // message and lost the zone-less identity on the way.
22708 CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22709 CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22710 CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22711 CastTarget::Json | CastTarget::Jsonb => {
22712 Some(CastTarget::Named("jsonb_array".to_string()))
22713 }
22714 CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22715 CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22716 CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22717 CastTarget::Named(name) => {
22718 let mut a = name.clone();
22719 a.push_str("_array");
22720 Some(CastTarget::Named(a))
22721 }
22722 // Int / BigInt / Text / Vector / TsVector / TsQuery /
22723 // RegType / RegClass / TextArray / IntArray /
22724 // BigIntArray already finalised — leave as is.
22725 _ => None,
22726 };
22727 if let Some(w) = widened {
22728 self.advance();
22729 self.advance();
22730 return Ok(w);
22731 }
22732 }
22733 Ok(target)
22734 }
22735
22736 fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
22737 loop {
22738 // v7.38 (read01, T9) — composite field access `(expr).field`.
22739 // A bare `a.b` is consumed as a qualified column inside the ident
22740 // atom, so a Dot only survives to this postfix position when the
22741 // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
22742 // `.*` whole-row expansion is not handled here (projection-level).
22743 if matches!(self.peek(), Token::Dot)
22744 && matches!(
22745 self.tokens.get(self.pos + 1),
22746 Some(Token::Ident(_) | Token::QuotedIdent(_))
22747 )
22748 {
22749 self.advance(); // .
22750 let field = match self.advance() {
22751 Token::Ident(s) | Token::QuotedIdent(s) => s,
22752 other => {
22753 return Err(
22754 self.err(format!("expected a field name after '.', got {other:?}"))
22755 );
22756 }
22757 };
22758 expr = Expr::FieldAccess {
22759 base: Box::new(expr),
22760 field,
22761 };
22762 continue;
22763 }
22764 if matches!(self.peek(), Token::DoubleColon) {
22765 self.advance();
22766 // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
22767 // target set to include INTERVAL (reserved Token),
22768 // TIMESTAMPTZ, and PG catalog regtype / regclass.
22769 // mailrs follow-up H3a + H3b.
22770 let target = self.parse_cast_target()?;
22771 expr = Expr::Cast {
22772 expr: Box::new(expr),
22773 target,
22774 };
22775 continue;
22776 }
22777 // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
22778 // returns NULL for out-of-range. Multiple subscripts
22779 // chain: `a[i][j]` parses left-to-right.
22780 if matches!(self.peek(), Token::LBracket) {
22781 self.advance();
22782 // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
22783 // bare index stays a subscript.
22784 let lo = if matches!(self.peek(), Token::Colon) {
22785 None
22786 } else {
22787 Some(self.parse_expr(0)?)
22788 };
22789 if matches!(self.peek(), Token::Colon) {
22790 self.advance();
22791 let hi = if matches!(self.peek(), Token::RBracket) {
22792 None
22793 } else {
22794 Some(Box::new(self.parse_expr(0)?))
22795 };
22796 if !matches!(self.peek(), Token::RBracket) {
22797 return Err(self.err(alloc::format!(
22798 "expected ']' after array slice, got {:?}",
22799 self.peek()
22800 )));
22801 }
22802 self.advance();
22803 expr = Expr::ArraySlice {
22804 target: Box::new(expr),
22805 lo: lo.map(Box::new),
22806 hi,
22807 };
22808 continue;
22809 }
22810 let index = lo.expect("non-colon branch parsed an index");
22811 if !matches!(self.peek(), Token::RBracket) {
22812 return Err(self.err(alloc::format!(
22813 "expected ']' after array index, got {:?}",
22814 self.peek()
22815 )));
22816 }
22817 self.advance();
22818 expr = Expr::ArraySubscript {
22819 target: Box::new(expr),
22820 index: Box::new(index),
22821 };
22822 continue;
22823 }
22824 // `expr AT TIME ZONE zone` — lowers to PG's own function
22825 // form timezone(zone, expr); the scalar implements the
22826 // offset shift (named zones error there — no tzdata).
22827 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
22828 && matches!(self.tokens.get(self.pos + 1),
22829 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
22830 && matches!(self.tokens.get(self.pos + 2),
22831 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
22832 {
22833 self.advance(); // AT
22834 self.advance(); // TIME
22835 self.advance(); // ZONE
22836 // Zone at comparison precedence so AND/OR stay out.
22837 let zone = self.parse_expr(6)?;
22838 expr = Expr::FunctionCall {
22839 name: "timezone".to_string(),
22840 args: alloc::vec![zone, expr],
22841 };
22842 continue;
22843 }
22844 // `expr COLLATE "name"` — SPG's single text ordering IS
22845 // byte order, i.e. the C collation. The byte-order
22846 // spellings absorb as no-ops; a locale collation would
22847 // silently sort differently from PG, so it errors
22848 // honestly instead.
22849 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
22850 self.advance();
22851 let mut cname = match self.advance() {
22852 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22853 other => {
22854 return Err(self.err(alloc::format!(
22855 "expected collation name after COLLATE, got {other:?}"
22856 )));
22857 }
22858 };
22859 // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
22860 // is how `pg_dump` writes the default one:
22861 // `… COLLATE pg_catalog.default`. Reading a single token
22862 // left the SCHEMA as the name, so the clause was refused
22863 // as an unsupported locale collation and no dump ran.
22864 if matches!(self.peek(), Token::Dot) {
22865 self.advance();
22866 cname = match self.advance() {
22867 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22868 // `default` lexes as a KEYWORD, and it is the name
22869 // pg_dump writes — the same trap round 535 hit with
22870 // TABLE / INDEX / FULL.
22871 Token::Default => alloc::string::String::from("default"),
22872 other => {
22873 return Err(self.err(alloc::format!(
22874 "expected collation name after COLLATE, got {other:?}"
22875 )));
22876 }
22877 };
22878 }
22879 let lc = cname.to_ascii_lowercase();
22880 // v7.39 (round 371, M4 P4b) — a per-expression MySQL
22881 // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
22882 // family / `binary`) forces byte-wise, which is exactly
22883 // what `BINARY expr` does — lower onto that so every fold
22884 // site (comparison, LIKE, ORDER BY) suppresses via
22885 // `is_binary_coerced`. A `_ci` family override folds, and
22886 // under the MySQL dialect the default already folds, so it
22887 // absorbs as a no-op; likewise the C / byte-order spellings.
22888 if self.mysql_dialect && (lc.ends_with("_bin") || lc == "binary") {
22889 expr = Expr::Cast {
22890 expr: alloc::boxed::Box::new(expr),
22891 target: CastTarget::Named("binary".to_string()),
22892 };
22893 continue;
22894 }
22895 let mysql_ci = self.mysql_dialect
22896 && (lc.ends_with("_ci")
22897 || matches!(lc.as_str(), "case_insensitive" | "nocase"));
22898 // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
22899 // goes to the lowering channel, the byte-order spellings
22900 // included. Round 691 recorded only the names the old
22901 // allow-list rejected, which left `ORDER BY a COLLATE "C"`
22902 // absorbed as a no-op — and once a column could declare a
22903 // collation, absorbing the clause meant the COLUMN's
22904 // collation won where the query had asked for bytes.
22905 if self.in_order_by_key && !mysql_ci {
22906 self.order_key_collation = Some(cname);
22907 continue;
22908 }
22909 if !matches!(
22910 lc.as_str(),
22911 "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
22912 ) && !mysql_ci
22913 {
22914 // v7.38.18 — the old message read "SPG orders text
22915 // by bytes (the C collation); locale collations are
22916 // not supported yet", and both halves were false by
22917 // the time it was read. This build performs locale
22918 // collations: declared on a column or written in an
22919 // ORDER BY key, `en_US.utf8` orders `apple, client,
22920 // DateStyle, Zebra` exactly as PG 18.4 does. What it
22921 // cannot do is carry a collation on an arbitrary
22922 // expression, because there is no `Expr::Collate` to
22923 // carry it — so say that, and say where the clause
22924 // does work rather than telling the reader to drop it.
22925 return Err(self.err(alloc::format!(
22926 "COLLATE {cname:?} is not supported in this position: \
22927 SPG carries a collation on a column declaration and \
22928 on an ORDER BY key, not on an arbitrary expression. \
22929 Declare it on the column (`x text COLLATE \
22930 {cname:?}`) or move it into the ORDER BY key"
22931 )));
22932 }
22933 continue;
22934 }
22935 return Ok(expr);
22936 }
22937 }
22938
22939 /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
22940 /// the first token that is not one. Schema qualifiers collapse to the
22941 /// last part, which is what every other name path here does (SPG is
22942 /// single-schema).
22943 fn take_comma_separated_names(&mut self) -> Vec<String> {
22944 let mut out = Vec::new();
22945 while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
22946 self.advance();
22947 let mut last = n;
22948 while matches!(self.peek(), Token::Dot) {
22949 self.advance();
22950 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
22951 last = t;
22952 }
22953 }
22954 out.push(last);
22955 if matches!(self.peek(), Token::Comma) {
22956 self.advance();
22957 } else {
22958 break;
22959 }
22960 }
22961 out
22962 }
22963
22964 /// v7.39 (round 694) — is the next token pair a postfix `[]`?
22965 ///
22966 /// The general cast-target path tests this inline; the types with their
22967 /// own `CastTarget` variant need it as a guard on their match arm,
22968 /// which is what this exists for.
22969 fn peek_postfix_array_brackets(&self) -> bool {
22970 matches!(self.peek(), Token::LBracket)
22971 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22972 }
22973
22974 /// Parse the operator tail after a `(a, b, …)` row constructor
22975 /// and expand at parse time. `=` is the conjunction of element
22976 /// equalities; `<>` its negation; the order operators expand
22977 /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
22978 /// equalities. Anything else (a bare row value, a subquery
22979 /// RHS) errors honestly — SPG has no composite runtime value.
22980 fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
22981 fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
22982 let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
22983 lhs: Box::new(l.clone()),
22984 op: BinOp::Eq,
22985 rhs: Box::new(r.clone()),
22986 });
22987 let first = it.next().expect("row has at least two elements");
22988 it.fold(first, |acc, e| Expr::Binary {
22989 lhs: Box::new(acc),
22990 op: BinOp::And,
22991 rhs: Box::new(e),
22992 })
22993 }
22994 // Lexicographic (a,b) OP (c,d):
22995 // a STRICT c OR (a = c AND (b OP d)) — recursing right.
22996 fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
22997 if lhs.len() == 1 {
22998 return Expr::Binary {
22999 lhs: Box::new(lhs[0].clone()),
23000 op: last,
23001 rhs: Box::new(rhs[0].clone()),
23002 };
23003 }
23004 let head_strict = Expr::Binary {
23005 lhs: Box::new(lhs[0].clone()),
23006 op: strict,
23007 rhs: Box::new(rhs[0].clone()),
23008 };
23009 let head_eq = Expr::Binary {
23010 lhs: Box::new(lhs[0].clone()),
23011 op: BinOp::Eq,
23012 rhs: Box::new(rhs[0].clone()),
23013 };
23014 Expr::Binary {
23015 lhs: Box::new(head_strict),
23016 op: BinOp::Or,
23017 rhs: Box::new(Expr::Binary {
23018 lhs: Box::new(head_eq),
23019 op: BinOp::And,
23020 rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
23021 }),
23022 }
23023 }
23024 let negated_in = if matches!(self.peek(), Token::Not)
23025 && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
23026 {
23027 self.advance();
23028 true
23029 } else {
23030 false
23031 };
23032 if matches!(self.peek(), Token::In) {
23033 self.advance();
23034 if !matches!(self.peek(), Token::LParen) {
23035 return Err(self.err(alloc::format!(
23036 "expected '(' after row IN, got {:?}",
23037 self.peek()
23038 )));
23039 }
23040 self.advance();
23041 // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
23042 // not a list of literal rows. Row-vs-list decomposes to
23043 // OR-of-AND above, but the subquery's rows are only known at
23044 // runtime, so keep it as a RowInSubquery node.
23045 if matches!(self.peek(), Token::Select) {
23046 let inner = self.parse_select_stmt()?;
23047 if !matches!(self.peek(), Token::RParen) {
23048 return Err(self.err(alloc::format!(
23049 "expected ')' after row IN-subquery, got {:?}",
23050 self.peek()
23051 )));
23052 }
23053 self.advance();
23054 let Statement::Select(s) = inner else {
23055 unreachable!("parse_select_stmt always returns Statement::Select")
23056 };
23057 return Ok(Expr::RowInSubquery {
23058 row,
23059 subquery: Box::new(s),
23060 negated: negated_in,
23061 });
23062 }
23063 let mut alternatives: Vec<Expr> = Vec::new();
23064 loop {
23065 // Optional ROW keyword before the paren row.
23066 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23067 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23068 {
23069 self.advance();
23070 }
23071 if !matches!(self.peek(), Token::LParen) {
23072 return Err(self.err(alloc::format!(
23073 "expected '(' to open a row inside IN, got {:?}",
23074 self.peek()
23075 )));
23076 }
23077 self.advance();
23078 let mut rhs = alloc::vec![self.parse_expr(0)?];
23079 while matches!(self.peek(), Token::Comma) {
23080 self.advance();
23081 rhs.push(self.parse_expr(0)?);
23082 }
23083 if !matches!(self.peek(), Token::RParen) {
23084 return Err(self.err(alloc::format!(
23085 "expected ')' after row inside IN, got {:?}",
23086 self.peek()
23087 )));
23088 }
23089 self.advance();
23090 if rhs.len() != row.len() {
23091 return Err(self.err(alloc::format!(
23092 "row IN arity mismatch: left has {}, right has {}",
23093 row.len(),
23094 rhs.len()
23095 )));
23096 }
23097 alternatives.push(row_eq(&row, &rhs));
23098 if matches!(self.peek(), Token::Comma) {
23099 self.advance();
23100 continue;
23101 }
23102 break;
23103 }
23104 if !matches!(self.peek(), Token::RParen) {
23105 return Err(self.err(alloc::format!(
23106 "expected ')' to close row IN list, got {:?}",
23107 self.peek()
23108 )));
23109 }
23110 self.advance();
23111 let mut it = alternatives.into_iter();
23112 let first = it.next().expect("IN list has at least one row");
23113 let combined = it.fold(first, |acc, e| Expr::Binary {
23114 lhs: Box::new(acc),
23115 op: BinOp::Or,
23116 rhs: Box::new(e),
23117 });
23118 return Ok(maybe_not(combined, negated_in));
23119 }
23120 // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
23121 // two periods share at least one time point. Each pair is
23122 // normalised with least/greatest (PG accepts the endpoints
23123 // in either order), then lowered to the standard
23124 // `start1 < end2 AND start2 < end1` form.
23125 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
23126 if row.len() != 2 {
23127 return Err(self.err(alloc::format!(
23128 "OVERLAPS needs (start, end) pairs; left side has {} elements",
23129 row.len()
23130 )));
23131 }
23132 self.advance();
23133 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23134 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23135 {
23136 self.advance();
23137 }
23138 if !matches!(self.peek(), Token::LParen) {
23139 return Err(self.err(alloc::format!(
23140 "expected '(' after OVERLAPS, got {:?}",
23141 self.peek()
23142 )));
23143 }
23144 self.advance();
23145 let r0 = self.parse_expr(0)?;
23146 if !matches!(self.peek(), Token::Comma) {
23147 return Err(self.err(alloc::format!(
23148 "OVERLAPS needs (start, end) on the right, got {:?}",
23149 self.peek()
23150 )));
23151 }
23152 self.advance();
23153 let r1 = self.parse_expr(0)?;
23154 if !matches!(self.peek(), Token::RParen) {
23155 return Err(self.err(alloc::format!(
23156 "expected ')' after OVERLAPS pair, got {:?}",
23157 self.peek()
23158 )));
23159 }
23160 self.advance();
23161 let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
23162 name: String::from(name),
23163 args: alloc::vec![a.clone(), b.clone()],
23164 };
23165 let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
23166 lhs: Box::new(lhs),
23167 op: BinOp::Lt,
23168 rhs: Box::new(rhs),
23169 };
23170 return Ok(Expr::Binary {
23171 lhs: Box::new(lt(
23172 pair_fn("least", &row[0], &row[1]),
23173 pair_fn("greatest", &r0, &r1),
23174 )),
23175 op: BinOp::And,
23176 rhs: Box::new(lt(
23177 pair_fn("least", &r0, &r1),
23178 pair_fn("greatest", &row[0], &row[1]),
23179 )),
23180 });
23181 }
23182 // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
23183 // PG, `IS NULL` is true only when EVERY field is NULL, and
23184 // `IS NOT NULL` is true only when every field is non-NULL — the
23185 // latter is NOT the negation of the former (a mixed row is
23186 // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
23187 // which reproduces exactly that all-fields semantics.
23188 if matches!(self.peek(), Token::Is) {
23189 self.advance();
23190 let negated = if matches!(self.peek(), Token::Not) {
23191 self.advance();
23192 true
23193 } else {
23194 false
23195 };
23196 if !matches!(self.peek(), Token::Null) {
23197 return Err(self.err(alloc::format!(
23198 "expected NULL after row IS [NOT], got {:?}",
23199 self.peek()
23200 )));
23201 }
23202 self.advance();
23203 let mut it = row.iter().map(|e| Expr::IsNull {
23204 expr: Box::new(e.clone()),
23205 negated,
23206 });
23207 let first = it.next().expect("row has at least two elements");
23208 return Ok(it.fold(first, |acc, e| Expr::Binary {
23209 lhs: Box::new(acc),
23210 op: BinOp::And,
23211 rhs: Box::new(e),
23212 }));
23213 }
23214 let op = match self.peek() {
23215 Token::Eq => BinOp::Eq,
23216 Token::NotEq => BinOp::NotEq,
23217 Token::Lt => BinOp::Lt,
23218 Token::LtEq => BinOp::LtEq,
23219 Token::Gt => BinOp::Gt,
23220 Token::GtEq => BinOp::GtEq,
23221 // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
23222 // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
23223 // constructor value, identical to the `ROW(a, b, …)` keyword form:
23224 // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
23225 // (`::text`, `.field`) applies at the caller just as it does for the
23226 // ROW(...) node. All the comparison / predicate forms returned above.
23227 _ => {
23228 return Ok(Expr::FunctionCall {
23229 name: String::from("row"),
23230 args: row,
23231 });
23232 }
23233 };
23234 self.advance();
23235 // Optional ROW keyword before the paren row.
23236 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23237 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23238 {
23239 self.advance();
23240 }
23241 if !matches!(self.peek(), Token::LParen) {
23242 return Err(self.err(alloc::format!(
23243 "expected '(' to open the right-hand row, got {:?}",
23244 self.peek()
23245 )));
23246 }
23247 self.advance();
23248 // `(a, b) <op> (SELECT x, y)` — compare against a single-row
23249 // subquery. Kept as a node (the subquery's row is a runtime value);
23250 // the literal-RHS form below still decomposes at parse time.
23251 if matches!(self.peek(), Token::Select) {
23252 let inner = self.parse_select_stmt()?;
23253 if !matches!(self.peek(), Token::RParen) {
23254 return Err(self.err(alloc::format!(
23255 "expected ')' after row comparison subquery, got {:?}",
23256 self.peek()
23257 )));
23258 }
23259 self.advance();
23260 let Statement::Select(s) = inner else {
23261 unreachable!("parse_select_stmt always returns Statement::Select")
23262 };
23263 return Ok(Expr::RowCmpSubquery {
23264 row,
23265 op,
23266 subquery: Box::new(s),
23267 });
23268 }
23269 let mut rhs = alloc::vec![self.parse_expr(0)?];
23270 while matches!(self.peek(), Token::Comma) {
23271 self.advance();
23272 rhs.push(self.parse_expr(0)?);
23273 }
23274 if !matches!(self.peek(), Token::RParen) {
23275 return Err(self.err(alloc::format!(
23276 "expected ')' after right-hand row, got {:?}",
23277 self.peek()
23278 )));
23279 }
23280 self.advance();
23281 if rhs.len() != row.len() {
23282 // v7.39 (round 239) — PG's wording (42601).
23283 return Err(self.err("unequal number of entries in row expressions".to_string()));
23284 }
23285 Ok(match op {
23286 BinOp::Eq => row_eq(&row, &rhs),
23287 BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
23288 BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
23289 BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
23290 BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
23291 BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
23292 _ => unreachable!("op restricted above"),
23293 })
23294 }
23295
23296 /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
23297 /// escape character becomes the matcher's default backslash:
23298 /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
23299 /// → the char itself, and any pre-existing backslash escapes
23300 /// itself so it stays literal. Both operands must be string
23301 /// literals — a runtime pattern would need matcher support.
23302 fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
23303 let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
23304 (&pattern, &esc)
23305 else {
23306 return Err(
23307 "LIKE ... ESCAPE requires string-literal pattern and escape \
23308 (runtime escape characters are not supported yet)"
23309 .into(),
23310 );
23311 };
23312 // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
23313 // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
23314 // multi-character escape is an error.
23315 let esc_ch: Option<char> = {
23316 let mut ch_iter = e.chars();
23317 match (ch_iter.next(), ch_iter.next()) {
23318 (Some(c), None) => Some(c),
23319 (None, _) => None,
23320 (Some(_), Some(_)) => {
23321 return Err(alloc::format!(
23322 "ESCAPE must be a single character, got {e:?}"
23323 ));
23324 }
23325 }
23326 };
23327 let mut out = String::with_capacity(p.len() + 4);
23328 let mut chars = p.chars();
23329 while let Some(c) = chars.next() {
23330 if Some(c) == esc_ch {
23331 match chars.next() {
23332 // Escaped wildcard / escaped escape → keep the
23333 // next char literal via backslash.
23334 Some(next) => {
23335 out.push('\\');
23336 out.push(next);
23337 }
23338 None => {
23339 return Err("LIKE pattern ends with the escape character".into());
23340 }
23341 }
23342 } else if c == '\\' && esc_ch != Some('\\') {
23343 // A raw backslash is literal under a custom (or absent) escape
23344 // — escape it for the backslash-based matcher.
23345 out.push('\\');
23346 out.push('\\');
23347 } else {
23348 out.push(c);
23349 }
23350 }
23351 Ok(Expr::Literal(Literal::String(out)))
23352 }
23353
23354 /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
23355 /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
23356 /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
23357 /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
23358 /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
23359 /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
23360 /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
23361 /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
23362 /// array expression errors honestly rather than silently mismatching.
23363 fn try_like_any_all(
23364 &mut self,
23365 base: &Expr,
23366 negated: bool,
23367 case_insensitive: bool,
23368 ) -> Result<Option<Expr>, ParseError> {
23369 let is_any = match self.peek() {
23370 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
23371 Token::Ident(s)
23372 if s.eq_ignore_ascii_case("any")
23373 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
23374 {
23375 true
23376 }
23377 _ => return Ok(None),
23378 };
23379 self.advance(); // ANY / ALL
23380 self.advance(); // '('
23381 let arr = self.parse_expr(0)?;
23382 if !matches!(self.peek(), Token::RParen) {
23383 return Err(self.err(format!(
23384 "expected ')' after LIKE {} argument, got {:?}",
23385 if is_any { "ANY" } else { "ALL" },
23386 self.peek()
23387 )));
23388 }
23389 self.advance(); // ')'
23390 let Expr::Array(items) = arr else {
23391 return Err(self.err(
23392 "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
23393 ));
23394 };
23395 let mut clauses = items.into_iter().map(|p| Expr::Like {
23396 expr: Box::new(base.clone()),
23397 pattern: Box::new(p),
23398 negated,
23399 case_insensitive,
23400 });
23401 let Some(first) = clauses.next() else {
23402 // ANY(empty) = FALSE, ALL(empty) = TRUE.
23403 return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
23404 };
23405 let op = if is_any { BinOp::Or } else { BinOp::And };
23406 let combined = clauses.fold(first, |acc, c| Expr::Binary {
23407 lhs: Box::new(acc),
23408 op,
23409 rhs: Box::new(c),
23410 });
23411 Ok(Some(combined))
23412 }
23413
23414 /// `x BETWEEN low AND high` → `(x >= low) AND (x <= high)`, wrapped in
23415 /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
23416 /// `AND` is not swallowed.
23417 fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23418 self.advance(); // BETWEEN
23419 // SYMMETRIC — the bounds may arrive in either order; both
23420 // orientations OR together. ASYMMETRIC is the default and
23421 // absorbs as noise.
23422 let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
23423 {
23424 self.advance();
23425 true
23426 } else {
23427 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
23428 self.advance();
23429 }
23430 false
23431 };
23432 let low = self.parse_expr(6)?;
23433 if !matches!(self.peek(), Token::And) {
23434 return Err(self.err(format!(
23435 "expected AND after BETWEEN low bound, got {:?}",
23436 self.peek()
23437 )));
23438 }
23439 self.advance();
23440 let high = self.parse_expr(6)?;
23441 let target = Box::new(expr);
23442 let range = |lo: Expr, hi: Expr| Expr::Binary {
23443 lhs: Box::new(Expr::Binary {
23444 lhs: target.clone(),
23445 op: BinOp::GtEq,
23446 rhs: Box::new(lo),
23447 }),
23448 op: BinOp::And,
23449 rhs: Box::new(Expr::Binary {
23450 lhs: target.clone(),
23451 op: BinOp::LtEq,
23452 rhs: Box::new(hi),
23453 }),
23454 };
23455 let combined = if symmetric {
23456 Expr::Binary {
23457 lhs: Box::new(range(low.clone(), high.clone())),
23458 op: BinOp::Or,
23459 rhs: Box::new(range(high, low)),
23460 }
23461 } else {
23462 range(low, high)
23463 };
23464 Ok(maybe_not(combined, negated))
23465 }
23466
23467 /// `x IN (a, b, c)` → chained OR of equalities. Empty list collapses
23468 /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
23469 /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
23470 /// Caller already consumed the leading `WITH` ident.
23471 /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
23472 /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
23473 /// self-reference that appears more than once in a single term.
23474 fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
23475 use crate::ast::{CteBody, SelectStatement};
23476 if !cte.recursive {
23477 return Ok(());
23478 }
23479 let CteBody::Select(body) = &cte.body else {
23480 return Ok(());
23481 };
23482 // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
23483 // check the anchor and every peer term.
23484 let has_order = |s: &SelectStatement| !s.order_by.is_empty();
23485 let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
23486 if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
23487 return Err(self.err(String::from(
23488 "ORDER BY in a recursive query is not implemented",
23489 )));
23490 }
23491 if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
23492 return Err(self.err(String::from(
23493 "LIMIT in a recursive query is not implemented",
23494 )));
23495 }
23496 let self_refs = |s: &SelectStatement| -> usize {
23497 let Some(from) = &s.from else {
23498 return 0;
23499 };
23500 let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
23501 for j in &from.joins {
23502 if j.table.name.eq_ignore_ascii_case(&cte.name) {
23503 n += 1;
23504 }
23505 }
23506 n
23507 };
23508 if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
23509 return Err(self.err(alloc::format!(
23510 "recursive reference to query \"{}\" must not appear more than once",
23511 cte.name
23512 )));
23513 }
23514 // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
23515 // apply only when the body actually references itself (a non-self-
23516 // referencing CTE under WITH RECURSIVE may use any set-op shape).
23517 let anchor_refs = self_refs(body);
23518 let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
23519 if anchor_refs > 0 || union_refs {
23520 // Shape: the top level must be UNION [ALL] arms only. A self-ref
23521 // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
23522 // "does not have the form" error — SPG used to compute a value.
23523 if body.unions.is_empty()
23524 || body.unions.iter().any(|(k, _)| {
23525 !matches!(
23526 k,
23527 crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
23528 )
23529 })
23530 {
23531 return Err(self.err(alloc::format!(
23532 "recursive query \"{}\" does not have the form non-recursive-term \
23533 UNION [ALL] recursive-term",
23534 cte.name
23535 )));
23536 }
23537 if anchor_refs > 0 {
23538 return Err(self.err(alloc::format!(
23539 "recursive reference to query \"{}\" must not appear within its non-recursive term",
23540 cte.name
23541 )));
23542 }
23543 }
23544 let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23545 for (_, u) in &body.unions {
23546 if self_refs(u) == 0 {
23547 continue;
23548 }
23549 // The self-reference must not sit on the nullable side of an outer
23550 // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23551 if let Some(from) = &u.from {
23552 for (i, j) in from.joins.iter().enumerate() {
23553 let left_has_self = is_self(&from.primary)
23554 || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23555 let violated = match j.kind {
23556 crate::ast::JoinKind::Left => is_self(&j.table),
23557 crate::ast::JoinKind::Right => left_has_self,
23558 crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23559 _ => false,
23560 };
23561 if violated {
23562 return Err(self.err(alloc::format!(
23563 "recursive reference to query \"{}\" must not appear within an outer join",
23564 cte.name
23565 )));
23566 }
23567 }
23568 }
23569 // No aggregates at the top level of the recursive term (SPG used
23570 // to run them and surface a misleading downstream error).
23571 let mut items_and_having: Vec<&Expr> = Vec::new();
23572 for it in &u.items {
23573 if let crate::ast::SelectItem::Expr { expr, .. } = it {
23574 items_and_having.push(expr);
23575 }
23576 }
23577 if let Some(h) = &u.having {
23578 items_and_having.push(h);
23579 }
23580 for e in items_and_having {
23581 if expr_has_toplevel_aggregate(e) {
23582 return Err(self.err(String::from(
23583 "aggregate functions are not allowed in a recursive query's recursive term",
23584 )));
23585 }
23586 }
23587 }
23588 // A self-reference inside a sublink expression (EXISTS / IN / scalar
23589 // subquery) anywhere in the body is rejected; a plain FROM derived
23590 // table is legal in PG and untouched here.
23591 let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23592 all_terms.extend(body.unions.iter().map(|(_, u)| u));
23593 for term in all_terms {
23594 if select_has_self_ref_in_sublink(term, &cte.name) {
23595 return Err(self.err(alloc::format!(
23596 "recursive reference to query \"{}\" must not appear within a subquery",
23597 cte.name
23598 )));
23599 }
23600 }
23601 Ok(())
23602 }
23603
23604 /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23605 /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23606 /// right after parse so the engine sees a plain recursive CTE with the
23607 /// tracking columns already projected. DEPTH FIRST and CYCLE are
23608 /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23609 /// text-rendered rows can't provide, and errors honestly.
23610 fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23611 use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23612 if cte.search.is_none() && cte.cycle.is_none() {
23613 return Ok(());
23614 }
23615 let cte_name = cte.name.clone();
23616 let col_names = cte.column_overrides.clone();
23617 if col_names.is_empty() {
23618 return Err(
23619 self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23620 );
23621 }
23622 let search = cte.search.take();
23623 let cycle = cte.cycle.take();
23624 let mut extra_cols: Vec<String> = Vec::new();
23625 let col_ref = |name: &str| {
23626 Expr::Column(ColumnName {
23627 qualifier: Some(cte_name.clone()),
23628 name: name.to_string(),
23629 })
23630 };
23631 // Position of a SEARCH/CYCLE column within the CTE's column list.
23632 let pos_of = |name: &str| -> Result<usize, ParseError> {
23633 col_names
23634 .iter()
23635 .position(|c| c.eq_ignore_ascii_case(name))
23636 .ok_or_else(|| {
23637 self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23638 })
23639 };
23640 let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23641 let mut args = Vec::with_capacity(positions.len());
23642 for &p in positions {
23643 match items.get(p) {
23644 Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23645 _ => {
23646 return Err(self.err(
23647 "SEARCH/CYCLE column maps to a non-expression select item".into(),
23648 ));
23649 }
23650 }
23651 }
23652 Ok(Expr::FunctionCall {
23653 name: "row".into(),
23654 args,
23655 })
23656 };
23657 let CteBody::Select(body) = &mut cte.body else {
23658 return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23659 };
23660 if body.unions.is_empty() {
23661 return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23662 }
23663 let rec = body.unions.len() - 1; // recursive term = last UNION peer
23664
23665 if let Some(srch) = search {
23666 // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23667 // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23668 // no typed `record[]`, but element-wise array ORDER BY is correct
23669 // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23670 // exactly onto a typed array: DEPTH is the root→node path
23671 // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23672 // orders numerically (multi-digit keys included), matching PG.
23673 //
23674 // A multi-column BY would need a record[] to keep the per-node key
23675 // tuple orderable, which SPG can't express — error honestly there
23676 // rather than mis-order.
23677 if srch.by_columns.len() != 1 {
23678 return Err(self.err(
23679 "SEARCH … BY with multiple columns needs typed record[] ordering \
23680 SPG doesn't have yet; a single BY column is supported"
23681 .into(),
23682 ));
23683 }
23684 let key_pos = pos_of(&srch.by_columns[0])?;
23685 let base_key = match body.items.get(key_pos) {
23686 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23687 _ => {
23688 return Err(
23689 self.err("SEARCH BY column maps to a non-expression select item".into())
23690 );
23691 }
23692 };
23693 let rec_key = match body.unions[rec].1.items.get(key_pos) {
23694 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23695 _ => {
23696 return Err(
23697 self.err("SEARCH BY column maps to a non-expression select item".into())
23698 );
23699 }
23700 };
23701 if srch.depth_first {
23702 // base: ARRAY[key]; rec: array_append(cte.set, key).
23703 body.items.push(SelectItem::Expr {
23704 expr: Expr::Array(alloc::vec![base_key]),
23705 alias: Some(srch.set_column.clone()),
23706 });
23707 body.unions[rec].1.items.push(SelectItem::Expr {
23708 expr: Expr::FunctionCall {
23709 name: "array_append".into(),
23710 args: alloc::vec![col_ref(&srch.set_column), rec_key],
23711 },
23712 alias: Some(srch.set_column.clone()),
23713 });
23714 } else {
23715 // BREADTH: [depth, key]; depth starts at 0 and increments. The
23716 // leading depth element dominates the element-wise comparison,
23717 // so shallower rows sort first, then by key — PG's (depth, key).
23718 body.items.push(SelectItem::Expr {
23719 expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
23720 alias: Some(srch.set_column.clone()),
23721 });
23722 // rec depth = cte.set[1] + 1.
23723 let parent_depth = Expr::ArraySubscript {
23724 target: Box::new(col_ref(&srch.set_column)),
23725 index: Box::new(Expr::Literal(Literal::Integer(1))),
23726 };
23727 body.unions[rec].1.items.push(SelectItem::Expr {
23728 expr: Expr::Array(alloc::vec![
23729 Expr::Binary {
23730 lhs: Box::new(parent_depth),
23731 op: BinOp::Add,
23732 rhs: Box::new(Expr::Literal(Literal::Integer(1))),
23733 },
23734 rec_key,
23735 ]),
23736 alias: Some(srch.set_column.clone()),
23737 });
23738 }
23739 extra_cols.push(srch.set_column);
23740 }
23741
23742 if let Some(cyc) = cycle {
23743 let positions: Vec<usize> = cyc
23744 .columns
23745 .iter()
23746 .map(|c| pos_of(c))
23747 .collect::<Result<_, _>>()?;
23748 // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
23749 // cast it to text for the cycle path: membership only needs equality,
23750 // and the record text form gives SPG a TextArray path (SPG has no
23751 // typed record[] array). Cycle detection is unaffected.
23752 let base_row = Expr::Cast {
23753 expr: Box::new(row_of(&body.items, &positions)?),
23754 target: CastTarget::Text,
23755 };
23756 let rec_row = Expr::Cast {
23757 expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
23758 target: CastTarget::Text,
23759 };
23760 let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
23761 let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
23762 // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
23763 body.items.push(SelectItem::Expr {
23764 expr: Expr::Literal(dflt.clone()),
23765 alias: Some(cyc.mark_column.clone()),
23766 });
23767 body.items.push(SelectItem::Expr {
23768 expr: Expr::Array(alloc::vec![base_row]),
23769 alias: Some(cyc.path_column.clone()),
23770 });
23771 // rec mark: ROW(cols) already in the path → cycle.
23772 let hit = Expr::AnyAll {
23773 expr: Box::new(rec_row.clone()),
23774 op: BinOp::Eq,
23775 array: Box::new(col_ref(&cyc.path_column)),
23776 is_any: true,
23777 };
23778 let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
23779 Expr::Case {
23780 operand: None,
23781 branches: alloc::vec![(hit, Expr::Literal(mark))],
23782 else_branch: Some(Box::new(Expr::Literal(dflt))),
23783 }
23784 } else {
23785 hit
23786 };
23787 body.unions[rec].1.items.push(SelectItem::Expr {
23788 expr: mark_expr,
23789 alias: Some(cyc.mark_column.clone()),
23790 });
23791 // rec path: array_append(cte.path, ROW(cols)).
23792 body.unions[rec].1.items.push(SelectItem::Expr {
23793 expr: Expr::FunctionCall {
23794 name: "array_append".into(),
23795 args: alloc::vec![col_ref(&cyc.path_column), rec_row],
23796 },
23797 alias: Some(cyc.path_column.clone()),
23798 });
23799 // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
23800 let stop = Expr::Unary {
23801 op: UnOp::Not,
23802 expr: Box::new(col_ref(&cyc.mark_column)),
23803 };
23804 let w = &mut body.unions[rec].1.where_;
23805 *w = Some(match w.take() {
23806 Some(prev) => Expr::Binary {
23807 lhs: Box::new(prev),
23808 op: BinOp::And,
23809 rhs: Box::new(stop),
23810 },
23811 None => stop,
23812 });
23813 extra_cols.push(cyc.mark_column);
23814 extra_cols.push(cyc.path_column);
23815 }
23816 cte.column_overrides.extend(extra_cols);
23817 Ok(())
23818 }
23819
23820 /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
23821 /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
23822 fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
23823 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
23824 return Ok(None);
23825 }
23826 self.advance(); // SEARCH
23827 let depth_first = match self.peek() {
23828 Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
23829 Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
23830 other => {
23831 return Err(self.err(format!(
23832 "expected DEPTH or BREADTH after SEARCH, got {other:?}"
23833 )));
23834 }
23835 };
23836 self.advance();
23837 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
23838 return Err(self.err(format!(
23839 "expected FIRST after SEARCH mode, got {:?}",
23840 self.peek()
23841 )));
23842 }
23843 self.advance();
23844 if !self.peek_is_by() {
23845 return Err(self.err(format!(
23846 "expected BY after SEARCH … FIRST, got {:?}",
23847 self.peek()
23848 )));
23849 }
23850 self.advance();
23851 let mut by_columns = alloc::vec![self.expect_ident_like()?];
23852 while matches!(self.peek(), Token::Comma) {
23853 self.advance();
23854 by_columns.push(self.expect_ident_like()?);
23855 }
23856 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23857 return Err(self.err(format!(
23858 "expected SET in SEARCH clause, got {:?}",
23859 self.peek()
23860 )));
23861 }
23862 self.advance();
23863 let set_column = self.expect_ident_like()?;
23864 Ok(Some(crate::ast::SearchClause {
23865 depth_first,
23866 by_columns,
23867 set_column,
23868 }))
23869 }
23870
23871 /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
23872 /// USING pathcol`. Returns None when the next token isn't CYCLE.
23873 fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
23874 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
23875 return Ok(None);
23876 }
23877 self.advance(); // CYCLE
23878 let mut columns = alloc::vec![self.expect_ident_like()?];
23879 while matches!(self.peek(), Token::Comma) {
23880 self.advance();
23881 columns.push(self.expect_ident_like()?);
23882 }
23883 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23884 return Err(self.err(format!(
23885 "expected SET in CYCLE clause, got {:?}",
23886 self.peek()
23887 )));
23888 }
23889 self.advance();
23890 let mark_column = self.expect_ident_like()?;
23891 let mut mark_value = None;
23892 let mut default_value = None;
23893 if matches!(self.peek(), Token::To) {
23894 self.advance();
23895 mark_value = Some(self.parse_cycle_literal()?);
23896 if !matches!(self.peek(), Token::Default) {
23897 return Err(self.err(format!(
23898 "expected DEFAULT after CYCLE … TO, got {:?}",
23899 self.peek()
23900 )));
23901 }
23902 self.advance();
23903 default_value = Some(self.parse_cycle_literal()?);
23904 }
23905 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
23906 return Err(self.err(format!(
23907 "expected USING in CYCLE clause, got {:?}",
23908 self.peek()
23909 )));
23910 }
23911 self.advance();
23912 let path_column = self.expect_ident_like()?;
23913 Ok(Some(crate::ast::CycleClause {
23914 columns,
23915 mark_column,
23916 mark_value,
23917 default_value,
23918 path_column,
23919 }))
23920 }
23921
23922 /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
23923 /// literal (string / bool / number) in PG.
23924 fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
23925 match self.parse_expr(0)? {
23926 Expr::Literal(l) => Ok(l),
23927 other => Err(self.err(format!(
23928 "CYCLE mark/default value must be a literal, got {other:?}"
23929 ))),
23930 }
23931 }
23932
23933 fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
23934 // v4.22: WITH RECURSIVE — optional keyword right after WITH.
23935 // Comes through as an identifier; consume it if present and
23936 // mark every CTE in the clause as recursive (PG semantics —
23937 // the flag is per-WITH, not per-CTE).
23938 let mut recursive = false;
23939 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
23940 && s.eq_ignore_ascii_case("recursive")
23941 {
23942 self.advance();
23943 recursive = true;
23944 }
23945 let mut ctes = Vec::new();
23946 loop {
23947 let name = self.expect_ident_like()?;
23948 // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
23949 // PG uses these to rename the body's output columns; we
23950 // do the same below by overriding `columns[i].name`.
23951 let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
23952 self.advance();
23953 let mut names = Vec::new();
23954 loop {
23955 names.push(self.expect_ident_like()?);
23956 if matches!(self.peek(), Token::Comma) {
23957 self.advance();
23958 continue;
23959 }
23960 break;
23961 }
23962 if !matches!(self.peek(), Token::RParen) {
23963 return Err(self.err(format!(
23964 "expected ')' to close CTE column list, got {:?}",
23965 self.peek()
23966 )));
23967 }
23968 self.advance();
23969 names
23970 } else {
23971 Vec::new()
23972 };
23973 // AS is a reserved Token::As (used by SELECT-item / FROM
23974 // aliasing) — handle it specially rather than as a bare
23975 // ident.
23976 if !matches!(self.peek(), Token::As) {
23977 return Err(self.err(format!(
23978 "expected AS after CTE name {name:?}, got {:?}",
23979 self.peek()
23980 )));
23981 }
23982 self.advance();
23983 // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
23984 // MATERIALIZED` optimizer hints. SPG materialises every
23985 // CTE, so both spellings are accepted and absorbed.
23986 if matches!(self.peek(), Token::Not) {
23987 self.advance(); // NOT
23988 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23989 if s.eq_ignore_ascii_case("materialized"))
23990 {
23991 self.advance();
23992 } else {
23993 return Err(self.err(format!(
23994 "expected MATERIALIZED after AS NOT, got {:?}",
23995 self.peek()
23996 )));
23997 }
23998 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23999 if s.eq_ignore_ascii_case("materialized"))
24000 {
24001 self.advance();
24002 }
24003 if !matches!(self.peek(), Token::LParen) {
24004 return Err(self.err(format!(
24005 "expected '(' after AS in WITH clause, got {:?}",
24006 self.peek()
24007 )));
24008 }
24009 self.advance();
24010 // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
24011 // RETURNING) as the CTE body in addition to SELECT.
24012 // PG writable CTE semantics. UPDATE / DELETE come in as
24013 // bare Idents (lexer keeps SELECT / INSERT as reserved
24014 // tokens but treats the rest of DML as case-insensitive
24015 // idents).
24016 let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24017 let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24018 let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24019 let body = match self.peek() {
24020 Token::Select => {
24021 let inner = self.parse_select_stmt()?;
24022 let Statement::Select(s) = inner else {
24023 unreachable!("parse_select_stmt returns Select");
24024 };
24025 crate::ast::CteBody::Select(s)
24026 }
24027 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24028 // `SELECT * FROM t` this way and accepts it wherever a
24029 // SELECT goes, so the CTE body dispatch needs its own
24030 // arm: this match is keyed on the FIRST token, and
24031 // `Token::Table` fell through to a tail that then
24032 // rejected what it got. `parse_table_shorthand` has
24033 // returned a desugared SelectStatement since the
24034 // shorthand landed — only the routing was missing.
24035 // Round 868 found this by putting the shorthand in a
24036 // subquery; every earlier check used a top-level form.
24037 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24038 // `SELECT * FROM t` this way and accepts it wherever a
24039 // SELECT goes, so the CTE body dispatch needs its own
24040 // arm: this match is keyed on the FIRST token, and
24041 // `Token::Table` fell through to a tail that rejected
24042 // what it got. `parse_table_shorthand` has returned a
24043 // desugared SelectStatement since the shorthand landed —
24044 // only the routing was missing, here and in the derived
24045 // table's second-token gate. Round 868 found both by
24046 // putting the shorthand in a subquery; every earlier
24047 // check had used a top-level form.
24048 Token::Table
24049 if matches!(
24050 self.tokens.get(self.pos + 1),
24051 Some(Token::Ident(_) | Token::QuotedIdent(_))
24052 ) =>
24053 {
24054 let mut head = self.parse_table_shorthand()?;
24055 self.parse_setop_chain_into(&mut head)?;
24056 self.parse_select_tail_into(&mut head)?;
24057 crate::ast::CteBody::Select(head)
24058 }
24059 // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
24060 // WITH t(a) AS (VALUES (1), (2)) … lowers through
24061 // the shared rows helper onto a Select body.
24062 Token::Values => {
24063 self.advance(); // VALUES
24064 let mut head = self.parse_values_rows_body()?;
24065 // A VALUES seed can head a set-operation chain —
24066 // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
24067 // SELECT n+1 FROM t …). Attach any trailing
24068 // UNION / INTERSECT / EXCEPT peers so the
24069 // recursive-CTE body parses like the SELECT seed.
24070 self.parse_setop_chain_into(&mut head)?;
24071 crate::ast::CteBody::Select(head)
24072 }
24073 Token::Insert => {
24074 let inner = self.parse_one_statement()?;
24075 let Statement::Insert(s) = inner else {
24076 unreachable!("Token::Insert routes to Insert");
24077 };
24078 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24079 }
24080 _ if is_update_kw => {
24081 let inner = self.parse_one_statement()?;
24082 let Statement::Update(s) = inner else {
24083 return Err(
24084 self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
24085 );
24086 };
24087 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24088 }
24089 _ if is_delete_kw => {
24090 let inner = self.parse_one_statement()?;
24091 let Statement::Delete(s) = inner else {
24092 return Err(
24093 self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
24094 );
24095 };
24096 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24097 }
24098 // v7.39 (round 149) — PG 17 allows MERGE as a
24099 // data-modifying CTE body.
24100 _ if is_merge_kw => {
24101 let inner = self.parse_one_statement()?;
24102 let Statement::Merge(s) = inner else {
24103 return Err(
24104 self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
24105 );
24106 };
24107 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24108 }
24109 // v7.39 (round 151) — a CTE body may itself be
24110 // WITH-headed (PG grammar: PreparableStmt carries its
24111 // own with_clause). The nested statement keeps its own
24112 // ctes; the modifying-CTE-at-top-level rule is enforced
24113 // at execution.
24114 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
24115 self.advance(); // WITH
24116 match self.parse_with_cte_then_select()? {
24117 Statement::Select(s) => crate::ast::CteBody::Select(s),
24118 Statement::Insert(s) => {
24119 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24120 }
24121 Statement::Update(s) => {
24122 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24123 }
24124 Statement::Delete(s) => {
24125 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24126 }
24127 Statement::Merge(s) => {
24128 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24129 }
24130
24131 other => {
24132 return Err(self.err(format!(
24133 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24134 )));
24135 }
24136 }
24137 }
24138 other => {
24139 return Err(self.err(format!(
24140 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24141 )));
24142 }
24143 };
24144 if !matches!(self.peek(), Token::RParen) {
24145 return Err(self.err(format!(
24146 "expected ')' after CTE body, got {:?}",
24147 self.peek()
24148 )));
24149 }
24150 self.advance();
24151 // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
24152 // CTE, desugared into extra body columns by the engine.
24153 let search = self.parse_cte_search_clause()?;
24154 let cycle = self.parse_cte_cycle_clause()?;
24155 let mut cte = crate::ast::Cte {
24156 name,
24157 body,
24158 recursive,
24159 column_overrides,
24160 search,
24161 cycle,
24162 };
24163 self.validate_recursive_cte(&cte)?;
24164 self.desugar_cte_search_cycle(&mut cte)?;
24165 ctes.push(cte);
24166 if matches!(self.peek(), Token::Comma) {
24167 self.advance();
24168 continue;
24169 }
24170 break;
24171 }
24172 // v7.37.43-T4.4 — the outer body may be SELECT (classical),
24173 // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
24174 // the parsed CTEs to whichever statement the body produces.
24175 let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24176 let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24177 let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24178 match self.peek() {
24179 Token::Select => {
24180 let body_stmt = self.parse_select_stmt()?;
24181 let Statement::Select(mut body) = body_stmt else {
24182 unreachable!()
24183 };
24184 body.ctes = ctes;
24185 Ok(Statement::Select(body))
24186 }
24187 Token::Insert => {
24188 let body_stmt = self.parse_one_statement()?;
24189 let Statement::Insert(mut body) = body_stmt else {
24190 unreachable!()
24191 };
24192 body.ctes = ctes;
24193 Ok(Statement::Insert(body))
24194 }
24195 _ if outer_is_update => {
24196 let body_stmt = self.parse_one_statement()?;
24197 let Statement::Update(mut body) = body_stmt else {
24198 return Err(self.err(format!("expected UPDATE after WITH clause")));
24199 };
24200 body.ctes = ctes;
24201 Ok(Statement::Update(body))
24202 }
24203 _ if outer_is_delete => {
24204 let body_stmt = self.parse_one_statement()?;
24205 let Statement::Delete(mut body) = body_stmt else {
24206 return Err(self.err(format!("expected DELETE after WITH clause")));
24207 };
24208 body.ctes = ctes;
24209 Ok(Statement::Delete(body))
24210 }
24211 // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
24212 // WITH RECURSIVE is rejected with PG's exact message
24213 // (parse analysis, transformWithClause).
24214 _ if outer_is_merge => {
24215 if recursive {
24216 return Err(self.err(String::from(
24217 "WITH RECURSIVE is not supported for MERGE statement",
24218 )));
24219 }
24220 let body_stmt = self.parse_one_statement()?;
24221 let Statement::Merge(mut body) = body_stmt else {
24222 return Err(self.err(format!("expected MERGE after WITH clause")));
24223 };
24224 body.ctes = ctes;
24225 Ok(Statement::Merge(body))
24226 }
24227 other => Err(self.err(format!(
24228 "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
24229 ))),
24230 }
24231 }
24232
24233 /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
24234 /// already consumed the leading `EXISTS` ident via
24235 /// `self.advance()`.
24236 /// v7.13.0 — parse the rest of a `CASE … END` expression after
24237 /// the leading `CASE` ident has been consumed (mailrs round-5
24238 /// G9). Supports both the searched form
24239 /// (`CASE WHEN cond THEN val …`) and the simple form
24240 /// (`CASE operand WHEN val THEN val …`).
24241 fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
24242 // Disambiguate searched vs simple form: if the next token
24243 // is `WHEN`, we're in the searched form. Otherwise the
24244 // intervening expression is the operand.
24245 let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
24246 None
24247 } else {
24248 Some(Box::new(self.parse_expr(0)?))
24249 };
24250 let mut branches: Vec<(Expr, Expr)> = Vec::new();
24251 loop {
24252 match self.peek() {
24253 Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
24254 self.advance();
24255 let cond = self.parse_expr(0)?;
24256 match self.peek() {
24257 Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
24258 self.advance();
24259 }
24260 other => {
24261 return Err(self.err(alloc::format!(
24262 "expected THEN after CASE WHEN <expr>, got {other:?}"
24263 )));
24264 }
24265 }
24266 let value = self.parse_expr(0)?;
24267 branches.push((cond, value));
24268 }
24269 _ => break,
24270 }
24271 }
24272 if branches.is_empty() {
24273 return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
24274 }
24275 let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
24276 {
24277 self.advance();
24278 Some(Box::new(self.parse_expr(0)?))
24279 } else {
24280 None
24281 };
24282 match self.peek() {
24283 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
24284 self.advance();
24285 }
24286 other => {
24287 return Err(self.err(alloc::format!(
24288 "expected END to close CASE expression, got {other:?}"
24289 )));
24290 }
24291 }
24292 Ok(Expr::Case {
24293 operand,
24294 branches,
24295 else_branch,
24296 })
24297 }
24298
24299 /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
24300 /// query-source position (EXISTS / IN / INSERT source / CTE body /
24301 /// view body). Caller consumed the WITH keyword. Only a SELECT
24302 /// outer is grammatical here; the data-modifying-CTE-at-top-level
24303 /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
24304 /// maps correctly.
24305 fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24306 let inner = self.parse_with_cte_then_select()?;
24307 match inner {
24308 Statement::Select(s) => Ok(s),
24309 other => Err(self.err(format!(
24310 "expected SELECT after WITH in a subquery, got {other:?}"
24311 ))),
24312 }
24313 }
24314
24315 /// True when the next token is the (unquoted) WITH keyword. WITH is
24316 /// reserved in PG, so a bare `with` can never be a column reference
24317 /// in these positions; a quoted `"with"` stays an identifier.
24318 fn peek_is_with_kw(&self) -> bool {
24319 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
24320 }
24321
24322 /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
24323 /// `#[inline(never)]` keeps the large SelectStatement temporaries
24324 /// off parse_expr's recursive frame (the nesting-budget stack
24325 /// cliff — see the round-153 gate regression).
24326 #[inline(never)]
24327 fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24328 if self.peek_is_with_kw() {
24329 self.advance();
24330 self.parse_nested_with_select()
24331 } else {
24332 match self.parse_select_stmt()? {
24333 Statement::Select(s) => Ok(s),
24334 other => Err(self.err(alloc::format!(
24335 "expected SELECT inside ANY/ALL, got {other:?}"
24336 ))),
24337 }
24338 }
24339 }
24340
24341 fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
24342 if !matches!(self.peek(), Token::LParen) {
24343 return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
24344 }
24345 self.advance();
24346 // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
24347 let s = if self.peek_is_with_kw() {
24348 self.advance();
24349 self.parse_nested_with_select()?
24350 } else {
24351 let inner = self.parse_select_stmt()?;
24352 let Statement::Select(s) = inner else {
24353 unreachable!("parse_select_stmt returns Select")
24354 };
24355 s
24356 };
24357 if !matches!(self.peek(), Token::RParen) {
24358 return Err(self.err(format!(
24359 "expected ')' after EXISTS-subquery, got {:?}",
24360 self.peek()
24361 )));
24362 }
24363 self.advance();
24364 Ok(Expr::Exists {
24365 subquery: Box::new(s),
24366 negated,
24367 })
24368 }
24369
24370 fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
24371 self.advance(); // IN
24372 if !matches!(self.peek(), Token::LParen) {
24373 return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
24374 }
24375 self.advance();
24376 // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
24377 // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
24378 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
24379 let s = if self.peek_is_with_kw() {
24380 self.advance();
24381 self.parse_nested_with_select()?
24382 } else {
24383 let inner = self.parse_select_stmt()?;
24384 let Statement::Select(s) = inner else {
24385 unreachable!("parse_select_stmt always returns Statement::Select")
24386 };
24387 s
24388 };
24389 if !matches!(self.peek(), Token::RParen) {
24390 return Err(self.err(format!(
24391 "expected ')' after IN-subquery, got {:?}",
24392 self.peek()
24393 )));
24394 }
24395 self.advance();
24396 return Ok(Expr::InSubquery {
24397 expr: Box::new(expr),
24398 subquery: Box::new(s),
24399 negated,
24400 });
24401 }
24402 let mut elements = Vec::new();
24403 if !matches!(self.peek(), Token::RParen) {
24404 loop {
24405 elements.push(self.parse_expr(0)?);
24406 match self.peek() {
24407 Token::Comma => {
24408 self.advance();
24409 }
24410 Token::RParen => break,
24411 other => {
24412 return Err(
24413 self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
24414 );
24415 }
24416 }
24417 }
24418 }
24419 self.advance(); // ')'
24420 // v7.30.2 (mailrs round-25) — flat InList node instead of a
24421 // left-deep OR-Eq chain: chain depth scaled with the element
24422 // count and overflowed the stack (eval + drop are recursive).
24423 if elements.is_empty() {
24424 return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
24425 }
24426 Ok(Expr::InList {
24427 expr: Box::new(expr),
24428 list: elements,
24429 negated,
24430 })
24431 }
24432
24433 /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
24434 /// already consumed by the caller. Elements must be numeric literals
24435 /// (with optional unary `-`); any compound expression is rejected at
24436 /// parse time so the runtime never needs to evaluate inside a vector.
24437 /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
24438 /// has already consumed the `EXTRACT` token before calling us —
24439 /// we pick up at the opening `(`.
24440 /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
24441 /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
24442 /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
24443 /// per-column OR-fold of
24444 /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
24445 /// term)` so the existing FTS evaluator handles semantics.
24446 ///
24447 /// The mode modifier is accepted-and-ignored at v7.17 — all
24448 /// modes map to the same `plainto_tsquery` rewrite. Boolean-
24449 /// mode operators (`+foo -bar`) would need their own parser
24450 /// (Phase 2.2c); customers who hit them today already get a
24451 /// correct lexeme-match against the bare term, only without
24452 /// the +/- precedence the customer asked for.
24453 fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
24454 // Already at `MATCH`-consumed position; the dispatcher
24455 // confirmed the next token is `(`.
24456 if !matches!(self.peek(), Token::LParen) {
24457 return Err(self.err(alloc::format!(
24458 "expected '(' after MATCH, got {:?}",
24459 self.peek()
24460 )));
24461 }
24462 self.advance();
24463 let mut cols: Vec<Expr> = Vec::new();
24464 loop {
24465 cols.push(self.parse_expr(0)?);
24466 match self.peek() {
24467 Token::Comma => {
24468 self.advance();
24469 }
24470 Token::RParen => break,
24471 other => {
24472 return Err(self.err(alloc::format!(
24473 "expected ',' or ')' in MATCH column list, got {other:?}"
24474 )));
24475 }
24476 }
24477 }
24478 self.advance(); // ')'
24479 // Expect AGAINST.
24480 match self.peek() {
24481 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
24482 self.advance();
24483 }
24484 other => {
24485 return Err(self.err(alloc::format!(
24486 "expected AGAINST after MATCH column list, got {other:?}"
24487 )));
24488 }
24489 }
24490 if !matches!(self.peek(), Token::LParen) {
24491 return Err(self.err(alloc::format!(
24492 "expected '(' after AGAINST, got {:?}",
24493 self.peek()
24494 )));
24495 }
24496 self.advance();
24497 // Read AGAINST's argument as a single primary token —
24498 // string literal, placeholder, or column-ref ident. We
24499 // can't call `parse_expr` / `parse_unary` here because
24500 // the postfix chain inside `parse_atom` would greedily
24501 // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
24502 // and fail at "expected '(' after IN". Customers always
24503 // write a literal or bound parameter in AGAINST, so this
24504 // restriction is non-blocking; the error path explains
24505 // the limit if a more complex expression shows up.
24506 let term = match self.advance() {
24507 Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
24508 Token::Placeholder(n) => Expr::Placeholder(n),
24509 Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
24510 qualifier: None,
24511 name: s,
24512 }),
24513 other => {
24514 return Err(self.err(alloc::format!(
24515 "MATCH ... AGAINST(<term>) expects a string literal, \
24516 bound parameter, or column ref, got {other:?}"
24517 )));
24518 }
24519 };
24520 // Optional mode tail — accept-and-ignore at v7.17:
24521 // IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
24522 // IN BOOLEAN MODE
24523 // WITH QUERY EXPANSION
24524 loop {
24525 match self.peek() {
24526 // IN lexes as a reserved Token::In, not an ident,
24527 // so it gets its own arm.
24528 Token::In => {
24529 self.advance();
24530 }
24531 Token::Ident(s) | Token::QuotedIdent(s)
24532 if s.eq_ignore_ascii_case("natural")
24533 || s.eq_ignore_ascii_case("language")
24534 || s.eq_ignore_ascii_case("boolean")
24535 || s.eq_ignore_ascii_case("mode")
24536 || s.eq_ignore_ascii_case("with")
24537 || s.eq_ignore_ascii_case("query")
24538 || s.eq_ignore_ascii_case("expansion") =>
24539 {
24540 self.advance();
24541 }
24542 _ => break,
24543 }
24544 }
24545 if !matches!(self.peek(), Token::RParen) {
24546 return Err(self.err(alloc::format!(
24547 "expected ')' to close AGAINST, got {:?}",
24548 self.peek()
24549 )));
24550 }
24551 self.advance();
24552 // Build per-column `to_tsvector('simple', col) @@
24553 // plainto_tsquery('simple', term)` and OR-fold.
24554 let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24555 let plainto = Expr::FunctionCall {
24556 name: String::from("plainto_tsquery"),
24557 args: alloc::vec![simple_lit(), term.clone()],
24558 };
24559 let mut folded: Option<Expr> = None;
24560 for col in cols {
24561 let to_tsv = Expr::FunctionCall {
24562 name: String::from("to_tsvector"),
24563 args: alloc::vec![simple_lit(), col],
24564 };
24565 let leaf = Expr::Binary {
24566 lhs: Box::new(to_tsv),
24567 op: crate::ast::BinOp::TsMatch,
24568 rhs: Box::new(plainto.clone()),
24569 };
24570 folded = Some(match folded {
24571 None => leaf,
24572 Some(prev) => Expr::Binary {
24573 lhs: Box::new(prev),
24574 op: crate::ast::BinOp::Or,
24575 rhs: Box::new(leaf),
24576 },
24577 });
24578 }
24579 match folded {
24580 Some(e) => Ok(e),
24581 None => Err(self.err(String::from(
24582 "MATCH(...) AGAINST(...) requires at least one column",
24583 ))),
24584 }
24585 }
24586
24587 fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24588 if !matches!(self.peek(), Token::LParen) {
24589 return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24590 }
24591 self.advance();
24592 let field_name = self.expect_ident_like()?;
24593 let field = match field_name.to_ascii_lowercase().as_str() {
24594 // PG accepts the plural spellings (years/months/…/millenniums) as
24595 // aliases for the singular fields — its datetime unit table has both.
24596 // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24597 "year" | "years" => ExtractField::Year,
24598 "month" | "months" => ExtractField::Month,
24599 "day" | "days" => ExtractField::Day,
24600 "hour" | "hours" => ExtractField::Hour,
24601 "minute" | "minutes" => ExtractField::Minute,
24602 "second" | "seconds" => ExtractField::Second,
24603 "microsecond" | "microseconds" => ExtractField::Microsecond,
24604 "epoch" => ExtractField::Epoch,
24605 "dow" => ExtractField::Dow,
24606 "isodow" => ExtractField::Isodow,
24607 "doy" => ExtractField::Doy,
24608 "week" | "weeks" => ExtractField::Week,
24609 "isoyear" => ExtractField::Isoyear,
24610 "quarter" => ExtractField::Quarter,
24611 "decade" | "decades" => ExtractField::Decade,
24612 "century" | "centuries" => ExtractField::Century,
24613 "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24614 "julian" => ExtractField::Julian,
24615 "millisecond" | "milliseconds" => ExtractField::Millisecond,
24616 "timezone" => ExtractField::Timezone,
24617 "timezone_hour" => ExtractField::TimezoneHour,
24618 "timezone_minute" => ExtractField::TimezoneMinute,
24619 // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24620 // reports an unknown one with the source type (22023); carry the
24621 // raw name so eval can word it.
24622 other => ExtractField::Other(alloc::string::String::from(other)),
24623 };
24624 if !matches!(self.peek(), Token::From) {
24625 return Err(self.err(format!(
24626 "expected FROM after EXTRACT field, got {:?}",
24627 self.peek()
24628 )));
24629 }
24630 self.advance();
24631 let source = self.parse_expr(0)?;
24632 if !matches!(self.peek(), Token::RParen) {
24633 return Err(self.err(format!(
24634 "expected ')' to close EXTRACT, got {:?}",
24635 self.peek()
24636 )));
24637 }
24638 self.advance();
24639 Ok(Expr::Extract {
24640 field,
24641 source: Box::new(source),
24642 })
24643 }
24644
24645 /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24646 /// is already consumed; we expect a single string literal next and
24647 /// resolve it into `Literal::Interval` at parse time so the engine
24648 /// never has to re-tokenise inside the string.
24649 /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24650 /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24651 /// is the SQL-standard form and is left to the path below.
24652 fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24653 // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24654 let (offset, sign) = match self.peek() {
24655 Token::Minus => (1, "-"),
24656 _ => (0, ""),
24657 };
24658 let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24659 return None;
24660 };
24661 self.tokens
24662 .get(self.pos + offset + 1)
24663 .filter(|t| mysql_interval_unit(t).is_some())?;
24664 Some((alloc::format!("{sign}{n}"), offset + 1))
24665 }
24666
24667 /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24668 /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24669 /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24670 ///
24671 /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24672 /// this by parsing the group and then restoring `self.pos` — which could
24673 /// never have worked, because `advance()` DESTROYS the token it returns
24674 /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24675 /// inert only because both branches errored back then.
24676 fn interval_paren_is_quantity(&self) -> bool {
24677 let mut depth = 0usize;
24678 let mut saw_top_level_comma = false;
24679 let mut i = self.pos;
24680 while let Some(tok) = self.tokens.get(i) {
24681 match tok {
24682 Token::LParen => depth += 1,
24683 Token::RParen => {
24684 depth = depth.saturating_sub(1);
24685 if depth == 0 {
24686 return !saw_top_level_comma
24687 && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
24688 .is_some();
24689 }
24690 }
24691 // A comma directly inside the outermost parens means the
24692 // argument list of the INTERVAL() function.
24693 Token::Comma if depth == 1 => saw_top_level_comma = true,
24694 Token::Eof => return false,
24695 _ => {}
24696 }
24697 i += 1;
24698 }
24699 false
24700 }
24701
24702 fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
24703 // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
24704 // (the index of the last Ni ≤ N), distinct from the interval literal.
24705 // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
24706 // is decided by a non-destructive lookahead (round 422) before either
24707 // branch consumes anything. MySQL only.
24708 if self.mysql_dialect
24709 && matches!(self.peek(), Token::LParen)
24710 && !self.interval_paren_is_quantity()
24711 {
24712 self.advance(); // (
24713 let mut args = Vec::new();
24714 if !matches!(self.peek(), Token::RParen) {
24715 loop {
24716 args.push(self.parse_expr(0)?);
24717 if matches!(self.peek(), Token::Comma) {
24718 self.advance();
24719 continue;
24720 }
24721 break;
24722 }
24723 }
24724 if !matches!(self.peek(), Token::RParen) {
24725 return Err(self.err(alloc::format!(
24726 "expected ')' after INTERVAL() arguments, got {:?}",
24727 self.peek()
24728 )));
24729 }
24730 self.advance(); // )
24731 return Ok(Expr::FunctionCall {
24732 name: alloc::string::String::from("interval"),
24733 args,
24734 });
24735 }
24736 // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
24737 // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
24738 // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
24739 // writes every date arithmetic there is, and it did not parse at
24740 // all. PG rejects the unquoted form outright (`syntax error at or
24741 // near "1"`, measured), so it is taken only in the MySQL dialect —
24742 // PG's own `INTERVAL '1' DAY` is untouched below.
24743 if self.mysql_dialect
24744 && let Some((text, consume)) = self.peek_unquoted_interval_count()
24745 {
24746 for _ in 0..consume {
24747 self.advance(); // the optional `-` and the number
24748 }
24749 let Some(unit) = mysql_interval_unit(self.peek()) else {
24750 return Err(self.err(alloc::format!(
24751 "expected an interval unit after INTERVAL {text}, got {:?}",
24752 self.peek()
24753 )));
24754 };
24755 self.advance(); // the unit
24756 let (months, days, micros) = scale_mysql_interval(&text, unit)
24757 .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
24758 return Ok(Expr::Literal(Literal::Interval {
24759 months,
24760 days,
24761 micros,
24762 // The canonical rendering, so Display round-trips into a
24763 // form both dialects read back.
24764 text: alloc::format!("{text} {unit}"),
24765 }));
24766 }
24767 // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
24768 // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
24769 // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
24770 // Those cannot fold into a compile-time `Literal::Interval`, so they
24771 // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
24772 // builtin, which builds the value at run time (and yields NULL for a
24773 // NULL quantity, as MariaDB does). The literal path above still folds
24774 // the constant case — it is cheaper and round-trips through Display.
24775 //
24776 // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
24777 // MySQL's quoted spelling) keep the qualifier path below.
24778 if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
24779 let qty = self.parse_expr(0)?;
24780 let Some(unit) = mysql_interval_unit(self.peek()) else {
24781 return Err(self.err(alloc::format!(
24782 "expected an interval unit after INTERVAL <expr>, got {:?}",
24783 self.peek()
24784 )));
24785 };
24786 self.advance(); // the unit
24787 return Ok(make_interval_call(qty, unit));
24788 }
24789 let tok = self.advance();
24790 let Token::String(text) = tok else {
24791 return Err(self.err(format!(
24792 "expected string literal after INTERVAL, got {tok:?}"
24793 )));
24794 };
24795 // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
24796 // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
24797 // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
24798 // bare number means and the leading/trailing precision.
24799 let field1 = interval_field_of(self.peek());
24800 let qualifier = if let Some(f1) = field1 {
24801 self.advance();
24802 let f2 = if matches!(self.peek(), Token::To) {
24803 self.advance();
24804 let Some(f) = interval_field_of(self.peek()) else {
24805 return Err(self.err(format!(
24806 "expected an interval field after TO, got {:?}",
24807 self.peek()
24808 )));
24809 };
24810 self.advance();
24811 Some(f)
24812 } else {
24813 None
24814 };
24815 Some((f1, f2))
24816 } else {
24817 None
24818 };
24819 let (months, days, micros) = match qualifier {
24820 Some(q) => interpret_qualified_interval(&text, q),
24821 None => parse_interval_text(&text),
24822 }
24823 .ok_or_else(|| ParseError {
24824 message: format!(
24825 "cannot parse INTERVAL {text:?}; \
24826 expected `<n> <unit> [<n> <unit> ...]` with units \
24827 microsecond[s], millisecond[s], second[s], minute[s], \
24828 hour[s], day[s], week[s], month[s], year[s]"
24829 ),
24830 token_pos: self.consumed_pos(),
24831 })?;
24832 Ok(Expr::Literal(Literal::Interval {
24833 months,
24834 days,
24835 micros,
24836 text,
24837 }))
24838 }
24839
24840 /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
24841 /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
24842 /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
24843 /// than a pgvector literal.
24844 fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
24845 self.advance(); // consume `[`
24846 let mut items: Vec<Expr> = Vec::new();
24847 if !matches!(self.peek(), Token::RBracket) {
24848 loop {
24849 if matches!(self.peek(), Token::LBracket) {
24850 items.push(self.parse_array_bracket_body()?);
24851 } else {
24852 items.push(self.parse_expr(0)?);
24853 }
24854 match self.peek() {
24855 Token::Comma => {
24856 self.advance();
24857 }
24858 Token::RBracket => break,
24859 other => {
24860 return Err(self.err(alloc::format!(
24861 "expected ',' or ']' in array literal, got {other:?}"
24862 )));
24863 }
24864 }
24865 }
24866 }
24867 self.advance(); // consume `]`
24868 Ok(Expr::Array(items))
24869 }
24870
24871 fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
24872 let mut elems = Vec::new();
24873 if matches!(self.peek(), Token::RBracket) {
24874 self.advance();
24875 return Ok(Expr::Literal(Literal::Vector(elems)));
24876 }
24877 loop {
24878 let e = self.parse_expr(0)?;
24879 let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
24880 message: format!("vector element must be a numeric literal, got {e:?}"),
24881 token_pos: self.pos,
24882 })?;
24883 elems.push(x);
24884 match self.peek() {
24885 Token::Comma => {
24886 self.advance();
24887 }
24888 Token::RBracket => {
24889 self.advance();
24890 break;
24891 }
24892 other => {
24893 return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
24894 }
24895 }
24896 }
24897 Ok(Expr::Literal(Literal::Vector(elems)))
24898 }
24899
24900 /// Atom that started with an identifier: could be `t.col`, `col`, or
24901 /// `func(arg, ...)`. Detect each shape by looking at the next token.
24902 /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
24903 /// [, ...])`. Caller has already consumed `OVER`. Either clause
24904 /// is optional; an empty `()` is also legal (PG semantics).
24905 /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
24906 /// modifier between `name(args)` and `OVER (...)`. Default is
24907 /// `Respect`. Unrecognised idents leave the stream unchanged.
24908 fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
24909 let Token::Ident(s) = self.peek().clone() else {
24910 return NullTreatment::Respect;
24911 };
24912 let is_ignore = s.eq_ignore_ascii_case("ignore");
24913 let is_respect = s.eq_ignore_ascii_case("respect");
24914 if !is_ignore && !is_respect {
24915 return NullTreatment::Respect;
24916 }
24917 // Lookahead for NULLS — only consume both tokens together.
24918 // pos+1 must hold a "nulls" ident.
24919 if self.pos + 1 < self.tokens.len()
24920 && let Token::Ident(s2) = &self.tokens[self.pos + 1]
24921 && s2.eq_ignore_ascii_case("nulls")
24922 {
24923 self.advance();
24924 self.advance();
24925 return if is_ignore {
24926 NullTreatment::Ignore
24927 } else {
24928 NullTreatment::Respect
24929 };
24930 }
24931 NullTreatment::Respect
24932 }
24933
24934 /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
24935 /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
24936 /// (same shape as the `OVER` tail). Consumes the whole clause and
24937 /// returns the predicate; returns `None` when no `FILTER` follows.
24938 fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
24939 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24940 return Ok(None);
24941 };
24942 if !s.eq_ignore_ascii_case("filter") {
24943 return Ok(None);
24944 }
24945 self.advance(); // FILTER
24946 if !matches!(self.peek(), Token::LParen) {
24947 return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
24948 }
24949 self.advance(); // (
24950 if !matches!(self.peek(), Token::Where) {
24951 return Err(self.err(format!(
24952 "expected WHERE inside FILTER (...), got {:?}",
24953 self.peek()
24954 )));
24955 }
24956 self.advance(); // WHERE
24957 let cond = self.parse_expr(0)?;
24958 if !matches!(self.peek(), Token::RParen) {
24959 return Err(self.err(format!(
24960 "expected ')' to close FILTER (WHERE ...), got {:?}",
24961 self.peek()
24962 )));
24963 }
24964 self.advance(); // )
24965 Ok(Some(Box::new(cond)))
24966 }
24967
24968 /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
24969 /// the separator as the aggregate's second argument, which is the
24970 /// shape `string_agg` already takes. Returns whether one was there.
24971 fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
24972 if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
24973 return Ok(false);
24974 }
24975 self.advance();
24976 let Token::String(sep) = self.peek().clone() else {
24977 return Err(self.err(alloc::format!(
24978 "expected a string literal after SEPARATOR, got {:?}",
24979 self.peek()
24980 )));
24981 };
24982 self.advance();
24983 args.push(Expr::Literal(Literal::String(sep)));
24984 Ok(true)
24985 }
24986
24987 /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
24988 /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
24989 /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
24990 /// keys, or an empty vec when no `WITHIN GROUP` follows.
24991 fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
24992 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24993 return Ok(Vec::new());
24994 };
24995 if !s.eq_ignore_ascii_case("within") {
24996 return Ok(Vec::new());
24997 }
24998 self.advance(); // WITHIN
24999 if !matches!(self.peek(), Token::Group) {
25000 return Err(self.err(format!(
25001 "expected GROUP after WITHIN, got {:?}",
25002 self.peek()
25003 )));
25004 }
25005 self.advance(); // GROUP
25006 if !matches!(self.peek(), Token::LParen) {
25007 return Err(self.err(format!(
25008 "expected '(' after WITHIN GROUP, got {:?}",
25009 self.peek()
25010 )));
25011 }
25012 self.advance(); // (
25013 if !matches!(self.peek(), Token::Order) {
25014 return Err(self.err(format!(
25015 "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
25016 self.peek()
25017 )));
25018 }
25019 self.advance(); // ORDER
25020 if !self.peek_is_by() {
25021 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25022 }
25023 self.advance(); // BY
25024 let mut keys: Vec<OrderBy> = Vec::new();
25025 loop {
25026 // v7.39 (round 691) — save/restore, the discipline this parser
25027 // already uses around `pending_sample_preds`, so a subquery inside
25028 // a key neither inherits nor leaks the channel.
25029 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25030 let saved_coll = self.order_key_collation.take();
25031 let parsed = self.parse_expr(0);
25032 self.in_order_by_key = saved_flag;
25033 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
25034 let expr = parsed?;
25035 let desc = if matches!(self.peek(), Token::Desc) {
25036 self.advance();
25037 true
25038 } else if matches!(self.peek(), Token::Asc) {
25039 self.advance();
25040 false
25041 } else {
25042 false
25043 };
25044 let nulls_first = self.parse_optional_nulls_placement()?;
25045 keys.push(OrderBy {
25046 expr,
25047 desc,
25048 nulls_first,
25049 collation,
25050 });
25051 if matches!(self.peek(), Token::Comma) {
25052 self.advance();
25053 } else {
25054 break;
25055 }
25056 }
25057 if !matches!(self.peek(), Token::RParen) {
25058 return Err(self.err(format!(
25059 "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
25060 self.peek()
25061 )));
25062 }
25063 self.advance(); // )
25064 Ok(keys)
25065 }
25066
25067 /// No frame clause is supported.
25068 #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
25069 fn parse_over_clause(
25070 &mut self,
25071 ) -> Result<
25072 (
25073 Vec<Expr>,
25074 Vec<(Expr, bool, Option<bool>)>,
25075 Option<WindowFrame>,
25076 ),
25077 ParseError,
25078 > {
25079 // `OVER w` — a named-window reference. The WINDOW clause
25080 // parses after the select list, so the name rides out as a
25081 // marker in partition_by; parse_bare_select substitutes the
25082 // definition once the clause is known.
25083 if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
25084 let name = w.clone();
25085 self.advance();
25086 return Ok((
25087 alloc::vec![Expr::Column(crate::ast::ColumnName {
25088 qualifier: Some("__named_window__".to_string()),
25089 name,
25090 })],
25091 Vec::new(),
25092 None,
25093 ));
25094 }
25095 if !matches!(self.peek(), Token::LParen) {
25096 return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
25097 }
25098 self.advance();
25099 let mut partition_by = Vec::new();
25100 let mut order_by = Vec::new();
25101 // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
25102 // window, refined in place. PG's rules (probed against 18.4) differ
25103 // from the bare `OVER w1` form, so the reference rides out under its
25104 // own marker and `substitute_named_windows` applies them. The base
25105 // name is any leading identifier that isn't a window-spec keyword.
25106 let base_window = match self.peek() {
25107 Token::Ident(s) | Token::QuotedIdent(s)
25108 if !s.eq_ignore_ascii_case("partition")
25109 && !s.eq_ignore_ascii_case("rows")
25110 && !s.eq_ignore_ascii_case("range")
25111 && !s.eq_ignore_ascii_case("groups") =>
25112 {
25113 let n = s.clone();
25114 self.advance();
25115 Some(n)
25116 }
25117 _ => None,
25118 };
25119 // PARTITION BY ?
25120 // v7.37.6-B promoted PARTITION to a reserved keyword
25121 // (Token::Partition); pre-7.37.6-B catalogs lexed it as
25122 // `Token::Ident("partition")`. Accept both so older sources
25123 // and the new lexer surface land on the same path.
25124 let is_partition_kw = match self.peek() {
25125 Token::Partition => true,
25126 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
25127 _ => false,
25128 };
25129 if is_partition_kw {
25130 self.advance();
25131 if !self.peek_is_by() {
25132 return Err(self.err(format!(
25133 "expected BY after PARTITION, got {:?}",
25134 self.peek()
25135 )));
25136 }
25137 self.advance();
25138 loop {
25139 partition_by.push(self.parse_expr(0)?);
25140 if matches!(self.peek(), Token::Comma) {
25141 self.advance();
25142 continue;
25143 }
25144 break;
25145 }
25146 }
25147 // ORDER BY ?
25148 if matches!(self.peek(), Token::Order) {
25149 self.advance();
25150 if !self.peek_is_by() {
25151 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25152 }
25153 self.advance();
25154 loop {
25155 let e = self.parse_expr(0)?;
25156 let desc = if matches!(self.peek(), Token::Desc) {
25157 self.advance();
25158 true
25159 } else if matches!(self.peek(), Token::Asc) {
25160 self.advance();
25161 false
25162 } else {
25163 false
25164 };
25165 // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
25166 let nulls_first = self.parse_optional_nulls_placement()?;
25167 order_by.push((e, desc, nulls_first));
25168 if matches!(self.peek(), Token::Comma) {
25169 self.advance();
25170 continue;
25171 }
25172 break;
25173 }
25174 }
25175 // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
25176 // Both keywords come through the lexer as identifiers; match
25177 // case-insensitively.
25178 let mut frame: Option<WindowFrame> = None;
25179 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
25180 let kind = if s.eq_ignore_ascii_case("rows") {
25181 Some(FrameKind::Rows)
25182 } else if s.eq_ignore_ascii_case("range") {
25183 Some(FrameKind::Range)
25184 } else if s.eq_ignore_ascii_case("groups") {
25185 // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
25186 Some(FrameKind::Groups)
25187 } else {
25188 None
25189 };
25190 if let Some(kind) = kind {
25191 self.advance();
25192 frame = Some(self.parse_frame_tail(kind)?);
25193 }
25194 }
25195 if !matches!(self.peek(), Token::RParen) {
25196 return Err(self.err(format!(
25197 "expected ')' to close OVER clause, got {:?}",
25198 self.peek()
25199 )));
25200 }
25201 self.advance();
25202 if let Some(base) = base_window {
25203 // A copy may refine but never override the base's partitioning
25204 // (PG rejects it outright, before looking the name up).
25205 if !partition_by.is_empty() {
25206 return Err(self.err(alloc::format!(
25207 "cannot override PARTITION BY clause of window \"{base}\""
25208 )));
25209 }
25210 partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
25211 qualifier: Some("__named_window_ref__".to_string()),
25212 name: base,
25213 })];
25214 }
25215 Ok((partition_by, order_by, frame))
25216 }
25217
25218 /// v4.20: parse the tail of an explicit frame, given the `ROWS`
25219 /// or `RANGE` keyword was just consumed. Accepts both
25220 /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
25221 /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
25222 /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
25223 fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
25224 let (start, end) = if matches!(self.peek(), Token::Between) {
25225 self.advance();
25226 let start = self.parse_frame_bound()?;
25227 if !matches!(self.peek(), Token::And) {
25228 return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
25229 }
25230 self.advance();
25231 let end = self.parse_frame_bound()?;
25232 (start, Some(end))
25233 } else {
25234 (self.parse_frame_bound()?, None)
25235 };
25236 let exclude = self.parse_frame_exclusion()?;
25237 Ok(WindowFrame {
25238 kind,
25239 start,
25240 end,
25241 exclude,
25242 })
25243 }
25244
25245 /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
25246 /// after a frame spec. NO OTHERS is the default no-op.
25247 fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
25248 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
25249 return Ok(FrameExclusion::NoOthers);
25250 }
25251 self.advance(); // EXCLUDE
25252 match self.peek() {
25253 Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
25254 self.advance();
25255 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
25256 return Err(self.err(format!(
25257 "expected ROW after EXCLUDE CURRENT, got {:?}",
25258 self.peek()
25259 )));
25260 }
25261 self.advance();
25262 Ok(FrameExclusion::CurrentRow)
25263 }
25264 // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
25265 // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
25266 // Without this arm it fell to the catch-all, whose message
25267 // self-contradictingly listed GROUP as expected.
25268 Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
25269 self.advance();
25270 Ok(FrameExclusion::Group)
25271 }
25272 Token::Group => {
25273 self.advance();
25274 Ok(FrameExclusion::Group)
25275 }
25276 Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
25277 self.advance();
25278 Ok(FrameExclusion::Ties)
25279 }
25280 Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
25281 self.advance();
25282 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
25283 return Err(self.err(format!(
25284 "expected OTHERS after EXCLUDE NO, got {:?}",
25285 self.peek()
25286 )));
25287 }
25288 self.advance();
25289 Ok(FrameExclusion::NoOthers)
25290 }
25291 other => Err(self.err(format!(
25292 "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
25293 ))),
25294 }
25295 }
25296
25297 /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
25298 /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
25299 /// `UNBOUNDED FOLLOWING`.
25300 fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
25301 // Interval-typed offset for a value-based RANGE frame over a
25302 // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
25303 // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
25304 // PRECEDING`.
25305 if let Some((months, days, micros)) = self.try_take_interval_offset()? {
25306 let dir = self.expect_ident_like()?;
25307 return if dir.eq_ignore_ascii_case("preceding") {
25308 Ok(FrameBound::IntervalPreceding {
25309 months,
25310 days,
25311 micros,
25312 })
25313 } else if dir.eq_ignore_ascii_case("following") {
25314 Ok(FrameBound::IntervalFollowing {
25315 months,
25316 days,
25317 micros,
25318 })
25319 } else {
25320 Err(self.err(format!(
25321 "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
25322 )))
25323 };
25324 }
25325 // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
25326 if let Token::Integer(n) = *self.peek() {
25327 self.advance();
25328 let n: u64 = u64::try_from(n).map_err(|_| {
25329 self.err(format!(
25330 "invalid frame offset {n} — expected non-negative integer"
25331 ))
25332 })?;
25333 let dir = self.expect_ident_like()?;
25334 return if dir.eq_ignore_ascii_case("preceding") {
25335 Ok(FrameBound::OffsetPreceding(n))
25336 } else if dir.eq_ignore_ascii_case("following") {
25337 Ok(FrameBound::OffsetFollowing(n))
25338 } else {
25339 Err(self.err(format!(
25340 "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
25341 )))
25342 };
25343 }
25344 let first = self.expect_ident_like()?;
25345 if first.eq_ignore_ascii_case("unbounded") {
25346 let dir = self.expect_ident_like()?;
25347 return if dir.eq_ignore_ascii_case("preceding") {
25348 Ok(FrameBound::UnboundedPreceding)
25349 } else if dir.eq_ignore_ascii_case("following") {
25350 Ok(FrameBound::UnboundedFollowing)
25351 } else {
25352 Err(self.err(format!(
25353 "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
25354 )))
25355 };
25356 }
25357 if first.eq_ignore_ascii_case("current") {
25358 let row = self.expect_ident_like()?;
25359 if !row.eq_ignore_ascii_case("row") {
25360 return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
25361 }
25362 return Ok(FrameBound::CurrentRow);
25363 }
25364 Err(self.err(format!(
25365 "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
25366 )))
25367 }
25368
25369 /// Detect and consume a leading interval offset in a frame bound —
25370 /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
25371 /// `(months, days, micros)`. Leaves the cursor on the trailing
25372 /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
25373 /// when the next tokens are not an interval offset.
25374 fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
25375 // Shape A — `INTERVAL '1 day'`.
25376 if matches!(self.peek(), Token::Interval) {
25377 self.advance(); // INTERVAL
25378 let atom = self.parse_interval_atom()?;
25379 if let Expr::Literal(Literal::Interval {
25380 months,
25381 days,
25382 micros,
25383 ..
25384 }) = atom
25385 {
25386 return Ok(Some((months, days, micros)));
25387 }
25388 return Err(self.err("expected an interval literal in frame offset".to_string()));
25389 }
25390 // Shape B — `'1 day'::interval`. Look ahead for the exact
25391 // string / `::` / interval-target triple before committing.
25392 if let Token::String(text) = self.peek() {
25393 let target_is_interval = match self.tokens.get(self.pos + 2) {
25394 Some(Token::Interval) => true,
25395 Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
25396 _ => false,
25397 };
25398 let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
25399 && target_is_interval;
25400 if is_cast {
25401 let text = text.clone();
25402 self.advance(); // string
25403 self.advance(); // ::
25404 self.advance(); // interval
25405 let parts = parse_interval_text(&text).ok_or_else(|| {
25406 self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
25407 })?;
25408 return Ok(Some(parts));
25409 }
25410 }
25411 Ok(None)
25412 }
25413
25414 fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
25415 if matches!(self.peek(), Token::Dot) {
25416 self.advance();
25417 let name = self.expect_ident_like()?;
25418 // v7.14.0 — schema-qualified function call
25419 // `<schema>.<fn>(args)`. PG dumps emit
25420 // `pg_catalog.set_config(...)` in the preamble. SPG
25421 // is single-namespace: drop the schema prefix and
25422 // route the dispatch on the bare function name.
25423 if matches!(self.peek(), Token::LParen) {
25424 return self.finish_ident_atom(name);
25425 }
25426 return Ok(Expr::Column(ColumnName {
25427 qualifier: Some(first),
25428 name,
25429 }));
25430 }
25431 if matches!(self.peek(), Token::LParen) {
25432 self.advance();
25433 // `COUNT(*)` — special-cased here because `*` isn't a normal
25434 // expression token. Lower-case match on `first` since the lexer
25435 // folds identifiers.
25436 if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
25437 self.advance();
25438 if !matches!(self.peek(), Token::RParen) {
25439 return Err(self.err(format!(
25440 "expected ')' after COUNT(*), got {:?}",
25441 self.peek()
25442 )));
25443 }
25444 self.advance();
25445 // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
25446 let filter = self.parse_filter_clause()?;
25447 // v4.12: COUNT(*) OVER (...) — same window tail.
25448 let null_treatment = self.parse_null_treatment_modifier();
25449 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25450 && s.eq_ignore_ascii_case("over")
25451 {
25452 self.advance();
25453 let (partition_by, order_by, frame) = self.parse_over_clause()?;
25454 return Ok(Expr::WindowFunction {
25455 name: "count_star".into(),
25456 args: Vec::new(),
25457 partition_by,
25458 order_by,
25459 frame,
25460 null_treatment,
25461 filter,
25462 });
25463 }
25464 if let Some(filter) = filter {
25465 return Ok(Expr::AggregateOrdered {
25466 call: Box::new(Expr::FunctionCall {
25467 name: "count_star".into(),
25468 args: Vec::new(),
25469 }),
25470 order_by: Vec::new(),
25471 distinct: false,
25472 filter: Some(filter),
25473 });
25474 }
25475 return Ok(Expr::FunctionCall {
25476 name: "count_star".into(),
25477 args: Vec::new(),
25478 });
25479 }
25480 // Function call. PG-style: zero-or-more comma-separated args.
25481 let mut args = Vec::new();
25482 // v7.38 (read01, T14) — named-argument notation `argname => value`.
25483 // Names are collected in lock-step with `args` and resolved to
25484 // positional order after the loop (the AST stays positional).
25485 let mut arg_names: Vec<Option<String>> = Vec::new();
25486 let mut agg_order_by: Vec<OrderBy> = Vec::new();
25487 // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
25488 // seen, so the value arguments before it can be folded.
25489 let mut saw_separator = false;
25490 // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
25491 // v7.32 (round-29) — accept the dual `ALL` quantifier too
25492 // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
25493 let agg_distinct = if matches!(self.peek(), Token::Distinct) {
25494 self.advance();
25495 true
25496 } else if matches!(self.peek(), Token::All) {
25497 self.advance();
25498 false
25499 } else {
25500 false
25501 };
25502 // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
25503 // TIMESTAMPDIFF take a bare unit keyword as the first
25504 // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
25505 // bare type keyword (DATE / TIME / DATETIME); lower them
25506 // onto string literals so the evaluator sees plain text.
25507 if ((first.eq_ignore_ascii_case("timestampadd")
25508 || first.eq_ignore_ascii_case("timestampdiff"))
25509 && matches!(self.peek(), Token::Ident(u) if matches!(
25510 u.to_ascii_lowercase().as_str(),
25511 "microsecond" | "second" | "minute" | "hour" | "day"
25512 | "week" | "month" | "quarter" | "year"
25513 )))
25514 || (first.eq_ignore_ascii_case("get_format")
25515 && matches!(self.peek(), Token::Ident(u) if matches!(
25516 u.to_ascii_lowercase().as_str(),
25517 "date" | "time" | "datetime" | "timestamp"
25518 )))
25519 {
25520 if let Token::Ident(u) = self.peek() {
25521 args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
25522 }
25523 self.advance();
25524 if matches!(self.peek(), Token::Comma) {
25525 self.advance();
25526 }
25527 }
25528 // `ROW(a, b, …)` keyword constructor. Followed by a
25529 // comparison operator or [NOT] IN it joins the paren
25530 // row-constructor machinery (fieldwise parse-time
25531 // expansion); bare, it stays a `row` call the evaluator
25532 // renders as PG record text.
25533 if first.eq_ignore_ascii_case("row") {
25534 let mut row_items = Vec::new();
25535 if !matches!(self.peek(), Token::RParen) {
25536 loop {
25537 row_items.push(self.parse_expr(0)?);
25538 match self.peek() {
25539 Token::Comma => {
25540 self.advance();
25541 }
25542 Token::RParen => break,
25543 other => {
25544 return Err(self.err(format!(
25545 "expected ',' or ')' in ROW(...), got {other:?}"
25546 )));
25547 }
25548 }
25549 }
25550 }
25551 self.advance(); // ')'
25552 let comparison_follows = matches!(
25553 self.peek(),
25554 Token::Eq
25555 | Token::NotEq
25556 | Token::Lt
25557 | Token::LtEq
25558 | Token::Gt
25559 | Token::GtEq
25560 | Token::In
25561 ) || (matches!(self.peek(), Token::Not)
25562 && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25563 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25564 if comparison_follows && !row_items.is_empty() {
25565 return self.parse_row_comparison_tail(row_items);
25566 }
25567 return Ok(Expr::FunctionCall {
25568 name: String::from("row"),
25569 args: row_items,
25570 });
25571 }
25572 // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25573 // the parse-mode keyword introduces the source text. SPG
25574 // carries XML as text, so both modes lower to __xmlparse(expr)
25575 // which validates well-formedness and returns Value::Xml.
25576 if first.eq_ignore_ascii_case("xmlparse")
25577 && matches!(self.peek(), Token::Ident(kw)
25578 if kw.eq_ignore_ascii_case("document")
25579 || kw.eq_ignore_ascii_case("content"))
25580 {
25581 let mode = match self.advance() {
25582 Token::Ident(kw) => kw.to_ascii_lowercase(),
25583 _ => unreachable!("peeked an ident"),
25584 };
25585 let src = self.parse_expr(0)?;
25586 if !matches!(self.peek(), Token::RParen) {
25587 return Err(self.err(format!(
25588 "expected ')' to close XMLPARSE, got {:?}",
25589 self.peek()
25590 )));
25591 }
25592 self.advance();
25593 return Ok(Expr::FunctionCall {
25594 name: String::from("__xmlparse"),
25595 args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25596 });
25597 }
25598 // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25599 // keyword introduces the element name (a bare or quoted
25600 // identifier), then optional content expressions. Lower to a
25601 // plain `xmlelement(name_text, content …)` call.
25602 if first.eq_ignore_ascii_case("xmlelement")
25603 && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25604 {
25605 self.advance(); // consume NAME
25606 let elem_name = match self.peek().clone() {
25607 Token::Ident(n) | Token::QuotedIdent(n) => {
25608 self.advance();
25609 n
25610 }
25611 other => {
25612 return Err(self.err(format!(
25613 "expected element name after XMLELEMENT NAME, got {other:?}"
25614 )));
25615 }
25616 };
25617 let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25618 while matches!(self.peek(), Token::Comma) {
25619 self.advance();
25620 args.push(self.parse_expr(0)?);
25621 }
25622 if !matches!(self.peek(), Token::RParen) {
25623 return Err(self.err(format!(
25624 "expected ')' to close XMLELEMENT, got {:?}",
25625 self.peek()
25626 )));
25627 }
25628 self.advance();
25629 return Ok(Expr::FunctionCall {
25630 name: String::from("xmlelement"),
25631 args,
25632 });
25633 }
25634 // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
25635 // becomes a `<name>value</name>` element; a bare column infers its
25636 // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
25637 if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
25638 let mut args: Vec<Expr> = Vec::new();
25639 loop {
25640 let val = self.parse_expr(0)?;
25641 let name = if matches!(self.peek(), Token::As) {
25642 self.advance();
25643 match self.peek().clone() {
25644 Token::Ident(n) | Token::QuotedIdent(n) => {
25645 self.advance();
25646 n
25647 }
25648 other => {
25649 return Err(self.err(format!(
25650 "expected name after AS in XMLFOREST, got {other:?}"
25651 )));
25652 }
25653 }
25654 } else if let Expr::Column(c) = &val {
25655 c.name.clone()
25656 } else {
25657 return Err(
25658 self.err("XMLFOREST element without a column name needs AS".into())
25659 );
25660 };
25661 args.push(Expr::Literal(Literal::String(name)));
25662 args.push(val);
25663 if matches!(self.peek(), Token::Comma) {
25664 self.advance();
25665 } else {
25666 break;
25667 }
25668 }
25669 if !matches!(self.peek(), Token::RParen) {
25670 return Err(self.err(format!(
25671 "expected ')' to close XMLFOREST, got {:?}",
25672 self.peek()
25673 )));
25674 }
25675 self.advance();
25676 return Ok(Expr::FunctionCall {
25677 name: String::from("xmlforest"),
25678 args,
25679 });
25680 }
25681 // SQL-standard `POSITION(sub IN str)` — lowers onto
25682 // strpos(str, sub). IN is the argument separator here,
25683 // so the needle parses with the IN-tail suppressed.
25684 if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
25685 let saved = self.suppress_in_tail;
25686 self.suppress_in_tail = true;
25687 let needle = self.parse_expr(0);
25688 self.suppress_in_tail = saved;
25689 let needle = needle?;
25690 if matches!(self.peek(), Token::In) {
25691 self.advance();
25692 let haystack = self.parse_expr(0)?;
25693 if !matches!(self.peek(), Token::RParen) {
25694 return Err(self.err(format!(
25695 "expected ')' to close POSITION, got {:?}",
25696 self.peek()
25697 )));
25698 }
25699 self.advance();
25700 return Ok(Expr::FunctionCall {
25701 name: String::from("strpos"),
25702 args: alloc::vec![haystack, needle],
25703 });
25704 }
25705 // position(sub, str) comma form (incl. bytea) —
25706 // hand the parsed first arg to the generic list.
25707 args.push(needle);
25708 if matches!(self.peek(), Token::Comma) {
25709 self.advance();
25710 }
25711 }
25712 // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
25713 // FROM str)` — lowers onto btrim / ltrim / rtrim. The
25714 // plain comma forms TRIM(str) / TRIM(str, chars) keep
25715 // riding the generic argument list below.
25716 if first.eq_ignore_ascii_case("trim") {
25717 let mode = match self.peek() {
25718 Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
25719 self.advance();
25720 Some("btrim")
25721 }
25722 Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
25723 self.advance();
25724 Some("ltrim")
25725 }
25726 Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
25727 self.advance();
25728 Some("rtrim")
25729 }
25730 _ => None,
25731 };
25732 if mode.is_some() || matches!(self.peek(), Token::From) {
25733 // TRIM([mode] FROM str) — no strip-chars.
25734 let chars = if matches!(self.peek(), Token::From) {
25735 None
25736 } else {
25737 Some(self.parse_expr(0)?)
25738 };
25739 if !matches!(self.peek(), Token::From) {
25740 return Err(self.err(format!(
25741 "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
25742 self.peek()
25743 )));
25744 }
25745 self.advance();
25746 let target = self.parse_expr(0)?;
25747 if !matches!(self.peek(), Token::RParen) {
25748 return Err(
25749 self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
25750 );
25751 }
25752 self.advance();
25753 let mut trim_args = alloc::vec![target];
25754 if let Some(c) = chars {
25755 trim_args.push(c);
25756 }
25757 return Ok(Expr::FunctionCall {
25758 name: String::from(mode.unwrap_or("btrim")),
25759 args: trim_args,
25760 });
25761 }
25762 }
25763 if !matches!(self.peek(), Token::RParen) {
25764 loop {
25765 // v7.38 (read01, T14) — `argname => value` names this arg.
25766 // v7.39 (read01 round 77) — `argname := value` is the same
25767 // thing, and it is the spelling PG's own docs lead with. It
25768 // was simply never lexed here, so every `f(x := 1)` died in
25769 // the parser regardless of what `f` was.
25770 let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
25771 (
25772 Token::Ident(n) | Token::QuotedIdent(n),
25773 Some(Token::FatArrow | Token::ColonEq),
25774 ) => {
25775 let name = n.clone();
25776 self.advance(); // name
25777 self.advance(); // => / :=
25778 Some(name)
25779 }
25780 _ => None,
25781 };
25782 // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
25783 // array's elements into a variadic call's trailing args
25784 // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
25785 // reserved, so it arrives as a bare ident before the arg.
25786 let is_variadic = this_name.is_none()
25787 && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
25788 if is_variadic {
25789 self.advance();
25790 }
25791 let arg = self.parse_expr(0)?;
25792 args.push(match &this_name {
25793 // The callee's parameter names decide the slot, and a
25794 // user function's live in the catalog. Carry the name
25795 // to eval rather than guessing here.
25796 Some(n) => Expr::NamedArg {
25797 name: n.clone(),
25798 expr: Box::new(arg),
25799 },
25800 None if is_variadic => Expr::Variadic(Box::new(arg)),
25801 None => arg,
25802 });
25803 arg_names.push(this_name);
25804 // v7.25 (round-17) — standard `CAST(expr AS type)`.
25805 // The `::` cast already worked; this lowers the
25806 // function form onto the same Expr::Cast node.
25807 if first.eq_ignore_ascii_case("cast")
25808 && args.len() == 1
25809 && matches!(self.peek(), Token::As)
25810 {
25811 self.advance();
25812 let target = self.parse_cast_target()?;
25813 if !matches!(self.peek(), Token::RParen) {
25814 return Err(self.err(format!(
25815 "expected ')' to close CAST, got {:?}",
25816 self.peek()
25817 )));
25818 }
25819 self.advance();
25820 return Ok(Expr::Cast {
25821 expr: Box::new(args.pop().expect("one arg")),
25822 target,
25823 });
25824 }
25825 // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
25826 // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
25827 // keywords; SPG's lexer makes them plain idents (so they'd be
25828 // read as column refs). Lower the keyword to the string form
25829 // the evaluator already accepts.
25830 if first.eq_ignore_ascii_case("normalize")
25831 && args.len() == 1
25832 && matches!(self.peek(), Token::Comma)
25833 {
25834 let form = match self.tokens.get(self.pos + 1) {
25835 Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
25836 let up = f.to_ascii_uppercase();
25837 matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
25838 }
25839 _ => None,
25840 };
25841 if let Some(up) = form {
25842 self.advance(); // comma
25843 self.advance(); // form keyword
25844 args.push(Expr::Literal(Literal::String(up)));
25845 }
25846 }
25847 // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
25848 // form. Desugars to the comma-list shape evaluator already
25849 // handles. Triggered after the first arg when the function
25850 // name is substring / substr and the next token is FROM
25851 // (a reserved keyword in PG; SPG also reserves it).
25852 // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
25853 // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
25854 // internal __substring_similar(str, pat, esc) call.
25855 if (first.eq_ignore_ascii_case("substring")
25856 || first.eq_ignore_ascii_case("substr"))
25857 && args.len() == 1
25858 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
25859 {
25860 self.advance(); // SIMILAR
25861 let pattern = self.parse_expr(0)?;
25862 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
25863 {
25864 return Err(self.err(format!(
25865 "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
25866 self.peek()
25867 )));
25868 }
25869 self.advance(); // ESCAPE
25870 let esc = self.parse_expr(0)?;
25871 if !matches!(self.peek(), Token::RParen) {
25872 return Err(self.err(format!(
25873 "expected ')' to close substring(... SIMILAR ...), got {:?}",
25874 self.peek()
25875 )));
25876 }
25877 self.advance();
25878 args.push(pattern);
25879 args.push(esc);
25880 return Ok(Expr::FunctionCall {
25881 name: "__substring_similar".to_string(),
25882 args,
25883 });
25884 }
25885 if (first.eq_ignore_ascii_case("substring")
25886 || first.eq_ignore_ascii_case("substr"))
25887 && args.len() == 1
25888 && matches!(self.peek(), Token::From | Token::For)
25889 {
25890 // `substring(str FROM pos [FOR len])`, or the FOR-only
25891 // `substring(str FOR len)` which PG treats as FROM 1.
25892 if matches!(self.peek(), Token::From) {
25893 self.advance();
25894 let start = self.parse_expr(0)?;
25895 args.push(start);
25896 } else {
25897 args.push(Expr::Literal(Literal::Integer(1)));
25898 }
25899 if matches!(self.peek(), Token::For) {
25900 self.advance();
25901 let length = self.parse_expr(0)?;
25902 args.push(length);
25903 }
25904 if !matches!(self.peek(), Token::RParen) {
25905 return Err(self.err(format!(
25906 "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
25907 self.peek()
25908 )));
25909 }
25910 self.advance();
25911 return Ok(Expr::FunctionCall {
25912 name: first.to_ascii_lowercase(),
25913 args,
25914 });
25915 }
25916 // PG `overlay(str PLACING repl FROM n [FOR len])`
25917 // syntactic form. Desugars to the `overlay(str,
25918 // repl, n[, len])` comma-list shape the evaluator
25919 // already implements. `PLACING` is not a reserved
25920 // token in SPG, so it arrives as a bare Ident.
25921 if first.eq_ignore_ascii_case("overlay")
25922 && args.len() == 1
25923 && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
25924 {
25925 self.advance(); // consume PLACING
25926 args.push(self.parse_expr(0)?); // replacement
25927 if !matches!(self.peek(), Token::From) {
25928 return Err(self.err(format!(
25929 "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
25930 self.peek()
25931 )));
25932 }
25933 self.advance();
25934 args.push(self.parse_expr(0)?); // start position
25935 if matches!(self.peek(), Token::For) {
25936 self.advance();
25937 args.push(self.parse_expr(0)?); // length
25938 }
25939 if !matches!(self.peek(), Token::RParen) {
25940 return Err(self.err(format!(
25941 "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
25942 self.peek()
25943 )));
25944 }
25945 self.advance();
25946 return Ok(Expr::FunctionCall {
25947 name: String::from("overlay"),
25948 args,
25949 });
25950 }
25951 // `TRIM(chars FROM str)` — the keyword-less
25952 // spelling lands here after the chars parse
25953 // (the keyword forms return earlier).
25954 if first.eq_ignore_ascii_case("trim")
25955 && args.len() == 1
25956 && matches!(self.peek(), Token::From)
25957 {
25958 self.advance();
25959 let target = self.parse_expr(0)?;
25960 if !matches!(self.peek(), Token::RParen) {
25961 return Err(self.err(format!(
25962 "expected ')' to close TRIM(chars FROM str), got {:?}",
25963 self.peek()
25964 )));
25965 }
25966 self.advance();
25967 let chars = args.pop().expect("one arg");
25968 return Ok(Expr::FunctionCall {
25969 name: String::from("btrim"),
25970 args: alloc::vec![target, chars],
25971 });
25972 }
25973 // v7.24 (round-16 A) — aggregate-internal
25974 // ordering: `array_agg(x ORDER BY y DESC NULLS
25975 // LAST)`. Keys close the argument list.
25976 if matches!(self.peek(), Token::Order) {
25977 self.advance();
25978 if !self.peek_is_by() {
25979 return Err(self.err(format!(
25980 "expected BY after ORDER in aggregate args, got {:?}",
25981 self.peek()
25982 )));
25983 }
25984 self.advance();
25985 loop {
25986 // v7.39 (round 691) — save/restore, the discipline this parser
25987 // already uses around `pending_sample_preds`, so a subquery inside
25988 // a key neither inherits nor leaks the channel.
25989 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25990 let saved_coll = self.order_key_collation.take();
25991 let parsed = self.parse_expr(0);
25992 self.in_order_by_key = saved_flag;
25993 let collation =
25994 core::mem::replace(&mut self.order_key_collation, saved_coll);
25995 let expr = parsed?;
25996 let desc = if matches!(self.peek(), Token::Desc) {
25997 self.advance();
25998 true
25999 } else if matches!(self.peek(), Token::Asc) {
26000 self.advance();
26001 false
26002 } else {
26003 false
26004 };
26005 let nulls_first = self.parse_optional_nulls_placement()?;
26006 agg_order_by.push(OrderBy {
26007 expr,
26008 desc,
26009 nulls_first,
26010 collation,
26011 });
26012 if matches!(self.peek(), Token::Comma) {
26013 self.advance();
26014 } else {
26015 break;
26016 }
26017 }
26018 // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
26019 // follow the ORDER BY inside GROUP_CONCAT.
26020 if self.consume_group_concat_separator(&mut args)? {
26021 saw_separator = true;
26022 }
26023 if !matches!(self.peek(), Token::RParen) {
26024 return Err(self.err(format!(
26025 "expected ')' after aggregate ORDER BY, got {:?}",
26026 self.peek()
26027 )));
26028 }
26029 break;
26030 }
26031 // v7.39 (round 354, M12) — …or directly after the
26032 // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
26033 // own spelling of what PG passes as string_agg's second
26034 // argument; it was a parse error, so every MySQL query
26035 // that names its own separator failed outright.
26036 if self.consume_group_concat_separator(&mut args)? {
26037 saw_separator = true;
26038 break;
26039 }
26040 match self.peek() {
26041 Token::Comma => {
26042 self.advance();
26043 }
26044 Token::RParen => break,
26045 other => {
26046 return Err(self.err(format!(
26047 "expected ',' or ')' in function args, got {other:?}"
26048 )));
26049 }
26050 }
26051 }
26052 }
26053 // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
26054 // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
26055 // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
26056 // meaning a separator — that is what the explicit SEPARATOR
26057 // tail is for. Fold them into one `concat(...)` so the
26058 // aggregate keeps its single value argument.
26059 if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
26060 let values = args.len() - usize::from(saw_separator);
26061 if values > 1 {
26062 let sep_arg = if saw_separator { args.pop() } else { None };
26063 let folded = Expr::FunctionCall {
26064 name: "concat".to_string(),
26065 args: core::mem::take(&mut args),
26066 };
26067 args.push(folded);
26068 if let Some(sep) = sep_arg {
26069 args.push(sep);
26070 }
26071 }
26072 }
26073 self.advance(); // consume ')'
26074 // v7.39 (read01 round 77) — named arguments are NOT reordered here
26075 // any more. The parser has no catalog, so it could only ever resolve
26076 // the handful of `make_*` builtins whose parameter names were baked
26077 // into a table right here — every user function got
26078 // "does not support named arguments", though the catalog has been
26079 // storing its parameter names all along. Reordering happens in eval,
26080 // in one place, for builtins and user functions alike.
26081 // v7.32 (round-29) — ordered-set aggregate tail
26082 // `name(direct_args) WITHIN GROUP (ORDER BY …)`
26083 // (percentile_cont / percentile_disc / mode). The sort spec
26084 // lands in the same `order_by` slot a decorated aggregate
26085 // uses; the executor dispatches on the function name. WITHIN
26086 // GROUP and an intra-argument ORDER BY are mutually
26087 // exclusive (PG rejects both).
26088 let within_group_order = self.parse_within_group_clause()?;
26089 if !within_group_order.is_empty() && !agg_order_by.is_empty() {
26090 return Err(self.err(
26091 "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
26092 .into(),
26093 ));
26094 }
26095 let within_group_seen = !within_group_order.is_empty();
26096 let agg_order_by = if within_group_order.is_empty() {
26097 agg_order_by
26098 } else {
26099 within_group_order
26100 };
26101 // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
26102 let filter = self.parse_filter_clause()?;
26103 // v4.12: window-function tail — `name(args) OVER (...)`.
26104 // Promotes the just-parsed FunctionCall into a
26105 // WindowFunction node carrying partition + order.
26106 // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
26107 // / `RESPECT NULLS OVER (...)` between the closing paren
26108 // and `OVER`.
26109 let null_treatment = self.parse_null_treatment_modifier();
26110 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
26111 && s.eq_ignore_ascii_case("over")
26112 {
26113 self.advance();
26114 // v7.39 (round 230) — PG implements neither modifier for a
26115 // windowed call and says so (0A000). Both used to be parsed
26116 // and then silently dropped here, so `count(DISTINCT v)
26117 // OVER (…)` quietly answered the non-distinct count.
26118 if agg_distinct {
26119 return Err(
26120 self.err("DISTINCT is not implemented for window functions".to_string())
26121 );
26122 }
26123 if !agg_order_by.is_empty() {
26124 // PG separates the two shapes that land here: a
26125 // WITHIN GROUP call is an ordered-set aggregate and gets
26126 // its own message naming the aggregate; a plain
26127 // `agg(x ORDER BY y)` gets the generic one.
26128 let msg = if within_group_seen {
26129 alloc::format!("OVER is not supported for ordered-set aggregate {first}")
26130 } else {
26131 "aggregate ORDER BY is not implemented for window functions".to_string()
26132 };
26133 return Err(self.err(msg));
26134 }
26135 let (partition_by, order_by, frame) = self.parse_over_clause()?;
26136 return Ok(Expr::WindowFunction {
26137 name: first,
26138 args,
26139 partition_by,
26140 order_by,
26141 frame,
26142 null_treatment,
26143 filter,
26144 });
26145 }
26146 if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
26147 return Ok(Expr::AggregateOrdered {
26148 call: Box::new(Expr::FunctionCall { name: first, args }),
26149 order_by: agg_order_by,
26150 distinct: agg_distinct,
26151 filter,
26152 });
26153 }
26154 // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
26155 // over TIMESTAMPTZ and has no timestamp overload, so a
26156 // timestamp argument is coerced on the way in and the answer
26157 // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
26158 // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
26159 // zone`. SPG answered `timestamp without time zone`, dropping
26160 // the offset from every rendering.
26161 //
26162 // Writing the coercion PG performs makes the existing
26163 // argument-driven typing (the one `date_trunc` uses) reach the
26164 // right answer, rather than teaching the type layer a second
26165 // rule. MySQL's DATE_ADD is a different function that returns
26166 // DATE or DATETIME, so this is PG-dialect only.
26167 //
26168 // Out-of-line because this sits on the RECURSIVE descent
26169 // frame: an inline block with locals here costs every nesting
26170 // level, and the suite's deep-nesting sentinel overflowed the
26171 // 512 KiB parser stack the moment one was added (round 430's
26172 // lesson, in the same shape).
26173 if !self.mysql_dialect {
26174 lift_date_add_arg_to_timestamptz(&first, &mut args);
26175 }
26176 return Ok(Expr::FunctionCall { name: first, args });
26177 }
26178 // v7.9.20 — SQL-standard parenless keyword expressions
26179 // (PG treats these as functions called without parens).
26180 // Resolve to a synthetic FunctionCall so the engine's
26181 // eval path reuses the existing function-call routing.
26182 // mailrs G3.
26183 let lc = first.to_ascii_lowercase();
26184 if matches!(
26185 lc.as_str(),
26186 "current_date"
26187 | "current_time"
26188 | "current_timestamp"
26189 | "localtimestamp"
26190 | "localtime"
26191 // v7.37.17 (17.6 siblings) — session-identity SQL-
26192 // standard parenless keywords. current_user /
26193 // session_user / user were already caught by the
26194 // pgwire canned-response shortcut but bare-select
26195 // in the embedded engine went through Expr::Column
26196 // and errored. Adding them here so the parser
26197 // resolves to a synthetic FunctionCall that reuses
26198 // the existing eval/functions.rs dispatch.
26199 | "current_user"
26200 | "session_user"
26201 | "current_role"
26202 | "current_catalog"
26203 | "current_schema"
26204 | "current_database"
26205 // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
26206 | "system_user"
26207 ) {
26208 return Ok(Expr::FunctionCall {
26209 name: lc,
26210 args: Vec::new(),
26211 });
26212 }
26213 Ok(Expr::Column(ColumnName {
26214 qualifier: None,
26215 name: first,
26216 }))
26217 }
26218}
26219
26220/// v7.39 (round 522) — write the coercion PG's `date_add` /
26221/// `date_subtract` signature performs.
26222///
26223/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
26224/// timestamp argument is cast on the way in and the answer is
26225/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
26226/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
26227/// `timestamp without time zone`, dropping the offset from every
26228/// rendering of the result.
26229///
26230/// Writing the cast the signature implies lets the existing
26231/// argument-driven typing (the one `date_trunc` uses) reach the right
26232/// answer instead of teaching the type layer a second rule. MySQL's
26233/// DATE_ADD is a different function returning DATE or DATETIME, so the
26234/// caller applies this in PG dialect only.
26235///
26236/// A free function, and not a block at the call site, because the caller
26237/// is on the recursive-descent frame chain.
26238#[inline(never)]
26239fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
26240 if args.len() != 2
26241 || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
26242 {
26243 return;
26244 }
26245 let base = args.remove(0);
26246 args.insert(
26247 0,
26248 Expr::Cast {
26249 expr: Box::new(base),
26250 target: CastTarget::Timestamptz,
26251 },
26252 );
26253}
26254
26255/// v6.8.2 — walk an expression tree and return the first column
26256/// reference's bare name. Used by `parse_create_index_stmt_after_create`
26257/// to derive `CreateIndexStatement.column` from an expression
26258/// key (so downstream planner code resolving a primary column
26259/// position keeps working with expression indexes). Returns
26260/// `None` when the expression has no column ref at all — caller
26261/// surfaces that as a parse error.
26262fn extract_first_column(expr: &Expr) -> Option<String> {
26263 match expr {
26264 Expr::Column(cn) => Some(cn.name.clone()),
26265 Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
26266 Expr::Binary { lhs, rhs, .. } => {
26267 extract_first_column(lhs).or_else(|| extract_first_column(rhs))
26268 }
26269 Expr::Unary { expr: e, .. } => extract_first_column(e),
26270 // v7.39 (read01 round 93) — a cast wraps its operand: a common
26271 // expression-index key is `lower(col::text)`, where the column
26272 // sits under the `::text` cast inside the function arg. Without
26273 // descending here the key was rejected as "references no column".
26274 Expr::Cast { expr: e, .. } => extract_first_column(e),
26275 _ => None,
26276 }
26277}
26278
26279fn maybe_not(expr: Expr, negated: bool) -> Expr {
26280 if negated {
26281 Expr::Unary {
26282 op: UnOp::Not,
26283 expr: Box::new(expr),
26284 }
26285 } else {
26286 expr
26287 }
26288}
26289
26290/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
26291/// things in the two dialects, and SPG read all three PG's way:
26292///
26293/// | token | PG (and SPG) | MySQL, measured |
26294/// |---|---|---|
26295/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
26296/// | `&&` | inet / array overlap | **AND** |
26297/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
26298///
26299/// `1 || 0` answering the string '10' on a MySQL session is a wrong
26300/// answer with no error, which is why they are routed here rather than
26301/// left to the shared table.
26302impl Parser {
26303 fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
26304 if self.mysql_dialect {
26305 // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
26306 // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
26307 // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
26308 if let Token::Ident(w) = tok
26309 && w.eq_ignore_ascii_case("div")
26310 {
26311 return Some((BinOp::IntDiv, 8));
26312 }
26313 // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
26314 // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
26315 // the lexer; the `MOD(x, y)` function form is unaffected (MOD
26316 // there sits in operand position, not infix).
26317 if let Token::Ident(w) = tok
26318 && w.eq_ignore_ascii_case("mod")
26319 {
26320 return Some((BinOp::Mod, 8));
26321 }
26322 // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
26323 // plain ident to the lexer. Its precedence sits between OR (1)
26324 // and AND (3) — hence rung 2, the slot freed by moving AND up.
26325 if let Token::Ident(w) = tok
26326 && w.eq_ignore_ascii_case("xor")
26327 {
26328 return Some((BinOp::LogicalXor, 2));
26329 }
26330 match tok {
26331 Token::Concat => return Some((BinOp::Or, 1)),
26332 // MySQL's `&&` is logical AND, sharing AND's rung (3).
26333 Token::InetOverlap => return Some((BinOp::And, 3)),
26334 // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
26335 Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
26336 _ => {}
26337 }
26338 }
26339 binop_from(tok)
26340 }
26341}
26342
26343// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
26344// (which sits strictly between OR and AND), every level from AND upward was
26345// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
26346// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
26347// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
26348// the *relative* order of every PG operator is unchanged by the shift.
26349fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
26350 let pair = match tok {
26351 Token::Or => (BinOp::Or, 1),
26352 Token::And => (BinOp::And, 3),
26353 Token::Eq => (BinOp::Eq, 5),
26354 Token::NotEq => (BinOp::NotEq, 5),
26355 Token::Lt => (BinOp::Lt, 5),
26356 Token::LtEq => (BinOp::LtEq, 5),
26357 Token::Gt => (BinOp::Gt, 5),
26358 Token::GtEq => (BinOp::GtEq, 5),
26359 // pgvector distance ops all sit on the same rung — tighter than
26360 // comparisons (5) so `col <-> v < threshold` parses correctly.
26361 Token::L2Distance => (BinOp::L2Distance, 6),
26362 // v7.39 (read01 geo_ops.c) — geometric predicates ride the
26363 // comparison rung.
26364 Token::GeomParallel => (BinOp::GeomParallel, 5),
26365 // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
26366 // comparison rung.
26367 Token::OverLeft => (BinOp::OverLeft, 5),
26368 Token::OverRight => (BinOp::OverRight, 5),
26369 Token::GeomPerp => (BinOp::GeomPerp, 5),
26370 Token::GeomSameAs => (BinOp::GeomSameAs, 5),
26371 Token::ClosestPoint => (BinOp::ClosestPoint, 6),
26372 Token::GeomHoriz => (BinOp::GeomHoriz, 5),
26373 Token::InnerProduct => (BinOp::InnerProduct, 6),
26374 Token::CosineDistance => (BinOp::CosineDistance, 6),
26375 Token::Plus => (BinOp::Add, 7),
26376 Token::Minus => (BinOp::Sub, 7),
26377 // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
26378 // binds every "other" operator (`||`, `|`, `&`, `#`, the
26379 // pgvector distances above) BETWEEN additive (7) and the
26380 // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
26381 // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
26382 // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
26383 // ("matches PG conceptually" — the round-753 audit measured it
26384 // false; the old rung errored on `'a' || 1 + 1` with
26385 // `text + integer`). Same-level chains left-fold, as PG does.
26386 Token::Concat => (BinOp::Concat, 6),
26387 Token::Pipe => (BinOp::BitOr, 6),
26388 Token::Amp => (BinOp::BitAnd, 6),
26389 Token::Star => (BinOp::Mul, 8),
26390 Token::Slash => (BinOp::Div, 8),
26391 Token::Percent => (BinOp::Mod, 8),
26392 // v4.14: JSON path ops bind tighter than comparisons (5)
26393 // and additive (7) so `doc->'k' = 'v'` parses correctly.
26394 // Same rung as the multiplicative ops.
26395 Token::JsonGet => (BinOp::JsonGet, 8),
26396 Token::JsonGetText => (BinOp::JsonGetText, 8),
26397 Token::JsonGetPath => (BinOp::JsonGetPath, 8),
26398 Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
26399 Token::JsonContains => (BinOp::JsonContains, 8),
26400 Token::JsonPathExists => (BinOp::JsonPathExists, 8),
26401 Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
26402 Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
26403 Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
26404 Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
26405 Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
26406 // v7.12.2 — `@@` binds at the comparison rung (looser than
26407 // arithmetic, tighter than AND / OR). PG places `@@` at
26408 // the same precedence as `=` / `<`, so we follow.
26409 Token::TsMatch => (BinOp::TsMatch, 5),
26410 // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
26411 // PG places these at the comparison rung (same level as `=`),
26412 // so we follow.
26413 Token::InetContainedBy => (BinOp::InetContainedBy, 5),
26414 Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
26415 Token::InetContains => (BinOp::InetContains, 5),
26416 Token::InetContainsEq => (BinOp::InetContainsEq, 5),
26417 Token::InetOverlap => (BinOp::InetOverlap, 5),
26418 // v7.39 (round 508) — the geometric and pattern-order predicates
26419 // ride the comparison rung, as every other predicate does.
26420 Token::Intersects => (BinOp::Intersects, 5),
26421 Token::IsBelow => (BinOp::IsBelow, 5),
26422 Token::IsAbove => (BinOp::IsAbove, 5),
26423 Token::PatternLt => (BinOp::PatternLt, 5),
26424 Token::PatternLtEq => (BinOp::PatternLtEq, 5),
26425 Token::PatternGt => (BinOp::PatternGt, 5),
26426 Token::PatternGtEq => (BinOp::PatternGtEq, 5),
26427 // `@@@` is the old spelling of `@@` and means exactly it.
26428 Token::TsMatchOld => (BinOp::TsMatch, 5),
26429 _ => return None,
26430 };
26431 Some(pair)
26432}
26433
26434#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26435// `as f32` here is intentional: vector elements widen / narrow into f32 on
26436// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
26437// past ~15 decimal digits — both are acceptable for a fixed-precision
26438// pgvector column.
26439/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
26440/// implicit table alias and break trailing clauses. WITH lands
26441/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
26442/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
26443/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
26444/// / VALUES / FOR / LATERAL — all of which would otherwise be
26445/// silently swallowed by `parse_optional_alias`.
26446fn is_alias_stopword(s: &str) -> bool {
26447 matches!(
26448 s.to_ascii_lowercase().as_str(),
26449 "with"
26450 | "on"
26451 | "where"
26452 | "having"
26453 | "group"
26454 | "order"
26455 | "limit"
26456 | "offset"
26457 | "union"
26458 | "except"
26459 | "intersect"
26460 | "returning"
26461 | "set"
26462 | "values"
26463 | "for"
26464 | "window"
26465 | "tablesample"
26466 | "lateral"
26467 | "left"
26468 | "right"
26469 | "inner"
26470 | "outer"
26471 | "full"
26472 | "cross"
26473 | "join"
26474 | "natural"
26475 | "using"
26476 | "fetch"
26477 )
26478}
26479
26480fn extract_numeric_literal(e: &Expr) -> Option<f32> {
26481 match e {
26482 Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
26483 Expr::Literal(Literal::Float(x)) => Some(*x as f32),
26484 // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
26485 // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
26486 // so scale the divisor by hand instead of `f32::powi`.)
26487 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26488 let mut div = 1.0f32;
26489 for _ in 0..*scale {
26490 div *= 10.0;
26491 }
26492 Some(*unscaled as f32 / div)
26493 }
26494 Expr::Unary {
26495 op: UnOp::Neg,
26496 expr,
26497 } => extract_numeric_literal(expr).map(|x| -x),
26498 _ => None,
26499 }
26500}
26501
26502/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
26503/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
26504/// negative. Returns `None` if any pair fails to parse or no pair is found.
26505///
26506/// Recognised units (case-insensitive, optional trailing `s`):
26507/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
26508/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
26509/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
26510/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
26511/// (PG-canonical: DST and month-boundary semantics depend on this).
26512/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
26513/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
26514/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
26515#[allow(clippy::cast_possible_truncation)]
26516fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
26517 let mut months: i64 = 0;
26518 let mut days: i64 = 0;
26519 let mut micros: i64 = 0;
26520 let mut in_time = false;
26521 let mut num = alloc::string::String::new();
26522 for ch in rest.chars() {
26523 if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
26524 num.push(ch);
26525 continue;
26526 }
26527 if ch == 'T' || ch == 't' {
26528 if !num.is_empty() {
26529 return None;
26530 }
26531 in_time = true;
26532 continue;
26533 }
26534 let n: f64 = num.parse().ok()?;
26535 num.clear();
26536 match (ch, in_time) {
26537 ('Y' | 'y', false) => months += (n * 12.0) as i64,
26538 ('M', false) => months += n as i64,
26539 ('W' | 'w', false) => days += (n * 7.0) as i64,
26540 ('D' | 'd', false) => days += n as i64,
26541 ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26542 ('M', true) => micros += (n * 60_000_000.0) as i64,
26543 ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26544 _ => return None,
26545 }
26546 }
26547 if !num.is_empty() {
26548 return None;
26549 }
26550 Some((
26551 i32::try_from(months).ok()?,
26552 i32::try_from(days).ok()?,
26553 micros,
26554 ))
26555}
26556
26557/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
26558/// leading `-` negates the whole value). Rejects date-like strings.
26559fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
26560 let (neg, body) = match s.strip_prefix('-') {
26561 Some(b) => (true, b),
26562 None => (false, s),
26563 };
26564 let (y, m) = body.split_once('-')?;
26565 let years: i32 = y.parse().ok()?;
26566 let mons: i32 = m.parse().ok()?;
26567 if years < 0 || mons < 0 {
26568 return None;
26569 }
26570 let total = years.checked_mul(12)?.checked_add(mons)?;
26571 Some((if neg { -total } else { total }, 0, 0))
26572}
26573
26574/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
26575/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
26576fn parse_interval_clock(tok: &str) -> Option<i64> {
26577 let (neg, body) = match tok.strip_prefix('-') {
26578 Some(r) => (true, r),
26579 None => (false, tok.strip_prefix('+').unwrap_or(tok)),
26580 };
26581 let mut it = body.split(':');
26582 let h: i64 = it.next()?.parse().ok()?;
26583 let m: i64 = it.next()?.parse().ok()?;
26584 let s_tok = it.next().unwrap_or("0");
26585 if it.next().is_some() {
26586 return None;
26587 }
26588 let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
26589 let sec: i64 = sec.parse().ok()?;
26590 let mut f = alloc::string::String::from(frac);
26591 while f.len() < 6 {
26592 f.push('0');
26593 }
26594 f.truncate(6);
26595 let fus: i64 = f.parse().ok()?;
26596 sec.checked_mul(1_000_000)?.checked_add(fus)?
26597 } else {
26598 s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
26599 };
26600 let total = h
26601 .checked_mul(3_600_000_000)?
26602 .checked_add(m.checked_mul(60_000_000)?)?
26603 .checked_add(sec_us)?;
26604 Some(if neg { -total } else { total })
26605}
26606
26607/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
26608/// every spelling PG accepts (measured against live PG18.4, not guessed):
26609/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
26610/// Before this, the unit table matched long names only, with an ad-hoc
26611/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
26612/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
26613/// INTERVAL", and it had also grown arms for the debris that stripping leaves
26614/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
26615/// fractional) both read from this one table now.
26616fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
26617 let u = raw.to_ascii_lowercase();
26618 Some(match u.as_str() {
26619 "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
26620 "microsecond"
26621 }
26622 "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
26623 "millisecond"
26624 }
26625 "second" | "seconds" | "sec" | "secs" | "s" => "second",
26626 "minute" | "minutes" | "min" | "mins" | "m" => "minute",
26627 "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
26628 "day" | "days" | "d" => "day",
26629 "week" | "weeks" | "w" => "week",
26630 "month" | "months" | "mon" | "mons" => "month",
26631 "year" | "years" | "yr" | "yrs" | "y" => "year",
26632 "decade" | "decades" | "dec" | "decs" => "decade",
26633 "century" | "centuries" | "cent" | "c" => "century",
26634 "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
26635 _ => return None,
26636 })
26637}
26638
26639/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
26640/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
26641#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26642pub(crate) enum IntervalField {
26643 Year,
26644 Month,
26645 Day,
26646 Hour,
26647 Minute,
26648 Second,
26649}
26650
26651/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
26652/// spellings aren't standard for the qualifier position, so only the singular
26653/// forms are accepted.
26654/// v7.39 (round 350, M7) — MySQL's interval units, measured against
26655/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
26656/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
26657/// take a `'1 2'` style literal — are not read here; they stay a parse
26658/// error rather than being silently misread.)
26659/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
26660///
26661/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
26662/// to do with a `@@` engine setting, and an unset one reads NULL rather
26663/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
26664/// were the same node and `SELECT @x` answered "Unknown system variable".)
26665/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
26666/// not see a session override — measured, after `SET autocommit=0`,
26667/// `@@global.autocommit` is still 1.
26668///
26669/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
26670/// the parser's nesting budget is tuned against, and building these
26671/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
26672/// wall `parse_left_right_atom` and friends were factored out for).
26673#[inline(never)]
26674fn variable_ref_atom(raw: &str) -> Expr {
26675 let user_var = !raw.starts_with("@@");
26676 let bare = raw.trim_start_matches('@').to_ascii_lowercase();
26677 Expr::FunctionCall {
26678 name: String::from(if user_var {
26679 "__spg_user_var"
26680 } else {
26681 "__spg_session_var"
26682 }),
26683 args: alloc::vec![Expr::Literal(Literal::String(bare))],
26684 }
26685}
26686
26687fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
26688 let Token::Ident(s) = tok else { return None };
26689 Some(match () {
26690 () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
26691 () if s.eq_ignore_ascii_case("second") => "second",
26692 () if s.eq_ignore_ascii_case("minute") => "minute",
26693 () if s.eq_ignore_ascii_case("hour") => "hour",
26694 () if s.eq_ignore_ascii_case("day") => "day",
26695 () if s.eq_ignore_ascii_case("week") => "week",
26696 () if s.eq_ignore_ascii_case("month") => "month",
26697 () if s.eq_ignore_ascii_case("quarter") => "quarter",
26698 () if s.eq_ignore_ascii_case("year") => "year",
26699 () => return None,
26700 })
26701}
26702
26703/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
26704/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
26705/// which constructs the value at run time. Only the slot the unit names
26706/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
26707/// slot the builtin has (months and fractional seconds respectively).
26708fn make_interval_call(qty: Expr, unit: &str) -> Expr {
26709 let zero = || Expr::Literal(Literal::Integer(0));
26710 let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
26711 lhs: alloc::boxed::Box::new(qty.clone()),
26712 op,
26713 rhs: alloc::boxed::Box::new(by),
26714 };
26715 // (years, months, weeks, days, hours, mins, secs)
26716 let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
26717 match unit {
26718 "year" => args[0] = qty,
26719 "quarter" => {
26720 args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
26721 }
26722 "month" => args[1] = qty,
26723 "week" => args[2] = qty,
26724 "day" => args[3] = qty,
26725 "hour" => args[4] = qty,
26726 "minute" => args[5] = qty,
26727 "second" => args[6] = qty,
26728 // The builtin's seconds slot takes a fraction, so microseconds ride
26729 // it scaled down; the divisor is a NUMERIC literal so the division
26730 // stays exact rather than going through a float.
26731 "microsecond" => {
26732 args[6] = scaled(
26733 crate::ast::BinOp::Div,
26734 Expr::Literal(Literal::Numeric {
26735 unscaled: 1_000_000,
26736 scale: 0,
26737 }),
26738 );
26739 }
26740 _ => args[3] = qty,
26741 }
26742 Expr::FunctionCall {
26743 name: alloc::string::String::from("make_interval"),
26744 args,
26745 }
26746}
26747
26748/// `(count, unit)` → `(months, days, micros)`.
26749fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
26750 let n: i64 = count.trim().parse().ok()?;
26751 Some(match unit {
26752 "microsecond" => (0, 0, n),
26753 "second" => (0, 0, n.checked_mul(1_000_000)?),
26754 "minute" => (0, 0, n.checked_mul(60_000_000)?),
26755 "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
26756 "day" => (0, i32::try_from(n).ok()?, 0),
26757 "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
26758 "month" => (i32::try_from(n).ok()?, 0, 0),
26759 "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
26760 "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
26761 _ => return None,
26762 })
26763}
26764
26765fn interval_field_of(tok: &Token) -> Option<IntervalField> {
26766 let Token::Ident(s) = tok else { return None };
26767 Some(match () {
26768 () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
26769 () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
26770 () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
26771 () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
26772 () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
26773 () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
26774 () => return None,
26775 })
26776}
26777
26778/// v7.39 (read01 round 102) — interpret an interval literal under a field
26779/// qualifier. Returns `(months, days, micros)`.
26780///
26781/// * A single field applied to a bare number sets which unit the number means,
26782/// truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
26783/// SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
26784/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
26785/// * Every other range, and any literal a single field can't read as a plain
26786/// number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
26787/// interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
26788/// like PG, and the qualifier there only bounds precision.
26789fn interpret_qualified_interval(
26790 text: &str,
26791 (f1, f2): (IntervalField, Option<IntervalField>),
26792) -> Option<(i32, i32, i64)> {
26793 if let Some(f2) = f2 {
26794 if f1 == IntervalField::Year && f2 == IntervalField::Month {
26795 if let Some(m) = parse_year_month_literal(text) {
26796 return Some((m, 0, 0));
26797 }
26798 }
26799 return parse_interval_text(text);
26800 }
26801 // Single field: reinterpret a bare number; otherwise the default parse.
26802 let trimmed = text.trim();
26803 if let Ok(val) = trimmed.parse::<f64>() {
26804 // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
26805 #[allow(clippy::cast_possible_truncation)]
26806 let whole = val as i64;
26807 #[allow(clippy::cast_possible_truncation)]
26808 let secs_micros = {
26809 let m = val * 1_000_000.0;
26810 if m >= 0.0 {
26811 (m + 0.5) as i64
26812 } else {
26813 (m - 0.5) as i64
26814 }
26815 };
26816 return Some(match f1 {
26817 IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
26818 IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
26819 IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
26820 IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
26821 IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
26822 IntervalField::Second => (0, 0, secs_micros),
26823 });
26824 }
26825 parse_interval_text(text)
26826}
26827
26828/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
26829fn parse_year_month_literal(text: &str) -> Option<i32> {
26830 let t = text.trim();
26831 let (neg, body) = match t.strip_prefix('-') {
26832 Some(r) => (true, r),
26833 None => (false, t.strip_prefix('+').unwrap_or(t)),
26834 };
26835 let mut it = body.split('-');
26836 let years: i32 = it.next()?.trim().parse().ok()?;
26837 let months: i32 = match it.next() {
26838 Some(m) => m.trim().parse().ok()?,
26839 None => 0,
26840 };
26841 if it.next().is_some() {
26842 return None;
26843 }
26844 let total = years.checked_mul(12)?.checked_add(months)?;
26845 Some(if neg { -total } else { total })
26846}
26847
26848pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
26849 // v7.38.19 — the two infinities, answered as the three extreme
26850 // fields PostgreSQL itself puts on the wire for them:
26851 //
26852 // COPY (SELECT 'infinity'::interval) TO STDOUT (FORMAT binary)
26853 // … 7fffffffffffffff 7fffffff 7fffffff
26854 //
26855 // So no caller has to know the spelling — every one of them already
26856 // reads the three numbers, and `IntervalKind::from_fields` names
26857 // what they mean.
26858 //
26859 // `inf` is NOT one of them, measured: `'inf'::interval` is *invalid
26860 // input syntax* on PostgreSQL 18.4 while `'inf'::float8` is
26861 // infinity. Interval takes the full word, in any case.
26862 {
26863 let word = s.trim();
26864 let word = word.strip_prefix('@').map_or(word, str::trim);
26865 let (neg, body) = match word.strip_prefix('-') {
26866 Some(rest) => (true, rest.trim_start()),
26867 None => (false, word.strip_prefix('+').map_or(word, str::trim_start)),
26868 };
26869 if body.eq_ignore_ascii_case("infinity") {
26870 return Some(if neg {
26871 (i32::MIN, i32::MIN, i64::MIN)
26872 } else {
26873 (i32::MAX, i32::MAX, i64::MAX)
26874 });
26875 }
26876 }
26877 // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
26878 // `@` is decorative; a trailing `ago` negates the whole interval.
26879 let mut trimmed = s.trim();
26880 trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
26881 let mut negate = false;
26882 if let Some(rest) = trimmed
26883 .strip_suffix("ago")
26884 .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
26885 {
26886 negate = true;
26887 trimmed = rest.trim();
26888 }
26889 let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
26890 let (mo, d, us) = v?;
26891 if negate {
26892 Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
26893 } else {
26894 Some((mo, d, us))
26895 }
26896 };
26897 let s = trimmed;
26898 // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
26899 // are single tokens, not the `<n> <unit>` pair form handled below.
26900 if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
26901 return finish(parse_iso8601_interval(rest));
26902 }
26903 if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
26904 if let Some(iv) = parse_year_month_interval(trimmed) {
26905 return finish(Some(iv));
26906 }
26907 }
26908 // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
26909 // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
26910 // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
26911 if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
26912 if let Ok(n) = trimmed.parse::<i64>() {
26913 return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
26914 }
26915 if let Ok(f) = trimmed.parse::<f64>() {
26916 if f.is_finite() {
26917 #[allow(clippy::cast_possible_truncation)]
26918 return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
26919 }
26920 }
26921 }
26922 // v7.39 (round 243) — PG accepts the number and unit run together
26923 // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
26924 // the `<n> <unit>` pair loop below sees them as two.
26925 let raw_parts: Vec<&str> = s.split_whitespace().collect();
26926 let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
26927 for p in raw_parts {
26928 let boundary = p
26929 .char_indices()
26930 .find(|(i, c)| {
26931 *i > 0
26932 && c.is_ascii_alphabetic()
26933 && p[..*i]
26934 .chars()
26935 .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
26936 && p[..*i].chars().any(|d| d.is_ascii_digit())
26937 })
26938 .map(|(i, _)| i);
26939 match boundary {
26940 Some(i) => {
26941 parts.push(&p[..i]);
26942 parts.push(&p[i..]);
26943 }
26944 None => parts.push(p),
26945 }
26946 }
26947 // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
26948 // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
26949 // remains is the `<n> <unit>` pair form handled below.
26950 let mut clock_us: i64 = 0;
26951 let mut had_clock = false;
26952 if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
26953 clock_us = parse_interval_clock(parts[pos])?;
26954 parts.remove(pos);
26955 had_clock = true;
26956 }
26957 // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
26958 // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
26959 let mut lone_days: i32 = 0;
26960 if had_clock && parts.len() == 1 {
26961 if let Ok(n) = parts[0].parse::<i64>() {
26962 lone_days = i32::try_from(n).ok()?;
26963 parts.clear();
26964 }
26965 }
26966 if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
26967 return None;
26968 }
26969 let mut months: i32 = 0;
26970 let mut days: i32 = lone_days;
26971 let mut micros: i64 = clock_us;
26972 let mut i = 0;
26973 while i < parts.len() {
26974 let unit_stripped = canonical_interval_unit(parts[i + 1])?;
26975 if let Ok(n) = parts[i].parse::<i64>() {
26976 match unit_stripped {
26977 "microsecond" => micros = micros.checked_add(n)?,
26978 "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
26979 "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
26980 "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
26981 "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
26982 "day" => {
26983 let n32 = i32::try_from(n).ok()?;
26984 days = days.checked_add(n32)?;
26985 }
26986 "week" => {
26987 let n32 = i32::try_from(n).ok()?;
26988 days = days.checked_add(n32.checked_mul(7)?)?;
26989 }
26990 "month" => {
26991 let n32 = i32::try_from(n).ok()?;
26992 months = months.checked_add(n32)?;
26993 }
26994 "year" => {
26995 let n32 = i32::try_from(n).ok()?;
26996 months = months.checked_add(n32.checked_mul(12)?)?;
26997 }
26998 // v7.39 (read01 timestamp.c) — the larger calendar units.
26999 "decade" => {
27000 let n32 = i32::try_from(n).ok()?;
27001 months = months.checked_add(n32.checked_mul(120)?)?;
27002 }
27003 "century" => {
27004 let n32 = i32::try_from(n).ok()?;
27005 months = months.checked_add(n32.checked_mul(1200)?)?;
27006 }
27007 "millennium" => {
27008 let n32 = i32::try_from(n).ok()?;
27009 months = months.checked_add(n32.checked_mul(12000)?)?;
27010 }
27011 _ => return None,
27012 }
27013 } else if let Ok(f) = parts[i].parse::<f64>() {
27014 // Fractional units cascade down to the next-finer field the way
27015 // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
27016 // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
27017 // no_std: f64 has no trunc/fract/round methods, so do them with
27018 // casts (toward-zero) + explicit round-half-away-from-zero.
27019 #[allow(clippy::cast_possible_truncation)]
27020 fn round_i64(x: f64) -> i64 {
27021 if x >= 0.0 {
27022 (x + 0.5) as i64
27023 } else {
27024 (x - 0.5) as i64
27025 }
27026 }
27027 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27028 fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
27029 const DAY_US: f64 = 86_400_000_000.0;
27030 let whole = d as i64; // truncates toward zero
27031 let frac = d - whole as f64;
27032 *days = days.checked_add(i32::try_from(whole).ok()?)?;
27033 *micros = micros.checked_add(round_i64(frac * DAY_US))?;
27034 Some(())
27035 }
27036 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27037 match unit_stripped {
27038 "microsecond" => micros = micros.checked_add(round_i64(f))?,
27039 "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
27040 "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
27041 "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
27042 "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
27043 "day" => add_days_frac(&mut days, &mut micros, f)?,
27044 "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
27045 "month" => {
27046 let whole = f as i64;
27047 months = months.checked_add(i32::try_from(whole).ok()?)?;
27048 add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
27049 }
27050 "year" => {
27051 let m = f * 12.0;
27052 let whole = m as i64;
27053 months = months.checked_add(i32::try_from(whole).ok()?)?;
27054 add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
27055 }
27056 _ => return None,
27057 }
27058 } else {
27059 return None;
27060 }
27061 i += 2;
27062 }
27063 finish(Some((months, days, micros)))
27064}
27065
27066/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
27067/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
27068/// `interval` is intentionally absent (handled by its own parser arm).
27069/// Returns `None` for names that aren't sensible as a bare typed literal, so
27070/// the caller falls back to treating the ident as a column reference.
27071fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
27072 Some(match ident {
27073 "date" => CastTarget::Date,
27074 "timestamp" | "datetime" => CastTarget::Timestamp,
27075 "timestamptz" => CastTarget::Timestamptz,
27076 "bool" | "boolean" => CastTarget::Bool,
27077 "int" | "integer" | "int4" => CastTarget::Int,
27078 "bigint" | "int8" => CastTarget::BigInt,
27079 "float8" | "double precision" => CastTarget::Float,
27080 "uuid" => CastTarget::Uuid,
27081 "bytea" => CastTarget::Bytea,
27082 "json" => CastTarget::Json,
27083 "jsonb" => CastTarget::Jsonb,
27084 // Types without a dedicated CastTarget variant flow through the
27085 // generic Named path (engine resolves via column_type_to_data_type).
27086 "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
27087 | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
27088 | "money" | "bit" | "varbit"
27089 // Geometric types accept the `TYPE 'literal'` prefix spelling too.
27090 | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
27091 // Range / multirange types likewise.
27092 | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
27093 | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
27094 | "datemultirange" | "tsmultirange" | "tstzmultirange"
27095 // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
27096 | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
27097 CastTarget::Named(alloc::string::String::from(ident))
27098 }
27099 _ => return None,
27100 })
27101}
27102
27103/// v7.12.4 — map a bare type-name identifier (the form that
27104/// appears in a function arg list or RETURNS clause) to a
27105/// [`ColumnTypeName`]. Returns `None` for unknown / extension
27106/// types so the caller can preserve them as
27107/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
27108///
27109/// Subset of the full column-type grammar — we deliberately
27110/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
27111/// here because function-arg types in v7.12.4 are mostly the
27112/// bare form (`text`, `int`, `bytea`, …).
27113/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
27114/// than being `name TYPE`?
27115///
27116/// The multi-word spellings SQL allows for a bare argument type, each
27117/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
27118///
27119/// NOTE this list also exists in `spg-storage`, which computes the
27120/// signature key from the rendered argument text and has to reach the
27121/// same verdict. The two crates are siblings — neither depends on the
27122/// other — and each already carries its own table of type spellings
27123/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
27124/// there), so this follows the structure rather than inventing new
27125/// duplication. Recorded as V49.
27126pub fn is_multiword_type_phrase(phrase: &str) -> bool {
27127 let t = phrase.trim().to_ascii_lowercase();
27128 let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
27129 matches!(
27130 base,
27131 "double precision"
27132 | "character varying"
27133 | "bit varying"
27134 | "timestamp with time zone"
27135 | "timestamp without time zone"
27136 | "time with time zone"
27137 | "time without time zone"
27138 | "national character"
27139 | "national character varying"
27140 )
27141}
27142
27143fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
27144 Some(match ident.to_ascii_lowercase().as_str() {
27145 "smallint" | "tinyint" => ColumnTypeName::SmallInt,
27146 "int" | "integer" | "mediumint" => ColumnTypeName::Int,
27147 "bigint" => ColumnTypeName::BigInt,
27148 "float" | "double" => ColumnTypeName::Float,
27149 // v7.39 (round 269) — real is 32-bit.
27150 "real" | "float4" => ColumnTypeName::Real,
27151 "text" => ColumnTypeName::Text,
27152 "bool" | "boolean" => ColumnTypeName::Bool,
27153 "date" => ColumnTypeName::Date,
27154 "timestamp" | "datetime" => ColumnTypeName::Timestamp,
27155 "timestamptz" => ColumnTypeName::Timestamptz,
27156 "json" => ColumnTypeName::Json,
27157 "jsonb" => ColumnTypeName::Jsonb,
27158 "bytea" | "bytes" => ColumnTypeName::Bytes,
27159 "tsvector" => ColumnTypeName::TsVector,
27160 "tsquery" => ColumnTypeName::TsQuery,
27161 "uuid" => ColumnTypeName::Uuid,
27162 "interval" => ColumnTypeName::Interval,
27163 "time" => ColumnTypeName::Time,
27164 "year" => ColumnTypeName::Year,
27165 "timetz" => ColumnTypeName::TimeTz,
27166 "money" => ColumnTypeName::Money,
27167 _ => return None,
27168 })
27169}
27170
27171/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
27172/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
27173///
27174/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
27175/// / embedded SQL land in v7.12.5+):
27176///
27177/// ```text
27178/// body := [ws] block [ws]
27179/// block := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
27180/// stmt := assign | return
27181/// assign := assign_target := expr
27182/// assign_target := ( NEW | OLD ) . ident | ident
27183/// return := RETURN ( NEW | OLD | NULL | expr )
27184/// ```
27185///
27186/// `expr` is parsed by recursing into the regular `Parser` — so a
27187/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
27188/// NEW.subject || ' ' || NEW.sender)` body shape works without
27189/// the body parser knowing what `to_tsvector` is.
27190///
27191/// Errors here cause the caller to fall back to
27192/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
27193/// successful, but the executor will refuse to invoke the
27194/// function with an "unparseable body" error.
27195/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
27196/// from the crate root as `spg_sql::parse_function_body`.
27197pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
27198 parse_plpgsql_body(body)
27199}
27200
27201fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
27202 // Use the regular lexer on the body text. The trailing
27203 // `END;` may or may not have a semicolon; the lexer treats
27204 // both forms identically.
27205 let tokens = lexer::tokenize(body).map_err(|e| ParseError {
27206 message: alloc::format!("plpgsql body lex error: {e}"),
27207 token_pos: 0,
27208 })?;
27209 let mut parser = Parser::new(tokens);
27210 parser.parse_plpgsql_block()
27211}
27212
27213/// v7.39 (GUC) — the textual body of a SET value, for list joining.
27214fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
27215 match v {
27216 crate::ast::SetValue::String(s)
27217 | crate::ast::SetValue::Ident(s)
27218 | crate::ast::SetValue::Number(s) => s.clone(),
27219 crate::ast::SetValue::Default => "DEFAULT".into(),
27220 }
27221}
27222
27223/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
27224/// contains an aggregate call at ITS OWN query level (recursion stops at
27225/// sublink boundaries — a sublink's aggregates belong to the sublink).
27226/// Backs the "aggregate functions are not allowed in a recursive query's
27227/// recursive term" well-formedness check.
27228fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
27229 const AGG_NAMES: &[&str] = &[
27230 "count",
27231 "sum",
27232 "min",
27233 "max",
27234 "avg",
27235 "string_agg",
27236 "array_agg",
27237 "bool_and",
27238 "bool_or",
27239 "every",
27240 "any_value",
27241 "json_agg",
27242 "jsonb_agg",
27243 "json_object_agg",
27244 "jsonb_object_agg",
27245 "bit_and",
27246 "bit_or",
27247 "bit_xor",
27248 "var_pop",
27249 "var_samp",
27250 "variance",
27251 "stddev",
27252 "stddev_pop",
27253 "stddev_samp",
27254 "range_agg",
27255 "range_intersect_agg",
27256 "percentile_cont",
27257 "percentile_disc",
27258 "mode",
27259 "corr",
27260 "covar_pop",
27261 "covar_samp",
27262 ];
27263 match e {
27264 Expr::AggregateOrdered { .. } => true,
27265 Expr::FunctionCall { name, args } => {
27266 AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
27267 || args.iter().any(expr_has_toplevel_aggregate)
27268 }
27269 Expr::NamedArg { expr, .. }
27270 | Expr::Variadic(expr)
27271 | Expr::Unary { expr, .. }
27272 | Expr::Cast { expr, .. }
27273 | Expr::IsNull { expr, .. }
27274 | Expr::FieldAccess { base: expr, .. }
27275 | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
27276 Expr::Binary { lhs, rhs, .. } => {
27277 expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
27278 }
27279 Expr::Like { expr, pattern, .. } => {
27280 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
27281 }
27282 Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
27283 Expr::InList { expr, list, .. } => {
27284 expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
27285 }
27286 Expr::ArraySubscript { target, index } => {
27287 expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
27288 }
27289 Expr::ArraySlice { target, lo, hi } => {
27290 expr_has_toplevel_aggregate(target)
27291 || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
27292 || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
27293 }
27294 Expr::AnyAll { expr, array, .. } => {
27295 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
27296 }
27297 Expr::Case {
27298 operand,
27299 branches,
27300 else_branch,
27301 } => {
27302 operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
27303 || branches
27304 .iter()
27305 .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
27306 || else_branch
27307 .as_deref()
27308 .is_some_and(expr_has_toplevel_aggregate)
27309 }
27310 // The outer-level operands of a sublink can aggregate; the sublink's
27311 // own body cannot leak its aggregates up here.
27312 Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
27313 Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
27314 row.iter().any(expr_has_toplevel_aggregate)
27315 }
27316 _ => false,
27317 }
27318}
27319
27320/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
27321/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
27322/// named table anywhere in its subtree. A plain FROM derived table is NOT a
27323/// sublink and is legal in a recursive term, so it is not walked here.
27324fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
27325 let mut exprs: Vec<&Expr> = Vec::new();
27326 for it in &s.items {
27327 if let crate::ast::SelectItem::Expr { expr, .. } = it {
27328 exprs.push(expr);
27329 }
27330 }
27331 if let Some(w) = &s.where_ {
27332 exprs.push(w);
27333 }
27334 if let Some(h) = &s.having {
27335 exprs.push(h);
27336 }
27337 if let Some(g) = &s.group_by {
27338 exprs.extend(g.iter());
27339 }
27340 if let Some(from) = &s.from {
27341 for j in &from.joins {
27342 if let Some(on) = &j.on {
27343 exprs.push(on);
27344 }
27345 }
27346 }
27347 exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
27348}
27349
27350/// Does this expression contain a sublink whose subquery mentions `name`?
27351fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
27352 match e {
27353 Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
27354 Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
27355 Expr::InSubquery { expr, subquery, .. } => {
27356 expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
27357 }
27358 Expr::RowInSubquery { row, subquery, .. } => {
27359 row.iter().any(|x| expr_sublink_mentions(x, name))
27360 || select_mentions_table(subquery, name)
27361 }
27362 Expr::RowCmpSubquery { row, subquery, .. } => {
27363 row.iter().any(|x| expr_sublink_mentions(x, name))
27364 || select_mentions_table(subquery, name)
27365 }
27366 Expr::NamedArg { expr, .. }
27367 | Expr::Variadic(expr)
27368 | Expr::Unary { expr, .. }
27369 | Expr::Cast { expr, .. }
27370 | Expr::IsNull { expr, .. }
27371 | Expr::FieldAccess { base: expr, .. }
27372 | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
27373 Expr::Binary { lhs, rhs, .. } => {
27374 expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
27375 }
27376 Expr::Like { expr, pattern, .. } => {
27377 expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
27378 }
27379 Expr::FunctionCall { args, .. } | Expr::Array(args) => {
27380 args.iter().any(|x| expr_sublink_mentions(x, name))
27381 }
27382 Expr::InList { expr, list, .. } => {
27383 expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
27384 }
27385 Expr::ArraySubscript { target, index } => {
27386 expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
27387 }
27388 Expr::ArraySlice { target, lo, hi } => {
27389 expr_sublink_mentions(target, name)
27390 || lo
27391 .as_deref()
27392 .is_some_and(|x| expr_sublink_mentions(x, name))
27393 || hi
27394 .as_deref()
27395 .is_some_and(|x| expr_sublink_mentions(x, name))
27396 }
27397 Expr::AnyAll { expr, array, .. } => {
27398 expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
27399 }
27400 Expr::Case {
27401 operand,
27402 branches,
27403 else_branch,
27404 } => {
27405 operand
27406 .as_deref()
27407 .is_some_and(|x| expr_sublink_mentions(x, name))
27408 || branches
27409 .iter()
27410 .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
27411 || else_branch
27412 .as_deref()
27413 .is_some_and(|x| expr_sublink_mentions(x, name))
27414 }
27415 _ => false,
27416 }
27417}
27418
27419/// Does this SELECT (in full — FROM tables, derived tables, its own
27420/// sublinks, and union arms) mention the named table?
27421fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
27422 if let Some(from) = &s.from {
27423 if from.primary.name.eq_ignore_ascii_case(name) {
27424 return true;
27425 }
27426 if let Some(sub) = &from.primary.lateral_subquery
27427 && select_mentions_table(sub, name)
27428 {
27429 return true;
27430 }
27431 for j in &from.joins {
27432 if j.table.name.eq_ignore_ascii_case(name) {
27433 return true;
27434 }
27435 if let Some(sub) = &j.table.lateral_subquery
27436 && select_mentions_table(sub, name)
27437 {
27438 return true;
27439 }
27440 }
27441 }
27442 if select_has_self_ref_in_sublink(s, name) {
27443 return true;
27444 }
27445 s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
27446}
27447
27448/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
27449/// row count, the way PG evaluates one before applying it.
27450///
27451/// `None` = not a constant (a column, a subquery, a function call).
27452/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
27453/// message stands in for LIMIT / OFFSET, which the caller substitutes.
27454/// All wordings were read off live PG 18.4.
27455fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
27456 use crate::ast::{BinOp, Expr, Literal, UnOp};
27457 match e {
27458 Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
27459 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27460 Some(Ok(round_scaled_half_away(*unscaled, *scale)))
27461 }
27462 // PG coerces a string by its CONTENT, and fails on the value.
27463 Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
27464 |_| {
27465 Err(alloc::format!(
27466 "invalid input syntax for type bigint: \"{t}\""
27467 ))
27468 },
27469 |n| Ok(i128::from(n)),
27470 )),
27471 Expr::Literal(Literal::Bool(_)) => Some(Err(
27472 "argument of {L} must be type bigint, not type boolean".into(),
27473 )),
27474 Expr::Unary {
27475 op: UnOp::Neg,
27476 expr,
27477 } => match fold_limit_constant(expr)? {
27478 Ok(v) => Some(Ok(-v)),
27479 e @ Err(_) => Some(e),
27480 },
27481 Expr::Binary { lhs, op, rhs } => {
27482 let a = match fold_limit_constant(lhs)? {
27483 Ok(v) => v,
27484 e @ Err(_) => return Some(e),
27485 };
27486 let b = match fold_limit_constant(rhs)? {
27487 Ok(v) => v,
27488 e @ Err(_) => return Some(e),
27489 };
27490 let out = match op {
27491 BinOp::Add => a.checked_add(b),
27492 BinOp::Sub => a.checked_sub(b),
27493 BinOp::Mul => a.checked_mul(b),
27494 BinOp::Div if b != 0 => a.checked_div(b),
27495 BinOp::Div => return Some(Err("division by zero".into())),
27496 BinOp::Mod if b != 0 => a.checked_rem(b),
27497 BinOp::Mod => return Some(Err("division by zero".into())),
27498 _ => return None,
27499 };
27500 // PG evaluates the arithmetic in the operand's own type, so an
27501 // int-by-int product that leaves int range fails there — before
27502 // the row count is ever looked at.
27503 match out {
27504 Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
27505 Some(Err("integer out of range".into()))
27506 }
27507 Some(v) => Some(Ok(v)),
27508 None => Some(Err("integer out of range".into())),
27509 }
27510 }
27511 _ => None,
27512 }
27513}
27514
27515/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
27516/// cast, which is what makes `LIMIT 2.5` keep three rows.
27517fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
27518 if scale == 0 {
27519 return unscaled;
27520 }
27521 let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
27522 return 0;
27523 };
27524 let neg = unscaled < 0;
27525 let mag = unscaled.unsigned_abs() as i128;
27526 let rounded = (mag + div / 2) / div;
27527 if neg { -rounded } else { rounded }
27528}
27529
27530#[cfg(test)]
27531mod tests {
27532 use super::*;
27533 use alloc::string::ToString;
27534
27535 fn parse(s: &str) -> Statement {
27536 parse_statement(s).expect("parse ok")
27537 }
27538
27539 // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
27540 // `tables`, `partition`, etc. are unreserved keywords per PG's
27541 // `pg_get_keywords()` and MUST be usable as column / table /
27542 // alias names. Pre-T4 every drop-in user whose schema had one
27543 // of these as a column name (sentori events.release, mailrs
27544 // messages.index in some forks) blew the parser up at CREATE
27545 // TABLE time with "expected identifier, got Release". The
27546 // generalisation lives in `unreserved_keyword_text` + the
27547 // `expect_ident_like` and `parse_atom` arms that consult it.
27548 #[test]
27549 fn release_usable_as_column_name_in_create_table() {
27550 let stmt =
27551 parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
27552 if let Statement::CreateTable(t) = stmt {
27553 let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
27554 assert_eq!(names, alloc::vec!["id", "release", "payload"]);
27555 } else {
27556 panic!("expected CreateTable");
27557 }
27558 }
27559
27560 #[test]
27561 fn release_usable_as_column_ref_in_select_projection() {
27562 // The sentori `0003_partition_events.sql` INSERT-SELECT
27563 // walk references `release` in both column lists; the
27564 // projection-side use exercises `parse_atom`'s relaxed
27565 // identifier set.
27566 parse("SELECT id, release, payload FROM events WHERE id = 1");
27567 }
27568
27569 #[test]
27570 fn release_usable_as_column_ref_in_insert_column_list() {
27571 // INSERT INTO t (id, release, payload) VALUES (…)
27572 parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
27573 }
27574
27575 #[test]
27576 fn alter_column_drop_not_null_uses_keyword_drop_token() {
27577 // Sentori `0013_audit_tombstone.sql` issues
27578 // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
27579 // emits Token::Drop (not Ident("drop")); the parser must
27580 // accept both in the ALTER COLUMN sub-dispatch.
27581 parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
27582 }
27583
27584 #[test]
27585 fn create_index_accepts_parenthesised_expression_key() {
27586 // sentori `0040_events_bundle_idx.sql` shape — JSONB
27587 // expression index. Pre-T4 the parser bailed at the
27588 // inner `(` with "expected column ident or expression,
27589 // got LParen". The Token::LParen arm in CREATE INDEX
27590 // routes through the expression parser instead.
27591 parse(
27592 "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
27593 ON events ((payload->'bundle'->>'id'))",
27594 );
27595 }
27596
27597 // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
27598 // surface as parse errors, never stack overflows (embed hosts
27599 // abort on overflow).
27600 /// The nesting budget is a COUNT; what it has to fit inside is a
27601 /// number of BYTES, and only one of those two is stable across
27602 /// compiler versions. Round 847 measured 30,336 bytes per level
27603 /// after a toolchain move, which puts 64 levels at 1.94 MB and
27604 /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
27605 /// aborted instead of erroring, which is precisely the outcome it
27606 /// exists to rule out.
27607 ///
27608 /// So the budget is metered rather than assumed. The ceiling leaves
27609 /// the depth SPG advertises fitting in a default 2 MiB thread with
27610 /// room to spare, in the debug build, where frames are widest.
27611 #[test]
27612 fn nesting_frame_cost_stays_under_ceiling() {
27613 // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
27614 // thread keeps a margin for whatever called the parser.
27615 const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
27616
27617 frame_meter::reset();
27618 let depth = frame_meter::SAMPLE_HI + 8;
27619 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27620 parse(&sql);
27621
27622 let per_level = frame_meter::bytes_per_level();
27623 {
27624 extern crate std;
27625 std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
27626 }
27627 assert!(
27628 per_level <= CEILING,
27629 "{per_level} bytes per nesting level exceeds {CEILING}; \
27630 {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
27631 in parse_expr_inner / parse_unary rather than lowering the \
27632 depth or widening the stack.",
27633 per_level * MAX_NEST_DEPTH
27634 );
27635 }
27636
27637 #[test]
27638 fn nesting_budget_errors_cleanly() {
27639 let depth = MAX_NEST_DEPTH + 50;
27640 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27641 let err = parse_statement(&sql).expect_err("must reject");
27642 assert!(err.message.contains("nests deeper"), "{err:?}");
27643 // Within budget still parses.
27644 let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
27645 parse(&sql);
27646 }
27647
27648 #[test]
27649 fn binary_chain_budget_errors_cleanly() {
27650 let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
27651 let err = parse_statement(&sql).expect_err("must reject");
27652 assert!(err.message.contains("chained binary"), "{err:?}");
27653 // Within budget still parses (chain depth ≤ budget is safe
27654 // for recursive eval/drop on 2 MiB stacks).
27655 let sql = format!("SELECT 1{}", " + 1".repeat(200));
27656 parse(&sql);
27657 }
27658
27659 #[test]
27660 fn in_list_unaffected_by_chain_budget() {
27661 // Flat InList: 20k elements parse fine and stay flat.
27662 let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
27663 let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
27664 let Statement::Select(s) = parse(&sql) else {
27665 panic!("expected select")
27666 };
27667 let Some(Expr::InList { list, negated, .. }) = s.where_ else {
27668 panic!("expected flat InList, got {:?}", s.where_)
27669 };
27670 assert_eq!(list.len(), 20_000);
27671 assert!(!negated);
27672 }
27673
27674 fn lit_int(n: i64) -> Expr {
27675 Expr::Literal(Literal::Integer(n))
27676 }
27677
27678 fn col(name: &str) -> Expr {
27679 Expr::Column(ColumnName {
27680 qualifier: None,
27681 name: name.into(),
27682 })
27683 }
27684
27685 #[test]
27686 fn select_single_integer() {
27687 let s = parse("SELECT 1");
27688 let Statement::Select(s) = s else {
27689 panic!("expected SELECT")
27690 };
27691 assert_eq!(s.items.len(), 1);
27692 assert!(s.from.is_none());
27693 assert!(s.where_.is_none());
27694 }
27695
27696 #[test]
27697 fn select_multiple_literal_kinds() {
27698 let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
27699 let Statement::Select(s) = s else {
27700 panic!("expected SELECT")
27701 };
27702 assert_eq!(s.items.len(), 5);
27703 }
27704
27705 #[test]
27706 fn select_wildcard_from_table() {
27707 let s = parse("SELECT * FROM users");
27708 let Statement::Select(s) = s else {
27709 panic!("expected SELECT")
27710 };
27711 assert!(matches!(s.items[..], [SelectItem::Wildcard]));
27712 assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
27713 }
27714
27715 #[test]
27716 fn select_with_table_alias() {
27717 let s = parse("SELECT * FROM users AS u");
27718 let Statement::Select(s) = s else {
27719 panic!("expected SELECT")
27720 };
27721 let t = &s.from.as_ref().unwrap().primary;
27722 assert_eq!(t.name, "users");
27723 assert_eq!(t.alias.as_deref(), Some("u"));
27724 }
27725
27726 #[test]
27727 fn select_with_where_eq() {
27728 let s = parse("SELECT a FROM t WHERE a = 1");
27729 let Statement::Select(s) = s else {
27730 panic!("expected SELECT")
27731 };
27732 let w = s.where_.unwrap();
27733 assert_eq!(
27734 w,
27735 Expr::Binary {
27736 lhs: Box::new(col("a")),
27737 op: BinOp::Eq,
27738 rhs: Box::new(lit_int(1)),
27739 }
27740 );
27741 }
27742
27743 #[test]
27744 fn arithmetic_precedence() {
27745 let s = parse("SELECT 1 + 2 * 3");
27746 let Statement::Select(s) = s else {
27747 panic!("expected SELECT")
27748 };
27749 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27750 panic!("wildcard?")
27751 };
27752 assert_eq!(
27753 expr,
27754 &Expr::Binary {
27755 lhs: Box::new(lit_int(1)),
27756 op: BinOp::Add,
27757 rhs: Box::new(Expr::Binary {
27758 lhs: Box::new(lit_int(2)),
27759 op: BinOp::Mul,
27760 rhs: Box::new(lit_int(3)),
27761 }),
27762 }
27763 );
27764 }
27765
27766 #[test]
27767 fn parentheses_override_precedence() {
27768 let s = parse("SELECT (1 + 2) * 3");
27769 let Statement::Select(s) = s else {
27770 panic!("expected SELECT")
27771 };
27772 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27773 panic!()
27774 };
27775 assert_eq!(
27776 expr,
27777 &Expr::Binary {
27778 lhs: Box::new(Expr::Binary {
27779 lhs: Box::new(lit_int(1)),
27780 op: BinOp::Add,
27781 rhs: Box::new(lit_int(2)),
27782 }),
27783 op: BinOp::Mul,
27784 rhs: Box::new(lit_int(3)),
27785 }
27786 );
27787 }
27788
27789 #[test]
27790 fn not_binds_below_comparison() {
27791 // `NOT a = 1` should parse as `NOT (a = 1)`.
27792 let s = parse("SELECT NOT a = 1 FROM t");
27793 let Statement::Select(s) = s else {
27794 panic!("expected SELECT")
27795 };
27796 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27797 panic!()
27798 };
27799 assert_eq!(
27800 expr,
27801 &Expr::Unary {
27802 op: UnOp::Not,
27803 expr: Box::new(Expr::Binary {
27804 lhs: Box::new(col("a")),
27805 op: BinOp::Eq,
27806 rhs: Box::new(lit_int(1)),
27807 }),
27808 }
27809 );
27810 }
27811
27812 #[test]
27813 fn unary_minus_binds_above_multiplication() {
27814 // `-a * 2` should be `(-a) * 2`.
27815 let s = parse("SELECT -a * 2 FROM t");
27816 let Statement::Select(s) = s else {
27817 panic!("expected SELECT")
27818 };
27819 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27820 panic!()
27821 };
27822 assert_eq!(
27823 expr,
27824 &Expr::Binary {
27825 lhs: Box::new(Expr::Unary {
27826 op: UnOp::Neg,
27827 expr: Box::new(col("a")),
27828 }),
27829 op: BinOp::Mul,
27830 rhs: Box::new(lit_int(2)),
27831 }
27832 );
27833 }
27834
27835 #[test]
27836 fn qualified_column() {
27837 let s = parse("SELECT t.col FROM t");
27838 let Statement::Select(s) = s else {
27839 panic!("expected SELECT")
27840 };
27841 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27842 panic!()
27843 };
27844 assert_eq!(
27845 expr,
27846 &Expr::Column(ColumnName {
27847 qualifier: Some("t".into()),
27848 name: "col".into()
27849 })
27850 );
27851 }
27852
27853 #[test]
27854 fn select_item_alias_with_as() {
27855 let s = parse("SELECT a AS y FROM t");
27856 let Statement::Select(s) = s else {
27857 panic!("expected SELECT")
27858 };
27859 let SelectItem::Expr { alias, .. } = &s.items[0] else {
27860 panic!()
27861 };
27862 assert_eq!(alias.as_deref(), Some("y"));
27863 }
27864
27865 #[test]
27866 fn trailing_semicolon_accepted() {
27867 let s = parse("SELECT 1;");
27868 let Statement::Select(s) = s else {
27869 panic!("expected SELECT")
27870 };
27871 assert_eq!(s.items.len(), 1);
27872 }
27873
27874 #[test]
27875 fn boolean_chain_with_and_or_not() {
27876 // (NOT a) OR (b AND (NOT c))
27877 let s = parse("SELECT NOT a OR b AND NOT c FROM t");
27878 let Statement::Select(s) = s else {
27879 panic!("expected SELECT")
27880 };
27881 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27882 panic!()
27883 };
27884 let expected = Expr::Binary {
27885 lhs: Box::new(Expr::Unary {
27886 op: UnOp::Not,
27887 expr: Box::new(col("a")),
27888 }),
27889 op: BinOp::Or,
27890 rhs: Box::new(Expr::Binary {
27891 lhs: Box::new(col("b")),
27892 op: BinOp::And,
27893 rhs: Box::new(Expr::Unary {
27894 op: UnOp::Not,
27895 expr: Box::new(col("c")),
27896 }),
27897 }),
27898 };
27899 assert_eq!(expr, &expected);
27900 }
27901
27902 #[test]
27903 fn empty_input_errors() {
27904 // v7.14.0 — pg_dump preambles emit several comment-only
27905 // / blank-line statements that collapse to Statement::
27906 // Empty rather than a parse error. The old "SELECT in
27907 // message" assertion is stale; verify the new contract:
27908 // empty / whitespace / comment-only input parses to
27909 // Statement::Empty.
27910 assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
27911 assert!(matches!(
27912 parse_statement(" \n\t ").unwrap(),
27913 Statement::Empty
27914 ));
27915 // Sanity: malformed-but-non-empty still errors.
27916 assert!(parse_statement("SELECT FROM WHERE").is_err());
27917 }
27918
27919 #[test]
27920 fn unmatched_paren_errors() {
27921 assert!(parse_statement("SELECT (1 + 2").is_err());
27922 }
27923
27924 #[test]
27925 fn display_round_trip_simple_select() {
27926 let original = parse("SELECT a + 1 FROM t WHERE a > 0");
27927 let text = original.to_string();
27928 let again = parse_statement(&text).expect("re-parse");
27929 assert_eq!(original, again);
27930 }
27931
27932 // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
27933
27934 #[test]
27935 fn create_table_single_column() {
27936 let s = parse("CREATE TABLE foo (a INT)");
27937 let Statement::CreateTable(c) = s else {
27938 panic!("expected CreateTable")
27939 };
27940 assert_eq!(c.name, "foo");
27941 assert_eq!(c.columns.len(), 1);
27942 assert_eq!(c.columns[0].name, "a");
27943 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27944 assert!(c.columns[0].nullable);
27945 }
27946
27947 #[test]
27948 fn create_table_multi_column_with_not_null_mix() {
27949 let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
27950 let Statement::CreateTable(c) = s else {
27951 panic!()
27952 };
27953 assert_eq!(c.columns.len(), 4);
27954 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27955 assert!(!c.columns[0].nullable);
27956 assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
27957 assert!(c.columns[1].nullable);
27958 assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
27959 assert!(!c.columns[2].nullable);
27960 assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
27961 }
27962
27963 #[test]
27964 fn create_table_bigint_supported() {
27965 let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
27966 let Statement::CreateTable(c) = s else {
27967 panic!()
27968 };
27969 assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
27970 }
27971
27972 #[test]
27973 fn create_table_vector_default_is_f32() {
27974 let s = parse("CREATE TABLE t (v VECTOR(128))");
27975 let Statement::CreateTable(c) = s else {
27976 panic!()
27977 };
27978 assert_eq!(
27979 c.columns[0].ty,
27980 ColumnTypeName::Vector {
27981 dim: 128,
27982 encoding: VecEncoding::F32,
27983 },
27984 );
27985 }
27986
27987 #[test]
27988 fn create_table_vector_using_sq8() {
27989 // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
27990 // Case-insensitive on both `USING` and the encoding name.
27991 for sql in [
27992 "CREATE TABLE t (v VECTOR(128) USING SQ8)",
27993 "CREATE TABLE t (v VECTOR(128) using sq8)",
27994 ] {
27995 let s = parse(sql);
27996 let Statement::CreateTable(c) = s else {
27997 panic!()
27998 };
27999 assert_eq!(
28000 c.columns[0].ty,
28001 ColumnTypeName::Vector {
28002 dim: 128,
28003 encoding: VecEncoding::Sq8,
28004 },
28005 "{sql}",
28006 );
28007 }
28008 }
28009
28010 #[test]
28011 fn create_table_vector_using_unknown_errors() {
28012 // v7.16.1 — the inline `USING <encoding>` shape on
28013 // CREATE TABLE column defs was withdrawn before
28014 // v7.14.0 in favour of `CREATE INDEX … USING hnsw
28015 // (col vector_<metric>_ops)`; the parser now rejects
28016 // USING at column-list position with a clearer
28017 // "expected ',' or ')'" message. Test asserts the
28018 // current rejection, not the old "unknown vector
28019 // encoding" string.
28020 let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
28021 assert!(
28022 err.message.contains("USING")
28023 || err.message.contains("using")
28024 || err.message.contains("')'")
28025 || err.message.contains("','"),
28026 "expected USING/column-list rejection, got: {}",
28027 err.message
28028 );
28029 }
28030
28031 #[test]
28032 fn vector_using_sq8_display_roundtrips() {
28033 // The Display impl must produce text that re-parses to the
28034 // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
28035 let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
28036 let Statement::CreateTable(c) = s else {
28037 panic!()
28038 };
28039 assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
28040 }
28041
28042 #[test]
28043 fn parser_recognises_placeholders() {
28044 use crate::ast::{Expr, SelectItem, Statement};
28045 // $N in expression position parses as Expr::Placeholder(N).
28046 let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
28047 let Statement::Select(sel) = s else { panic!() };
28048 assert!(matches!(
28049 sel.items[0],
28050 SelectItem::Expr {
28051 expr: Expr::Placeholder(1),
28052 alias: None
28053 }
28054 ));
28055 // $2 + 1
28056 let SelectItem::Expr {
28057 expr: Expr::Binary { lhs, rhs, .. },
28058 ..
28059 } = &sel.items[1]
28060 else {
28061 panic!()
28062 };
28063 assert!(matches!(**lhs, Expr::Placeholder(2)));
28064 assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
28065 // WHERE x = $3
28066 let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
28067 panic!()
28068 };
28069 assert!(matches!(**rhs, Expr::Placeholder(3)));
28070 }
28071
28072 #[test]
28073 fn parser_rejects_dollar_zero() {
28074 // $0 is not valid in PG; the lexer rejects it.
28075 assert!(parse_statement("SELECT $0").is_err());
28076 }
28077
28078 #[test]
28079 fn placeholder_display_roundtrips() {
28080 // The Display impl must produce text that re-lexes to the
28081 // same Placeholder token.
28082 let s = parse("SELECT $42 FROM t");
28083 let printed = s.to_string();
28084 assert!(printed.contains("$42"));
28085 let again = parse(&printed);
28086 assert_eq!(s, again);
28087 }
28088
28089 #[test]
28090 fn alter_index_rebuild_bare() {
28091 use crate::ast::{AlterIndexTarget, Statement};
28092 let s = parse("ALTER INDEX my_idx REBUILD");
28093 let Statement::AlterIndex(a) = s else {
28094 panic!("expected AlterIndex, got {s:?}")
28095 };
28096 assert_eq!(a.name, "my_idx");
28097 assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
28098 }
28099
28100 #[test]
28101 fn alter_index_rebuild_with_encoding() {
28102 use crate::ast::{AlterIndexTarget, Statement};
28103 for (sql, want) in [
28104 (
28105 "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
28106 VecEncoding::F32,
28107 ),
28108 (
28109 "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
28110 VecEncoding::Sq8,
28111 ),
28112 (
28113 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28114 VecEncoding::F16,
28115 ),
28116 ] {
28117 let s = parse(sql);
28118 let Statement::AlterIndex(a) = s else {
28119 panic!("{sql}: expected AlterIndex")
28120 };
28121 assert_eq!(a.name, "my_idx");
28122 assert_eq!(
28123 a.target,
28124 AlterIndexTarget::Rebuild {
28125 encoding: Some(want)
28126 },
28127 "{sql}"
28128 );
28129 }
28130 }
28131
28132 #[test]
28133 fn alter_index_rebuild_unknown_encoding_errors() {
28134 let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
28135 assert!(
28136 err.message.contains("unknown vector encoding"),
28137 "got: {}",
28138 err.message
28139 );
28140 }
28141
28142 #[test]
28143 fn alter_index_rebuild_display_roundtrips() {
28144 for (input, want) in [
28145 ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
28146 (
28147 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28148 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28149 ),
28150 (
28151 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28152 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28153 ),
28154 ] {
28155 let s = parse(input);
28156 assert_eq!(s.to_string(), want);
28157 }
28158 }
28159
28160 #[test]
28161 fn create_table_unknown_type_defers_to_engine() {
28162 // v4.9 picked XML as a parse-time "unsupported column
28163 // type" probe. v7.17.0 Phase 1.4 changed the contract:
28164 // an unknown type ident parses as Text + `user_type_ref`
28165 // so CREATE TABLE can resolve user-defined enum / domain
28166 // types — rejection of truly-unknown types moved to the
28167 // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
28168 // to a first-class built-in, so this probe switched to a
28169 // synthetic name nothing in the lexer will ever recognise.
28170 let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
28171 let Statement::CreateTable(t) = stmt else {
28172 panic!("expected CreateTable");
28173 };
28174 assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
28175 }
28176
28177 #[test]
28178 fn create_table_missing_table_keyword_errors() {
28179 assert!(parse_statement("CREATE x (a INT)").is_err());
28180 }
28181
28182 // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
28183 // `PARTITION OF parent <bounds>` child parse + Display round-trip.
28184
28185 #[test]
28186 fn parse_create_table_partition_by_range() {
28187 use crate::ast::{PartitionBySpec, PartitionKindAst};
28188 let stmt = parse_statement(
28189 "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
28190 payload JSONB) PARTITION BY RANGE (ts)",
28191 )
28192 .unwrap();
28193 let Statement::CreateTable(t) = stmt else {
28194 panic!("expected CreateTable");
28195 };
28196 assert!(t.partition_of.is_none(), "parent has no partition_of");
28197 assert_eq!(t.columns.len(), 3);
28198 let by = t.partition_by.as_ref().expect("expected PARTITION BY");
28199 assert_eq!(
28200 by,
28201 &PartitionBySpec {
28202 kind: PartitionKindAst::Range,
28203 key_columns: alloc::vec!["ts".to_string()],
28204 }
28205 );
28206 // Display round-trip preserves the suffix. `quote_ident`
28207 // only adds double quotes when the ident needs escaping, so
28208 // a plain `ts` survives bare here.
28209 assert!(
28210 t.to_string().contains("PARTITION BY RANGE (ts)"),
28211 "Display lost PARTITION BY suffix: {t}"
28212 );
28213 }
28214
28215 #[test]
28216 fn parse_create_table_partition_of_range() {
28217 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
28218 let stmt = parse_statement(
28219 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
28220 FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
28221 )
28222 .unwrap();
28223 let Statement::CreateTable(t) = stmt else {
28224 panic!("expected CreateTable");
28225 };
28226 assert!(t.columns.is_empty(), "child inherits columns from parent");
28227 assert!(t.partition_by.is_none());
28228 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
28229 assert_eq!(of.parent_name, "events_partitioned");
28230 let PartitionOfSpec { bounds, .. } = of.clone();
28231 match bounds {
28232 PartitionOfBoundsAst::Range { lower, upper } => {
28233 assert!(lower.to_string().contains("2026-06-01"));
28234 assert!(upper.to_string().contains("2026-07-01"));
28235 }
28236 other => panic!("expected Range, got {other:?}"),
28237 }
28238 // Display round-trip emits the FOR VALUES tail. `quote_ident`
28239 // skips quotes when not required, so the parent name appears
28240 // bare here.
28241 let s = t.to_string();
28242 assert!(
28243 s.contains("PARTITION OF events_partitioned"),
28244 "Display lost PARTITION OF: {s}"
28245 );
28246 assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
28247 assert!(s.contains(") TO ("), "Display lost TO: {s}");
28248 }
28249
28250 #[test]
28251 fn parse_create_table_partition_of_default() {
28252 use crate::ast::PartitionOfBoundsAst;
28253 let stmt =
28254 parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
28255 .unwrap();
28256 let Statement::CreateTable(t) = stmt else {
28257 panic!("expected CreateTable");
28258 };
28259 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
28260 assert_eq!(of.parent_name, "events_partitioned");
28261 assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
28262 assert!(
28263 t.to_string()
28264 .contains("PARTITION OF events_partitioned DEFAULT"),
28265 "Display lost DEFAULT: {t}"
28266 );
28267 }
28268
28269 #[test]
28270 fn parse_create_table_partition_by_list() {
28271 // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
28272 // child with `FOR VALUES IN (lit, lit, …)`.
28273 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28274 let parent =
28275 parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
28276 .unwrap();
28277 let Statement::CreateTable(t) = parent else {
28278 panic!("expected CreateTable");
28279 };
28280 let Some(PartitionBySpec {
28281 kind,
28282 ref key_columns,
28283 }) = t.partition_by
28284 else {
28285 panic!("expected PARTITION BY");
28286 };
28287 assert_eq!(kind, PartitionKindAst::List);
28288 assert_eq!(*key_columns, vec!["region".to_string()]);
28289 assert!(t.to_string().contains("PARTITION BY LIST (region)"));
28290
28291 let child = parse_statement(
28292 "CREATE TABLE events_apac PARTITION OF events_listed \
28293 FOR VALUES IN ('jp', 'kr', 'tw')",
28294 )
28295 .unwrap();
28296 let Statement::CreateTable(c) = child else {
28297 panic!("expected CreateTable");
28298 };
28299 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28300 let PartitionOfBoundsAst::List { values } = &of.bounds else {
28301 panic!("expected List bounds, got {:?}", of.bounds);
28302 };
28303 assert_eq!(values.len(), 3);
28304 let disp = c.to_string();
28305 assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
28306 }
28307
28308 #[test]
28309 fn parse_create_table_partition_by_hash() {
28310 // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
28311 // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
28312 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28313 let parent =
28314 parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
28315 let Statement::CreateTable(t) = parent else {
28316 panic!("expected CreateTable");
28317 };
28318 let Some(PartitionBySpec {
28319 kind,
28320 ref key_columns,
28321 }) = t.partition_by
28322 else {
28323 panic!("expected PARTITION BY");
28324 };
28325 assert_eq!(kind, PartitionKindAst::Hash);
28326 assert_eq!(*key_columns, vec!["id".to_string()]);
28327 assert!(t.to_string().contains("PARTITION BY HASH (id)"));
28328
28329 let child = parse_statement(
28330 "CREATE TABLE orders_h_0 PARTITION OF orders_h \
28331 FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
28332 )
28333 .unwrap();
28334 let Statement::CreateTable(c) = child else {
28335 panic!("expected CreateTable");
28336 };
28337 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28338 let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
28339 panic!("expected Hash bounds");
28340 };
28341 assert_eq!(modulus, 4);
28342 assert_eq!(remainder, 0);
28343 let disp = c.to_string();
28344 assert!(
28345 disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
28346 "Display lost HASH bounds: {disp}"
28347 );
28348
28349 // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
28350 let bad = parse_statement(
28351 "CREATE TABLE orders_h_bad PARTITION OF orders_h \
28352 FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
28353 );
28354 let msg = format!("{}", bad.unwrap_err());
28355 assert!(
28356 msg.contains("REMAINDER") && msg.contains("MODULUS"),
28357 "expected REMAINDER/MODULUS validation error: {msg}"
28358 );
28359 }
28360
28361 #[test]
28362 fn parse_create_table_partition_of_rejects_columns() {
28363 // v7.37.6-B contract: PARTITION OF children inherit columns
28364 // from the parent; an explicit list MUST surface as a parse
28365 // error rather than getting silently ignored.
28366 let err = parse_statement(
28367 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
28368 FOR VALUES FROM ('a') TO ('b')",
28369 );
28370 assert!(err.is_err(), "expected parse error for explicit columns");
28371 let msg = format!("{}", err.unwrap_err());
28372 assert!(
28373 msg.contains("PARTITION OF") && msg.contains("column"),
28374 "error should mention PARTITION OF + columns: {msg}"
28375 );
28376 }
28377
28378 #[test]
28379 fn insert_single_value() {
28380 let s = parse("INSERT INTO foo VALUES (42)");
28381 let Statement::Insert(i) = s else {
28382 panic!("expected Insert")
28383 };
28384 assert_eq!(i.table, "foo");
28385 assert_eq!(i.rows.len(), 1);
28386 assert_eq!(i.rows[0].len(), 1);
28387 assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
28388 }
28389
28390 #[test]
28391 fn insert_multi_value_with_mixed_literals() {
28392 let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
28393 let Statement::Insert(i) = s else { panic!() };
28394 assert_eq!(i.rows.len(), 1);
28395 assert_eq!(i.rows[0].len(), 5);
28396 }
28397
28398 #[test]
28399 fn insert_missing_into_errors() {
28400 assert!(parse_statement("INSERT foo VALUES (1)").is_err());
28401 }
28402
28403 #[test]
28404 fn create_table_round_trip() {
28405 let original =
28406 parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
28407 let text = original.to_string();
28408 let again = parse_statement(&text).expect("re-parse");
28409 assert_eq!(original, again);
28410 }
28411
28412 #[test]
28413 fn insert_round_trip_with_negation_and_string() {
28414 let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
28415 let text = original.to_string();
28416 let again = parse_statement(&text).expect("re-parse");
28417 assert_eq!(original, again);
28418 }
28419
28420 #[test]
28421 fn unknown_keyword_at_statement_start_errors() {
28422 // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
28423 // the top-level dispatch still has no branch to take.
28424 let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
28425 assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
28426 }
28427
28428 // --- v0.8 CREATE INDEX --------------------------------------------------
28429
28430 #[test]
28431 fn create_index_basic() {
28432 let s = parse("CREATE INDEX idx_id ON users (id)");
28433 let Statement::CreateIndex(c) = s else {
28434 panic!("expected CreateIndex")
28435 };
28436 assert_eq!(c.name, "idx_id");
28437 assert_eq!(c.table, "users");
28438 assert_eq!(c.column, "id");
28439 }
28440
28441 #[test]
28442 fn create_index_missing_on_errors() {
28443 assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
28444 }
28445
28446 #[test]
28447 fn create_index_missing_paren_errors() {
28448 assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
28449 }
28450
28451 #[test]
28452 fn create_index_round_trip() {
28453 let original = parse("CREATE INDEX by_name ON users (name)");
28454 let again = parse_statement(&original.to_string()).unwrap();
28455 assert_eq!(original, again);
28456 }
28457
28458 // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
28459
28460 #[test]
28461 fn create_unique_index_basic() {
28462 let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
28463 let Statement::CreateIndex(c) = s else {
28464 panic!("expected CreateIndex");
28465 };
28466 assert!(c.is_unique);
28467 assert_eq!(c.column, "a");
28468 assert!(c.partial_predicate.is_none());
28469 }
28470
28471 #[test]
28472 fn create_unique_index_partial() {
28473 // mailrs's email_templates "one default per user" shape.
28474 let s = parse(
28475 "CREATE UNIQUE INDEX idx_email_templates_user_default \
28476 ON email_templates (user_address) WHERE is_default = true",
28477 );
28478 let Statement::CreateIndex(c) = s else {
28479 panic!("expected CreateIndex");
28480 };
28481 assert!(c.is_unique);
28482 assert_eq!(c.table, "email_templates");
28483 assert_eq!(c.column, "user_address");
28484 assert!(c.partial_predicate.is_some());
28485 }
28486
28487 #[test]
28488 fn create_unique_index_composite_with_predicate() {
28489 // mailrs's calendar_events instance: composite columns.
28490 let s = parse(
28491 "CREATE UNIQUE INDEX uq_calendar_events_instance \
28492 ON calendar_events (calendar_id, uid, recurrence_id) \
28493 WHERE recurrence_id IS NOT NULL",
28494 );
28495 let Statement::CreateIndex(c) = s else {
28496 panic!("expected CreateIndex");
28497 };
28498 assert!(c.is_unique);
28499 assert_eq!(c.column, "calendar_id");
28500 assert_eq!(
28501 c.extra_columns,
28502 vec!["uid".to_string(), "recurrence_id".to_string()]
28503 );
28504 assert!(c.partial_predicate.is_some());
28505 }
28506
28507 #[test]
28508 fn create_unique_index_using_btree_ok() {
28509 let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
28510 assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
28511 }
28512
28513 #[test]
28514 fn create_unique_index_using_hnsw_rejected() {
28515 let err =
28516 parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
28517 assert!(err.message.contains("UNIQUE"), "{}", err.message);
28518 }
28519
28520 #[test]
28521 fn create_unique_index_round_trip() {
28522 let original = parse(
28523 "CREATE UNIQUE INDEX uq_calendar_events_master \
28524 ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
28525 );
28526 let again = parse_statement(&original.to_string()).unwrap();
28527 assert_eq!(original, again);
28528 }
28529
28530 #[test]
28531 fn create_unique_without_index_errors() {
28532 let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
28533 // v7.39 (round 340, V56) — PG 18.4, verbatim.
28534 assert_eq!(err.message, "syntax error at or near \"TABLE\"");
28535 }
28536
28537 // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
28538
28539 #[test]
28540 fn create_table_bytea_column() {
28541 let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
28542 let Statement::CreateTable(c) = s else {
28543 panic!("expected CreateTable");
28544 };
28545 assert_eq!(c.columns.len(), 2);
28546 assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
28547 assert!(!c.columns[1].nullable);
28548 }
28549
28550 #[test]
28551 fn create_table_bytes_alias_column() {
28552 let s = parse("CREATE TABLE t (blob BYTES)");
28553 let Statement::CreateTable(c) = s else {
28554 panic!("expected CreateTable");
28555 };
28556 assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
28557 }
28558
28559 #[test]
28560 fn bytea_round_trip_display() {
28561 let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
28562 let again = parse_statement(&original.to_string()).unwrap();
28563 assert_eq!(original, again);
28564 }
28565
28566 // --- v0.9 transactions -------------------------------------------------
28567
28568 #[test]
28569 fn begin_commit_rollback_parse_as_unit_variants() {
28570 let plain = crate::ast::TransactionModes::default();
28571 assert_eq!(parse("BEGIN"), Statement::Begin(plain));
28572 assert_eq!(parse("COMMIT"), Statement::Commit);
28573 // r1066 — PG synonyms pgbench's tpcb script relies on.
28574 assert_eq!(parse("END"), Statement::Commit);
28575 assert_eq!(parse("END TRANSACTION"), Statement::Commit);
28576 assert_eq!(parse("COMMIT WORK"), Statement::Commit);
28577 assert_eq!(parse("ROLLBACK"), Statement::Rollback);
28578 // Trailing semicolons accepted too.
28579 assert_eq!(parse("BEGIN;"), Statement::Begin(plain));
28580 // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
28581 // statement (with or without the WORK/TRANSACTION noise word).
28582 assert_eq!(
28583 parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
28584 Statement::Begin(crate::ast::TransactionModes {
28585 isolation: Some(IsolationLevel::RepeatableRead),
28586 read_only: None,
28587 })
28588 );
28589 assert_eq!(
28590 parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
28591 Statement::Begin(crate::ast::TransactionModes {
28592 isolation: Some(IsolationLevel::Serializable),
28593 read_only: None,
28594 })
28595 );
28596 // v7.39 — this line used to read
28597 //
28598 // // A non-isolation mode keeps the session default (None).
28599 // assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
28600 //
28601 // which pinned the defect rather than catching it: the READ ONLY
28602 // was thrown away, so the statement opened an ordinary read-write
28603 // transaction and every write inside it was accepted. The
28604 // isolation level is still absent here, because this statement
28605 // does not name one — that part was right.
28606 assert_eq!(
28607 parse("BEGIN READ ONLY"),
28608 Statement::Begin(crate::ast::TransactionModes {
28609 isolation: None,
28610 read_only: Some(true),
28611 })
28612 );
28613 assert_eq!(
28614 parse("START TRANSACTION READ WRITE"),
28615 Statement::Begin(crate::ast::TransactionModes {
28616 isolation: None,
28617 read_only: Some(false),
28618 })
28619 );
28620 assert_eq!(
28621 parse("BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY"),
28622 Statement::Begin(crate::ast::TransactionModes {
28623 isolation: Some(IsolationLevel::Serializable),
28624 read_only: Some(true),
28625 })
28626 );
28627 }
28628
28629 // --- v1.2: pgvector distance ops + ::vector cast --------------------
28630
28631 #[test]
28632 fn inner_product_binop_parses() {
28633 let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
28634 let Statement::Select(s) = s else { panic!() };
28635 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28636 panic!()
28637 };
28638 assert!(matches!(
28639 expr,
28640 Expr::Binary {
28641 op: BinOp::InnerProduct,
28642 ..
28643 }
28644 ));
28645 }
28646
28647 #[test]
28648 fn cosine_distance_binop_parses() {
28649 let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
28650 let Statement::Select(s) = s else { panic!() };
28651 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28652 panic!()
28653 };
28654 assert!(matches!(
28655 expr,
28656 Expr::Binary {
28657 op: BinOp::CosineDistance,
28658 ..
28659 }
28660 ));
28661 }
28662
28663 #[test]
28664 fn vector_cast_postfix_wraps_string_literal() {
28665 let s = parse("SELECT '[1,2,3]'::vector FROM t");
28666 let Statement::Select(s) = s else { panic!() };
28667 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28668 panic!()
28669 };
28670 assert!(matches!(
28671 expr,
28672 Expr::Cast {
28673 target: CastTarget::Vector,
28674 ..
28675 }
28676 ));
28677 }
28678
28679 #[test]
28680 fn unsupported_cast_target_errors() {
28681 // v7.37.5 ship triage promoted the parser to accept every
28682 // ident as a `CastTarget::Named(canonical)`; the engine
28683 // surfaces the "unsupported cast target" error at eval
28684 // time when `type_name_to_data_type` can't resolve it.
28685 // Parser-side error now requires a NON-ident after `::`
28686 // (e.g. a punctuation token).
28687 let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
28688 assert_eq!(err.message, "syntax error at or near \",\"");
28689 }
28690
28691 #[test]
28692 fn tx_statements_round_trip() {
28693 for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
28694 let original = parse(q);
28695 let again = parse_statement(&original.to_string()).unwrap();
28696 assert_eq!(original, again);
28697 }
28698 }
28699
28700 #[test]
28701 fn interval_text_parsing_units() {
28702 // v7.37.5 β — three-field shape `(months, days, micros)` so
28703 // `'1 day'` and `'24 hours'` no longer collide (PG parity).
28704 // Single unit.
28705 assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
28706 assert_eq!(
28707 parse_interval_text("24 hours"),
28708 Some((0, 0, 86_400_000_000))
28709 );
28710 assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
28711 assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
28712 assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
28713 assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
28714 // Compound spans accumulate per-dimension.
28715 assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
28716 assert_eq!(
28717 parse_interval_text("1 day 2 hours"),
28718 Some((0, 1, 7_200_000_000))
28719 );
28720 // Negative numbers carry through per-dimension.
28721 assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
28722 // Bad shapes return None.
28723 assert_eq!(parse_interval_text(""), None);
28724 assert_eq!(parse_interval_text("garbage"), None);
28725 assert_eq!(parse_interval_text("1 fortnight"), None);
28726 // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
28727 // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
28728 assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
28729 assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
28730 assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
28731 }
28732
28733 #[test]
28734 fn interval_literal_roundtrips_via_display() {
28735 let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
28736 let s = parsed.to_string();
28737 // Display preserves the original text verbatim.
28738 assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
28739 // And re-parsing yields a structurally equal statement.
28740 let again = parse_statement(&s).unwrap();
28741 assert_eq!(parsed, again);
28742 }
28743
28744 // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
28745
28746 #[test]
28747 fn parser_recognises_create_publication_bare() {
28748 let s = parse("CREATE PUBLICATION pub_a");
28749 let Statement::CreatePublication(p) = s else {
28750 panic!("expected CreatePublication, got {s:?}")
28751 };
28752 assert_eq!(p.name, "pub_a");
28753 assert_eq!(p.scope, PublicationScope::AllTables);
28754 }
28755
28756 #[test]
28757 fn parser_recognises_create_publication_for_all_tables() {
28758 let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
28759 let Statement::CreatePublication(p) = s else {
28760 panic!("expected CreatePublication, got {s:?}")
28761 };
28762 assert_eq!(p.name, "pub_a");
28763 assert_eq!(p.scope, PublicationScope::AllTables);
28764 }
28765
28766 #[test]
28767 fn parser_recognises_drop_publication() {
28768 let s = parse("DROP PUBLICATION pub_a");
28769 let Statement::DropPublication { name, .. } = s else {
28770 panic!("expected DropPublication, got {s:?}")
28771 };
28772 assert_eq!(name, "pub_a");
28773 }
28774
28775 #[test]
28776 fn parser_recognises_for_table_list() {
28777 let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
28778 let Statement::CreatePublication(p) = s else {
28779 panic!("expected CreatePublication, got {s:?}")
28780 };
28781 assert_eq!(p.name, "pub_a");
28782 let PublicationScope::ForTables(ts) = p.scope else {
28783 panic!("expected ForTables scope")
28784 };
28785 assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
28786 }
28787
28788 #[test]
28789 fn parser_rejects_bare_for_tables_and_takes_in_schema() {
28790 // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
28791 // is rejected (`invalid publication object list`; the old
28792 // test pinned an unverifiable "PG 19 accepts both" claim);
28793 // TABLES pairs with IN SCHEMA.
28794 let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
28795 .expect_err("bare FOR TABLES must reject");
28796 assert!(
28797 alloc::format!("{err}").contains("invalid publication object list"),
28798 "got: {err}"
28799 );
28800 let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
28801 let Statement::CreatePublication(p) = s else {
28802 panic!("expected CreatePublication, got {s:?}")
28803 };
28804 let PublicationScope::TablesInSchema(schema) = p.scope else {
28805 panic!("expected TablesInSchema")
28806 };
28807 assert_eq!(schema, "public");
28808 }
28809
28810 #[test]
28811 fn parser_recognises_for_all_tables_except_list() {
28812 let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
28813 let Statement::CreatePublication(p) = s else {
28814 panic!()
28815 };
28816 let PublicationScope::AllTablesExcept(ts) = p.scope else {
28817 panic!("expected AllTablesExcept")
28818 };
28819 assert_eq!(ts, alloc::vec!["t1", "t2"]);
28820 }
28821
28822 #[test]
28823 fn parser_rejects_for_table_with_empty_list() {
28824 // `FOR TABLE` with nothing after is a parse error.
28825 let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
28826 .expect_err("must error on empty list");
28827 // No specific message asserted — the call falls through to
28828 // expect_ident_like which yields "expected identifier, got …".
28829 assert!(!err.message.is_empty());
28830 }
28831
28832 #[test]
28833 fn parser_recognises_show_publications() {
28834 // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
28835 // bare ident in this position, NOT a reserved keyword.
28836 let s = parse("SHOW PUBLICATIONS");
28837 assert!(matches!(s, Statement::ShowPublications));
28838 }
28839
28840 // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
28841
28842 #[test]
28843 fn parser_recognises_create_subscription_single_publication() {
28844 let s = parse(
28845 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
28846 );
28847 let Statement::CreateSubscription(c) = s else {
28848 panic!("expected CreateSubscription, got {s:?}")
28849 };
28850 assert_eq!(c.name, "sub_a");
28851 assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
28852 assert_eq!(c.publications, alloc::vec!["pub_a"]);
28853 }
28854
28855 #[test]
28856 fn parser_recognises_create_subscription_multi_publication() {
28857 let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
28858 let Statement::CreateSubscription(c) = s else {
28859 panic!()
28860 };
28861 assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
28862 }
28863
28864 #[test]
28865 fn parser_rejects_create_subscription_missing_connection() {
28866 let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
28867 .expect_err("must error on missing CONNECTION");
28868 assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
28869 }
28870
28871 #[test]
28872 fn parser_rejects_create_subscription_missing_publication() {
28873 let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
28874 .expect_err("must error on missing PUBLICATION");
28875 assert_eq!(err.message, "syntax error at end of input");
28876 }
28877
28878 #[test]
28879 fn parser_recognises_drop_subscription() {
28880 let s = parse("DROP SUBSCRIPTION sub_a");
28881 let Statement::DropSubscription { name, .. } = s else {
28882 panic!("expected DropSubscription, got {s:?}")
28883 };
28884 assert_eq!(name, "sub_a");
28885 }
28886
28887 #[test]
28888 fn parser_recognises_show_subscriptions() {
28889 let s = parse("SHOW SUBSCRIPTIONS");
28890 assert!(matches!(s, Statement::ShowSubscriptions));
28891 }
28892
28893 #[test]
28894 fn parser_recognises_wait_for_wal_position_no_timeout() {
28895 let s = parse("WAIT FOR WAL POSITION 12345");
28896 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28897 panic!("expected WaitForWalPosition, got {s:?}")
28898 };
28899 assert_eq!(pos, 12345);
28900 assert!(timeout_ms.is_none());
28901 }
28902
28903 #[test]
28904 fn parser_recognises_wait_for_wal_position_with_timeout() {
28905 let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
28906 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28907 panic!()
28908 };
28909 assert_eq!(pos, 67890);
28910 assert_eq!(timeout_ms, Some(5000));
28911 }
28912
28913 #[test]
28914 fn parser_rejects_wait_with_negative_position() {
28915 // The lexer treats `-` as a token; `expect_u64_literal`
28916 // only sees the Integer that follows, so the negative
28917 // arrives as a unary-minus expression at higher levels.
28918 // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
28919 // parse error one way or another.
28920 let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
28921 assert!(!err.message.is_empty());
28922 }
28923
28924 #[test]
28925 fn parser_recognises_bare_analyze() {
28926 let s = parse("ANALYZE");
28927 assert!(matches!(s, Statement::Analyze(None)));
28928 }
28929
28930 #[test]
28931 fn parser_recognises_analyze_with_table() {
28932 let s = parse("ANALYZE users");
28933 let Statement::Analyze(Some(name)) = s else {
28934 panic!("expected Analyze, got {s:?}")
28935 };
28936 assert_eq!(name, "users");
28937 }
28938
28939 #[test]
28940 fn parser_recognises_analyze_with_quoted_table() {
28941 let s = parse("ANALYZE \"Mixed Case\"");
28942 let Statement::Analyze(Some(name)) = s else {
28943 panic!()
28944 };
28945 assert_eq!(name, "Mixed Case");
28946 }
28947
28948 #[test]
28949 fn parser_rejects_analyze_with_garbage_token() {
28950 let err = parse_statement("ANALYZE 42").expect_err("must error");
28951 assert!(!err.message.is_empty());
28952 }
28953
28954 #[test]
28955 fn analyze_display_roundtrips() {
28956 for sql in ["ANALYZE", "ANALYZE users"] {
28957 let s = parse(sql);
28958 let printed = s.to_string();
28959 let again = parse_statement(&printed)
28960 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28961 assert_eq!(s, again);
28962 }
28963 }
28964
28965 #[test]
28966 fn wait_for_display_roundtrips() {
28967 for sql in [
28968 "WAIT FOR WAL POSITION 12345",
28969 "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
28970 ] {
28971 let s = parse(sql);
28972 let printed = s.to_string();
28973 let again = parse_statement(&printed)
28974 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28975 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28976 }
28977 }
28978
28979 #[test]
28980 fn subscription_ddl_display_roundtrips() {
28981 for sql in [
28982 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
28983 "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
28984 "DROP SUBSCRIPTION sub_a",
28985 "SHOW SUBSCRIPTIONS",
28986 ] {
28987 let s = parse(sql);
28988 let printed = s.to_string();
28989 let again = parse_statement(&printed)
28990 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28991 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28992 }
28993 }
28994
28995 #[test]
28996 fn parser_drop_dispatches_user_vs_publication() {
28997 // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
28998 // tokenises DROP. Both targets must still parse.
28999 let s = parse("DROP USER 'alice'");
29000 let Statement::DropUser { name, .. } = s else {
29001 panic!("expected DropUser, got {s:?}")
29002 };
29003 assert_eq!(name, "alice");
29004 // And DROP PUBLICATION lands the new variant.
29005 let s = parse("DROP PUBLICATION p1");
29006 assert!(matches!(s, Statement::DropPublication { .. }));
29007 }
29008
29009 #[test]
29010 fn publication_ddl_display_roundtrips() {
29011 // Every CREATE PUBLICATION variant must Display → parse →
29012 // same AST. v6.1.3 covers all three scope shapes.
29013 for sql in [
29014 "CREATE PUBLICATION pub_a",
29015 "CREATE PUBLICATION pub_a FOR ALL TABLES",
29016 "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
29017 "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
29018 "DROP PUBLICATION pub_a",
29019 "SHOW PUBLICATIONS",
29020 ] {
29021 let s = parse(sql);
29022 let printed = s.to_string();
29023 let again = parse_statement(&printed)
29024 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29025 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29026 }
29027 }
29028
29029 // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
29030
29031 #[test]
29032 fn create_function_returns_trigger_plpgsql_minimal() {
29033 let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
29034 let s = parse(sql);
29035 let Statement::CreateFunction(f) = s else {
29036 panic!("expected CreateFunction");
29037 };
29038 assert_eq!(f.name, "noop");
29039 assert!(!f.or_replace);
29040 assert!(f.args.is_empty());
29041 assert!(matches!(f.returns, FunctionReturn::Trigger));
29042 assert_eq!(f.language, "plpgsql");
29043 let FunctionBody::PlPgSql(block) = f.body else {
29044 panic!("expected PlPgSql body");
29045 };
29046 assert_eq!(block.statements.len(), 1);
29047 assert!(matches!(
29048 block.statements[0],
29049 PlPgSqlStmt::Return(ReturnTarget::New)
29050 ));
29051 }
29052
29053 #[test]
29054 fn create_function_or_replace_with_assignment() {
29055 // mailrs-shape trigger function: NEW.col := to_tsvector(...);
29056 // RETURN NEW.
29057 let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
29058BEGIN
29059 NEW.search_vector := to_tsvector('english', NEW.subject);
29060 RETURN NEW;
29061END;
29062$$";
29063 let s = parse(sql);
29064 let Statement::CreateFunction(f) = s else {
29065 panic!("expected CreateFunction");
29066 };
29067 assert!(f.or_replace);
29068 let FunctionBody::PlPgSql(block) = &f.body else {
29069 panic!("expected PlPgSql body");
29070 };
29071 assert_eq!(block.statements.len(), 2);
29072 // First statement: NEW.search_vector := to_tsvector(...)
29073 let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
29074 panic!("expected Assign as first stmt");
29075 };
29076 match target {
29077 AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
29078 other => panic!("expected NEW.col, got {other:?}"),
29079 }
29080 // Second statement: RETURN NEW
29081 assert!(matches!(
29082 block.statements[1],
29083 PlPgSqlStmt::Return(ReturnTarget::New)
29084 ));
29085 }
29086
29087 #[test]
29088 fn create_trigger_after_insert_or_update() {
29089 let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
29090 let s = parse(sql);
29091 let Statement::CreateTrigger(t) = s else {
29092 panic!("expected CreateTrigger");
29093 };
29094 assert_eq!(t.name, "tg");
29095 assert_eq!(t.table, "messages");
29096 assert_eq!(t.timing, TriggerTiming::After);
29097 assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
29098 assert_eq!(t.for_each, TriggerForEach::Row);
29099 assert_eq!(t.function, "update_sv");
29100 }
29101
29102 #[test]
29103 fn create_trigger_before_delete_execute_procedure_alias() {
29104 // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
29105 let sql =
29106 "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
29107 let s = parse(sql);
29108 let Statement::CreateTrigger(t) = s else {
29109 panic!("expected CreateTrigger");
29110 };
29111 assert_eq!(t.timing, TriggerTiming::Before);
29112 assert_eq!(t.events, vec![TriggerEvent::Delete]);
29113 }
29114
29115 #[test]
29116 fn drop_trigger_if_exists_round_trips() {
29117 // No parser support for DROP TRIGGER yet — added in v7.12.5
29118 // alongside the broader DROP …{IF EXISTS} cleanup. The
29119 // AST + Display impls are in place so we round-trip via
29120 // construction:
29121 let s = Statement::DropTrigger {
29122 name: "tg".into(),
29123 table: "messages".into(),
29124 if_exists: true,
29125 };
29126 assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
29127 }
29128
29129 #[test]
29130 fn trigger_ddl_display_roundtrips_through_parser() {
29131 // CREATE TRIGGER + its referenced CREATE FUNCTION must
29132 // Display → parse → same AST (modulo PL/pgSQL body
29133 // formatting which is parser-canonicalised).
29134 for sql in [
29135 "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
29136 "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
29137 ] {
29138 let s = parse(sql);
29139 let printed = s.to_string();
29140 let again = parse_statement(&printed)
29141 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29142 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29143 }
29144 }
29145}