spg_sql/parser.rs
1//! Recursive-descent parser with a Pratt (precedence-climbing) sub-parser for
2//! expressions.
3//!
4//! Precedence (lowest → highest binding):
5//! `OR` (1) `<` `AND` (2) `<` `NOT` unary (3) `<`
6//! comparisons `=` `<>` `<` `<=` `>` `>=` (4) `<`
7//! `+` `-` (5) `<` `*` `/` (6) `<` unary `-` (7) `<` parens / atom.
8//!
9//! This matches PG's behaviour for the operators we support — e.g. `NOT a = b`
10//! parses as `NOT (a = b)` and `-a * b` as `(-a) * b`.
11
12use alloc::boxed::Box;
13use alloc::format;
14use alloc::string::{String, ToString};
15use alloc::vec;
16use alloc::vec::Vec;
17use core::fmt;
18use core::mem;
19
20use crate::ast::{
21 AssignTarget, BinOp, CastTarget, Collation, ColumnDef, ColumnName, ColumnTypeName,
22 CreateFunctionStatement, CreateIndexStatement, CreatePublicationStatement,
23 CreateSubscriptionStatement, CreateTableStatement, CreateTriggerStatement, DiscardTarget, Expr,
24 ExtractField, FkAction, ForeignKeyConstraint, FrameBound, FrameExclusion, FrameKind,
25 FromClause, FromJoin, FunctionArg, FunctionArgMode, FunctionArgType, FunctionAttrs,
26 FunctionBody, FunctionParallel, FunctionReturn, FunctionVolatility, GrantObject, GrantPriv,
27 GrantStatement, IndexMethod, InsertStatement, IsolationLevel, JoinKind, Literal, MysqlIntWidth,
28 NullTreatment, OrderBy, Overriding, PlPgSqlBlock, PlPgSqlDeclare, PlPgSqlStmt,
29 PublicationScope, RaiseLevel, RangeKindAst, ReturnTarget, SelectItem, SelectStatement,
30 Statement, TableRef, TriggerEvent, TriggerForEach, TriggerTiming, UnOp, UnionKind, VecEncoding,
31 WindowFrame,
32};
33use crate::lexer::{self, LexError, Token};
34
35/// v7.38 — a `WINDOW w AS (…)` definition body:
36/// `(PARTITION BY exprs, ORDER BY (expr, desc, nulls_first), frame)`.
37type WindowDef = (
38 Vec<Expr>,
39 Vec<(Expr, bool, Option<bool>)>,
40 Option<WindowFrame>,
41);
42
43/// v7.14.0 — true when the leading keyword of a top-level
44/// statement is one of the dump-emitted DDL forms SPG accepts
45/// as a no-op (no behavioural effect on the single-schema /
46/// single-database model). These statements are consumed up to
47/// the next `;` / EOF and returned as `Statement::Empty`.
48/// v7.39 (read01 round 57) — wrap a parsed GRANT body in the right statement.
49fn finish_grant(grant: bool, g: GrantStatement) -> Statement {
50 if grant {
51 Statement::Grant(g)
52 } else {
53 Statement::Revoke(g)
54 }
55}
56
57fn is_dump_noise_statement(lc: &str) -> bool {
58 matches!(
59 lc,
60 // v7.39 (read01 round 50): "comment" moved OUT — COMMENT ON is now a
61 // real statement with a real store. v7.39 (read01 round 57): "grant" /
62 // "revoke" moved OUT — table privileges are now REAL (stored in
63 // `relacl`, enforced against the session role); a grant on any other
64 // object class still parses and no-ops so dumps restore.
65 // MySQL bulk-load brackets.
66 "unlock"
67 // MySQL OPTIMIZE / ANALYZE TABLE / CHECK TABLE
68 // diagnostics that pg_dump-style tools also emit
69 // post-restore.
70 | "optimize"
71 | "check"
72 | "use"
73 // PG psql backslash meta-commands that newer
74 // pg_dump versions emit unescaped (\restrict /
75 // \unrestrict). Real psql intercepts these; SPG's
76 // PG-wire sees them as raw text.
77 | "\\restrict"
78 | "\\unrestrict"
79 // v7.17.0 Phase 4.1 — MySQL `DELIMITER //` and
80 // `DELIMITER ;` directives. Technically client-side
81 // (the `mysql` CLI uses them to set the statement
82 // terminator), not SQL — but mysqldump and stored-
83 // procedure scripts emit them inline. SPG's parser
84 // sees one statement at a time and doesn't care
85 // about the terminator, so consume DELIMITER lines
86 // as Empty.
87 | "delimiter"
88 // v7.37.17 (17.6 siblings) — additional PG maintenance /
89 // session-state statements pg_dump + application startup
90 // scripts emit. SPG has no matching session-state to
91 // discard (no prepared-plan cache surface, no temp
92 // sequences), no matching security-label / storage-
93 // option to apply, no separate CREATE/DROP CAST that
94 // affects execution.
95 // v7.37.17 (17.6 siblings) — PG role-cleanup statements
96 // pg_dump / pg_dumpall emit around DROP ROLE:
97 // REASSIGN OWNED BY <role> [, ...] TO <newrole>
98 // DROP OWNED BY <role> [, ...] [CASCADE | RESTRICT]
99 // Both operate on the role's owned objects; SPG has no
100 // role-owner model, so accept-and-no-op.
101 // v7.37.17 (17.6 sibling) — LOAD '<library>'. pg_dump
102 // + extension scripts use LOAD to preload shared
103 // libraries. SPG doesn't have a shared-library extension
104 // point today (extensions ship as first-class crates
105 // linked at build time); accept as a no-op.
106 | "load"
107 )
108}
109
110/// v7.37.43-T4 — PG-unreserved keywords that are legal identifiers
111/// per `pg_get_keywords()`. SPG tokenizes these as named variants
112/// so the parser can dispatch on them in their owning contexts
113/// (`RELEASE SAVEPOINT`, `SHOW name`, `BEGIN`/`COMMIT`/`ROLLBACK`,
114/// `CREATE INDEX`, etc.), but they MUST stay usable as table /
115/// column / alias names — that's the PG contract for unreserved
116/// keywords (see PG docs Appendix C.1).
117///
118/// Before this generalisation, sentori migration 0001_init.sql
119/// `release TEXT NOT NULL` blew up the parser with "expected
120/// identifier, got Release", and the same gap stalked every
121/// SPG drop-in user whose schema had a column / alias named
122/// `release` / `index` / `tables` / `show` / `savepoint` /
123/// `begin` / `commit` / `rollback` / `drop` / `insert` / `values`
124/// / `limit` / `partition`. PG accepts all of them as identifiers
125/// when unquoted, so SPG must too.
126///
127/// Returns the canonical lowercase identifier text when the token
128/// belongs to PG's unreserved class, `None` otherwise. Used by
129/// `expect_ident_like` (column / table / alias names) so the
130/// generalisation applies everywhere an identifier may appear,
131/// not just in the contexts these tokens were introduced for.
132fn unreserved_keyword_text(tok: &Token) -> Option<String> {
133 let s = match tok {
134 // PG keyword class: unreserved or col_name.
135 //
136 Token::Release => "release",
137 Token::Savepoint => "savepoint",
138 Token::Show => "show",
139 Token::Index => "index",
140 Token::Begin => "begin",
141 Token::Commit => "commit",
142 Token::Rollback => "rollback",
143 Token::Drop => "drop",
144 Token::Insert => "insert",
145 Token::Values => "values",
146 Token::Limit => "limit",
147 Token::Partition => "partition",
148 Token::Tables => "tables",
149 Token::Connection => "connection",
150 Token::Publication => "publication",
151 Token::Subscription => "subscription",
152 Token::Interval => "interval",
153 // `extract` is non-reserved in PG too (it's a function the
154 // parser dispatches via context — outside that context it's
155 // a plain identifier).
156 Token::Extract => "extract",
157 Token::Offset => "offset",
158 // `to` is reserved in PG (used in many "AS … TO …" forms), so
159 // it is NOT relaxed here. Same for `from`, `where`, `as`,
160 // `select`, `not`, `and`, `or`, `null`, `true`, `false`,
161 // `create`, `table`, `into`, `on`, `order`, `by`, `having`,
162 // `group`, `distinct`, `union`, `all`, `join`, `inner`,
163 // `left`, `cross`, `outer`, `default`, `is`, `between`,
164 // `in`, `like`, `for`, `except`, `desc`, `asc`, `partition`
165 // (partial — keep partition as unreserved per modern PG).
166 _ => return None,
167 };
168 Some(s.to_string())
169}
170
171/// v7.9.22 — recognise pgvector / SPG vector-index opclass names
172/// in CREATE INDEX. SPG's HNSW already routes by query operator;
173/// the opclass is accepted for `pg_dump` compatibility (mailrs
174/// migration follow-up G5).
175/// v7.13.0 — extended to recognise PG built-in / pg_trgm opclasses
176/// (mailrs round-5 G5). These are tokens-only acceptance — SPG
177/// doesn't change index behaviour based on them.
178/// v7.37.17 (17.6 siblings) — the four PG `each` SRFs share one
179/// FROM-clause pipeline; the stored name tells the executor whether
180/// the value column keeps JSON rendering (`jsonb_each` / `json_each`)
181/// or unwraps to text (`*_each_text`).
182fn is_json_each_name(s: &str) -> bool {
183 s.eq_ignore_ascii_case("jsonb_each_text")
184 || s.eq_ignore_ascii_case("jsonb_each")
185 || s.eq_ignore_ascii_case("json_each_text")
186 || s.eq_ignore_ascii_case("json_each")
187}
188
189/// v7.38 (read01, T14) — resolve named function arguments (`argname => value`)
190/// to positional order for the `make_*` family (the AST stays positional).
191/// Positional args fill slots left-to-right; a named arg goes to its registered
192/// slot; unfilled slots default to integer 0 (PG's optional make_interval
193/// fields — the make_date/time arity is still checked at eval time).
194fn reorder_named_args(
195 fname: &str,
196 args: Vec<Expr>,
197 names: &[Option<String>],
198) -> Result<Vec<Expr>, String> {
199 let params: &[&str] = match fname.to_ascii_lowercase().as_str() {
200 "make_date" => &["year", "month", "day"],
201 "make_time" => &["hour", "min", "sec"],
202 "make_timestamp" | "make_timestamptz" => &["year", "month", "mday", "hour", "min", "sec"],
203 "make_interval" => &["years", "months", "weeks", "days", "hours", "mins", "secs"],
204 other => {
205 return Err(alloc::format!(
206 "function {other}(...) does not support named arguments"
207 ));
208 }
209 };
210 let mut slots: Vec<Option<Expr>> = (0..params.len()).map(|_| None).collect();
211 let mut next_positional = 0usize;
212 for (arg, name) in args.into_iter().zip(names.iter()) {
213 let idx = match name {
214 Some(n) => params
215 .iter()
216 .position(|p| p.eq_ignore_ascii_case(n))
217 .ok_or_else(|| alloc::format!("{fname}(...) has no argument named \"{n}\""))?,
218 None => {
219 let i = next_positional;
220 next_positional += 1;
221 i
222 }
223 };
224 if idx >= slots.len() {
225 return Err(alloc::format!("too many arguments for {fname}(...)"));
226 }
227 if slots[idx].is_some() {
228 return Err(alloc::format!(
229 "argument \"{}\" specified more than once",
230 params[idx]
231 ));
232 }
233 slots[idx] = Some(arg);
234 }
235 Ok(slots
236 .into_iter()
237 .map(|s| s.unwrap_or(Expr::Literal(Literal::Integer(0))))
238 .collect())
239}
240
241/// v7.38 (read01) — parse a lexer `Token::Numeric` source string (digits with
242/// an optional single `.`, no sign, no exponent) into `(unscaled, scale)` for
243/// `Literal::Numeric`. Returns `None` if the mantissa overflows i128.
244/// v7.39 (read01 numeric.c) — the result of expanding an `1.5e3`-style
245/// scientific literal into PG's plain NUMERIC decimal form.
246#[derive(Debug)]
247pub enum SciExpanded {
248 /// Plain decimal string ("1.5e3" → "1500", "1e-5" → "0.00001").
249 Expanded(String),
250 /// Exponent pushes the value outside PG's numeric format
251 /// (more than 131072 integer digits or 16383 fractional digits).
252 Overflow,
253 /// Not a `[±]digits[.digits]e[±]digits` literal at all.
254 NotScientific,
255}
256
257/// Expand scientific notation into a plain decimal string by moving the
258/// decimal point — no float round-trip, so the value stays exact. PG treats
259/// such literals as NUMERIC; the digit-count caps mirror PG's numeric format
260/// limits ("value overflows numeric format").
261pub fn expand_scientific_literal(s: &str) -> SciExpanded {
262 let s = s.trim();
263 let Some(epos) = s.find(['e', 'E']) else {
264 return SciExpanded::NotScientific;
265 };
266 let (mant, exp_str) = (&s[..epos], &s[epos + 1..]);
267 let Ok(exp) = exp_str.parse::<i64>() else {
268 return SciExpanded::NotScientific;
269 };
270 let (neg, mant) = match mant.strip_prefix('-') {
271 Some(r) => (true, r),
272 None => (false, mant.strip_prefix('+').unwrap_or(mant)),
273 };
274 let (int_part, frac_part) = match mant.split_once('.') {
275 Some((i, f)) => (i, f),
276 None => (mant, ""),
277 };
278 if (int_part.is_empty() && frac_part.is_empty())
279 || !int_part.bytes().all(|b| b.is_ascii_digit())
280 || !frac_part.bytes().all(|b| b.is_ascii_digit())
281 {
282 return SciExpanded::NotScientific;
283 }
284 let mut digits = String::with_capacity(int_part.len() + frac_part.len());
285 digits.push_str(int_part);
286 digits.push_str(frac_part);
287 // Decimal point position within `digits` after applying the exponent.
288 let new_point = int_part.len() as i64 + exp;
289 // PG's numeric format: up to 131072 digits before the point, 16383 after.
290 if new_point > 131_072 {
291 return SciExpanded::Overflow;
292 }
293 if (digits.len() as i64 - new_point) > 16_383 {
294 return SciExpanded::Overflow;
295 }
296 let sign = if neg { "-" } else { "" };
297 let plain = if new_point <= 0 {
298 let mut out = String::with_capacity(digits.len() + 2 + (-new_point) as usize);
299 out.push_str("0.");
300 for _ in 0..(-new_point) {
301 out.push('0');
302 }
303 out.push_str(&digits);
304 out
305 } else if (new_point as usize) >= digits.len() {
306 let mut out = digits;
307 for _ in 0..(new_point as usize - out.len()) {
308 out.push('0');
309 }
310 out
311 } else {
312 let mut out = String::with_capacity(digits.len() + 1);
313 out.push_str(&digits[..new_point as usize]);
314 out.push('.');
315 out.push_str(&digits[new_point as usize..]);
316 out
317 };
318 SciExpanded::Expanded(alloc::format!("{sign}{plain}"))
319}
320
321/// v7.39 (round 367, M20) — lower a MySQL hexadecimal binary-string
322/// literal (`0x…` / `X'…'`) onto the existing bytea cast. The hex digits
323/// are left-padded to an even count (`0x123` → byte string `01 23`, per
324/// MariaDB) and handed to the PG bytea input format (`\x…`).
325#[inline(never)]
326fn hex_literal_to_bytea_expr(hex: &str) -> Expr {
327 let padded = if hex.len() % 2 == 1 {
328 alloc::format!("0{hex}")
329 } else {
330 hex.to_string()
331 };
332 Expr::Cast {
333 expr: alloc::boxed::Box::new(Expr::Literal(Literal::String(alloc::format!(
334 "\\x{padded}"
335 )))),
336 target: CastTarget::Named("bytea".to_string()),
337 }
338}
339
340/// v7.39 (round 367, M20) — lower a MySQL bit-value literal (`b'1010'`)
341/// onto the bytea cast. The bits are read big-endian and left-padded to a
342/// whole number of bytes (`b'1010'` → one byte `0x0A`, per MariaDB).
343#[inline(never)]
344fn bits_literal_to_bytea_expr(bits: &str) -> Expr {
345 let pad = (8 - bits.len() % 8) % 8;
346 let mut hex = String::with_capacity((bits.len() + pad).div_ceil(4));
347 let padded: String = core::iter::repeat_n('0', pad).chain(bits.chars()).collect();
348 for nibble in padded.as_bytes().chunks(4) {
349 let mut v = 0u8;
350 for &b in nibble {
351 v = (v << 1) | (b - b'0');
352 }
353 hex.push(char::from_digit(u32::from(v), 16).unwrap_or('0'));
354 }
355 hex_literal_to_bytea_expr(&hex)
356}
357
358/// Resolve a lexer `Token::Numeric` into its literal. PG semantics: a dotted
359/// or over-i64 literal is exact NUMERIC; scientific notation is NUMERIC too
360/// (expanded to the plain decimal form); only a fractional depth beyond SPG's
361/// scale width (u8) falls back to double precision (recorded delta).
362/// Kept out of the parse_expr recursion frame — see the call site.
363#[inline(never)]
364fn numeric_token_to_literal(s: String) -> Result<Literal, String> {
365 match parse_decimal_literal(&s) {
366 Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
367 // v7.38 (read01, T3.C3) — a plain decimal too wide for i128 keeps
368 // its exact value as a NumericBig.
369 None if !s.contains(['e', 'E']) => Ok(Literal::NumericBig(s)),
370 // v7.39 (read01 numeric.c) — expand the exponent form.
371 None => match expand_scientific_literal(&s) {
372 SciExpanded::Expanded(plain) => match parse_decimal_literal(&plain) {
373 Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
374 None if plain
375 .split_once('.')
376 .is_none_or(|(_, f)| u8::try_from(f.len()).is_ok()) =>
377 {
378 Ok(Literal::NumericBig(plain))
379 }
380 None => s
381 .parse::<f64>()
382 .map(Literal::Float)
383 .map_err(|_| format!("invalid numeric literal {s:?}")),
384 },
385 SciExpanded::Overflow => Err("value overflows numeric format".to_string()),
386 SciExpanded::NotScientific => s
387 .parse::<f64>()
388 .map(Literal::Float)
389 .map_err(|_| format!("invalid numeric literal {s:?}")),
390 },
391 }
392}
393
394fn parse_decimal_literal(s: &str) -> Option<(i128, u16)> {
395 let (int_part, frac_part) = match s.split_once('.') {
396 Some((i, f)) => (i, f),
397 None => (s, ""),
398 };
399 // v7.39 (round 271) — was u8::MAX. A literal with 256 decimal
400 // places fell out of the numeric path here, which is why
401 // `pg_typeof(1e-256)` answered double precision and a plain
402 // 256-place decimal aborted the query in the big-decimal converter.
403 if frac_part.len() > u16::MAX as usize {
404 return None;
405 }
406 let mut digits = String::with_capacity(int_part.len() + frac_part.len());
407 digits.push_str(int_part);
408 digits.push_str(frac_part);
409 let mantissa: i128 = digits.parse().ok()?;
410 #[allow(clippy::cast_possible_truncation)]
411 Some((mantissa, frac_part.len() as u16))
412}
413
414/// `jsonb_to_record` / `jsonb_to_recordset` (+ `json_` variants) — the
415/// record-returning JSON functions that take a `AS alias(col type, …)`
416/// column-definition list in FROM position.
417fn is_json_to_record_name(s: &str) -> bool {
418 s.eq_ignore_ascii_case("jsonb_to_recordset")
419 || s.eq_ignore_ascii_case("jsonb_to_record")
420 // v7.39 (read01 jsonfuncs.c) — the populate family with an AS
421 // column-definition list desugars identically (the record base
422 // argument only carries the type; a non-NULL base's field
423 // defaults are a recorded delta).
424 || s.eq_ignore_ascii_case("json_populate_record")
425 || s.eq_ignore_ascii_case("jsonb_populate_record")
426 || s.eq_ignore_ascii_case("json_populate_recordset")
427 || s.eq_ignore_ascii_case("jsonb_populate_recordset")
428 || s.eq_ignore_ascii_case("json_to_recordset")
429 || s.eq_ignore_ascii_case("json_to_record")
430}
431
432impl Parser {
433 /// Whether what follows an identifier ends an index key, which is how
434 /// an operator class is told from anything else in that position.
435 fn opclass_position_follows(next: Option<&Token>) -> bool {
436 match next {
437 // `ASC` / `DESC` have their own tokens; matching them as
438 // identifiers named "asc" / "desc" — which the first version of
439 // this did — never fires, and `(c text_pattern_ops DESC)` (which
440 // PG18.4 accepts, verified) went on failing to parse.
441 Some(Token::Comma | Token::RParen | Token::Asc | Token::Desc) => true,
442 Some(Token::Ident(w)) => {
443 w.eq_ignore_ascii_case("nulls") || w.eq_ignore_ascii_case("collate")
444 }
445 _ => false,
446 }
447 }
448}
449
450fn is_vector_opclass_name(name: &str) -> bool {
451 let lc = name.to_ascii_lowercase();
452 matches!(
453 lc.as_str(),
454 "vector_cosine_ops"
455 | "vector_l2_ops"
456 | "vector_ip_ops"
457 | "halfvec_cosine_ops"
458 | "halfvec_l2_ops"
459 | "halfvec_ip_ops"
460 | "sq8_cosine_ops"
461 | "sq8_l2_ops"
462 | "sq8_ip_ops"
463 // pg_trgm — trigram operator class. SPG's GIN index
464 // already uses tsvector tokens; trigram-style LIKE
465 // pattern matching still routes through a sequential
466 // scan, but the opclass name is accepted so PG schemas
467 // load.
468 | "gin_trgm_ops"
469 | "gist_trgm_ops"
470 // PG built-in btree opclasses occasionally appear in
471 // pg_dump output for column types with multiple
472 // sort orders (text_pattern_ops, varchar_pattern_ops,
473 // bpchar_pattern_ops).
474 | "text_pattern_ops"
475 | "varchar_pattern_ops"
476 | "bpchar_pattern_ops"
477 | "int4_ops"
478 | "int8_ops"
479 | "text_ops"
480 )
481}
482
483#[derive(Debug, Clone, PartialEq, Eq)]
484pub struct ParseError {
485 pub message: String,
486 /// Index into the token stream where parsing tripped. Not a byte offset.
487 /// v7.39 (read01 round 95) — the byte/char position is NOT stored here: a
488 /// field would grow every `Result<_, ParseError>` slot on the deeply
489 /// recursive parse stack and tip the nesting-budget frame cliff. PG's
490 /// 1-based char position is recovered on the cold error path by
491 /// [`syntax_error_position`], which re-tokenizes to map this token index.
492 pub token_pos: usize,
493}
494
495impl fmt::Display for ParseError {
496 /// v7.39 (round 322/V24) — the message ALONE. It used to be prefixed
497 /// with `parse error at token #N: `, which PG has no equivalent of:
498 /// the message bodies are already PG's verbatim (`LIMIT must not be
499 /// negative`, `invalid input syntax for type bigint: "abc"`), and the
500 /// prefix was SPG's internal token index leaking into every one of
501 /// them. `token_pos` stays a field — the wire recovers PG's 1-based
502 /// character position from it for the ErrorResponse `P`.
503 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504 f.write_str(&self.message)
505 }
506}
507
508impl From<LexError> for ParseError {
509 fn from(e: LexError) -> Self {
510 Self {
511 message: format!("lex: {e}"),
512 token_pos: 0,
513 }
514 }
515}
516
517/// v7.9.30 — parse a single expression (no trailing junk). Used by
518/// the engine to re-hydrate stored partial-index / unique-index
519/// predicates from their canonical Display form. The same Pratt
520/// parser the statement path uses; this entry point just skips the
521/// statement dispatch.
522pub fn parse_expression(input: &str) -> Result<Expr, ParseError> {
523 let (tokens, offsets) =
524 lexer::tokenize_with_offsets(input, false).map_err(|e| shape_lex_error(&e, input))?;
525 let mut p = Parser::new(tokens);
526 let expr = p
527 .parse_expr(0)
528 .and_then(|e| p.expect_eof().map(|()| e))
529 .map_err(|e| shape_syntax_error(e, input, &offsets))?;
530 Ok(expr)
531}
532
533/// Parse exactly one statement, swallow an optional trailing `;`, and require
534/// the token stream to end there. PG string semantics.
535pub fn parse_statement(input: &str) -> Result<Statement, ParseError> {
536 parse_statement_with(input, false)
537}
538
539/// v7.22 (round-13 T3) — dialect-aware entry: `backslash_escapes`
540/// selects MySQL-style string lexing (see `lexer::tokenize_with`).
541/// The engine threads its session flag through here.
542pub fn parse_statement_with(input: &str, backslash_escapes: bool) -> Result<Statement, ParseError> {
543 let (tokens, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes)
544 .map_err(|e| shape_lex_error(&e, input))?;
545 // The same session flag names the dialect for both the lexer and
546 // the type mapping.
547 let mut p = Parser::new_with_dialect(tokens, backslash_escapes).with_source(input, &offsets);
548 let stmt = (|| {
549 let stmt = p.parse_one_statement()?;
550 if matches!(p.peek(), Token::Semicolon) {
551 p.advance();
552 }
553 p.expect_eof()?;
554 Ok(stmt)
555 })()
556 .map_err(|e: ParseError| shape_syntax_error(e, input, &offsets))?;
557 Ok(stmt)
558}
559
560/// v7.39 (round 340, V56) — PG has exactly two syntax-error wordings:
561/// `syntax error at or near "<token>"` and `syntax error at end of input`
562/// (measured on 18.4 across a dozen shapes). SPG wrote its own per-site
563/// prose — `expected identifier, got Eof`, `unexpected token From in
564/// expression`, `expected end of input, got Ident("with")` — which named
565/// internal token types and, in the Debug forms, leaked the parser's own
566/// enum into a message clients read.
567///
568/// Applied once on the way out, so every construction site is covered and
569/// the token named is the one the error itself points at. Messages whose
570/// bodies are already PG's verbatim (`LIMIT must not be negative`,
571/// `invalid input syntax for type bigint: "abc"`) are left alone — those
572/// are PG's own errors, not its syntax error.
573fn shape_syntax_error(e: ParseError, input: &str, offsets: &[usize]) -> ParseError {
574 if !(e.message.starts_with("expected ") || e.message.starts_with("unexpected token ")) {
575 return e;
576 }
577 let message = match offending_lexeme(input, offsets, e.token_pos) {
578 Some(tok) => alloc::format!("syntax error at or near \"{tok}\""),
579 None => "syntax error at end of input".into(),
580 };
581 ParseError {
582 message,
583 token_pos: e.token_pos,
584 }
585}
586
587/// v7.39 (round 340, V56) — a lexer-level failure the way PG words it.
588/// Measured on 18.4: `unterminated quoted string at or near "'abc"`,
589/// `unterminated quoted identifier at or near ""abc"`, `unterminated /*
590/// comment at or near "/* x"` — the quoted part runs from the opening
591/// delimiter to the end of the input. SPG reported its own internal
592/// shape instead (`unterminated string literal at byte 7`), which named
593/// a byte offset no client can use.
594fn shape_lex_error(e: &lexer::LexError, input: &str) -> ParseError {
595 use lexer::LexErrorKind as K;
596 let from_here = input.get(e.pos..).map(str::trim_end).unwrap_or("");
597 let message = match &e.kind {
598 K::UnterminatedString => {
599 alloc::format!("unterminated quoted string at or near \"{from_here}\"")
600 }
601 K::UnterminatedQuotedIdent => {
602 alloc::format!("unterminated quoted identifier at or near \"{from_here}\"")
603 }
604 K::UnterminatedBlockComment => {
605 alloc::format!("unterminated /* comment at or near \"{from_here}\"")
606 }
607 // PG has no "unknown character" error of its own — the character
608 // is skipped and the parser reports the next token. SPG stops at
609 // the character itself and names it, which is the same shape.
610 K::UnknownChar(c) => alloc::format!("syntax error at or near \"{c}\""),
611 // The number-literal kinds already carry PG's `at or near` form.
612 other => alloc::format!(
613 "{}",
614 lexer::LexError {
615 kind: other.clone(),
616 pos: e.pos,
617 }
618 ),
619 };
620 ParseError {
621 message,
622 token_pos: 0,
623 }
624}
625
626/// The offending token exactly as it appears in the input, or `None` at
627/// end of input. PG echoes the source spelling — a lower-case `frm`
628/// reports as `frm`, not as a canonicalised keyword.
629fn offending_lexeme<'a>(input: &'a str, offsets: &[usize], token_pos: usize) -> Option<&'a str> {
630 let start = *offsets.get(token_pos)?;
631 if start >= input.len() {
632 return None;
633 }
634 let end = offsets
635 .get(token_pos + 1)
636 .copied()
637 .unwrap_or(input.len())
638 .min(input.len());
639 let seg = input.get(start..end)?.trim();
640 if seg.is_empty() {
641 return None;
642 }
643 // A quoted literal / identifier keeps its inner spaces; anything else
644 // ends at the first whitespace (the segment runs to the NEXT token's
645 // start, which may swallow a comment).
646 if seg.starts_with('\'') || seg.starts_with('"') || seg.starts_with('`') {
647 Some(seg)
648 } else {
649 seg.split_whitespace().next()
650 }
651}
652
653/// v7.39 (read01 round 95) — recover PG's 1-based CHARACTER error position for
654/// a [`ParseError::token_pos`]. Kept off the `ParseError` struct (and so off
655/// every recursive `Result` slot) to protect the nesting-budget frame cliff:
656/// this re-tokenizes `input` on the cold error path to map the failing token
657/// index to its start byte, then to a character offset. `backslash_escapes`
658/// must match the parse that produced `token_pos` (it barely shifts offsets,
659/// but stay consistent). Returns `None` when the index has no offset or the
660/// byte isn't a char boundary. The wire attaches it as the ErrorResponse `P`.
661#[must_use]
662pub fn syntax_error_position(
663 input: &str,
664 backslash_escapes: bool,
665 token_pos: usize,
666) -> Option<usize> {
667 let (_, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes).ok()?;
668 let byte_off = *offsets.get(token_pos)?;
669 if byte_off > input.len() || !input.is_char_boundary(byte_off) {
670 return None;
671 }
672 Some(input[..byte_off].chars().count() + 1)
673}
674
675struct Parser {
676 tokens: Vec<Token>,
677 pos: usize,
678 /// v7.39 (round 274) — the session's dialect, carried by the same
679 /// signal that drives string-literal escaping: `SET sql_mode` (only
680 /// MySQL clients and mysqldump preambles emit it) turns it on,
681 /// `SET standard_conforming_strings` (every pg_dump preamble) turns
682 /// it off. Needed here because the two dialects disagree about what
683 /// `REAL` means — see the type mapping below.
684 mysql_dialect: bool,
685 /// v7.30.2 (mailrs round-25 ask 2) — live nesting depth of the
686 /// mutually recursive expr/select parsers. Bounded so a deeply
687 /// nested input returns a parse error instead of overflowing
688 /// the stack (embed hosts die on overflow — it is an abort,
689 /// not a catchable error).
690 nest_depth: usize,
691 /// TABLESAMPLE lowering channel: the table-ref parser pushes a
692 /// `random() < p/100` predicate here; the enclosing SELECT
693 /// drains the list after its WHERE parses and ANDs the
694 /// predicates in. parse_bare_select save/restores around its
695 /// FROM+WHERE so nested selects only drain their own.
696 pending_sample_preds: Vec<Expr>,
697 /// v7.39 (round 691) — collation lowering channel, the same shape as
698 /// `pending_sample_preds` above. `expr COLLATE "name"` is ORDERING
699 /// information, and `ast::OrderBy` is where this parser keeps ordering
700 /// information (`desc`, `nulls_first`); the alternative — a new `Expr`
701 /// variant — puts a new arm on `eval_expr`, which this repo has
702 /// measured to overflow the debug stack. So while an ORDER BY KEY is
703 /// being parsed the postfix loop drops the name here instead of
704 /// refusing it, and the key's parser takes it.
705 ///
706 /// Only inside an ORDER BY key: everywhere else an unperformable
707 /// collation still errors, because accepting one at a COMPARISON and
708 /// ignoring it is the defect F36 exists to close.
709 in_order_by_key: bool,
710 order_key_collation: Option<String>,
711 /// POSITION(sub IN str) — while parsing the needle, the IN
712 /// keyword is the argument separator, not a membership test.
713 /// The postfix loop leaves IN unconsumed when this is set.
714 suppress_in_tail: bool,
715 /// Index of the token the last `advance()` returned — see
716 /// [`Parser::consumed_pos`].
717 last_consumed: usize,
718 /// v7.39 (round 506) — the statement's own text and the byte each token
719 /// starts at, so a MySQL projection item can report the SOURCE TEXT
720 /// MariaDB reports: `SELECT a + b` names its column `a + b`,
721 /// spacing and all. Only filled for a MySQL session — a PG one names
722 /// columns from the parsed shape and pays nothing for this.
723 src: Option<(String, Vec<usize>)>,
724}
725
726/// Max expr/select parser nesting (parens, subqueries, CASE, …).
727/// Real SQL nests a few dozen levels at the extreme. Each nesting level
728/// costs a parse_expr→parse_unary→parse_atom frame chain, so the budget
729/// exists to turn a deep statement into a catchable parse ERROR: a stack
730/// overflow is an abort, and in the server it does not fail one query, it
731/// takes the process down and every other connection with it.
732///
733/// v7.39 (round 507) — measured, because the figure here used to be a
734/// guess ("over 10 KiB in debug … comfortably inside a 2 MiB worker stack
735/// in BOTH debug and release"), and the debug half of that is wrong by
736/// more than an order of magnitude:
737///
738/// * RELEASE, on a 2 MiB worker stack: every recursive shape reaches
739/// this budget and errors. Verified against a live server for nested
740/// derived tables, parens, calls, CASE, IN-subqueries, scalar
741/// subqueries, NOT and unary minus — the server stayed up through all
742/// of them. This is the contract that matters, and it holds.
743/// * DEBUG: nested derived tables cost roughly 235 KiB of stack PER
744/// LEVEL, so parsing alone aborts around 35 levels on an 8 MiB stack
745/// and executing aborts around 8 inside a test thread. The budget is
746/// simply unreachable there, which is why a deep-nesting test has to
747/// ask for a large stack of its own — see `nesting_budget_errors_at`
748/// in the parser tests.
749/// v7.39 (round 541) — the pg_catalog relations SPG synthesises, in
750/// one place.
751///
752/// There were two copies of this fact: a curated list, used for BARE
753/// names, and — in `try_peek_meta_qualified` — no list at all, which
754/// rewrote `pg_catalog.<anything>` to `__spg_pg_<anything>` and left
755/// the engine to complain about a view it could not materialise. So
756/// writing the schema qualifier CHANGED THE ANSWER: `pg_stat_activity`
757/// had rows, `pg_catalog.pg_stat_activity` was an error.
758///
759/// PG puts `pg_catalog` at the implicit front of every search_path, so
760/// the two spellings name the same relation and must resolve the same
761/// way. Names NOT here (`pg_stat_activity`, `pg_locks`,
762/// `pg_stat_statements`, `pg_statio_user_tables`) route through the
763/// meta_view_result path under their own names and must not be
764/// rewritten; a name that is neither reaches the ordinary resolver,
765/// which reports that the relation does not exist — PG's answer.
766const SYNTHESISED_PG_CATALOGS: &[&str] = &[
767 "pg_am",
768 "pg_attrdef",
769 "pg_attribute",
770 "pg_cast",
771 "pg_db_role_setting",
772 "pg_conversion",
773 "pg_default_acl",
774 "pg_shadow",
775 "pg_sequences",
776 "pg_range",
777 "pg_partitioned_table",
778 "pg_language",
779 "pg_group",
780 "pg_authid",
781 "pg_class",
782 "pg_collation",
783 "pg_constraint",
784 "pg_database",
785 "pg_depend",
786 // v7.39 (read01 round 50) — COMMENT ON store, PG's pg_description.
787 "pg_description",
788 "pg_enum",
789 "pg_extension",
790 // v7.39 (round 541) — pg_dump reads it for every relation of kind
791 // 'f'. SPG has no foreign tables, so it is empty, which is also
792 // what PG reports on a database that has none.
793 "pg_foreign_table",
794 // v7.39 (round 541) — the empty-by-truth family; see
795 // EMPTY_PG_CATALOGS in spg-engine::system_catalog.
796 "pg_event_trigger",
797 "pg_file_settings",
798 "pg_foreign_data_wrapper",
799 "pg_foreign_server",
800 "pg_hba_file_rules",
801 "pg_ident_file_mappings",
802 "pg_init_privs",
803 "pg_parameter_acl",
804 "pg_prepared_xacts",
805 "pg_publication_namespace",
806 "pg_publication_rel",
807 "pg_publication_tables",
808 "pg_replication_origin",
809 "pg_replication_origin_status",
810 "pg_seclabel",
811 "pg_seclabels",
812 "pg_shdepend",
813 "pg_shdescription",
814 "pg_shmem_allocations",
815 "pg_shmem_allocations_numa",
816 "pg_shseclabel",
817 "pg_statistic_ext_data",
818 "pg_stats_ext",
819 "pg_stats_ext_exprs",
820 "pg_subscription_rel",
821 "pg_transform",
822 "pg_user_mapping",
823 "pg_user_mappings",
824 "pg_index",
825 "pg_indexes",
826 "pg_inherits",
827 // v7.39 (round 650) — the text-search catalogs SPG can fill
828 // honestly. `pg_ts_config_map` is deliberately NOT here: it maps
829 // token types to dictionaries and SPG has no token-type model,
830 // the same gap that leaves `ts_token_type` / `ts_debug` unbuilt.
831 "pg_ts_config",
832 "pg_ts_config_map",
833 "pg_ts_dict",
834 "pg_ts_parser",
835 "pg_ts_template",
836 "pg_matviews",
837 "pg_namespace",
838 // v7.39 (round 621)
839 "pg_operator",
840 "pg_policies",
841 "pg_policy",
842 "pg_proc",
843 "pg_publication",
844 "pg_replication_slots",
845 "pg_roles",
846 // v7.39 (round 143) — the rewrite-rule listing view.
847 // v7.39 (round 312) — and the rule catalogue itself, which
848 // `pg_get_ruledef(oid)` resolves against.
849 "pg_rewrite",
850 "pg_rules",
851 "pg_sequence",
852 "pg_settings",
853 "pg_stat_archiver",
854 "pg_stat_bgwriter",
855 "pg_stat_checkpointer",
856 "pg_stat_database",
857 "pg_stat_io",
858 "pg_stat_progress_analyze",
859 "pg_auth_members",
860 "pg_stat_progress_create_index",
861 "pg_stat_progress_vacuum",
862 "pg_stat_replication",
863 "pg_stat_slru",
864 "pg_stat_subscription_stats",
865 "pg_stat_user_functions",
866 "pg_stat_user_indexes",
867 "pg_stat_user_tables",
868 "pg_stat_wal",
869 "pg_prepared_statements",
870 "pg_largeobject",
871 "pg_largeobject_metadata",
872 "pg_statistic",
873 "pg_statistic_ext",
874 "pg_subscription",
875 "pg_tables",
876 "pg_tablespace",
877 // v7.39 (round 502) — the timezone catalogues. SPG resolved
878 // named zones correctly but could not list them, so a client
879 // populating a timezone picker got "relation does not exist".
880 "pg_timezone_abbrevs",
881 "pg_timezone_names",
882 "pg_trigger",
883 "pg_type",
884 "pg_user",
885 "pg_views",
886];
887
888const MAX_NEST_DEPTH: usize = 64;
889
890/// Stack accounting for the nesting budget, test-only.
891///
892/// `MAX_NEST_DEPTH` is a fixed count calibrated against a frame size
893/// that MOVES: a compiler upgrade grew the parser's debug frames and
894/// silently ate the margin until `nesting_budget_errors_cleanly` went
895/// from erroring cleanly to aborting on a stack overflow. A count
896/// cannot notice that on its own, so the budget is measured here and
897/// held to a ceiling.
898///
899/// The reading has to come from a helper whose OWN frame is the same at
900/// every call: debug slot placement does not follow source order, so a
901/// local's address inside the function under test is not that
902/// function's frame boundary. Two earlier probes were wrong that way —
903/// one read `&self.nest_depth`, which is the `Parser`'s address and
904/// never moves at all.
905#[cfg(test)]
906mod frame_meter {
907 extern crate std;
908 use std::cell::Cell;
909
910 // Per-THREAD, not global. `cargo test` runs tests in parallel and
911 // plenty of them parse nested expressions, so shared statics get
912 // stack addresses from several threads at once and the subtraction
913 // below turns into noise — it read 229,772 bytes per level that way,
914 // while passing when the test was run on its own.
915 std::thread_local! {
916 static AT_LO: Cell<usize> = const { Cell::new(0) };
917 static AT_HI: Cell<usize> = const { Cell::new(0) };
918 }
919
920 pub(super) const SAMPLE_LO: usize = 4;
921 pub(super) const SAMPLE_HI: usize = 24;
922
923 #[inline(never)]
924 pub(super) fn record(depth: usize) {
925 let anchor = 0u8;
926 let at = core::ptr::from_ref(&anchor) as usize;
927 if depth == SAMPLE_LO {
928 AT_LO.with(|c| c.set(at));
929 } else if depth == SAMPLE_HI {
930 AT_HI.with(|c| c.set(at));
931 }
932 }
933
934 /// Bytes of stack one nesting level costs, averaged over the span.
935 pub(super) fn bytes_per_level() -> usize {
936 let lo = AT_LO.with(Cell::get);
937 let hi = AT_HI.with(Cell::get);
938 assert!(lo > 0 && hi > 0, "meter never sampled: lo={lo} hi={hi}");
939 assert!(lo > hi, "stack grew upwards? lo={lo} hi={hi}");
940 (lo - hi) / (SAMPLE_HI - SAMPLE_LO)
941 }
942
943 pub(super) fn reset() {
944 AT_LO.with(|c| c.set(0));
945 AT_HI.with(|c| c.set(0));
946 }
947}
948
949/// v7.39 (read01 geo_ops.c) — prefix `@@` desugar target, out-of-line so
950/// the constructor's temporaries stay off `parse_unary`'s recursion frame.
951#[inline(never)]
952fn build_center_call(e: Expr) -> Expr {
953 Expr::FunctionCall {
954 name: alloc::string::String::from("center"),
955 args: alloc::vec![e],
956 }
957}
958
959/// Max consecutive binary operators at ONE precedence level
960/// (`a OR b OR c …`, `1+1+1…`). The chain builds iteratively at
961/// parse time but evaluates and drops recursively — depth beyond
962/// this overflows 2 MiB worker stacks (debug eval frames run
963/// multiple KiB). `IN (…)` lists are flat and unaffected.
964const MAX_BINARY_CHAIN: usize = 256;
965
966/// v7.22 (round-13 gap 5) — the kind keyword after `CONSTRAINT
967/// <name>` in a CREATE TABLE column list. FOREIGN KEY is not here:
968/// it keeps its dedicated path (`parse_table_level_fk`).
969enum NamedTableConstraintKind {
970 Check,
971 Unique,
972 PrimaryKey,
973 Exclude,
974}
975
976impl Parser {
977 fn new(tokens: Vec<Token>) -> Self {
978 Self::new_with_dialect(tokens, false)
979 }
980
981 fn new_with_dialect(tokens: Vec<Token>, mysql_dialect: bool) -> Self {
982 Self {
983 tokens,
984 mysql_dialect,
985 in_order_by_key: false,
986 order_key_collation: None,
987 pos: 0,
988 nest_depth: 0,
989 pending_sample_preds: Vec::new(),
990 suppress_in_tail: false,
991 last_consumed: 0,
992 src: None,
993 }
994 }
995
996 /// Hand the parser the text it is parsing, for [`Parser::source_span`].
997 fn with_source(mut self, input: &str, offsets: &[usize]) -> Self {
998 if self.mysql_dialect {
999 self.src = Some((input.to_string(), offsets.to_vec()));
1000 }
1001 self
1002 }
1003
1004 /// The source text spanning tokens `start ..= end`, trimmed.
1005 ///
1006 /// The offsets are token STARTS, so the span runs to the start of the
1007 /// token after `end` and gives back the whitespace between them —
1008 /// trimming is what makes `a + b FROM t` end at `b`.
1009 fn source_span(&self, start: usize, end: usize) -> Option<&str> {
1010 let (text, offsets) = self.src.as_ref()?;
1011 let from = *offsets.get(start)?;
1012 let to = *offsets.get(end + 1)?;
1013 text.get(from..to).map(str::trim_end)
1014 }
1015
1016 /// v7.30.2 (mailrs round-25 ask 2) — bump the expr/select
1017 /// nesting depth, erroring out cleanly past the budget.
1018 fn enter_nested(&mut self) -> Result<(), ParseError> {
1019 self.nest_depth += 1;
1020 #[cfg(test)]
1021 frame_meter::record(self.nest_depth);
1022 if self.nest_depth > MAX_NEST_DEPTH {
1023 self.nest_depth -= 1;
1024 return Err(self.err(alloc::format!(
1025 "statement nests deeper than {MAX_NEST_DEPTH} levels"
1026 )));
1027 }
1028 Ok(())
1029 }
1030
1031 fn peek(&self) -> &Token {
1032 // tokens always ends with Eof; pos is clamped in advance().
1033 &self.tokens[self.pos]
1034 }
1035
1036 fn advance(&mut self) -> Token {
1037 let t = mem::replace(&mut self.tokens[self.pos], Token::Eof);
1038 self.last_consumed = self.pos;
1039 if self.pos + 1 < self.tokens.len() {
1040 self.pos += 1;
1041 }
1042 t
1043 }
1044
1045 /// v7.39 (round 340, V56) — the index of the token `advance()` just
1046 /// returned. It was computed as `pos - 1`, which is wrong at both
1047 /// ends: `advance()` parks on the final Eof rather than running off
1048 /// the end (so `SELECT * FROM` named `FROM` where PG says `at end of
1049 /// input`), and after backtracking `pos` is no longer one past the
1050 /// token that failed. Recorded by `advance()` itself instead.
1051 const fn consumed_pos(&self) -> usize {
1052 self.last_consumed
1053 }
1054
1055 fn err(&self, message: String) -> ParseError {
1056 ParseError {
1057 message,
1058 token_pos: self.pos,
1059 }
1060 }
1061
1062 fn expect_eof(&self) -> Result<(), ParseError> {
1063 if matches!(self.peek(), Token::Eof) {
1064 Ok(())
1065 } else {
1066 Err(self.err(format!("expected end of input, got {:?}", self.peek())))
1067 }
1068 }
1069
1070 /// v7.14.0 — swallow every token up to (but not including) the
1071 /// next semicolon / EOF. Used by the dump-noise dispatcher
1072 /// to consume `COMMENT ON …`, `GRANT …`, `LOCK TABLES …`,
1073 /// etc. without modeling each grammar.
1074 /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
1075 /// Kinds SPG stores by name: TABLE / COLUMN / INDEX / VIEW / SEQUENCE /
1076 /// SCHEMA / TYPE / DATABASE / FUNCTION. Anything else (and the multi-word
1077 /// `CONSTRAINT c ON t` / `MATERIALIZED VIEW` forms) keeps the old
1078 /// swallow-as-no-op behaviour so a pg_dump tail still loads.
1079 fn parse_comment_on(&mut self) -> Result<Statement, ParseError> {
1080 let start = self.pos;
1081 self.advance(); // COMMENT
1082 if !matches!(self.peek(), Token::On) {
1083 self.pos = start;
1084 self.consume_until_statement_boundary();
1085 return Ok(Statement::Empty);
1086 }
1087 self.advance(); // ON
1088 let kind = match self.peek() {
1089 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
1090 Token::Table => "table".into(),
1091 _ => {
1092 self.consume_until_statement_boundary();
1093 return Ok(Statement::Empty);
1094 }
1095 };
1096 if !matches!(
1097 kind.as_str(),
1098 "table"
1099 | "column"
1100 | "index"
1101 | "view"
1102 | "sequence"
1103 | "schema"
1104 | "type"
1105 | "database"
1106 | "function"
1107 ) {
1108 self.consume_until_statement_boundary();
1109 return Ok(Statement::Empty);
1110 }
1111 self.advance(); // the kind keyword
1112 // The object name. ⚠️ `expect_ident_like` strips a leading
1113 // `<schema>.` qualifier and returns only the trailing ident (SPG is
1114 // single-schema), which would turn `COMMENT ON COLUMN t.c` into just
1115 // `c`. Read the dotted parts from raw tokens instead, then let a
1116 // 3-part `schema.t.c` drop its leading schema like everywhere else.
1117 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
1118 loop {
1119 match self.advance() {
1120 Token::Ident(s) | Token::QuotedIdent(s) => parts.push(s),
1121 other if unreserved_keyword_text(&other).is_some() => {
1122 parts.push(unreserved_keyword_text(&other).unwrap());
1123 }
1124 other => {
1125 return Err(ParseError {
1126 message: alloc::format!("expected identifier, got {other:?}"),
1127 token_pos: self.consumed_pos(),
1128 });
1129 }
1130 }
1131 if matches!(self.peek(), Token::Dot) {
1132 self.advance();
1133 } else {
1134 break;
1135 }
1136 }
1137 // COLUMN wants `table.column`; every other kind wants a bare name.
1138 let want = if kind == "column" { 2 } else { 1 };
1139 while parts.len() > want {
1140 parts.remove(0);
1141 }
1142 let name = parts.join(".");
1143 // v7.39 (round 710) — `COMMENT ON FUNCTION f(int, text) IS …`.
1144 // pg_dump writes the SIGNATURE, and the paren list was a syntax
1145 // error here — a dump carrying one function comment failed to
1146 // restore. The list is consumed (the comment store keys by name;
1147 // overload-precise comments are the function-predicate follow-up).
1148 if matches!(self.peek(), Token::LParen)
1149 && matches!(
1150 kind.as_str(),
1151 "function" | "procedure" | "aggregate" | "routine"
1152 )
1153 {
1154 let mut depth = 0usize;
1155 loop {
1156 match self.advance() {
1157 Token::LParen => depth += 1,
1158 Token::RParen => {
1159 depth -= 1;
1160 if depth == 0 {
1161 break;
1162 }
1163 }
1164 Token::Eof => {
1165 return Err(self.err(alloc::string::String::from(
1166 "unterminated argument list in COMMENT ON",
1167 )));
1168 }
1169 _ => {}
1170 }
1171 }
1172 }
1173 // `IS`
1174 if !matches!(self.peek(), Token::Is) {
1175 self.expect_keyword_ident("is")?;
1176 } else {
1177 self.advance();
1178 }
1179 let comment = match self.peek() {
1180 Token::Null => {
1181 self.advance();
1182 None
1183 }
1184 _ => Some(self.expect_string_literal()?),
1185 };
1186 Ok(Statement::CommentOn {
1187 kind,
1188 name,
1189 comment,
1190 })
1191 }
1192
1193 /// v7.39 (read01 round 57) — `GRANT <privs> ON <obj> TO <roles> [WITH GRANT
1194 /// OPTION]` / `REVOKE [GRANT OPTION FOR] <privs> ON <obj> FROM <roles>
1195 /// [CASCADE|RESTRICT]`.
1196 ///
1197 /// TABLE privileges are the real ones (stored, enforced, introspectable).
1198 /// Every other object class — SCHEMA / SEQUENCE / DATABASE / FUNCTION / …,
1199 /// and the no-ON `GRANT role TO role` membership form — parses into
1200 /// `GrantObject::Other` and no-ops in the engine, so a pg_dump that grants
1201 /// on them still restores.
1202 fn parse_grant_or_revoke(&mut self, grant: bool) -> Result<Statement, ParseError> {
1203 self.advance(); // GRANT / REVOKE
1204 // REVOKE's optional `GRANT OPTION FOR` prefix.
1205 let mut grant_option = false;
1206 if !grant
1207 && self.peek_keyword_ident("grant")
1208 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("option"))
1209 {
1210 self.advance(); // GRANT
1211 self.advance(); // OPTION
1212 self.expect_keyword_ident("for")?;
1213 grant_option = true;
1214 }
1215 // The privilege list: `ALL [PRIVILEGES] [(cols)]`, or comma-separated
1216 // words each with an optional COLUMN list.
1217 let mut privileges: Vec<GrantPriv> = Vec::new();
1218 if matches!(self.peek(), Token::All) {
1219 self.advance();
1220 if self.peek_keyword_ident("privileges") {
1221 self.advance();
1222 }
1223 // `GRANT ALL (col) ON t TO r` — every column privilege, on that
1224 // column only.
1225 if matches!(self.peek(), Token::LParen) {
1226 let columns = self.parse_grant_column_list()?;
1227 privileges.push(GrantPriv {
1228 word: "ALL".into(),
1229 columns,
1230 });
1231 }
1232 } else {
1233 loop {
1234 // SELECT and INSERT lex as reserved tokens, so they never
1235 // reach `expect_ident_like` as plain idents; the rest
1236 // (UPDATE / DELETE / TRUNCATE / REFERENCES / TRIGGER /
1237 // MAINTAIN) are ordinary identifiers.
1238 let w = match self.peek() {
1239 Token::Select => {
1240 self.advance();
1241 "SELECT".to_string()
1242 }
1243 Token::Insert => {
1244 self.advance();
1245 "INSERT".to_string()
1246 }
1247 // v7.39 (read01 round 60) — CREATE is a privilege word on a
1248 // schema / database, and it lexes as a reserved token.
1249 Token::Create => {
1250 self.advance();
1251 "CREATE".to_string()
1252 }
1253 // NOT upper-cased: in the no-ON shape (`GRANT devs TO
1254 // alice`) these "privilege words" are ROLE NAMES, and a
1255 // role name is case-sensitive. `priv_from_word` folds case
1256 // itself when they really are privileges.
1257 _ => self.expect_ident_like()?,
1258 };
1259 // v7.39 (read01 round 59) — the optional per-privilege COLUMN
1260 // list: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
1261 let columns = if matches!(self.peek(), Token::LParen) {
1262 self.parse_grant_column_list()?
1263 } else {
1264 Vec::new()
1265 };
1266 privileges.push(GrantPriv { word: w, columns });
1267 if matches!(self.peek(), Token::Comma) {
1268 self.advance();
1269 } else {
1270 break;
1271 }
1272 }
1273 }
1274 // v7.39 (read01 round 58) — no ON clause at all = `GRANT devs TO alice`:
1275 // role MEMBERSHIP. The words parsed as "privileges" are the role names.
1276 if !matches!(self.peek(), Token::On) {
1277 let roles: Vec<String> = core::mem::take(&mut privileges)
1278 .into_iter()
1279 .map(|p| p.word)
1280 .collect();
1281 let grantees = self.parse_grantee_list(grant)?;
1282 // `WITH ADMIN OPTION` / `GRANTED BY x` — accepted, ignored (SPG has
1283 // no admin-option layer: a member cannot re-grant).
1284 self.consume_until_statement_boundary();
1285 return Ok(finish_grant(
1286 grant,
1287 GrantStatement {
1288 privileges: Vec::new(),
1289 object: GrantObject::Roles(roles),
1290 grantees,
1291 grant_option,
1292 },
1293 ));
1294 }
1295 self.advance(); // ON
1296 // An optional object-class keyword. `TABLE` (or no keyword at all) is
1297 // the enforced case; anything else parses and no-ops.
1298 let mut class = "TABLE";
1299 match self.peek() {
1300 Token::Table => {
1301 self.advance();
1302 }
1303 Token::All => {
1304 // v7.39 (read01 round 61) — `ALL TABLES IN SCHEMA x` expands to
1305 // every table at GRANT time, like PG. `ALL SEQUENCES/FUNCTIONS
1306 // IN SCHEMA` stay no-ops and keep their own object class.
1307 self.advance(); // ALL
1308 let kind = match self.peek() {
1309 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
1310 // TABLES has its own token (SHOW TABLES owns it).
1311 Token::Tables | Token::Table => "tables".to_string(),
1312 _ => String::new(),
1313 };
1314 if !kind.is_empty() {
1315 self.advance();
1316 }
1317 // `IN SCHEMA <name>`
1318 if matches!(self.peek(), Token::In) {
1319 self.advance();
1320 if self.peek_keyword_ident("schema") {
1321 self.advance();
1322 let _schema = self.expect_ident_like()?;
1323 }
1324 }
1325 if kind != "tables" {
1326 self.consume_until_statement_boundary();
1327 return Ok(finish_grant(
1328 grant,
1329 GrantStatement {
1330 privileges,
1331 object: GrantObject::Other("ALL … IN SCHEMA".into()),
1332 grantees: Vec::new(),
1333 grant_option,
1334 },
1335 ));
1336 }
1337 let grantees = self.parse_grantee_list(grant)?;
1338 self.consume_until_statement_boundary();
1339 return Ok(finish_grant(
1340 grant,
1341 GrantStatement {
1342 privileges,
1343 object: GrantObject::AllTablesInSchema,
1344 grantees,
1345 grant_option,
1346 },
1347 ));
1348 }
1349 Token::Ident(w) | Token::QuotedIdent(w) => {
1350 let lc = w.to_ascii_lowercase();
1351 // v7.39 (read01 round 60) — SEQUENCE / SCHEMA / DATABASE are
1352 // real objects with real ACLs now.
1353 if matches!(lc.as_str(), "sequence" | "schema" | "database") {
1354 self.advance();
1355 let mut names: Vec<String> = Vec::new();
1356 loop {
1357 let mut parts: Vec<String> = Vec::new();
1358 loop {
1359 parts.push(self.expect_ident_like()?);
1360 if matches!(self.peek(), Token::Dot) {
1361 self.advance();
1362 } else {
1363 break;
1364 }
1365 }
1366 names.push(parts.pop().expect("at least one part"));
1367 if matches!(self.peek(), Token::Comma) {
1368 self.advance();
1369 } else {
1370 break;
1371 }
1372 }
1373 let grantees = self.parse_grantee_list(grant)?;
1374 let mut grant_option = grant_option;
1375 if grant && self.peek_keyword_ident("with") {
1376 self.advance();
1377 self.expect_keyword_ident("grant")?;
1378 self.expect_keyword_ident("option")?;
1379 grant_option = true;
1380 }
1381 self.consume_until_statement_boundary();
1382 let object = match lc.as_str() {
1383 "sequence" => GrantObject::Sequences(names),
1384 "schema" => GrantObject::Schemas(names),
1385 _ => GrantObject::Databases(names),
1386 };
1387 return Ok(finish_grant(
1388 grant,
1389 GrantStatement {
1390 privileges,
1391 object,
1392 grantees,
1393 grant_option,
1394 },
1395 ));
1396 }
1397 // v7.39 (read01 round 61) — `ON FUNCTION f(int)` is real. SPG
1398 // keys functions by NAME, so the argument list parses and is
1399 // dropped (an overload set shares one ACL — recorded residual).
1400 if matches!(lc.as_str(), "function" | "procedure" | "routine") {
1401 self.advance();
1402 let mut names: Vec<(String, Option<Vec<String>>)> = Vec::new();
1403 loop {
1404 let mut parts: Vec<String> = Vec::new();
1405 loop {
1406 parts.push(self.expect_ident_like()?);
1407 if matches!(self.peek(), Token::Dot) {
1408 self.advance();
1409 } else {
1410 break;
1411 }
1412 }
1413 let fname = parts.pop().expect("at least one part");
1414 // v7.39 (read01 round 62) — the signature picks the
1415 // overload, so it is captured.
1416 let sig = if matches!(self.peek(), Token::LParen) {
1417 Some(self.parse_function_signature_types()?)
1418 } else {
1419 None
1420 };
1421 names.push((fname, sig));
1422 if matches!(self.peek(), Token::Comma) {
1423 self.advance();
1424 } else {
1425 break;
1426 }
1427 }
1428 let grantees = self.parse_grantee_list(grant)?;
1429 self.consume_until_statement_boundary();
1430 return Ok(finish_grant(
1431 grant,
1432 GrantStatement {
1433 privileges,
1434 object: GrantObject::Functions(names),
1435 grantees,
1436 grant_option,
1437 },
1438 ));
1439 }
1440 if matches!(
1441 lc.as_str(),
1442 "type"
1443 | "domain"
1444 | "language"
1445 | "tablespace"
1446 | "large"
1447 | "foreign"
1448 | "parameter"
1449 ) {
1450 self.consume_until_statement_boundary();
1451 return Ok(finish_grant(
1452 grant,
1453 GrantStatement {
1454 privileges,
1455 object: GrantObject::Other(lc.to_ascii_uppercase()),
1456 grantees: Vec::new(),
1457 grant_option,
1458 },
1459 ));
1460 }
1461 class = "TABLE";
1462 }
1463 _ => {}
1464 }
1465 let _ = class;
1466 // The table list. Schema-qualified names drop their qualifier (SPG is
1467 // single-schema) — but read the dotted parts from raw tokens, since
1468 // `expect_ident_like` would silently swallow the leading part.
1469 let mut tables: Vec<String> = Vec::new();
1470 loop {
1471 let mut parts: Vec<String> = Vec::new();
1472 loop {
1473 parts.push(self.expect_ident_like()?);
1474 if matches!(self.peek(), Token::Dot) {
1475 self.advance();
1476 } else {
1477 break;
1478 }
1479 }
1480 tables.push(parts.pop().expect("at least one part"));
1481 if matches!(self.peek(), Token::Comma) {
1482 self.advance();
1483 } else {
1484 break;
1485 }
1486 }
1487 let grantees = self.parse_grantee_list(grant)?;
1488 if grant && self.peek_keyword_ident("with") {
1489 self.advance();
1490 self.expect_keyword_ident("grant")?;
1491 self.expect_keyword_ident("option")?;
1492 grant_option = true;
1493 }
1494 // REVOKE's trailing CASCADE / RESTRICT — SPG has no dependent grants
1495 // to cascade to (no re-granting), so both are accepted and ignored.
1496 if !grant && (self.peek_keyword_ident("cascade") || self.peek_keyword_ident("restrict")) {
1497 self.advance();
1498 }
1499 Ok(finish_grant(
1500 grant,
1501 GrantStatement {
1502 privileges,
1503 object: GrantObject::Tables(tables),
1504 grantees,
1505 grant_option,
1506 },
1507 ))
1508 }
1509
1510 /// v7.39 (read01 round 62) — the argument TYPES in a function signature:
1511 /// `(int, text)` or `(x int, y text)` (PG accepts either). Returns the type
1512 /// words; the caller normalises them into a signature key.
1513 fn parse_function_signature_types(&mut self) -> Result<Vec<String>, ParseError> {
1514 self.advance(); // (
1515 let mut types: Vec<String> = Vec::new();
1516 if matches!(self.peek(), Token::RParen) {
1517 self.advance();
1518 return Ok(types);
1519 }
1520 loop {
1521 // Collect the words of one argument up to a comma / close paren.
1522 let mut words: Vec<String> = Vec::new();
1523 loop {
1524 match self.peek() {
1525 Token::Comma | Token::RParen | Token::Eof => break,
1526 _ => {}
1527 }
1528 let tok = self.advance();
1529 match tok {
1530 Token::Ident(w) | Token::QuotedIdent(w) => words.push(w),
1531 other => {
1532 if let Some(w) = unreserved_keyword_text(&other) {
1533 words.push(w);
1534 }
1535 }
1536 }
1537 }
1538 // `name TYPE` or a bare `TYPE`. Several of PG's type names are
1539 // themselves several words (`double precision`, `character
1540 // varying`, `timestamp with time zone`), so "two words means the
1541 // first is a parameter name" reads the type off `f(double
1542 // precision)` as `precision`. v7.39 (round 282): recognise the
1543 // multi-word spellings first — a leading word that STARTS one of
1544 // them is part of the type, not a name.
1545 let joined = words.join(" ");
1546 let ty = if words.len() >= 2 && is_multiword_type_phrase(&joined) {
1547 joined
1548 } else if words.len() >= 2 {
1549 words[1..].join(" ")
1550 } else {
1551 words.first().cloned().unwrap_or_default()
1552 };
1553 types.push(ty);
1554 if matches!(self.peek(), Token::Comma) {
1555 self.advance();
1556 } else {
1557 break;
1558 }
1559 }
1560 if matches!(self.peek(), Token::RParen) {
1561 self.advance();
1562 }
1563 Ok(types)
1564 }
1565
1566 /// v7.39 (read01 round 59) — `( col, col, … )` after a privilege word.
1567 fn parse_grant_column_list(&mut self) -> Result<Vec<String>, ParseError> {
1568 self.advance(); // (
1569 let mut cols = Vec::new();
1570 loop {
1571 cols.push(self.expect_ident_like()?);
1572 if matches!(self.peek(), Token::Comma) {
1573 self.advance();
1574 } else {
1575 break;
1576 }
1577 }
1578 if !matches!(self.peek(), Token::RParen) {
1579 return Err(self.err(alloc::format!(
1580 "expected ')' to close the column list, got {:?}",
1581 self.peek()
1582 )));
1583 }
1584 self.advance(); // )
1585 Ok(cols)
1586 }
1587
1588 /// `TO <roles>` (grant) / `FROM <roles>` (revoke). An empty-string entry is
1589 /// PUBLIC.
1590 fn parse_grantee_list(&mut self, grant: bool) -> Result<Vec<String>, ParseError> {
1591 if grant {
1592 if matches!(self.peek(), Token::To) {
1593 self.advance();
1594 } else {
1595 self.expect_keyword_ident("to")?;
1596 }
1597 } else if matches!(self.peek(), Token::From) {
1598 self.advance();
1599 } else {
1600 self.expect_keyword_ident("from")?;
1601 }
1602 let mut grantees: Vec<String> = Vec::new();
1603 loop {
1604 // `GROUP name` is the legacy spelling of a plain role name.
1605 if self.peek_keyword_ident("group") {
1606 self.advance();
1607 }
1608 if self.peek_keyword_ident("public") {
1609 self.advance();
1610 grantees.push(String::new()); // PUBLIC
1611 } else {
1612 grantees.push(self.expect_ident_like()?);
1613 }
1614 if matches!(self.peek(), Token::Comma) {
1615 self.advance();
1616 } else {
1617 break;
1618 }
1619 }
1620 Ok(grantees)
1621 }
1622
1623 /// v7.39 (round 277) — `PREPARE <name> [(type, …)] AS <statement>`.
1624 /// The body keeps its `$N` placeholders; substitution happens at
1625 /// EXECUTE. The declared types are recorded for
1626 /// `pg_prepared_statements.parameter_types` but are not enforced —
1627 /// PG infers when the list is omitted, and SPG resolves the values
1628 /// at substitution time either way.
1629 fn parse_prepare(&mut self) -> Result<Statement, ParseError> {
1630 let start = self.pos;
1631 self.advance(); // PREPARE
1632 // v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'` is 2PC, a
1633 // different statement that happens to share the keyword. PG
1634 // ships with `max_prepared_transactions = 0` and reports it
1635 // this way; SPG has no prepared-transaction registry, so the
1636 // same wording is the accurate answer rather than a dodge.
1637 // Round 277 turned this from a silent no-op into a confusing
1638 // "expected AS in PREPARE" parse error.
1639 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
1640 self.advance();
1641 let gid = match self.advance() {
1642 Token::String(g) => g,
1643 other => {
1644 return Err(self.err(alloc::format!(
1645 "expected a transaction identifier after PREPARE TRANSACTION, got {other:?}"
1646 )));
1647 }
1648 };
1649 return Ok(Statement::PrepareTransaction(gid));
1650 }
1651 let name = self.expect_ident_like()?;
1652 let mut param_types = Vec::new();
1653 if matches!(self.peek(), Token::LParen) {
1654 self.advance();
1655 loop {
1656 let mut ty = self.expect_ident_like()?;
1657 // A parameterised type name (`numeric(10,2)`,
1658 // `varchar(20)`) keeps its argument list in the text.
1659 if matches!(self.peek(), Token::LParen) {
1660 let mut depth = 0usize;
1661 let mut buf = String::from("(");
1662 loop {
1663 match self.advance() {
1664 Token::LParen => {
1665 depth += 1;
1666 if depth > 1 {
1667 buf.push('(');
1668 }
1669 }
1670 Token::RParen => {
1671 depth -= 1;
1672 buf.push(')');
1673 if depth == 0 {
1674 break;
1675 }
1676 }
1677 Token::Comma => buf.push(','),
1678 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
1679 Token::Eof => break,
1680 _ => {}
1681 }
1682 }
1683 ty.push_str(&buf);
1684 }
1685 // r1049 — `PREPARE p(bigint[]) AS …`: the sixth `[]`
1686 // position, same family as the parameter list above.
1687 let array_suffix = self.consume_array_suffix();
1688 ty.push_str(&array_suffix);
1689 param_types.push(ty);
1690 match self.peek() {
1691 Token::Comma => {
1692 self.advance();
1693 }
1694 Token::RParen => {
1695 self.advance();
1696 break;
1697 }
1698 other => {
1699 return Err(self.err(alloc::format!(
1700 "expected ',' or ')' in PREPARE parameter list, got {other:?}"
1701 )));
1702 }
1703 }
1704 }
1705 }
1706 if !matches!(self.peek(), Token::As) {
1707 return Err(self.err(alloc::format!(
1708 "expected AS in PREPARE, got {:?}",
1709 self.peek()
1710 )));
1711 }
1712 self.advance();
1713 let body = self.parse_one_statement()?;
1714 // The Parser holds tokens, not the source text, so the
1715 // statement PG reports in `pg_prepared_statements.statement`
1716 // is rebuilt from the AST rather than sliced from the input.
1717 let _ = start;
1718 let mut source = alloc::format!("PREPARE {}", crate::ast::quote_ident(&name));
1719 if !param_types.is_empty() {
1720 source.push_str(" (");
1721 source.push_str(¶m_types.join(", "));
1722 source.push(')');
1723 }
1724 source.push_str(" AS ");
1725 source.push_str(&alloc::format!("{body}"));
1726 Ok(Statement::Prepare {
1727 name,
1728 param_types,
1729 body: alloc::boxed::Box::new(body),
1730 source,
1731 })
1732 }
1733
1734 /// v7.39 (round 277) — `EXECUTE <name> [(<expr>, …)]`.
1735 fn parse_execute(&mut self) -> Result<Statement, ParseError> {
1736 self.advance(); // EXECUTE
1737 let name = self.expect_ident_like()?;
1738 let mut args = Vec::new();
1739 if matches!(self.peek(), Token::LParen) {
1740 self.advance();
1741 if matches!(self.peek(), Token::RParen) {
1742 self.advance();
1743 } else {
1744 loop {
1745 args.push(self.parse_expr(0)?);
1746 match self.advance() {
1747 Token::Comma => {}
1748 Token::RParen => break,
1749 other => {
1750 return Err(self.err(alloc::format!(
1751 "expected ',' or ')' in EXECUTE arguments, got {other:?}"
1752 )));
1753 }
1754 }
1755 }
1756 }
1757 }
1758 Ok(Statement::Execute { name, args })
1759 }
1760
1761 /// v7.39 (round 277) — `DEALLOCATE {[PREPARE] <name> | ALL}`.
1762 /// v7.39 (round 278) — `CALL <proc>([args])`. There is no
1763 /// procedure catalog yet, so this reports PG's not-found error
1764 /// (with its HINT) rather than pretending the call ran.
1765 /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
1766 /// Bare `DISCARD` is a syntax error in PG; so it is here.
1767 fn parse_discard(&mut self) -> Result<Statement, ParseError> {
1768 self.advance(); // DISCARD
1769 let target = match self.advance() {
1770 Token::All => DiscardTarget::All,
1771 Token::Ident(w) | Token::QuotedIdent(w) => match w.to_ascii_lowercase().as_str() {
1772 "all" => DiscardTarget::All,
1773 "plans" => DiscardTarget::Plans,
1774 "sequences" => DiscardTarget::Sequences,
1775 "temp" | "temporary" => DiscardTarget::Temp,
1776 other => {
1777 return Err(self.err(format!(
1778 "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1779 )));
1780 }
1781 },
1782 other => {
1783 return Err(self.err(format!(
1784 "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1785 )));
1786 }
1787 };
1788 Ok(Statement::Discard(target))
1789 }
1790
1791 /// v7.39 (round 318, V51) — MySQL `KILL [HARD|SOFT] [CONNECTION|QUERY]
1792 /// <expr>`. MariaDB accepts an expression for the id (its own docs use
1793 /// `KILL connection_id()`), and the HARD / SOFT prefixes only pick how
1794 /// aggressively the server interrupts, which SPG does not distinguish.
1795 /// Bare `KILL <id>` means CONNECTION.
1796 fn parse_kill(&mut self) -> Result<Statement, ParseError> {
1797 self.advance(); // KILL
1798 let mut query_only = false;
1799 loop {
1800 // CONNECTION is a reserved keyword token (it also opens
1801 // `CREATE SUBSCRIPTION … CONNECTION '…'`), so it arrives as
1802 // `Token::Connection` rather than a bare ident.
1803 if matches!(self.peek(), Token::Connection) {
1804 self.advance();
1805 break;
1806 }
1807 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
1808 break;
1809 };
1810 match w.to_ascii_lowercase().as_str() {
1811 "hard" | "soft" => {
1812 self.advance();
1813 }
1814 "query" => {
1815 self.advance();
1816 query_only = true;
1817 break;
1818 }
1819 _ => break,
1820 }
1821 }
1822 let id = self.parse_expr(0)?;
1823 Ok(Statement::Kill {
1824 query_only,
1825 id: Box::new(id),
1826 })
1827 }
1828
1829 fn parse_call(&mut self) -> Result<Statement, ParseError> {
1830 self.advance(); // CALL
1831 let name = self.expect_ident_like()?;
1832 self.consume_until_statement_boundary();
1833 Ok(Statement::Call(name))
1834 }
1835
1836 fn parse_deallocate(&mut self) -> Result<Statement, ParseError> {
1837 self.advance(); // DEALLOCATE
1838 // PG accepts an optional noise `PREPARE` keyword here.
1839 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("prepare")) {
1840 self.advance();
1841 }
1842 if matches!(self.peek(), Token::All) {
1843 self.advance();
1844 return Ok(Statement::Deallocate(None));
1845 }
1846 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("all")) {
1847 self.advance();
1848 return Ok(Statement::Deallocate(None));
1849 }
1850 let name = self.expect_ident_like()?;
1851 Ok(Statement::Deallocate(Some(name)))
1852 }
1853
1854 fn consume_until_statement_boundary(&mut self) {
1855 loop {
1856 match self.peek() {
1857 Token::Semicolon | Token::Eof => return,
1858 _ => self.advance(),
1859 };
1860 }
1861 }
1862
1863 /// v7.22 (round-13 T2) — consume to the statement boundary like
1864 /// `consume_until_statement_boundary`, but pick out the sequence
1865 /// name on the way: either `SEQUENCE NAME <ident>` (identity
1866 /// columns) or the first string literal (`nextval('<seq>')`).
1867 /// Schema qualifiers and `::regclass` casts are stripped.
1868 fn scan_sequence_name_until_boundary(&mut self) -> Option<String> {
1869 let mut seq: Option<String> = None;
1870 let mut after_sequence_kw = false;
1871 let mut after_name_kw = false;
1872 loop {
1873 match self.peek().clone() {
1874 Token::Semicolon | Token::Eof => break,
1875 Token::Ident(s) | Token::QuotedIdent(s) => {
1876 if after_name_kw && seq.is_none() {
1877 self.advance();
1878 let mut name = s;
1879 // `SEQUENCE NAME public.groups_id_seq` — keep
1880 // the bare name, drop qualifiers.
1881 while matches!(self.peek(), Token::Dot) {
1882 self.advance();
1883 if let Token::Ident(n) | Token::QuotedIdent(n) = self.advance() {
1884 name = n;
1885 }
1886 }
1887 seq = Some(name);
1888 after_name_kw = false;
1889 continue;
1890 }
1891 if after_sequence_kw && s.eq_ignore_ascii_case("name") {
1892 after_name_kw = true;
1893 after_sequence_kw = false;
1894 } else {
1895 after_sequence_kw = s.eq_ignore_ascii_case("sequence");
1896 }
1897 self.advance();
1898 }
1899 Token::String(s) => {
1900 if seq.is_none() {
1901 // `nextval('public.groups_id_seq'::regclass)`
1902 let bare = s
1903 .rsplit_once('.')
1904 .map_or_else(|| s.clone(), |(_, b)| b.to_string());
1905 seq = Some(bare);
1906 }
1907 self.advance();
1908 }
1909 _ => {
1910 after_sequence_kw = false;
1911 after_name_kw = false;
1912 self.advance();
1913 }
1914 }
1915 }
1916 seq
1917 }
1918
1919 /// v7.39 (round 621) — is the next token the keyword `BY`?
1920 ///
1921 /// `pg_get_keywords()` classes `by` as `U` (unreserved), so it is a legal
1922 /// column, table and alias name — and SPG lexed it into a dedicated
1923 /// `Token::By`, which made it unusable as a name ANYWHERE. Of the seven
1924 /// two-letter keywords the lexer knew, this was the only one PG leaves
1925 /// unreserved (`as`, `in`, `on`, `or`, `to` are reserved and `is` is `T`).
1926 ///
1927 /// The token is gone; the three clauses that own the word — GROUP BY,
1928 /// ORDER BY, PARTITION BY — and the handful of other places that expect it
1929 /// ask this instead. Adding it to the unreserved-identifier table was not
1930 /// enough on its own: identifier positions that match the token shape
1931 /// directly (an index's column list, a table alias) never consult that
1932 /// table, so `CREATE INDEX … ON t(by)` and `FROM t AS by` still failed.
1933 /// Not lexing it as a keyword closes the whole class rather than the two
1934 /// positions that happened to be noticed.
1935 fn peek_is_by(&self) -> bool {
1936 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by"))
1937 }
1938
1939 /// v7.39 (round 621) — the optional `CASCADE` / `RESTRICT` trailer a DROP
1940 /// takes. Accepted and dropped: SPG tracks no dependents to cascade to,
1941 /// which is what `DROP TABLE` and `DROP INDEX` have done since v7.14.
1942 fn consume_drop_behaviour(&mut self) {
1943 if matches!(
1944 self.peek(),
1945 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") || s.eq_ignore_ascii_case("restrict")
1946 ) {
1947 self.advance();
1948 }
1949 }
1950
1951 fn expect_ident_like(&mut self) -> Result<String, ParseError> {
1952 let first = match self.advance() {
1953 Token::Ident(s) | Token::QuotedIdent(s) => s,
1954 // v7.37.43-T4 — PG-unreserved keywords are legal identifiers
1955 // per PG's `pg_get_keywords()` classification. SPG tokenizes
1956 // these as named variants for parsing leverage in the
1957 // contexts that own them (`RELEASE SAVEPOINT`, `SHOW name`,
1958 // `BEGIN`, etc.), but they MUST still be usable as table /
1959 // column / alias names in DDL+DML. Sentori migrations like
1960 // 0001_init.sql ship `release TEXT NOT NULL` in the events
1961 // table — the `events.release` column carries the release
1962 // identifier string. Pre-T4 this triggered "expected
1963 // identifier, got Release" and blocked every drop-in user
1964 // whose schema had a column / alias with one of these names.
1965 other if unreserved_keyword_text(&other).is_some() => {
1966 unreserved_keyword_text(&other).unwrap()
1967 }
1968 other => {
1969 return Err(ParseError {
1970 message: format!("expected identifier, got {other:?}"),
1971 token_pos: self.consumed_pos(),
1972 });
1973 }
1974 };
1975 // v7.14.0 — strip optional `<schema>.` prefix. PG dumps
1976 // qualify every name with `public.` (and pg_catalog.* for
1977 // functions); SPG is single-schema so we discard the
1978 // prefix and return only the trailing ident. Same shape
1979 // also handles MySQL `db.tbl` cross-database refs (SPG
1980 // ignores the db part).
1981 if matches!(self.peek(), Token::Dot) {
1982 self.advance();
1983 match self.advance() {
1984 Token::Ident(s) | Token::QuotedIdent(s) => return Ok(s),
1985 other if unreserved_keyword_text(&other).is_some() => {
1986 return Ok(unreserved_keyword_text(&other).unwrap());
1987 }
1988 other => {
1989 return Err(ParseError {
1990 message: format!("expected identifier after '{first}.', got {other:?}"),
1991 token_pos: self.consumed_pos(),
1992 });
1993 }
1994 }
1995 }
1996 Ok(first)
1997 }
1998
1999 #[allow(clippy::too_many_lines)]
2000 fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
2001 // v7.14.0 — empty / comment-only / semicolon-only input
2002 // (after the lexer strips line + block + MySQL
2003 // conditional comments) lands as Statement::Empty.
2004 // pg_dump and mysqldump emit several wrappers that
2005 // collapse to nothing after stripping (`/*!40101 SET …
2006 // */;`, blank lines between statements); the engine
2007 // returns CommandOk no-op so the dump loads cleanly.
2008 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
2009 return Ok(Statement::Empty);
2010 }
2011 // v7.14.0 — pg_dump / mysqldump "noise" statements:
2012 // catalog / metadata DDL that has no behavioural effect
2013 // on SPG's single-schema, single-database, single-user
2014 // model. Consume the whole statement up to the next
2015 // semicolon / EOF and return Empty. This is broader than
2016 // the per-keyword DROP / SET / COMMENT arms but lets the
2017 // long tail of `LOCK TABLES`, `UNLOCK TABLES`, `GRANT`,
2018 // `REVOKE`, `ALTER OWNER TO`, `\restrict`, `\unrestrict`,
2019 // `BEGIN; COMMIT;` wrappers, etc. all pass through.
2020 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
2021 let lc = s.to_ascii_lowercase();
2022 // v7.39 (read01 round 50) — COMMENT ON is a real statement now.
2023 if lc == "comment" {
2024 return self.parse_comment_on();
2025 }
2026 // v7.39 (read01 round 57) — so is GRANT / REVOKE.
2027 if lc == "grant" || lc == "revoke" {
2028 return self.parse_grant_or_revoke(lc == "grant");
2029 }
2030 // v7.39 (round 277) — the SQL-level prepared-statement
2031 // surface is REAL now. It used to be accepted and dropped
2032 // on the theory that "real execution still happens via the
2033 // extended-query flow" — true only for a driver that uses
2034 // that flow; a plain SQL PREPARE / EXECUTE returned no
2035 // rows at all.
2036 if lc == "prepare" {
2037 return self.parse_prepare();
2038 }
2039 if lc == "execute" {
2040 return self.parse_execute();
2041 }
2042 if lc == "deallocate" {
2043 return self.parse_deallocate();
2044 }
2045 // v7.39 (round 278) — `CALL <proc>(<args>)` used to be
2046 // accepted and dropped, so an application's stored-procedure
2047 // invocation reported success and did nothing. SPG has no
2048 // procedure catalog, so every CALL names a procedure that
2049 // does not exist — which is exactly what PG says.
2050 if lc == "call" {
2051 return self.parse_call();
2052 }
2053 // v7.39 (round 318, V51) — MySQL `KILL`. Not dump noise: it
2054 // names one connection and acts on it.
2055 if lc == "kill" {
2056 return self.parse_kill();
2057 }
2058 if lc == "discard" {
2059 return self.parse_discard();
2060 }
2061 // v7.39 (round 696) — REASSIGN OWNED BY <role> [, …] TO <role>.
2062 // Still performs nothing; the roles are carried out so a name
2063 // that does not exist is refused, as PG18 refuses it.
2064 if lc == "reassign" {
2065 self.advance();
2066 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("owned")) {
2067 self.advance();
2068 }
2069 if self.peek_is_by() {
2070 self.advance();
2071 }
2072 // Only the roles BEFORE the TO are the ones that must
2073 // exist — `TO` names the new owner, which PG checks as
2074 // well, so both lists are collected.
2075 let mut names = self.take_comma_separated_names();
2076 if matches!(self.peek(), Token::To) {
2077 self.advance();
2078 names.extend(self.take_comma_separated_names());
2079 }
2080 self.consume_until_statement_boundary();
2081 return Ok(Statement::ValidateOnly {
2082 kind: crate::ast::ValidateOnlyKind::RoleName,
2083 names,
2084 });
2085 }
2086 // v7.39 (round 696) — SECURITY LABEL. PG18 refuses it
2087 // unconditionally with `no security label providers have been
2088 // loaded`, whatever object it names, because none is loaded.
2089 // SPG has none either; accepting it told the caller a label had
2090 // been applied when nothing anywhere records one.
2091 if lc == "security" {
2092 self.consume_until_statement_boundary();
2093 return Ok(Statement::ValidateOnly {
2094 kind: crate::ast::ValidateOnlyKind::SecurityLabel,
2095 names: Vec::new(),
2096 });
2097 }
2098 if is_dump_noise_statement(&lc) {
2099 self.consume_until_statement_boundary();
2100 return Ok(Statement::Empty);
2101 }
2102 }
2103 match self.peek() {
2104 Token::Select => self.parse_select_stmt(),
2105 // v7.37.17 (17.6 siblings) — a statement opening with a
2106 // parenthesized query group: `(SELECT … UNION …)
2107 // INTERSECT …`. parse_bare_select's group arm consumes
2108 // the parens; the select parser handles the outer chain
2109 // and tail.
2110 Token::LParen
2111 if matches!(
2112 self.tokens.get(self.pos + 1),
2113 Some(Token::Select | Token::LParen | Token::Values)
2114 ) =>
2115 {
2116 self.parse_select_stmt()
2117 }
2118 // v7.37.17 (17.6 siblings) — top-level bare VALUES
2119 // statement (`VALUES (1), (2) [ORDER BY …] [LIMIT …]`).
2120 // Lowers to the same UNION ALL chain the FROM-position
2121 // form uses, then reuses the shared SELECT tail.
2122 Token::Values => {
2123 self.advance(); // VALUES
2124 let mut head = self.parse_values_rows_body()?;
2125 self.parse_select_tail_into(&mut head)?;
2126 Ok(Statement::Select(head))
2127 }
2128 // SQL-standard `TABLE name` shorthand for
2129 // `SELECT * FROM name` — pg_dump never emits it, but
2130 // psql users and PG docs use it constantly. Set-op
2131 // chains and the ORDER BY/LIMIT tail compose like any
2132 // SELECT head.
2133 Token::Table
2134 if matches!(
2135 self.tokens.get(self.pos + 1),
2136 Some(Token::Ident(_) | Token::QuotedIdent(_))
2137 ) =>
2138 {
2139 let mut head = self.parse_table_shorthand()?;
2140 self.parse_setop_chain_into(&mut head)?;
2141 self.parse_select_tail_into(&mut head)?;
2142 Ok(Statement::Select(head))
2143 }
2144 // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2145 // body is a dollar-quoted plpgsql block (lexer already
2146 // collapsed `$$…$$` into a single Token::String).
2147 // v7.16.2 — mailrs round-10 A.2: parse the body as a
2148 // real PlPgSqlBlock so the engine can EXECUTE it at
2149 // top level instead of silently swallowing. Pre-
2150 // v7.16.2 the parser threw the body away and the
2151 // engine returned CommandOk for the entire DO; that
2152 // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2153 // $$` into a SEV-1 silent no-op (the IF + the rename
2154 // were both invisible — mailrs's migrate-042 didn't
2155 // actually run). Now the body parses + executes;
2156 // EmbeddedSql inside the block runs immediately
2157 // against the engine (not deferred — we're at top
2158 // level, not inside a trigger row-write loop).
2159 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2160 self.advance();
2161 let body_text = match self.advance() {
2162 Token::String(s) => s,
2163 other => {
2164 return Err(self.err(alloc::format!(
2165 "expected dollar-quoted body after DO, got {other:?}"
2166 )));
2167 }
2168 };
2169 // Optional `LANGUAGE <name>` trailer (idents only).
2170 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2171 self.advance();
2172 let _ = self.expect_ident_like()?;
2173 }
2174 // Parse the body — same shape CREATE FUNCTION
2175 // uses for trigger function bodies. If the body
2176 // doesn't parse cleanly we surface the error
2177 // (better than silent no-op).
2178 let block = parse_plpgsql_body(&body_text)?;
2179 Ok(Statement::DoBlock(block))
2180 }
2181 // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2182 // WITH isn't a reserved token in our lexer — comes through
2183 // as `Token::Ident("with")` (case-insensitive).
2184 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2185 self.advance();
2186 self.parse_with_cte_then_select()
2187 }
2188 // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2189 // an identifier — not a reserved keyword.
2190 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2191 self.advance();
2192 let mut analyze = false;
2193 let mut suggest = false;
2194 let mut costs_off = false;
2195 let mut buffers = false;
2196 let mut timing_off = false;
2197 let mut settings = false;
2198 let mut wal = false;
2199 let mut summary_off = false;
2200 let mut format = crate::ast::ExplainFormat::Text;
2201 // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2202 // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2203 // options are comma-separated. Booleans default to ON
2204 // when the value token is omitted (matches PG).
2205 if matches!(self.peek(), Token::LParen) {
2206 self.advance();
2207 loop {
2208 let opt = match self.peek().clone() {
2209 Token::Ident(s) | Token::QuotedIdent(s) => s,
2210 other => {
2211 return Err(self.err(format!(
2212 "expected option keyword inside EXPLAIN (…), got {other:?}"
2213 )));
2214 }
2215 };
2216 self.advance();
2217 if opt.eq_ignore_ascii_case("suggest") {
2218 suggest = true;
2219 // SUGGEST takes no explicit value today.
2220 } else if opt.eq_ignore_ascii_case("costs") {
2221 // PG syntax: `COSTS [ON | OFF]`. Default
2222 // when value omitted is ON, so plain
2223 // `COSTS` is a no-op. `COSTS OFF` flips.
2224 // `ON` lexes to `Token::On` (reserved
2225 // keyword in JOIN ... ON contexts); accept
2226 // it alongside the bare Ident form so the
2227 // grammar matches PG verbatim.
2228 let value = match self.peek().clone() {
2229 Token::On => {
2230 self.advance();
2231 true
2232 }
2233 Token::Ident(v) | Token::QuotedIdent(v)
2234 if v.eq_ignore_ascii_case("off") =>
2235 {
2236 self.advance();
2237 false
2238 }
2239 Token::Ident(v) | Token::QuotedIdent(v)
2240 if v.eq_ignore_ascii_case("true") =>
2241 {
2242 self.advance();
2243 true
2244 }
2245 _ => true,
2246 };
2247 costs_off = !value;
2248 } else if opt.eq_ignore_ascii_case("analyze")
2249 || opt.eq_ignore_ascii_case("analyse")
2250 {
2251 // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2252 // Same default-ON rule as ANALYZE keyword form.
2253 let value = match self.peek().clone() {
2254 Token::On => {
2255 self.advance();
2256 true
2257 }
2258 Token::Ident(v) | Token::QuotedIdent(v)
2259 if v.eq_ignore_ascii_case("off") =>
2260 {
2261 self.advance();
2262 false
2263 }
2264 Token::Ident(v) | Token::QuotedIdent(v)
2265 if v.eq_ignore_ascii_case("true") =>
2266 {
2267 self.advance();
2268 true
2269 }
2270 _ => true,
2271 };
2272 analyze = value;
2273 } else if opt.eq_ignore_ascii_case("buffers") {
2274 // v7.37.22 — `BUFFERS [ON|OFF]`.
2275 let value = match self.peek().clone() {
2276 Token::On => {
2277 self.advance();
2278 true
2279 }
2280 Token::Ident(v) | Token::QuotedIdent(v)
2281 if v.eq_ignore_ascii_case("off") =>
2282 {
2283 self.advance();
2284 false
2285 }
2286 Token::Ident(v) | Token::QuotedIdent(v)
2287 if v.eq_ignore_ascii_case("true") =>
2288 {
2289 self.advance();
2290 true
2291 }
2292 _ => true,
2293 };
2294 buffers = value;
2295 } else if opt.eq_ignore_ascii_case("timing") {
2296 // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2297 // the measured wall-clock annotation.
2298 let value = match self.peek().clone() {
2299 Token::On => {
2300 self.advance();
2301 true
2302 }
2303 Token::Ident(v) | Token::QuotedIdent(v)
2304 if v.eq_ignore_ascii_case("off") =>
2305 {
2306 self.advance();
2307 false
2308 }
2309 Token::Ident(v) | Token::QuotedIdent(v)
2310 if v.eq_ignore_ascii_case("true") =>
2311 {
2312 self.advance();
2313 true
2314 }
2315 _ => true,
2316 };
2317 timing_off = !value;
2318 } else if opt.eq_ignore_ascii_case("settings") {
2319 settings = true;
2320 } else if opt.eq_ignore_ascii_case("wal") {
2321 wal = true;
2322 } else if opt.eq_ignore_ascii_case("summary") {
2323 // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2324 // gates the trailing Planning/Execution Time
2325 // lines now (was accept-and-no-op).
2326 let value = match self.peek().clone() {
2327 Token::On => {
2328 self.advance();
2329 true
2330 }
2331 Token::Ident(v) | Token::QuotedIdent(v)
2332 if v.eq_ignore_ascii_case("off") =>
2333 {
2334 self.advance();
2335 false
2336 }
2337 Token::Ident(v) | Token::QuotedIdent(v)
2338 if v.eq_ignore_ascii_case("true") =>
2339 {
2340 self.advance();
2341 true
2342 }
2343 _ => true,
2344 };
2345 summary_off = !value;
2346 } else if opt.eq_ignore_ascii_case("verbose")
2347 || opt.eq_ignore_ascii_case("format")
2348 {
2349 // v7.37.22 — accept-but-no-op the remaining
2350 // PG options so EXPLAIN-using clients
2351 // (pgAdmin / DataGrip) don't see syntax
2352 // errors. FORMAT takes a value (text /
2353 // json / yaml / xml); skip the next token
2354 // if it's an ident.
2355 if opt.eq_ignore_ascii_case("format") {
2356 if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2357 {
2358 self.advance();
2359 format = match v.to_ascii_lowercase().as_str() {
2360 "text" => crate::ast::ExplainFormat::Text,
2361 "json" => crate::ast::ExplainFormat::Json,
2362 "xml" => crate::ast::ExplainFormat::Xml,
2363 "yaml" => crate::ast::ExplainFormat::Yaml,
2364 other => {
2365 return Err(self.err(format!(
2366 "EXPLAIN (FORMAT …): unknown format {other:?}; \
2367 supports text, json, xml, yaml"
2368 )));
2369 }
2370 };
2371 }
2372 } else {
2373 // VERBOSE / SUMMARY take optional ON/OFF;
2374 // consume if present.
2375 if matches!(self.peek(), Token::On) {
2376 self.advance();
2377 } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2378 self.peek().clone()
2379 && (v.eq_ignore_ascii_case("off")
2380 || v.eq_ignore_ascii_case("true"))
2381 {
2382 self.advance();
2383 let _ = v;
2384 }
2385 }
2386 } else {
2387 return Err(self.err(format!(
2388 "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2389 )));
2390 }
2391 if matches!(self.peek(), Token::Comma) {
2392 self.advance();
2393 continue;
2394 }
2395 break;
2396 }
2397 if !matches!(self.peek(), Token::RParen) {
2398 return Err(self.err(format!(
2399 "expected ')' after EXPLAIN options, got {:?}",
2400 self.peek()
2401 )));
2402 }
2403 self.advance();
2404 } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2405 && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2406 {
2407 self.advance();
2408 analyze = true;
2409 }
2410 // v7.39 (round 224) — the body may open with WITH (CTEs);
2411 // route through the same CTE-then-SELECT path the top-level
2412 // WITH statement uses. v7.39 (round 225) — DML bodies parse
2413 // too (PG explains INSERT / UPDATE / DELETE).
2414 let inner = match self.peek().clone() {
2415 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2416 self.advance();
2417 self.parse_with_cte_then_select()?
2418 }
2419 Token::Insert => self.parse_insert_stmt(false)?,
2420 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2421 self.advance();
2422 self.parse_update_after_keyword()?
2423 }
2424 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2425 self.advance();
2426 self.parse_delete_after_keyword()?
2427 }
2428 _ => self.parse_select_stmt()?,
2429 };
2430 if !matches!(
2431 inner,
2432 Statement::Select(_)
2433 | Statement::Insert(_)
2434 | Statement::Update(_)
2435 | Statement::Delete(_)
2436 ) {
2437 return Err(self.err(format!(
2438 "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2439 )));
2440 }
2441 Ok(Statement::Explain(crate::ast::ExplainStatement {
2442 analyze,
2443 inner: Box::new(inner),
2444 suggest,
2445 costs_off,
2446 buffers,
2447 timing_off,
2448 settings,
2449 wal,
2450 summary_off,
2451 format,
2452 }))
2453 }
2454 Token::Create => self.parse_create_stmt(),
2455 Token::Insert => self.parse_insert_stmt(false),
2456 // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2457 // spelling; route to the same handler. DESC is the
2458 // reserved ORDER BY token, so it gets its own arm.
2459 Token::Ident(s)
2460 if s.eq_ignore_ascii_case("describe")
2461 && matches!(
2462 self.tokens.get(self.pos + 1),
2463 Some(Token::Ident(_) | Token::QuotedIdent(_))
2464 ) =>
2465 {
2466 self.advance();
2467 let table = self.expect_ident_like()?;
2468 Ok(Statement::ShowColumns(table))
2469 }
2470 Token::Desc
2471 if matches!(
2472 self.tokens.get(self.pos + 1),
2473 Some(Token::Ident(_) | Token::QuotedIdent(_))
2474 ) =>
2475 {
2476 self.advance();
2477 let table = self.expect_ident_like()?;
2478 Ok(Statement::ShowColumns(table))
2479 }
2480 // `COPY table [(cols)] TO STDOUT` — the export half of
2481 // pg_dump's COPY pair (the FROM stdin half rides the
2482 // embed import path). Options need a format design and
2483 // error honestly.
2484 Token::Ident(s)
2485 if s.eq_ignore_ascii_case("copy")
2486 && matches!(
2487 self.tokens.get(self.pos + 1),
2488 Some(Token::Ident(_) | Token::QuotedIdent(_))
2489 ) =>
2490 {
2491 self.advance(); // COPY
2492 let table = self.expect_ident_like()?;
2493 let columns = if matches!(self.peek(), Token::LParen) {
2494 self.advance();
2495 let mut cols = alloc::vec![self.expect_ident_like()?];
2496 while matches!(self.peek(), Token::Comma) {
2497 self.advance();
2498 cols.push(self.expect_ident_like()?);
2499 }
2500 if !matches!(self.peek(), Token::RParen) {
2501 return Err(self.err(format!(
2502 "expected ')' after COPY column list, got {:?}",
2503 self.peek()
2504 )));
2505 }
2506 self.advance();
2507 Some(cols)
2508 } else {
2509 None
2510 };
2511 // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2512 // endpoint. (FROM STDIN still rides the wire/import path —
2513 // its data arrives out of band.)
2514 if matches!(self.peek(), Token::From)
2515 && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2516 {
2517 self.advance(); // FROM
2518 let Token::String(path) = self.advance() else {
2519 unreachable!()
2520 };
2521 let options = self.parse_copy_to_options()?;
2522 return Ok(Statement::CopyFromFile {
2523 table,
2524 columns,
2525 path,
2526 options,
2527 });
2528 }
2529 if !matches!(self.peek(), Token::To) {
2530 return Err(self.err(format!(
2531 "COPY: only TO STDOUT is supported here (FROM stdin \
2532 rides the import path); got {:?}",
2533 self.peek()
2534 )));
2535 }
2536 self.advance();
2537 if matches!(self.peek(), Token::String(_)) {
2538 let Token::String(path) = self.advance() else { unreachable!() };
2539 let options = self.parse_copy_to_options()?;
2540 return Ok(Statement::CopyToFile {
2541 table,
2542 columns,
2543 query: None,
2544 path,
2545 options,
2546 });
2547 }
2548 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2549 return Err(self.err(format!(
2550 "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2551 self.peek()
2552 )));
2553 }
2554 self.advance();
2555 let options = self.parse_copy_to_options()?;
2556 Ok(Statement::CopyTo {
2557 table,
2558 columns,
2559 query: None,
2560 options,
2561 })
2562 }
2563 // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2564 // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2565 // result set is streamed in COPY format (PG's query form).
2566 Token::Ident(s)
2567 if s.eq_ignore_ascii_case("copy")
2568 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2569 {
2570 self.advance(); // COPY
2571 self.advance(); // (
2572 let query = self.parse_select_stmt()?;
2573 if !matches!(self.peek(), Token::RParen) {
2574 return Err(self.err(format!(
2575 "expected ')' after COPY query, got {:?}",
2576 self.peek()
2577 )));
2578 }
2579 self.advance(); // )
2580 if !matches!(self.peek(), Token::To) {
2581 return Err(self.err(format!(
2582 "COPY (query): only TO STDOUT is supported, got {:?}",
2583 self.peek()
2584 )));
2585 }
2586 self.advance();
2587 if matches!(self.peek(), Token::String(_)) {
2588 let Token::String(path) = self.advance() else { unreachable!() };
2589 let options = self.parse_copy_to_options()?;
2590 return Ok(Statement::CopyToFile {
2591 table: String::new(),
2592 columns: None,
2593 query: Some(alloc::boxed::Box::new(query)),
2594 path,
2595 options,
2596 });
2597 }
2598 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2599 return Err(self.err(format!(
2600 "COPY (query): TO supports STDOUT only, got {:?}",
2601 self.peek()
2602 )));
2603 }
2604 self.advance();
2605 let options = self.parse_copy_to_options()?;
2606 Ok(Statement::CopyTo {
2607 table: String::new(),
2608 columns: None,
2609 query: Some(alloc::boxed::Box::new(query)),
2610 options,
2611 })
2612 }
2613 // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2614 // Shares the INSERT body; the replace flag lowers it
2615 // onto ON CONFLICT DO UPDATE with an empty assignment
2616 // list (engine: replace the whole row).
2617 Token::Ident(s)
2618 if s.eq_ignore_ascii_case("replace")
2619 && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2620 {
2621 self.parse_insert_stmt(true)
2622 }
2623 Token::Begin => {
2624 self.advance();
2625 // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2626 // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2627 // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2628 // is consumed first, then the trailing modes — including the
2629 // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2630 // WORK/TRANSACTION). The explicit level, when present, rides the
2631 // statement so `exec_begin` applies it for this transaction.
2632 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2633 {
2634 self.advance();
2635 }
2636 let iso = self.parse_isolation_level_clauses()?;
2637 Ok(Statement::Begin(iso))
2638 }
2639 // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2640 // for BEGIN. START is contextual in PG too; pattern-match
2641 // on the ident here. Iso clauses are parse-and-ignored,
2642 // same as BEGIN above.
2643 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2644 self.advance();
2645 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2646 {
2647 return Err(self.err(alloc::format!(
2648 "expected TRANSACTION after START, got {:?}",
2649 self.peek()
2650 )));
2651 }
2652 self.advance();
2653 let iso = self.parse_isolation_level_clauses()?;
2654 Ok(Statement::Begin(iso))
2655 }
2656 Token::Commit => {
2657 self.advance();
2658 // PG: `COMMIT [WORK | TRANSACTION]`.
2659 if let Token::Ident(w) = self.peek()
2660 && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2661 {
2662 self.advance();
2663 }
2664 Ok(Statement::Commit)
2665 }
2666 // r1066 (7.38 S5.1) — `END [WORK | TRANSACTION]` is PG's
2667 // COMMIT synonym; pgbench's builtin tpcb-like script closes
2668 // every transaction with `END;` and the drop-in aborted on
2669 // it. Only reachable at statement start (CASE … END lives
2670 // inside expressions), so no ambiguity.
2671 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
2672 self.advance();
2673 if let Token::Ident(w) = self.peek()
2674 && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2675 {
2676 self.advance();
2677 }
2678 Ok(Statement::Commit)
2679 }
2680 Token::Rollback => {
2681 self.advance();
2682 // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2683 // savepoint without ending the transaction. Bare
2684 // `ROLLBACK` drops the whole TX.
2685 if matches!(self.peek(), Token::To) {
2686 self.advance();
2687 if matches!(self.peek(), Token::Savepoint) {
2688 self.advance();
2689 }
2690 let name = self.expect_ident_like()?;
2691 Ok(Statement::RollbackToSavepoint(name))
2692 } else {
2693 Ok(Statement::Rollback)
2694 }
2695 }
2696 Token::Savepoint => {
2697 self.advance();
2698 let name = self.expect_ident_like()?;
2699 Ok(Statement::Savepoint(name))
2700 }
2701 Token::Release => {
2702 self.advance();
2703 // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2704 // is optional in standard SQL.
2705 if matches!(self.peek(), Token::Savepoint) {
2706 self.advance();
2707 }
2708 let name = self.expect_ident_like()?;
2709 Ok(Statement::ReleaseSavepoint(name))
2710 }
2711 Token::Show => {
2712 self.advance();
2713 // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2714 // v6.1.2 promoted TABLES to a reserved keyword (for
2715 // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2716 // arrives as `Token::Tables` rather than a bare ident.
2717 // USERS / COLUMNS remain bare idents.
2718 let target = match self.advance() {
2719 Token::Tables => "tables".to_string(),
2720 // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2721 // keyword token; recognise it as the SHOW CREATE
2722 // dispatch keyword too.
2723 Token::Create => "create".to_string(),
2724 // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2725 // keyword too; let SHOW INDEX FROM parse.
2726 Token::Index => "index".to_string(),
2727 // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2728 // reserved (used in aggregate function calls);
2729 // recognise it here so the parser dispatches
2730 // to ShowParameter("all") — the engine returns
2731 // the curated parameter inventory.
2732 Token::All => "all".to_string(),
2733 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2734 other => {
2735 return Err(self.err(format!(
2736 "expected SHOW target, got {other:?}"
2737 )));
2738 }
2739 };
2740 match target.as_str() {
2741 "tables" => Ok(Statement::ShowTables),
2742 "users" => Ok(Statement::ShowUsers),
2743 // v7.38 轴 4 — `SHOW transaction_isolation`
2744 // returns the currently-selected isolation level.
2745 "transaction_isolation" => Ok(Statement::ShowParameter(
2746 "transaction_isolation".to_string(),
2747 )),
2748 // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2749 // TABLE <t>` returns a 2-column row: (Table,
2750 // Create Table). mysqldump emits this for every
2751 // table at scrape time; without it the dump
2752 // round-trip stalls.
2753 // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2754 // FROM <t>` (also spelled `SHOW INDEX` and
2755 // `SHOW KEYS`). admin / mysqldump probes use
2756 // it to list per-table indexes.
2757 "indexes" | "index" | "keys" => {
2758 if !matches!(self.peek(), Token::From) {
2759 return Err(self.err(format!(
2760 "expected FROM after SHOW INDEXES, got {:?}",
2761 self.peek()
2762 )));
2763 }
2764 self.advance();
2765 let table = self.expect_ident_like()?;
2766 Ok(Statement::ShowIndexes(table))
2767 }
2768 // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2769 // `SHOW VARIABLES`. Both return a 2-column row
2770 // set listing server-side state; clients probe
2771 // them at connect time.
2772 "status" => Ok(Statement::ShowStatus),
2773 "variables" => {
2774 // r1067 — `SHOW VARIABLES LIKE 'pat'`.
2775 if matches!(self.peek(), Token::Like) {
2776 self.advance();
2777 let pat = match self.advance() {
2778 Token::String(p) => p,
2779 other => {
2780 return Err(self.err(format!(
2781 "SHOW VARIABLES LIKE expects a quoted pattern, got {other:?}"
2782 )));
2783 }
2784 };
2785 return Ok(Statement::ShowVariablesLike(pat));
2786 }
2787 Ok(Statement::ShowVariables)
2788 }
2789 // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2790 "processlist" => Ok(Statement::ShowProcesslist),
2791 "create" => {
2792 // SHOW CREATE TABLE / VIEW / DATABASE — only
2793 // TABLE is supported in v7.17.
2794 let kind = match self.advance() {
2795 Token::Ident(s) | Token::QuotedIdent(s) => s,
2796 Token::Table => "table".to_string(),
2797 other => {
2798 return Err(self.err(format!(
2799 "expected TABLE after SHOW CREATE, got {other:?}"
2800 )));
2801 }
2802 };
2803 if !kind.eq_ignore_ascii_case("table") {
2804 return Err(self.err(format!(
2805 "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2806 )));
2807 }
2808 let name = self.expect_ident_like()?;
2809 Ok(Statement::ShowCreateTable(name))
2810 }
2811 // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2812 // (and `SHOW SCHEMAS` alias). The mysql client uses
2813 // it to populate the database selector at connect
2814 // time; without it `mysql -p` errors before the
2815 // first user query.
2816 "databases" | "schemas" => Ok(Statement::ShowDatabases),
2817 // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2818 // keyword on its own; it lands here as a bare
2819 // ident. Returning all publications + their
2820 // scope summary.
2821 "publications" => Ok(Statement::ShowPublications),
2822 // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2823 "subscriptions" => Ok(Statement::ShowSubscriptions),
2824 "columns" => {
2825 if !matches!(self.peek(), Token::From) {
2826 return Err(self.err(format!(
2827 "expected FROM after SHOW COLUMNS, got {:?}",
2828 self.peek()
2829 )));
2830 }
2831 self.advance();
2832 let table = self.expect_ident_like()?;
2833 Ok(Statement::ShowColumns(table))
2834 }
2835 // v7.38 轴 4 surface — `SHOW <param>` for any
2836 // remaining session / preset parameter name
2837 // (server_version, search_path, client_encoding,
2838 // …). The engine's ShowParameter handler does the
2839 // dispatch; unrecognised names error there with
2840 // a pointer to pg_settings, not at parse time —
2841 // so a driver that issues `SHOW spam_setting`
2842 // gets a clear runtime error instead of a
2843 // confusing "unknown SHOW target".
2844 other => {
2845 // v7.38 (read01 P3.20) — a custom namespaced GUC
2846 // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
2847 // consume the dotted tail so it round-trips with
2848 // `SET app.foo` / `current_setting('app.foo')`.
2849 let mut full = other.to_string();
2850 while matches!(self.peek(), Token::Dot) {
2851 self.advance();
2852 let seg = self.expect_ident_like()?;
2853 full.push('.');
2854 full.push_str(&seg.to_ascii_lowercase());
2855 }
2856 Ok(Statement::ShowParameter(full))
2857 }
2858 }
2859 }
2860 // v6.1.2: `DROP` is now a reserved keyword (it dispatches
2861 // to DROP USER and DROP PUBLICATION today; DROP TABLE /
2862 // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
2863 // arrived as a bare ident; tokenising it dedicatedly
2864 // keeps the dispatch tree small.
2865 Token::Drop => {
2866 self.advance();
2867 match self.peek() {
2868 // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
2869 // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
2870 // around DROP ROLE cleanup. SPG has no role-owner
2871 // model, so consume to boundary as a no-op.
2872 Token::Ident(s) | Token::QuotedIdent(s)
2873 if s.eq_ignore_ascii_case("owned") =>
2874 {
2875 // v7.39 (round 696) — still a no-op (SPG has no
2876 // role-owner model), but the ROLE is carried out so
2877 // the engine can refuse one that does not exist,
2878 // which is what PG18 does.
2879 self.advance();
2880 if self.peek_is_by() {
2881 self.advance();
2882 }
2883 let names = self.take_comma_separated_names();
2884 self.consume_until_statement_boundary();
2885 Ok(Statement::ValidateOnly {
2886 kind: crate::ast::ValidateOnlyKind::RoleName,
2887 names,
2888 })
2889 }
2890 // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
2891 // It drops only a TEMPORARY table, and name resolution
2892 // already prefers the session's own, so the keyword is
2893 // consumed and the ordinary DROP TABLE path runs.
2894 Token::Ident(s) | Token::QuotedIdent(s)
2895 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
2896 {
2897 self.advance();
2898 if !matches!(self.peek(), Token::Table) {
2899 return Err(self.err(alloc::format!(
2900 "expected TABLE after DROP TEMPORARY, got {:?}",
2901 self.peek()
2902 )));
2903 }
2904 self.parse_drop_table_after_keyword()
2905 }
2906 Token::Publication => {
2907 self.advance();
2908 // v7.39 (round 754, F31-B4) — the round-753
2909 // audit probe tripped over the missing
2910 // `IF EXISTS` here (syntax error).
2911 let if_exists = self.consume_if_exists();
2912 let name = self.expect_ident_or_string()?;
2913 Ok(Statement::DropPublication { name, if_exists })
2914 }
2915 Token::Subscription => {
2916 self.advance();
2917 let if_exists = self.consume_if_exists();
2918 let name = self.expect_ident_or_string()?;
2919 Ok(Statement::DropSubscription { name, if_exists })
2920 }
2921 Token::Ident(s) | Token::QuotedIdent(s)
2922 if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
2923 {
2924 self.advance();
2925 // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
2926 // login user IS a role in PG, and SPG's store holds
2927 // both. `IF EXISTS` is accepted on either spelling.
2928 let if_exists = self.consume_if_exists();
2929 let name = self.expect_ident_or_string()?;
2930 Ok(Statement::DropUser { name, if_exists })
2931 }
2932 // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
2933 // CREATE DATABASE has parsed since v7.14 and this did
2934 // not, so `DROP DATABASE IF EXISTS x` — what every
2935 // teardown script and pg_dumpall preamble opens with —
2936 // came back as a syntax error, which IF EXISTS cannot
2937 // soften. The name is carried so the engine can answer
2938 // the way PG does; PG never lets this succeed on a
2939 // single-database server, since the name is either
2940 // unknown ("database … does not exist", or a notice
2941 // under IF EXISTS) or the one you are connected to
2942 // ("cannot drop the currently open database").
2943 Token::Ident(s) | Token::QuotedIdent(s)
2944 if s.eq_ignore_ascii_case("database") =>
2945 {
2946 self.advance();
2947 let if_exists = self.consume_if_exists();
2948 let name = self.expect_ident_or_string()?;
2949 self.consume_until_statement_boundary();
2950 Ok(Statement::DropDatabase { name, if_exists })
2951 }
2952 // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
2953 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
2954 self.advance();
2955 let if_exists = self.consume_if_exists();
2956 let name = self.expect_ident_like()?;
2957 // ON <table>
2958 if !matches!(self.peek(), Token::On) {
2959 return Err(self.err(alloc::format!(
2960 "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
2961 self.peek()
2962 )));
2963 }
2964 self.advance();
2965 let table = self.expect_ident_like()?;
2966 Ok(Statement::DropTrigger {
2967 name,
2968 table,
2969 if_exists,
2970 })
2971 }
2972 // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
2973 // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
2974 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
2975 self.advance();
2976 let if_exists = self.consume_if_exists();
2977 let name = self.expect_ident_like()?;
2978 if !matches!(self.peek(), Token::On) {
2979 return Err(self.err(alloc::format!(
2980 "expected ON <table> after DROP RULE {name:?}, got {:?}",
2981 self.peek()
2982 )));
2983 }
2984 self.advance();
2985 let table = self.expect_ident_like()?;
2986 // Optional CASCADE / RESTRICT — accepted, no effect.
2987 self.consume_until_statement_boundary();
2988 Ok(Statement::DropRule {
2989 name,
2990 table,
2991 if_exists,
2992 })
2993 }
2994 // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
2995 // v7.12.4 ignores any optional arg-list (signature-
2996 // based overload disambiguation lands in v7.12.5+).
2997 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
2998 self.advance();
2999 let if_exists = self.consume_if_exists();
3000 let name = self.expect_ident_like()?;
3001 // v7.39 (read01 round 62) — the argument list identifies
3002 // WHICH overload to drop, so it is captured, not
3003 // discarded. `DROP FUNCTION f` (no list) is legal when
3004 // the name is unambiguous; the engine enforces that.
3005 let args = if matches!(self.peek(), Token::LParen) {
3006 Some(self.parse_function_signature_types()?)
3007 } else {
3008 None
3009 };
3010 // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
3011 // trailer, which `DROP TABLE` and `DROP INDEX` have
3012 // accepted since v7.14 and this one refused outright.
3013 // pg_dump writes it, so refusing was a parse error in
3014 // the middle of a restore. SPG drops the function
3015 // either way — it tracks no dependents to cascade to —
3016 // which is the same reading the other two give it.
3017 self.consume_drop_behaviour();
3018 Ok(Statement::DropFunction {
3019 name,
3020 args,
3021 if_exists,
3022 })
3023 }
3024 // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
3025 // [CASCADE|RESTRICT]. pg_dump and mysqldump both
3026 // emit DROP TABLE IF EXISTS at the head of every
3027 // CREATE TABLE block so re-importing a dump
3028 // overwrites prior state. SPG accepts and removes
3029 // matching tables; CASCADE/RESTRICT trailers
3030 // accepted silently.
3031 Token::Table => self.parse_drop_table_after_keyword(),
3032 // v7.14.0 — DROP INDEX [IF EXISTS] name
3033 // [CASCADE|RESTRICT]. PG / mysqldump emit this
3034 // for partial-index renames and pgvector
3035 // migrations. SPG removes the matching index;
3036 // IF EXISTS makes the drop idempotent.
3037 Token::Index => {
3038 self.advance();
3039 let if_exists = self.consume_if_exists();
3040 let name = self.expect_ident_like()?;
3041 if matches!(
3042 self.peek(),
3043 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3044 || s.eq_ignore_ascii_case("restrict")
3045 ) {
3046 self.advance();
3047 }
3048 Ok(Statement::DropIndex { name, if_exists })
3049 }
3050 // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3051 // [CASCADE|RESTRICT]. SPG is single-database;
3052 // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3053 // name [, name…] [CASCADE | RESTRICT]. Real
3054 // unregister (was silent no-op pre-v7.17).
3055 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3056 self.advance();
3057 let if_exists = self.consume_if_exists();
3058 let mut names = vec![self.expect_ident_like()?];
3059 while matches!(self.peek(), Token::Comma) {
3060 self.advance();
3061 names.push(self.expect_ident_like()?);
3062 }
3063 if matches!(
3064 self.peek(),
3065 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3066 || s.eq_ignore_ascii_case("restrict")
3067 ) {
3068 self.advance();
3069 }
3070 Ok(Statement::DropSchema { names, if_exists })
3071 }
3072 // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3073 // name [, name…] [CASCADE|RESTRICT].
3074 Token::Ident(s) | Token::QuotedIdent(s)
3075 if s.eq_ignore_ascii_case("type") =>
3076 {
3077 self.advance();
3078 let if_exists = self.consume_if_exists();
3079 let mut names = vec![self.expect_ident_like()?];
3080 while matches!(self.peek(), Token::Comma) {
3081 self.advance();
3082 names.push(self.expect_ident_like()?);
3083 }
3084 if matches!(
3085 self.peek(),
3086 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3087 || s.eq_ignore_ascii_case("restrict")
3088 ) {
3089 self.advance();
3090 }
3091 Ok(Statement::DropType { names, if_exists })
3092 }
3093 // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3094 // name [, name…] [CASCADE|RESTRICT].
3095 Token::Ident(s) | Token::QuotedIdent(s)
3096 if s.eq_ignore_ascii_case("domain") =>
3097 {
3098 self.advance();
3099 let if_exists = self.consume_if_exists();
3100 let mut names = vec![self.expect_ident_like()?];
3101 while matches!(self.peek(), Token::Comma) {
3102 self.advance();
3103 names.push(self.expect_ident_like()?);
3104 }
3105 if matches!(
3106 self.peek(),
3107 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3108 || s.eq_ignore_ascii_case("restrict")
3109 ) {
3110 self.advance();
3111 }
3112 Ok(Statement::DropDomain { names, if_exists })
3113 }
3114 // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3115 // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3116 Token::Ident(s) | Token::QuotedIdent(s)
3117 if s.eq_ignore_ascii_case("materialized") =>
3118 {
3119 self.advance();
3120 let nxt = self.peek().clone();
3121 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3122 {
3123 return Err(self.err(alloc::format!(
3124 "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3125 )));
3126 }
3127 self.advance();
3128 let if_exists = self.consume_if_exists();
3129 let mut names = vec![self.expect_ident_like()?];
3130 while matches!(self.peek(), Token::Comma) {
3131 self.advance();
3132 names.push(self.expect_ident_like()?);
3133 }
3134 if matches!(
3135 self.peek(),
3136 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3137 || s.eq_ignore_ascii_case("restrict")
3138 ) {
3139 self.advance();
3140 }
3141 Ok(Statement::DropMaterializedView { names, if_exists })
3142 }
3143 // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3144 // name [, name…] [CASCADE|RESTRICT].
3145 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3146 self.advance();
3147 let if_exists = self.consume_if_exists();
3148 let mut names = vec![self.expect_ident_like()?];
3149 while matches!(self.peek(), Token::Comma) {
3150 self.advance();
3151 names.push(self.expect_ident_like()?);
3152 }
3153 if matches!(
3154 self.peek(),
3155 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3156 || s.eq_ignore_ascii_case("restrict")
3157 ) {
3158 self.advance();
3159 }
3160 Ok(Statement::DropView { names, if_exists })
3161 }
3162 // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3163 // [CASCADE|RESTRICT]. Real removal from catalog
3164 // (was a silent no-op pre-v7.17).
3165 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3166 self.advance();
3167 let if_exists = self.consume_if_exists();
3168 let mut names = vec![self.expect_ident_like()?];
3169 while matches!(self.peek(), Token::Comma) {
3170 self.advance();
3171 names.push(self.expect_ident_like()?);
3172 }
3173 if matches!(
3174 self.peek(),
3175 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3176 || s.eq_ignore_ascii_case("restrict")
3177 ) {
3178 self.advance();
3179 }
3180 Ok(Statement::DropSequence { names, if_exists })
3181 }
3182 // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3183 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3184 self.advance();
3185 self.parse_drop_policy_after_keyword()
3186 }
3187 // v7.37.17 (17.6 siblings) — DROP <target> for
3188 // targets SPG doesn't natively track. pg_dump
3189 // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3190 // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3191 // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3192 // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3193 // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3194 // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3195 // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3196 // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3197 // etc. — accept + Empty-return so pg_dump tails
3198 // load through. Materialized-view drop dispatches
3199 // to the existing DropTable path when the token
3200 // is Materialized-View-shaped (elsewhere in
3201 // this parser).
3202 Token::Ident(s) | Token::QuotedIdent(s)
3203 if s.eq_ignore_ascii_case("text")
3204 // The DROP dispatch matches on PEEK — `text` is
3205 // not yet consumed, so SEARCH/CONFIGURATION sit
3206 // at pos+1/pos+2 (the round-695 trap's mirror).
3207 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3208 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3209 {
3210 // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3211 // validates the name; DICTIONARY / PARSER / TEMPLATE
3212 // stay in the noise arm below.
3213 self.advance(); // TEXT
3214 self.advance(); // SEARCH
3215 self.advance(); // CONFIGURATION
3216 let if_exists = self.consume_if_exists();
3217 let names = self.take_comma_separated_names();
3218 self.consume_until_statement_boundary();
3219 if if_exists {
3220 return Ok(Statement::Empty);
3221 }
3222 Ok(Statement::ValidateOnly {
3223 kind: crate::ast::ValidateOnlyKind::TsConfigName,
3224 names,
3225 })
3226 }
3227 Token::Ident(s) | Token::QuotedIdent(s)
3228 if matches!(
3229 s.to_ascii_lowercase().as_str(),
3230 "type"
3231 | "domain"
3232 | "operator"
3233 | "cast"
3234 // `text` = TEXT SEARCH DICTIONARY / PARSER /
3235 // TEMPLATE (CONFIGURATION intercepted above).
3236 | "text"
3237 | "materialized"
3238 | "large"
3239 | "role"
3240 | "access"
3241 | "procedure"
3242 | "routine"
3243 ) =>
3244 {
3245 self.consume_until_statement_boundary();
3246 Ok(Statement::Empty)
3247 }
3248 // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3249 // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3250 // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3251 // foreign-data warning family (round 706) so a
3252 // CREATE→DROP sequence in a dump stays consistent.
3253 Token::Ident(s) | Token::QuotedIdent(s)
3254 if s.eq_ignore_ascii_case("server")
3255 || s.eq_ignore_ascii_case("foreign") =>
3256 {
3257 self.advance();
3258 self.consume_until_statement_boundary();
3259 Ok(Statement::ValidateOnly {
3260 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3261 names: Vec::new(),
3262 })
3263 }
3264 Token::Ident(s) | Token::QuotedIdent(s)
3265 if s.eq_ignore_ascii_case("collation")
3266 || s.eq_ignore_ascii_case("tablespace") =>
3267 {
3268 let kind = if s.eq_ignore_ascii_case("collation") {
3269 crate::ast::ValidateOnlyKind::CollationName
3270 } else {
3271 crate::ast::ValidateOnlyKind::TablespaceName
3272 };
3273 self.advance();
3274 let if_exists = self.consume_if_exists();
3275 let names = self.take_comma_separated_names();
3276 self.consume_until_statement_boundary();
3277 if if_exists {
3278 return Ok(Statement::Empty);
3279 }
3280 Ok(Statement::ValidateOnly { kind, names })
3281 }
3282 Token::Ident(s) | Token::QuotedIdent(s)
3283 if s.eq_ignore_ascii_case("event") =>
3284 {
3285 self.advance();
3286 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3287 {
3288 self.advance();
3289 }
3290 let if_exists = self.consume_if_exists();
3291 let names = self.take_comma_separated_names();
3292 self.consume_until_statement_boundary();
3293 if if_exists {
3294 return Ok(Statement::Empty);
3295 }
3296 Ok(Statement::ValidateOnly {
3297 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3298 names,
3299 })
3300 }
3301 // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3302 // leave the noise list; see the ValidateOnly kinds.
3303 Token::Ident(s) | Token::QuotedIdent(s)
3304 if s.eq_ignore_ascii_case("conversion")
3305 || s.eq_ignore_ascii_case("language")
3306 // `DROP PROCEDURAL LANGUAGE` puts the modifier
3307 // FIRST — the first draft looked for it after.
3308 || s.eq_ignore_ascii_case("procedural") =>
3309 {
3310 let kind = if s.eq_ignore_ascii_case("conversion") {
3311 crate::ast::ValidateOnlyKind::ConversionName
3312 } else {
3313 crate::ast::ValidateOnlyKind::LanguageName
3314 };
3315 self.advance();
3316 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3317 {
3318 self.advance();
3319 }
3320 let if_exists = self.consume_if_exists();
3321 let names = self.take_comma_separated_names();
3322 self.consume_until_statement_boundary();
3323 if if_exists {
3324 return Ok(Statement::Empty);
3325 }
3326 Ok(Statement::ValidateOnly { kind, names })
3327 }
3328 // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3329 // name(argtypes)[, …]`. Parsed for real so the engine
3330 // can answer as PG does; see Statement::DropAggregate.
3331 Token::Ident(s) | Token::QuotedIdent(s)
3332 if s.eq_ignore_ascii_case("aggregate") =>
3333 {
3334 self.advance();
3335 let if_exists = self.consume_if_exists();
3336 let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3337 loop {
3338 let name = self.expect_ident_like()?;
3339 if !matches!(self.peek(), Token::LParen) {
3340 return Err(self.err(alloc::format!(
3341 "expected argument list after DROP AGGREGATE {name}"
3342 )));
3343 }
3344 self.advance();
3345 let mut args: Vec<String> = Vec::new();
3346 let mut star = false;
3347 loop {
3348 match self.peek().clone() {
3349 Token::RParen => {
3350 self.advance();
3351 break;
3352 }
3353 Token::Star => {
3354 self.advance();
3355 star = true;
3356 }
3357 Token::Comma => {
3358 self.advance();
3359 }
3360 _ => {
3361 // A type name may be multi-token
3362 // (`double precision`); glue idents
3363 // until , or ).
3364 let mut t = self.expect_ident_like()?;
3365 while let Token::Ident(nx) = self.peek() {
3366 let nx = nx.clone();
3367 self.advance();
3368 t.push(' ');
3369 t.push_str(&nx);
3370 }
3371 args.push(t);
3372 }
3373 }
3374 }
3375 items.push((name, if star { None } else { Some(args) }));
3376 if matches!(self.peek(), Token::Comma) {
3377 self.advance();
3378 } else {
3379 break;
3380 }
3381 }
3382 self.consume_until_statement_boundary();
3383 Ok(Statement::DropAggregate { if_exists, items })
3384 }
3385 // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3386 // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3387 // installed; `IF EXISTS` is the spelling that says do
3388 // not, and it keeps the no-op.
3389 Token::Ident(s) | Token::QuotedIdent(s)
3390 if s.eq_ignore_ascii_case("extension") =>
3391 {
3392 self.advance();
3393 let if_exists = self.consume_if_exists();
3394 let names = self.take_comma_separated_names();
3395 self.consume_until_statement_boundary();
3396 if if_exists {
3397 return Ok(Statement::Empty);
3398 }
3399 Ok(Statement::ValidateOnly {
3400 kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3401 names,
3402 })
3403 }
3404 Token::Ident(s) | Token::QuotedIdent(s)
3405 if s.eq_ignore_ascii_case("statistics") =>
3406 {
3407 self.parse_drop_statistics_after_drop()
3408 }
3409 other => Err(self.err(format!(
3410 "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3411 SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3412 ))),
3413 }
3414 }
3415 // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3416 // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3417 // and accepted before the view name. SPG materialised
3418 // views re-evaluate on read (always-fresh semantics), so
3419 // the CONCURRENTLY-vs-serial distinction has no runtime
3420 // effect — the refresh body does not block readers either
3421 // way. Same accept-and-no-op pattern as DETACH PARTITION
3422 // CONCURRENTLY (16.5).
3423 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3424 self.advance();
3425 let nxt = self.peek().clone();
3426 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3427 {
3428 return Err(self.err(alloc::format!(
3429 "expected MATERIALIZED after REFRESH, got {nxt:?}"
3430 )));
3431 }
3432 self.advance();
3433 let nxt2 = self.peek().clone();
3434 if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3435 {
3436 return Err(self.err(alloc::format!(
3437 "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3438 )));
3439 }
3440 self.advance();
3441 // Optional CONCURRENTLY noise word — consumed without
3442 // changing semantics.
3443 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3444 {
3445 self.advance();
3446 }
3447 let name = self.expect_ident_like()?;
3448 let with_data = self.parse_optional_with_data(true)?;
3449 Ok(Statement::RefreshMaterializedView { name, with_data })
3450 }
3451 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3452 self.advance();
3453 self.parse_update_after_keyword()
3454 }
3455 // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3456 // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3457 // [CASCADE | RESTRICT]. Clears every row from each named
3458 // table. Parses at the top level; the engine dispatcher
3459 // walks Statement::Truncate.
3460 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3461 self.advance();
3462 // Optional TABLE noise word — PG accepts both the reserved
3463 // token and the bare identifier spelling.
3464 if matches!(self.peek(), Token::Table)
3465 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3466 {
3467 self.advance();
3468 }
3469 // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3470 // not absorbed. The lookahead keeps a table genuinely
3471 // called `only` working: the keyword is a keyword only
3472 // when a name follows it.
3473 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3474 if s.eq_ignore_ascii_case("only"))
3475 && matches!(
3476 self.tokens.get(self.pos + 1),
3477 Some(Token::Ident(_) | Token::QuotedIdent(_))
3478 );
3479 if only {
3480 self.advance();
3481 }
3482 // Table names (comma-separated).
3483 let mut tables = Vec::new();
3484 loop {
3485 tables.push(self.expect_ident_like()?);
3486 if matches!(self.peek(), Token::Comma) {
3487 self.advance();
3488 continue;
3489 }
3490 break;
3491 }
3492 // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3493 let mut restart_identity = false;
3494 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3495 {
3496 self.advance();
3497 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3498 {
3499 self.advance();
3500 restart_identity = true;
3501 }
3502 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3503 {
3504 self.advance();
3505 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3506 {
3507 self.advance();
3508 }
3509 }
3510 // Optional CASCADE / RESTRICT.
3511 let mut cascade = false;
3512 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3513 {
3514 self.advance();
3515 cascade = true;
3516 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3517 {
3518 self.advance();
3519 }
3520 Ok(Statement::Truncate {
3521 tables,
3522 restart_identity,
3523 cascade,
3524 only,
3525 })
3526 }
3527 // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3528 // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3529 // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3530 // rows change so the index tree is always up-to-date;
3531 // REINDEX is a strict no-op. Accept the whole statement
3532 // shape to boundary for pg_dump round-trip compatibility.
3533 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3534 // v7.39 (round 535) — the target is CARRIED now. SPG has no
3535 // index bloat to rebuild, so the work stays a no-op, but PG
3536 // validates what it was pointed at and this swallowed the
3537 // name at parse time — `REINDEX TABLE typo` reported
3538 // success. Measured on PG18: INDEX / TABLE name a relation,
3539 // SCHEMA a schema, SYSTEM nothing.
3540 self.advance();
3541 self.parse_reindex_tail()
3542 }
3543 // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3544 // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3545 // SPG has no MVCC bloat today (Phase D visibility map
3546 // queues with v7.38); the freezer collapses hot-tier
3547 // rows into cold segments automatically. VACUUM is a
3548 // no-op — pg_dump maintenance scripts and Discourse's
3549 // periodic-maintenance path both emit it.
3550 // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3551 // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3552 // actual bloat, so the pre-MVCC accept-and-ignore posture
3553 // became a silent no-op on a customer's manual reclaim.
3554 // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3555 // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3556 // ANALYZE is captured, the optional table name is captured.
3557 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3558 self.advance();
3559 // Parenthesised option list: absorb it.
3560 if matches!(self.peek(), Token::LParen) {
3561 let mut depth = 0usize;
3562 loop {
3563 match self.advance() {
3564 Token::LParen => depth += 1,
3565 Token::RParen => {
3566 depth -= 1;
3567 if depth == 0 {
3568 break;
3569 }
3570 }
3571 Token::Eof => break,
3572 _ => {}
3573 }
3574 }
3575 }
3576 let mut analyze = false;
3577 let mut table: Option<String> = None;
3578 loop {
3579 match self.peek() {
3580 // v7.39 (round 535) — `FULL` lexes as a keyword, not
3581 // an identifier, so the loop below broke out on it and
3582 // dropped the table name: `VACUUM FULL nosuch` was
3583 // accepted where `VACUUM nosuch` was refused.
3584 Token::Full => {
3585 self.advance();
3586 }
3587 Token::Ident(w) | Token::QuotedIdent(w) => {
3588 let wl = w.to_ascii_lowercase();
3589 match wl.as_str() {
3590 "full" | "freeze" | "verbose" => {
3591 self.advance();
3592 }
3593 "analyze" | "analyse" => {
3594 analyze = true;
3595 self.advance();
3596 }
3597 _ => {
3598 table = Some(self.expect_ident_like()?);
3599 break;
3600 }
3601 }
3602 }
3603 _ => break,
3604 }
3605 }
3606 // Optional trailing column list / anything else to the
3607 // statement boundary (PG accepts per-column ANALYZE).
3608 self.consume_until_statement_boundary();
3609 Ok(Statement::Vacuum { table, analyze })
3610 }
3611 // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3612 // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3613 // <index>. PG stores rows in physical order matching
3614 // an index; SPG's hot-tier is append-only + cold-tier
3615 // is segment-frozen, so clustering has no persistent
3616 // effect. Accept-and-no-op for pg_dump compat.
3617 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3618 // v7.39 (round 535) — same as REINDEX above: the relation is
3619 // carried so the engine can refuse one that does not exist.
3620 // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3621 self.advance();
3622 self.parse_cluster_tail()
3623 }
3624 // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3625 // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3626 // optional string payload; UNLISTEN takes a channel or `*`.
3627 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3628 self.advance();
3629 let ch = match self.advance() {
3630 Token::Ident(c) | Token::QuotedIdent(c) => c,
3631 other => {
3632 return Err(self.err(format!(
3633 "expected channel name after LISTEN, got {other:?}"
3634 )));
3635 }
3636 };
3637 Ok(Statement::Listen(ch))
3638 }
3639 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3640 self.advance();
3641 let channel = match self.advance() {
3642 Token::Ident(c) | Token::QuotedIdent(c) => c,
3643 other => {
3644 return Err(self.err(format!(
3645 "expected channel name after NOTIFY, got {other:?}"
3646 )));
3647 }
3648 };
3649 let payload = if matches!(self.peek(), Token::Comma) {
3650 self.advance();
3651 match self.advance() {
3652 Token::String(p) => Some(p),
3653 other => {
3654 return Err(self.err(format!(
3655 "expected string payload after NOTIFY <channel>, got {other:?}"
3656 )));
3657 }
3658 }
3659 } else {
3660 None
3661 };
3662 Ok(Statement::Notify { channel, payload })
3663 }
3664 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3665 self.advance();
3666 match self.advance() {
3667 Token::Star => Ok(Statement::Unlisten(None)),
3668 Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3669 other => Err(self.err(format!(
3670 "expected channel name or * after UNLISTEN, got {other:?}"
3671 ))),
3672 }
3673 }
3674 // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3675 // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3676 // process-wide write lock today; explicit LOCK has no
3677 // effect. Accept-and-no-op for pg_dump / migration
3678 // compat.
3679 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3680 self.advance();
3681 // v7.39 (round 696) — the LOCK still has no effect (SPG's
3682 // engine holds a process-wide write lock), but the TABLE
3683 // NAME is now carried out so the engine can refuse one that
3684 // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3685 // READ|WRITE` is a different statement with the same first
3686 // word; it keeps the old no-op, because a MySQL dump's
3687 // bracket names tables it is about to create.
3688 let mysql_tables = matches!(self.peek(), Token::Ident(k)
3689 if k.eq_ignore_ascii_case("tables"));
3690 if mysql_tables {
3691 self.consume_until_statement_boundary();
3692 return Ok(Statement::Empty);
3693 }
3694 if matches!(self.peek(), Token::Table) {
3695 self.advance();
3696 }
3697 let names = self.take_comma_separated_names();
3698 self.consume_until_statement_boundary();
3699 Ok(Statement::ValidateOnly {
3700 kind: crate::ast::ValidateOnlyKind::LockTable,
3701 names,
3702 })
3703 }
3704 // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3705 // durability marker + snapshot in PG. SPG has WAL
3706 // checkpointing on a byte / time schedule (v7.37.10
3707 // 60s / 4 MiB defaults). The bare statement parses to
3708 // `Statement::Empty` here (the no_std engine owns no
3709 // WAL / snapshot); v7.37 Epic Du wires the HOST
3710 // (embedded `Database::execute_buffered`, via
3711 // `sql_is_checkpoint`) to force an immediate synchronous
3712 // checkpoint through `Database::checkpoint` — a real
3713 // durability barrier, matching PG.
3714 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3715 self.advance();
3716 self.consume_until_statement_boundary();
3717 Ok(Statement::Empty)
3718 }
3719 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3720 self.advance();
3721 self.parse_delete_after_keyword()
3722 }
3723 // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3724 // ALTER is not a reserved keyword in the lexer — handled
3725 // as a bare ident here.
3726 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3727 self.advance();
3728 self.parse_alter_after_keyword()
3729 }
3730 // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3731 // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3732 // additions needed.
3733 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3734 self.advance();
3735 self.parse_wait_after_keyword()
3736 }
3737 // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3738 // Bare ANALYZE → analyse every user table; ANALYZE
3739 // <name> → re-stats one. The argument is an optional
3740 // ident (or quoted ident); anything else is a parse
3741 // error.
3742 // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3743 // `WHERE` filter (carved out per V6_7_DESIGN.md
3744 // STABILITY). Lex order: identifier "compact" → "cold"
3745 // → "segments". Anything else after `COMPACT` is a
3746 // parse error.
3747 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3748 self.advance();
3749 let next = self.peek().clone();
3750 let cold = match next {
3751 Token::Ident(s) | Token::QuotedIdent(s) => s,
3752 _ => {
3753 return Err(
3754 self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3755 );
3756 }
3757 };
3758 if !cold.eq_ignore_ascii_case("cold") {
3759 return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3760 }
3761 self.advance();
3762 let next = self.peek().clone();
3763 let segments = match next {
3764 Token::Ident(s) | Token::QuotedIdent(s) => s,
3765 _ => {
3766 return Err(self.err(format!(
3767 "expected SEGMENTS after COMPACT COLD, got {:?}",
3768 self.peek()
3769 )));
3770 }
3771 };
3772 if !segments.eq_ignore_ascii_case("segments") {
3773 return Err(self.err(format!(
3774 "expected SEGMENTS after COMPACT COLD, got {segments:?}"
3775 )));
3776 }
3777 self.advance();
3778 Ok(Statement::CompactColdSegments)
3779 }
3780 // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
3781 // Parsed as a case-insensitive identifier since MERGE
3782 // isn't a reserved lexer keyword (collides with the
3783 // mysqldump `ALGORITHM = MERGE` view clause if it
3784 // were); the inner parser drives the rest of the
3785 // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
3786 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
3787 self.advance();
3788 self.parse_merge_after_keyword()
3789 }
3790 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
3791 self.advance();
3792 let target = match self.peek() {
3793 Token::Eof | Token::Semicolon => None,
3794 Token::Ident(_) | Token::QuotedIdent(_) => {
3795 Some(self.expect_ident_like()?)
3796 }
3797 other => {
3798 return Err(self.err(format!(
3799 "expected table name or end of statement after ANALYZE, got {other:?}"
3800 )));
3801 }
3802 };
3803 // v7.39 (round 776, F31 J7) — the per-column form
3804 // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
3805 // here while the VACUUM arm already consumed it; SPG
3806 // analyzes whole tables, so the list parses and is
3807 // accepted like the VACUUM path's.
3808 if target.is_some() && matches!(self.peek(), Token::LParen) {
3809 self.advance();
3810 loop {
3811 let _ = self.expect_ident_like()?;
3812 match self.peek() {
3813 Token::Comma => {
3814 self.advance();
3815 }
3816 Token::RParen => {
3817 self.advance();
3818 break;
3819 }
3820 other => {
3821 return Err(self.err(format!(
3822 "expected ',' or ')' in ANALYZE column list, got {other:?}"
3823 )));
3824 }
3825 }
3826 }
3827 }
3828 Ok(Statement::Analyze(target))
3829 }
3830 // v7.12.1 — `SET <name> [TO|=] <value>`. The
3831 // `default_text_search_config` parameter is consumed
3832 // by the FTS function dispatcher; other parameter
3833 // names are recorded but treated as a no-op so PG
3834 // dump output loads.
3835 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
3836 self.advance();
3837 // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
3838 // adds `SET GLOBAL` too (and the alias `SET @@global.name =
3839 // …` which the SessionVar path handles). `LOCAL` is the only
3840 // one that changes semantics — it scopes the change to the
3841 // current transaction — so capture it; SESSION / GLOBAL are
3842 // accepted and treated as the default session scope.
3843 let mut set_local = false;
3844 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
3845 let q = s.to_ascii_lowercase();
3846 if q == "local" || q == "session" || q == "global" {
3847 set_local = q == "local";
3848 self.advance();
3849 }
3850 }
3851 // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
3852 // <collation>]` — change the connection client
3853 // charset. SPG stores UTF-8 always and orders
3854 // bytewise; accept as a no-op.
3855 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
3856 {
3857 self.advance();
3858 // Charset ident-or-string.
3859 if matches!(
3860 self.peek(),
3861 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3862 ) {
3863 self.advance();
3864 }
3865 // Optional `COLLATE <name>`.
3866 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
3867 {
3868 self.advance();
3869 if matches!(
3870 self.peek(),
3871 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3872 ) {
3873 self.advance();
3874 }
3875 }
3876 return Ok(Statement::Empty);
3877 }
3878 // v7.37.17 (17.6 sibling) — PG `SET ROLE
3879 // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
3880 // uses this to switch to the object owner before
3881 // recreating tables. SPG has no role system so this
3882 // is a no-op.
3883 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
3884 {
3885 self.advance(); // ROLE
3886 // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
3887 // reset to the login identity; a name / string sets the
3888 // effective role that drives current_user + RLS.
3889 let role = match self.peek().clone() {
3890 Token::Default => {
3891 self.advance();
3892 None
3893 }
3894 Token::Ident(s) | Token::QuotedIdent(s)
3895 if s.eq_ignore_ascii_case("none") =>
3896 {
3897 self.advance();
3898 None
3899 }
3900 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
3901 self.advance();
3902 Some(s)
3903 }
3904 _ => None,
3905 };
3906 return Ok(Statement::SetRole(role));
3907 }
3908 // v7.37.17 (17.6 sibling) — PG `SET SESSION
3909 // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
3910 // ISO SQL surface). pg_dump prepends this to fix
3911 // the isolation level for the restore session. SPG
3912 // defaults to READ COMMITTED and doesn't yet honor
3913 // session-set isolation across statements — accept
3914 // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
3915 // per-tx form is handled elsewhere.
3916 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
3917 {
3918 self.advance(); // CHARACTERISTICS
3919 self.consume_until_statement_boundary();
3920 return Ok(Statement::Empty);
3921 }
3922 // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
3923 // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
3924 // pg_dump emits this to control the deferrability of
3925 // FK / UNIQUE constraints across a bulk restore. SPG
3926 // has no deferrable-constraint machinery today; the
3927 // FK checker is strict-immediate. Accept-and-no-op
3928 // for pg_dump round-trip compatibility.
3929 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
3930 {
3931 self.advance(); // CONSTRAINTS
3932 // v7.39 (round 288) — no longer a no-op: the trailing
3933 // DEFERRED / IMMEDIATE sets the transaction's timing.
3934 // v7.39 (round 308, V29) — and the names are kept.
3935 // They used to be skipped over on the way to the
3936 // DEFERRED keyword, so a named form silently behaved
3937 // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
3938 // every deferrable constraint in the transaction.
3939 let mut names: alloc::vec::Vec<alloc::string::String> =
3940 alloc::vec::Vec::new();
3941 if matches!(self.peek(), Token::All) {
3942 self.advance();
3943 } else {
3944 loop {
3945 let mut n = self.expect_ident_like()?;
3946 // A schema-qualified name (`public.fk_a`)
3947 // identifies the same constraint; PG resolves
3948 // it by the trailing segment.
3949 while matches!(self.peek(), Token::Dot) {
3950 self.advance();
3951 n = self.expect_ident_like()?;
3952 }
3953 names.push(n);
3954 if matches!(self.peek(), Token::Comma) {
3955 self.advance();
3956 } else {
3957 break;
3958 }
3959 }
3960 }
3961 let deferred = match self.peek() {
3962 Token::Ident(s) | Token::QuotedIdent(s)
3963 if s.eq_ignore_ascii_case("deferred") =>
3964 {
3965 true
3966 }
3967 Token::Ident(s) | Token::QuotedIdent(s)
3968 if s.eq_ignore_ascii_case("immediate") =>
3969 {
3970 false
3971 }
3972 other => {
3973 return Err(self.err(alloc::format!(
3974 "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
3975 )));
3976 }
3977 };
3978 self.advance();
3979 return Ok(Statement::SetConstraints { names, deferred });
3980 }
3981 // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
3982 // { DEFAULT | '<role>' | <ident> }` (mailrs
3983 // round-10 A.1). pg_dump preamble emits the
3984 // `DEFAULT` form to reset session authorization.
3985 //
3986 // v7.39 (round 697) — this said "SPG has no role system so
3987 // this is a strict no-op". SPG has had one since round 58;
3988 // the comment outlived it, and with it the reason a name
3989 // that is not a role was accepted here. It still switches
3990 // no authorization — what it does now is refuse a role
3991 // that does not exist, as PG18 does. PG also accepts `RESET SESSION
3992 // AUTHORIZATION` (handled by the RESET parser
3993 // elsewhere). Reference:
3994 // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
3995 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
3996 {
3997 self.advance(); // AUTHORIZATION
3998 match self.peek().clone() {
3999 Token::Default => {
4000 self.advance();
4001 }
4002 Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4003 self.advance();
4004 return Ok(Statement::ValidateOnly {
4005 kind: crate::ast::ValidateOnlyKind::RoleName,
4006 names: alloc::vec![r],
4007 });
4008 }
4009 other => {
4010 return Err(self.err(alloc::format!(
4011 "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4012 )));
4013 }
4014 }
4015 return Ok(Statement::Empty);
4016 }
4017 // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4018 // ISOLATION LEVEL { READ COMMITTED | READ
4019 // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4020 // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4021 // PG-standard surface. v7.37.8 accepts the syntax
4022 // and tracks the selected level on
4023 // `Engine::current_isolation_level()`; the actual
4024 // MVCC / SSI semantics implementation lands in
4025 // the 轴 4 isolation framework (separate train).
4026 // PG itself maps READ UNCOMMITTED to READ COMMITTED
4027 // internally; SPG behaves the same (effectively
4028 // READ COMMITTED at every level today).
4029 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4030 {
4031 self.advance(); // TRANSACTION
4032 let level = self.parse_isolation_level_clauses()?.unwrap_or_default();
4033 return Ok(Statement::SetTransaction { isolation: level });
4034 }
4035 // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4036 // alias — same accept-as-no-op as SET NAMES.
4037 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4038 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4039 {
4040 self.advance(); // CHARACTER
4041 self.advance(); // SET
4042 if matches!(
4043 self.peek(),
4044 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4045 ) {
4046 self.advance();
4047 }
4048 return Ok(Statement::Empty);
4049 }
4050 // v7.39 (GUC) — PG spells the timezone GUC as two
4051 // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4052 // where <value> is a string/ident or the LOCAL /
4053 // DEFAULT keyword (both mean "back to the default").
4054 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4055 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4056 {
4057 self.advance(); // TIME
4058 self.advance(); // ZONE
4059 let value = match self.peek().clone() {
4060 Token::Ident(s)
4061 if s.eq_ignore_ascii_case("local")
4062 || s.eq_ignore_ascii_case("default") =>
4063 {
4064 self.advance();
4065 crate::ast::SetValue::Default
4066 }
4067 Token::Default => {
4068 self.advance();
4069 crate::ast::SetValue::Default
4070 }
4071 _ => self.parse_set_value()?,
4072 };
4073 return Ok(Statement::SetParameter {
4074 name: "timezone".into(),
4075 value,
4076 local: set_local,
4077 });
4078 }
4079 // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4080 // MySQL USER-variable assignment: its own per-session
4081 // namespace, an arbitrary expression on the right, and `:=`
4082 // as a second spelling of `=`. It used to fall into the
4083 // session-PARAMETER list below, whose values are literals and
4084 // whose store nothing reads back under a `@` name — so the
4085 // assignment reported success and vanished.
4086 //
4087 // A `@@`-prefixed LHS is a real engine setting and keeps the
4088 // old path.
4089 if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4090 return self.parse_set_user_vars();
4091 }
4092 // v7.14.0 — multi-assignment form
4093 // `SET a = 1, b = 2, …`. Single-assignment is the
4094 // 1-element case. Each LHS may be a regular ident
4095 // or a SessionVar (`@VAR` / `@@VAR`).
4096 let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4097 loop {
4098 let lhs = match self.peek().clone() {
4099 Token::SessionVar(s) => {
4100 self.advance();
4101 s
4102 }
4103 Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4104 other => {
4105 return Err(self.err(format!(
4106 "expected parameter name after SET, got {other:?}"
4107 )));
4108 }
4109 };
4110 // Accept either `=` or the bare `TO` keyword.
4111 match self.peek() {
4112 Token::Eq => {
4113 self.advance();
4114 }
4115 Token::To => {
4116 self.advance();
4117 }
4118 other => {
4119 return Err(self.err(format!(
4120 "expected `=` or TO after SET {lhs}, got {other:?}"
4121 )));
4122 }
4123 }
4124 let mut value = self.parse_set_value()?;
4125 // v7.39 (GUC) — disambiguate the comma: `, name =` /
4126 // `, name TO` continues a MySQL-style multi-assign,
4127 // anything else is a PG list VALUE
4128 // (`SET search_path = myschema, public`) folded into
4129 // one comma-joined string.
4130 while matches!(self.peek(), Token::Comma) {
4131 let is_assign = matches!(
4132 self.tokens.get(self.pos + 1),
4133 Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4134 ) && matches!(
4135 self.tokens.get(self.pos + 2),
4136 Some(Token::Eq | Token::To)
4137 );
4138 if is_assign {
4139 break;
4140 }
4141 self.advance(); // comma
4142 let next = self.parse_set_value()?;
4143 let joined = alloc::format!(
4144 "{}, {}",
4145 set_value_text(&value),
4146 set_value_text(&next)
4147 );
4148 value = crate::ast::SetValue::String(joined);
4149 }
4150 pairs.push((lhs, value));
4151 if matches!(self.peek(), Token::Comma) {
4152 self.advance();
4153 continue;
4154 }
4155 break;
4156 }
4157 if pairs.len() == 1 {
4158 let (name, value) = pairs.into_iter().next().unwrap();
4159 Ok(Statement::SetParameter {
4160 name,
4161 value,
4162 local: set_local,
4163 })
4164 } else {
4165 Ok(Statement::SetParameterList(pairs))
4166 }
4167 }
4168 // v7.12.1 — `RESET <name>` / `RESET ALL`.
4169 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4170 self.advance();
4171 match self.peek().clone() {
4172 Token::All => {
4173 self.advance();
4174 Ok(Statement::ResetParameter(None))
4175 }
4176 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4177 self.advance();
4178 Ok(Statement::ResetParameter(None))
4179 }
4180 // v7.39 (RLS) — `RESET ROLE` clears the session role.
4181 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4182 self.advance();
4183 Ok(Statement::SetRole(None))
4184 }
4185 _ => {
4186 let name = self.parse_set_param_name()?;
4187 Ok(Statement::ResetParameter(Some(name)))
4188 }
4189 }
4190 }
4191 // v7.39 (round 218) — server-side cursors.
4192 Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4193 Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4194 Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4195 Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4196 self.advance();
4197 match self.peek().clone() {
4198 Token::All => {
4199 self.advance();
4200 Ok(Statement::CloseCursor { name: None })
4201 }
4202 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4203 self.advance();
4204 Ok(Statement::CloseCursor { name: None })
4205 }
4206 Token::Ident(n) | Token::QuotedIdent(n) => {
4207 self.advance();
4208 Ok(Statement::CloseCursor { name: Some(n) })
4209 }
4210 other => Err(self.err(format!(
4211 "expected cursor name or ALL after CLOSE, got {other:?}"
4212 ))),
4213 }
4214 }
4215 other => Err(self.err(format!(
4216 "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4217 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4218 ))),
4219 }
4220 }
4221
4222 /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4223 /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4224 /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4225 /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4226 fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4227 self.advance(); // DECLARE
4228 let name = match self.advance() {
4229 Token::Ident(n) | Token::QuotedIdent(n) => n,
4230 other => {
4231 return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4232 }
4233 };
4234 let mut scroll: Option<bool> = None;
4235 loop {
4236 match self.peek() {
4237 Token::Ident(s)
4238 if s.eq_ignore_ascii_case("binary")
4239 || s.eq_ignore_ascii_case("insensitive")
4240 || s.eq_ignore_ascii_case("asensitive") =>
4241 {
4242 self.advance();
4243 }
4244 Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4245 self.advance();
4246 scroll = Some(true);
4247 }
4248 Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4249 {
4250 self.advance(); // NO
4251 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4252 return Err(self.err(format!(
4253 "expected SCROLL after NO in DECLARE, got {:?}",
4254 self.peek()
4255 )));
4256 }
4257 self.advance();
4258 scroll = Some(false);
4259 }
4260 _ => break,
4261 }
4262 }
4263 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4264 return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4265 }
4266 self.advance();
4267 let mut hold = false;
4268 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4269 self.advance();
4270 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4271 return Err(self.err(format!(
4272 "expected HOLD after WITH in DECLARE, got {:?}",
4273 self.peek()
4274 )));
4275 }
4276 self.advance();
4277 hold = true;
4278 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4279 self.advance();
4280 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4281 return Err(self.err(format!(
4282 "expected HOLD after WITHOUT in DECLARE, got {:?}",
4283 self.peek()
4284 )));
4285 }
4286 self.advance();
4287 }
4288 if !matches!(self.peek(), Token::For) {
4289 return Err(self.err(format!(
4290 "expected FOR before the cursor query, got {:?}",
4291 self.peek()
4292 )));
4293 }
4294 self.advance();
4295 let query = self.parse_one_statement()?;
4296 Ok(Statement::DeclareCursor {
4297 name,
4298 scroll,
4299 hold,
4300 query: alloc::boxed::Box::new(query),
4301 })
4302 }
4303
4304 /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4305 /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4306 /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4307 fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4308 use crate::ast::CursorDirection as D;
4309 self.advance(); // FETCH / MOVE
4310 let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4311 let neg = if matches!(this.peek(), Token::Minus) {
4312 this.advance();
4313 true
4314 } else {
4315 false
4316 };
4317 match this.advance() {
4318 Token::Integer(v) => Ok(if neg { -v } else { v }),
4319 other => Err(this.err(format!("expected count, got {other:?}"))),
4320 }
4321 };
4322 let direction = match self.peek().clone() {
4323 Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4324 self.advance();
4325 D::Next
4326 }
4327 Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4328 self.advance();
4329 D::Prior
4330 }
4331 Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4332 self.advance();
4333 D::First
4334 }
4335 Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4336 self.advance();
4337 D::Last
4338 }
4339 Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4340 self.advance();
4341 D::Absolute(signed_count(self)?)
4342 }
4343 Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4344 self.advance();
4345 D::Relative(signed_count(self)?)
4346 }
4347 Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4348 self.advance();
4349 match self.peek().clone() {
4350 Token::All => {
4351 self.advance();
4352 D::All
4353 }
4354 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4355 self.advance();
4356 D::All
4357 }
4358 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4359 _ => D::Next, // bare FORWARD = FORWARD 1
4360 }
4361 }
4362 Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4363 self.advance();
4364 match self.peek().clone() {
4365 Token::All => {
4366 self.advance();
4367 D::BackwardAll
4368 }
4369 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4370 self.advance();
4371 D::BackwardAll
4372 }
4373 Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4374 _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4375 }
4376 }
4377 Token::All => {
4378 self.advance();
4379 D::All
4380 }
4381 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4382 self.advance();
4383 D::All
4384 }
4385 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4386 // Bare `FETCH <name>` — direction defaults to NEXT.
4387 _ => D::Next,
4388 };
4389 // Optional FROM / IN.
4390 if matches!(self.peek(), Token::From)
4391 || matches!(self.peek(), Token::In)
4392 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4393 {
4394 self.advance();
4395 }
4396 let name = match self.advance() {
4397 Token::Ident(n) | Token::QuotedIdent(n) => n,
4398 other => {
4399 return Err(self.err(format!("expected cursor name, got {other:?}")));
4400 }
4401 };
4402 Ok(if is_move {
4403 Statement::MoveCursor { name, direction }
4404 } else {
4405 Statement::FetchCursor { name, direction }
4406 })
4407 }
4408
4409 /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4410 /// [(kind, …)] ON <col>, … FROM <table>`.
4411 fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4412 self.advance(); // STATISTICS
4413 // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4414 let mut if_not_exists = false;
4415 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4416 && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4417 {
4418 self.advance();
4419 self.advance();
4420 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4421 self.advance();
4422 if_not_exists = true;
4423 }
4424 }
4425 let name = self.expect_ident_like()?;
4426 let mut kinds = Vec::new();
4427 if matches!(self.peek(), Token::LParen) {
4428 self.advance();
4429 loop {
4430 let k = self.expect_ident_like()?;
4431 // PG stores the single letters; accept the spelled-out
4432 // names the SQL uses and record what PG records.
4433 kinds.push(match k.to_ascii_lowercase().as_str() {
4434 "ndistinct" => String::from("d"),
4435 "dependencies" => String::from("f"),
4436 "mcv" => String::from("m"),
4437 other => {
4438 return Err(
4439 self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4440 );
4441 }
4442 });
4443 match self.advance() {
4444 Token::Comma => {}
4445 Token::RParen => break,
4446 other => {
4447 return Err(self.err(alloc::format!(
4448 "expected ',' or ')' in statistics kind list, got {other:?}"
4449 )));
4450 }
4451 }
4452 }
4453 }
4454 if !matches!(self.peek(), Token::On) {
4455 return Err(self.err(alloc::format!(
4456 "expected ON in CREATE STATISTICS, got {:?}",
4457 self.peek()
4458 )));
4459 }
4460 self.advance();
4461 let mut columns = Vec::new();
4462 loop {
4463 columns.push(self.expect_ident_like()?);
4464 if matches!(self.peek(), Token::Comma) {
4465 self.advance();
4466 } else {
4467 break;
4468 }
4469 }
4470 if !matches!(self.peek(), Token::From) {
4471 return Err(self.err(alloc::format!(
4472 "expected FROM in CREATE STATISTICS, got {:?}",
4473 self.peek()
4474 )));
4475 }
4476 self.advance();
4477 let table = self.expect_ident_like()?;
4478 Ok(Statement::CreateStatistics {
4479 name,
4480 if_not_exists,
4481 kinds,
4482 columns,
4483 table,
4484 })
4485 }
4486
4487 /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4488 /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4489 /// entered with the `TABLE` keyword still unconsumed. Extracted so
4490 /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4491 /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4492 /// forward call.
4493 fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4494 self.advance(); // TABLE
4495 let if_exists = self.consume_if_exists();
4496 let mut names: Vec<String> = Vec::new();
4497 loop {
4498 names.push(self.expect_ident_like()?);
4499 if matches!(self.peek(), Token::Comma) {
4500 self.advance();
4501 continue;
4502 }
4503 break;
4504 }
4505 if matches!(
4506 self.peek(),
4507 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4508 || s.eq_ignore_ascii_case("restrict")
4509 ) {
4510 self.advance();
4511 }
4512 Ok(Statement::DropTable { names, if_exists })
4513 }
4514
4515 fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4516 self.advance(); // STATISTICS
4517 let mut if_exists = false;
4518 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4519 && matches!(self.tokens.get(self.pos + 1),
4520 Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4521 {
4522 self.advance();
4523 self.advance();
4524 if_exists = true;
4525 }
4526 let name = self.expect_ident_like()?;
4527 Ok(Statement::DropStatistics { name, if_exists })
4528 }
4529
4530 fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4531 debug_assert!(matches!(self.peek(), Token::Create));
4532 self.advance();
4533 match self.peek() {
4534 Token::Table => self.parse_create_table_stmt_after_create(),
4535 Token::Index => self.parse_create_index_stmt_after_create(false),
4536 // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4537 // object now. It used to be consumed by the CREATE-noise
4538 // arm, so a pg_dump that declares extended statistics
4539 // restored silently without them and reflection showed
4540 // nothing.
4541 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4542 self.parse_create_statistics_after_create()
4543 }
4544 // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4545 // The `UNIQUE` modifier turns a partial index into a
4546 // partial-uniqueness invariant (only rows matching the
4547 // WHERE predicate are checked for duplicates). mailrs
4548 // K1 (3 hits: email_templates default, calendar_events
4549 // master, calendar_events instance).
4550 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4551 self.advance();
4552 if !matches!(self.peek(), Token::Index) {
4553 return Err(self.err(alloc::format!(
4554 "expected INDEX after CREATE UNIQUE, got {:?}",
4555 self.peek()
4556 )));
4557 }
4558 self.parse_create_index_stmt_after_create(true)
4559 }
4560 Token::Publication => {
4561 self.advance();
4562 self.parse_create_publication_after_keyword()
4563 }
4564 Token::Subscription => {
4565 self.advance();
4566 self.parse_create_subscription_after_keyword()
4567 }
4568 // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4569 // USER isn't a reserved keyword — we look for the bare
4570 // identifier so the lexer doesn't have to grow a token.
4571 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4572 self.advance();
4573 self.parse_create_user_after_keyword(true)
4574 }
4575 // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4576 // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4577 // the default of the LOGIN attribute.
4578 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4579 self.advance();
4580 self.parse_create_user_after_keyword(false)
4581 }
4582 // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4583 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4584 self.advance();
4585 self.parse_create_policy_after_keyword()
4586 }
4587 // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4588 // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4589 // no-op. mailrs follow-up F3.
4590 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4591 self.advance();
4592 self.parse_create_extension_after_keyword()
4593 }
4594 // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4595 // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4596 // optional; absorb it here and forward to the
4597 // per-kind parsers with the flag. OR is a reserved
4598 // keyword token.
4599 Token::Or => {
4600 self.advance();
4601 let next = self.peek();
4602 let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4603 return Err(self.err(alloc::format!(
4604 "expected REPLACE after CREATE OR, got {next:?}"
4605 )));
4606 };
4607 if !s2.eq_ignore_ascii_case("replace") {
4608 return Err(self.err(alloc::format!(
4609 "expected REPLACE after CREATE OR, got {s2:?}"
4610 )));
4611 }
4612 self.advance();
4613 self.parse_create_function_or_trigger_after_or_replace(true)
4614 }
4615 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4616 self.advance();
4617 self.parse_create_function_after_keyword(false)
4618 }
4619 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4620 self.advance();
4621 self.parse_create_trigger_after_keyword(false)
4622 }
4623 // v7.39 (round 139) — CREATE RULE …
4624 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4625 self.advance();
4626 self.parse_create_rule_after_keyword(false)
4627 }
4628 // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4629 // trigger is a row-level AFTER trigger that additionally carries
4630 // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4631 // path already tolerates and skips those clauses, so consuming the
4632 // CONSTRAINT keyword and reusing it makes the statement parse and the
4633 // trigger fire. (The deferral timing itself is not yet honoured —
4634 // SPG fires it as a plain AFTER trigger, which is correct behaviour
4635 // for every non-deferred use.)
4636 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4637 self.advance();
4638 if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4639 if t.eq_ignore_ascii_case("trigger"))
4640 {
4641 return Err(self.err(alloc::format!(
4642 "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4643 self.peek()
4644 )));
4645 }
4646 self.advance();
4647 self.parse_create_trigger_after_keyword(false)
4648 }
4649 // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4650 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4651 self.advance();
4652 self.parse_create_sequence_after_keyword(false)
4653 }
4654 // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4655 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4656 self.advance();
4657 self.parse_create_view_after_keyword(false, false, false)
4658 }
4659 // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4660 // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4661 // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4662 // appear (in any order) between `CREATE` and `VIEW` in
4663 // every mysqldump-emitted view. Pre-2.6 the parser
4664 // rejected the prefix and the customer's whole view
4665 // backup failed on the first view. The hints are pure
4666 // planner / permission metadata; SPG's view-rewrite
4667 // path is semantically equivalent for all three
4668 // algorithms in v7.17 (TEMPTABLE differs only in
4669 // perf for huge views — out of v7.17 scope), and
4670 // DEFINER / SQL SECURITY are pure single-user
4671 // permissioning that SPG ignores by design.
4672 Token::Ident(s) | Token::QuotedIdent(s)
4673 if s.eq_ignore_ascii_case("algorithm")
4674 || s.eq_ignore_ascii_case("definer")
4675 || s.eq_ignore_ascii_case("sql") =>
4676 {
4677 self.consume_mysql_view_prefix()?;
4678 // After absorbing ALGORITHM / DEFINER / SQL SECURITY
4679 // (in any order, in any combination), the next
4680 // keyword must be VIEW. mysqldump never emits these
4681 // prefixes on non-view statements.
4682 let next = self.peek().clone();
4683 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
4684 if s2.eq_ignore_ascii_case("view"))
4685 {
4686 self.advance();
4687 self.parse_create_view_after_keyword(false, false, false)
4688 } else {
4689 Err(self.err(alloc::format!(
4690 "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
4691 )))
4692 }
4693 }
4694 // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
4695 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
4696 self.advance();
4697 self.parse_create_type_after_keyword()
4698 }
4699 // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
4700 // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
4701 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
4702 self.advance();
4703 self.parse_create_domain_after_keyword()
4704 }
4705 // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
4706 // name [AUTHORIZATION user]. Real catalog registry
4707 // (was silent-no-op'd pre-v7.17).
4708 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
4709 self.advance();
4710 let if_not_exists = self.parse_if_not_exists();
4711 let name = self.expect_ident_like()?;
4712 // Optional `AUTHORIZATION <user>` trailer — accepted,
4713 // ignored (single-user catalog).
4714 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4715 if s.eq_ignore_ascii_case("authorization"))
4716 {
4717 self.advance();
4718 let _ = self.expect_ident_like()?;
4719 }
4720 Ok(Statement::CreateSchema { name, if_not_exists })
4721 }
4722 // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
4723 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
4724 self.advance();
4725 let next = self.peek().clone();
4726 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4727 {
4728 self.advance();
4729 self.parse_create_materialized_view_after_keyword()
4730 } else {
4731 Err(self.err(alloc::format!(
4732 "expected VIEW after CREATE MATERIALIZED, got {next:?}"
4733 )))
4734 }
4735 }
4736 // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
4737 // no-op below), an UNLOGGED table is a real, fully-usable table in
4738 // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
4739 // durability optimisation is a follow-up), so a dump / app that
4740 // declares UNLOGGED tables works instead of failing to parse.
4741 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
4742 self.advance(); // UNLOGGED
4743 if matches!(self.peek(), Token::Table) {
4744 self.parse_create_table_stmt_after_create()
4745 } else {
4746 Err(self.err(format!(
4747 "expected TABLE after CREATE UNLOGGED, got {:?}",
4748 self.peek()
4749 )))
4750 }
4751 }
4752 Token::Ident(s) | Token::QuotedIdent(s)
4753 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
4754 {
4755 self.advance();
4756 // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
4757 let next = self.peek().clone();
4758 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
4759 {
4760 self.advance();
4761 self.parse_create_sequence_after_keyword(true)
4762 } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4763 {
4764 self.advance();
4765 self.parse_create_view_after_keyword(false, false, true)
4766 } else {
4767 // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
4768 // consumed and answered OK while creating nothing, so
4769 // every statement that touched the table afterwards failed
4770 // with "table not found" — the DDL itself lied. It is a
4771 // real CREATE TABLE now, marked temporary so the executor
4772 // puts it in the session's own namespace. An optional
4773 // TABLE keyword may or may not be present (`CREATE TEMP t`
4774 // is not legal, but the keyword is consumed by the
4775 // CREATE TABLE parser itself).
4776 let stmt = self.parse_create_table_stmt_after_create()?;
4777 match stmt {
4778 Statement::CreateTable(mut c) => {
4779 c.temporary = true;
4780 Ok(Statement::CreateTable(c))
4781 }
4782 // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
4783 // CTAS node, which needs the same session namespace.
4784 Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
4785 m.temporary = true;
4786 Ok(Statement::CreateMaterializedView(m))
4787 }
4788 other => Ok(other),
4789 }
4790 }
4791 }
4792 // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
4793 // BEGIN <body> END`. The body may reference `@var`
4794 // session variables, SET statements, internal `;`
4795 // terminators, etc. SPG has no procedure runtime, so
4796 // consume the whole `CREATE PROCEDURE … END` block as
4797 // a no-op so mysqldump scripts that include stored
4798 // routines load through. The matching-END consumer
4799 // tracks BEGIN/END nesting depth to handle nested
4800 // BEGIN blocks correctly.
4801 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
4802 self.consume_mysql_routine_body();
4803 Ok(Statement::Empty)
4804 }
4805 // v7.14.0 — pg_dump / mysqldump emit
4806 // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
4807 // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
4808 // SPG is single-schema / single-database; these have
4809 // no behavioural effect, so consume + return Empty.
4810 // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
4811 // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
4812 // moved up to real parser branches. DATABASE / ROLE /
4813 // POLICY / OPERATOR stay no-op forever
4814 // (single-database, hardcoded roles).
4815 Token::Ident(s) | Token::QuotedIdent(s)
4816 if matches!(
4817 s.to_ascii_lowercase().as_str(),
4818 "database"
4819 | "role"
4820 | "operator"
4821 | "cast"
4822 | "aggregate"
4823 | "language"
4824 | "collation"
4825 | "conversion"
4826 // v7.17.0 Phase 8 (audit N6) — rarely-
4827 // emitted pg_dump shapes that should
4828 // load through without a parser error.
4829 // SPG has no planner statistics catalog,
4830 // no event-trigger hooks, no foreign-
4831 // data-wrapper infrastructure; consume
4832 // + return Empty.
4833 | "statistics"
4834 | "event"
4835 // v7.37.17 (17.6 siblings) — additional CREATE
4836 // targets pg_dump / operator install scripts
4837 // may emit that SPG has no matching machinery
4838 // for. Consume + Empty-return.
4839 | "text"
4840 | "tablespace"
4841 | "access"
4842 | "large"
4843 ) =>
4844 {
4845 // DATABASE is the one member of this list PG refuses
4846 // inside a transaction block; the rest (ROLE, CAST,
4847 // TABLESPACE, …) it runs there quite happily, so only
4848 // this one is named. Still a no-op otherwise — SPG is
4849 // single-database.
4850 let is_database = s.eq_ignore_ascii_case("database");
4851 self.consume_until_statement_boundary();
4852 if is_database {
4853 return Ok(Statement::NoOpPreventedInTransaction {
4854 what: String::from("CREATE DATABASE"),
4855 });
4856 }
4857 Ok(Statement::Empty)
4858 }
4859 // v7.39 (round 706) — the foreign-data family leaves the silent
4860 // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
4861 // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
4862 // FDW machinery), but the ENGINE now warns, so a restore log
4863 // says what will not function instead of reporting success.
4864 Token::Ident(s) | Token::QuotedIdent(s)
4865 if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
4866 {
4867 self.consume_until_statement_boundary();
4868 Ok(Statement::ValidateOnly {
4869 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
4870 names: Vec::new(),
4871 })
4872 }
4873 other => Err(self.err(format!(
4874 "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
4875 ))),
4876 }
4877 }
4878
4879 /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
4880 /// keyword decides whether we parse a function or trigger
4881 /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
4882 /// PROCEDURE) — those land in later releases.
4883 fn parse_create_function_or_trigger_after_or_replace(
4884 &mut self,
4885 or_replace: bool,
4886 ) -> Result<Statement, ParseError> {
4887 let tok = self.peek();
4888 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
4889 return Err(self.err(alloc::format!(
4890 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
4891 )));
4892 };
4893 if s.eq_ignore_ascii_case("function") {
4894 self.advance();
4895 self.parse_create_function_after_keyword(or_replace)
4896 } else if s.eq_ignore_ascii_case("trigger") {
4897 self.advance();
4898 self.parse_create_trigger_after_keyword(or_replace)
4899 } else if s.eq_ignore_ascii_case("rule") {
4900 // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
4901 self.advance();
4902 self.parse_create_rule_after_keyword(or_replace)
4903 } else if s.eq_ignore_ascii_case("view") {
4904 // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
4905 self.advance();
4906 self.parse_create_view_after_keyword(or_replace, false, false)
4907 } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
4908 // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
4909 self.advance();
4910 let nxt = self.peek().clone();
4911 if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
4912 {
4913 self.advance();
4914 self.parse_create_view_after_keyword(or_replace, false, true)
4915 } else {
4916 Err(self.err(alloc::format!(
4917 "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
4918 )))
4919 }
4920 } else {
4921 Err(self.err(alloc::format!(
4922 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
4923 )))
4924 }
4925 }
4926
4927 /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
4928 /// SPG doesn't have a registry; pgvector / similar are
4929 /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
4930 /// the syntax lets dual-target schemas keep the line.
4931 fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
4932 // Optional `IF NOT EXISTS`.
4933 self.consume_if_not_exists();
4934 let name = self.expect_ident_like()?;
4935 // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
4936 // CASCADE / FROM '<v>' clauses; we don't model them.
4937 loop {
4938 match self.peek() {
4939 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
4940 self.advance();
4941 continue;
4942 }
4943 Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
4944 self.advance();
4945 let _ = self.expect_ident_like()?;
4946 continue;
4947 }
4948 Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
4949 self.advance();
4950 // String or ident literal.
4951 let _ = self.advance();
4952 continue;
4953 }
4954 Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
4955 self.advance();
4956 let _ = self.advance();
4957 continue;
4958 }
4959 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
4960 self.advance();
4961 continue;
4962 }
4963 _ => break,
4964 }
4965 }
4966 // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
4967 // nosuch` reported success and `pg_extension` then did not list it,
4968 // which is the accept-and-do-nothing shape F31 exists to find.
4969 Ok(Statement::ValidateOnly {
4970 kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
4971 names: alloc::vec![name],
4972 })
4973 }
4974
4975 /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
4976 /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
4977 /// already been consumed by the caller. Grammar accepted:
4978 ///
4979 /// name `(` arg-list `)`
4980 /// `RETURNS` return-type
4981 /// [ `LANGUAGE` ident ]
4982 /// `AS` $$ body $$
4983 /// [ `LANGUAGE` ident ]
4984 ///
4985 /// Either `LANGUAGE` position is allowed; PG accepts both.
4986 fn parse_create_function_after_keyword(
4987 &mut self,
4988 or_replace: bool,
4989 ) -> Result<Statement, ParseError> {
4990 let name = self.expect_ident_like()?;
4991 // Argument list. v7.12.4 commonly sees the empty `()`
4992 // (trigger functions); typed args parse and round-trip
4993 // but the executor only invokes nullary functions.
4994 if !matches!(self.peek(), Token::LParen) {
4995 return Err(self.err(alloc::format!(
4996 "expected '(' after function name {name:?}, got {:?}",
4997 self.peek()
4998 )));
4999 }
5000 self.advance();
5001 let args = self.parse_function_arg_list()?;
5002 // RETURNS clause.
5003 let tok = self.peek();
5004 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5005 return Err(self.err(alloc::format!(
5006 "expected RETURNS after function arg list, got {tok:?}"
5007 )));
5008 };
5009 if !s.eq_ignore_ascii_case("returns") {
5010 return Err(self.err(alloc::format!(
5011 "expected RETURNS after function arg list, got {s:?}"
5012 )));
5013 }
5014 self.advance();
5015 let returns = self.parse_function_return()?;
5016 // Optional LANGUAGE clause (PG also accepts after AS — we'll
5017 // re-check after the body too).
5018 let mut language: Option<String> = self.parse_optional_language()?;
5019 // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5020 // either side of the body and in any order, interleaved with
5021 // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5022 // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5023 // PG's own pg_dump output did not restore.
5024 let mut attrs = FunctionAttrs::default();
5025 loop {
5026 let before = self.pos;
5027 self.parse_function_attrs_into(&mut attrs)?;
5028 if language.is_none() {
5029 language = self.parse_optional_language()?;
5030 }
5031 if self.pos == before {
5032 break;
5033 }
5034 }
5035 // `AS` followed by a $$-quoted body (lexer already
5036 // collapses both `$$…$$` and `$tag$…$tag$` to a single
5037 // Token::String). AS is a reserved keyword (Token::As).
5038 if !matches!(self.peek(), Token::As) {
5039 return Err(self.err(alloc::format!(
5040 "expected AS before function body, got {:?}",
5041 self.peek()
5042 )));
5043 }
5044 self.advance();
5045 let body_text = match self.peek() {
5046 Token::String(s) => {
5047 let body = s.clone();
5048 self.advance();
5049 body
5050 }
5051 other => {
5052 return Err(self.err(alloc::format!(
5053 "expected $$-quoted function body after AS, got {other:?}"
5054 )));
5055 }
5056 };
5057 // Trailing clauses — PG's other accepted position for both the
5058 // LANGUAGE and the attributes.
5059 loop {
5060 let before = self.pos;
5061 self.parse_function_attrs_into(&mut attrs)?;
5062 if language.is_none() {
5063 language = self.parse_optional_language()?;
5064 }
5065 if self.pos == before {
5066 break;
5067 }
5068 }
5069 let language = language.unwrap_or_else(|| String::from("sql"));
5070 // PL/pgSQL bodies get structure-parsed. Other languages
5071 // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5072 // recognise) round-trip as Raw text — the executor errors
5073 // when invoked with a clear unsupported message.
5074 let body = if language.eq_ignore_ascii_case("plpgsql") {
5075 match parse_plpgsql_body(&body_text) {
5076 Ok(block) => FunctionBody::PlPgSql(block),
5077 // Best-effort: if the body parser doesn't yet
5078 // support a construct used inside, fall back to
5079 // raw — keeps `CREATE FUNCTION` itself working
5080 // (catalogue accepts), executor errors on
5081 // invocation only.
5082 Err(_) => FunctionBody::Raw(body_text),
5083 }
5084 } else {
5085 FunctionBody::Raw(body_text)
5086 };
5087 Ok(Statement::CreateFunction(CreateFunctionStatement {
5088 name,
5089 or_replace,
5090 args,
5091 returns,
5092 language,
5093 body,
5094 attrs,
5095 }))
5096 }
5097
5098 /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5099 /// attribute clauses into `attrs`, stopping at the first token that
5100 /// is not one. Measured against PG 18.4, which accepts them in any
5101 /// order and on either side of the body.
5102 fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5103 loop {
5104 let word = match self.peek() {
5105 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5106 // NOT LEAKPROOF — NOT is a reserved keyword token.
5107 Token::Not
5108 if matches!(
5109 self.tokens.get(self.pos + 1),
5110 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5111 ) =>
5112 {
5113 self.advance();
5114 self.advance();
5115 attrs.leakproof = false;
5116 continue;
5117 }
5118 _ => return Ok(()),
5119 };
5120 match word.as_str() {
5121 "immutable" => {
5122 self.advance();
5123 attrs.volatility = FunctionVolatility::Immutable;
5124 }
5125 "stable" => {
5126 self.advance();
5127 attrs.volatility = FunctionVolatility::Stable;
5128 }
5129 "volatile" => {
5130 self.advance();
5131 attrs.volatility = FunctionVolatility::Volatile;
5132 }
5133 "strict" => {
5134 self.advance();
5135 attrs.strict = true;
5136 }
5137 "leakproof" => {
5138 self.advance();
5139 attrs.leakproof = true;
5140 }
5141 // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5142 // spelled-out forms of STRICT and its opposite.
5143 "returns" | "called" => {
5144 let strict = word == "returns";
5145 let mut probe = self.pos + 1;
5146 if strict {
5147 // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5148 // is not ours.
5149 match self.tokens.get(probe) {
5150 Some(Token::Null) => probe += 1,
5151 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5152 _ => return Ok(()),
5153 }
5154 }
5155 let ok = matches!(self.tokens.get(probe), Some(Token::On))
5156 || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5157 if !ok {
5158 return Ok(());
5159 }
5160 probe += 1;
5161 match self.tokens.get(probe) {
5162 Some(Token::Null) => probe += 1,
5163 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5164 _ => return Ok(()),
5165 }
5166 match self.tokens.get(probe) {
5167 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5168 _ => return Ok(()),
5169 }
5170 self.pos = probe;
5171 attrs.strict = strict;
5172 }
5173 "security" | "external" => {
5174 // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5175 let mut probe = self.pos + 1;
5176 if word == "external" {
5177 match self.tokens.get(probe) {
5178 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5179 probe += 1;
5180 }
5181 _ => return Ok(()),
5182 }
5183 }
5184 let definer = match self.tokens.get(probe) {
5185 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5186 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5187 _ => return Ok(()),
5188 };
5189 self.pos = probe + 1;
5190 attrs.security_definer = definer;
5191 }
5192 "parallel" => {
5193 let level = match self.tokens.get(self.pos + 1) {
5194 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5195 FunctionParallel::Safe
5196 }
5197 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5198 FunctionParallel::Restricted
5199 }
5200 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5201 FunctionParallel::Unsafe
5202 }
5203 _ => return Ok(()),
5204 };
5205 self.pos += 2;
5206 attrs.parallel = level;
5207 }
5208 "cost" | "rows" => {
5209 let Some(n) = self.peek_number_at(self.pos + 1) else {
5210 return Ok(());
5211 };
5212 self.pos += 2;
5213 if word == "cost" {
5214 attrs.cost = Some(n);
5215 } else {
5216 attrs.rows = Some(n);
5217 }
5218 }
5219 _ => return Ok(()),
5220 }
5221 }
5222 }
5223
5224 /// The numeric literal at `idx`, if there is one.
5225 fn peek_number_at(&self, idx: usize) -> Option<f64> {
5226 match self.tokens.get(idx)? {
5227 Token::Integer(n) => Some(*n as f64),
5228 Token::Float(f) => Some(*f),
5229 Token::Numeric(t) => t.parse::<f64>().ok(),
5230 _ => None,
5231 }
5232 }
5233
5234 /// Closing `)`-terminated argument list. v7.12.4 commonly
5235 /// sees the empty `()`; typed args round-trip but the
5236 /// executor (yet) doesn't invoke them.
5237 /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5238 /// it away, which is what PG does with one on a function parameter.
5239 fn skip_type_modifier(&mut self) {
5240 if !matches!(self.peek(), Token::LParen) {
5241 return;
5242 }
5243 // Only a numeric modifier — anything else is not one, and eating
5244 // it would swallow real grammar.
5245 let mut i = self.pos + 1;
5246 let mut seen_number = false;
5247 loop {
5248 match self.tokens.get(i) {
5249 Some(Token::Integer(_)) => seen_number = true,
5250 Some(Token::Comma) => {}
5251 Some(Token::RParen) => break,
5252 _ => return,
5253 }
5254 i += 1;
5255 }
5256 if !seen_number {
5257 return;
5258 }
5259 while self.pos <= i {
5260 self.advance();
5261 }
5262 }
5263
5264 fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5265 let mut args: Vec<FunctionArg> = Vec::new();
5266 if matches!(self.peek(), Token::RParen) {
5267 self.advance();
5268 return Ok(args);
5269 }
5270 loop {
5271 // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5272 // a reserved token; OUT / INOUT are bare idents.
5273 let mode = if matches!(self.peek(), Token::In) {
5274 self.advance();
5275 FunctionArgMode::In
5276 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5277 {
5278 self.advance();
5279 FunctionArgMode::Out
5280 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5281 {
5282 self.advance();
5283 FunctionArgMode::InOut
5284 } else {
5285 FunctionArgMode::In
5286 };
5287 // Optional name. The next token is either a name
5288 // (followed by a type ident) or the type itself.
5289 // Disambiguate by peeking ahead: if the token after
5290 // the next ident is also an ident, we treat the
5291 // first as the name.
5292 // v7.39 (round 315, V19) — take EVERY ident-like word up to
5293 // the comma or paren, then decide. Reading at most two of
5294 // them could not spell `x double precision` at all, and
5295 // silently mis-read the bare `double precision` as a
5296 // parameter named "double" — which is what made the same
5297 // signature key two different ways.
5298 let (name, ty_token) = {
5299 let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5300 while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5301 words.push(self.expect_ident_like()?);
5302 }
5303 // v7.39 (round 344) — a length / precision modifier on the
5304 // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5305 // accepts it and DROPS it — `pg_get_function_arguments`
5306 // reports plain `character varying` / `numeric`, measured on
5307 // 18.4 — but SPG raised `syntax error at or near "("`,
5308 // because the modifier's parens were never consumed.
5309 self.skip_type_modifier();
5310 // r1049 — `f(v bigint[])`. The array suffix parsed in
5311 // the column position, the cast position and (r1038)
5312 // the RETURNS position, but not here: the fifth
5313 // member of the same family, reported by sentori as
5314 // presumably the same code. It is now.
5315 let array_suffix = self.consume_array_suffix();
5316 let whole = words.join(" ");
5317 let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5318 {
5319 (Some(words[0].clone()), words[1..].join(" "))
5320 } else {
5321 (None, whole)
5322 };
5323 ty_token.push_str(&array_suffix);
5324 (name, ty_token)
5325 };
5326 // Type — try to map to ColumnTypeName, else Raw.
5327 let ty = match map_type_ident_to_column_type_name(&ty_token) {
5328 Some(t) => FunctionArgType::Typed(t),
5329 None => FunctionArgType::Raw(ty_token),
5330 };
5331 args.push(FunctionArg { mode, name, ty });
5332 match self.peek() {
5333 Token::Comma => {
5334 self.advance();
5335 continue;
5336 }
5337 Token::RParen => {
5338 self.advance();
5339 return Ok(args);
5340 }
5341 other => {
5342 return Err(self.err(alloc::format!(
5343 "expected , or ) in function arg list, got {other:?}"
5344 )));
5345 }
5346 }
5347 }
5348 }
5349
5350 fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5351 // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5352 // function whose row shape is named inline.
5353 if matches!(self.peek(), Token::Table)
5354 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5355 {
5356 self.advance(); // TABLE
5357 self.advance(); // (
5358 let mut cols: Vec<String> = Vec::new();
5359 loop {
5360 let cname = self.expect_ident_like()?;
5361 let mut ty: Vec<String> = Vec::new();
5362 loop {
5363 match self.peek() {
5364 Token::Comma | Token::RParen | Token::Eof => break,
5365 _ => {}
5366 }
5367 match self.advance() {
5368 Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5369 other => {
5370 if let Some(w) = unreserved_keyword_text(&other) {
5371 ty.push(w);
5372 }
5373 }
5374 }
5375 }
5376 cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5377 if matches!(self.peek(), Token::Comma) {
5378 self.advance();
5379 } else {
5380 break;
5381 }
5382 }
5383 if matches!(self.peek(), Token::RParen) {
5384 self.advance();
5385 }
5386 return Ok(FunctionReturn::Other(alloc::format!(
5387 "TABLE({})",
5388 cols.join(", ")
5389 )));
5390 }
5391 let ident = self.expect_ident_like()?;
5392 // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5393 if ident.eq_ignore_ascii_case("setof") {
5394 let inner = self.expect_ident_like()?;
5395 let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5396 return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5397 }
5398 if ident.eq_ignore_ascii_case("trigger") {
5399 return Ok(FunctionReturn::Trigger);
5400 }
5401 if ident.eq_ignore_ascii_case("void") {
5402 return Ok(FunctionReturn::Void);
5403 }
5404 // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5405 // RETURN position did not, so the `[` was a syntax error and the
5406 // whole migration stopped. sentori worked around it by returning
5407 // zero-padded text.
5408 let suffix = self.consume_array_suffix();
5409 if !suffix.is_empty() {
5410 return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5411 }
5412 match map_type_ident_to_column_type_name(&ident) {
5413 Some(t) => Ok(FunctionReturn::Type(t)),
5414 None => Ok(FunctionReturn::Other(ident)),
5415 }
5416 }
5417
5418 /// Consume any `[]` / `[N]` array markers after a type name and give
5419 /// back their text. Empty when there are none.
5420 fn consume_array_suffix(&mut self) -> String {
5421 let mut out = String::new();
5422 while matches!(self.peek(), Token::LBracket) {
5423 self.advance();
5424 // `[N]` is accepted and, as in PG, the length is not enforced.
5425 if let Token::Integer(n) = self.peek().clone() {
5426 self.advance();
5427 out.push_str(&alloc::format!("[{n}]"));
5428 } else {
5429 out.push_str("[]");
5430 }
5431 if matches!(self.peek(), Token::RBracket) {
5432 self.advance();
5433 }
5434 }
5435 out
5436 }
5437
5438 fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5439 match self.peek() {
5440 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5441 self.advance();
5442 let lang = self.expect_ident_like()?;
5443 Ok(Some(lang.to_ascii_lowercase()))
5444 }
5445 _ => Ok(None),
5446 }
5447 }
5448
5449 /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5450 /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5451 /// (expr)]*`. The `DOMAIN` keyword has already been
5452 /// consumed. PG allows the trailing constraints in any
5453 /// order; we approximate with a small loop.
5454 fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5455 let name = self.expect_ident_like()?;
5456 // Optional `AS`.
5457 if matches!(self.peek(), Token::As) {
5458 self.advance();
5459 }
5460 // v7.39 (round 259) — keep the raw type NAME when the base is not
5461 // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5462 // parent domain.
5463 let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _) =
5464 self.parse_type_with_implied_flags()?;
5465 let mut default: Option<Expr> = None;
5466 let mut not_null = false;
5467 let mut checks: Vec<Expr> = Vec::new();
5468 loop {
5469 match self.peek() {
5470 Token::Default => {
5471 if default.is_some() {
5472 return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5473 }
5474 self.advance();
5475 default = Some(self.parse_expr(0)?);
5476 }
5477 Token::Not => {
5478 self.advance();
5479 if !matches!(self.peek(), Token::Null) {
5480 return Err(self.err(alloc::format!(
5481 "expected NULL after NOT in DOMAIN, got {:?}",
5482 self.peek()
5483 )));
5484 }
5485 self.advance();
5486 not_null = true;
5487 }
5488 Token::Null => {
5489 self.advance();
5490 // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5491 // is the default-nullable marker (PG accepts it),
5492 // but AFTER a NOT NULL it is a conflict PG refuses
5493 // (`conflicting NULL/NOT NULL constraints`,
5494 // PG18-measured); the old arm no-opped both ways.
5495 if not_null {
5496 return Err(self.err(alloc::string::String::from(
5497 "conflicting NULL/NOT NULL constraints",
5498 )));
5499 }
5500 }
5501 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5502 self.advance();
5503 if !matches!(self.peek(), Token::LParen) {
5504 return Err(self.err(alloc::format!(
5505 "expected '(' after CHECK in DOMAIN, got {:?}",
5506 self.peek()
5507 )));
5508 }
5509 self.advance();
5510 let expr = self.parse_expr(0)?;
5511 if !matches!(self.peek(), Token::RParen) {
5512 return Err(self.err(alloc::format!(
5513 "expected ')' after CHECK expr, got {:?}",
5514 self.peek()
5515 )));
5516 }
5517 self.advance();
5518 checks.push(expr);
5519 }
5520 // CONSTRAINT <name> CHECK (…) — PG accepts a name
5521 // prefix on the constraint; we drop the name and
5522 // recurse into the constraint parsing.
5523 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5524 self.advance();
5525 let _ = self.expect_ident_like()?;
5526 }
5527 _ => break,
5528 }
5529 }
5530 Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5531 name,
5532 base_type,
5533 base_domain: base_user_ref,
5534 default,
5535 not_null,
5536 checks,
5537 }))
5538 }
5539
5540 /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5541 /// ('a', 'b', …)`. The `TYPE` keyword has already been
5542 /// consumed.
5543 fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5544 let name = self.expect_ident_like()?;
5545 // Required `AS`.
5546 if !matches!(self.peek(), Token::As) {
5547 return Err(self.err(alloc::format!(
5548 "expected AS after CREATE TYPE {name:?}, got {:?}",
5549 self.peek()
5550 )));
5551 }
5552 self.advance();
5553 // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5554 // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5555 // on the next token: `(` = composite, ident `ENUM` = enum.
5556 if matches!(self.peek(), Token::LParen) {
5557 self.advance();
5558 let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5559 let mut field_user_types: Vec<Option<String>> = Vec::new();
5560 // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5561 // is legal PG (an attribute-less composite; measured — the old
5562 // e2e note claimed PG requires at least one attribute).
5563 if matches!(self.peek(), Token::RParen) {
5564 self.advance();
5565 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5566 name,
5567 kind: crate::ast::TypeKind::Composite {
5568 fields,
5569 field_user_types,
5570 },
5571 }));
5572 }
5573 loop {
5574 let field_name = self.expect_ident_like()?;
5575 // v7.39 (round 264) — keep the raw type name when it is not
5576 // a builtin: that is how a NESTED composite field records
5577 // which composite it holds.
5578 let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _) =
5579 self.parse_type_with_implied_flags()?;
5580 fields.push((field_name, field_type));
5581 field_user_types.push(field_user_ref);
5582 if matches!(self.peek(), Token::Comma) {
5583 self.advance();
5584 continue;
5585 }
5586 if matches!(self.peek(), Token::RParen) {
5587 self.advance();
5588 break;
5589 }
5590 return Err(self.err(alloc::format!(
5591 "expected , or ) in composite field list, got {:?}",
5592 self.peek()
5593 )));
5594 }
5595 if fields.is_empty() {
5596 return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5597 }
5598 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5599 name,
5600 kind: crate::ast::TypeKind::Composite {
5601 fields,
5602 field_user_types,
5603 },
5604 }));
5605 }
5606 // Required `ENUM` ident.
5607 let kind_ident = match self.peek().clone() {
5608 Token::Ident(s) | Token::QuotedIdent(s) => s,
5609 other => {
5610 return Err(self.err(alloc::format!(
5611 "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5612 )));
5613 }
5614 };
5615 if !kind_ident.eq_ignore_ascii_case("enum") {
5616 return Err(self.err(alloc::format!(
5617 "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5618 )));
5619 }
5620 self.advance();
5621 if !matches!(self.peek(), Token::LParen) {
5622 return Err(self.err(alloc::format!(
5623 "expected '(' after ENUM, got {:?}",
5624 self.peek()
5625 )));
5626 }
5627 self.advance();
5628 let mut labels: Vec<String> = Vec::new();
5629 loop {
5630 match self.peek().clone() {
5631 Token::String(s) => {
5632 self.advance();
5633 labels.push(s);
5634 }
5635 other => {
5636 return Err(
5637 self.err(alloc::format!("expected enum label string, got {other:?}"))
5638 );
5639 }
5640 }
5641 if matches!(self.peek(), Token::Comma) {
5642 self.advance();
5643 continue;
5644 }
5645 if matches!(self.peek(), Token::RParen) {
5646 self.advance();
5647 break;
5648 }
5649 return Err(self.err(alloc::format!(
5650 "expected , or ) in ENUM label list, got {:?}",
5651 self.peek()
5652 )));
5653 }
5654 if labels.is_empty() {
5655 return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5656 }
5657 Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5658 name,
5659 kind: crate::ast::TypeKind::Enum { labels },
5660 }))
5661 }
5662
5663 /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
5664 /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
5665 /// The `CREATE MATERIALIZED VIEW` keywords have already been
5666 /// consumed.
5667 fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
5668 let if_not_exists = self.parse_if_not_exists();
5669 let name = self.expect_ident_like()?;
5670 let mut columns: Vec<String> = Vec::new();
5671 if matches!(self.peek(), Token::LParen) {
5672 self.advance();
5673 loop {
5674 let c = self.expect_ident_like()?;
5675 columns.push(c);
5676 if matches!(self.peek(), Token::Comma) {
5677 self.advance();
5678 continue;
5679 }
5680 if matches!(self.peek(), Token::RParen) {
5681 self.advance();
5682 break;
5683 }
5684 return Err(self.err(alloc::format!(
5685 "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
5686 self.peek()
5687 )));
5688 }
5689 }
5690 if !matches!(self.peek(), Token::As) {
5691 return Err(self.err(alloc::format!(
5692 "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
5693 self.peek()
5694 )));
5695 }
5696 self.advance();
5697 // v7.39 (round 151) — a WITH-headed body is legal (read-only
5698 // CTEs only; the engine rejects data-modifying ones with PG's
5699 // message). A trailing `WITH [NO] DATA` can't START the body,
5700 // so WITH here heads the query.
5701 let body = if self.peek_is_with_kw() {
5702 self.advance();
5703 self.parse_nested_with_select()?
5704 } else {
5705 let body_stmt = self.parse_select_stmt()?;
5706 let Statement::Select(body) = body_stmt else {
5707 return Err(self.err(alloc::format!(
5708 "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
5709 )));
5710 };
5711 body
5712 };
5713 // Optional trailing `WITH [NO] DATA`.
5714 let with_data = self.parse_optional_with_data(true)?;
5715 Ok(Statement::CreateMaterializedView(
5716 crate::ast::CreateMaterializedViewStatement {
5717 temporary: false,
5718 name,
5719 if_not_exists,
5720 columns,
5721 body,
5722 with_data,
5723 as_plain_table: false,
5724 },
5725 ))
5726 }
5727
5728 /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
5729 /// `default_when_absent` is what to return if the tail is
5730 /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
5731 /// WITH DATA).
5732 fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
5733 let save = self.pos;
5734 // `WITH` is an Ident (not reserved in the lexer).
5735 let is_with = match self.peek() {
5736 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
5737 _ => false,
5738 };
5739 if !is_with {
5740 return Ok(default_when_absent);
5741 }
5742 self.advance();
5743 // Optional `NO`.
5744 let mut with_data = true;
5745 let is_no = match self.peek() {
5746 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
5747 _ => false,
5748 };
5749 if is_no {
5750 self.advance();
5751 with_data = false;
5752 }
5753 // Required `DATA` ident.
5754 let is_data = match self.peek() {
5755 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
5756 _ => false,
5757 };
5758 if is_data {
5759 self.advance();
5760 Ok(with_data)
5761 } else {
5762 // Caller's WITH wasn't WITH-DATA — rewind so the outer
5763 // parser can interpret it.
5764 self.pos = save;
5765 Ok(default_when_absent)
5766 }
5767 }
5768
5769 /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
5770 /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
5771 /// All keyword prefixes have already been consumed; the flags
5772 /// say which were present.
5773 fn parse_create_view_after_keyword(
5774 &mut self,
5775 or_replace: bool,
5776 _materialized_unused: bool,
5777 temporary: bool,
5778 ) -> Result<Statement, ParseError> {
5779 let if_not_exists = self.parse_if_not_exists();
5780 let name = self.expect_ident_like()?;
5781 // Optional `(col, col, …)` rename list.
5782 let mut columns: Vec<String> = Vec::new();
5783 if matches!(self.peek(), Token::LParen) {
5784 self.advance();
5785 loop {
5786 let c = self.expect_ident_like()?;
5787 columns.push(c);
5788 if matches!(self.peek(), Token::Comma) {
5789 self.advance();
5790 continue;
5791 }
5792 if matches!(self.peek(), Token::RParen) {
5793 self.advance();
5794 break;
5795 }
5796 return Err(self.err(alloc::format!(
5797 "expected , or ) in VIEW column list, got {:?}",
5798 self.peek()
5799 )));
5800 }
5801 }
5802 // Required `AS`.
5803 if !matches!(self.peek(), Token::As) {
5804 return Err(self.err(alloc::format!(
5805 "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
5806 self.peek()
5807 )));
5808 }
5809 self.advance();
5810 // Body: a regular SELECT statement. v7.39 (round 151) — a
5811 // WITH-headed body is legal too (read-only CTEs only; the
5812 // engine rejects data-modifying ones with PG's message).
5813 // Disambiguation vs `WITH CHECK OPTION`: a body can't START
5814 // with the check-option clause, so WITH here heads the query.
5815 let body = if self.peek_is_with_kw() {
5816 self.advance();
5817 self.parse_nested_with_select()?
5818 } else {
5819 let body_stmt = self.parse_select_stmt()?;
5820 let Statement::Select(body) = body_stmt else {
5821 return Err(self.err(alloc::format!(
5822 "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
5823 )));
5824 };
5825 body
5826 };
5827 // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
5828 // The SELECT parser stops before a trailing WITH, so it lands here.
5829 let check_option = if matches!(self.peek(),
5830 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
5831 {
5832 self.advance(); // WITH
5833 let opt = match self.peek() {
5834 Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
5835 self.advance();
5836 crate::ast::ViewCheckOption::Local
5837 }
5838 Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
5839 self.advance();
5840 crate::ast::ViewCheckOption::Cascaded
5841 }
5842 // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
5843 _ => crate::ast::ViewCheckOption::Cascaded,
5844 };
5845 if !matches!(self.peek(),
5846 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
5847 {
5848 return Err(self.err(alloc::format!(
5849 "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
5850 self.peek()
5851 )));
5852 }
5853 self.advance(); // CHECK
5854 if !matches!(self.peek(),
5855 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
5856 {
5857 return Err(self.err(alloc::format!(
5858 "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
5859 self.peek()
5860 )));
5861 }
5862 self.advance(); // OPTION
5863 Some(opt)
5864 } else {
5865 None
5866 };
5867 Ok(Statement::CreateView(crate::ast::CreateViewStatement {
5868 name,
5869 or_replace,
5870 if_not_exists,
5871 temporary,
5872 columns,
5873 body,
5874 check_option,
5875 }))
5876 }
5877
5878 /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
5879 /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
5880 /// consumed; `temporary` carries whether TEMPORARY was seen.
5881 fn parse_create_sequence_after_keyword(
5882 &mut self,
5883 temporary: bool,
5884 ) -> Result<Statement, ParseError> {
5885 let if_not_exists = self.parse_if_not_exists();
5886 let name = self.expect_ident_like()?;
5887 // Optional `AS data_type`.
5888 let data_type = if matches!(self.peek(), Token::As) {
5889 self.advance();
5890 Some(self.parse_sequence_data_type()?)
5891 } else {
5892 None
5893 };
5894 let options = self.parse_sequence_options(/* allow_restart = */ false)?;
5895 Ok(Statement::CreateSequence(
5896 crate::ast::CreateSequenceStatement {
5897 name,
5898 if_not_exists,
5899 temporary,
5900 data_type,
5901 options,
5902 },
5903 ))
5904 }
5905
5906 /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
5907 /// already been consumed; this is reached after `SEQUENCE`.
5908 /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
5909 fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5910 use crate::ast::AlterDomainAction as A;
5911 let name = self.expect_ident_like()?;
5912 // DROP / SET / ADD lex as reserved keyword tokens, not idents.
5913 let kw = match self.peek() {
5914 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
5915 Token::Drop => alloc::string::String::from("drop"),
5916 Token::Default => alloc::string::String::from("default"),
5917 other => {
5918 return Err(self.err(alloc::format!(
5919 "expected an ALTER DOMAIN action, got {other:?}"
5920 )));
5921 }
5922 };
5923 let action = match kw.as_str() {
5924 "add" => {
5925 self.advance();
5926 let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
5927 {
5928 self.advance();
5929 Some(self.expect_ident_like()?)
5930 } else {
5931 None
5932 };
5933 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
5934 return Err(self.err(alloc::format!(
5935 "ALTER DOMAIN ADD supports CHECK only, got {:?}",
5936 self.peek()
5937 )));
5938 }
5939 self.advance();
5940 if !matches!(self.peek(), Token::LParen) {
5941 return Err(self.err("expected '(' after CHECK".into()));
5942 }
5943 self.advance();
5944 let check = self.parse_expr(0)?;
5945 if !matches!(self.peek(), Token::RParen) {
5946 return Err(self.err("expected ')' after CHECK expression".into()));
5947 }
5948 self.advance();
5949 A::AddConstraint { name: cname, check }
5950 }
5951 "drop" => {
5952 self.advance();
5953 match self.peek() {
5954 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
5955 self.advance();
5956 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
5957 {
5958 self.advance();
5959 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
5960 {
5961 return Err(self.err("expected EXISTS after IF".into()));
5962 }
5963 self.advance();
5964 true
5965 } else {
5966 false
5967 };
5968 let cn = self.expect_ident_like()?;
5969 A::DropConstraint {
5970 name: cn,
5971 if_exists,
5972 }
5973 }
5974 Token::Default => {
5975 self.advance();
5976 A::DropDefault
5977 }
5978 Token::Not => {
5979 self.advance();
5980 if !matches!(self.peek(), Token::Null) {
5981 return Err(self.err("expected NULL after NOT".into()));
5982 }
5983 self.advance();
5984 A::DropNotNull
5985 }
5986 other => {
5987 return Err(self.err(alloc::format!(
5988 "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
5989 )));
5990 }
5991 }
5992 }
5993 "set" => {
5994 self.advance();
5995 match self.peek() {
5996 Token::Default => {
5997 self.advance();
5998 A::SetDefault(self.parse_expr(0)?)
5999 }
6000 Token::Not => {
6001 self.advance();
6002 if !matches!(self.peek(), Token::Null) {
6003 return Err(self.err("expected NULL after NOT".into()));
6004 }
6005 self.advance();
6006 A::SetNotNull
6007 }
6008 other => {
6009 return Err(self.err(alloc::format!(
6010 "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6011 )));
6012 }
6013 }
6014 }
6015 "rename" => {
6016 self.advance();
6017 if !matches!(self.peek(), Token::To) {
6018 return Err(self.err("expected TO after RENAME".into()));
6019 }
6020 self.advance();
6021 A::RenameTo(self.expect_ident_like()?)
6022 }
6023 other => {
6024 return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6025 }
6026 };
6027 Ok(Statement::AlterDomain { name, action })
6028 }
6029
6030 fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6031 let if_exists = self.parse_if_exists();
6032 let name = self.expect_ident_like()?;
6033 // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6034 // the option list (PG allows only one or the other).
6035 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6036 self.advance();
6037 if matches!(self.peek(), Token::To) {
6038 self.advance();
6039 } else {
6040 self.expect_keyword_ident("to")?;
6041 }
6042 let new = self.expect_ident_like()?;
6043 return Ok(Statement::AlterSequence(
6044 crate::ast::AlterSequenceStatement {
6045 name,
6046 if_exists,
6047 options: crate::ast::SequenceOptions::default(),
6048 rename_to: Some(new),
6049 },
6050 ));
6051 }
6052 let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6053 Ok(Statement::AlterSequence(
6054 crate::ast::AlterSequenceStatement {
6055 name,
6056 if_exists,
6057 options,
6058 rename_to: None,
6059 },
6060 ))
6061 }
6062
6063 fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6064 let kw = self.expect_ident_like()?;
6065 match kw.to_ascii_lowercase().as_str() {
6066 "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6067 "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6068 "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6069 other => Err(self.err(alloc::format!(
6070 "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6071 ))),
6072 }
6073 }
6074
6075 fn parse_sequence_options(
6076 &mut self,
6077 allow_restart: bool,
6078 ) -> Result<crate::ast::SequenceOptions, ParseError> {
6079 use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6080 let mut opts = SequenceOptions::default();
6081 #[allow(clippy::while_let_loop)]
6082 loop {
6083 // Match an ident; stop at any non-ident token (sentinel,
6084 // semicolon, end of statement).
6085 let kw_lc = match self.peek() {
6086 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6087 _ => break,
6088 };
6089 match kw_lc.as_str() {
6090 "increment" => {
6091 self.advance();
6092 // Optional BY.
6093 if self.peek_is_by() {
6094 self.advance();
6095 }
6096 opts.increment = Some(self.expect_signed_int()?);
6097 }
6098 "minvalue" => {
6099 self.advance();
6100 opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6101 }
6102 "maxvalue" => {
6103 self.advance();
6104 opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6105 }
6106 "no" => {
6107 self.advance();
6108 let what = self.expect_ident_like()?;
6109 match what.to_ascii_lowercase().as_str() {
6110 "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6111 "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6112 "cycle" => opts.cycle = Some(false),
6113 other => {
6114 return Err(self.err(alloc::format!(
6115 "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6116 )));
6117 }
6118 }
6119 }
6120 "start" => {
6121 self.advance();
6122 // Optional WITH.
6123 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6124 if s.eq_ignore_ascii_case("with"))
6125 {
6126 self.advance();
6127 }
6128 opts.start = Some(self.expect_signed_int()?);
6129 }
6130 "restart" if allow_restart => {
6131 self.advance();
6132 // Optional WITH n; bare RESTART means restart at START.
6133 let mut with_val: Option<i64> = None;
6134 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6135 if s.eq_ignore_ascii_case("with"))
6136 {
6137 self.advance();
6138 with_val = Some(self.expect_signed_int()?);
6139 } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6140 with_val = Some(self.expect_signed_int()?);
6141 }
6142 opts.restart = Some(with_val);
6143 }
6144 "cache" => {
6145 self.advance();
6146 opts.cache = Some(self.expect_signed_int()?);
6147 }
6148 "cycle" => {
6149 self.advance();
6150 opts.cycle = Some(true);
6151 }
6152 "owned" => {
6153 self.advance();
6154 match self.peek() {
6155 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6156 self.advance();
6157 }
6158 other => {
6159 return Err(
6160 self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6161 );
6162 }
6163 }
6164 // OWNED BY {NONE | tab.col}. Read just one ident
6165 // (NOT expect_ident_like which would auto-strip
6166 // a schema prefix and consume the `.col` we need).
6167 let first = match self.advance() {
6168 Token::Ident(s) | Token::QuotedIdent(s) => s,
6169 other => {
6170 return Err(self.err(alloc::format!(
6171 "expected identifier or NONE after OWNED BY, got {other:?}"
6172 )));
6173 }
6174 };
6175 if first.eq_ignore_ascii_case("none") {
6176 opts.owned_by = Some(SequenceOwnedBy::None);
6177 } else if matches!(self.peek(), Token::Dot) {
6178 self.advance();
6179 let second = match self.advance() {
6180 Token::Ident(s) | Token::QuotedIdent(s) => s,
6181 other => {
6182 return Err(self.err(alloc::format!(
6183 "expected column name after OWNED BY {first}., got {other:?}"
6184 )));
6185 }
6186 };
6187 // v7.17 dump-compat fix — pg_dump emits
6188 // OWNED BY clauses as
6189 // `schema.table.column` (three segments).
6190 // If a third `.<ident>` follows, treat the
6191 // first ident as schema (drop it; SPG is
6192 // single-schema) and the middle / last
6193 // pair as table.column. Otherwise it's
6194 // the two-segment form table.column.
6195 if matches!(self.peek(), Token::Dot) {
6196 self.advance();
6197 let third = match self.advance() {
6198 Token::Ident(s) | Token::QuotedIdent(s) => s,
6199 other => {
6200 return Err(self.err(alloc::format!(
6201 "expected column name after OWNED BY {first}.{second}., got {other:?}"
6202 )));
6203 }
6204 };
6205 let _ = first; // schema prefix discarded
6206 opts.owned_by = Some(SequenceOwnedBy::Column {
6207 table: second,
6208 column: third,
6209 });
6210 } else {
6211 opts.owned_by = Some(SequenceOwnedBy::Column {
6212 table: first,
6213 column: second,
6214 });
6215 }
6216 } else {
6217 return Err(self.err(alloc::format!(
6218 "expected table.column or NONE after OWNED BY, got {first:?}"
6219 )));
6220 }
6221 }
6222 _ => break,
6223 }
6224 }
6225 Ok(opts)
6226 }
6227
6228 fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6229 let neg = if matches!(self.peek(), Token::Minus) {
6230 self.advance();
6231 true
6232 } else {
6233 false
6234 };
6235 match self.peek() {
6236 Token::Integer(n) => {
6237 let v = *n;
6238 self.advance();
6239 Ok(if neg { -v } else { v })
6240 }
6241 other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6242 }
6243 }
6244
6245 /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6246 /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6247 /// clause is fully accepted and discarded — SPG always runs
6248 /// constraint checks immediately (single-writer model). The
6249 /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6250 /// in either order (per the SQL spec they're independent),
6251 /// though pg_dump always emits them in the canonical
6252 /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6253 /// Stops at the first token that isn't part of the clause.
6254 fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6255 self.consume_deferrable_clauses_timed().map(|_| ())
6256 }
6257
6258 /// v7.39 (round 288) — the same scan, but reporting what it saw:
6259 /// `(deferrable, initially_deferred)`. The clauses were parsed and
6260 /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6261 /// NOT DEFERRABLE and a circular-FK migration could not load.
6262 fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6263 let mut deferrable = false;
6264 let mut initially_deferred = false;
6265 loop {
6266 // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6267 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6268 self.advance();
6269 deferrable = true;
6270 if self.consume_optional_initially_clause()? {
6271 initially_deferred = true;
6272 }
6273 continue;
6274 }
6275 // `NOT DEFERRABLE` — already worked pre-3.1.
6276 if matches!(self.peek(), Token::Not) {
6277 let look = self.tokens.get(self.pos + 1);
6278 if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6279 self.advance(); // NOT
6280 self.advance(); // DEFERRABLE
6281 deferrable = false;
6282 initially_deferred = false;
6283 let _ = self.consume_optional_initially_clause()?;
6284 continue;
6285 }
6286 break;
6287 }
6288 // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6289 // accepts this without a leading [NOT] DEFERRABLE
6290 // (the timing keyword alone). pg_dump occasionally
6291 // emits it on FK constraints that inherit timing.
6292 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6293 if self.consume_optional_initially_clause()? {
6294 initially_deferred = true;
6295 // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6296 deferrable = true;
6297 }
6298 continue;
6299 }
6300 break;
6301 }
6302 Ok((deferrable, initially_deferred))
6303 }
6304
6305 /// Helper for [`consume_optional_deferrable_clauses`]. When the
6306 /// next token is `INITIALLY`, consume it plus the required
6307 /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6308 /// Returns true when the timing seen was `DEFERRED`.
6309 fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6310 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6311 return Ok(false);
6312 }
6313 self.advance(); // INITIALLY
6314 match self.advance() {
6315 Token::Ident(s)
6316 if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6317 {
6318 Ok(s.eq_ignore_ascii_case("deferred"))
6319 }
6320 other => Err(self.err(alloc::format!(
6321 "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6322 ))),
6323 }
6324 }
6325
6326 /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6327 /// in its entirety so the parser returns Empty without
6328 /// touching the runtime. The CREATE+PROCEDURE keywords are
6329 /// already consumed; this swallows everything from the
6330 /// procedure name through the matching `END`, including
6331 /// nested `BEGIN`/`END` blocks, internal `;` terminators
6332 /// (DELIMITER `//` makes the script splitter forward the
6333 /// whole block as one statement), `@var` session-variable
6334 /// references, and the trailing terminator.
6335 ///
6336 /// Tracks nesting depth so:
6337 /// BEGIN
6338 /// IF cond THEN
6339 /// BEGIN ... END;
6340 /// END IF;
6341 /// END
6342 /// terminates at the outer END.
6343 fn consume_mysql_routine_body(&mut self) {
6344 // Outer skeleton: name, (...), optional clauses, BEGIN
6345 // <body> END [;]. Scan for the first BEGIN — anything
6346 // before it is signature decoration we don't care about.
6347 // Once inside BEGIN, count up on BEGIN, down on END.
6348 let mut depth: i32 = 0;
6349 let mut started = false;
6350 loop {
6351 match self.peek().clone() {
6352 Token::Begin => {
6353 self.advance();
6354 depth += 1;
6355 started = true;
6356 }
6357 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6358 self.advance();
6359 if started {
6360 depth -= 1;
6361 if depth <= 0 {
6362 // Optional trailing ident (`END IF`,
6363 // `END LOOP`, `END WHILE`, `END CASE`,
6364 // `END label_name`) — eat the next
6365 // ident if present so we don't
6366 // mistake `END IF;` for the outer
6367 // close.
6368 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6369 // If the next token is one of the
6370 // PL/SQL block-closer keywords,
6371 // the END belongs to an inner
6372 // block; bump depth back up.
6373 let is_inner_close = matches!(
6374 self.peek(),
6375 Token::Ident(s) | Token::QuotedIdent(s)
6376 if matches!(
6377 s.to_ascii_lowercase().as_str(),
6378 "if" | "loop" | "while" | "case" | "repeat"
6379 )
6380 );
6381 if is_inner_close {
6382 self.advance();
6383 depth += 1;
6384 continue;
6385 }
6386 }
6387 // Eat optional trailing `;`.
6388 if matches!(self.peek(), Token::Semicolon) {
6389 self.advance();
6390 }
6391 return;
6392 }
6393 }
6394 }
6395 Token::Eof => return,
6396 _ => {
6397 self.advance();
6398 }
6399 }
6400 }
6401 }
6402
6403 /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6404 /// that appear between `CREATE` and `VIEW` in mysqldump output:
6405 ///
6406 /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6407 /// * `DEFINER = <user>` (user may be a quoted string, a bare
6408 /// ident, or `ident @ ident-or-quoted-string` host form)
6409 /// * `SQL SECURITY {DEFINER|INVOKER}`
6410 ///
6411 /// Each clause may appear at most once but in any order.
6412 /// The hints are pure planner / permission metadata that
6413 /// SPG's view-rewrite engine handles uniformly; we accept
6414 /// and discard. Returns `Ok(())` once a non-clause token is
6415 /// peeked (the caller then checks for the `VIEW` keyword).
6416 fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6417 loop {
6418 match self.peek().clone() {
6419 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6420 self.advance(); // ALGORITHM
6421 // Optional `=`. MySQL spec requires it but be
6422 // generous.
6423 if matches!(self.peek(), Token::Eq) {
6424 self.advance();
6425 }
6426 // UNDEFINED / MERGE / TEMPTABLE — accept any
6427 // bare ident; unknown values still parse so
6428 // future MySQL versions don't break.
6429 if matches!(
6430 self.peek(),
6431 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6432 ) {
6433 self.advance();
6434 }
6435 }
6436 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6437 self.advance(); // DEFINER
6438 if matches!(self.peek(), Token::Eq) {
6439 self.advance();
6440 }
6441 // User: quoted string, ident, OR ident @ host
6442 // (host may itself be quoted or bare).
6443 match self.peek().clone() {
6444 Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6445 self.advance();
6446 // Optional `@host`.
6447 if matches!(self.peek(), Token::At) {
6448 self.advance();
6449 if matches!(
6450 self.peek(),
6451 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6452 ) {
6453 self.advance();
6454 }
6455 }
6456 }
6457 _ => {}
6458 }
6459 }
6460 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6461 // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6462 // when followed by SECURITY — the dispatcher must
6463 // not consume a bare `SQL` token (it's not a
6464 // legal CREATE prefix on its own).
6465 let save = self.pos;
6466 self.advance(); // SQL
6467 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6468 if s2.eq_ignore_ascii_case("security"))
6469 {
6470 self.advance(); // SECURITY
6471 // DEFINER / INVOKER trailing ident.
6472 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6473 self.advance();
6474 }
6475 } else {
6476 // Not a SQL SECURITY clause — roll back and
6477 // bail; the caller will error out cleanly.
6478 self.pos = save;
6479 return Ok(());
6480 }
6481 }
6482 _ => return Ok(()),
6483 }
6484 }
6485 }
6486
6487 fn parse_if_not_exists(&mut self) -> bool {
6488 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6489 {
6490 let save = self.pos;
6491 self.advance();
6492 if matches!(self.peek(), Token::Not) {
6493 self.advance();
6494 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6495 {
6496 self.advance();
6497 return true;
6498 }
6499 }
6500 self.pos = save;
6501 }
6502 false
6503 }
6504
6505 fn parse_if_exists(&mut self) -> bool {
6506 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6507 {
6508 let save = self.pos;
6509 self.advance();
6510 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6511 {
6512 self.advance();
6513 return true;
6514 }
6515 self.pos = save;
6516 }
6517 false
6518 }
6519
6520 /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6521 /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6522 /// been consumed.
6523 fn parse_create_trigger_after_keyword(
6524 &mut self,
6525 or_replace: bool,
6526 ) -> Result<Statement, ParseError> {
6527 let name = self.expect_ident_like()?;
6528 let timing = {
6529 let ident = self.expect_ident_like()?;
6530 if ident.eq_ignore_ascii_case("before") {
6531 TriggerTiming::Before
6532 } else if ident.eq_ignore_ascii_case("after") {
6533 TriggerTiming::After
6534 } else if ident.eq_ignore_ascii_case("instead") {
6535 let next = self.expect_ident_like()?;
6536 if !next.eq_ignore_ascii_case("of") {
6537 return Err(self.err(alloc::format!(
6538 "expected OF after INSTEAD in trigger timing, got {next:?}"
6539 )));
6540 }
6541 TriggerTiming::InsteadOf
6542 } else {
6543 return Err(self.err(alloc::format!(
6544 "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6545 )));
6546 }
6547 };
6548 // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6549 // OR is a reserved keyword token (Token::Or), not an Ident.
6550 // v7.13.0 — after an UPDATE event we may optionally see
6551 // `OF col, col, …` (mailrs round-5 G7). Columns are
6552 // captured into `update_columns` once across the whole
6553 // events list; multiple `UPDATE OF` clauses are rejected.
6554 let mut events: Vec<TriggerEvent> = Vec::new();
6555 let mut update_columns: Vec<String> = Vec::new();
6556 let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6557 events.push(first_ev);
6558 if !first_cols.is_empty() {
6559 update_columns = first_cols;
6560 }
6561 while matches!(self.peek(), Token::Or) {
6562 self.advance();
6563 let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6564 events.push(ev);
6565 if !cols.is_empty() {
6566 if !update_columns.is_empty() {
6567 return Err(
6568 self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6569 );
6570 }
6571 update_columns = cols;
6572 }
6573 }
6574 // ON <table>
6575 let tok = self.peek();
6576 let Token::On = tok else {
6577 return Err(self.err(alloc::format!(
6578 "expected ON after trigger events, got {tok:?}"
6579 )));
6580 };
6581 self.advance();
6582 let table = self.expect_ident_like()?;
6583 // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6584 // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6585 // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6586 // the trigger as a plain AFTER trigger (correct for every non-deferred
6587 // use; deferral timing is not yet honoured).
6588 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6589 if s.eq_ignore_ascii_case("from"))
6590 {
6591 self.advance();
6592 let _reftable = self.expect_ident_like()?;
6593 }
6594 self.consume_optional_deferrable_clauses()?;
6595 // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6596 // keyword (Token::For); EACH / ROW / STATEMENT are bare
6597 // idents.
6598 if !matches!(self.peek(), Token::For) {
6599 return Err(self.err(alloc::format!(
6600 "expected FOR EACH ROW / STATEMENT, got {:?}",
6601 self.peek()
6602 )));
6603 }
6604 self.advance();
6605 let for_each = {
6606 let e = self.expect_ident_like()?;
6607 if !e.eq_ignore_ascii_case("each") {
6608 return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6609 }
6610 let unit = self.expect_ident_like()?;
6611 if unit.eq_ignore_ascii_case("row") {
6612 TriggerForEach::Row
6613 } else if unit.eq_ignore_ascii_case("statement") {
6614 TriggerForEach::Statement
6615 } else {
6616 return Err(self.err(alloc::format!(
6617 "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6618 )));
6619 }
6620 };
6621 // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6622 let when_condition = if matches!(self.peek(),
6623 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6624 {
6625 self.advance();
6626 Some(self.parse_paren_expr("WHEN")?)
6627 } else {
6628 None
6629 };
6630 // EXECUTE FUNCTION/PROCEDURE name(...)
6631 let exec = self.expect_ident_like()?;
6632 if !exec.eq_ignore_ascii_case("execute") {
6633 return Err(self.err(alloc::format!(
6634 "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6635 )));
6636 }
6637 let fn_or_proc = self.expect_ident_like()?;
6638 if !(fn_or_proc.eq_ignore_ascii_case("function")
6639 || fn_or_proc.eq_ignore_ascii_case("procedure"))
6640 {
6641 return Err(self.err(alloc::format!(
6642 "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6643 )));
6644 }
6645 let function = self.expect_ident_like()?;
6646 // Optional empty arg list `()`.
6647 if matches!(self.peek(), Token::LParen) {
6648 self.advance();
6649 if !matches!(self.peek(), Token::RParen) {
6650 return Err(self.err(alloc::format!(
6651 "v7.12.4 trigger function calls take no args; got {:?}",
6652 self.peek()
6653 )));
6654 }
6655 self.advance();
6656 }
6657 Ok(Statement::CreateTrigger(CreateTriggerStatement {
6658 name,
6659 or_replace,
6660 timing,
6661 events,
6662 table,
6663 for_each,
6664 function,
6665 update_columns,
6666 when_condition,
6667 }))
6668 }
6669
6670 /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
6671 /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
6672 fn parse_create_rule_after_keyword(
6673 &mut self,
6674 or_replace: bool,
6675 ) -> Result<Statement, ParseError> {
6676 let name = self.expect_ident_like()?;
6677 if !matches!(self.peek(), Token::As) {
6678 return Err(self.err(alloc::format!(
6679 "expected AS in CREATE RULE, got {:?}",
6680 self.peek()
6681 )));
6682 }
6683 self.advance();
6684 if !matches!(self.peek(), Token::On) {
6685 return Err(self.err(alloc::format!(
6686 "expected ON in CREATE RULE, got {:?}",
6687 self.peek()
6688 )));
6689 }
6690 self.advance();
6691 let event = self.parse_rule_event()?;
6692 if !matches!(self.peek(), Token::To)
6693 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
6694 {
6695 return Err(self.err(alloc::format!(
6696 "expected TO after rule event, got {:?}",
6697 self.peek()
6698 )));
6699 }
6700 self.advance();
6701 let table = self.expect_ident_like()?;
6702 // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
6703 let when_condition = if matches!(self.peek(), Token::Where) {
6704 self.advance();
6705 Some(self.parse_expr(0)?)
6706 } else {
6707 None
6708 };
6709 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
6710 {
6711 return Err(self.err(alloc::format!(
6712 "expected DO in CREATE RULE, got {:?}",
6713 self.peek()
6714 )));
6715 }
6716 self.advance();
6717 // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
6718 let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
6719 {
6720 self.advance();
6721 true
6722 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
6723 self.advance();
6724 false
6725 } else {
6726 false
6727 };
6728 // `NOTHING` | `( cmd; … )` | `cmd`.
6729 let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
6730 {
6731 self.advance();
6732 Vec::new()
6733 } else if matches!(self.peek(), Token::LParen) {
6734 self.advance();
6735 let mut cmds = Vec::new();
6736 loop {
6737 cmds.push(self.parse_one_statement()?);
6738 if matches!(self.peek(), Token::Semicolon) {
6739 self.advance();
6740 if matches!(self.peek(), Token::RParen) {
6741 break;
6742 }
6743 continue;
6744 }
6745 break;
6746 }
6747 if !matches!(self.peek(), Token::RParen) {
6748 return Err(self.err(alloc::format!(
6749 "expected ) closing the CREATE RULE command list, got {:?}",
6750 self.peek()
6751 )));
6752 }
6753 self.advance();
6754 cmds
6755 } else {
6756 alloc::vec![self.parse_one_statement()?]
6757 };
6758 Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
6759 name,
6760 or_replace,
6761 event,
6762 table,
6763 instead,
6764 when_condition,
6765 commands,
6766 }))
6767 }
6768
6769 /// v7.39 (round 139) — a rule event keyword → uppercase string.
6770 fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
6771 if matches!(self.peek(), Token::Insert) {
6772 self.advance();
6773 return Ok(alloc::string::String::from("INSERT"));
6774 }
6775 if matches!(self.peek(), Token::Select) {
6776 self.advance();
6777 return Ok(alloc::string::String::from("SELECT"));
6778 }
6779 match self.peek() {
6780 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
6781 self.advance();
6782 Ok(alloc::string::String::from("UPDATE"))
6783 }
6784 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
6785 self.advance();
6786 Ok(alloc::string::String::from("DELETE"))
6787 }
6788 other => Err(self.err(alloc::format!(
6789 "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
6790 ))),
6791 }
6792 }
6793
6794 /// v7.13.0 — parse one trigger event, then optionally consume
6795 /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
6796 /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
6797 fn parse_trigger_event_with_optional_of(
6798 &mut self,
6799 ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
6800 let ev = self.parse_trigger_event()?;
6801 if !matches!(ev, TriggerEvent::Update) {
6802 return Ok((ev, Vec::new()));
6803 }
6804 // `OF` is a bare ident.
6805 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
6806 return Ok((ev, Vec::new()));
6807 }
6808 self.advance(); // OF
6809 let mut cols: Vec<String> = Vec::new();
6810 loop {
6811 cols.push(self.expect_ident_like()?);
6812 if matches!(self.peek(), Token::Comma) {
6813 self.advance();
6814 continue;
6815 }
6816 break;
6817 }
6818 if cols.is_empty() {
6819 return Err(
6820 self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
6821 );
6822 }
6823 Ok((ev, cols))
6824 }
6825
6826 /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
6827 /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
6828 /// before `BEGIN`, and IF / RAISE / embedded SQL statements
6829 /// inside the body.
6830 /// Called by [`parse_plpgsql_body`] after the body's tokens
6831 /// have been lexed into this temporary parser.
6832 pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
6833 // v7.12.6 — optional DECLARE prelude.
6834 let declarations = if matches!(
6835 self.peek(),
6836 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
6837 ) {
6838 self.advance();
6839 self.parse_plpgsql_declare_block()?
6840 } else {
6841 Vec::new()
6842 };
6843 // BEGIN keyword (PL/pgSQL — distinct from the SQL
6844 // `BEGIN` transaction-start, but we can reuse the
6845 // reserved Token::Begin since the body is a separate
6846 // lex/parse context).
6847 if !matches!(self.peek(), Token::Begin) {
6848 return Err(self.err(alloc::format!(
6849 "expected BEGIN at start of plpgsql block, got {:?}",
6850 self.peek()
6851 )));
6852 }
6853 self.advance();
6854 let statements = self.parse_plpgsql_stmt_list_until_end()?;
6855 // v7.37.20 (20.10) — optional EXCEPTION clause between the
6856 // body's last statement and the trailing END. When present
6857 // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
6858 // arms terminated by END.
6859 let exception_handlers = if matches!(
6860 self.peek(),
6861 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
6862 ) {
6863 self.advance();
6864 self.parse_plpgsql_exception_handlers()?
6865 } else {
6866 Vec::new()
6867 };
6868 Ok(PlPgSqlBlock {
6869 declarations,
6870 statements,
6871 exception_handlers,
6872 })
6873 }
6874
6875 /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
6876 /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
6877 fn parse_plpgsql_exception_handlers(
6878 &mut self,
6879 ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
6880 let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
6881 loop {
6882 // Stop at END — the block-level trailing END LOOP / END;
6883 // is handled by the caller.
6884 if matches!(
6885 self.peek(),
6886 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
6887 ) {
6888 return Ok(out);
6889 }
6890 // WHEN <cond> [OR <cond>]* THEN <body>
6891 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6892 {
6893 return Err(self.err(alloc::format!(
6894 "expected WHEN or END inside EXCEPTION clause, got {:?}",
6895 self.peek()
6896 )));
6897 }
6898 self.advance();
6899 let mut conditions: Vec<String> = Vec::new();
6900 conditions.push(self.expect_ident_like()?);
6901 while matches!(self.peek(), Token::Or) {
6902 self.advance();
6903 conditions.push(self.expect_ident_like()?);
6904 }
6905 let then_kw = self.expect_ident_like()?;
6906 if !then_kw.eq_ignore_ascii_case("then") {
6907 return Err(self.err(alloc::format!(
6908 "expected THEN after WHEN condition list, got {then_kw:?}"
6909 )));
6910 }
6911 let body = self.parse_plpgsql_stmt_list_until_end()?;
6912 out.push(crate::ast::ExceptionHandler { conditions, body });
6913 }
6914 }
6915
6916 /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
6917 /// prelude. Caller has already consumed `DECLARE`. We stop
6918 /// reading entries when we hit `BEGIN`.
6919 fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
6920 let mut out: Vec<PlPgSqlDeclare> = Vec::new();
6921 loop {
6922 if matches!(self.peek(), Token::Begin) {
6923 return Ok(out);
6924 }
6925 let name = self.expect_ident_like()?;
6926 // v7.37.20 (20.7) — type inference: if the next token is
6927 // `:=` or `=` (no explicit type), infer from the default
6928 // expression. Otherwise the ident that follows is the
6929 // declared type.
6930 //
6931 // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
6932 // (PG-standard). SPG parse-accepts and treats identically
6933 // to inference — the eventual runtime value determines
6934 // the local's type, which is faithful to how SPG handles
6935 // untyped locals today (see 20.7). Full compile-time
6936 // catalog lookup queues with v7.40 PL/pgSQL epic.
6937 let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
6938 // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
6939 // downstream declaration walker to type the local by
6940 // the runtime type of the default expression.
6941 FunctionArgType::Raw("_infer_".into())
6942 } else {
6943 let ty_token = self.expect_ident_like()?;
6944 // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
6945 // consume optional `.<ident>` qualifier + `%<KW>`
6946 // suffix. Both qualifier and suffix map to _infer_.
6947 if matches!(self.peek(), Token::Dot) {
6948 self.advance();
6949 let _ = self.expect_ident_like()?;
6950 }
6951 if matches!(self.peek(), Token::Percent) {
6952 self.advance();
6953 // Consume the trailing TYPE / ROWTYPE ident.
6954 let _ = self.expect_ident_like()?;
6955 FunctionArgType::Raw("_infer_".into())
6956 } else {
6957 match map_type_ident_to_column_type_name(&ty_token) {
6958 Some(t) => FunctionArgType::Typed(t),
6959 None => FunctionArgType::Raw(ty_token),
6960 }
6961 }
6962 };
6963 let default = match self.peek() {
6964 Token::ColonEq => {
6965 self.advance();
6966 Some(self.parse_expr(0)?)
6967 }
6968 Token::Eq => {
6969 // PL/pgSQL also accepts `=` for the
6970 // DECLARE default (PG treats them the same
6971 // in this position).
6972 self.advance();
6973 Some(self.parse_expr(0)?)
6974 }
6975 _ => None,
6976 };
6977 // Mandatory `;` between declarations.
6978 if !matches!(self.peek(), Token::Semicolon) {
6979 return Err(self.err(alloc::format!(
6980 "expected ; after DECLARE entry for {name:?}, got {:?}",
6981 self.peek()
6982 )));
6983 }
6984 self.advance();
6985 out.push(PlPgSqlDeclare { name, ty, default });
6986 }
6987 }
6988
6989 /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
6990 /// the terminating `END;` (or `END IF;` etc — handled by the
6991 /// per-construct sub-parsers). Used by both the outer block
6992 /// and the IF/ELSE branch bodies.
6993 fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
6994 let mut statements: Vec<PlPgSqlStmt> = Vec::new();
6995 loop {
6996 // Allow trailing semicolons + END.
6997 while matches!(self.peek(), Token::Semicolon) {
6998 self.advance();
6999 }
7000 // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7001 if matches!(
7002 self.peek(),
7003 Token::Ident(s) | Token::QuotedIdent(s)
7004 if s.eq_ignore_ascii_case("end")
7005 || s.eq_ignore_ascii_case("else")
7006 || s.eq_ignore_ascii_case("elsif")
7007 || s.eq_ignore_ascii_case("elseif")
7008 || s.eq_ignore_ascii_case("exception")
7009 || s.eq_ignore_ascii_case("when")
7010 ) {
7011 return Ok(statements);
7012 }
7013 // Otherwise: one statement, then expect `;` or
7014 // a block-terminator keyword.
7015 let stmt = self.parse_plpgsql_stmt()?;
7016 statements.push(stmt);
7017 match self.peek() {
7018 Token::Semicolon => {
7019 self.advance();
7020 }
7021 Token::Ident(s) | Token::QuotedIdent(s)
7022 if s.eq_ignore_ascii_case("end")
7023 || s.eq_ignore_ascii_case("else")
7024 || s.eq_ignore_ascii_case("elsif")
7025 || s.eq_ignore_ascii_case("elseif")
7026 || s.eq_ignore_ascii_case("exception")
7027 || s.eq_ignore_ascii_case("when") =>
7028 {
7029 // Final statement of the block without `;`.
7030 }
7031 other => {
7032 return Err(self.err(alloc::format!(
7033 "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7034 )));
7035 }
7036 }
7037 }
7038 }
7039
7040 fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7041 // RETURN keyword?
7042 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7043 {
7044 self.advance();
7045 return self.parse_plpgsql_return();
7046 }
7047 // v7.12.6 — IF block.
7048 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7049 {
7050 self.advance();
7051 return self.parse_plpgsql_if();
7052 }
7053 // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7054 // Detected by peeking that token pos+3 is Ident("execute").
7055 if matches!(self.peek(), Token::For)
7056 && matches!(
7057 self.tokens.get(self.pos + 1),
7058 Some(Token::Ident(_) | Token::QuotedIdent(_))
7059 )
7060 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7061 && matches!(
7062 self.tokens.get(self.pos + 3),
7063 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7064 )
7065 {
7066 self.advance(); // FOR
7067 let var = self.expect_ident_like()?;
7068 self.advance(); // IN
7069 self.advance(); // EXECUTE
7070 // Prescan for LOOP at paren depth 0 so parse_expr stops
7071 // before the LOOP keyword (same trick as the bare-SELECT
7072 // ForQuery arm).
7073 let mut depth: i32 = 0;
7074 let mut loop_pos: Option<usize> = None;
7075 let mut scan = self.pos;
7076 while scan < self.tokens.len() {
7077 match self.tokens.get(scan) {
7078 Some(Token::LParen) => depth += 1,
7079 Some(Token::RParen) => depth -= 1,
7080 Some(Token::Ident(s) | Token::QuotedIdent(s))
7081 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7082 {
7083 loop_pos = Some(scan);
7084 break;
7085 }
7086 _ => {}
7087 }
7088 scan += 1;
7089 }
7090 let loop_pos = loop_pos.ok_or_else(|| {
7091 self.err(alloc::format!(
7092 "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7093 ))
7094 })?;
7095 let saved_loop = self.tokens[loop_pos].clone();
7096 self.tokens[loop_pos] = Token::Semicolon;
7097 let expr_result = self.parse_expr(0);
7098 self.tokens[loop_pos] = saved_loop;
7099 let sql_expr = expr_result?;
7100 let loop_kw = self.expect_ident_like()?;
7101 if !loop_kw.eq_ignore_ascii_case("loop") {
7102 return Err(self.err(alloc::format!(
7103 "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7104 )));
7105 }
7106 let body = self.parse_plpgsql_stmt_list_until_end()?;
7107 let end_kw = self.expect_ident_like()?;
7108 if !end_kw.eq_ignore_ascii_case("end") {
7109 return Err(self.err(alloc::format!(
7110 "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7111 )));
7112 }
7113 let loop_kw2 = self.expect_ident_like()?;
7114 if !loop_kw2.eq_ignore_ascii_case("loop") {
7115 return Err(self.err(alloc::format!(
7116 "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7117 )));
7118 }
7119 return Ok(PlPgSqlStmt::ForExecute {
7120 var,
7121 sql_expr,
7122 body,
7123 });
7124 }
7125 // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7126 //
7127 // Two syntactic forms:
7128 // FOR var IN SELECT ... ORDER BY ... LOOP ...
7129 // FOR var IN (SELECT ...) LOOP ...
7130 //
7131 // Bare-SELECT form: to keep parse_select_stmt from swallowing
7132 // the trailing `LOOP` keyword as a table alias, we prescan
7133 // forward to find LOOP at paren depth 0, splice a fake
7134 // Semicolon at that position (so SELECT parses cleanly),
7135 // then re-splice LOOP back in.
7136 //
7137 // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7138 // LOOP directly — no scan required.
7139 if matches!(self.peek(), Token::For)
7140 && matches!(
7141 self.tokens.get(self.pos + 1),
7142 Some(Token::Ident(_) | Token::QuotedIdent(_))
7143 )
7144 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7145 && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7146 || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7147 {
7148 self.advance(); // FOR
7149 let var = self.expect_ident_like()?;
7150 // IN
7151 self.advance();
7152 let query = if matches!(self.peek(), Token::LParen) {
7153 // Paren-wrapped SELECT.
7154 self.advance();
7155 let inner = self.parse_select_stmt()?;
7156 let Statement::Select(q) = inner else {
7157 return Err(self.err(alloc::format!(
7158 "expected SELECT inside (…), got {:?}",
7159 self.peek()
7160 )));
7161 };
7162 if !matches!(self.peek(), Token::RParen) {
7163 return Err(self.err(alloc::format!(
7164 "expected ')' after FOR-IN-SELECT body, got {:?}",
7165 self.peek()
7166 )));
7167 }
7168 self.advance();
7169 q
7170 } else {
7171 // Bare SELECT: prescan to find the LOOP boundary.
7172 let mut depth: i32 = 0;
7173 let mut loop_pos: Option<usize> = None;
7174 let mut scan = self.pos;
7175 while scan < self.tokens.len() {
7176 match self.tokens.get(scan) {
7177 Some(Token::LParen) => depth += 1,
7178 Some(Token::RParen) => depth -= 1,
7179 Some(Token::Ident(s) | Token::QuotedIdent(s))
7180 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7181 {
7182 loop_pos = Some(scan);
7183 break;
7184 }
7185 _ => {}
7186 }
7187 scan += 1;
7188 }
7189 let loop_pos = loop_pos.ok_or_else(|| {
7190 self.err(alloc::format!(
7191 "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7192 ))
7193 })?;
7194 // Swap the LOOP token with a synthetic Semicolon so
7195 // parse_select_stmt stops there, then restore afterward.
7196 let saved_loop = self.tokens[loop_pos].clone();
7197 self.tokens[loop_pos] = Token::Semicolon;
7198 let parse_result = self.parse_select_stmt();
7199 self.tokens[loop_pos] = saved_loop;
7200 let inner = parse_result?;
7201 let Statement::Select(q) = inner else {
7202 return Err(self.err(alloc::format!(
7203 "expected SELECT after FOR <var> IN, got {:?}",
7204 self.peek()
7205 )));
7206 };
7207 q
7208 };
7209 let loop_kw = self.expect_ident_like()?;
7210 if !loop_kw.eq_ignore_ascii_case("loop") {
7211 return Err(self.err(alloc::format!(
7212 "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7213 )));
7214 }
7215 let body = self.parse_plpgsql_stmt_list_until_end()?;
7216 let end_kw = self.expect_ident_like()?;
7217 if !end_kw.eq_ignore_ascii_case("end") {
7218 return Err(self.err(alloc::format!(
7219 "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7220 )));
7221 }
7222 let loop_kw2 = self.expect_ident_like()?;
7223 if !loop_kw2.eq_ignore_ascii_case("loop") {
7224 return Err(self.err(alloc::format!(
7225 "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7226 )));
7227 }
7228 return Ok(PlPgSqlStmt::ForQuery {
7229 var,
7230 query: Box::new(query),
7231 body,
7232 });
7233 }
7234 // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7235 // FOR is a reserved keyword token (Token::For).
7236 if matches!(self.peek(), Token::For)
7237 && matches!(
7238 self.tokens.get(self.pos + 1),
7239 Some(Token::Ident(_) | Token::QuotedIdent(_))
7240 )
7241 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7242 {
7243 self.advance(); // FOR
7244 let var = self.expect_ident_like()?;
7245 if !matches!(self.peek(), Token::In) {
7246 return Err(self.err(alloc::format!(
7247 "expected IN after FOR <var>, got {:?}",
7248 self.peek()
7249 )));
7250 }
7251 self.advance();
7252 let reverse = matches!(
7253 self.peek(),
7254 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7255 );
7256 if reverse {
7257 self.advance();
7258 }
7259 let start = self.parse_expr(0)?;
7260 if !matches!(self.peek(), Token::DotDot) {
7261 return Err(self.err(alloc::format!(
7262 "expected '..' between FOR loop bounds, got {:?}",
7263 self.peek()
7264 )));
7265 }
7266 self.advance();
7267 let end = self.parse_expr(0)?;
7268 let loop_kw = self.expect_ident_like()?;
7269 if !loop_kw.eq_ignore_ascii_case("loop") {
7270 return Err(self.err(alloc::format!(
7271 "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7272 )));
7273 }
7274 let body = self.parse_plpgsql_stmt_list_until_end()?;
7275 let end_kw = self.expect_ident_like()?;
7276 if !end_kw.eq_ignore_ascii_case("end") {
7277 return Err(self.err(alloc::format!(
7278 "expected END LOOP after FOR body, got {end_kw:?}"
7279 )));
7280 }
7281 let loop_kw2 = self.expect_ident_like()?;
7282 if !loop_kw2.eq_ignore_ascii_case("loop") {
7283 return Err(self.err(alloc::format!(
7284 "expected END LOOP after FOR body, got END {loop_kw2:?}"
7285 )));
7286 }
7287 return Ok(PlPgSqlStmt::ForRange {
7288 var,
7289 start,
7290 end,
7291 reverse,
7292 body,
7293 });
7294 }
7295 // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7296 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7297 {
7298 self.advance();
7299 let body = self.parse_plpgsql_stmt_list_until_end()?;
7300 let end_kw = self.expect_ident_like()?;
7301 if !end_kw.eq_ignore_ascii_case("end") {
7302 return Err(self.err(alloc::format!(
7303 "expected END LOOP after LOOP body, got {end_kw:?}"
7304 )));
7305 }
7306 let loop_kw = self.expect_ident_like()?;
7307 if !loop_kw.eq_ignore_ascii_case("loop") {
7308 return Err(self.err(alloc::format!(
7309 "expected END LOOP after LOOP body, got END {loop_kw:?}"
7310 )));
7311 }
7312 return Ok(PlPgSqlStmt::Loop { body });
7313 }
7314 // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7315 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7316 {
7317 self.advance();
7318 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7319 {
7320 self.advance();
7321 Some(self.parse_expr(0)?)
7322 } else {
7323 None
7324 };
7325 return Ok(PlPgSqlStmt::Exit { when });
7326 }
7327 // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7328 // already-parsed Statement or a runtime-computed SQL string.
7329 // The disambiguator vs the extended-query-protocol `EXECUTE
7330 // <stmt_name>` (which is a top-level Statement, not a
7331 // plpgsql line) is that inside a DO block / trigger body the
7332 // EXECUTE keyword ALWAYS refers to dynamic SQL.
7333 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7334 {
7335 self.advance();
7336 let sql = self.parse_expr(0)?;
7337 return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7338 }
7339 // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7340 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7341 {
7342 self.advance();
7343 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7344 {
7345 self.advance();
7346 Some(self.parse_expr(0)?)
7347 } else {
7348 None
7349 };
7350 return Ok(PlPgSqlStmt::Continue { when });
7351 }
7352 // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7353 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7354 {
7355 self.advance();
7356 let condition = self.parse_expr(0)?;
7357 let loop_kw = self.expect_ident_like()?;
7358 if !loop_kw.eq_ignore_ascii_case("loop") {
7359 return Err(self.err(alloc::format!(
7360 "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7361 )));
7362 }
7363 let body = self.parse_plpgsql_stmt_list_until_end()?;
7364 // Expect END LOOP.
7365 let end_kw = self.expect_ident_like()?;
7366 if !end_kw.eq_ignore_ascii_case("end") {
7367 return Err(self.err(alloc::format!(
7368 "expected END LOOP after WHILE body, got {end_kw:?}"
7369 )));
7370 }
7371 let loop_kw2 = self.expect_ident_like()?;
7372 if !loop_kw2.eq_ignore_ascii_case("loop") {
7373 return Err(self.err(alloc::format!(
7374 "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7375 )));
7376 }
7377 return Ok(PlPgSqlStmt::While { condition, body });
7378 }
7379 // v7.12.6 — RAISE.
7380 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7381 {
7382 self.advance();
7383 return self.parse_plpgsql_raise();
7384 }
7385 // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7386 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7387 {
7388 self.advance();
7389 let condition = self.parse_expr(0)?;
7390 let message = if matches!(self.peek(), Token::Comma) {
7391 self.advance();
7392 Some(self.parse_expr(0)?)
7393 } else {
7394 None
7395 };
7396 return Ok(PlPgSqlStmt::Assert { condition, message });
7397 }
7398 // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7399 // "PERFORM is equivalent to SELECT but discards the
7400 // result." Side effects (function calls, RAISE inside
7401 // SQL functions, etc.) still execute. We desugar to
7402 // `SELECT <body>` and wrap in EmbeddedSql so the engine's
7403 // existing embedded-statement path handles execution +
7404 // result-discard cleanly. The result is naturally
7405 // discarded because EmbeddedSql doesn't propagate row
7406 // sets back to the plpgsql interpreter.
7407 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7408 {
7409 self.advance();
7410 // Splice a synthetic Token::Select into the stream at
7411 // the current position so parse_select_stmt parses the
7412 // remainder as a normal SELECT body. Token-stream
7413 // surgery mirrors the try_parse_plpgsql_select_into
7414 // pattern used for SELECT … INTO desugaring.
7415 self.tokens.insert(self.pos, Token::Select);
7416 let select = self.parse_select_stmt()?;
7417 let Statement::Select(s) = select else {
7418 return Err(self.err(alloc::format!(
7419 "expected SELECT body after PERFORM, got {:?}",
7420 self.peek()
7421 )));
7422 };
7423 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7424 }
7425 // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7426 // plpgsql-specific shape (mailrs round-10 migrate-042).
7427 // PG's SELECT INTO at top-level SQL would CREATE a new
7428 // table; inside plpgsql it ASSIGNS the query result to
7429 // a local variable. We detect the INTO at paren-depth
7430 // 0 between SELECT and the statement boundary; if
7431 // found, split the token stream into "pre-INTO
7432 // projection" + "var" + "post-INTO FROM/WHERE…" and
7433 // rebuild as a SelectInto with a regular SELECT body
7434 // (no INTO clause).
7435 if matches!(self.peek(), Token::Select)
7436 && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7437 {
7438 return Ok(PlPgSqlStmt::SelectInto {
7439 var: var_name,
7440 body: Box::new(select_body),
7441 });
7442 }
7443 // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7444 // SELECT can appear directly inside a trigger body; we
7445 // recurse into the regular Statement parser, which will
7446 // stop at the trailing `;` (which our caller then
7447 // consumes).
7448 // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7449 // also embed ALTER / CREATE / DROP statements; route
7450 // those through the same parser so the DO body parses
7451 // cleanly.
7452 if matches!(self.peek(), Token::Insert)
7453 || matches!(self.peek(), Token::Select)
7454 || matches!(self.peek(), Token::Create)
7455 || matches!(self.peek(), Token::Drop)
7456 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7457 if s.eq_ignore_ascii_case("update")
7458 || s.eq_ignore_ascii_case("delete")
7459 || s.eq_ignore_ascii_case("alter"))
7460 {
7461 let stmt = self.parse_one_statement()?;
7462 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7463 }
7464 // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7465 // followed by `:=` and an expression.
7466 let target = self.parse_plpgsql_assign_target()?;
7467 // PL/pgSQL assignment uses `:=`. The lexer represents
7468 // this as a colon followed by `=`; check both shapes.
7469 match self.peek() {
7470 Token::ColonEq => {
7471 self.advance();
7472 }
7473 Token::Colon => {
7474 self.advance();
7475 if !matches!(self.peek(), Token::Eq) {
7476 return Err(self.err(alloc::format!(
7477 "expected := after plpgsql assign target, got `:` then {:?}",
7478 self.peek()
7479 )));
7480 }
7481 self.advance();
7482 }
7483 other => {
7484 return Err(self.err(alloc::format!(
7485 "expected := after plpgsql assign target, got {other:?}"
7486 )));
7487 }
7488 }
7489 let value = self.parse_expr(0)?;
7490 Ok(PlPgSqlStmt::Assign { target, value })
7491 }
7492
7493 /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7494 /// [ELSE body] END IF`. `IF` keyword already consumed.
7495 fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7496 let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7497 let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7498 loop {
7499 // <expr> THEN
7500 let cond = self.parse_expr(0)?;
7501 let then_kw = self.expect_ident_like()?;
7502 if !then_kw.eq_ignore_ascii_case("then") {
7503 return Err(self.err(alloc::format!(
7504 "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7505 )));
7506 }
7507 let body = self.parse_plpgsql_stmt_list_until_end()?;
7508 branches.push((cond, body));
7509 // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7510 match self.peek() {
7511 Token::Ident(s) | Token::QuotedIdent(s)
7512 if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7513 {
7514 self.advance();
7515 continue;
7516 }
7517 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7518 self.advance();
7519 else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7520 break;
7521 }
7522 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7523 break;
7524 }
7525 other => {
7526 return Err(self.err(alloc::format!(
7527 "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7528 )));
7529 }
7530 }
7531 }
7532 // Expect `END IF` (the END keyword is the one we're
7533 // looking at right now).
7534 let end_kw = self.expect_ident_like()?;
7535 if !end_kw.eq_ignore_ascii_case("end") {
7536 return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7537 }
7538 let if_kw = self.expect_ident_like()?;
7539 if !if_kw.eq_ignore_ascii_case("if") {
7540 return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7541 }
7542 Ok(PlPgSqlStmt::If {
7543 branches,
7544 else_branch,
7545 })
7546 }
7547
7548 /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7549 /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7550 /// is already consumed.
7551 fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7552 let lvl_ident = self.expect_ident_like()?;
7553 let level = match lvl_ident.to_ascii_lowercase().as_str() {
7554 "notice" => RaiseLevel::Notice,
7555 "warning" => RaiseLevel::Warning,
7556 "info" => RaiseLevel::Info,
7557 "log" => RaiseLevel::Log,
7558 "debug" => RaiseLevel::Debug,
7559 "exception" => RaiseLevel::Exception,
7560 other => {
7561 return Err(self.err(alloc::format!(
7562 "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7563 )));
7564 }
7565 };
7566 // Message: required for v7.12.6. PG accepts a bare
7567 // RAISE-rethrow form (no message), reserved for future
7568 // RAISE-no-args support.
7569 let Token::String(msg) = self.peek() else {
7570 return Err(self.err(alloc::format!(
7571 "expected RAISE message string, got {:?}",
7572 self.peek()
7573 )));
7574 };
7575 let message = msg.clone();
7576 self.advance();
7577 // Optional comma-separated args (PG `%` format substitution).
7578 let mut args: Vec<Expr> = Vec::new();
7579 while matches!(self.peek(), Token::Comma) {
7580 self.advance();
7581 args.push(self.parse_expr(0)?);
7582 }
7583 Ok(PlPgSqlStmt::Raise {
7584 level,
7585 message,
7586 args,
7587 })
7588 }
7589
7590 /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7591 /// <projection> INTO <var> [FROM …]` (mailrs round-10
7592 /// migrate-042). Returns `(rebuilt_select_without_into,
7593 /// var_name)` when the pattern matches; `None` for
7594 /// regular SELECTs (those go through the embedded-SQL
7595 /// path). Token-stream surgery so the rebuilt SELECT
7596 /// parses through the regular `parse_select_stmt`.
7597 #[allow(clippy::too_many_lines)]
7598 fn try_parse_plpgsql_select_into(
7599 &mut self,
7600 ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7601 // Scan forward from `self.pos + 1` (past Token::Select)
7602 // for Token::Into at paren-depth 0, stopping at the
7603 // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7604 // end the plpgsql statement.
7605 let start = self.pos;
7606 let mut into_pos: Option<usize> = None;
7607 let mut depth: i32 = 0;
7608 let mut i = start + 1;
7609 while i < self.tokens.len() {
7610 match &self.tokens[i] {
7611 Token::LParen => depth += 1,
7612 Token::RParen => depth -= 1,
7613 Token::Semicolon if depth == 0 => break,
7614 Token::Ident(s)
7615 if depth == 0
7616 && (s.eq_ignore_ascii_case("end")
7617 || s.eq_ignore_ascii_case("else")
7618 || s.eq_ignore_ascii_case("elsif")) =>
7619 {
7620 break;
7621 }
7622 Token::Into if depth == 0 => {
7623 into_pos = Some(i);
7624 break;
7625 }
7626 _ => {}
7627 }
7628 i += 1;
7629 }
7630 let Some(into_at) = into_pos else {
7631 return Ok(None);
7632 };
7633 // The token immediately after INTO must be the target
7634 // var ident; anything else (e.g. INSERT INTO table)
7635 // ruled out by the depth-0 check above. Capture it.
7636 let var = match self.tokens.get(into_at + 1) {
7637 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7638 other => {
7639 return Err(self.err(alloc::format!(
7640 "expected variable name after SELECT … INTO, got {other:?}"
7641 )));
7642 }
7643 };
7644 // Find the end of the plpgsql SELECT INTO statement —
7645 // same boundary rules as the depth-0 scan above.
7646 let mut end = into_at + 2;
7647 let mut depth2: i32 = 0;
7648 while end < self.tokens.len() {
7649 match &self.tokens[end] {
7650 Token::LParen => depth2 += 1,
7651 Token::RParen => depth2 -= 1,
7652 Token::Semicolon if depth2 == 0 => break,
7653 Token::Ident(s)
7654 if depth2 == 0
7655 && (s.eq_ignore_ascii_case("end")
7656 || s.eq_ignore_ascii_case("else")
7657 || s.eq_ignore_ascii_case("elsif")) =>
7658 {
7659 break;
7660 }
7661 _ => {}
7662 }
7663 end += 1;
7664 }
7665 // Rebuild a token stream that represents the SELECT
7666 // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
7667 // post-var tokens up to statement end]. Run the
7668 // regular `parse_select_stmt` against it.
7669 let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
7670 for j in start..into_at {
7671 rebuilt.push(self.tokens[j].clone());
7672 }
7673 for j in (into_at + 2)..end {
7674 rebuilt.push(self.tokens[j].clone());
7675 }
7676 rebuilt.push(Token::Eof);
7677 let saved_pos = self.pos;
7678 let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
7679 self.pos = 0;
7680 // parse_select_stmt → parse_bare_select consumes Token::Select itself.
7681 if !matches!(self.peek(), Token::Select) {
7682 self.tokens = saved_tokens;
7683 self.pos = saved_pos;
7684 return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
7685 }
7686 let sel = self.parse_select_stmt();
7687 self.tokens = saved_tokens;
7688 self.pos = end;
7689 let sel = sel?;
7690 let Statement::Select(body) = sel else {
7691 return Err(self.err(alloc::format!(
7692 "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
7693 )));
7694 };
7695 Ok(Some((body, var)))
7696 }
7697
7698 fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
7699 // v7.16.1 — read the head token DIRECTLY rather than
7700 // via `expect_ident_like`. The v7.14.0 schema-qualifier
7701 // strip (`public.t` → `t`) inside `expect_ident_like`
7702 // greedily consumes any `ident . ident` pair, which
7703 // silently turned every `NEW.col := …` /
7704 // `OLD.col := …` plpgsql assignment into a Local("col")
7705 // assignment — the head "new"/"old" was eaten as if it
7706 // were a schema name and the Dot was consumed too, so
7707 // this function's own `peek() == Token::Dot` check
7708 // below never fired. Every BEFORE trigger that rewrote
7709 // a NEW cell was a silent no-op for two major releases
7710 // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
7711 // gate failures were investigated as v7.16.1 backlog.
7712 let head = match self.advance() {
7713 Token::Ident(s) | Token::QuotedIdent(s) => s,
7714 other => {
7715 return Err(self.err(alloc::format!(
7716 "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
7717 )));
7718 }
7719 };
7720 if matches!(self.peek(), Token::Dot) {
7721 self.advance();
7722 let col = self.expect_ident_like()?;
7723 if head.eq_ignore_ascii_case("new") {
7724 return Ok(AssignTarget::NewColumn(col));
7725 }
7726 if head.eq_ignore_ascii_case("old") {
7727 return Ok(AssignTarget::OldColumn(col));
7728 }
7729 return Err(self.err(alloc::format!(
7730 "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
7731 got {head:?}.<col>"
7732 )));
7733 }
7734 Ok(AssignTarget::Local(head))
7735 }
7736
7737 fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7738 // RETURN NEW / OLD / NULL — bare-ident forms.
7739 match self.peek() {
7740 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
7741 self.advance();
7742 return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
7743 }
7744 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
7745 self.advance();
7746 return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
7747 }
7748 Token::Null => {
7749 self.advance();
7750 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7751 }
7752 // Bare `RETURN;` (no value) — treated as `RETURN NULL`
7753 // per PL/pgSQL convention.
7754 Token::Semicolon => {
7755 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7756 }
7757 _ => {}
7758 }
7759 // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
7760 // EXECUTE <expr>. In a DO block context RETURN QUERY has no
7761 // caller-visible effect (blocks don't return sets), so we
7762 // desugar it identically to PERFORM: parse the SELECT (or
7763 // EXECUTE dynamic) as embedded SQL that runs for side
7764 // effects and discards the result. RETURN NEXT <expr>
7765 // (single-row accumulator) queues with v7.40 SETOF function
7766 // infrastructure.
7767 // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
7768 // and keep going.
7769 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
7770 {
7771 self.advance();
7772 let e = self.parse_expr(0)?;
7773 return Ok(PlPgSqlStmt::ReturnNext(e));
7774 }
7775 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
7776 {
7777 self.advance();
7778 // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
7779 // rows go to the set, like the static form. It used to desugar to a
7780 // bare ExecuteDynamic, whose result was DISCARDED.
7781 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7782 {
7783 self.advance();
7784 let sql = self.parse_expr(0)?;
7785 return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
7786 }
7787 // Bare RETURN QUERY <select>. If the current token is
7788 // not already SELECT (e.g., the user wrote `RETURN QUERY
7789 // <projection> FROM ...` in a shorthand — rare but PG
7790 // accepts a bare projection here), splice one in. Same
7791 // trick as PERFORM.
7792 if !matches!(self.peek(), Token::Select) {
7793 self.tokens.insert(self.pos, Token::Select);
7794 }
7795 let select = self.parse_select_stmt()?;
7796 let Statement::Select(s) = select else {
7797 return Err(self.err(alloc::format!(
7798 "expected SELECT body after RETURN QUERY, got {:?}",
7799 self.peek()
7800 )));
7801 };
7802 // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
7803 // to an embedded side-effect SELECT whose rows were DISCARDED, which
7804 // in a SETOF function is the entire answer thrown away.
7805 return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
7806 }
7807 // Fall through: parse a full expression.
7808 let e = self.parse_expr(0)?;
7809 Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
7810 }
7811
7812 fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
7813 // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
7814 // are ident-shaped (the parser keys off case-insensitive
7815 // match — same shape used by the top-level Update / Delete
7816 // dispatchers at parse_one_statement).
7817 if matches!(self.peek(), Token::Insert) {
7818 self.advance();
7819 return Ok(TriggerEvent::Insert);
7820 }
7821 match self.peek() {
7822 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7823 self.advance();
7824 Ok(TriggerEvent::Update)
7825 }
7826 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7827 self.advance();
7828 Ok(TriggerEvent::Delete)
7829 }
7830 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
7831 self.advance();
7832 Ok(TriggerEvent::Truncate)
7833 }
7834 other => Err(self.err(alloc::format!(
7835 "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
7836 ))),
7837 }
7838 }
7839
7840 /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
7841 /// - (no clause) → implicit `FOR ALL TABLES`
7842 /// - `FOR ALL TABLES`
7843 /// - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
7844 /// - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
7845 /// accepted as an SPG lenience. PG18-measured (round 753): PG
7846 /// REJECTS the bare plural (`invalid publication object list`,
7847 /// TABLES only pairs with IN SCHEMA); the old note claimed an
7848 /// unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
7849 fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
7850 let name = self.expect_ident_or_string()?;
7851 // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
7852 // shape so existing publications keep parsing identically.
7853 let scope = if matches!(self.peek(), Token::For) {
7854 self.advance();
7855 if matches!(self.peek(), Token::All) {
7856 self.advance();
7857 if !matches!(self.peek(), Token::Tables) {
7858 return Err(self.err(format!(
7859 "expected TABLES after FOR ALL, got {:?}",
7860 self.peek()
7861 )));
7862 }
7863 self.advance();
7864 if matches!(self.peek(), Token::Except) {
7865 self.advance();
7866 let tables = self.parse_publication_table_list()?;
7867 PublicationScope::AllTablesExcept(tables)
7868 } else {
7869 PublicationScope::AllTables
7870 }
7871 } else if matches!(self.peek(), Token::Table) {
7872 self.advance();
7873 let tables = self.parse_publication_table_list()?;
7874 PublicationScope::ForTables(tables)
7875 } else if matches!(self.peek(), Token::Tables) {
7876 // v7.39 (round 754, F31-B5) — PG18-measured: the bare
7877 // plural (`FOR TABLES t`) is REJECTED (`invalid
7878 // publication object list`); TABLES only pairs with
7879 // `IN SCHEMA`. The old arm accepted it on an
7880 // unverifiable "PG 19 accepts both" claim.
7881 self.advance();
7882 if !matches!(self.peek(), Token::In) {
7883 return Err(self.err(alloc::string::String::from(
7884 "invalid publication object list",
7885 )));
7886 }
7887 self.advance();
7888 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
7889 return Err(self.err(format!(
7890 "expected SCHEMA after FOR TABLES IN, got {:?}",
7891 self.peek()
7892 )));
7893 }
7894 self.advance();
7895 let schema = self.expect_ident_or_string()?;
7896 PublicationScope::TablesInSchema(schema)
7897 } else {
7898 return Err(self.err(format!(
7899 "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
7900 self.peek()
7901 )));
7902 }
7903 } else {
7904 PublicationScope::AllTables
7905 };
7906 Ok(Statement::CreatePublication(CreatePublicationStatement {
7907 name,
7908 scope,
7909 }))
7910 }
7911
7912 /// v6.1.3 — Comma-separated identifier list for the publication
7913 /// FOR-clause. Requires at least one entry; empty list is a
7914 /// parse error (PG behaviour). Quoted idents are accepted; the
7915 /// names round-trip through `Display` as `quote_ident(name)`.
7916 ///
7917 /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
7918 /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
7919 /// pg_dump output. SPG's publication state today is per-table
7920 /// only (matching the pre-PG-15 surface); the col list + WHERE
7921 /// are parsed so dumps load through and the table name reaches
7922 /// `PublicationScope::ForTables`, but the filter is not enforced
7923 /// at publish time. Re-open when a customer dogfood gate
7924 /// requires per-row-filter or column-subset publish semantics
7925 /// (which gates on persistent slot state landing first, 21.12).
7926 fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
7927 let first = self.parse_publication_table_entry()?;
7928 let mut out = alloc::vec![first];
7929 while matches!(self.peek(), Token::Comma) {
7930 self.advance();
7931 out.push(self.parse_publication_table_entry()?);
7932 }
7933 Ok(out)
7934 }
7935
7936 /// One table entry inside a FOR TABLE clause:
7937 /// tab_name [ (col, col, …) ] [ WHERE (predicate) ]
7938 /// Returns just the table name; the column list + WHERE predicate
7939 /// are consumed and discarded per the parse-accept-discard
7940 /// commitment above.
7941 fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
7942 let name = self.expect_ident_like()?;
7943 // Optional column list — `(col, col, …)`.
7944 if matches!(self.peek(), Token::LParen) {
7945 self.advance();
7946 // Empty parens are a PG error too; require ≥ 1 column.
7947 let _ = self.expect_ident_like()?;
7948 while matches!(self.peek(), Token::Comma) {
7949 self.advance();
7950 let _ = self.expect_ident_like()?;
7951 }
7952 if !matches!(self.peek(), Token::RParen) {
7953 return Err(self.err(alloc::format!(
7954 "expected ')' to close publication column list, got {:?}",
7955 self.peek()
7956 )));
7957 }
7958 self.advance();
7959 }
7960 // Optional row filter — `WHERE (predicate)`.
7961 if matches!(self.peek(), Token::Where) {
7962 self.advance();
7963 if !matches!(self.peek(), Token::LParen) {
7964 return Err(self.err(alloc::format!(
7965 "expected '(' after WHERE in publication row filter, got {:?}",
7966 self.peek()
7967 )));
7968 }
7969 self.advance();
7970 let _ = self.parse_expr(0)?;
7971 if !matches!(self.peek(), Token::RParen) {
7972 return Err(self.err(alloc::format!(
7973 "expected ')' to close publication WHERE filter, got {:?}",
7974 self.peek()
7975 )));
7976 }
7977 self.advance();
7978 }
7979 Ok(name)
7980 }
7981
7982 /// v6.1.4 — `CREATE SUBSCRIPTION <name>
7983 /// CONNECTION '<conn>'
7984 /// PUBLICATION <pub> [, <pub> ...]`.
7985 ///
7986 /// The clause order is fixed (CONNECTION first, then
7987 /// PUBLICATION) to match PG. No WITH-options accepted in
7988 /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
7989 fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
7990 let name = self.expect_ident_or_string()?;
7991 if !matches!(self.peek(), Token::Connection) {
7992 return Err(self.err(format!(
7993 "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
7994 self.peek()
7995 )));
7996 }
7997 self.advance();
7998 let conn_str = self.expect_string_literal()?;
7999 if !matches!(self.peek(), Token::Publication) {
8000 return Err(self.err(format!(
8001 "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8002 self.peek()
8003 )));
8004 }
8005 self.advance();
8006 // Reuse the publication FOR-list parser shape: at least one
8007 // identifier, comma-separated.
8008 let first = self.expect_ident_like()?;
8009 let mut publications = alloc::vec![first];
8010 while matches!(self.peek(), Token::Comma) {
8011 self.advance();
8012 publications.push(self.expect_ident_like()?);
8013 }
8014 Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8015 name,
8016 conn_str,
8017 publications,
8018 }))
8019 }
8020
8021 /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8022 /// All keywords after `WAIT` are bare idents in v6.1.x; no
8023 /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8024 /// that fit `u64`.
8025 /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8026 /// qualifier is a *namespace* the app owns (`app.user_id`,
8027 /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8028 /// to discard. So parse the raw segments here instead of
8029 /// `expect_ident_like`, which strips a leading `schema.` qualifier
8030 /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8031 /// a single segment and round-trip unchanged.
8032 fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8033 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8034 loop {
8035 let seg = match self.advance() {
8036 Token::Ident(s) | Token::QuotedIdent(s) => s,
8037 other if unreserved_keyword_text(&other).is_some() => {
8038 unreserved_keyword_text(&other).unwrap()
8039 }
8040 other => {
8041 return Err(ParseError {
8042 message: format!("expected parameter name, got {other:?}"),
8043 token_pos: self.consumed_pos(),
8044 });
8045 }
8046 };
8047 parts.push(seg);
8048 if matches!(self.peek(), Token::Dot) {
8049 self.advance();
8050 continue;
8051 }
8052 break;
8053 }
8054 Ok(parts.join(".").to_ascii_lowercase())
8055 }
8056
8057 fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8058 Self::parse_set_value_inner(self)
8059 }
8060
8061 fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8062 match self.advance() {
8063 Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8064 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8065 Ok(crate::ast::SetValue::Default)
8066 }
8067 Token::Ident(s) | Token::QuotedIdent(s) => {
8068 let mut accum = s;
8069 while matches!(self.peek(), Token::Dot) {
8070 self.advance();
8071 let next = self.expect_ident_like()?;
8072 accum.push('.');
8073 accum.push_str(&next);
8074 }
8075 Ok(crate::ast::SetValue::Ident(accum))
8076 }
8077 Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8078 Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8079 // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8080 // spellings that lex as keyword tokens, not idents:
8081 // `SET standard_conforming_strings = on` is in every
8082 // pg_dump preamble (`off` already lexes as an ident).
8083 // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8084 // DEFAULT lexes as its keyword token, so the ident arm above
8085 // never saw it and the everyday reset form was a syntax error.
8086 Token::Default => Ok(crate::ast::SetValue::Default),
8087 Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8088 Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8089 Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8090 // v7.14.0 — MySQL session/user variable RHS
8091 // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8092 // Wrap as Ident so the SET handler can record it; the
8093 // engine treats `@VAR` / `@@VAR` values as opaque
8094 // strings.
8095 Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8096 // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8097 // is the common MySQL preamble shape. Allow a `+` or
8098 // `-` prefix on negative numerics for parity with PG
8099 // (some param defaults are negative).
8100 Token::Minus => match self.advance() {
8101 Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8102 Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8103 other => Err(self.err(format!(
8104 "expected numeric after `-` in SET value, got {other:?}"
8105 ))),
8106 },
8107 other => Err(self.err(format!(
8108 "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8109 ))),
8110 }
8111 }
8112
8113 /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8114 /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8115 /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8116 /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8117 /// present). Modes are comma-separated per PG; SPG also
8118 /// accepts space-separated for tolerance. READ ONLY / WRITE
8119 /// / DEFERRABLE are parsed-and-ignored (recorded for future
8120 /// surface but not behaviorally honoured today).
8121 /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8122 /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8123 /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8124 /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8125 /// session default rather than forcing READ COMMITTED.
8126 fn parse_isolation_level_clauses(&mut self) -> Result<Option<IsolationLevel>, ParseError> {
8127 let mut level = IsolationLevel::default();
8128 let mut have_level = false;
8129 loop {
8130 // ISOLATION LEVEL …
8131 let saw_isolation =
8132 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8133 if saw_isolation {
8134 self.advance(); // ISOLATION
8135 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8136 return Err(self.err(alloc::format!(
8137 "expected LEVEL after ISOLATION, got {:?}",
8138 self.peek()
8139 )));
8140 }
8141 self.advance(); // LEVEL
8142 // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8143 let w1 = self
8144 .expect_ident_like()
8145 .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8146 let lc = w1.to_ascii_lowercase();
8147 level = match lc.as_str() {
8148 "serializable" => IsolationLevel::Serializable,
8149 "repeatable" => {
8150 // Expect READ
8151 let w2 = self
8152 .expect_ident_like()
8153 .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8154 if !w2.eq_ignore_ascii_case("read") {
8155 return Err(self.err(alloc::format!(
8156 "expected READ after REPEATABLE, got {w2:?}"
8157 )));
8158 }
8159 IsolationLevel::RepeatableRead
8160 }
8161 "read" => {
8162 let w2 = self
8163 .expect_ident_like()
8164 .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8165 match w2.to_ascii_lowercase().as_str() {
8166 "committed" => IsolationLevel::ReadCommitted,
8167 "uncommitted" => IsolationLevel::ReadUncommitted,
8168 other => {
8169 return Err(self.err(alloc::format!(
8170 "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8171 )));
8172 }
8173 }
8174 }
8175 other => {
8176 return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8177 }
8178 };
8179 have_level = true;
8180 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8181 // READ ONLY | READ WRITE — parsed, not behaviorally honoured.
8182 self.advance();
8183 match self.peek().clone() {
8184 Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8185 self.advance();
8186 }
8187 Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8188 self.advance();
8189 }
8190 other => {
8191 return Err(self.err(alloc::format!(
8192 "expected ONLY or WRITE after READ, got {other:?}"
8193 )));
8194 }
8195 }
8196 } else if matches!(self.peek(), Token::Not) {
8197 // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8198 self.advance();
8199 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8200 return Err(self.err(alloc::format!(
8201 "expected DEFERRABLE after NOT, got {:?}",
8202 self.peek()
8203 )));
8204 }
8205 self.advance();
8206 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8207 {
8208 self.advance();
8209 } else {
8210 break;
8211 }
8212 // Optional comma between modes.
8213 if matches!(self.peek(), Token::Comma) {
8214 self.advance();
8215 }
8216 }
8217 Ok(have_level.then_some(level))
8218 }
8219
8220 fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8221 // FOR is a v6.1.2-reserved keyword (Token::For). The
8222 // other two are bare idents — they've never needed lexer
8223 // support and we keep it that way.
8224 if !matches!(self.peek(), Token::For) {
8225 return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8226 }
8227 self.advance();
8228 self.expect_keyword_ident("wal")?;
8229 self.expect_keyword_ident("position")?;
8230 let pos = self.expect_u64_literal()?;
8231 let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8232 {
8233 self.advance();
8234 self.expect_keyword_ident("timeout")?;
8235 Some(self.expect_u64_literal()?)
8236 } else {
8237 None
8238 };
8239 Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8240 }
8241
8242 /// v6.1.7 helper — consume a `Token::Integer` and check it
8243 /// fits `u64`. WAL positions and millisecond timeouts are
8244 /// non-negative.
8245 fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8246 match self.advance() {
8247 Token::Integer(n) if n >= 0 => Ok(n as u64),
8248 Token::Integer(n) => Err(ParseError {
8249 message: format!("expected non-negative integer, got {n}"),
8250 token_pos: self.consumed_pos(),
8251 }),
8252 other => Err(ParseError {
8253 message: format!("expected integer literal, got {other:?}"),
8254 token_pos: self.consumed_pos(),
8255 }),
8256 }
8257 }
8258
8259 /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8260 /// ROLE '<role>' (defaults to readonly). All string slots accept
8261 /// either a quoted ident or a quoted string literal.
8262 /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8263 /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8264 ///
8265 /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8266 /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8267 /// wire role) still parses — it is a different axis from the PG attributes.
8268 /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8269 /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8270 /// or RESET, so the plain attribute forms keep their old path.
8271 fn peeks_db_role_setting(&self) -> bool {
8272 let mut i = self.pos + 1; // past the object's name
8273 let word = |p: usize| -> Option<String> {
8274 match self.tokens.get(p) {
8275 Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8276 Some(Token::In) => Some(String::from("in")),
8277 _ => None,
8278 }
8279 };
8280 if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8281 i += 3; // IN DATABASE <name>
8282 }
8283 matches!(word(i).as_deref(), Some("set" | "reset"))
8284 }
8285
8286 fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8287 use crate::ast::SetDbRoleSettingStatement;
8288 // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8289 // identifier, so the ordinary name reader refuses it. Same trap
8290 // as TABLE / INDEX / FULL / DEFAULT before it.
8291 let name = if matches!(self.peek(), Token::All) {
8292 self.advance();
8293 String::from("all")
8294 } else {
8295 self.expect_ident_or_string()?
8296 };
8297 // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8298 let all = name.eq_ignore_ascii_case("all");
8299 let (mut database, mut role) = if is_database {
8300 (Some(name), None)
8301 } else if all {
8302 (None, None)
8303 } else {
8304 (None, Some(name))
8305 };
8306 if matches!(self.peek(), Token::In) {
8307 self.advance();
8308 self.advance(); // DATABASE
8309 database = Some(self.expect_ident_or_string()?);
8310 }
8311 let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8312 self.advance(); // SET | RESET
8313 if resetting && matches!(self.peek(), Token::All) {
8314 self.advance();
8315 self.consume_until_statement_boundary();
8316 return Ok(Statement::SetDbRoleSetting(Box::new(
8317 SetDbRoleSettingStatement {
8318 database,
8319 role,
8320 param: None,
8321 value: None,
8322 },
8323 )));
8324 }
8325 let param = self.expect_ident_like()?;
8326 let value = if resetting {
8327 None
8328 } else {
8329 // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8330 // KEYWORD, so the ident-only check missed it and consumed
8331 // the word itself as the value — the same trap as ALL, one
8332 // clause over.
8333 if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8334 self.advance();
8335 }
8336 Some(self.take_guc_value())
8337 };
8338 self.consume_until_statement_boundary();
8339 Ok(Statement::SetDbRoleSetting(Box::new(
8340 SetDbRoleSettingStatement {
8341 database,
8342 role,
8343 param: Some(param),
8344 value,
8345 },
8346 )))
8347 }
8348
8349 /// The remainder of a `SET <p> = …` clause as PG renders it back:
8350 /// a quoted literal loses its quotes, a bare word or number does not.
8351 fn take_guc_value(&mut self) -> String {
8352 match self.advance() {
8353 Token::String(s) => s,
8354 Token::Integer(n) => format!("{n}"),
8355 Token::Float(f) => format!("{f}"),
8356 Token::Ident(s) | Token::QuotedIdent(s) => s,
8357 other => format!("{other:?}"),
8358 }
8359 }
8360
8361 fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8362 let name = self.expect_ident_or_string()?;
8363 if self.peek_keyword_ident("with") {
8364 self.advance();
8365 }
8366 let mut password = String::new();
8367 let mut role = String::new();
8368 let mut login: Option<bool> = None;
8369 let mut inherit: Option<bool> = None;
8370 let mut superuser: Option<bool> = None;
8371 // Not a `while let`: the pattern would borrow `self` across the
8372 // body, which calls `self.advance()` / `self.expect_*` (&mut).
8373 #[allow(clippy::while_let_loop)]
8374 loop {
8375 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8376 break;
8377 };
8378 match w.to_ascii_lowercase().as_str() {
8379 "password" => {
8380 self.advance();
8381 password = self.expect_string_literal()?;
8382 }
8383 // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8384 // is the same slot.
8385 "encrypted" => {
8386 self.advance();
8387 self.expect_keyword_ident("password")?;
8388 password = self.expect_string_literal()?;
8389 }
8390 "login" => {
8391 self.advance();
8392 login = Some(true);
8393 }
8394 "nologin" => {
8395 self.advance();
8396 login = Some(false);
8397 }
8398 "inherit" => {
8399 self.advance();
8400 inherit = Some(true);
8401 }
8402 "noinherit" => {
8403 self.advance();
8404 inherit = Some(false);
8405 }
8406 "superuser" => {
8407 self.advance();
8408 superuser = Some(true);
8409 }
8410 "nosuperuser" => {
8411 self.advance();
8412 superuser = Some(false);
8413 }
8414 // SPG's own coarse wire role: `ROLE 'readwrite'`.
8415 "role" => {
8416 self.advance();
8417 role = self.expect_string_literal()?;
8418 }
8419 // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8420 // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8421 // accepted and ignored so a pg_dump role block restores. They
8422 // gate capabilities SPG does not have.
8423 "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8424 | "noreplication" | "bypassrls" | "nobypassrls" => {
8425 self.advance();
8426 }
8427 "connection" => {
8428 self.advance();
8429 self.expect_keyword_ident("limit")?;
8430 self.advance(); // the number
8431 }
8432 "valid" => {
8433 self.advance();
8434 self.expect_keyword_ident("until")?;
8435 self.expect_string_literal()?;
8436 }
8437 _ => break,
8438 }
8439 }
8440 if role.is_empty() {
8441 role = "readonly".to_string();
8442 }
8443 Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8444 name,
8445 password,
8446 role,
8447 login,
8448 inherit,
8449 superuser,
8450 is_user,
8451 }))
8452 }
8453
8454 /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8455 /// consumed the USING / WITH CHECK keyword.
8456 fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8457 if !matches!(self.peek(), Token::LParen) {
8458 return Err(self.err(alloc::format!(
8459 "expected '(' after {clause}, got {:?}",
8460 self.peek()
8461 )));
8462 }
8463 self.advance();
8464 let e = self.parse_expr(0)?;
8465 if !matches!(self.peek(), Token::RParen) {
8466 return Err(self.err(alloc::format!(
8467 "expected ')' to close {clause}, got {:?}",
8468 self.peek()
8469 )));
8470 }
8471 self.advance();
8472 Ok(e)
8473 }
8474
8475 /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8476 fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8477 let mut roles = Vec::new();
8478 loop {
8479 roles.push(self.expect_ident_like()?);
8480 if matches!(self.peek(), Token::Comma) {
8481 self.advance();
8482 } else {
8483 break;
8484 }
8485 }
8486 Ok(roles)
8487 }
8488
8489 /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8490 /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8491 /// `CREATE POLICY`.
8492 fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8493 use crate::ast::PolicyCmd;
8494 let name = self.expect_ident_like()?;
8495 if !matches!(self.peek(), Token::On) {
8496 return Err(self.err(alloc::format!(
8497 "expected ON after CREATE POLICY name, got {:?}",
8498 self.peek()
8499 )));
8500 }
8501 self.advance();
8502 let table = self.expect_ident_like()?;
8503
8504 let mut permissive = true;
8505 if matches!(self.peek(), Token::As) {
8506 self.advance();
8507 let w = self.expect_ident_like()?;
8508 permissive = if w.eq_ignore_ascii_case("permissive") {
8509 true
8510 } else if w.eq_ignore_ascii_case("restrictive") {
8511 false
8512 } else {
8513 return Err(self.err(alloc::format!(
8514 "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8515 )));
8516 };
8517 }
8518
8519 let mut cmd = PolicyCmd::All;
8520 if matches!(self.peek(), Token::For) {
8521 self.advance();
8522 cmd = self.parse_policy_cmd()?;
8523 }
8524
8525 let mut roles = Vec::new();
8526 if matches!(self.peek(), Token::To) {
8527 self.advance();
8528 roles = self.parse_policy_roles()?;
8529 }
8530
8531 let mut using = None;
8532 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8533 {
8534 self.advance();
8535 using = Some(self.parse_paren_expr("USING")?);
8536 }
8537
8538 let mut with_check = None;
8539 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8540 {
8541 self.advance();
8542 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8543 {
8544 return Err(self.err(alloc::format!(
8545 "expected CHECK after WITH, got {:?}",
8546 self.peek()
8547 )));
8548 }
8549 self.advance();
8550 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8551 }
8552
8553 // Clause-per-command matrix (PG wording).
8554 match cmd {
8555 PolicyCmd::Insert => {
8556 if using.is_some() {
8557 return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8558 }
8559 }
8560 PolicyCmd::Select | PolicyCmd::Delete => {
8561 if with_check.is_some() {
8562 return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8563 }
8564 }
8565 PolicyCmd::Update | PolicyCmd::All => {}
8566 }
8567
8568 Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8569 name,
8570 table,
8571 permissive,
8572 cmd,
8573 roles,
8574 using,
8575 with_check,
8576 }))
8577 }
8578
8579 /// v7.39 (RLS) — the command word after `FOR`.
8580 fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8581 use crate::ast::PolicyCmd;
8582 match self.peek().clone() {
8583 Token::All => {
8584 self.advance();
8585 Ok(PolicyCmd::All)
8586 }
8587 Token::Select => {
8588 self.advance();
8589 Ok(PolicyCmd::Select)
8590 }
8591 Token::Insert => {
8592 self.advance();
8593 Ok(PolicyCmd::Insert)
8594 }
8595 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8596 self.advance();
8597 Ok(PolicyCmd::Update)
8598 }
8599 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8600 self.advance();
8601 Ok(PolicyCmd::Delete)
8602 }
8603 other => Err(self.err(alloc::format!(
8604 "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8605 ))),
8606 }
8607 }
8608
8609 /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8610 /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8611 fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8612 let name = self.expect_ident_like()?;
8613 if !matches!(self.peek(), Token::On) {
8614 return Err(self.err(alloc::format!(
8615 "expected ON after ALTER POLICY name, got {:?}",
8616 self.peek()
8617 )));
8618 }
8619 self.advance();
8620 let table = self.expect_ident_like()?;
8621
8622 // RENAME TO new
8623 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8624 {
8625 self.advance();
8626 if !matches!(self.peek(), Token::To) {
8627 return Err(self.err(alloc::format!(
8628 "expected TO after RENAME, got {:?}",
8629 self.peek()
8630 )));
8631 }
8632 self.advance();
8633 let new = self.expect_ident_like()?;
8634 return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8635 name,
8636 table,
8637 rename_to: Some(new),
8638 roles: None,
8639 using: None,
8640 with_check: None,
8641 }));
8642 }
8643
8644 let mut roles = None;
8645 if matches!(self.peek(), Token::To) {
8646 self.advance();
8647 roles = Some(self.parse_policy_roles()?);
8648 }
8649 let mut using = None;
8650 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8651 {
8652 self.advance();
8653 using = Some(self.parse_paren_expr("USING")?);
8654 }
8655 let mut with_check = None;
8656 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8657 {
8658 self.advance();
8659 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8660 {
8661 return Err(self.err(alloc::format!(
8662 "expected CHECK after WITH, got {:?}",
8663 self.peek()
8664 )));
8665 }
8666 self.advance();
8667 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8668 }
8669 Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8670 name,
8671 table,
8672 rename_to: None,
8673 roles,
8674 using,
8675 with_check,
8676 }))
8677 }
8678
8679 /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
8680 /// `DROP POLICY`.
8681 fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8682 let if_exists = self.consume_if_exists();
8683 let name = self.expect_ident_like()?;
8684 if !matches!(self.peek(), Token::On) {
8685 return Err(self.err(alloc::format!(
8686 "expected ON after DROP POLICY name, got {:?}",
8687 self.peek()
8688 )));
8689 }
8690 self.advance();
8691 let table = self.expect_ident_like()?;
8692 Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
8693 name,
8694 table,
8695 if_exists,
8696 }))
8697 }
8698}
8699fn wrap_from_leaves(
8700 e: &mut Expr,
8701 names: &[String],
8702 make: &dyn Fn(Expr) -> Expr,
8703 refs: &dyn Fn(&Expr) -> bool,
8704) {
8705 if let Expr::Column(c) = e {
8706 if c.qualifier
8707 .as_deref()
8708 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
8709 {
8710 let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
8711 *e = make(taken);
8712 }
8713 return;
8714 }
8715 match e {
8716 Expr::Binary { lhs, rhs, .. } => {
8717 wrap_from_leaves(lhs, names, make, refs);
8718 wrap_from_leaves(rhs, names, make, refs);
8719 }
8720 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
8721 wrap_from_leaves(expr, names, make, refs)
8722 }
8723 Expr::FunctionCall { args, .. } => {
8724 for a in args.iter_mut() {
8725 wrap_from_leaves(a, names, make, refs);
8726 }
8727 }
8728 Expr::Case {
8729 operand,
8730 branches,
8731 else_branch,
8732 } => {
8733 if let Some(o) = operand.as_deref_mut() {
8734 wrap_from_leaves(o, names, make, refs);
8735 }
8736 for (w, t) in branches.iter_mut() {
8737 wrap_from_leaves(w, names, make, refs);
8738 wrap_from_leaves(t, names, make, refs);
8739 }
8740 if let Some(el) = else_branch.as_deref_mut() {
8741 wrap_from_leaves(el, names, make, refs);
8742 }
8743 }
8744 // Compound variants the walk doesn't decompose: keep the
8745 // pre-D.30 behavior — wrap the whole sub-expr if it touches
8746 // a source table, so nothing regresses.
8747 other => {
8748 if refs(other) {
8749 let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
8750 *other = make(taken);
8751 }
8752 }
8753 }
8754}
8755
8756/// v7.39 (round 241) — does this expression reference any of the FROM /
8757/// USING table names (shared by the UPDATE…FROM and DELETE…USING
8758/// lowerings)?
8759fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
8760 match e {
8761 Expr::Column(c) => c
8762 .qualifier
8763 .as_deref()
8764 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
8765 Expr::Binary { lhs, rhs, .. } => {
8766 expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
8767 }
8768 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
8769 Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
8770 Expr::Case {
8771 operand,
8772 branches,
8773 else_branch,
8774 } => {
8775 operand
8776 .as_deref()
8777 .is_some_and(|o| expr_refs_tables(o, names))
8778 || branches
8779 .iter()
8780 .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
8781 || else_branch
8782 .as_deref()
8783 .is_some_and(|el| expr_refs_tables(el, names))
8784 }
8785 _ => false,
8786 }
8787}
8788
8789impl Parser {
8790 /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
8791 /// Caller already consumed the leading `UPDATE` ident.
8792 /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
8793 /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
8794 /// after the target name has been read. `JOIN` is a reserved token;
8795 /// the qualifiers are bare idents.
8796 fn peek_is_update_join_start(&self) -> bool {
8797 match self.peek() {
8798 // JOIN and its qualifiers are reserved lexer tokens (the grammar
8799 // dedicates arms to `LEFT [OUTER] JOIN` and friends).
8800 Token::Join
8801 | Token::Inner
8802 | Token::Left
8803 | Token::Right
8804 | Token::Cross
8805 | Token::Full => true,
8806 // NATURAL / STRAIGHT_JOIN arrive as bare idents.
8807 Token::Ident(s) | Token::QuotedIdent(s) => {
8808 matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
8809 }
8810 _ => false,
8811 }
8812 }
8813
8814 /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
8815 /// USER-variable assignment. Its own per-session namespace, an arbitrary
8816 /// expression on the right, and `:=` as a second spelling of `=`.
8817 ///
8818 /// Out-of-line (`inline(never)`): the statement-parse frame it is called
8819 /// from sits on the nesting recursion chain (a CTE body, a subquery),
8820 /// and holding this loop's `Vec` + `String` locals there overflowed the
8821 /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
8822 #[inline(never)]
8823 fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
8824 let mut assigns: Vec<(String, Expr)> = Vec::new();
8825 let mut settings: Vec<(String, Expr)> = Vec::new();
8826 loop {
8827 // v7.39 (round 554) — a plain NAME here is a session
8828 // setting, not a user variable. mysqldump writes the two in
8829 // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
8830 // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
8831 // changes it — and this refused the mixture outright, so no
8832 // dump could be restored past its preamble.
8833 if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
8834 self.advance();
8835 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8836 return Err(self.err(alloc::format!(
8837 "expected `=` after {name}, got {:?}",
8838 self.peek()
8839 )));
8840 }
8841 self.advance();
8842 let value = self.parse_expr(0)?;
8843 settings.push((name.to_ascii_lowercase(), value));
8844 if matches!(self.peek(), Token::Comma) {
8845 self.advance();
8846 continue;
8847 }
8848 break;
8849 }
8850 let Token::SessionVar(raw) = self.peek().clone() else {
8851 return Err(self.err(alloc::format!(
8852 "expected a user variable after SET, got {:?}",
8853 self.peek()
8854 )));
8855 };
8856 if raw.starts_with("@@") {
8857 return Err(self.err(alloc::string::String::from(
8858 "cannot mix `@@` settings with `@` user variables in one SET",
8859 )));
8860 }
8861 self.advance();
8862 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8863 return Err(self.err(alloc::format!(
8864 "expected `=` or `:=` after {raw}, got {:?}",
8865 self.peek()
8866 )));
8867 }
8868 self.advance();
8869 let value = self.parse_expr(0)?;
8870 assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
8871 if matches!(self.peek(), Token::Comma) {
8872 self.advance();
8873 continue;
8874 }
8875 break;
8876 }
8877 Ok(Statement::SetUserVars(assigns, settings))
8878 }
8879
8880 fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
8881 // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
8882 // NAMED `only` until now, which failed on `relation "only" does
8883 // not exist`. The lookahead is what keeps a table actually
8884 // called `only` working: the keyword is only a keyword when a
8885 // TABLE NAME follows it — and `SET` arrives as an identifier
8886 // here, so `UPDATE only SET a = 2` would otherwise take `SET`
8887 // for the table and die on the `=`. Measured by the pin.
8888 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
8889 if s.eq_ignore_ascii_case("only"))
8890 && matches!(
8891 self.tokens.get(self.pos + 1),
8892 Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
8893 );
8894 if only {
8895 self.advance();
8896 }
8897 let table = self.expect_ident_like()?;
8898 // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
8899 // bare spelling; a bare identifier that is the SET keyword itself
8900 // is the clause, not an alias.
8901 // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
8902 // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
8903 // multi-table form, and swallowing `LEFT` as `a`'s alias made the
8904 // following JOIN a syntax error.
8905 let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
8906 let alias = if matches!(self.peek(), Token::As) {
8907 self.advance();
8908 Some(self.expect_ident_like()?)
8909 } else {
8910 match self.peek() {
8911 Token::Ident(s) | Token::QuotedIdent(s)
8912 if !s.eq_ignore_ascii_case("set") && !starts_join =>
8913 {
8914 let a = s.clone();
8915 self.advance();
8916 Some(a)
8917 }
8918 _ => None,
8919 }
8920 };
8921 // v7.39 (round 420) — MySQL's multi-table UPDATE:
8922 // UPDATE a, b SET a.v = b.v WHERE a.id = b.id
8923 // UPDATE a JOIN b ON a.id = b.id SET a.v = b.v + 1
8924 // UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
8925 // The FIRST table is the mutation target and the rest are sources —
8926 // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
8927 // SPG already lowers onto correlated subqueries. So rewind, let
8928 // `parse_from_clause` read the whole list (it handles aliases, comma
8929 // lists, and every JOIN form), then peel the target off the front.
8930 let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
8931 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
8932 {
8933 // NOTE: `advance()` destroys the tokens it returns
8934 // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
8935 // is NOT possible — the tail is read forward, once, through the
8936 // same grammar `parse_from_clause` uses after its primary.
8937 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
8938 let mut joins = self.parse_from_joins(&target_qual)?;
8939 if joins.is_empty() {
8940 return Err(self.err(alloc::string::String::from(
8941 "multi-table UPDATE needs at least one source table",
8942 )));
8943 }
8944 let head = joins.remove(0);
8945 // A LEFT join keeps every target row (the unmatched ones see NULL
8946 // on the source side), so it must NOT get the EXISTS row filter
8947 // the inner / comma forms use.
8948 let outer = matches!(head.kind, crate::ast::JoinKind::Left);
8949 let src = FromClause {
8950 primary: head.table,
8951 joins,
8952 };
8953 (Some(src), head.on, outer)
8954 } else {
8955 (None, None, false)
8956 };
8957 self.expect_keyword_ident("set")?;
8958 let mut assignments = Vec::new();
8959 loop {
8960 // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
8961 // …)` — the parenthesized multi-assignment. Expressions
8962 // assign positionally; a subquery RHS clones per column
8963 // keeping only the Nth projection item.
8964 if matches!(self.peek(), Token::LParen) {
8965 self.advance();
8966 let mut cols = alloc::vec![self.expect_ident_like()?];
8967 while matches!(self.peek(), Token::Comma) {
8968 self.advance();
8969 cols.push(self.expect_ident_like()?);
8970 }
8971 if !matches!(self.peek(), Token::RParen) {
8972 return Err(self.err(format!(
8973 "expected ')' after SET column list, got {:?}",
8974 self.peek()
8975 )));
8976 }
8977 self.advance();
8978 if !matches!(self.peek(), Token::Eq) {
8979 return Err(self.err(format!(
8980 "expected `=` after SET column list, got {:?}",
8981 self.peek()
8982 )));
8983 }
8984 self.advance();
8985 if !matches!(self.peek(), Token::LParen) {
8986 return Err(self.err(format!(
8987 "expected '(' after SET (…) =, got {:?}",
8988 self.peek()
8989 )));
8990 }
8991 self.advance();
8992 if matches!(self.peek(), Token::Select) {
8993 let inner = match self.parse_select_stmt()? {
8994 Statement::Select(s) => s,
8995 other => {
8996 return Err(self.err(alloc::format!(
8997 "expected SELECT in SET (…) = (SELECT …), got {other:?}"
8998 )));
8999 }
9000 };
9001 if !matches!(self.peek(), Token::RParen) {
9002 return Err(self.err(format!(
9003 "expected ')' after SET subquery, got {:?}",
9004 self.peek()
9005 )));
9006 }
9007 self.advance();
9008 if inner.items.len() != cols.len() {
9009 return Err(self.err(alloc::format!(
9010 "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9011 cols.len(),
9012 inner.items.len()
9013 )));
9014 }
9015 for (i, col) in cols.into_iter().enumerate() {
9016 let mut sub = inner.clone();
9017 sub.items = alloc::vec![sub.items[i].clone()];
9018 assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9019 }
9020 } else {
9021 let mut exprs = alloc::vec![self.parse_expr(0)?];
9022 while matches!(self.peek(), Token::Comma) {
9023 self.advance();
9024 exprs.push(self.parse_expr(0)?);
9025 }
9026 if !matches!(self.peek(), Token::RParen) {
9027 return Err(self.err(format!(
9028 "expected ')' after SET row values, got {:?}",
9029 self.peek()
9030 )));
9031 }
9032 self.advance();
9033 if exprs.len() != cols.len() {
9034 return Err(self.err(alloc::format!(
9035 "SET (…) = (…) arity mismatch: {} columns, {} values",
9036 cols.len(),
9037 exprs.len()
9038 )));
9039 }
9040 for (col, e) in cols.into_iter().zip(exprs) {
9041 assignments.push((col, e));
9042 }
9043 }
9044 if matches!(self.peek(), Token::Comma) {
9045 self.advance();
9046 continue;
9047 }
9048 break;
9049 }
9050 // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9051 // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9052 // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9053 // `public.` dump qualifiers), so the qualifier has to be read off
9054 // the token stream first — otherwise `SET b.v = 888` would write
9055 // to the TARGET table's `v` while naming a source table, a
9056 // silent-wrong. A qualifier naming a SOURCE table means a
9057 // multi-TARGET update — mutating two tables in one statement —
9058 // which SPG does not model, so it is refused loudly.
9059 let set_qual: Option<String> = if mysql_from.is_some()
9060 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9061 {
9062 match self.peek() {
9063 Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9064 _ => None,
9065 }
9066 } else {
9067 None
9068 };
9069 let col = self.expect_ident_like()?;
9070 if let Some(q) = set_qual {
9071 let names_target = q.eq_ignore_ascii_case(&table)
9072 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9073 if !names_target {
9074 return Err(self.err(alloc::format!(
9075 "multi-table UPDATE can only assign to its first table \
9076 ({table}); `{q}.{col}` targets another table"
9077 )));
9078 }
9079 }
9080 // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9081 // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9082 // `__column_default` marker lowering just below). PG assigns to the
9083 // i-th (1-based) element, NULL-padding when i exceeds the length.
9084 if matches!(self.peek(), Token::LBracket) {
9085 self.advance();
9086 let index = self.parse_expr(0)?;
9087 // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9088 // (and the open `arr[lo:]`), lowered to
9089 // `__array_assign_slice`. Only the single-subscript form
9090 // parsed before, so a slice assignment was a syntax error.
9091 let mut slice_hi: Option<Option<Expr>> = None;
9092 if matches!(self.peek(), Token::Colon) {
9093 self.advance();
9094 slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9095 None
9096 } else {
9097 Some(self.parse_expr(0)?)
9098 });
9099 }
9100 if !matches!(self.peek(), Token::RBracket) {
9101 return Err(self.err(format!(
9102 "expected `]` after array subscript in UPDATE SET, got {:?}",
9103 self.peek()
9104 )));
9105 }
9106 self.advance();
9107 if !matches!(self.peek(), Token::Eq) {
9108 return Err(self.err(format!(
9109 "expected `=` after array subscript in UPDATE SET, got {:?}",
9110 self.peek()
9111 )));
9112 }
9113 self.advance();
9114 let value = self.parse_expr(0)?;
9115 // PG merges several subscript writes to the same column into one
9116 // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9117 // assignment to `col` rather than each overwriting the original.
9118 let existing = assignments.iter().position(|(c, _)| c == &col);
9119 let base = match existing {
9120 Some(i) => assignments[i].1.clone(),
9121 None => Expr::Column(ColumnName {
9122 qualifier: None,
9123 name: col.clone(),
9124 }),
9125 };
9126 let assigned = match slice_hi {
9127 None => Expr::FunctionCall {
9128 name: "__array_assign".to_string(),
9129 args: alloc::vec![base, index, value],
9130 },
9131 Some(hi) => Expr::FunctionCall {
9132 name: "__array_assign_slice".to_string(),
9133 args: alloc::vec![
9134 base,
9135 index,
9136 hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9137 value,
9138 ],
9139 },
9140 };
9141 match existing {
9142 Some(i) => assignments[i].1 = assigned,
9143 None => assignments.push((col, assigned)),
9144 }
9145 if matches!(self.peek(), Token::Comma) {
9146 self.advance();
9147 continue;
9148 }
9149 break;
9150 }
9151 if !matches!(self.peek(), Token::Eq) {
9152 return Err(self.err(format!(
9153 "expected `=` after column name in UPDATE SET, got {:?}",
9154 self.peek()
9155 )));
9156 }
9157 self.advance();
9158 // `SET col = DEFAULT` — the column's declared default;
9159 // rides out as a marker call the update executor
9160 // resolves against the schema.
9161 let value = if matches!(self.peek(), Token::Default) {
9162 self.advance();
9163 Expr::FunctionCall {
9164 name: "__column_default".to_string(),
9165 args: Vec::new(),
9166 }
9167 } else {
9168 self.parse_expr(0)?
9169 };
9170 assignments.push((col, value));
9171 if matches!(self.peek(), Token::Comma) {
9172 self.advance();
9173 continue;
9174 }
9175 break;
9176 }
9177 // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9178 // update. Lowers onto the correlated-subquery machinery:
9179 // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9180 // and each assignment that references a FROM-list table
9181 // wraps into a correlated scalar subquery
9182 // (SELECT expr FROM src WHERE cond). Equivalent for the
9183 // unique-join shape (the overwhelmingly common one); a
9184 // multi-match, which PG resolves by arbitrary pick,
9185 // surfaces as a scalar-subquery cardinality error instead
9186 // of a silent arbitrary result.
9187 // v7.39 (round 420) — the MySQL multi-table form supplies the source
9188 // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9189 // the SAME lowering below. Both spellings together is not legal in
9190 // either dialect.
9191 let from_clause = if let Some(fc) = mysql_from {
9192 if matches!(self.peek(), Token::From) {
9193 return Err(self.err(alloc::string::String::from(
9194 "multi-table UPDATE already names its sources; drop the FROM clause",
9195 )));
9196 }
9197 Some(fc)
9198 } else if matches!(self.peek(), Token::From) {
9199 self.advance();
9200 Some(self.parse_from_clause()?)
9201 } else {
9202 None
9203 };
9204 let where_ = if matches!(self.peek(), Token::Where) {
9205 self.advance();
9206 Some(self.parse_expr(0)?)
9207 } else {
9208 None
9209 };
9210 // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9211 // and the TARGET-row filter are NOT the same predicate once a LEFT
9212 // join is involved:
9213 // * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9214 // one conjunction, and the whole thing filters target rows via
9215 // EXISTS.
9216 // * LEFT join: only the ON predicate belongs inside the source
9217 // subquery. The WHERE still filters TARGET rows (with source
9218 // columns read through the correlated subquery, which yields NULL
9219 // for an unmatched row — exactly LEFT-join semantics).
9220 // Round 420 folded ON into WHERE unconditionally and then dropped the
9221 // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9222 // WHERE a.id > 1` updated EVERY row.
9223 let sub_where = match (mysql_on.clone(), where_.clone()) {
9224 _ if mysql_outer => mysql_on.clone(),
9225 (Some(on), Some(w)) => Some(Expr::Binary {
9226 lhs: Box::new(on),
9227 op: crate::ast::BinOp::And,
9228 rhs: Box::new(w),
9229 }),
9230 (Some(on), None) => Some(on),
9231 (None, w) => w,
9232 };
9233 // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9234 // has no such clause on UPDATE, so this is accepted only under the
9235 // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9236 let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9237 let mut returning = self.parse_optional_returning()?;
9238 // v7.39 (round 533) — kept for the engine, which can resolve the
9239 // UNQUALIFIED leaves this lowering has to leave alone.
9240 let from_sources = from_clause.as_ref().map(|fc| {
9241 alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9242 from: fc.clone(),
9243 sub_where: sub_where.clone(),
9244 })
9245 });
9246 let (assignments, where_) = if let Some(fc) = from_clause {
9247 let names: Vec<String> = core::iter::once(&fc.primary)
9248 .chain(fc.joins.iter().map(|j| &j.table))
9249 .flat_map(|t| {
9250 t.alias
9251 .clone()
9252 .into_iter()
9253 .chain(core::iter::once(t.name.clone()))
9254 })
9255 .collect();
9256 let refs_list = |e: &Expr| -> bool {
9257 fn walk(e: &Expr, names: &[String]) -> bool {
9258 match e {
9259 Expr::Column(c) => c
9260 .qualifier
9261 .as_deref()
9262 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9263 Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9264 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9265 Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9266 Expr::Case {
9267 operand,
9268 branches,
9269 else_branch,
9270 } => {
9271 operand.as_deref().is_some_and(|o| walk(o, names))
9272 || branches
9273 .iter()
9274 .any(|(w, t)| walk(w, names) || walk(t, names))
9275 || else_branch.as_deref().is_some_and(|el| walk(el, names))
9276 }
9277 _ => false,
9278 }
9279 }
9280 walk(e, &names)
9281 };
9282 let sub_select = |items: Vec<SelectItem>| SelectStatement {
9283 locking: None,
9284 ctes: Vec::new(),
9285 distinct: false,
9286 distinct_on: Vec::new(),
9287 items,
9288 from: Some(fc.clone()),
9289 where_: sub_where.clone(),
9290 group_by: None,
9291 group_by_all: false,
9292 having: None,
9293 unions: Vec::new(),
9294 order_by: Vec::new(),
9295 limit: None,
9296 offset: None,
9297 limit_with_ties: false,
9298 window_check_exprs: Vec::new(),
9299 };
9300 // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9301 // assignment RHS with a correlated scalar subquery, instead of
9302 // wrapping the whole RHS. Wrapping the whole expr moved a target-
9303 // column reference (`SET v = v + u.bonus`, where `v` is the target
9304 // table's column) inside a subquery whose FROM only has the source
9305 // table, so the unqualified `v` resolved against the source and
9306 // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9307 // context — where they belong — fixes it; only the source columns
9308 // (`u.bonus`) become subqueries. A whole-expr fallback covers
9309 // compound variants the leaf-walk doesn't decompose.
9310 let make_subq = |inner: Expr| {
9311 Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9312 expr: inner,
9313 alias: None,
9314 }])))
9315 };
9316 let assignments = assignments
9317 .into_iter()
9318 .map(|(col, mut expr)| {
9319 wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9320 (col, expr)
9321 })
9322 .collect();
9323 let exists = Expr::Exists {
9324 subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9325 expr: Expr::Literal(Literal::Integer(1)),
9326 alias: None,
9327 }])),
9328 negated: false,
9329 };
9330 // v7.39 (round 241) — RETURNING may reference the FROM-list
9331 // tables too (`RETURNING emp.id, dept.name`); the same
9332 // leaf-to-correlated-subquery lowering the assignments get.
9333 // Without it the qualifier died at eval with "unknown table
9334 // qualifier". (RETURNING was parsed before this block — the
9335 // lowering is a pure AST transformation.)
9336 if let Some(items) = returning.as_mut() {
9337 for item in items.iter_mut() {
9338 if let SelectItem::Expr { expr, .. } = item {
9339 wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9340 }
9341 }
9342 }
9343 // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9344 // EVERY matching target row: it gets no EXISTS filter, but the
9345 // caller's WHERE still applies, with source columns read through
9346 // the correlated subquery (NULL when unmatched — LEFT-join
9347 // semantics). `sub_where` above already excluded the WHERE from
9348 // the source subquery for this case.
9349 if mysql_outer {
9350 let mut outer = where_;
9351 if let Some(w) = outer.as_mut() {
9352 wrap_from_leaves(w, &names, &make_subq, &refs_list);
9353 }
9354 (assignments, outer)
9355 } else {
9356 (assignments, Some(exists))
9357 }
9358 } else {
9359 (assignments, where_)
9360 };
9361 Ok(Statement::Update(crate::ast::UpdateStatement {
9362 ctes: Vec::new(),
9363 table,
9364 only,
9365 alias,
9366 assignments,
9367 from_sources,
9368 where_,
9369 order_limit: update_order_limit,
9370 returning,
9371 }))
9372 }
9373
9374 /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9375 /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9376 /// clause and its meaning are identical, so both call this rather than
9377 /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9378 /// legal. PG has no such clause on either statement, so it is read only
9379 /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9380 /// errors.
9381 ///
9382 /// `#[inline(never)]`: its locals would otherwise land on the statement-
9383 /// parsing recursion frame, which is what tipped the 512 KiB nesting
9384 /// stack in round 430.
9385 #[inline(never)]
9386 fn parse_mysql_dml_order_limit(
9387 &mut self,
9388 what: &str,
9389 ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9390 if !self.mysql_dialect {
9391 return Ok(None);
9392 }
9393 let order_by = self.parse_order_by_keys()?;
9394 let limit = if matches!(self.peek(), Token::Limit) {
9395 self.advance();
9396 let tok = self.advance();
9397 let Token::Integer(n) = tok else {
9398 return Err(self.err(alloc::format!(
9399 "expected integer after {what} LIMIT, got {tok:?}"
9400 )));
9401 };
9402 // MySQL rejects the `LIMIT offset, count` form here — only a
9403 // single row count is legal on a DML statement.
9404 if matches!(self.peek(), Token::Comma) {
9405 return Err(self.err(alloc::format!(
9406 "{what} LIMIT takes a row count, not an offset"
9407 )));
9408 }
9409 let n = u32::try_from(n)
9410 .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9411 Some(n)
9412 } else {
9413 None
9414 };
9415 if order_by.is_empty() && limit.is_none() {
9416 return Ok(None);
9417 }
9418 Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9419 order_by,
9420 limit,
9421 })))
9422 }
9423
9424 /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9425 /// the leading `DELETE` ident.
9426 fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9427 // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9428 // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9429 // USING a, b WHERE …` — the third MySQL spelling — needs no special
9430 // parse here; it reaches the existing USING path with the target
9431 // repeated in the list, which the source-list peel below handles.)
9432 // More than one name is a multi-TARGET delete, which SPG does not
9433 // model; it is refused rather than half-applied.
9434 let mysql_pre_target: Option<String> =
9435 if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9436 let first = self.expect_ident_like()?;
9437 if matches!(self.peek(), Token::Comma) {
9438 return Err(self.err(alloc::format!(
9439 "multi-table DELETE can only delete from one table; \
9440 `DELETE {first}, …` names several"
9441 )));
9442 }
9443 Some(first)
9444 } else {
9445 None
9446 };
9447 if !matches!(self.peek(), Token::From) {
9448 return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9449 }
9450 self.advance();
9451 // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9452 // lookahead as the UPDATE spelling.
9453 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9454 if s.eq_ignore_ascii_case("only"))
9455 && matches!(
9456 self.tokens.get(self.pos + 1),
9457 Some(Token::Ident(_) | Token::QuotedIdent(_))
9458 );
9459 if only {
9460 self.advance();
9461 }
9462 let table = self.expect_ident_like()?;
9463 // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9464 // spelling must not swallow the clause keywords that can follow
9465 // the target.
9466 let alias = if matches!(self.peek(), Token::As) {
9467 self.advance();
9468 Some(self.expect_ident_like()?)
9469 } else {
9470 match self.peek() {
9471 Token::Ident(s) | Token::QuotedIdent(s)
9472 if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9473 {
9474 let a = s.clone();
9475 self.advance();
9476 Some(a)
9477 }
9478 _ => None,
9479 }
9480 };
9481 // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9482 // through the SAME join grammar the FROM clause uses (see the
9483 // `advance()`-destroys-tokens note on `parse_from_joins`).
9484 let mut mysql_on: Option<Expr> = None;
9485 let mut mysql_outer = false;
9486 let mysql_using = if mysql_pre_target.is_some()
9487 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9488 {
9489 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9490 let mut joins = self.parse_from_joins(&target_qual)?;
9491 if joins.is_empty() {
9492 return Err(self.err(alloc::string::String::from(
9493 "multi-table DELETE needs at least one source table",
9494 )));
9495 }
9496 let head = joins.remove(0);
9497 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9498 mysql_on = head.on;
9499 Some(FromClause {
9500 primary: head.table,
9501 joins,
9502 })
9503 } else {
9504 None
9505 };
9506 // The pre-FROM target must be the table the FROM names (or its
9507 // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9508 // is not the scan target.
9509 if let Some(t) = &mysql_pre_target {
9510 let names_target = t.eq_ignore_ascii_case(&table)
9511 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9512 if !names_target {
9513 return Err(self.err(alloc::format!(
9514 "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9515 )));
9516 }
9517 }
9518 // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9519 // delete. Same lowering as UPDATE … FROM: the WHERE
9520 // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9521 // target row by the correlated machinery.
9522 let using_clause = if let Some(fc) = mysql_using {
9523 Some(fc)
9524 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9525 self.advance();
9526 let mut fc = self.parse_from_clause()?;
9527 // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9528 // repeats the TARGET as the first USING entry (PG's spelling
9529 // lists only the extra sources). Peel it so the source subquery
9530 // does not re-scan — and shadow — the target table.
9531 let primary_is_target =
9532 fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9533 if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9534 let head = fc.joins.remove(0);
9535 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9536 mysql_on = head.on;
9537 fc = FromClause {
9538 primary: head.table,
9539 joins: fc.joins,
9540 };
9541 }
9542 Some(fc)
9543 } else {
9544 None
9545 };
9546 let where_ = if matches!(self.peek(), Token::Where) {
9547 self.advance();
9548 Some(self.parse_expr(0)?)
9549 } else {
9550 None
9551 };
9552 // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9553 // read before RETURNING (MariaDB's own extension trails the LIMIT).
9554 let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9555 let mut returning = self.parse_optional_returning()?;
9556 let where_ = if let Some(fc) = using_clause {
9557 // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9558 // a USING-table reference in RETURNING becomes a correlated
9559 // scalar subquery over the USING list.
9560 let names: Vec<String> = core::iter::once(&fc.primary)
9561 .chain(fc.joins.iter().map(|j| &j.table))
9562 .flat_map(|t| {
9563 t.alias
9564 .clone()
9565 .into_iter()
9566 .chain(core::iter::once(t.name.clone()))
9567 })
9568 .collect();
9569 // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9570 // join filters the SOURCE subquery on the ON predicate alone and
9571 // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9572 // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9573 // rows); every other form folds ON and WHERE into one EXISTS.
9574 let sub_where = match (mysql_on.clone(), where_.clone()) {
9575 _ if mysql_outer => mysql_on.clone(),
9576 (Some(on), Some(w)) => Some(Expr::Binary {
9577 lhs: Box::new(on),
9578 op: crate::ast::BinOp::And,
9579 rhs: Box::new(w),
9580 }),
9581 (Some(on), None) => Some(on),
9582 (None, w) => w,
9583 };
9584 let exists_where = sub_where.clone();
9585 let sub_fc = fc.clone();
9586 let make_subq = move |leaf: Expr| -> Expr {
9587 Expr::ScalarSubquery(Box::new(SelectStatement {
9588 locking: None,
9589 ctes: Vec::new(),
9590 distinct: false,
9591 distinct_on: Vec::new(),
9592 items: alloc::vec![SelectItem::Expr {
9593 expr: leaf,
9594 alias: None,
9595 }],
9596 from: Some(sub_fc.clone()),
9597 where_: sub_where.clone(),
9598 group_by: None,
9599 group_by_all: false,
9600 having: None,
9601 unions: Vec::new(),
9602 order_by: Vec::new(),
9603 limit: None,
9604 offset: None,
9605 limit_with_ties: false,
9606 window_check_exprs: Vec::new(),
9607 }))
9608 };
9609 let refs = |e: &Expr| expr_refs_tables(e, &names);
9610 if let Some(items) = returning.as_mut() {
9611 for item in items.iter_mut() {
9612 if let SelectItem::Expr { expr, .. } = item {
9613 wrap_from_leaves(expr, &names, &make_subq, &refs);
9614 }
9615 }
9616 }
9617 // A LEFT join deletes the target rows the WHERE selects, reading
9618 // source columns through the correlated subquery (NULL when
9619 // unmatched); no EXISTS row filter.
9620 if mysql_outer {
9621 let mut outer = where_;
9622 if let Some(w) = outer.as_mut() {
9623 wrap_from_leaves(w, &names, &make_subq, &refs);
9624 }
9625 outer
9626 } else {
9627 Some(Expr::Exists {
9628 subquery: Box::new(SelectStatement {
9629 locking: None,
9630 ctes: Vec::new(),
9631 distinct: false,
9632 distinct_on: Vec::new(),
9633 items: alloc::vec![SelectItem::Expr {
9634 expr: Expr::Literal(Literal::Integer(1)),
9635 alias: None,
9636 }],
9637 from: Some(fc),
9638 where_: exists_where,
9639 group_by: None,
9640 group_by_all: false,
9641 having: None,
9642 unions: Vec::new(),
9643 order_by: Vec::new(),
9644 limit: None,
9645 offset: None,
9646 limit_with_ties: false,
9647 window_check_exprs: Vec::new(),
9648 }),
9649 negated: false,
9650 })
9651 }
9652 } else {
9653 where_
9654 };
9655 Ok(Statement::Delete(crate::ast::DeleteStatement {
9656 ctes: Vec::new(),
9657 table,
9658 only,
9659 alias,
9660 where_,
9661 order_limit: delete_order_limit,
9662 returning,
9663 }))
9664 }
9665
9666 /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
9667 /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
9668 /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
9669 /// keyword. v7.17 surface:
9670 /// * source: table reference (subquery source is a follow-up)
9671 /// * actions: UPDATE SET / DELETE / DO NOTHING (matched);
9672 /// INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
9673 /// * AND-conditioned WHEN clauses; clauses tried in declaration
9674 /// order
9675 fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
9676 // INTO
9677 let is_into_kw = matches!(self.peek(), Token::Into)
9678 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
9679 if !is_into_kw {
9680 return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
9681 }
9682 self.advance();
9683 let target = self.expect_ident_like()?;
9684 // Optional alias — bare ident before USING.
9685 let target_alias = match self.peek() {
9686 Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
9687 Some(self.expect_ident_like()?)
9688 }
9689 _ => None,
9690 };
9691 // USING
9692 let is_using_kw = matches!(
9693 self.peek(),
9694 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
9695 );
9696 if !is_using_kw {
9697 return Err(self.err(format!(
9698 "expected USING after MERGE INTO target, got {:?}",
9699 self.peek()
9700 )));
9701 }
9702 self.advance();
9703 // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
9704 // <table> [alias]`. PG requires an alias after a subquery source.
9705 let (source, source_select) = if matches!(self.peek(), Token::LParen) {
9706 self.advance(); // (
9707 // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
9708 // constant-SELECT lowering the derived-table parser uses
9709 // (PG deletes through this form; it was a parse error).
9710 let inner = if matches!(self.peek(), Token::Values) {
9711 self.advance(); // VALUES
9712 Statement::Select(self.parse_values_rows_body()?)
9713 } else {
9714 self.parse_select_stmt()?
9715 };
9716 match self.advance() {
9717 Token::RParen => {}
9718 other => {
9719 return Err(self.err(format!(
9720 "expected ')' after MERGE USING subquery, got {other:?}"
9721 )));
9722 }
9723 }
9724 let Statement::Select(sub) = inner else {
9725 return Err(self.err("MERGE USING subquery must be a SELECT".into()));
9726 };
9727 (String::new(), Some(Box::new(sub)))
9728 } else {
9729 (self.expect_ident_like()?, None)
9730 };
9731 let source_alias = match self.peek() {
9732 Token::Ident(s) | Token::QuotedIdent(s)
9733 if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
9734 {
9735 Some(self.expect_ident_like()?)
9736 }
9737 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
9738 self.advance(); // AS
9739 Some(self.expect_ident_like()?)
9740 }
9741 _ => None,
9742 };
9743 // v7.39 (round 768, F31-D5) — optional positional column-alias
9744 // list after the source alias (`s(id, v)`).
9745 let mut source_column_aliases: Vec<String> = Vec::new();
9746 if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
9747 self.advance();
9748 loop {
9749 source_column_aliases.push(self.expect_ident_like()?);
9750 match self.peek() {
9751 Token::Comma => {
9752 self.advance();
9753 }
9754 Token::RParen => {
9755 self.advance();
9756 break;
9757 }
9758 other => {
9759 return Err(self.err(format!(
9760 "expected ',' or ')' in MERGE source column list, got {other:?}"
9761 )));
9762 }
9763 }
9764 }
9765 }
9766 if source_select.is_some() && source_alias.is_none() {
9767 return Err(self.err("MERGE USING (subquery) requires an alias".into()));
9768 }
9769 // ON
9770 if !matches!(self.peek(), Token::On) {
9771 return Err(self.err(format!(
9772 "expected ON after MERGE … USING source, got {:?}",
9773 self.peek()
9774 )));
9775 }
9776 self.advance();
9777 let on = self.parse_expr(0)?;
9778 // One or more WHEN clauses.
9779 let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
9780 loop {
9781 let is_when_kw = matches!(
9782 self.peek(),
9783 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
9784 );
9785 if !is_when_kw {
9786 break;
9787 }
9788 self.advance(); // WHEN
9789 // [NOT] MATCHED
9790 let matched = if matches!(self.peek(), Token::Not) {
9791 self.advance();
9792 crate::ast::MergeMatched::NotMatched
9793 } else {
9794 crate::ast::MergeMatched::Matched
9795 };
9796 let is_matched_kw = matches!(
9797 self.peek(),
9798 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
9799 );
9800 if !is_matched_kw {
9801 return Err(self.err(format!(
9802 "expected MATCHED in WHEN clause, got {:?}",
9803 self.peek()
9804 )));
9805 }
9806 self.advance();
9807 // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
9808 // BY TARGET is the default (a synonym); BY SOURCE flips the clause
9809 // to fire for target rows no source row matches.
9810 let mut matched = matched;
9811 if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
9812 self.advance();
9813 match self.peek() {
9814 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
9815 self.advance();
9816 matched = crate::ast::MergeMatched::NotMatchedBySource;
9817 }
9818 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
9819 self.advance();
9820 }
9821 other => {
9822 return Err(self.err(format!(
9823 "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
9824 )));
9825 }
9826 }
9827 }
9828 // Optional AND <expr>
9829 let condition = if matches!(self.peek(), Token::And) {
9830 self.advance();
9831 Some(self.parse_expr(0)?)
9832 } else {
9833 None
9834 };
9835 // THEN
9836 let is_then_kw = matches!(
9837 self.peek(),
9838 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
9839 );
9840 if !is_then_kw {
9841 return Err(self.err(format!(
9842 "expected THEN in WHEN clause, got {:?}",
9843 self.peek()
9844 )));
9845 }
9846 self.advance();
9847 // Action: INSERT / UPDATE / DELETE / DO NOTHING
9848 let action = match self.peek().clone() {
9849 Token::Insert => {
9850 self.advance();
9851 // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
9852 // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
9853 // VALUES (…)` omits it and fills every column in declaration
9854 // order. PG accepts this; SPG used to require the list.
9855 let mut columns: Vec<String> = Vec::new();
9856 if matches!(self.peek(), Token::LParen) {
9857 self.advance();
9858 loop {
9859 columns.push(self.expect_ident_like()?);
9860 if matches!(self.peek(), Token::Comma) {
9861 self.advance();
9862 continue;
9863 }
9864 break;
9865 }
9866 if !matches!(self.peek(), Token::RParen) {
9867 return Err(self.err(format!(
9868 "expected ')' after INSERT column list, got {:?}",
9869 self.peek()
9870 )));
9871 }
9872 self.advance();
9873 }
9874 // VALUES (...)
9875 if !matches!(self.peek(), Token::Values) {
9876 return Err(self.err(format!(
9877 "expected VALUES in MERGE INSERT, got {:?}",
9878 self.peek()
9879 )));
9880 }
9881 self.advance();
9882 if !matches!(self.peek(), Token::LParen) {
9883 return Err(self.err(format!(
9884 "expected '(' after VALUES in MERGE INSERT, got {:?}",
9885 self.peek()
9886 )));
9887 }
9888 self.advance();
9889 let mut values: Vec<crate::ast::Expr> = Vec::new();
9890 loop {
9891 values.push(self.parse_expr(0)?);
9892 if matches!(self.peek(), Token::Comma) {
9893 self.advance();
9894 continue;
9895 }
9896 break;
9897 }
9898 if !matches!(self.peek(), Token::RParen) {
9899 return Err(self.err(format!(
9900 "expected ')' after MERGE INSERT values, got {:?}",
9901 self.peek()
9902 )));
9903 }
9904 self.advance();
9905 // Empty column list = positional into every column, so the
9906 // count is checked against the table arity at execution.
9907 if !columns.is_empty() && columns.len() != values.len() {
9908 return Err(self.err(format!(
9909 "MERGE INSERT column count ({}) ≠ value count ({})",
9910 columns.len(),
9911 values.len()
9912 )));
9913 }
9914 crate::ast::MergeAction::Insert { columns, values }
9915 }
9916 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
9917 self.advance();
9918 // SET
9919 let is_set_kw = matches!(
9920 self.peek(),
9921 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
9922 );
9923 if !is_set_kw {
9924 return Err(self.err(format!(
9925 "expected SET after UPDATE in MERGE, got {:?}",
9926 self.peek()
9927 )));
9928 }
9929 self.advance();
9930 let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
9931 loop {
9932 let col = self.expect_ident_like()?;
9933 if !matches!(self.peek(), Token::Eq) {
9934 return Err(self.err(format!(
9935 "expected '=' in MERGE UPDATE assignment, got {:?}",
9936 self.peek()
9937 )));
9938 }
9939 self.advance();
9940 let expr = self.parse_expr(0)?;
9941 assignments.push((col, expr));
9942 if matches!(self.peek(), Token::Comma) {
9943 self.advance();
9944 continue;
9945 }
9946 break;
9947 }
9948 crate::ast::MergeAction::Update { assignments }
9949 }
9950 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
9951 self.advance();
9952 crate::ast::MergeAction::Delete
9953 }
9954 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
9955 self.advance();
9956 let is_nothing_kw = matches!(
9957 self.peek(),
9958 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
9959 );
9960 if !is_nothing_kw {
9961 return Err(self.err(format!(
9962 "expected NOTHING after DO in MERGE clause, got {:?}",
9963 self.peek()
9964 )));
9965 }
9966 self.advance();
9967 crate::ast::MergeAction::DoNothing
9968 }
9969 other => {
9970 return Err(self.err(format!(
9971 "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
9972 )));
9973 }
9974 };
9975 // PG's grammar simply has no INSERT production under BY SOURCE
9976 // (a target row already exists there) — same syntax error.
9977 if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
9978 && matches!(action, crate::ast::MergeAction::Insert { .. })
9979 {
9980 return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
9981 }
9982 clauses.push(crate::ast::MergeWhenClause {
9983 matched,
9984 condition,
9985 action,
9986 });
9987 }
9988 if clauses.is_empty() {
9989 return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
9990 }
9991 // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
9992 // unconditional (no `AND`) WHEN of the same match kind: it could
9993 // never fire. Check per match kind in clause order.
9994 let mut seen_unconditional_matched = false;
9995 let mut seen_unconditional_not_matched = false;
9996 let mut seen_unconditional_by_source = false;
9997 for c in &clauses {
9998 let seen = match c.matched {
9999 crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10000 crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10001 crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10002 };
10003 if *seen {
10004 return Err(self.err(String::from(
10005 "unreachable WHEN clause specified after unconditional WHEN clause",
10006 )));
10007 }
10008 if c.condition.is_none() {
10009 *seen = true;
10010 }
10011 }
10012 // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10013 let returning = self.parse_optional_returning()?;
10014 Ok(Statement::Merge(crate::ast::MergeStatement {
10015 // Attached by `parse_with_cte_then_select` when the MERGE
10016 // heads a WITH clause (round 149).
10017 ctes: Vec::new(),
10018 target,
10019 target_alias,
10020 source,
10021 source_alias,
10022 source_select,
10023 source_column_aliases,
10024 on,
10025 clauses,
10026 returning,
10027 }))
10028 }
10029
10030 /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10031 /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10032 /// as SELECT, so `RETURNING *`, `RETURNING col`,
10033 /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10034 fn parse_optional_returning(
10035 &mut self,
10036 ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10037 let is_returning_kw = matches!(
10038 self.peek(),
10039 Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10040 );
10041 if !is_returning_kw {
10042 return Ok(None);
10043 }
10044 self.advance();
10045 let mut items = Vec::new();
10046 loop {
10047 items.push(self.parse_select_item()?);
10048 if matches!(self.peek(), Token::Comma) {
10049 self.advance();
10050 continue;
10051 }
10052 break;
10053 }
10054 Ok(Some(items))
10055 }
10056
10057 /// v6.0.4 — parse the tail of an ALTER statement after the
10058 /// leading `ALTER` keyword has been consumed. Only one form is
10059 /// supported in v6.0.4:
10060 ///
10061 /// ```text
10062 /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10063 /// ```
10064 fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10065 // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10066 // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10067 // exclusion) is accepted by stripping the `ONLY` keyword
10068 // before the table parse.
10069 // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10070 // and the long PG-dump tail are accepted as no-ops.
10071 match self.advance() {
10072 Token::Index => {}
10073 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10074 // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10075 // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10076 Token::Table => {
10077 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10078 self.advance();
10079 }
10080 return self.parse_alter_table_after_keyword();
10081 }
10082 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10083 return self.parse_alter_policy_after_keyword();
10084 }
10085 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10086 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10087 self.advance();
10088 }
10089 return self.parse_alter_table_after_keyword();
10090 }
10091 // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10092 // of the silent-noop tail.
10093 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10094 return self.parse_alter_sequence_after_keyword();
10095 }
10096 // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10097 // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10098 // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10099 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10100 // NB: the match arm consumed `TYPE` via self.advance(); the
10101 // cursor is now at the type name — do NOT advance again.
10102 let type_name = self.expect_ident_like()?;
10103 let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10104 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10105 if is_add_value {
10106 self.advance(); // ADD
10107 self.advance(); // VALUE
10108 // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10109 // IF/EXISTS as identifiers.
10110 let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10111 {
10112 let n1 = self.tokens.get(self.pos + 1);
10113 let n2 = self.tokens.get(self.pos + 2);
10114 if matches!(n1, Some(Token::Not))
10115 && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10116 {
10117 self.advance();
10118 self.advance();
10119 self.advance();
10120 true
10121 } else {
10122 false
10123 }
10124 } else {
10125 false
10126 };
10127 let label = self.expect_string_literal()?;
10128 let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10129 {
10130 let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10131 self.advance();
10132 let anchor = self.expect_string_literal()?;
10133 Some((is_before, anchor))
10134 } else {
10135 None
10136 };
10137 return Ok(Statement::AlterTypeAddValue {
10138 type_name,
10139 label,
10140 if_not_exists,
10141 position,
10142 });
10143 }
10144 // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10145 // Used to fall into the no-op tail below: accepted, silently
10146 // ignored. `RENAME TO <newtype>` keeps falling through.
10147 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10148 && matches!(
10149 self.tokens.get(self.pos + 1),
10150 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10151 )
10152 {
10153 self.advance(); // RENAME
10154 self.advance(); // VALUE
10155 let old = self.expect_string_literal()?;
10156 if matches!(self.peek(), Token::To) {
10157 self.advance();
10158 } else {
10159 self.expect_keyword_ident("to")?;
10160 }
10161 let new = self.expect_string_literal()?;
10162 return Ok(Statement::AlterTypeRenameValue {
10163 type_name,
10164 old,
10165 new,
10166 });
10167 }
10168 // Other ALTER TYPE forms — the ACTION stays a no-op
10169 // (pg_dump tail), but v7.39 (round 708) the NAME is
10170 // validated: `ALTER TYPE nosuch RENAME TO x` reported
10171 // success for a type that does not exist.
10172 self.consume_until_statement_boundary();
10173 return Ok(Statement::ValidateOnly {
10174 kind: crate::ast::ValidateOnlyKind::TypeName,
10175 names: alloc::vec![type_name],
10176 });
10177 }
10178 // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10179 // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10180 // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10181 // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10182 // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10183 // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10184 // pg_dump no-op list below: every form used to report success
10185 // and change nothing, which is worse than refusing outright
10186 // (a migration dropping a constraint kept rejecting data).
10187 // NOTE: the enclosing `match self.advance()` already consumed
10188 // the DOMAIN keyword, so the name is next.
10189 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10190 return self.parse_alter_domain_after_keyword();
10191 }
10192 // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10193 // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10194 // used to fall into the pg_dump no-op tail below, so a DBA
10195 // setting a per-role default was told it worked and nothing
10196 // happened. Intercepted here, BEFORE that tail.
10197 // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10198 // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10199 // interception below exists: swallowed with the no-op tail, an
10200 // unknown parameter name was ACCEPTED where PG18 answers
10201 // `unrecognized configuration parameter`. SPG applies nothing
10202 // either way — there is no postgresql.auto.conf — but it now
10203 // says so about a name it does not know.
10204 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10205 // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10206 // already consumed here. An extra advance eats the SET and
10207 // the parameter name is never seen — which is exactly the
10208 // bug a panic in this branch disproved: the branch WAS on
10209 // the path, the reading of it was wrong.
10210 let mut parameter = None;
10211 // SET <name> … | RESET <name> | RESET ALL
10212 if matches!(self.peek(), Token::Ident(k)
10213 if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10214 {
10215 self.advance();
10216 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10217 && !n.eq_ignore_ascii_case("all")
10218 {
10219 self.advance();
10220 // A dotted GUC (`plpgsql.check_asserts`) is two
10221 // tokens; keep the whole name.
10222 let mut full = n;
10223 while matches!(self.peek(), Token::Dot) {
10224 self.advance();
10225 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10226 full.push('.');
10227 full.push_str(&t);
10228 }
10229 }
10230 parameter = Some(full);
10231 }
10232 }
10233 self.consume_until_statement_boundary();
10234 return Ok(Statement::AlterSystem { parameter });
10235 }
10236 Token::Ident(s) | Token::QuotedIdent(s)
10237 if matches!(
10238 s.to_ascii_lowercase().as_str(),
10239 "role" | "user" | "database"
10240 ) && self.peeks_db_role_setting() =>
10241 {
10242 let is_database = s.eq_ignore_ascii_case("database");
10243 return self.parse_db_role_setting(is_database);
10244 }
10245 // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10246 // (the non-SET forms; SET/RESET took the branch above). The
10247 // attributes still no-op — recorded, and the ignored PASSWORD
10248 // is ledgered as its own follow-up — but the ROLE is validated:
10249 // any name was accepted for a role that does not exist.
10250 Token::Ident(s) | Token::QuotedIdent(s)
10251 if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10252 {
10253 // NB: the enclosing `match self.advance()` already consumed
10254 // ROLE/USER — the round-695 trap, hit again in this round's
10255 // first draft (the name was eaten and WITH parsed as the
10256 // role). The cursor is at the name.
10257 let name = self.expect_ident_or_string()?;
10258 // v7.39 (round 750) — scan the attribute tail for
10259 // PASSWORD. Everything else stays a recorded no-op, but
10260 // a dropped credential rotation is a SECURITY bug:
10261 // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10262 // changed nothing, so the old password kept working.
10263 // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10264 // NULL` clears the credential.
10265 let mut password: Option<Option<String>> = None;
10266 loop {
10267 match self.peek() {
10268 Token::Semicolon | Token::Eof => break,
10269 Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10270 self.advance();
10271 match self.advance() {
10272 Token::String(p) => password = Some(Some(p)),
10273 Token::Null => password = Some(None),
10274 Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10275 password = Some(None);
10276 }
10277 other => {
10278 return Err(self.err(alloc::format!(
10279 "expected password string or NULL after PASSWORD, got {other:?}"
10280 )));
10281 }
10282 }
10283 }
10284 _ => {
10285 self.advance();
10286 }
10287 }
10288 }
10289 if name.eq_ignore_ascii_case("all") {
10290 // `ALTER ROLE ALL …` names every role; nothing to check.
10291 return Ok(Statement::Empty);
10292 }
10293 if let Some(pw) = password {
10294 return Ok(Statement::AlterRolePassword { name, password: pw });
10295 }
10296 return Ok(Statement::ValidateOnly {
10297 kind: crate::ast::ValidateOnlyKind::RoleName,
10298 names: alloc::vec![name],
10299 });
10300 }
10301 // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10302 // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10303 // list far enough to validate the NAME; the actions still no-op.
10304 // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10305 // models none of them and their dumps are rare.)
10306 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10307 let name = self.expect_ident_or_string()?;
10308 self.consume_until_statement_boundary();
10309 return Ok(Statement::ValidateOnly {
10310 kind: crate::ast::ValidateOnlyKind::CollationName,
10311 names: alloc::vec![name],
10312 });
10313 }
10314 Token::Ident(s) | Token::QuotedIdent(s)
10315 if s.eq_ignore_ascii_case("text")
10316 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10317 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10318 {
10319 self.advance(); // SEARCH
10320 self.advance(); // CONFIGURATION
10321 let name = self.expect_ident_like()?;
10322 self.consume_until_statement_boundary();
10323 return Ok(Statement::ValidateOnly {
10324 kind: crate::ast::ValidateOnlyKind::TsConfigName,
10325 names: alloc::vec![name],
10326 });
10327 }
10328 Token::Ident(s) | Token::QuotedIdent(s)
10329 if s.eq_ignore_ascii_case("event")
10330 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10331 {
10332 self.advance(); // TRIGGER
10333 let name = self.expect_ident_like()?;
10334 self.consume_until_statement_boundary();
10335 return Ok(Statement::ValidateOnly {
10336 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10337 names: alloc::vec![name],
10338 });
10339 }
10340 Token::Ident(s) | Token::QuotedIdent(s)
10341 if s.eq_ignore_ascii_case("large")
10342 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10343 {
10344 self.advance(); // OBJECT
10345 let oid = match self.advance() {
10346 Token::Integer(n) => alloc::format!("{n}"),
10347 other => {
10348 return Err(
10349 self.err(alloc::format!("expected large object oid, got {other:?}"))
10350 );
10351 }
10352 };
10353 self.consume_until_statement_boundary();
10354 return Ok(Statement::ValidateOnly {
10355 kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10356 names: alloc::vec![oid],
10357 });
10358 }
10359 // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10360 // argument-list parse as DROP AGGREGATE (round 707); the
10361 // action no-ops, the existence check is real.
10362 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10363 // Same round-695 trap as above: AGGREGATE is already
10364 // consumed; the cursor is at the name.
10365 let name = self.expect_ident_like()?;
10366 let mut names = alloc::vec![name];
10367 if matches!(self.peek(), Token::LParen) {
10368 self.advance();
10369 loop {
10370 match self.peek().clone() {
10371 Token::RParen => {
10372 self.advance();
10373 break;
10374 }
10375 Token::Star => {
10376 self.advance();
10377 names.push(String::from("*"));
10378 }
10379 Token::Comma => {
10380 self.advance();
10381 }
10382 _ => {
10383 let mut t = self.expect_ident_like()?;
10384 while let Token::Ident(nx) = self.peek() {
10385 let nx = nx.clone();
10386 self.advance();
10387 t.push(' ');
10388 t.push_str(&nx);
10389 }
10390 names.push(t);
10391 }
10392 }
10393 }
10394 }
10395 self.consume_until_statement_boundary();
10396 return Ok(Statement::ValidateOnly {
10397 kind: crate::ast::ValidateOnlyKind::AggregateName,
10398 names,
10399 });
10400 }
10401 Token::Ident(s) | Token::QuotedIdent(s)
10402 if matches!(
10403 s.to_ascii_lowercase().as_str(),
10404 "view"
10405 | "function"
10406 | "database"
10407 | "schema"
10408 | "owner"
10409 | "default"
10410 | "extension"
10411 | "materialized"
10412 | "publication"
10413 | "subscription"
10414 // v7.37.17 (17.6 siblings) — additional ALTER
10415 // targets pg_dump / pg_dumpall / operator DB
10416 // migration scripts commonly emit. SPG has
10417 // no matching machinery for any of these; the
10418 // parser accepts + Empty-returns so pg_dump
10419 // tail statements don't stall.
10420 | "tablespace"
10421 | "language"
10422 | "operator"
10423 | "conversion"
10424 | "statistics"
10425 | "server"
10426 | "foreign"
10427 // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10428 // / TEMPLATE (CONFIGURATION intercepted above).
10429 | "text"
10430 ) =>
10431 {
10432 self.consume_until_statement_boundary();
10433 return Ok(Statement::Empty);
10434 }
10435 other => {
10436 return Err(self.err(format!(
10437 "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10438 after ALTER, got {other:?}"
10439 )));
10440 }
10441 }
10442 // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10443 // (mailrs migrate-042 ships these). The presence of an
10444 // IF EXISTS makes the subsequent name lookup tolerate
10445 // a missing index — engine returns CommandOk no-op.
10446 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10447 let next = self.tokens.get(self.pos + 1);
10448 if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10449 self.advance();
10450 self.advance();
10451 true
10452 } else {
10453 false
10454 }
10455 } else {
10456 false
10457 };
10458 let name = self.expect_ident_like()?;
10459 // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10460 // Detect BEFORE the REBUILD path so the existing REBUILD
10461 // arm stays untouched.
10462 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10463 self.advance();
10464 if matches!(self.peek(), Token::To) {
10465 self.advance();
10466 } else {
10467 self.expect_keyword_ident("to")?;
10468 }
10469 let new = self.expect_ident_like()?;
10470 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10471 name,
10472 target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10473 }));
10474 }
10475 // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10476 // A syntax error before; the index is validated, the params no-op.
10477 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10478 || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10479 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10480 {
10481 self.consume_until_statement_boundary();
10482 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10483 name,
10484 target: crate::ast::AlterIndexTarget::StorageParams,
10485 }));
10486 }
10487 // REBUILD
10488 self.expect_keyword_ident("rebuild")?;
10489 // Optional: WITH (encoding = <enc>)
10490 let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10491 self.advance();
10492 if !matches!(self.peek(), Token::LParen) {
10493 return Err(self.err(format!(
10494 "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10495 self.peek()
10496 )));
10497 }
10498 self.advance();
10499 self.expect_keyword_ident("encoding")?;
10500 if !matches!(self.peek(), Token::Eq) {
10501 return Err(self.err(format!(
10502 "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10503 self.peek()
10504 )));
10505 }
10506 self.advance();
10507 let enc_ident = match self.advance() {
10508 Token::Ident(s) | Token::QuotedIdent(s) => s,
10509 other => {
10510 return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10511 }
10512 };
10513 let enc = match enc_ident.to_ascii_lowercase().as_str() {
10514 "f32" => VecEncoding::F32,
10515 "sq8" => VecEncoding::Sq8,
10516 "half" => VecEncoding::F16,
10517 other => {
10518 return Err(self.err(format!(
10519 "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10520 )));
10521 }
10522 };
10523 if !matches!(self.peek(), Token::RParen) {
10524 return Err(self.err(format!(
10525 "expected ')' after encoding value, got {:?}",
10526 self.peek()
10527 )));
10528 }
10529 self.advance();
10530 Some(enc)
10531 } else {
10532 None
10533 };
10534 Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10535 name,
10536 target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10537 }))
10538 }
10539
10540 /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10541 /// only `SET` form currently supported; future v6.7.x can add
10542 /// more SET subjects without changing the dispatch shape.
10543 /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10544 /// subactions. Single-subaction shape stays a 1-element vec.
10545 fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10546 let table_name = self.expect_ident_like()?;
10547 let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10548 loop {
10549 let subaction = self.parse_alter_table_subaction()?;
10550 // ADD COLUMN with inline REFERENCES emits both an
10551 // AddColumn and an AddForeignKey subaction; the
10552 // helper returns 1 or 2 items.
10553 targets.extend(subaction);
10554 if matches!(self.peek(), Token::Comma) {
10555 self.advance();
10556 continue;
10557 }
10558 break;
10559 }
10560 Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10561 name: table_name,
10562 targets,
10563 }))
10564 }
10565
10566 /// Parse one ALTER TABLE subaction. Returns a Vec because
10567 /// inline `REFERENCES` on `ADD COLUMN` produces both an
10568 /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10569 fn parse_alter_table_subaction(
10570 &mut self,
10571 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10572 match self.peek() {
10573 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10574 self.advance();
10575 // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10576 // storage parameters: paren-prefixed; consume.
10577 if matches!(self.peek(), Token::LParen) {
10578 self.consume_until_statement_boundary();
10579 return Ok(Vec::new());
10580 }
10581 let setting = self.expect_ident_like()?;
10582 if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10583 if !matches!(self.peek(), Token::Eq) {
10584 return Err(self.err(alloc::format!(
10585 "expected '=' after hot_tier_bytes, got {:?}",
10586 self.peek()
10587 )));
10588 }
10589 self.advance();
10590 let n = self.expect_u64_literal()?;
10591 return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10592 }
10593 // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10594 // accept-and-no-op for ALTER TABLE SET <subject>
10595 // forms that pg_dump emits but SPG either treats
10596 // as N/A (single-tenant, single-owner, no shared
10597 // tablespaces) or accepts the dump-side declaration
10598 // without runtime effect:
10599 // SET SCHEMA <name> (18.11)
10600 // SET TABLESPACE <name> (18.8)
10601 // SET LOGGED / UNLOGGED (18.7 alt-form)
10602 // SET WITHOUT CLUSTER (18.13)
10603 // SET WITHOUT OIDS (PG legacy)
10604 // SET (option = value, …) (storage parameters)
10605 // SET REPLICA IDENTITY {…} (18.14)
10606 if setting.eq_ignore_ascii_case("schema")
10607 || setting.eq_ignore_ascii_case("tablespace")
10608 || setting.eq_ignore_ascii_case("logged")
10609 || setting.eq_ignore_ascii_case("unlogged")
10610 || setting.eq_ignore_ascii_case("without")
10611 {
10612 self.consume_until_statement_boundary();
10613 return Ok(Vec::new());
10614 }
10615 if setting.eq_ignore_ascii_case("replica") {
10616 // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10617 self.consume_until_statement_boundary();
10618 return Ok(Vec::new());
10619 }
10620 // SET (option=value, …) — storage parameters.
10621 if matches!(self.peek(), Token::LParen) {
10622 self.consume_until_statement_boundary();
10623 return Ok(Vec::new());
10624 }
10625 Err(self.err(alloc::format!(
10626 "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10627 hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10628 WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10629 )))
10630 }
10631 // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10632 // not ignored: round 645 gave SPG the inheritance the
10633 // v7.37.18 no-op said it did not have.
10634 Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10635 self.advance();
10636 let parent = self.expect_ident_like()?;
10637 self.consume_until_statement_boundary();
10638 Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10639 parent,
10640 detach: false
10641 }])
10642 }
10643 // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10644 // LEVEL SECURITY`, which has its own RLS arm below — without
10645 // the guard this swallowed NO FORCE as a no-op.
10646 Token::Ident(s)
10647 if s.eq_ignore_ascii_case("no")
10648 && !matches!(
10649 self.tokens.get(self.pos + 1),
10650 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10651 ) =>
10652 {
10653 self.advance();
10654 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
10655 if k.eq_ignore_ascii_case("inherit"))
10656 {
10657 self.advance();
10658 let parent = self.expect_ident_like()?;
10659 self.consume_until_statement_boundary();
10660 return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10661 parent,
10662 detach: true
10663 }]);
10664 }
10665 self.consume_until_statement_boundary();
10666 Ok(Vec::new())
10667 }
10668 // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
10669 // single-owner, so there is still nothing to record.
10670 //
10671 // v7.39 (round 652) — but the name now reaches the engine,
10672 // which refuses a role that does not exist as PG does. The
10673 // no-op was swallowing the whole statement, so a dump naming
10674 // a role this server never heard of restored clean and left
10675 // the table owned by whoever ran the restore.
10676 Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
10677 self.advance();
10678 if matches!(self.peek(), Token::To) {
10679 self.advance();
10680 }
10681 let role = self.expect_ident_like()?;
10682 Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
10683 role
10684 }])
10685 }
10686 // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
10687 // PG sets a hint; SPG doesn't have clustered storage, so the
10688 // hint itself stays a no-op.
10689 //
10690 // v7.39 (round 652) — the index name is checked now. PG
10691 // errors on one that does not exist, and swallowing that let
10692 // a typo'd CLUSTER ON pass silently.
10693 Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
10694 self.advance();
10695 // `ON` is a reserved token, not an ident.
10696 if !matches!(self.peek(), Token::On) {
10697 return Err(self.err(alloc::format!(
10698 "expected ON after CLUSTER, got {:?}",
10699 self.peek()
10700 )));
10701 }
10702 self.advance();
10703 let index = self.expect_ident_like()?;
10704 Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
10705 index: Some(index)
10706 }])
10707 }
10708 // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
10709 // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
10710 // what a logical decoder puts in the old-tuple image; SPG's
10711 // replication is SQL-text, so there is nothing to record.
10712 // Accept-and-no-op (it used to be a parse error).
10713 Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
10714 self.advance();
10715 // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
10716 // validates the index; DEFAULT / FULL / NOTHING stay no-op.
10717 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
10718 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
10719 {
10720 self.advance(); // IDENTITY
10721 self.advance(); // USING
10722 if matches!(self.peek(), Token::Index)
10723 || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
10724 {
10725 self.advance();
10726 }
10727 let index = self.expect_ident_like()?;
10728 self.consume_until_statement_boundary();
10729 return Ok(alloc::vec![
10730 crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
10731 ]);
10732 }
10733 self.consume_until_statement_boundary();
10734 Ok(Vec::new())
10735 }
10736 // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
10737 //
10738 // v7.39 (round 652) — it used to consume the statement and
10739 // return nothing, on the stated theory that SPG validated at
10740 // ADD CONSTRAINT time so there was never anything left to
10741 // validate. Measured against PG18, ADD CONSTRAINT did not
10742 // scan the existing rows at all — the comment described a
10743 // property SPG did not have, which is why nobody looked. Both
10744 // halves are real now: ADD scans unless told NOT VALID, and
10745 // this scans what NOT VALID skipped.
10746 Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
10747 self.advance();
10748 self.expect_keyword_ident("constraint")?;
10749 let name = self.expect_ident_like()?;
10750 Ok(alloc::vec![
10751 crate::ast::AlterTableTarget::ValidateConstraint { name }
10752 ])
10753 }
10754 // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
10755 // SET (option = value, …). PG uses it to clear per-table
10756 // storage params like fillfactor or autovacuum_*. SPG
10757 // engine-manages those parameters; accept-and-no-op.
10758 Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
10759 self.advance();
10760 self.consume_until_statement_boundary();
10761 Ok(Vec::new())
10762 }
10763 // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
10764 // type-of binding (PG 9.0+). SPG composite types
10765 // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
10766 // TABLE OF is rare and inverse of CREATE TABLE OF.
10767 // Accept-and-no-op until a customer dump round-trips it.
10768 Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
10769 self.advance();
10770 // v7.39 (round 710) — the type name is validated now.
10771 let type_name = self.expect_ident_like()?;
10772 self.consume_until_statement_boundary();
10773 Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
10774 type_name
10775 }])
10776 }
10777 // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
10778 // (reserved keyword) rather than Token::Ident("not"),
10779 // so it needs its own arm. Accept-and-no-op same as OF.
10780 Token::Not => {
10781 self.advance();
10782 self.consume_until_statement_boundary();
10783 Ok(Vec::new())
10784 }
10785 // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
10786 Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
10787 self.advance();
10788 self.expect_row_level_security()?;
10789 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10790 enabled: None,
10791 force: Some(true),
10792 }])
10793 }
10794 // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
10795 Token::Ident(s)
10796 if s.eq_ignore_ascii_case("no")
10797 && matches!(
10798 self.tokens.get(self.pos + 1),
10799 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10800 ) =>
10801 {
10802 self.advance(); // NO
10803 self.advance(); // FORCE
10804 self.expect_row_level_security()?;
10805 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10806 enabled: None,
10807 force: Some(false),
10808 }])
10809 }
10810 // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
10811 // (sets relrowsecurity). The guard requires the next token to be
10812 // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
10813 Token::Ident(s)
10814 if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
10815 && matches!(
10816 self.tokens.get(self.pos + 1),
10817 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
10818 ) =>
10819 {
10820 let enabled = s.eq_ignore_ascii_case("enable");
10821 self.advance(); // ENABLE/DISABLE
10822 self.expect_row_level_security()?;
10823 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10824 enabled: Some(enabled),
10825 force: None,
10826 }])
10827 }
10828 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
10829 self.advance();
10830 // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
10831 // {INDEX|KEY} [name] (cols)`, which every ORM migration
10832 // emits. The same grammar CREATE TABLE already accepts
10833 // inline (`KEY idx (a)`, prefix lengths and all), so it goes
10834 // through the SAME parser — an ALTER-only copy would be a
10835 // second place for the two to drift.
10836 if self.peek_mysql_inline_key_start() {
10837 return Ok(match self.parse_mysql_inline_key()? {
10838 Some(c) => {
10839 alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
10840 }
10841 // FULLTEXT / SPATIAL parse and are accepted as a
10842 // no-op here exactly as they are inline.
10843 None => Vec::new(),
10844 });
10845 }
10846 // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
10847 // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
10848 // PRIMARY KEY this way; mysqldump emits both.
10849 // Peek-only dispatch (no advance) — `advance()`
10850 // destructively replaces consumed tokens with Eof,
10851 // so saved-pos restore would land on Eofs.
10852 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
10853 {
10854 // The next-but-one ident is the constraint
10855 // name; the one after THAT is the kind.
10856 let kind_pos = self.pos + 2;
10857 let kind = self.tokens.get(kind_pos).cloned();
10858 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
10859 {
10860 let fk = self.parse_table_level_fk()?;
10861 return Ok(alloc::vec![
10862 crate::ast::AlterTableTarget::AddForeignKey(fk)
10863 ]);
10864 }
10865 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
10866 {
10867 self.advance(); // CONSTRAINT
10868 // v7.39 (read01 round 48) — keep the name; the engine
10869 // stores it now instead of dropping it on the floor.
10870 let con_name = self.expect_ident_like()?;
10871 self.advance(); // PRIMARY
10872 self.expect_keyword_ident("key")?;
10873 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10874 // v7.39 (round 711) — the ALTER form carries the
10875 // timing too (pg_dump writes it here).
10876 let (deferrable, initially_deferred) =
10877 self.consume_deferrable_clauses_timed()?;
10878 return Ok(alloc::vec![
10879 crate::ast::AlterTableTarget::AddTableConstraint(
10880 crate::ast::TableConstraint::PrimaryKey {
10881 name: Some(con_name),
10882 columns: cols,
10883 deferrable,
10884 initially_deferred,
10885 }
10886 )
10887 ]);
10888 }
10889 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
10890 {
10891 self.advance(); // CONSTRAINT
10892 // v7.39 (read01 round 48) — keep the name.
10893 let con_name = self.expect_ident_like()?;
10894 // v7.22 (mailrs round-13 gap 6) — delegate so
10895 // the optional `NULLS [NOT] DISTINCT` modifier
10896 // parses here too (pg_dump emits the ALTER
10897 // form; semantics enforced by the engine
10898 // since v7.13).
10899 let mut uc = self.parse_table_level_unique()?;
10900 if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
10901 *name = Some(con_name);
10902 }
10903 return Ok(alloc::vec![
10904 crate::ast::AlterTableTarget::AddTableConstraint(uc)
10905 ]);
10906 }
10907 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
10908 {
10909 self.advance(); // CONSTRAINT
10910 // v7.39 (read01 round 48) — keep the name.
10911 let con_name = self.expect_ident_like()?;
10912 self.advance(); // CHECK
10913 if !matches!(self.peek(), Token::LParen) {
10914 return Err(self.err(alloc::format!(
10915 "expected '(' after CHECK, got {:?}", self.peek()
10916 )));
10917 }
10918 self.advance();
10919 let expr = self.parse_expr(0)?;
10920 if matches!(self.peek(), Token::RParen) {
10921 self.advance();
10922 }
10923 let not_valid = self.parse_not_valid_suffix();
10924 return Ok(alloc::vec![
10925 crate::ast::AlterTableTarget::AddTableConstraint(
10926 crate::ast::TableConstraint::Check {
10927 name: Some(con_name),
10928 expr,
10929 not_valid,
10930 }
10931 )
10932 ]);
10933 }
10934 // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
10935 // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
10936 // exclusion constraints via this ALTER form.
10937 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
10938 {
10939 self.advance(); // CONSTRAINT
10940 let con_name = self.expect_ident_like()?;
10941 let mut ex = self.parse_table_level_exclude()?;
10942 if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
10943 *name = Some(con_name);
10944 }
10945 return Ok(alloc::vec![
10946 crate::ast::AlterTableTarget::AddTableConstraint(ex)
10947 ]);
10948 }
10949 // Unknown kind — fall through to FK path which
10950 // produces a descriptive parse error.
10951 }
10952 let is_fk = matches!(
10953 self.peek(),
10954 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
10955 || s.eq_ignore_ascii_case("foreign")
10956 );
10957 if is_fk {
10958 let fk = self.parse_table_level_fk()?;
10959 return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
10960 }
10961 // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
10962 // (no CONSTRAINT prefix) — same dispatch.
10963 match self.peek().clone() {
10964 Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
10965 self.advance();
10966 self.expect_keyword_ident("key")?;
10967 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10968 let (deferrable, initially_deferred) =
10969 self.consume_deferrable_clauses_timed()?;
10970 return Ok(alloc::vec![
10971 crate::ast::AlterTableTarget::AddTableConstraint(
10972 crate::ast::TableConstraint::PrimaryKey {
10973 name: None,
10974 columns: cols,
10975 deferrable,
10976 initially_deferred,
10977 }
10978 )
10979 ]);
10980 }
10981 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
10982 // v7.22 — delegate (NULLS [NOT] DISTINCT).
10983 let uc = self.parse_table_level_unique()?;
10984 return Ok(alloc::vec![
10985 crate::ast::AlterTableTarget::AddTableConstraint(uc)
10986 ]);
10987 }
10988 // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
10989 // prefix). The other three bare forms were here and
10990 // this one was not, so it fell through to the column
10991 // path and came back as "unexpected reserved keyword
10992 // 'check' at start of column definition".
10993 _ if self.peek_table_level_check_start() => {
10994 let chk = self.parse_table_level_check()?;
10995 let not_valid = self.parse_not_valid_suffix();
10996 let crate::ast::TableConstraint::Check { expr, .. } = chk else {
10997 unreachable!("parse_table_level_check returns Check")
10998 };
10999 return Ok(alloc::vec![
11000 crate::ast::AlterTableTarget::AddTableConstraint(
11001 crate::ast::TableConstraint::Check {
11002 name: None,
11003 expr,
11004 not_valid,
11005 }
11006 )
11007 ]);
11008 }
11009 // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11010 Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11011 let ex = self.parse_table_level_exclude()?;
11012 return Ok(alloc::vec![
11013 crate::ast::AlterTableTarget::AddTableConstraint(ex)
11014 ]);
11015 }
11016 _ => {}
11017 }
11018 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11019 self.advance();
11020 }
11021 let mut if_not_exists = false;
11022 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11023 self.advance();
11024 if !matches!(self.peek(), Token::Not) {
11025 return Err(self.err(alloc::format!(
11026 "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11027 self.peek()
11028 )));
11029 }
11030 self.advance();
11031 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11032 return Err(self.err(alloc::format!(
11033 "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11034 self.peek()
11035 )));
11036 }
11037 self.advance();
11038 if_not_exists = true;
11039 }
11040 // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11041 // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11042 // returns ColumnDef + an optional inline FK.
11043 let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11044 let col_name = column.name.clone();
11045 let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11046 column,
11047 if_not_exists,
11048 }];
11049 if let Some(mut fk) = col_level_fk {
11050 if fk.columns.is_empty() {
11051 fk.columns.push(col_name);
11052 }
11053 out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11054 }
11055 Ok(out)
11056 }
11057 Token::Drop => {
11058 self.advance();
11059 // v7.13.3 — dispatch on the next token. mailrs round-7
11060 // S8 closed DROP COLUMN; round-6 S7 closed
11061 // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11062 // RESTRICT modifiers.
11063 // DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11064 // DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11065 let subject = match self.peek() {
11066 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11067 self.advance();
11068 "constraint"
11069 }
11070 Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11071 self.advance();
11072 "column"
11073 }
11074 // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11075 // `INDEX` lexes as the reserved Token::Index, so it is
11076 // unambiguous. `KEY` is a plain ident, and PG allows a
11077 // column literally named "key", so only read it as the
11078 // keyword when a name follows it.
11079 Token::Index => {
11080 self.advance();
11081 "index"
11082 }
11083 Token::Ident(s)
11084 if s.eq_ignore_ascii_case("key")
11085 && matches!(
11086 self.tokens.get(self.pos + 1),
11087 Some(Token::Ident(_) | Token::QuotedIdent(_))
11088 ) =>
11089 {
11090 self.advance();
11091 "index"
11092 }
11093 // PG-canonical bare `DROP <col>` without COLUMN
11094 // keyword is also valid; treat any other ident
11095 // as the column name.
11096 Token::Ident(_) | Token::QuotedIdent(_) => "column",
11097 other => {
11098 return Err(self.err(alloc::format!(
11099 "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11100 )));
11101 }
11102 };
11103 let mut if_exists = false;
11104 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11105 let n1 = self.tokens.get(self.pos + 1);
11106 if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11107 self.advance();
11108 self.advance();
11109 if_exists = true;
11110 }
11111 }
11112 let name = self.expect_ident_like()?;
11113 let mut cascade = false;
11114 if matches!(
11115 self.peek(),
11116 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11117 || s.eq_ignore_ascii_case("restrict")
11118 ) {
11119 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11120 {
11121 cascade = true;
11122 }
11123 self.advance();
11124 }
11125 if subject == "index" {
11126 Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11127 name,
11128 if_exists,
11129 }])
11130 } else if subject == "constraint" {
11131 Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11132 name,
11133 if_exists,
11134 }])
11135 } else {
11136 Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11137 column: name,
11138 if_exists,
11139 cascade,
11140 }])
11141 }
11142 }
11143 Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11144 self.advance();
11145 // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11146 // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11147 // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11148 // immediately; accept-and-no-op.
11149 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11150 self.advance();
11151 self.consume_until_statement_boundary();
11152 return Ok(Vec::new());
11153 }
11154 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11155 self.advance();
11156 }
11157 let col_name = self.expect_ident_like()?;
11158 match self.peek() {
11159 Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11160 self.advance();
11161 }
11162 // v7.14.0 — pg_dump emits BIGSERIAL via
11163 // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11164 // nextval('seq')` (the sequence is created
11165 // separately). SPG's BIGSERIAL already uses
11166 // AUTO_INCREMENT; accept SET DEFAULT / DROP
11167 // DEFAULT / SET NOT NULL / DROP NOT NULL as
11168 // engine no-ops by consuming the tail.
11169 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11170 // v7.22 (round-13 T2) — `SET DEFAULT
11171 // nextval('…')` is how pg_dump spells a
11172 // SERIAL column (plain integer in CREATE
11173 // TABLE + this ALTER). It used to be
11174 // swallowed as a no-op, which silently
11175 // STRIPPED auto-increment from imported
11176 // schemas — the first post-import INSERT
11177 // without an explicit id then violated NOT
11178 // NULL. Lower it to the auto-increment
11179 // marker instead.
11180 let is_default_nextval =
11181 matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11182 && matches!(
11183 self.tokens.get(self.pos + 2),
11184 Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11185 );
11186 if is_default_nextval {
11187 let seq_name = self.scan_sequence_name_until_boundary();
11188 return Ok(alloc::vec![
11189 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11190 column: col_name,
11191 seq_name,
11192 }
11193 ]);
11194 }
11195 // v7.37.18 (18.1 + 18.2) — proper lowering.
11196 self.advance(); // consume "set"
11197 match self.peek().clone() {
11198 Token::Default => {
11199 self.advance();
11200 let default_expr = self.parse_expr(0)?;
11201 return Ok(alloc::vec![
11202 crate::ast::AlterTableTarget::AlterColumnSetDefault {
11203 column: col_name,
11204 default_expr,
11205 }
11206 ]);
11207 }
11208 Token::Not => {
11209 self.advance();
11210 if !matches!(self.peek(), Token::Null) {
11211 return Err(self.err(alloc::format!(
11212 "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11213 self.peek()
11214 )));
11215 }
11216 self.advance();
11217 return Ok(alloc::vec![
11218 crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11219 column: col_name,
11220 }
11221 ]);
11222 }
11223 // `SET EXPRESSION AS (expr)` (PG 17) — change a
11224 // stored generated column's expression and
11225 // recompute existing rows.
11226 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11227 self.advance(); // EXPRESSION
11228 if matches!(self.peek(), Token::As) {
11229 self.advance();
11230 }
11231 let expr = self.parse_expr(0)?;
11232 return Ok(alloc::vec![
11233 crate::ast::AlterTableTarget::AlterColumnSetExpression {
11234 column: col_name,
11235 expr,
11236 }
11237 ]);
11238 }
11239 other => {
11240 // Other SET subjects (STATISTICS,
11241 // STORAGE, COMPRESSION, …) stay no-ops —
11242 // storage hints with no SPG semantics.
11243 let _ = other;
11244 self.consume_until_statement_boundary();
11245 return Ok(Vec::new());
11246 }
11247 }
11248 }
11249 Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11250 self.advance(); // consume "drop"
11251 return self.parse_alter_column_drop_tail(col_name);
11252 }
11253 Token::Drop => {
11254 self.advance(); // consume Drop token
11255 return self.parse_alter_column_drop_tail(col_name);
11256 }
11257 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11258 // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11259 // GENERATED { ALWAYS | BY DEFAULT } AS
11260 // IDENTITY ( … )`: pg_dump's spelling for
11261 // identity columns. Same auto-increment
11262 // lowering as the nextval default; the
11263 // sequence options inside the parens are
11264 // no-ops under SPG's max+1 semantics.
11265 let is_generated = matches!(
11266 self.tokens.get(self.pos + 1),
11267 Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11268 );
11269 if !is_generated {
11270 return Err(self.err(alloc::format!(
11271 "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11272 self.tokens.get(self.pos + 1)
11273 )));
11274 }
11275 let seq_name = self.scan_sequence_name_until_boundary();
11276 return Ok(alloc::vec![
11277 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11278 column: col_name,
11279 seq_name,
11280 }
11281 ]);
11282 }
11283 // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11284 // column: floor the next allocated value at n (bare
11285 // RESTART = restart from the start value, 1).
11286 Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11287 self.advance();
11288 let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11289 {
11290 self.advance();
11291 let neg = if matches!(self.peek(), Token::Minus) {
11292 self.advance();
11293 true
11294 } else {
11295 false
11296 };
11297 match self.advance() {
11298 Token::Integer(v) => Some(if neg { -v } else { v }),
11299 other => {
11300 return Err(self.err(alloc::format!(
11301 "expected integer after RESTART WITH, got {other:?}"
11302 )));
11303 }
11304 }
11305 } else {
11306 None
11307 };
11308 return Ok(alloc::vec![
11309 crate::ast::AlterTableTarget::AlterColumnRestart {
11310 column: col_name,
11311 with,
11312 }
11313 ]);
11314 }
11315 other => {
11316 return Err(self.err(alloc::format!(
11317 "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11318 )));
11319 }
11320 }
11321 // v7.39 (round 713) — the type parser has consumed a
11322 // trailing `COLLATE <name>` since Phase 2.5, and
11323 // `parse_column_type_name` discarded it: `ALTER COLUMN t
11324 // TYPE text COLLATE "C"` parsed clean and changed
11325 // nothing. Keep the clause; the engine re-collates.
11326 let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _) =
11327 self.parse_type_with_implied_flags()?;
11328 let collation = if coll_explicit {
11329 coll_name.map(|n| (coll, n))
11330 } else {
11331 None
11332 };
11333 let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11334 {
11335 self.advance();
11336 Some(self.parse_expr(0)?)
11337 } else {
11338 None
11339 };
11340 Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11341 column: col_name,
11342 new_type,
11343 using,
11344 collation,
11345 }])
11346 }
11347 // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11348 // PG also supports `RENAME TO new_table` for table-name
11349 // rename; that surface is deferred (pg_dump never emits
11350 // it). If the first post-RENAME ident is `TO`, the user
11351 // is asking for table rename — error with a clear
11352 // message rather than misparsing `TO` as a column name.
11353 Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11354 self.advance();
11355 // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11356 // table-name rename (mailrs round-10 A.5 — used
11357 // by migrate-042's `RENAME TO email_contacts`).
11358 // `TO` lexes as Token::To.
11359 if matches!(self.peek(), Token::To)
11360 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11361 {
11362 self.advance();
11363 let new = self.expect_ident_like()?;
11364 return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11365 new,
11366 }]);
11367 }
11368 // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11369 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11370 self.advance();
11371 let old = self.expect_ident_like()?;
11372 if matches!(self.peek(), Token::To) {
11373 self.advance();
11374 } else {
11375 self.expect_keyword_ident("to")?;
11376 }
11377 let new = self.expect_ident_like()?;
11378 return Ok(alloc::vec![
11379 crate::ast::AlterTableTarget::RenameConstraint { old, new }
11380 ]);
11381 }
11382 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11383 self.advance();
11384 }
11385 let old = self.expect_ident_like()?;
11386 // `TO` is a reserved keyword token; accept both
11387 // Token::To and Token::Ident("to") for consistency.
11388 if matches!(self.peek(), Token::To) {
11389 self.advance();
11390 } else {
11391 self.expect_keyword_ident("to")?;
11392 }
11393 let new = self.expect_ident_like()?;
11394 Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11395 old,
11396 new,
11397 }])
11398 }
11399 // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11400 // { ALL | <name> }`. pg_dump --disable-triggers wraps
11401 // every data block with these. Real disable semantics —
11402 // not no-op — because reload correctness assumes the
11403 // triggers don't fire (rows already carry their
11404 // computed values from prod).
11405 Token::Ident(s)
11406 if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11407 {
11408 let enabled = s.eq_ignore_ascii_case("enable");
11409 self.advance();
11410 // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11411 // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11412 // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11413 // pg_dump output) — anything else falls through to
11414 // the catch-all error below.
11415 // v7.22 (round-13 T3) — mysqldump wraps every data
11416 // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11417 // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11418 // maintains indexes incrementally — engine no-op.
11419 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11420 self.advance();
11421 return Ok(Vec::new());
11422 }
11423 // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11424 // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11425 // to gate triggers on session_replication_role; SPG
11426 // has no replica role, so the prefix is consumed and
11427 // treated identically to the plain ENABLE/DISABLE
11428 // TRIGGER form.
11429 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11430 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11431 {
11432 self.advance();
11433 }
11434 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11435 return Err(self.err(alloc::format!(
11436 "expected TRIGGER after {}, got {:?}",
11437 if enabled { "ENABLE" } else { "DISABLE" },
11438 self.peek()
11439 )));
11440 }
11441 self.advance();
11442 // `ALL` lexes as Token::All (reserved); also
11443 // accept Token::Ident("all") for symmetry.
11444 // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11445 // TRIGGER selectors. USER (= all user triggers) is
11446 // semantically ALL here; REPLICA / ALWAYS gate on
11447 // session_replication_role which SPG doesn't track.
11448 // All map to TriggerSelector::All.
11449 let which = if matches!(self.peek(), Token::All)
11450 || matches!(self.peek(), Token::Ident(s)
11451 if s.eq_ignore_ascii_case("all")
11452 || s.eq_ignore_ascii_case("user")
11453 || s.eq_ignore_ascii_case("replica")
11454 || s.eq_ignore_ascii_case("always"))
11455 {
11456 self.advance();
11457 crate::ast::TriggerSelector::All
11458 } else {
11459 let name = self.expect_ident_like()?;
11460 crate::ast::TriggerSelector::Named(name)
11461 };
11462 Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11463 which,
11464 enabled,
11465 }])
11466 }
11467 // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11468 Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11469 self.advance();
11470 if !matches!(self.peek(), Token::Partition)
11471 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11472 if s.eq_ignore_ascii_case("partition"))
11473 {
11474 return Err(self.err(alloc::format!(
11475 "expected PARTITION after ATTACH, got {:?}",
11476 self.peek()
11477 )));
11478 }
11479 self.advance();
11480 let child = self.expect_ident_like()?;
11481 let bounds = self.parse_partition_bounds_tail()?;
11482 Ok(alloc::vec![
11483 crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11484 ])
11485 }
11486 // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11487 Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11488 self.advance();
11489 if !matches!(self.peek(), Token::Partition)
11490 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11491 if s.eq_ignore_ascii_case("partition"))
11492 {
11493 return Err(self.err(alloc::format!(
11494 "expected PARTITION after DETACH, got {:?}",
11495 self.peek()
11496 )));
11497 }
11498 self.advance();
11499 let child = self.expect_ident_like()?;
11500 let mut concurrently = false;
11501 let mut finalize = false;
11502 loop {
11503 match self.peek().clone() {
11504 Token::Ident(s) | Token::QuotedIdent(s)
11505 if s.eq_ignore_ascii_case("concurrently") =>
11506 {
11507 self.advance();
11508 concurrently = true;
11509 }
11510 Token::Ident(s) | Token::QuotedIdent(s)
11511 if s.eq_ignore_ascii_case("finalize") =>
11512 {
11513 self.advance();
11514 finalize = true;
11515 }
11516 _ => break,
11517 }
11518 }
11519 Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11520 child,
11521 concurrently,
11522 finalize,
11523 }])
11524 }
11525 other => Err(self.err(alloc::format!(
11526 "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11527 ))),
11528 }
11529 }
11530
11531 /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11532 /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11533 /// TABLE … ATTACH PARTITION. Shares the same grammar as
11534 /// `parse_partition_of_tail`'s bounds branch.
11535 /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11536 /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11537 /// lowering each to the respective AlterTableTarget. Any
11538 /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11539 /// no-op via consume_until_statement_boundary.
11540 fn parse_alter_column_drop_tail(
11541 &mut self,
11542 col_name: String,
11543 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11544 match self.peek().clone() {
11545 Token::Default => {
11546 self.advance();
11547 Ok(alloc::vec![
11548 crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11549 ])
11550 }
11551 Token::Not => {
11552 self.advance();
11553 if !matches!(self.peek(), Token::Null) {
11554 return Err(self.err(alloc::format!(
11555 "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11556 self.peek()
11557 )));
11558 }
11559 self.advance();
11560 Ok(alloc::vec![
11561 crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11562 ])
11563 }
11564 // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11565 // generated column into a plain column.
11566 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11567 self.advance();
11568 // v7.39 (round 187, U10) — IF EXISTS was consumed but
11569 // dropped, so the engine still errored on a plain
11570 // column; PG's semantics are NOTICE + skip.
11571 let mut if_exists = false;
11572 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11573 self.advance();
11574 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11575 self.advance();
11576 if_exists = true;
11577 }
11578 }
11579 Ok(alloc::vec![
11580 crate::ast::AlterTableTarget::AlterColumnDropExpression {
11581 column: col_name,
11582 if_exists,
11583 }
11584 ])
11585 }
11586 // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11587 // identity column into a plain column.
11588 Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11589 self.advance();
11590 let mut if_exists = false;
11591 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11592 self.advance();
11593 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11594 self.advance();
11595 if_exists = true;
11596 }
11597 }
11598 Ok(alloc::vec![
11599 crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11600 column: col_name,
11601 if_exists,
11602 }
11603 ])
11604 }
11605 _ => {
11606 self.consume_until_statement_boundary();
11607 Ok(Vec::new())
11608 }
11609 }
11610 }
11611
11612 /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11613 /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11614 /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11615 /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11616 fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11617 let mut opts = crate::ast::CopyOptions::default();
11618 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11619 return Ok(opts);
11620 }
11621 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11622 self.advance();
11623 }
11624 if matches!(self.peek(), Token::LParen) {
11625 self.advance();
11626 loop {
11627 self.parse_one_copy_option(&mut opts)?;
11628 match self.peek() {
11629 Token::Comma => {
11630 self.advance();
11631 }
11632 Token::RParen => {
11633 self.advance();
11634 break;
11635 }
11636 other => {
11637 return Err(self.err(alloc::format!(
11638 "expected ',' or ')' in COPY options, got {other:?}"
11639 )));
11640 }
11641 }
11642 }
11643 } else {
11644 while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11645 self.parse_one_copy_option(&mut opts)?;
11646 }
11647 }
11648 if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11649 return Err(self.err(alloc::format!(
11650 "unexpected token after COPY options: {:?}",
11651 self.peek()
11652 )));
11653 }
11654 Ok(opts)
11655 }
11656
11657 fn parse_one_copy_option(
11658 &mut self,
11659 opts: &mut crate::ast::CopyOptions,
11660 ) -> Result<(), ParseError> {
11661 use crate::ast::CopyFormat;
11662 // The option keyword. NULL lexes as its own token; the rest are
11663 // bare identifiers.
11664 let kw = match self.advance() {
11665 Token::Null => alloc::string::String::from("NULL"),
11666 Token::Ident(s) => s.to_uppercase(),
11667 other => {
11668 return Err(self.err(alloc::format!(
11669 "expected a COPY option keyword, got {other:?}"
11670 )));
11671 }
11672 };
11673 match kw.as_str() {
11674 "FORMAT" => {
11675 let fmt = self.expect_ident_like()?;
11676 match fmt.to_ascii_uppercase().as_str() {
11677 "CSV" => opts.format = CopyFormat::Csv,
11678 "TEXT" => opts.format = CopyFormat::Text,
11679 other => {
11680 return Err(self.err(alloc::format!(
11681 "COPY format \"{}\" not recognized",
11682 other.to_ascii_lowercase()
11683 )));
11684 }
11685 }
11686 }
11687 // Legacy bare format keywords.
11688 "CSV" => opts.format = CopyFormat::Csv,
11689 "TEXT" => opts.format = CopyFormat::Text,
11690 "HEADER" => {
11691 opts.header = match self.peek() {
11692 Token::True => {
11693 self.advance();
11694 true
11695 }
11696 Token::False => {
11697 self.advance();
11698 false
11699 }
11700 Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
11701 self.advance();
11702 true
11703 }
11704 Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
11705 self.advance();
11706 false
11707 }
11708 // Bare HEADER (no boolean) means HEADER true.
11709 _ => true,
11710 };
11711 }
11712 // r1066 (7.38 S5.1) — pgbench 14+ loads with
11713 // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
11714 // vacuum bookkeeping on a freshly created/truncated
11715 // table; SPG's per-statement visibility makes it a
11716 // faithful no-op, and rejecting it aborted `pgbench -i`
11717 // against the drop-in. Accept ON/OFF/bare, change nothing.
11718 "FREEZE" => match self.peek() {
11719 Token::True | Token::False => {
11720 self.advance();
11721 }
11722 Token::Ident(s)
11723 if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
11724 {
11725 self.advance();
11726 }
11727 _ => {}
11728 },
11729 "DELIMITER" | "QUOTE" | "ESCAPE" => {
11730 let s = match self.advance() {
11731 Token::String(s) => s,
11732 other => {
11733 return Err(self.err(alloc::format!(
11734 "COPY {kw} expects a single-character string, got {other:?}"
11735 )));
11736 }
11737 };
11738 // v7.39 (round 247) — PG's wording (0A000), keyword in
11739 // lowercase: "COPY delimiter must be a single one-byte
11740 // character".
11741 let one_byte_err = || {
11742 self.err(alloc::format!(
11743 "COPY {} must be a single one-byte character",
11744 kw.to_ascii_lowercase()
11745 ))
11746 };
11747 let mut chars = s.chars();
11748 let c = chars.next().ok_or_else(one_byte_err)?;
11749 if chars.next().is_some() || c.len_utf8() != 1 {
11750 return Err(one_byte_err());
11751 }
11752 match kw.as_str() {
11753 "DELIMITER" => opts.delimiter = Some(c),
11754 "QUOTE" => opts.quote = Some(c),
11755 _ => opts.escape = Some(c),
11756 }
11757 }
11758 // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
11759 "FORCE_QUOTE" => {
11760 if matches!(self.peek(), Token::Star) {
11761 self.advance();
11762 opts.force_quote = Some(Vec::new());
11763 } else {
11764 if !matches!(self.peek(), Token::LParen) {
11765 return Err(self.err(alloc::format!(
11766 "expected '(' or '*' after FORCE_QUOTE, got {:?}",
11767 self.peek()
11768 )));
11769 }
11770 self.advance();
11771 let mut cols = Vec::new();
11772 loop {
11773 cols.push(self.expect_ident_like()?);
11774 match self.peek() {
11775 Token::Comma => {
11776 self.advance();
11777 }
11778 Token::RParen => {
11779 self.advance();
11780 break;
11781 }
11782 other => {
11783 return Err(self.err(alloc::format!(
11784 "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
11785 )));
11786 }
11787 }
11788 }
11789 opts.force_quote = Some(cols);
11790 }
11791 }
11792 "NULL" => {
11793 opts.null_str = Some(match self.advance() {
11794 Token::String(s) => s,
11795 other => {
11796 return Err(self.err(alloc::format!(
11797 "COPY NULL expects a quoted string, got {other:?}"
11798 )));
11799 }
11800 });
11801 }
11802 // v7.39 (round 265) — the two CSV FROM-side column lists. Same
11803 // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
11804 // FORCE_NULL too.
11805 "FORCE_NOT_NULL" | "FORCE_NULL" => {
11806 let cols = self.parse_copy_column_list(&kw)?;
11807 if kw == "FORCE_NOT_NULL" {
11808 opts.force_not_null = Some(cols);
11809 } else {
11810 opts.force_null = Some(cols);
11811 }
11812 }
11813 other => {
11814 // PG's wording, lowercased option name.
11815 return Err(self.err(alloc::format!(
11816 "option \"{}\" not recognized",
11817 other.to_ascii_lowercase()
11818 )));
11819 }
11820 }
11821 Ok(())
11822 }
11823
11824 /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
11825 /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
11826 /// is the `*` spelling.
11827 fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
11828 if matches!(self.peek(), Token::Star) {
11829 self.advance();
11830 return Ok(Vec::new());
11831 }
11832 if !matches!(self.peek(), Token::LParen) {
11833 return Err(self.err(alloc::format!(
11834 "expected '(' or '*' after {kw}, got {:?}",
11835 self.peek()
11836 )));
11837 }
11838 self.advance();
11839 let mut cols = Vec::new();
11840 loop {
11841 cols.push(self.expect_ident_like()?);
11842 match self.peek() {
11843 Token::Comma => {
11844 self.advance();
11845 }
11846 Token::RParen => {
11847 self.advance();
11848 break;
11849 }
11850 other => {
11851 return Err(self.err(alloc::format!(
11852 "expected ',' or ')' in {kw} list, got {other:?}"
11853 )));
11854 }
11855 }
11856 }
11857 Ok(cols)
11858 }
11859
11860 fn parse_partition_bounds_tail(
11861 &mut self,
11862 ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
11863 use crate::ast::PartitionOfBoundsAst;
11864 match self.peek() {
11865 Token::Default => {
11866 self.advance();
11867 Ok(PartitionOfBoundsAst::Default)
11868 }
11869 Token::For => {
11870 self.advance();
11871 if !matches!(self.peek(), Token::Values) {
11872 return Err(
11873 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
11874 );
11875 }
11876 self.advance();
11877 let want_with = matches!(
11878 self.peek(),
11879 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
11880 );
11881 if want_with {
11882 self.advance();
11883 if !matches!(self.peek(), Token::LParen) {
11884 return Err(self.err(format!(
11885 "expected '(' after FOR VALUES WITH, got {:?}",
11886 self.peek()
11887 )));
11888 }
11889 self.advance();
11890 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
11891 loop {
11892 let key = self.expect_ident_like()?;
11893 let n = match self.peek().clone() {
11894 Token::Integer(v) if u32::try_from(v).is_ok() => {
11895 self.advance();
11896 v as u32
11897 }
11898 other => {
11899 return Err(self.err(format!(
11900 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
11901 )));
11902 }
11903 };
11904 match key.to_ascii_uppercase().as_str() {
11905 "MODULUS" => modulus = Some(n),
11906 "REMAINDER" => remainder = Some(n),
11907 other => {
11908 return Err(self.err(format!(
11909 "FOR VALUES WITH: unknown key {other:?}; \
11910 expected MODULUS or REMAINDER"
11911 )));
11912 }
11913 }
11914 match self.peek() {
11915 Token::Comma => {
11916 self.advance();
11917 }
11918 Token::RParen => {
11919 self.advance();
11920 break;
11921 }
11922 other => {
11923 return Err(self.err(format!(
11924 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
11925 )));
11926 }
11927 }
11928 }
11929 let modulus = modulus
11930 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
11931 let remainder = remainder.ok_or_else(|| {
11932 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
11933 })?;
11934 if modulus == 0 {
11935 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
11936 }
11937 if remainder >= modulus {
11938 return Err(self.err(format!(
11939 "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
11940 )));
11941 }
11942 return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
11943 }
11944 match self.peek() {
11945 Token::From => {
11946 self.advance();
11947 let lower = Box::new(self.parse_partition_bound_expr()?);
11948 if !matches!(self.peek(), Token::To) {
11949 return Err(self.err(format!(
11950 "expected TO after FROM (...), got {:?}",
11951 self.peek()
11952 )));
11953 }
11954 self.advance();
11955 let upper = Box::new(self.parse_partition_bound_expr()?);
11956 Ok(PartitionOfBoundsAst::Range { lower, upper })
11957 }
11958 Token::In => {
11959 self.advance();
11960 if !matches!(self.peek(), Token::LParen) {
11961 return Err(self.err(format!(
11962 "expected '(' after FOR VALUES IN, got {:?}",
11963 self.peek()
11964 )));
11965 }
11966 self.advance();
11967 let mut values = Vec::new();
11968 loop {
11969 values.push(self.parse_expr(0)?);
11970 match self.peek() {
11971 Token::Comma => {
11972 self.advance();
11973 }
11974 Token::RParen => {
11975 self.advance();
11976 break;
11977 }
11978 other => {
11979 return Err(self.err(format!(
11980 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
11981 )));
11982 }
11983 }
11984 }
11985 if values.is_empty() {
11986 return Err(
11987 self.err("FOR VALUES IN requires at least one literal".to_string())
11988 );
11989 }
11990 Ok(PartitionOfBoundsAst::List { values })
11991 }
11992 other => Err(self.err(format!(
11993 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
11994 ))),
11995 }
11996 }
11997 other => Err(self.err(format!(
11998 "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
11999 ))),
12000 }
12001 }
12002
12003 /// v7.16.2 — peek for `information_schema.<tbl>` /
12004 /// `pg_catalog.<tbl>` triples and, if matched, consume all
12005 /// three tokens + return a synthetic table name the engine's
12006 /// SELECT path recognises as a virtual view. Returns `None`
12007 /// when the head doesn't look like a meta-qualified name.
12008 /// Used by `parse_table_ref` to bypass the
12009 /// `expect_ident_like` schema-strip for these specific PG
12010 /// meta schemas (mailrs round-10 A.3).
12011 fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12012 // Extract the schema name. Must be a plain ident token.
12013 let schema = match self.tokens.get(self.pos) {
12014 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12015 _ => return None,
12016 };
12017 // Dot.
12018 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12019 return None;
12020 }
12021 // The table-side ident may lex as a reserved keyword
12022 // (e.g. `Token::Tables`). Tolerate the common ones via a
12023 // helper that reads the trailing token's underlying name.
12024 let tbl = match self.tokens.get(self.pos + 2)? {
12025 Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12026 Token::Tables => "tables".to_string(),
12027 // Other PG meta table names that may collide with
12028 // reserved keywords land here as needed.
12029 _ => return None,
12030 };
12031 // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12032 // names so the synthetic name doesn't double-prefix
12033 // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12034 let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12035 ("__spg_info_", tbl.to_ascii_lowercase())
12036 } else if schema.eq_ignore_ascii_case("pg_catalog") {
12037 // v7.39 (round 541) — only the catalogs SPG actually
12038 // synthesises are rewritten, which is what the BARE path
12039 // has always checked. Anything else keeps its own name and
12040 // takes the ordinary route: `pg_stat_activity` and friends
12041 // resolve through meta_view_result, and a name that is no
12042 // catalog at all gets PG's "relation does not exist"
12043 // instead of a message about a view SPG cannot materialise.
12044 let lowered = tbl.to_ascii_lowercase();
12045 if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12046 self.advance(); // schema
12047 self.advance(); // dot
12048 self.advance(); // tbl
12049 return Some((lowered.clone(), lowered));
12050 }
12051 let bare = lowered
12052 .strip_prefix("pg_")
12053 .map(alloc::string::String::from)
12054 .unwrap_or(lowered);
12055 ("__spg_pg_", bare)
12056 } else if schema.eq_ignore_ascii_case("mysql") {
12057 // v7.17.0 Phase 3.P0-65 — MySQL system schema
12058 // (`mysql.user`, `mysql.db`). Same synthetic-name
12059 // shape as pg_catalog.
12060 ("__spg_mysql_", tbl.to_ascii_lowercase())
12061 } else {
12062 return None;
12063 };
12064 self.advance(); // schema
12065 self.advance(); // dot
12066 self.advance(); // tbl
12067 Some((
12068 alloc::format!("{prefix}{normalised}"),
12069 tbl.to_ascii_lowercase(),
12070 ))
12071 }
12072
12073 /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12074 /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12075 /// implicit front of every search_path, so a bare reference to a
12076 /// known catalog table always means the catalog table. Only the
12077 /// names the engine actually synthesises are recognised — any
12078 /// other `pg_*` ident stays a user table (mailrs embed round-12).
12079 fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12080 // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12081 // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12082 // `pg_catalog` at the front of every search_path. (pg_stat_activity
12083 // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12084 // through the meta_view_result path instead, and already resolve
12085 // bare — they must NOT be listed here or the __spg_ rewrite would
12086 // mis-target them.)
12087 const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12088 let name = match self.tokens.get(self.pos) {
12089 Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12090 _ => return None,
12091 };
12092 // A following dot means this ident is a schema qualifier,
12093 // not a table name — let the qualified path handle it.
12094 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12095 return None;
12096 }
12097 if !PG_META_TABLES.contains(&name.as_str()) {
12098 return None;
12099 }
12100 self.advance();
12101 let bare = name.strip_prefix("pg_").unwrap_or(&name);
12102 Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12103 }
12104
12105 /// Consume a bare ident if its lowercase matches `kw`, else err.
12106 /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12107 /// Peeks only; the caller advances.
12108 fn peek_keyword_ident(&self, kw: &str) -> bool {
12109 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12110 }
12111
12112 fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12113 match self.advance() {
12114 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12115 other => Err(ParseError {
12116 message: format!("expected {kw:?}, got {other:?}"),
12117 token_pos: self.consumed_pos(),
12118 }),
12119 }
12120 }
12121
12122 /// Accept either a quoted identifier (`"foo"`) or a quoted string
12123 /// literal (`'foo'`) — same shape used by CREATE USER for the
12124 /// username slot.
12125 fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12126 match self.advance() {
12127 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12128 other => Err(ParseError {
12129 message: format!("expected identifier or string, got {other:?}"),
12130 token_pos: self.consumed_pos(),
12131 }),
12132 }
12133 }
12134
12135 fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12136 match self.advance() {
12137 Token::String(s) => Ok(s),
12138 other => Err(ParseError {
12139 message: format!("expected quoted string, got {other:?}"),
12140 token_pos: self.consumed_pos(),
12141 }),
12142 }
12143 }
12144
12145 fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12146 // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12147 // subqueries recurse through here without passing
12148 // parse_expr; share the same nesting budget.
12149 self.enter_nested()?;
12150 let r = self.parse_select_stmt_inner();
12151 self.nest_depth -= 1;
12152 r
12153 }
12154
12155 fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12156 // Caller dispatches on Token::Select; the inner helper handles
12157 // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12158 // get a fresh bare-select parse and may not have their own ORDER
12159 // BY / LIMIT.
12160 let mut head = self.parse_bare_select()?;
12161 self.parse_setop_chain_into(&mut head)?;
12162 self.parse_select_tail_into(&mut head)?;
12163 Ok(Statement::Select(head))
12164 }
12165
12166 /// v7.37.17 (17.6 siblings) — the three SQL set operations
12167 /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12168 /// token), and INTERSECT [ALL] (a bare ident — it was never
12169 /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12170 /// tighter than UNION / EXCEPT — the executor folds the chain
12171 /// left-to-right, which is already correct for LEADING
12172 /// intersects; an INTERSECT pair that FOLLOWS a union/except
12173 /// pair nests into that previous peer, so A UNION B INTERSECT C
12174 /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12175 /// groups.
12176 fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12177 // A parenthesized group arrives with its own (already
12178 // regrouped) unions on `head`; only the pairs THIS chain
12179 // appends participate in the precedence regroup below —
12180 // nesting an outer INTERSECT into a group-internal peer
12181 // would dissolve the explicit grouping.
12182 let boundary = head.unions.len();
12183 loop {
12184 let base = match self.peek() {
12185 Token::Union => UnionKind::Distinct,
12186 Token::Except => UnionKind::Except,
12187 Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12188 _ => break,
12189 };
12190 self.advance();
12191 let kind = if matches!(self.peek(), Token::All) {
12192 self.advance();
12193 match base {
12194 UnionKind::Distinct => UnionKind::All,
12195 UnionKind::Except => UnionKind::ExceptAll,
12196 _ => UnionKind::IntersectAll,
12197 }
12198 } else {
12199 base
12200 };
12201 let peer = self.parse_bare_select()?;
12202 head.unions.push((kind, peer));
12203 }
12204 let mut pairs = core::mem::take(&mut head.unions);
12205 let tail = pairs.split_off(boundary);
12206 let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12207 for (kind, peer) in tail {
12208 let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12209 // An intersect nests into the previous element of THIS
12210 // chain only; with no new previous element it stays at
12211 // the outer level (the left fold applies it to the
12212 // whole head, group included).
12213 match (
12214 is_intersect,
12215 regrouped.len() > boundary,
12216 regrouped.last_mut(),
12217 ) {
12218 (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12219 _ => regrouped.push((kind, peer)),
12220 }
12221 }
12222 head.unions = regrouped;
12223 Ok(())
12224 }
12225
12226 /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12227 /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12228 /// the top-level bare VALUES statement reuses it verbatim.
12229 /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12230 /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12231 /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12232 /// where the grouping-set universe is still in scope.
12233 fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12234 if !matches!(self.peek(), Token::Order) {
12235 return Ok(Vec::new());
12236 }
12237 self.advance();
12238 if !self.peek_is_by() {
12239 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12240 }
12241 self.advance();
12242 let mut keys = Vec::new();
12243 loop {
12244 // v7.39 (round 691) — save/restore, the discipline this parser
12245 // already uses around `pending_sample_preds`, so a subquery inside
12246 // a key neither inherits nor leaks the channel.
12247 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12248 let saved_coll = self.order_key_collation.take();
12249 let parsed = self.parse_expr(0);
12250 self.in_order_by_key = saved_flag;
12251 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12252 let expr = parsed?;
12253 let desc = if matches!(self.peek(), Token::Desc) {
12254 self.advance();
12255 true
12256 } else if matches!(self.peek(), Token::Asc) {
12257 self.advance();
12258 false
12259 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12260 // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12261 // one ordering per type, so the btree comparison operators map
12262 // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12263 // would need a custom operator class — honest error.
12264 self.advance();
12265 match self.advance() {
12266 Token::Lt | Token::LtEq => false,
12267 Token::Gt | Token::GtEq => true,
12268 other => {
12269 return Err(self.err(alloc::format!(
12270 "ORDER BY USING supports the btree comparison \
12271 operators (< <= > >=); got {other:?}"
12272 )));
12273 }
12274 }
12275 } else {
12276 false
12277 };
12278 // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12279 let nulls_first = self.parse_optional_nulls_placement()?;
12280 keys.push(OrderBy {
12281 expr,
12282 desc,
12283 nulls_first,
12284 collation,
12285 });
12286 if matches!(self.peek(), Token::Comma) {
12287 self.advance();
12288 } else {
12289 break;
12290 }
12291 }
12292 Ok(keys)
12293 }
12294
12295 fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12296 // v7.39 (round 135) — a grouping-set query may have already parsed +
12297 // rewritten its ORDER BY (to reference synthetic grouping columns); if
12298 // no ORDER BY token is present, keep that pre-set order_by rather than
12299 // clobbering it with an empty list.
12300 let parsed_keys = self.parse_order_by_keys()?;
12301 head.order_by = if parsed_keys.is_empty() {
12302 core::mem::take(&mut head.order_by)
12303 } else {
12304 parsed_keys
12305 };
12306 // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12307 // order. PG's grammar takes a limit clause and an offset clause
12308 // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12309 // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12310 // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12311 // spelling died on `expected end of input, got Limit`.
12312 //
12313 // Each may appear at most once, and LIMIT and FETCH FIRST are
12314 // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12315 // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12316 // A second one is left unconsumed here, which the caller reports
12317 // as trailing input rather than silently taking the last.
12318 let mut saw_limit = false;
12319 let mut saw_offset = false;
12320 loop {
12321 if !saw_limit && matches!(self.peek(), Token::Limit) {
12322 self.advance();
12323 // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12324 // PG synonyms for "no limit". Treat both as None
12325 // (no head.limit set) so the engine's existing
12326 // unlimited-result path takes over. Reject was the
12327 // pre-5.1 behaviour and broke pg_dump-flavoured
12328 // tooling that occasionally emits LIMIT NULL.
12329 if self.consume_limit_unbounded_sentinel() {
12330 head.limit = None;
12331 } else {
12332 let first = self.parse_limit_expr("LIMIT")?;
12333 // MySQL `LIMIT offset, count` — the first number is
12334 // the offset when a comma follows.
12335 if matches!(self.peek(), Token::Comma) {
12336 self.advance();
12337 let count = self.parse_limit_expr("LIMIT")?;
12338 head.offset = Some(first);
12339 saw_offset = true;
12340 head.limit = Some(count);
12341 } else {
12342 head.limit = Some(first);
12343 }
12344 }
12345 saw_limit = true;
12346 continue;
12347 }
12348 if !saw_offset && matches!(self.peek(), Token::Offset) {
12349 self.advance();
12350 // PG also accepts an optional `ROW` / `ROWS` trailer
12351 // after the offset value (`OFFSET 10 ROWS`). The
12352 // FETCH-FIRST branch below relies on the same.
12353 let off = self.parse_limit_expr("OFFSET")?;
12354 self.consume_optional_rows_keyword();
12355 head.offset = Some(off);
12356 saw_offset = true;
12357 continue;
12358 }
12359 // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12360 // the SQL-standard alias for LIMIT. PG accepts both
12361 // spellings interchangeably; pg_dump emits FETCH FIRST in
12362 // newer versions. We map it onto `head.limit` so the
12363 // engine path is unified.
12364 if !saw_limit
12365 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12366 if s.eq_ignore_ascii_case("fetch"))
12367 {
12368 self.advance(); // FETCH
12369 // `FIRST` or `NEXT` (both legal per SQL standard).
12370 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12371 if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12372 {
12373 self.advance();
12374 }
12375 // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12376 // implicit 1 — but we always consume one if present).
12377 let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12378 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12379 {
12380 // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12381 crate::ast::LimitExpr::Literal(1)
12382 } else {
12383 self.parse_limit_expr("FETCH FIRST")?
12384 };
12385 // Eat `ROW` / `ROWS` if not already consumed above.
12386 self.consume_optional_rows_keyword();
12387 // Optional `ONLY` (the spec form) — or the SQL:2008
12388 // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12389 // now honours WITH TIES by extending past the LIMIT
12390 // truncation point through every row that shares the
12391 // last-kept row's ORDER BY key.
12392 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12393 if s.eq_ignore_ascii_case("only"))
12394 {
12395 self.advance();
12396 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12397 if s.eq_ignore_ascii_case("with"))
12398 {
12399 self.advance(); // WITH
12400 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12401 if s.eq_ignore_ascii_case("ties"))
12402 {
12403 self.advance();
12404 head.limit_with_ties = true;
12405 }
12406 }
12407 head.limit = Some(count);
12408 saw_limit = true;
12409 continue;
12410 }
12411 break;
12412 }
12413 // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12414 // FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12415 // [ OF table_name [, …] ]
12416 // [ NOWAIT | SKIP LOCKED ]
12417 // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12418 // FOR SHARE OF t2`). SPG is a single-writer engine — every
12419 // SELECT already returns a consistent snapshot — so these
12420 // are accept-and-discard: the parser absorbs them so
12421 // mailrs / Rails / Django code paths that emit `SELECT
12422 // … FOR UPDATE` for advisory pessimistic locking load
12423 // without a parser error. The on-disk locking model is
12424 // unchanged; callers that rely on FOR UPDATE for read-
12425 // through-write ordering still get the right answer
12426 // because SPG serialises writes anyway.
12427 head.locking = self
12428 .consume_optional_for_lock_clauses()
12429 .map(alloc::boxed::Box::new);
12430 Ok(())
12431 }
12432
12433 /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12434 /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12435 /// LOCKED ]` trailers. Each clause is fully accepted and
12436 /// discarded — SPG's single-writer model already satisfies the
12437 /// callers' implicit ordering requirement. Stops at the first
12438 /// token that isn't `FOR`.
12439 fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12440 // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12441 // not discarded. PG keeps the strongest of several clauses; the
12442 // policy of the last one wins, which is what this loop records.
12443 let mut seen: Option<crate::ast::LockingClause> = None;
12444 while matches!(self.peek(), Token::For) {
12445 // v7.37.14 (A2.5-stub) — record that this query asked
12446 // for a row lock the parser is about to silently
12447 // discard. Operators surface the count via
12448 // `spg_sql::silent_for_update_count()` so they can
12449 // gauge how much of the workload depends on advisory
12450 // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12451 // before v7.37.15's per-row tuple locking lands.
12452 crate::record_silent_for_update_clause();
12453 self.advance(); // FOR
12454 // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12455 // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12456 let mut no_key = false;
12457 let mut key = false;
12458 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12459 if s.eq_ignore_ascii_case("no"))
12460 {
12461 self.advance(); // NO
12462 no_key = true;
12463 // The next ident should be KEY but be generous;
12464 // anything followed by UPDATE/SHARE is accepted.
12465 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12466 if s.eq_ignore_ascii_case("key"))
12467 {
12468 self.advance(); // KEY
12469 }
12470 }
12471 // `KEY` prefix (PG `FOR KEY SHARE`).
12472 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12473 if s.eq_ignore_ascii_case("key"))
12474 {
12475 self.advance(); // KEY
12476 key = true;
12477 }
12478 // Lock-strength keyword: UPDATE / SHARE. Required, but
12479 // we're lenient — an unexpected token here just bails
12480 // (we already consumed FOR; caller's downstream
12481 // dispatch will error if anything actually depends on
12482 // the trailing tokens).
12483 let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12484 if s.eq_ignore_ascii_case("update"));
12485 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12486 if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12487 {
12488 self.advance();
12489 use crate::ast::LockStrength as LS;
12490 let strength = match (is_update, no_key, key) {
12491 (true, true, _) => LS::NoKeyUpdate,
12492 (true, _, _) => LS::Update,
12493 (false, _, true) => LS::KeyShare,
12494 (false, _, _) => LS::Share,
12495 };
12496 seen = Some(crate::ast::LockingClause {
12497 strength,
12498 of_tables: alloc::vec::Vec::new(),
12499 policy: crate::ast::LockWait::Wait,
12500 });
12501 } else {
12502 // FOR by itself (or `FOR KEY` with nothing after) —
12503 // give up on the lock-clause path. We've already
12504 // advanced past FOR; further attempts to parse
12505 // here would clobber state.
12506 return seen;
12507 }
12508 // Optional `OF tbl[, tbl …]`. mailrs emits this when
12509 // joining and locking only a subset of tables.
12510 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12511 if s.eq_ignore_ascii_case("of"))
12512 {
12513 self.advance(); // OF
12514 #[allow(clippy::while_let_loop)]
12515 loop {
12516 match self.peek() {
12517 Token::Ident(_) | Token::QuotedIdent(_) => {
12518 // v7.39 (round 294) — the name is CAPTURED now: PG
12519 // validates it against the FROM clause, and an
12520 // uncaptured list silently means "lock everything".
12521 let mut nm = match self.advance() {
12522 Token::Ident(n) | Token::QuotedIdent(n) => n,
12523 _ => alloc::string::String::new(),
12524 };
12525 // Optional schema-qualified `schema.table`.
12526 if matches!(self.peek(), Token::Dot) {
12527 self.advance();
12528 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12529 {
12530 self.advance();
12531 nm = n;
12532 }
12533 }
12534 if let Some(c) = seen.as_mut() {
12535 c.of_tables.push(nm);
12536 }
12537 }
12538 _ => break,
12539 }
12540 if matches!(self.peek(), Token::Comma) {
12541 self.advance();
12542 } else {
12543 break;
12544 }
12545 }
12546 }
12547 // Optional `NOWAIT` | `SKIP LOCKED`.
12548 match self.peek().clone() {
12549 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12550 self.advance();
12551 if let Some(c) = seen.as_mut() {
12552 c.policy = crate::ast::LockWait::NoWait;
12553 }
12554 }
12555 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12556 self.advance(); // SKIP
12557 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12558 if s.eq_ignore_ascii_case("locked"))
12559 {
12560 self.advance(); // LOCKED
12561 if let Some(c) = seen.as_mut() {
12562 c.policy = crate::ast::LockWait::SkipLocked;
12563 }
12564 }
12565 }
12566 _ => {}
12567 }
12568 // Loop: PG allows multiple FOR clauses chained.
12569 }
12570 seen
12571 }
12572
12573 /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12574 /// Bind value gets resolved during prepared-statement Execute;
12575 /// the Pratt expression parser would over-accept here (e.g.
12576 /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12577 /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12578 /// sentinel tokens (PG synonyms for "no limit"). Returns true
12579 /// when one was consumed; caller skips the regular
12580 /// limit-value parse and leaves `head.limit` at None.
12581 fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12582 if matches!(self.peek(), Token::Null) {
12583 self.advance();
12584 return true;
12585 }
12586 if matches!(self.peek(), Token::All) {
12587 self.advance();
12588 return true;
12589 }
12590 false
12591 }
12592
12593 /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12594 /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12595 /// SQL-standard shape. No-op when missing.
12596 fn consume_optional_rows_keyword(&mut self) {
12597 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12598 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12599 {
12600 self.advance();
12601 }
12602 }
12603
12604 /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12605 ///
12606 /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12607 /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12608 /// constant, which is why that spelling keeps the token path below.
12609 ///
12610 /// Constants are folded here rather than carried into the tree: the
12611 /// 15+ execution paths that read the row count go through
12612 /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12613 /// means "no limit". A clause the engine could not resolve would
12614 /// therefore return the WHOLE table instead of failing. Folding at
12615 /// parse time keeps that impossible; a non-constant clause is still
12616 /// a clean error (recorded residual — closing it wants a resolution
12617 /// pre-pass on the simple-query path, where `substitute_placeholders`
12618 /// does not run).
12619 fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12620 // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12621 // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12622 // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12623 // ONLY` both work (its grammar takes a c_expr). Both measured
12624 // against PG 18.4 in round 305.
12625 if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12626 return self.parse_limit_constant(label);
12627 }
12628 // One pass, no rewind: `advance()` takes each token by
12629 // `mem::replace`, so a consumed token reads back as Eof and this
12630 // parser cannot backtrack. Everything — bare literal included —
12631 // is therefore folded from the parsed expression rather than
12632 // re-read from the token stream.
12633 let start = self.pos;
12634 let e = self.parse_expr(0)?;
12635 if let crate::ast::Expr::Placeholder(n) = e {
12636 return Ok(crate::ast::LimitExpr::Placeholder(n));
12637 }
12638 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12639 match fold_limit_constant(&e) {
12640 Some(Ok(v)) if v < 0 => Err(ParseError {
12641 message: alloc::format!("{neg_label} must not be negative"),
12642 token_pos: start,
12643 }),
12644 Some(Ok(v)) => u32::try_from(v)
12645 .map(crate::ast::LimitExpr::Literal)
12646 .map_err(|_| ParseError {
12647 message: alloc::format!("{label} value too large: {v}"),
12648 token_pos: start,
12649 }),
12650 Some(Err(message)) => Err(ParseError {
12651 message: message.replace("{L}", neg_label),
12652 token_pos: start,
12653 }),
12654 // v7.39 (round 305, V23) — not foldable at parse time
12655 // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
12656 // expression; the engine evaluates it once before dispatch.
12657 None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
12658 }
12659 }
12660
12661 fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12662 // v7.39 (round 239) — PG's row-count clause takes a bigint with its
12663 // coercion rules, not just an integer token: a NUMERIC rounds half
12664 // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
12665 // refused with PG's wording ("LIMIT must not be negative", 2201W /
12666 // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
12667 // content, failing as an input-syntax error on the value. General
12668 // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
12669 // they need an Expr-carrying LimitExpr variant.
12670 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12671 let err_at = |message: alloc::string::String, pos: usize| ParseError {
12672 message,
12673 token_pos: pos,
12674 };
12675 match self.advance() {
12676 Token::Integer(n) if n >= 0 => u32::try_from(n)
12677 .map(crate::ast::LimitExpr::Literal)
12678 .map_err(|_| ParseError {
12679 message: alloc::format!("{label} value too large: {n}"),
12680 token_pos: self.consumed_pos(),
12681 }),
12682 Token::Integer(_) => Err(err_at(
12683 alloc::format!("{neg_label} must not be negative"),
12684 self.pos.saturating_sub(1),
12685 )),
12686 Token::Numeric(t) => {
12687 let pos = self.pos.saturating_sub(1);
12688 let v: f64 = t.parse().map_err(|_| {
12689 err_at(
12690 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12691 pos,
12692 )
12693 })?;
12694 if v < 0.0 {
12695 return Err(err_at(
12696 alloc::format!("{neg_label} must not be negative"),
12697 pos,
12698 ));
12699 }
12700 // Round half away from zero — PG's numeric→bigint cast.
12701 // (no_std: no f64::round; v is non-negative, so truncating
12702 // v + 0.5 is the same thing.)
12703 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
12704 let rounded = (v + 0.5) as u64;
12705 u32::try_from(rounded)
12706 .map(crate::ast::LimitExpr::Literal)
12707 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
12708 }
12709 Token::Minus => {
12710 let pos = self.pos.saturating_sub(1);
12711 match self.peek() {
12712 Token::Integer(_) | Token::Numeric(_) => {
12713 self.advance();
12714 Err(err_at(
12715 alloc::format!("{neg_label} must not be negative"),
12716 pos,
12717 ))
12718 }
12719 other => Err(err_at(
12720 alloc::format!(
12721 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12722 ),
12723 pos,
12724 )),
12725 }
12726 }
12727 Token::String(t) => {
12728 let pos = self.pos.saturating_sub(1);
12729 match t.trim().parse::<i64>() {
12730 Ok(n) if n < 0 => Err(err_at(
12731 alloc::format!("{neg_label} must not be negative"),
12732 pos,
12733 )),
12734 Ok(n) => u32::try_from(n)
12735 .map(crate::ast::LimitExpr::Literal)
12736 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
12737 Err(_) => Err(err_at(
12738 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12739 pos,
12740 )),
12741 }
12742 }
12743 Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
12744 other => Err(ParseError {
12745 message: alloc::format!(
12746 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12747 ),
12748 token_pos: self.consumed_pos(),
12749 }),
12750 }
12751 }
12752
12753 /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
12754 /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
12755 /// `unions` empty and `order_by` / `limit` `None`; the top-level
12756 /// `parse_select_stmt` is responsible for filling those in.
12757 /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
12758 /// call in the expression tree to the per-set integer bitmask
12759 /// (PG semantics: one bit per argument, MSB first; 1 = the key
12760 /// is dropped in this grouping set). Runs during the ROLLUP /
12761 /// CUBE / GROUPING SETS expansion, where the set is known.
12762 /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
12763 /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
12764 fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
12765 if let Expr::FunctionCall { name, .. } = expr
12766 && name.eq_ignore_ascii_case("grouping")
12767 {
12768 if !out.iter().any(|e| e == expr) {
12769 out.push(expr.clone());
12770 }
12771 return;
12772 }
12773 match expr {
12774 Expr::Binary { lhs, rhs, .. } => {
12775 Self::collect_grouping_calls(lhs, out);
12776 Self::collect_grouping_calls(rhs, out);
12777 }
12778 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12779 Self::collect_grouping_calls(expr, out)
12780 }
12781 Expr::FunctionCall { args, .. } => {
12782 for a in args {
12783 Self::collect_grouping_calls(a, out);
12784 }
12785 }
12786 Expr::Case {
12787 operand,
12788 branches,
12789 else_branch,
12790 } => {
12791 if let Some(o) = operand {
12792 Self::collect_grouping_calls(o, out);
12793 }
12794 for (c, v) in branches {
12795 Self::collect_grouping_calls(c, out);
12796 Self::collect_grouping_calls(v, out);
12797 }
12798 if let Some(x) = else_branch {
12799 Self::collect_grouping_calls(x, out);
12800 }
12801 }
12802 _ => {}
12803 }
12804 }
12805
12806 /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
12807 /// `grp_exprs[k]` with a reference to the synthetic ordering column
12808 /// `__grp_ord_k` (injected per grouping-set branch).
12809 fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
12810 if let Expr::FunctionCall { name, .. } = expr
12811 && name.eq_ignore_ascii_case("grouping")
12812 {
12813 if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
12814 *expr = Expr::Column(crate::ast::ColumnName {
12815 qualifier: None,
12816 name: alloc::format!("__grp_ord_{k}"),
12817 });
12818 }
12819 return;
12820 }
12821 match expr {
12822 Expr::Binary { lhs, rhs, .. } => {
12823 Self::rewrite_grouping_to_col(lhs, grp_exprs);
12824 Self::rewrite_grouping_to_col(rhs, grp_exprs);
12825 }
12826 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12827 Self::rewrite_grouping_to_col(expr, grp_exprs)
12828 }
12829 Expr::FunctionCall { args, .. } => {
12830 for a in args {
12831 Self::rewrite_grouping_to_col(a, grp_exprs);
12832 }
12833 }
12834 Expr::Case {
12835 operand,
12836 branches,
12837 else_branch,
12838 } => {
12839 if let Some(o) = operand {
12840 Self::rewrite_grouping_to_col(o, grp_exprs);
12841 }
12842 for (c, v) in branches {
12843 Self::rewrite_grouping_to_col(c, grp_exprs);
12844 Self::rewrite_grouping_to_col(v, grp_exprs);
12845 }
12846 if let Some(x) = else_branch {
12847 Self::rewrite_grouping_to_col(x, grp_exprs);
12848 }
12849 }
12850 _ => {}
12851 }
12852 }
12853
12854 /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
12855 /// as the list of key sets it contributes. A bare expression is one
12856 /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
12857 /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
12858 /// the concatenation of its items' sets, where an item is itself an
12859 /// element, a parenthesized key list, or the empty set `()`. A
12860 /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
12861 /// move together.
12862 fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
12863 let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
12864 // ROLLUP ( … ) / CUBE ( … )
12865 if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
12866 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
12867 {
12868 let is_cube = is_kw(self.peek(), "cube");
12869 self.advance(); // ROLLUP / CUBE
12870 self.advance(); // (
12871 let mut units: Vec<Vec<Expr>> = Vec::new();
12872 loop {
12873 if matches!(self.peek(), Token::LParen) {
12874 // Composite unit: (a, b) rolls up as one.
12875 self.advance();
12876 let mut unit = Vec::new();
12877 if !matches!(self.peek(), Token::RParen) {
12878 loop {
12879 unit.push(self.parse_expr(0)?);
12880 match self.peek() {
12881 Token::Comma => {
12882 self.advance();
12883 }
12884 Token::RParen => break,
12885 other => {
12886 return Err(self.err(format!(
12887 "expected ',' or ')' in grouping unit, got {other:?}"
12888 )));
12889 }
12890 }
12891 }
12892 }
12893 self.advance(); // )
12894 units.push(unit);
12895 } else {
12896 units.push(alloc::vec![self.parse_expr(0)?]);
12897 }
12898 match self.peek() {
12899 Token::Comma => {
12900 self.advance();
12901 }
12902 Token::RParen => break,
12903 other => {
12904 return Err(self.err(format!(
12905 "expected ',' or ')' in grouping list, got {other:?}"
12906 )));
12907 }
12908 }
12909 }
12910 self.advance(); // )
12911 let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
12912 units
12913 .iter()
12914 .zip(unit_sel.iter())
12915 .filter(|(_, keep)| **keep)
12916 .flat_map(|(u, _)| u.iter().cloned())
12917 .collect()
12918 };
12919 let n = units.len();
12920 if is_cube {
12921 let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
12922 .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
12923 .collect();
12924 subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
12925 return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
12926 }
12927 return Ok((0..=n)
12928 .rev()
12929 .map(|keep| {
12930 let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
12931 flatten(&sel)
12932 })
12933 .collect());
12934 }
12935 // GROUPING SETS ( item [, item]* )
12936 if is_kw(self.peek(), "grouping")
12937 && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
12938 {
12939 self.advance(); // GROUPING
12940 self.advance(); // SETS
12941 if !matches!(self.peek(), Token::LParen) {
12942 return Err(self.err(format!(
12943 "expected '(' after GROUPING SETS, got {:?}",
12944 self.peek()
12945 )));
12946 }
12947 self.advance(); // outer (
12948 let mut sets: Vec<Vec<Expr>> = Vec::new();
12949 loop {
12950 if matches!(self.peek(), Token::LParen) {
12951 // A parenthesized key list (or the empty set).
12952 self.advance();
12953 let mut set = Vec::new();
12954 if !matches!(self.peek(), Token::RParen) {
12955 loop {
12956 set.push(self.parse_expr(0)?);
12957 match self.peek() {
12958 Token::Comma => {
12959 self.advance();
12960 }
12961 Token::RParen => break,
12962 other => {
12963 return Err(self.err(format!(
12964 "expected ',' or ')' in grouping set, got {other:?}"
12965 )));
12966 }
12967 }
12968 }
12969 }
12970 self.advance(); // )
12971 sets.push(set);
12972 } else {
12973 // A nested element: ROLLUP/CUBE/GROUPING SETS or a
12974 // bare expression.
12975 sets.extend(self.parse_grouping_element()?);
12976 }
12977 match self.peek() {
12978 Token::Comma => {
12979 self.advance();
12980 }
12981 Token::RParen => break,
12982 other => {
12983 return Err(self.err(format!(
12984 "expected ',' or ')' after a grouping set, got {other:?}"
12985 )));
12986 }
12987 }
12988 }
12989 self.advance(); // outer )
12990 return Ok(sets);
12991 }
12992 Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
12993 }
12994
12995 fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
12996 // v7.38 (read01) — a reference to a key that is dropped in this grouping
12997 // set evaluates to NULL, at any depth. Previously only a *top-level*
12998 // select item equal to a dropped key was nullified, so a key nested in
12999 // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13000 // column and failed to resolve against the set's synthetic schema.
13001 if dropped.iter().any(|d| d == expr) {
13002 *expr = Expr::Literal(Literal::Null);
13003 return;
13004 }
13005 if let Expr::FunctionCall { name, args } = expr
13006 && name.eq_ignore_ascii_case("grouping")
13007 {
13008 let mut mask: i64 = 0;
13009 for a in args.iter() {
13010 mask <<= 1;
13011 if dropped.iter().any(|d| d == a) {
13012 mask |= 1;
13013 }
13014 }
13015 // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13016 // literal: a bare integer in a select item is indistinguishable
13017 // from a positional reference once `ORDER BY 1` substitutes the
13018 // item back in, and the round-232 position check then read the
13019 // mask value as an out-of-range position. The cast changes
13020 // nothing semantically (grouping() is integer).
13021 *expr = Expr::Cast {
13022 expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13023 target: crate::ast::CastTarget::Int,
13024 };
13025 return;
13026 }
13027 // Generic recursion over the common expression shapes the
13028 // SELECT list uses; anything without child expressions is
13029 // left alone.
13030 match expr {
13031 Expr::FunctionCall { args, .. } => {
13032 for a in args {
13033 Self::substitute_grouping_calls(a, dropped);
13034 }
13035 }
13036 Expr::Binary { lhs, rhs, .. } => {
13037 Self::substitute_grouping_calls(lhs, dropped);
13038 Self::substitute_grouping_calls(rhs, dropped);
13039 }
13040 Expr::Unary { expr: inner, .. } => {
13041 Self::substitute_grouping_calls(inner, dropped);
13042 }
13043 Expr::Cast { expr: inner, .. } => {
13044 Self::substitute_grouping_calls(inner, dropped);
13045 }
13046 Expr::Case {
13047 operand,
13048 branches,
13049 else_branch,
13050 } => {
13051 if let Some(op) = operand {
13052 Self::substitute_grouping_calls(op, dropped);
13053 }
13054 for (w, t) in branches {
13055 Self::substitute_grouping_calls(w, dropped);
13056 Self::substitute_grouping_calls(t, dropped);
13057 }
13058 if let Some(e) = else_branch {
13059 Self::substitute_grouping_calls(e, dropped);
13060 }
13061 }
13062 // v7.38 (read01) — recurse into the remaining child-bearing shapes
13063 // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13064 // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13065 // …` is the canonical rollup-total label idiom).
13066 Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13067 Expr::Like { expr, pattern, .. } => {
13068 Self::substitute_grouping_calls(expr, dropped);
13069 Self::substitute_grouping_calls(pattern, dropped);
13070 }
13071 Expr::InList { expr, list, .. } => {
13072 Self::substitute_grouping_calls(expr, dropped);
13073 for item in list {
13074 Self::substitute_grouping_calls(item, dropped);
13075 }
13076 }
13077 Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13078 Expr::Array(items) => {
13079 for item in items {
13080 Self::substitute_grouping_calls(item, dropped);
13081 }
13082 }
13083 Expr::ArraySubscript { target, index } => {
13084 Self::substitute_grouping_calls(target, dropped);
13085 Self::substitute_grouping_calls(index, dropped);
13086 }
13087 Expr::ArraySlice { target, lo, hi } => {
13088 Self::substitute_grouping_calls(target, dropped);
13089 if let Some(lo) = lo {
13090 Self::substitute_grouping_calls(lo, dropped);
13091 }
13092 if let Some(hi) = hi {
13093 Self::substitute_grouping_calls(hi, dropped);
13094 }
13095 }
13096 Expr::AnyAll { expr, array, .. } => {
13097 Self::substitute_grouping_calls(expr, dropped);
13098 Self::substitute_grouping_calls(array, dropped);
13099 }
13100 _ => {}
13101 }
13102 }
13103
13104 fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13105 // v7.37.17 (17.6 siblings) — parenthesized set-operation
13106 // group: `( <select chain> )` usable anywhere a query block
13107 // is (head or peer of an outer chain). The group's own
13108 // unions ride the returned SelectStatement; the executor's
13109 // nested-peer recursion runs them.
13110 if matches!(self.peek(), Token::LParen)
13111 && matches!(
13112 self.tokens.get(self.pos + 1),
13113 Some(Token::Select | Token::LParen | Token::Values)
13114 )
13115 {
13116 self.advance(); // (
13117 self.enter_nested()?;
13118 // v7.37 D.20 — a group whose head is a VALUES list:
13119 // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13120 // otherwise recurse into a nested SELECT/group head.
13121 let mut head = (if matches!(self.peek(), Token::Values) {
13122 self.advance(); // VALUES
13123 self.parse_values_rows_body()
13124 } else {
13125 self.parse_bare_select()
13126 })
13127 .and_then(|mut h| {
13128 self.parse_setop_chain_into(&mut h)?;
13129 Ok(h)
13130 });
13131 self.nest_depth -= 1;
13132 let mut head = match &mut head {
13133 Ok(h) => core::mem::take(h),
13134 Err(_) => return head,
13135 };
13136 // v7.37.17 (17.6 siblings) — group-internal tail:
13137 // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13138 // group head, then wrap the group as a derived table
13139 // (SELECT * FROM (group)) so the outer chain / outer
13140 // tail can't clobber the group's own ordering or limit.
13141 let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13142 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13143 if s.eq_ignore_ascii_case("fetch"));
13144 if has_tail {
13145 self.parse_select_tail_into(&mut head)?;
13146 head = SelectStatement {
13147 locking: None,
13148 ctes: Vec::new(),
13149 distinct: false,
13150 distinct_on: Vec::new(),
13151 items: alloc::vec![SelectItem::Wildcard],
13152 from: Some(FromClause {
13153 primary: TableRef {
13154 name: "subquery".to_string(),
13155 alias: None,
13156 only: false,
13157 as_of_segment: None,
13158 unnest_expr: None,
13159 unnest_column_aliases: Vec::new(),
13160 with_ordinality: false,
13161 generate_series_args: None,
13162 lateral_subquery: Some(Box::new(head)),
13163 jsonb_each_text_arg: None,
13164 table_fn_call: None,
13165 rows_from: None,
13166 json_table: None,
13167 scalar_fn_item: false,
13168 },
13169 joins: Vec::new(),
13170 }),
13171 where_: None,
13172 group_by: None,
13173 group_by_all: false,
13174 having: None,
13175 unions: Vec::new(),
13176 order_by: Vec::new(),
13177 limit: None,
13178 offset: None,
13179 limit_with_ties: false,
13180 window_check_exprs: Vec::new(),
13181 };
13182 }
13183 if !matches!(self.peek(), Token::RParen) {
13184 return Err(self.err(format!(
13185 "expected ')' after parenthesized query group, got {:?}",
13186 self.peek()
13187 )));
13188 }
13189 self.advance();
13190 return Ok(head);
13191 }
13192 // `TABLE name` shorthand as a query block — valid anywhere
13193 // a SELECT head is (set-op peers included).
13194 if matches!(self.peek(), Token::Table)
13195 && matches!(
13196 self.tokens.get(self.pos + 1),
13197 Some(Token::Ident(_) | Token::QuotedIdent(_))
13198 )
13199 {
13200 return self.parse_table_shorthand();
13201 }
13202 if !matches!(self.peek(), Token::Select) {
13203 return Err(self.err(format!(
13204 "expected SELECT to start a query block, got {:?}",
13205 self.peek()
13206 )));
13207 }
13208 self.advance();
13209 let distinct = if matches!(self.peek(), Token::Distinct) {
13210 self.advance();
13211 true
13212 } else {
13213 false
13214 };
13215 // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13216 // keep the first row (per ORDER BY) of each group the
13217 // expressions define. Django's .distinct('field') shape.
13218 let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13219 self.advance(); // ON
13220 if !matches!(self.peek(), Token::LParen) {
13221 return Err(self.err(format!(
13222 "expected '(' after DISTINCT ON, got {:?}",
13223 self.peek()
13224 )));
13225 }
13226 self.advance();
13227 let mut exprs = Vec::new();
13228 loop {
13229 exprs.push(self.parse_expr(0)?);
13230 match self.peek() {
13231 Token::Comma => {
13232 self.advance();
13233 }
13234 Token::RParen => break,
13235 other => {
13236 return Err(self.err(format!(
13237 "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13238 )));
13239 }
13240 }
13241 }
13242 self.advance(); // )
13243 exprs
13244 } else {
13245 Vec::new()
13246 };
13247 let mut items = self.parse_select_list()?;
13248 // Scope the TABLESAMPLE lowering channel to this SELECT:
13249 // stash whatever an enclosing select accumulated, collect
13250 // our own FROM's predicates, restore after the combine.
13251 let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13252 let mut from = if matches!(self.peek(), Token::From) {
13253 self.advance();
13254 Some(self.parse_from_clause()?)
13255 } else {
13256 None
13257 };
13258 // v7.37 D.22 — a set-returning function in the projection with no FROM
13259 // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13260 // rows. Move the first SRF projection item to a FROM-position derived
13261 // table and replace it in the projection with a reference to its output
13262 // column; sibling scalar columns repeat per SRF row. PG names the output
13263 // column after the function (or its AS alias). Reuses the FROM-SRF
13264 // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13265 // works via the targetlist-SRF path.
13266 // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13267 // `SELECT * FROM f(args)` — the record's fields become the columns, which
13268 // is exactly what the function's own row shape already is. Anywhere else
13269 // (per outer row, or beside other items) it would need a real record-typed
13270 // projection, so it says so rather than answering something else.
13271 if let [
13272 SelectItem::Expr {
13273 expr: Expr::FunctionCall { name, args },
13274 ..
13275 },
13276 ] = items.as_slice()
13277 && name == "__record_expand"
13278 {
13279 let Some(Expr::FunctionCall {
13280 name: inner_name,
13281 args: inner_args,
13282 }) = args.first()
13283 else {
13284 return Err(self.err(
13285 "(<expr>).* expands a function's record — it needs a function call".into(),
13286 ));
13287 };
13288 if from.is_some() {
13289 return Err(self.err(
13290 "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13291 .into(),
13292 ));
13293 }
13294 let fn_ref = TableRef {
13295 name: inner_name.clone(),
13296 alias: None,
13297 only: false,
13298 as_of_segment: None,
13299 unnest_expr: None,
13300 unnest_column_aliases: Vec::new(),
13301 with_ordinality: false,
13302 generate_series_args: None,
13303 lateral_subquery: None,
13304 jsonb_each_text_arg: None,
13305 table_fn_call: Some(Box::new((
13306 inner_name.to_ascii_lowercase(),
13307 inner_args.clone(),
13308 ))),
13309 rows_from: None,
13310 json_table: None,
13311 scalar_fn_item: false,
13312 };
13313 items = alloc::vec![SelectItem::Wildcard];
13314 from = Some(FromClause {
13315 primary: fn_ref,
13316 joins: Vec::new(),
13317 });
13318 }
13319 // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13320 // FROM, keeps its marker: the ENGINE lowers it, because naming the
13321 // record's fields takes the catalog. It becomes a LATERAL of the same
13322 // function plus one item per declared column — the machinery rounds 65
13323 // and 69 already built.
13324 // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13325 // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13326 // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13327 // express, since the lifted one becomes a scan and the other would
13328 // expand per its rows (a cross product, not a zip). So when the
13329 // projection holds more than one top-level function call, the lift steps
13330 // aside and the engine's target-list expansion takes the whole list.
13331 let fn_call_items = items
13332 .iter()
13333 .filter(|it| {
13334 matches!(
13335 it,
13336 SelectItem::Expr {
13337 expr: Expr::FunctionCall { .. },
13338 ..
13339 }
13340 )
13341 })
13342 .count();
13343 if from.is_none() && fn_call_items <= 1 {
13344 let mut found: Option<(usize, TableRef, String)> = None;
13345 for (i, item) in items.iter().enumerate() {
13346 if let SelectItem::Expr {
13347 expr: Expr::FunctionCall { name, args },
13348 alias,
13349 } = item
13350 {
13351 let lname = name.to_ascii_lowercase();
13352 let colname = alias.clone().unwrap_or_else(|| lname.clone());
13353 let (unnest, gs) = match lname.as_str() {
13354 "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13355 "generate_series" if (2..=3).contains(&args.len()) => {
13356 (None, Some(args.clone()))
13357 }
13358 // v7.38 (read01) — generate_subscripts(arr, dim) in a
13359 // no-FROM projection yields the 1-based subscripts, i.e.
13360 // generate_series(1, array_length(arr, dim)); an invalid
13361 // dimension makes array_length NULL → 0 rows, as in PG.
13362 "generate_subscripts" if args.len() == 2 => (
13363 None,
13364 Some(alloc::vec![
13365 Expr::Literal(Literal::Integer(1)),
13366 Expr::FunctionCall {
13367 name: "array_length".to_string(),
13368 args: args.clone(),
13369 },
13370 ]),
13371 ),
13372 // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13373 // in a no-FROM projection unnest their *_to_array form.
13374 "string_to_table" | "regexp_split_to_table" => {
13375 let array_fn = if lname == "string_to_table" {
13376 "string_to_array"
13377 } else {
13378 "regexp_split_to_array"
13379 };
13380 (
13381 Some(Box::new(Expr::FunctionCall {
13382 name: array_fn.to_string(),
13383 args: args.clone(),
13384 })),
13385 None,
13386 )
13387 }
13388 // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13389 // a no-FROM projection expand per element. The scalar form
13390 // returns the elements as a TEXT array, so unnest over the
13391 // same call materialises one row each (same rewrite the
13392 // FROM-clause form uses).
13393 "jsonb_array_elements"
13394 | "json_array_elements"
13395 | "jsonb_array_elements_text"
13396 | "json_array_elements_text"
13397 if args.len() == 1 =>
13398 {
13399 (
13400 Some(Box::new(Expr::FunctionCall {
13401 name: lname.clone(),
13402 args: args.clone(),
13403 })),
13404 None,
13405 )
13406 }
13407 // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13408 // in a no-FROM projection expands per match (scalar form
13409 // returns the matches as a TEXT array → unnest).
13410 "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13411 Some(Box::new(Expr::FunctionCall {
13412 name: lname.clone(),
13413 args: args.clone(),
13414 })),
13415 None,
13416 ),
13417 _ => continue,
13418 };
13419 found = Some((
13420 i,
13421 TableRef {
13422 name: colname.clone(),
13423 alias: Some(colname.clone()),
13424 only: false,
13425 as_of_segment: None,
13426 unnest_expr: unnest,
13427 unnest_column_aliases: alloc::vec![colname.clone()],
13428 with_ordinality: false,
13429 generate_series_args: gs,
13430 lateral_subquery: None,
13431 jsonb_each_text_arg: None,
13432 table_fn_call: None,
13433 rows_from: None,
13434 json_table: None,
13435 scalar_fn_item: false,
13436 },
13437 colname,
13438 ));
13439 break;
13440 }
13441 }
13442 if let Some((idx, tref, colname)) = found {
13443 from = Some(FromClause {
13444 primary: tref,
13445 joins: Vec::new(),
13446 });
13447 items[idx] = SelectItem::Expr {
13448 expr: Expr::Column(ColumnName {
13449 qualifier: None,
13450 name: colname.clone(),
13451 }),
13452 alias: Some(colname),
13453 };
13454 }
13455 }
13456 let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13457 let where_ = if matches!(self.peek(), Token::Where) {
13458 self.advance();
13459 Some(self.parse_expr(0)?)
13460 } else {
13461 None
13462 };
13463 let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13464 Some(match acc {
13465 Some(w) => Expr::Binary {
13466 lhs: Box::new(pred),
13467 op: crate::ast::BinOp::And,
13468 rhs: Box::new(w),
13469 },
13470 None => pred,
13471 })
13472 });
13473 self.pending_sample_preds = enclosing_sample_preds;
13474 let mut group_by_all = false;
13475 // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13476 // share one expansion: `grouping_sets` lists the key subsets
13477 // (first = primary, assigned to stmt.group_by; the rest
13478 // become UNION ALL peers), `grouping_universe` is the full
13479 // key list used to compute each peer's dropped keys.
13480 let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13481 let mut grouping_universe: Vec<Expr> = Vec::new();
13482 // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13483 // A BOOL, not the key list: this frame is the statement parser's, and
13484 // round 430 measured that a `Vec` local here is enough on its own to
13485 // tip the 512 KiB nesting guard. The keys are recoverable from
13486 // `grouping_universe`, which a rollup fills with exactly them.
13487 let mut mysql_rollup = false;
13488 let group_by = if matches!(self.peek(), Token::Group) {
13489 self.advance();
13490 if !self.peek_is_by() {
13491 return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13492 }
13493 self.advance();
13494 // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13495 // every non-aggregate SELECT-list item later.
13496 if matches!(self.peek(), Token::All) {
13497 self.advance();
13498 group_by_all = true;
13499 None
13500 } else {
13501 // v7.39 (round 242) — PG's general grouping-element grammar:
13502 // GROUP BY [DISTINCT] element [, element]*, where an element
13503 // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13504 // SETS (…) — mixed freely. Each element yields a list of
13505 // key sets; the query's grouping sets are the CARTESIAN
13506 // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13507 // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13508 // content. ROLLUP/CUBE members may be composite
13509 // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13510 // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13511 // parser handled only a lone ROLLUP/CUBE/GS as the whole
13512 // clause.
13513 let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13514 self.advance();
13515 true
13516 } else {
13517 false
13518 };
13519 let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13520 loop {
13521 element_sets.push(self.parse_grouping_element()?);
13522 if matches!(self.peek(), Token::Comma) {
13523 self.advance();
13524 } else {
13525 break;
13526 }
13527 }
13528 let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13529 for el in &element_sets {
13530 let mut next: Vec<Vec<Expr>> = Vec::new();
13531 for base in &total {
13532 for set in el {
13533 let mut merged = base.clone();
13534 for k in set {
13535 if !merged.iter().any(|m| m == k) {
13536 merged.push(k.clone());
13537 }
13538 }
13539 next.push(merged);
13540 }
13541 }
13542 total = next;
13543 }
13544 // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13545 // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13546 // The keys and the aggregates come out identical; the ROW
13547 // ORDER does not, and that is the part a report depends on.
13548 // MySQL interleaves each group's subtotal right after its
13549 // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13550 // where the union-of-grouping-sets expansion emits every
13551 // leaf first and then every subtotal. MariaDB REFUSES an
13552 // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13553 // order itself — measured on MariaDB 11 and MySQL 9.7, which
13554 // agree on the order and disagree only on whether ORDER BY
13555 // is allowed (MySQL allows it; SPG allows it too, since
13556 // refusing would break the clients that can write it).
13557 if self.mysql_dialect
13558 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13559 && matches!(
13560 self.tokens.get(self.pos + 1),
13561 Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13562 )
13563 {
13564 self.advance(); // WITH
13565 self.advance(); // ROLLUP
13566 let keys = total.into_iter().next().unwrap_or_default();
13567 mysql_rollup = true;
13568 // n+1 prefixes, largest first — the same expansion
13569 // `ROLLUP (…)` produces.
13570 total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13571 }
13572 if distinct_sets {
13573 let mut seen: Vec<Vec<String>> = Vec::new();
13574 total.retain(|set| {
13575 let mut key: Vec<String> =
13576 set.iter().map(|e| alloc::format!("{e}")).collect();
13577 key.sort();
13578 if seen.contains(&key) {
13579 false
13580 } else {
13581 seen.push(key);
13582 true
13583 }
13584 });
13585 }
13586 if total.len() > 1 {
13587 let mut universe: Vec<Expr> = Vec::new();
13588 for set in &total {
13589 for k in set {
13590 if !universe.iter().any(|u| u == k) {
13591 universe.push(k.clone());
13592 }
13593 }
13594 }
13595 grouping_universe = universe;
13596 let primary = total[0].clone();
13597 grouping_sets = total;
13598 Some(primary)
13599 } else {
13600 // One set (a plain GROUP BY list, or a single-set
13601 // spelling like GROUPING SETS ((a, b))). An EMPTY
13602 // single set — GROUPING SETS (()) — stays
13603 // `Some(vec![])`: the grand-total group, which must
13604 // run the aggregate path.
13605 Some(total.into_iter().next().unwrap_or_default())
13606 }
13607 }
13608 } else {
13609 None
13610 };
13611 let having = if matches!(self.peek(), Token::Having) {
13612 self.advance();
13613 Some(self.parse_expr(0)?)
13614 } else {
13615 None
13616 };
13617 // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
13618 // OVER w parsed to a marker above; inline each definition
13619 // into the referencing WindowFunction nodes.
13620 let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
13621 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
13622 self.advance();
13623 loop {
13624 let wname = self.expect_ident_like()?;
13625 if !matches!(self.peek(), Token::As) {
13626 return Err(self.err(format!(
13627 "expected AS after WINDOW {wname}, got {:?}",
13628 self.peek()
13629 )));
13630 }
13631 self.advance();
13632 // v7.39 (round 229) — PG rejects a redefinition outright.
13633 if window_defs
13634 .iter()
13635 .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
13636 {
13637 return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
13638 }
13639 let def = self.parse_over_clause()?;
13640 // A definition may itself copy an earlier one
13641 // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
13642 // so resolve it against the defs already in scope. Same
13643 // copy rules as an `OVER (w1 …)` in the select list.
13644 let mut probe = Expr::WindowFunction {
13645 name: String::new(),
13646 args: Vec::new(),
13647 partition_by: def.0,
13648 order_by: def.1,
13649 frame: def.2,
13650 null_treatment: crate::ast::NullTreatment::Respect,
13651 filter: None,
13652 };
13653 Self::substitute_named_windows(&mut probe, &window_defs)
13654 .map_err(|m| self.err(m))?;
13655 let Expr::WindowFunction {
13656 partition_by,
13657 order_by,
13658 frame,
13659 ..
13660 } = probe
13661 else {
13662 unreachable!("probe is a WindowFunction")
13663 };
13664 window_defs.push((wname, (partition_by, order_by, frame)));
13665 if matches!(self.peek(), Token::Comma) {
13666 self.advance();
13667 continue;
13668 }
13669 break;
13670 }
13671 }
13672 // v7.39 (round 705) — which definitions did anything reference?
13673 // The ones nothing did used to be dropped here, unexamined, so
13674 // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
13675 // definition whether referenced or not. Their key expressions ride
13676 // out on the statement for the engine to resolve.
13677 let mut window_refs: Vec<String> = Vec::new();
13678 if !window_defs.is_empty() {
13679 for it in &items {
13680 if let SelectItem::Expr { expr, .. } = it {
13681 Self::collect_named_window_refs(expr, &mut window_refs);
13682 }
13683 }
13684 }
13685 let window_check_exprs: Vec<Expr> = window_defs
13686 .iter()
13687 .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
13688 .flat_map(|(_, (partition, order, _))| {
13689 partition
13690 .iter()
13691 .cloned()
13692 .chain(order.iter().map(|(e, _, _)| e.clone()))
13693 })
13694 .collect();
13695 if !window_defs.is_empty()
13696 || items
13697 .iter()
13698 .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
13699 {
13700 for it in &mut items {
13701 if let SelectItem::Expr { expr, .. } = it {
13702 Self::substitute_named_windows(expr, &window_defs)
13703 .map_err(|m| self.err(m))?;
13704 }
13705 }
13706 }
13707 // `GROUP BY 1` — positional keys substitute with the Nth
13708 // select item's expression (same contract ORDER BY has had
13709 // since v6.x). Out-of-range positions error.
13710 let group_by = match group_by {
13711 Some(mut keys) => {
13712 for k in &mut keys {
13713 if let Expr::Literal(Literal::Integer(n)) = k {
13714 let idx = *n;
13715 if idx < 1 || idx as usize > items.len() {
13716 return Err(self.err(alloc::format!(
13717 "GROUP BY position {idx} is not in select list"
13718 )));
13719 }
13720 match &items[(idx - 1) as usize] {
13721 SelectItem::Expr { expr, .. } => *k = expr.clone(),
13722 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
13723 return Err(self.err(alloc::format!(
13724 "GROUP BY position {idx} references a wildcard item"
13725 )));
13726 }
13727 }
13728 }
13729 }
13730 Some(keys)
13731 }
13732 None => None,
13733 };
13734 let mut stmt = SelectStatement {
13735 locking: None,
13736 ctes: Vec::new(),
13737 distinct,
13738 distinct_on,
13739 items,
13740 from,
13741 where_,
13742 group_by,
13743 group_by_all,
13744 having,
13745 unions: Vec::new(),
13746 order_by: Vec::new(),
13747 limit: None,
13748 offset: None,
13749 limit_with_ties: false,
13750 window_check_exprs,
13751 };
13752 // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
13753 // first set is the primary (already on stmt.group_by); each
13754 // further set becomes a UNION ALL peer with its dropped
13755 // keys (universe minus the set) replaced by NULL literals
13756 // in the peer's items and group_by. PG-legal: non-grouped
13757 // select items must be group keys or aggregates, so a
13758 // dropped key's occurrences in the projection are exactly
13759 // the ones to nullify.
13760 // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
13761 // over a plain GROUP BY (every argument must be a group key; the
13762 // mask is then 0) and rejects anything else with 42803. SPG's
13763 // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
13764 // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
13765 // function `grouping`".
13766 if grouping_sets.len() <= 1 {
13767 let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
13768 let mut calls: Vec<Expr> = Vec::new();
13769 for item in &stmt.items {
13770 if let SelectItem::Expr { expr, .. } = item {
13771 Self::collect_grouping_calls(expr, &mut calls);
13772 }
13773 }
13774 if let Some(h) = &stmt.having {
13775 Self::collect_grouping_calls(h, &mut calls);
13776 }
13777 for call in &calls {
13778 let Expr::FunctionCall { args, .. } = call else {
13779 continue;
13780 };
13781 for a in args {
13782 if !keys.iter().any(|k| k == a) {
13783 return Err(self.err(
13784 "arguments to GROUPING must be grouping expressions of the associated query level"
13785 .to_string(),
13786 ));
13787 }
13788 }
13789 }
13790 if !calls.is_empty() {
13791 for item in &mut stmt.items {
13792 if let SelectItem::Expr { expr, .. } = item {
13793 Self::substitute_grouping_calls(expr, &[]);
13794 }
13795 }
13796 if let Some(h) = &mut stmt.having {
13797 Self::substitute_grouping_calls(h, &[]);
13798 }
13799 }
13800 }
13801 if grouping_sets.len() > 1 {
13802 // The primary set's own dropped keys nullify in the
13803 // HEAD's projection too (GROUPING SETS's first set may
13804 // omit keys other sets use).
13805 let primary = grouping_sets[0].clone();
13806 let head_dropped: Vec<Expr> = grouping_universe
13807 .iter()
13808 .filter(|u| !primary.iter().any(|k| k == *u))
13809 .cloned()
13810 .collect();
13811 for set in grouping_sets.iter().skip(1) {
13812 let mut peer = stmt.clone();
13813 peer.unions = Vec::new();
13814 let dropped: Vec<&Expr> = grouping_universe
13815 .iter()
13816 .filter(|u| !set.iter().any(|k| k == *u))
13817 .collect();
13818 // Empty set = grand-total group: `Some(vec![])` forces
13819 // the aggregate path (one collapsed row) instead of a
13820 // per-row passthrough. See the primary-set note above.
13821 peer.group_by = Some(set.clone());
13822 let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
13823 for item in &mut peer.items {
13824 if let SelectItem::Expr { expr, alias } = item {
13825 if dropped.iter().any(|d| *d == expr) {
13826 // v7.39 — keep the dropped key's name on the
13827 // NULL literal so the UNION output column
13828 // (and any top-level ORDER BY on it) still
13829 // resolves.
13830 if alias.is_none()
13831 && let Expr::Column(c) = &expr
13832 {
13833 *alias = Some(c.name.clone());
13834 }
13835 *expr = Expr::Literal(Literal::Null);
13836 } else {
13837 Self::substitute_grouping_calls(expr, &dropped_owned);
13838 }
13839 }
13840 }
13841 if let Some(h) = &mut peer.having {
13842 Self::substitute_grouping_calls(h, &dropped_owned);
13843 }
13844 stmt.unions.push((UnionKind::All, peer));
13845 }
13846 for item in &mut stmt.items {
13847 if let SelectItem::Expr { expr, alias } = item {
13848 if head_dropped.iter().any(|d| d == expr) {
13849 if alias.is_none()
13850 && let Expr::Column(c) = &expr
13851 {
13852 *alias = Some(c.name.clone());
13853 }
13854 *expr = Expr::Literal(Literal::Null);
13855 } else {
13856 Self::substitute_grouping_calls(expr, &head_dropped);
13857 }
13858 }
13859 }
13860 if let Some(h) = &mut stmt.having {
13861 Self::substitute_grouping_calls(h, &head_dropped);
13862 }
13863 // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
13864 // (while `grouping_universe` / the per-branch sets are in scope). For
13865 // each grouping() call in it, inject a per-branch hidden column
13866 // `__grp_ord_K` carrying that branch's mask into the head + every
13867 // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
13868 // preserves this pre-set order_by; the engine strips `__grp_ord_*`
13869 // from the final output. A standalone grouping-set query has ORDER BY
13870 // (not an explicit set-op) next, so consuming it here is safe.
13871 // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
13872 // rollup carries the hierarchical order: sort by the grouping
13873 // keys with the rolled-up NULLs last, which is exactly the
13874 // interleaving both oracles emit. A client's own ORDER BY wins,
13875 // which is what MySQL does (MariaDB refuses to let one be
13876 // written at all).
13877 // The synthesised keys have to travel the SAME path a written
13878 // ORDER BY does: the block below is what turns a `grouping()`
13879 // call into the per-branch `__grp_ord_K` column the engine can
13880 // actually sort on. Bypassing it left a bare `grouping(text)`
13881 // for the evaluator to reject.
13882 let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
13883 self.parse_order_by_keys()?
13884 } else if mysql_rollup {
13885 Self::mysql_rollup_order(&grouping_universe)
13886 } else {
13887 Vec::new()
13888 };
13889 if !synthesised_or_parsed.is_empty() {
13890 let mut order_keys = synthesised_or_parsed;
13891 let mut grp_exprs: Vec<Expr> = Vec::new();
13892 for ob in &order_keys {
13893 Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
13894 }
13895 for (k, gexpr) in grp_exprs.iter().enumerate() {
13896 let colname = alloc::format!("__grp_ord_{k}");
13897 // Head branch (primary set) uses `head_dropped`.
13898 let mut he = gexpr.clone();
13899 Self::substitute_grouping_calls(&mut he, &head_dropped);
13900 stmt.items.push(SelectItem::Expr {
13901 expr: he,
13902 alias: Some(colname.clone()),
13903 });
13904 // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
13905 for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
13906 let set = &grouping_sets[i + 1];
13907 let dropped: Vec<Expr> = grouping_universe
13908 .iter()
13909 .filter(|u| !set.iter().any(|k| k == *u))
13910 .cloned()
13911 .collect();
13912 let mut pe = gexpr.clone();
13913 Self::substitute_grouping_calls(&mut pe, &dropped);
13914 peer.items.push(SelectItem::Expr {
13915 expr: pe,
13916 alias: Some(colname.clone()),
13917 });
13918 }
13919 }
13920 for ob in &mut order_keys {
13921 Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
13922 }
13923 stmt.order_by = order_keys;
13924 }
13925 }
13926 Ok(stmt)
13927 }
13928
13929 /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
13930 /// as ORDER BY keys.
13931 ///
13932 /// Per key: the rollup marker, then the key. Sorting on the key alone
13933 /// is not enough, and a table with a NULL in it says why — MariaDB puts
13934 /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
13935 /// the ROLLUP-introduced NULL last, and both print as NULL.
13936 /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
13937 /// real group including the data-NULL one, 1 only for the row the
13938 /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
13939 /// rolls up to NULL|2, a|1, b|3, NULL|6.
13940 ///
13941 /// `#[inline(never)]`: its locals must not join the statement parser's
13942 /// frame, which round 430 measured sitting against the nesting guard.
13943 #[inline(never)]
13944 fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
13945 let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
13946 for e in keys {
13947 out.push(OrderBy {
13948 expr: Expr::FunctionCall {
13949 name: "grouping".into(),
13950 args: alloc::vec![e.clone()],
13951 },
13952 desc: false,
13953 nulls_first: None,
13954 collation: None,
13955 });
13956 out.push(OrderBy {
13957 expr: e.clone(),
13958 desc: false,
13959 // MySQL orders NULL first on an ascending key.
13960 nulls_first: Some(true),
13961 collation: None,
13962 });
13963 }
13964 out
13965 }
13966
13967 /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
13968 /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
13969 #[inline(never)]
13970 fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
13971 use crate::ast::MaintainKind;
13972 self.skip_paren_option_list();
13973 let kind = match self.peek() {
13974 // `TABLE` and `INDEX` lex as keywords, not identifiers.
13975 Token::Table | Token::Index => {
13976 self.advance();
13977 MaintainKind::ReindexRelation
13978 }
13979 Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
13980 "index" | "table" => {
13981 self.advance();
13982 MaintainKind::ReindexRelation
13983 }
13984 "schema" => {
13985 self.advance();
13986 MaintainKind::ReindexSchema
13987 }
13988 "system" | "database" => {
13989 self.advance();
13990 MaintainKind::Whole
13991 }
13992 // PG requires the object type; anything else is the
13993 // caller's problem, not something to swallow.
13994 _ => MaintainKind::ReindexRelation,
13995 },
13996 _ => MaintainKind::Whole,
13997 };
13998 // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
13999 // allows the plain form, so the modifier is recorded rather than
14000 // skipped. It still has no effect on how the reindex runs.
14001 let mut concurrently = false;
14002 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14003 self.advance();
14004 concurrently = true;
14005 }
14006 let target = self.take_optional_maintain_name();
14007 self.consume_until_statement_boundary();
14008 Ok(Statement::Maintain {
14009 kind,
14010 concurrently,
14011 target,
14012 })
14013 }
14014
14015 /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14016 /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14017 #[inline(never)]
14018 fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14019 use crate::ast::MaintainKind;
14020 self.skip_paren_option_list();
14021 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14022 self.advance();
14023 }
14024 let target = self.take_optional_maintain_name();
14025 self.consume_until_statement_boundary();
14026 Ok(Statement::Maintain {
14027 kind: if target.is_some() {
14028 MaintainKind::ClusterRelation
14029 } else {
14030 MaintainKind::Whole
14031 },
14032 // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14033 // transaction block quite happily (measured).
14034 concurrently: false,
14035 target,
14036 })
14037 }
14038
14039 /// The next token as a relation / schema name, when there is one.
14040 fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14041 match self.peek() {
14042 Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14043 Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14044 _ => None,
14045 },
14046 _ => None,
14047 }
14048 }
14049
14050 /// A parenthesised option list, absorbed.
14051 fn skip_paren_option_list(&mut self) {
14052 if !matches!(self.peek(), Token::LParen) {
14053 return;
14054 }
14055 let mut depth = 0usize;
14056 loop {
14057 match self.advance() {
14058 Token::LParen => depth += 1,
14059 Token::RParen => {
14060 depth -= 1;
14061 if depth == 0 {
14062 return;
14063 }
14064 }
14065 Token::Eof => return,
14066 _ => {}
14067 }
14068 }
14069 }
14070
14071 /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14072 /// column list.
14073 ///
14074 /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14075 /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14076 /// / ALL. The three that describe physical storage have no meaning
14077 /// here, so they parse and change nothing rather than making a
14078 /// dump that mentions them fail to load.
14079 ///
14080 /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14081 /// parse chain the nesting sentinel is tuned against.
14082 #[inline(never)]
14083 fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14084 self.advance(); // LIKE
14085 let source = self.expect_ident_like()?;
14086 let mut options = crate::ast::LikeOptions::default();
14087 loop {
14088 let including = match self.peek() {
14089 Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14090 Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14091 _ => break,
14092 };
14093 self.advance();
14094 // `ALL` lexes as its own keyword, not an identifier.
14095 let opt = if matches!(self.peek(), Token::All) {
14096 self.advance();
14097 alloc::string::String::from("all")
14098 } else {
14099 self.expect_ident_like()?
14100 };
14101 let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14102 o.defaults = on;
14103 o.constraints = on;
14104 o.identity = on;
14105 o.generated = on;
14106 o.indexes = on;
14107 o.comments = on;
14108 };
14109 match opt.to_ascii_lowercase().as_str() {
14110 "all" => set(&mut options, including),
14111 "defaults" => options.defaults = including,
14112 "constraints" => options.constraints = including,
14113 "identity" => options.identity = including,
14114 "generated" => options.generated = including,
14115 "indexes" => options.indexes = including,
14116 "comments" => options.comments = including,
14117 // No storage model to copy into.
14118 "storage" | "statistics" | "compression" => {}
14119 other => {
14120 return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14121 }
14122 }
14123 }
14124 Ok(crate::ast::LikeSpec {
14125 source,
14126 at,
14127 options,
14128 })
14129 }
14130
14131 fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14132 // Caller already consumed CREATE; we're sitting on TABLE.
14133 debug_assert!(matches!(self.peek(), Token::Table));
14134 self.advance();
14135 let if_not_exists = self.consume_if_not_exists();
14136 let name = self.expect_ident_like()?;
14137 // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14138 // child shape has no column list; the child inherits its
14139 // columns from the parent at engine-DDL time. Detect it
14140 // before the `(` requirement below.
14141 if matches!(self.peek(), Token::Partition)
14142 && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14143 {
14144 self.advance(); // PARTITION
14145 self.advance(); // of
14146 let partition_of = self.parse_partition_of_tail()?;
14147 return Ok(Statement::CreateTable(CreateTableStatement {
14148 temporary: false,
14149 name,
14150 columns: Vec::new(),
14151 like_specs: Vec::new(),
14152 inherits: Vec::new(),
14153 if_not_exists,
14154 foreign_keys: Vec::new(),
14155 table_constraints: Vec::new(),
14156 partition_by: None,
14157 partition_of: Some(partition_of),
14158 }));
14159 }
14160 // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14161 // the materialized-view materialisation path (run the SELECT, infer the
14162 // column types, create + populate the table) but marks the node so the
14163 // executor creates a plain table without a mat-view registry entry.
14164 if matches!(self.peek(), Token::As) {
14165 self.advance();
14166 let body_stmt = self.parse_select_stmt()?;
14167 let Statement::Select(body) = body_stmt else {
14168 return Err(self.err(format!(
14169 "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14170 )));
14171 };
14172 let with_data = self.parse_optional_with_data(true)?;
14173 return Ok(Statement::CreateMaterializedView(
14174 crate::ast::CreateMaterializedViewStatement {
14175 temporary: false,
14176 name,
14177 if_not_exists,
14178 columns: Vec::new(),
14179 body,
14180 with_data,
14181 as_plain_table: true,
14182 },
14183 ));
14184 }
14185 if !matches!(self.peek(), Token::LParen) {
14186 return Err(self.err(format!(
14187 "expected '(' after table name, got {:?}",
14188 self.peek()
14189 )));
14190 }
14191 self.advance();
14192 let mut columns = Vec::new();
14193 let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14194 let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14195 let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14196 loop {
14197 // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14198 // column list. It is how a child that adds nothing of its own is
14199 // written, and this loop demanded at least one entry: `syntax
14200 // error at or near ")"`. The child takes the parent's columns,
14201 // which the INHERITS clause already arranges.
14202 if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14203 self.advance();
14204 break;
14205 }
14206 // v7.6.0 / v7.9.18 — distinguish table-level constraint
14207 // clauses from column definitions. Constraints start
14208 // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14209 // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14210 // a column.
14211 if self.peek_table_level_pk_start() {
14212 table_constraints.push(self.parse_table_level_primary_key()?);
14213 } else if matches!(self.peek(), Token::Like) {
14214 // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14215 // <opt> ]*`. The source table's shape lives in the catalog,
14216 // so this records the clause and the engine expands it.
14217 like_specs.push(self.parse_create_table_like(columns.len())?);
14218 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14219 // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14220 table_constraints.push(self.parse_table_level_exclude()?);
14221 } else if self.peek_table_level_unique_start() {
14222 table_constraints.push(self.parse_table_level_unique()?);
14223 } else if self.peek_table_level_check_start() {
14224 // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14225 table_constraints.push(self.parse_table_level_check()?);
14226 } else if self.peek_mysql_inline_key_start() {
14227 // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14228 // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14229 // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14230 // inside the column list. Skip name + paren list;
14231 // for UNIQUE KEY, register as a UC.
14232 if let Some(uc) = self.parse_mysql_inline_key()? {
14233 table_constraints.push(uc);
14234 }
14235 } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14236 // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14237 // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14238 // CHECK is named, and the named-CONSTRAINT arm used
14239 // to accept FOREIGN KEY only. The name is accepted
14240 // and discarded — same handling as every other SPG
14241 // constraint name.
14242 self.advance(); // CONSTRAINT
14243 // v7.39 (read01 round 48) — the name is kept now: the schema
14244 // stores it, so DROP / RENAME CONSTRAINT can find it.
14245 let con_name = self.expect_ident_like()?;
14246 let mut tc = match kind {
14247 NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14248 NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14249 NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14250 NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14251 };
14252 match &mut tc {
14253 crate::ast::TableConstraint::Check { name, .. }
14254 | crate::ast::TableConstraint::Unique { name, .. }
14255 | crate::ast::TableConstraint::PrimaryKey { name, .. }
14256 | crate::ast::TableConstraint::Exclude { name, .. } => {
14257 *name = Some(con_name);
14258 }
14259 _ => {}
14260 }
14261 table_constraints.push(tc);
14262 } else if self.peek_constraint_or_fk_start() {
14263 foreign_keys.push(self.parse_table_level_fk()?);
14264 } else {
14265 let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14266 // v7.13.0 — fold inline UNIQUE / CHECK column
14267 // constraints into table-level entries so the
14268 // engine path stays uniform.
14269 if col.is_unique {
14270 table_constraints.push(crate::ast::TableConstraint::Unique {
14271 name: None,
14272 columns: alloc::vec![col.name.clone()],
14273 nulls_not_distinct: col.unique_nulls_not_distinct,
14274 deferrable: col.constraint_deferrable,
14275 initially_deferred: col.constraint_initially_deferred,
14276 });
14277 }
14278 if let Some(check_expr) = col.check.clone() {
14279 table_constraints.push(crate::ast::TableConstraint::Check {
14280 name: None,
14281 expr: check_expr,
14282 not_valid: false,
14283 });
14284 }
14285 columns.push(col);
14286 if let Some(fk) = col_level_fk {
14287 foreign_keys.push(fk);
14288 }
14289 }
14290 match self.peek() {
14291 Token::Comma => {
14292 self.advance();
14293 }
14294 Token::RParen => {
14295 self.advance();
14296 break;
14297 }
14298 other => {
14299 return Err(
14300 self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14301 );
14302 }
14303 }
14304 }
14305 // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14306 // `CREATE TABLE k (LIKE t)` is a complete definition even though
14307 // nothing is written between the parentheses.
14308 // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14309 // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14310 // empty parentheses were a parse error in their own right — quite apart
14311 // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14312 // SPG does not have (filed separately).
14313 let _ = &like_specs;
14314 // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14315 // It sits between the column list and the MySQL table options,
14316 // and it was a syntax error until this round.
14317 let mut inherits: Vec<String> = Vec::new();
14318 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14319 if k.eq_ignore_ascii_case("inherits"))
14320 {
14321 self.advance();
14322 if !matches!(self.peek(), Token::LParen) {
14323 return Err(self.err(alloc::format!(
14324 "expected ( after INHERITS, got {:?}",
14325 self.peek()
14326 )));
14327 }
14328 self.advance();
14329 loop {
14330 inherits.push(self.expect_ident_like()?);
14331 if matches!(self.peek(), Token::Comma) {
14332 self.advance();
14333 continue;
14334 }
14335 break;
14336 }
14337 if !matches!(self.peek(), Token::RParen) {
14338 return Err(self.err(alloc::format!(
14339 "expected ) closing INHERITS, got {:?}",
14340 self.peek()
14341 )));
14342 }
14343 self.advance();
14344 }
14345 // v7.14.0 — consume MySQL/MariaDB table options after the
14346 // closing `)`. mysqldump emits things like
14347 // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14348 // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14349 // SPG accepts all forms as no-ops (each option is
14350 // `<ident> [=] <ident-or-string>` separated by whitespace).
14351 self.consume_mysql_table_options();
14352 // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14353 // SPG has no per-table reloptions, so accept and ignore them so a
14354 // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14355 self.consume_with_reloptions();
14356 // v7.37.6-B — declarative-partition-parent suffix
14357 // (`PARTITION BY RANGE (key_col)`) sits after the column
14358 // list + MySQL table-options. v7.37.6-B only accepts RANGE
14359 // and locks the key column at one ident; the engine then
14360 // verifies the column type is TIMESTAMPTZ.
14361 let partition_by = if matches!(self.peek(), Token::Partition) {
14362 self.advance(); // PARTITION
14363 if !self.peek_is_by() {
14364 return Err(self.err(format!(
14365 "expected BY after PARTITION, got {:?}",
14366 self.peek()
14367 )));
14368 }
14369 self.advance();
14370 Some(self.parse_partition_by_tail()?)
14371 } else {
14372 None
14373 };
14374 Ok(Statement::CreateTable(CreateTableStatement {
14375 temporary: false,
14376 name,
14377 columns,
14378 like_specs,
14379 inherits,
14380 if_not_exists,
14381 foreign_keys,
14382 table_constraints,
14383 partition_by,
14384 partition_of: None,
14385 }))
14386 }
14387
14388 /// v7.37.6-B — case-insensitive ident match helper for the
14389 /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14390 /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14391 /// didn't burn a global keyword slot for each (see the
14392 /// `Token::Partition` doc-comment in `lexer.rs`).
14393 fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14394 matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14395 }
14396
14397 /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14398 /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14399 fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14400 use crate::ast::{PartitionBySpec, PartitionKindAst};
14401 let kind = match self.peek() {
14402 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14403 self.advance();
14404 PartitionKindAst::Range
14405 }
14406 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14407 self.advance();
14408 PartitionKindAst::List
14409 }
14410 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14411 self.advance();
14412 PartitionKindAst::Hash
14413 }
14414 other => {
14415 return Err(self.err(format!(
14416 "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14417 )));
14418 }
14419 };
14420 if !matches!(self.peek(), Token::LParen) {
14421 return Err(self.err(format!(
14422 "expected '(' after PARTITION BY <strategy>, got {:?}",
14423 self.peek()
14424 )));
14425 }
14426 self.advance();
14427 let mut key_columns = Vec::new();
14428 loop {
14429 key_columns.push(self.expect_ident_like()?);
14430 match self.peek() {
14431 Token::Comma => {
14432 self.advance();
14433 }
14434 Token::RParen => {
14435 self.advance();
14436 break;
14437 }
14438 other => {
14439 return Err(self.err(format!(
14440 "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14441 )));
14442 }
14443 }
14444 }
14445 if key_columns.is_empty() {
14446 return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14447 }
14448 Ok(PartitionBySpec { kind, key_columns })
14449 }
14450
14451 /// v7.37.6-B — after `PARTITION OF`, expect
14452 /// <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14453 /// or
14454 /// <parent> DEFAULT
14455 fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14456 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14457 let parent_name = self.expect_ident_like()?;
14458 // v7.37.6-B rejects an explicit column list — the child
14459 // inherits from the parent. mailrs round-7 taught us that
14460 // CREATE TABLE-side schema reconciliation hides drift, so
14461 // we surface this as a parse error rather than silently
14462 // ignoring user columns.
14463 if matches!(self.peek(), Token::LParen) {
14464 return Err(self.err(
14465 "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14466 at v7.37.6-B; the child inherits its columns from the parent"
14467 .to_string(),
14468 ));
14469 }
14470 let bounds = match self.peek() {
14471 Token::Default => {
14472 self.advance();
14473 PartitionOfBoundsAst::Default
14474 }
14475 Token::For => {
14476 self.advance();
14477 if !matches!(self.peek(), Token::Values) {
14478 return Err(
14479 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14480 );
14481 }
14482 self.advance();
14483 // WITH is not a reserved Token in the lexer — it lexes
14484 // as Token::Ident("with"). Disambiguate manually.
14485 let want_with = matches!(
14486 self.peek(),
14487 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14488 );
14489 if want_with {
14490 self.advance();
14491 if !matches!(self.peek(), Token::LParen) {
14492 return Err(self.err(format!(
14493 "expected '(' after FOR VALUES WITH, got {:?}",
14494 self.peek()
14495 )));
14496 }
14497 self.advance();
14498 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14499 loop {
14500 let key = self.expect_ident_like()?;
14501 let n = match self.peek().clone() {
14502 Token::Integer(v) if u32::try_from(v).is_ok() => {
14503 self.advance();
14504 v as u32
14505 }
14506 other => {
14507 return Err(self.err(format!(
14508 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14509 )));
14510 }
14511 };
14512 match key.to_ascii_uppercase().as_str() {
14513 "MODULUS" => modulus = Some(n),
14514 "REMAINDER" => remainder = Some(n),
14515 other => {
14516 return Err(self.err(format!(
14517 "FOR VALUES WITH: unknown key {other:?}; \
14518 expected MODULUS or REMAINDER"
14519 )));
14520 }
14521 }
14522 match self.peek() {
14523 Token::Comma => {
14524 self.advance();
14525 }
14526 Token::RParen => {
14527 self.advance();
14528 break;
14529 }
14530 other => {
14531 return Err(self.err(format!(
14532 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14533 )));
14534 }
14535 }
14536 }
14537 let modulus = modulus
14538 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14539 let remainder = remainder.ok_or_else(|| {
14540 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14541 })?;
14542 if modulus == 0 {
14543 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14544 }
14545 if remainder >= modulus {
14546 return Err(self.err(format!(
14547 "FOR VALUES WITH: REMAINDER ({remainder}) \
14548 must be < MODULUS ({modulus})"
14549 )));
14550 }
14551 PartitionOfBoundsAst::Hash { modulus, remainder }
14552 } else {
14553 match self.peek() {
14554 Token::From => {
14555 self.advance();
14556 let lower = Box::new(self.parse_partition_bound_expr()?);
14557 if !matches!(self.peek(), Token::To) {
14558 return Err(self.err(format!(
14559 "expected TO after FROM (...), got {:?}",
14560 self.peek()
14561 )));
14562 }
14563 self.advance();
14564 let upper = Box::new(self.parse_partition_bound_expr()?);
14565 PartitionOfBoundsAst::Range { lower, upper }
14566 }
14567 // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14568 Token::In => {
14569 self.advance();
14570 if !matches!(self.peek(), Token::LParen) {
14571 return Err(self.err(format!(
14572 "expected '(' after FOR VALUES IN, got {:?}",
14573 self.peek()
14574 )));
14575 }
14576 self.advance();
14577 let mut values = Vec::new();
14578 loop {
14579 values.push(self.parse_expr(0)?);
14580 match self.peek() {
14581 Token::Comma => {
14582 self.advance();
14583 }
14584 Token::RParen => {
14585 self.advance();
14586 break;
14587 }
14588 other => {
14589 return Err(self.err(format!(
14590 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
14591 )));
14592 }
14593 }
14594 }
14595 if values.is_empty() {
14596 return Err(self.err(
14597 "FOR VALUES IN requires at least one literal".to_string(),
14598 ));
14599 }
14600 PartitionOfBoundsAst::List { values }
14601 }
14602 other => {
14603 return Err(self.err(format!(
14604 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
14605 )));
14606 }
14607 }
14608 }
14609 }
14610 other => {
14611 return Err(self.err(format!(
14612 "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
14613 )));
14614 }
14615 };
14616 Ok(PartitionOfSpec {
14617 parent_name,
14618 bounds,
14619 })
14620 }
14621
14622 /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
14623 /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
14624 /// markers (no-arg builtins) so the engine resolves them
14625 /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
14626 fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
14627 if !matches!(self.peek(), Token::LParen) {
14628 return Err(self.err(format!(
14629 "expected '(' before partition bound, got {:?}",
14630 self.peek()
14631 )));
14632 }
14633 self.advance();
14634 let expr = match self.peek() {
14635 Token::Ident(s) | Token::QuotedIdent(s)
14636 if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
14637 {
14638 let name = s.to_ascii_uppercase();
14639 self.advance();
14640 crate::ast::Expr::FunctionCall {
14641 name,
14642 args: Vec::new(),
14643 }
14644 }
14645 _ => self.parse_expr(0)?,
14646 };
14647 if !matches!(self.peek(), Token::RParen) {
14648 return Err(self.err(format!(
14649 "expected ')' after partition bound, got {:?}",
14650 self.peek()
14651 )));
14652 }
14653 self.advance();
14654 Ok(expr)
14655 }
14656
14657 /// v7.14.0 — true when the next tokens look like an inline
14658 /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
14659 /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
14660 /// — each followed by an optional name + `(...)`. Critical:
14661 /// a column NAMED `key` / `index` (PG accepts as ident) must
14662 /// NOT be mistaken for the KEY constraint shape. We disambig
14663 /// by requiring the keyword to be followed by either `(` or
14664 /// `<ident> (`.
14665 fn peek_mysql_inline_key_start(&self) -> bool {
14666 let cur = self.peek();
14667 // Shapes:
14668 // KEY (cols)
14669 // KEY name (cols)
14670 // INDEX (cols)
14671 // INDEX name (cols)
14672 // UNIQUE KEY [name] (cols)
14673 // UNIQUE INDEX [name] (cols)
14674 // FULLTEXT [KEY|INDEX] [name] (cols)
14675 // SPATIAL [KEY|INDEX] [name] (cols)
14676 let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
14677 // tokens at skip = the position AFTER the index-form
14678 // keywords (KEY/INDEX) have been consumed.
14679 match self.tokens.get(skip) {
14680 Some(Token::LParen) => true,
14681 Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
14682 matches!(self.tokens.get(skip + 1), Some(Token::LParen))
14683 }
14684 _ => false,
14685 }
14686 };
14687 // `INDEX` lexes as Token::Index (reserved), not as
14688 // Token::Ident("index"). Both shapes count as a KEY/INDEX
14689 // start; the peek helper below handles either.
14690 let is_key_or_index_tok = |t: &Token| -> bool {
14691 matches!(t, Token::Index)
14692 || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
14693 };
14694 match cur {
14695 Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
14696 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14697 after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
14698 }
14699 Token::Ident(s)
14700 if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
14701 {
14702 let nxt = self.tokens.get(self.pos + 1);
14703 let after_after = if nxt.is_some_and(is_key_or_index_tok) {
14704 self.pos + 2
14705 } else {
14706 self.pos + 1
14707 };
14708 after_keyword_followed_by_paren_or_ident_paren(after_after)
14709 }
14710 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
14711 let nxt = self.tokens.get(self.pos + 1);
14712 if !nxt.is_some_and(is_key_or_index_tok) {
14713 return false;
14714 }
14715 after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
14716 }
14717 _ => false,
14718 }
14719 }
14720
14721 /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
14722 /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
14723 /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
14724 /// returns Some(TableConstraint::Index) so the engine builds
14725 /// a real BTree index on the leading column (mysqldump
14726 /// `KEY idx_posts_author (author_id)` shape).
14727 /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
14728 /// (the storage layer has no matching AM).
14729 fn parse_mysql_inline_key(
14730 &mut self,
14731 ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
14732 // Detect UNIQUE prefix.
14733 let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
14734 {
14735 self.advance();
14736 true
14737 } else {
14738 false
14739 };
14740 // Consume FULLTEXT / SPATIAL prefix and record which one
14741 // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
14742 // dedicated TableConstraint variant so the engine can
14743 // build a tsvector-GIN; SPATIAL still has no matching
14744 // AM, so it falls back to accept-as-no-op.
14745 let mut is_fulltext = false;
14746 let mut is_spatial = false;
14747 if let Token::Ident(s) = self.peek().clone() {
14748 if s.eq_ignore_ascii_case("fulltext") {
14749 self.advance();
14750 is_fulltext = true;
14751 } else if s.eq_ignore_ascii_case("spatial") {
14752 self.advance();
14753 is_spatial = true;
14754 }
14755 }
14756 // KEY / INDEX keyword. `INDEX` lexes as Token::Index
14757 // (reserved); accept either token shape.
14758 match self.peek() {
14759 Token::Index => {
14760 self.advance();
14761 }
14762 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14763 self.advance();
14764 }
14765 other => {
14766 return Err(self.err(alloc::format!(
14767 "expected KEY/INDEX in inline index declaration, got {other:?}"
14768 )));
14769 }
14770 }
14771 // Optional index name (an ident before the `(`).
14772 // v7.15.0 — capture the name when present so the engine
14773 // builds the secondary index under the user's chosen
14774 // name (matches mysqldump's `KEY idx_x (col)` shape).
14775 let mut idx_name: Option<String> = None;
14776 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
14777 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
14778 {
14779 if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
14780 idx_name = Some(s);
14781 }
14782 }
14783 // Optional `USING BTREE` / `USING HASH` (MySQL).
14784 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
14785 self.advance();
14786 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14787 self.advance();
14788 }
14789 }
14790 // Required column list `(col [, col]*)`.
14791 if !matches!(self.peek(), Token::LParen) {
14792 return Err(self.err(alloc::format!(
14793 "expected '(' in inline KEY/INDEX, got {:?}",
14794 self.peek()
14795 )));
14796 }
14797 self.advance();
14798 let mut cols: Vec<String> = Vec::new();
14799 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
14800 self.advance();
14801 cols.push(s);
14802 // Skip optional `(length)` per-column prefix.
14803 if matches!(self.peek(), Token::LParen) {
14804 let mut depth = 1usize;
14805 self.advance();
14806 while depth > 0 {
14807 match self.peek() {
14808 Token::LParen => depth += 1,
14809 Token::RParen => depth -= 1,
14810 Token::Eof => break,
14811 _ => {}
14812 }
14813 self.advance();
14814 }
14815 }
14816 // Skip optional ASC / DESC.
14817 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
14818 || matches!(self.peek(), Token::Asc | Token::Desc)
14819 {
14820 self.advance();
14821 }
14822 if matches!(self.peek(), Token::Comma) {
14823 self.advance();
14824 continue;
14825 }
14826 break;
14827 }
14828 if matches!(self.peek(), Token::RParen) {
14829 self.advance();
14830 }
14831 // Trailing options on the inline index — comment / etc.
14832 // Skip until comma or `)`.
14833 while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
14834 self.advance();
14835 }
14836 if cols.is_empty() {
14837 return Ok(None);
14838 }
14839 if is_unique {
14840 // Carry the captured idx_name on UNIQUE too so future
14841 // engine work can name the underlying BTree
14842 // accordingly; today the unique-constraint installer
14843 // synthesises the name itself, but Display round-trip
14844 // benefits from preserving it.
14845 Ok(Some(crate::ast::TableConstraint::Unique {
14846 name: idx_name,
14847 columns: cols,
14848 nulls_not_distinct: false,
14849 // MySQL inline UNIQUE KEY has no deferral vocabulary.
14850 deferrable: false,
14851 initially_deferred: false,
14852 }))
14853 } else if is_fulltext {
14854 // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
14855 // routes through `TableConstraint::FulltextIndex`;
14856 // the engine builds a tsvector-GIN over each named
14857 // column so MATCH AGAINST gets a real inverted
14858 // index instead of a silently-dropped declaration.
14859 Ok(Some(crate::ast::TableConstraint::FulltextIndex {
14860 name: idx_name,
14861 columns: cols,
14862 }))
14863 } else if is_spatial {
14864 // SPG has no native SPATIAL AM. Accept-as-no-op
14865 // (declaration is parsed, but no index is built).
14866 Ok(None)
14867 } else {
14868 // v7.15.0 — plain KEY / INDEX builds a real BTree
14869 // secondary index.
14870 Ok(Some(crate::ast::TableConstraint::Index {
14871 name: idx_name,
14872 columns: cols,
14873 }))
14874 }
14875 }
14876
14877 /// v7.14.0 — consume MySQL/MariaDB table-options tail after
14878 /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
14879 /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
14880 /// (in any order, separated by whitespace).
14881 /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
14882 /// storage-parameter clause on CREATE TABLE. SPG has no per-table
14883 /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
14884 /// bare ident here, and only the parenthesised form is reloptions (so this
14885 /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
14886 fn consume_with_reloptions(&mut self) {
14887 let is_with = matches!(
14888 self.peek(),
14889 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14890 );
14891 if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
14892 return;
14893 }
14894 self.advance(); // WITH
14895 self.advance(); // (
14896 let mut depth = 1u32;
14897 while depth > 0 && !matches!(self.peek(), Token::Eof) {
14898 match self.peek() {
14899 Token::LParen => depth += 1,
14900 Token::RParen => depth -= 1,
14901 _ => {}
14902 }
14903 self.advance();
14904 }
14905 }
14906
14907 fn consume_mysql_table_options(&mut self) {
14908 loop {
14909 // Heuristic: a table option is an ident (or `DEFAULT`
14910 // reserved keyword) followed by `=` and an
14911 // ident / string / integer.
14912 let name_lc = match self.peek().clone() {
14913 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
14914 Token::Default => alloc::string::String::from("default"),
14915 _ => break,
14916 };
14917 let known = matches!(
14918 name_lc.as_str(),
14919 "engine"
14920 | "default"
14921 | "charset"
14922 | "collate"
14923 | "auto_increment"
14924 | "row_format"
14925 | "comment"
14926 | "pack_keys"
14927 | "stats_persistent"
14928 | "stats_auto_recalc"
14929 | "stats_sample_pages"
14930 | "key_block_size"
14931 | "tablespace"
14932 | "min_rows"
14933 | "max_rows"
14934 | "checksum"
14935 | "delay_key_write"
14936 | "insert_method"
14937 | "data"
14938 | "index"
14939 | "encryption"
14940 | "compression"
14941 );
14942 if !known {
14943 break;
14944 }
14945 self.advance(); // option name
14946 // `DEFAULT` optional prefix is followed by `CHARSET` /
14947 // `COLLATE`; consume the next ident too.
14948 if name_lc == "default" {
14949 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14950 self.advance();
14951 }
14952 }
14953 if matches!(self.peek(), Token::Eq) {
14954 self.advance();
14955 }
14956 match self.peek() {
14957 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_) | Token::Integer(_) => {
14958 self.advance();
14959 }
14960 _ => {}
14961 }
14962 }
14963 }
14964
14965 /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
14966 /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
14967 /// sure (otherwise a column literally named `primary` would
14968 /// be mistaken).
14969 fn peek_table_level_pk_start(&self) -> bool {
14970 let cur = self.peek();
14971 let nxt = self.tokens.get(self.pos + 1);
14972 let nxt2 = self.tokens.get(self.pos + 2);
14973 let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
14974 let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
14975 let is_lparen = matches!(nxt2, Some(Token::LParen));
14976 is_primary && is_key && is_lparen
14977 }
14978
14979 /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
14980 /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
14981 /// (mailrs round-5 G10).
14982 fn peek_table_level_unique_start(&self) -> bool {
14983 let cur = self.peek();
14984 let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
14985 if !is_unique {
14986 return false;
14987 }
14988 let n1 = self.tokens.get(self.pos + 1);
14989 // Plain `UNIQUE (…)`.
14990 if matches!(n1, Some(Token::LParen)) {
14991 return true;
14992 }
14993 // `UNIQUE NULLS [NOT] DISTINCT (…)`.
14994 let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
14995 if !is_nulls {
14996 return false;
14997 }
14998 let n2 = self.tokens.get(self.pos + 2);
14999 let n3 = self.tokens.get(self.pos + 3);
15000 let n4 = self.tokens.get(self.pos + 4);
15001 // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15002 if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15003 return true;
15004 }
15005 // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15006 if matches!(n2, Some(Token::Not))
15007 && matches!(n3, Some(Token::Distinct))
15008 && matches!(n4, Some(Token::LParen))
15009 {
15010 return true;
15011 }
15012 false
15013 }
15014
15015 fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15016 self.advance(); // PRIMARY
15017 self.advance(); // KEY
15018 let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15019 // v7.39 (round 711) — the trailer's values are CARRIED now; round
15020 // 621 consumed and dropped them (the storing half of F08).
15021 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15022 Ok(crate::ast::TableConstraint::PrimaryKey {
15023 name: None,
15024 columns,
15025 deferrable,
15026 initially_deferred,
15027 })
15028 }
15029
15030 fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15031 self.advance(); // UNIQUE
15032 // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15033 // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15034 // is `NULLS DISTINCT` per the SQL standard.
15035 let mut nulls_not_distinct = false;
15036 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15037 let n1 = self.tokens.get(self.pos + 1);
15038 let n2 = self.tokens.get(self.pos + 2);
15039 let is_not = matches!(n1, Some(Token::Not));
15040 let is_distinct = matches!(n2, Some(Token::Distinct));
15041 if is_not && is_distinct {
15042 self.advance(); // NULLS
15043 self.advance(); // NOT
15044 self.advance(); // DISTINCT
15045 nulls_not_distinct = true;
15046 } else if matches!(n1, Some(Token::Distinct)) {
15047 self.advance(); // NULLS
15048 self.advance(); // DISTINCT
15049 }
15050 }
15051 let columns = self.parse_paren_ident_list("UNIQUE")?;
15052 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15053 Ok(crate::ast::TableConstraint::Unique {
15054 name: None,
15055 columns,
15056 nulls_not_distinct,
15057 deferrable,
15058 initially_deferred,
15059 })
15060 }
15061
15062 /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15063 /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15064 /// expression.
15065 /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15066 /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15067 /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15068 /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15069 /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15070 /// commit: `NOT` starts no other suffix here, but reading both
15071 /// tokens before advancing keeps the caller's error message intact
15072 /// if someone writes `NOT NULL` by mistake.
15073 fn parse_not_valid_suffix(&mut self) -> bool {
15074 if !matches!(self.peek(), Token::Not) {
15075 return false;
15076 }
15077 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15078 {
15079 return false;
15080 }
15081 self.advance();
15082 self.advance();
15083 true
15084 }
15085
15086 fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15087 self.advance(); // EXCLUDE
15088 // Optional `USING <method>`.
15089 let mut method = None;
15090 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15091 self.advance();
15092 method = Some(match self.advance() {
15093 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15094 other => {
15095 return Err(self.err(alloc::format!(
15096 "expected index method after USING, got {other:?}"
15097 )));
15098 }
15099 });
15100 }
15101 if !matches!(self.peek(), Token::LParen) {
15102 return Err(self.err(alloc::format!(
15103 "expected '(' after EXCLUDE, got {:?}",
15104 self.peek()
15105 )));
15106 }
15107 self.advance();
15108 let mut elements: Vec<(String, String)> = Vec::new();
15109 loop {
15110 let col = match self.advance() {
15111 Token::Ident(s) | Token::QuotedIdent(s) => s,
15112 other => {
15113 return Err(self.err(alloc::format!(
15114 "expected column name in EXCLUDE, got {other:?}"
15115 )));
15116 }
15117 };
15118 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15119 return Err(self.err(alloc::format!(
15120 "expected WITH after EXCLUDE column, got {:?}",
15121 self.peek()
15122 )));
15123 }
15124 self.advance();
15125 let op = match self.advance() {
15126 Token::InetOverlap => String::from("&&"),
15127 Token::Intersects => String::from("?#"),
15128 Token::IsBelow => String::from("<^"),
15129 Token::IsAbove => String::from(">^"),
15130 Token::PatternLt => String::from("~<~"),
15131 Token::PatternLtEq => String::from("~<=~"),
15132 Token::PatternGt => String::from("~>~"),
15133 Token::PatternGtEq => String::from("~>=~"),
15134 Token::TsMatchOld => String::from("@@@"),
15135 Token::Eq => String::from("="),
15136 Token::JsonContains => String::from("@>"),
15137 Token::JsonContainedBy => String::from("<@"),
15138 Token::OverLeft => String::from("&<"),
15139 Token::OverRight => String::from("&>"),
15140 other => {
15141 return Err(self.err(alloc::format!(
15142 "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15143 )));
15144 }
15145 };
15146 elements.push((col, op));
15147 if matches!(self.peek(), Token::Comma) {
15148 self.advance();
15149 continue;
15150 }
15151 break;
15152 }
15153 if !matches!(self.peek(), Token::RParen) {
15154 return Err(self.err(alloc::format!(
15155 "expected ')' to close EXCLUDE, got {:?}",
15156 self.peek()
15157 )));
15158 }
15159 self.advance();
15160 Ok(crate::ast::TableConstraint::Exclude {
15161 name: None,
15162 method,
15163 elements,
15164 })
15165 }
15166
15167 fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15168 self.advance(); // CHECK
15169 if !matches!(self.peek(), Token::LParen) {
15170 return Err(self.err(alloc::format!(
15171 "expected '(' after CHECK, got {:?}",
15172 self.peek()
15173 )));
15174 }
15175 self.advance();
15176 let expr = self.parse_expr(0)?;
15177 if !matches!(self.peek(), Token::RParen) {
15178 return Err(self.err(alloc::format!(
15179 "expected ')' to close CHECK predicate, got {:?}",
15180 self.peek()
15181 )));
15182 }
15183 self.advance();
15184 // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15185 // are no existing rows for PG to skip, so it rejects the suffix.
15186 Ok(crate::ast::TableConstraint::Check {
15187 name: None,
15188 expr,
15189 not_valid: false,
15190 })
15191 }
15192
15193 /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15194 fn peek_table_level_check_start(&self) -> bool {
15195 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15196 }
15197
15198 /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15199 /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15200 /// on the dedicated FK path (`parse_table_level_fk` consumes its
15201 /// own CONSTRAINT prefix).
15202 fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15203 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15204 return None;
15205 }
15206 // tokens[pos+1] is the constraint name (any ident-like);
15207 // tokens[pos+2] is the kind keyword.
15208 match self.tokens.get(self.pos + 2) {
15209 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15210 Some(NamedTableConstraintKind::Check)
15211 }
15212 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15213 Some(NamedTableConstraintKind::Unique)
15214 }
15215 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15216 Some(NamedTableConstraintKind::PrimaryKey)
15217 }
15218 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15219 Some(NamedTableConstraintKind::Exclude)
15220 }
15221 _ => None,
15222 }
15223 }
15224
15225 fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15226 if !matches!(self.peek(), Token::LParen) {
15227 return Err(self.err(alloc::format!(
15228 "expected '(' after {ctx}, got {:?}",
15229 self.peek()
15230 )));
15231 }
15232 self.advance();
15233 let mut out = Vec::new();
15234 loop {
15235 out.push(self.expect_ident_like()?);
15236 match self.peek() {
15237 Token::Comma => {
15238 self.advance();
15239 }
15240 Token::RParen => {
15241 self.advance();
15242 break;
15243 }
15244 other => {
15245 return Err(self.err(alloc::format!(
15246 "expected ',' or ')' in {ctx} list, got {other:?}"
15247 )));
15248 }
15249 }
15250 }
15251 if out.is_empty() {
15252 return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15253 }
15254 Ok(out)
15255 }
15256
15257 /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15258 /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15259 /// table-level FK; a column def never starts with either keyword
15260 /// (column names are not in this reserved set).
15261 fn peek_constraint_or_fk_start(&self) -> bool {
15262 let is_constraint_kw = matches!(
15263 self.peek(),
15264 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15265 );
15266 let is_foreign_kw = matches!(
15267 self.peek(),
15268 Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15269 );
15270 is_constraint_kw || is_foreign_kw
15271 }
15272
15273 /// v7.6.0 — parse a table-level FK clause:
15274 /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15275 /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15276 fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15277 let mut name: Option<String> = None;
15278 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15279 self.advance();
15280 name = Some(self.expect_ident_like()?);
15281 }
15282 // `FOREIGN`
15283 match self.advance() {
15284 Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15285 other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15286 }
15287 // `KEY`
15288 match self.advance() {
15289 Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15290 other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15291 }
15292 // `(col, col, ...)`
15293 if !matches!(self.peek(), Token::LParen) {
15294 return Err(self.err(format!(
15295 "expected '(' after FOREIGN KEY, got {:?}",
15296 self.peek()
15297 )));
15298 }
15299 self.advance();
15300 let mut columns = Vec::new();
15301 loop {
15302 columns.push(self.expect_ident_like()?);
15303 match self.peek() {
15304 Token::Comma => {
15305 self.advance();
15306 }
15307 Token::RParen => {
15308 self.advance();
15309 break;
15310 }
15311 other => {
15312 return Err(self.err(format!(
15313 "expected ',' or ')' in FK column list, got {other:?}"
15314 )));
15315 }
15316 }
15317 }
15318 if columns.is_empty() {
15319 return Err(self.err("FOREIGN KEY requires at least one column".into()));
15320 }
15321 let (
15322 parent_table,
15323 parent_columns,
15324 on_delete,
15325 on_update,
15326 match_type,
15327 deferrable,
15328 initially_deferred,
15329 ) = self.parse_references_tail(columns.len())?;
15330 Ok(ForeignKeyConstraint {
15331 name,
15332 columns,
15333 parent_table,
15334 parent_columns,
15335 on_delete,
15336 on_update,
15337 match_type,
15338 deferrable,
15339 initially_deferred,
15340 })
15341 }
15342
15343 /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15344 /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15345 /// the local column count, used to default the parent column
15346 /// list when omitted (SQL spec: parent's PK is implied).
15347 fn parse_references_tail(
15348 &mut self,
15349 expected_arity: usize,
15350 ) -> Result<
15351 (
15352 String,
15353 Vec<String>,
15354 FkAction,
15355 FkAction,
15356 crate::ast::MatchType,
15357 // v7.39 (round 288) — deferrable, initially_deferred.
15358 bool,
15359 bool,
15360 ),
15361 ParseError,
15362 > {
15363 match self.advance() {
15364 Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15365 other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15366 }
15367 let parent_table = self.expect_ident_like()?;
15368 let mut parent_columns: Vec<String> = Vec::new();
15369 if matches!(self.peek(), Token::LParen) {
15370 self.advance();
15371 loop {
15372 parent_columns.push(self.expect_ident_like()?);
15373 match self.peek() {
15374 Token::Comma => {
15375 self.advance();
15376 }
15377 Token::RParen => {
15378 self.advance();
15379 break;
15380 }
15381 other => {
15382 return Err(self.err(format!(
15383 "expected ',' or ')' in REFERENCES column list, got {other:?}"
15384 )));
15385 }
15386 }
15387 }
15388 }
15389 if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15390 return Err(self.err(format!(
15391 "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15392 expected_arity,
15393 parent_columns.len()
15394 )));
15395 }
15396 // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15397 // it between the referenced column list and the ON / DEFERRABLE
15398 // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15399 // is skipped when any referencing column is NULL), so SIMPLE —
15400 // the default, and the only spelling pg_dump emits — is accepted
15401 // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15402 // mixed-NULL rule, which is not wired yet; reject them honestly
15403 // rather than silently applying SIMPLE (PG itself errors on
15404 // MATCH PARTIAL as "not yet implemented").
15405 let mut match_type = crate::ast::MatchType::Simple;
15406 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15407 self.advance();
15408 // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15409 // SIMPLE / PARTIAL arrive as bare identifiers.
15410 let kind = match self.advance() {
15411 Token::Full => "FULL".to_string(),
15412 Token::Ident(s) => s.to_uppercase(),
15413 other => {
15414 return Err(self.err(format!(
15415 "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15416 )));
15417 }
15418 };
15419 match kind.as_str() {
15420 "SIMPLE" => {} // Default — match_type stays Simple.
15421 // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15422 // when ALL referencing columns are NULL; a mixed-NULL key errors.
15423 "FULL" => match_type = crate::ast::MatchType::Full,
15424 "PARTIAL" => {
15425 return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15426 }
15427 _ => {
15428 return Err(self.err(format!(
15429 "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15430 )));
15431 }
15432 }
15433 }
15434 // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15435 // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15436 // <action>` / `ON UPDATE <action>` in either order. PG /
15437 // pg_dump emits the timing clause AFTER the ON clauses
15438 // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15439 // but the SQL spec allows either order. We loop over
15440 // every possible trailer and dispatch on the next token,
15441 // stopping when nothing matches. Phase 3.1 changes the
15442 // bare DEFERRABLE form from hard-error to accept-as-
15443 // immediate; SPG is single-writer with no deferred-
15444 // constraint window so the runtime semantics are always
15445 // immediate even when INITIALLY DEFERRED is requested.
15446 // PG's default referential action (no ON DELETE / ON UPDATE
15447 // clause) is NO ACTION, not RESTRICT — the two enforce
15448 // identically in SPG (single-writer, no deferred window; see the
15449 // shared match arm in constraints.rs) but information_schema.
15450 // referential_constraints must report NO ACTION to match PG.
15451 let mut on_delete = FkAction::NoAction;
15452 let mut on_update = FkAction::NoAction;
15453 let mut seen_on_delete = false;
15454 let mut seen_on_update = false;
15455 let mut deferrable = false;
15456 let mut initially_deferred = false;
15457 loop {
15458 // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15459 let before = self.pos;
15460 let (d, idef) = self.consume_deferrable_clauses_timed()?;
15461 if self.pos != before {
15462 deferrable = d;
15463 initially_deferred = idef;
15464 continue;
15465 }
15466 // ON DELETE / ON UPDATE.
15467 if !matches!(self.peek(), Token::On) {
15468 break;
15469 }
15470 self.advance();
15471 let which = self.advance();
15472 let action = self.parse_fk_action()?;
15473 match which {
15474 Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15475 if seen_on_delete {
15476 return Err(self.err("ON DELETE specified twice".into()));
15477 }
15478 seen_on_delete = true;
15479 on_delete = action;
15480 }
15481 Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15482 if seen_on_update {
15483 return Err(self.err("ON UPDATE specified twice".into()));
15484 }
15485 seen_on_update = true;
15486 on_update = action;
15487 }
15488 other => {
15489 return Err(
15490 self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15491 );
15492 }
15493 }
15494 }
15495 Ok((
15496 parent_table,
15497 parent_columns,
15498 on_delete,
15499 on_update,
15500 match_type,
15501 deferrable,
15502 initially_deferred,
15503 ))
15504 }
15505
15506 /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15507 /// NO ACTION`.
15508 fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15509 match self.advance() {
15510 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15511 Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15512 Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15513 Token::Null => Ok(FkAction::SetNull),
15514 Token::Default => Ok(FkAction::SetDefault),
15515 other => Err(self.err(format!(
15516 "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15517 ))),
15518 },
15519 Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15520 Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15521 other => Err(self.err(format!(
15522 "expected ACTION after NO in FK action, got {other:?}"
15523 ))),
15524 },
15525 other => Err(self.err(format!(
15526 "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15527 ))),
15528 }
15529 }
15530
15531 /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15532 /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15533 fn consume_if_not_exists(&mut self) -> bool {
15534 // `IF` arrives as a bare Ident (we don't reserve it because it
15535 // also appears mid-expression in PG, though we don't support
15536 // those forms yet).
15537 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15538 if !looks_like_if {
15539 return false;
15540 }
15541 // Peek one ahead before committing: only consume IF when it's
15542 // actually `IF NOT EXISTS`.
15543 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
15544 return false;
15545 }
15546 if !matches!(
15547 self.tokens.get(self.pos + 2),
15548 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15549 ) {
15550 return false;
15551 }
15552 self.advance(); // IF
15553 self.advance(); // NOT
15554 self.advance(); // EXISTS
15555 true
15556 }
15557
15558 /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
15559 /// Consumes IF EXISTS as a pair; returns false otherwise
15560 /// without consuming any tokens.
15561 /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
15562 /// ENABLE/DISABLE/FORCE/NO FORCE.
15563 fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
15564 for kw in ["row", "level", "security"] {
15565 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
15566 {
15567 return Err(self.err(alloc::format!(
15568 "expected {} in ROW LEVEL SECURITY, got {:?}",
15569 kw.to_ascii_uppercase(),
15570 self.peek()
15571 )));
15572 }
15573 self.advance();
15574 }
15575 Ok(())
15576 }
15577
15578 fn consume_if_exists(&mut self) -> bool {
15579 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15580 if !looks_like_if {
15581 return false;
15582 }
15583 if !matches!(
15584 self.tokens.get(self.pos + 1),
15585 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15586 ) {
15587 return false;
15588 }
15589 self.advance(); // IF
15590 self.advance(); // EXISTS
15591 true
15592 }
15593
15594 /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
15595 /// qualifiers after an index column ref. ASC / DESC are
15596 /// reserved tokens; NULLS / FIRST / LAST are bare idents.
15597 /// We accept and discard them since single-column BTree
15598 /// stores rows in natural key order today.
15599 /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
15600 /// ORDER BY key. Returns None when absent.
15601 fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
15602 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15603 return Ok(None);
15604 }
15605 self.advance();
15606 match self.advance() {
15607 Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
15608 Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
15609 other => Err(self.err(alloc::format!(
15610 "expected FIRST or LAST after NULLS, got {other:?}"
15611 ))),
15612 }
15613 }
15614
15615 /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
15616 /// rather than discarded.
15617 ///
15618 /// SPG's index does not scan in a direction — column ordering is
15619 /// intrinsic to the storage — but `pg_indexes.indexdef` is a
15620 /// reproduction of the DDL, and dropping the clause meant
15621 /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
15622 /// dump lost it, and a schema diff saw drift on every run.
15623 fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
15624 let mut order = crate::ast::IndexColumnOrder::default();
15625 loop {
15626 match self.peek() {
15627 Token::Asc => {
15628 self.advance();
15629 }
15630 Token::Desc => {
15631 order.descending = true;
15632 self.advance();
15633 }
15634 Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
15635 let look = self.tokens.get(self.pos + 1);
15636 if matches!(
15637 look,
15638 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
15639 || k.eq_ignore_ascii_case("last")
15640 ) {
15641 self.advance();
15642 order.nulls_first = Some(matches!(
15643 self.advance(),
15644 Token::Ident(k) if k.eq_ignore_ascii_case("first")
15645 ));
15646 } else {
15647 break;
15648 }
15649 }
15650 _ => break,
15651 }
15652 }
15653 order
15654 }
15655
15656 fn parse_create_index_stmt_after_create(
15657 &mut self,
15658 is_unique: bool,
15659 ) -> Result<Statement, ParseError> {
15660 // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
15661 debug_assert!(matches!(self.peek(), Token::Index));
15662 self.advance();
15663 // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
15664 // SPG's CREATE INDEX is synchronous end-to-end today (real
15665 // CONCURRENTLY variant with restartable scans queues with
15666 // v7.39 indexes epic), so the modifier has no runtime effect
15667 // — same accept-and-no-op shape as v7.37.16.5 DETACH
15668 // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
15669 // VIEW CONCURRENTLY.
15670 let mut concurrently = false;
15671 if matches!(
15672 self.peek(),
15673 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
15674 ) {
15675 self.advance();
15676 concurrently = true;
15677 }
15678 let if_not_exists = self.consume_if_not_exists();
15679 // v7.39 (read01 round 93) — the index name is optional (PG since
15680 // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
15681 // When the token after `[IF NOT EXISTS]` is already `ON`, no name
15682 // was given; leave it empty and the engine derives a PG-style
15683 // `<table>_<cols>_idx` name at CREATE time (with collision counter).
15684 let name = if matches!(self.peek(), Token::On) {
15685 String::new()
15686 } else {
15687 self.expect_ident_like()?
15688 };
15689 if !matches!(self.peek(), Token::On) {
15690 return Err(self.err(format!(
15691 "expected ON after CREATE INDEX <name>, got {:?}",
15692 self.peek()
15693 )));
15694 }
15695 self.advance();
15696 let table = self.expect_ident_like()?;
15697 // Optional `USING <method>` — only recognised method in v2.0 is
15698 // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
15699 // ident `using` (we don't promote it to a reserved keyword
15700 // because it isn't reserved anywhere else in our SQL surface).
15701 let mut method_name: Option<String> = None;
15702 let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15703 self.advance();
15704 let m = self.expect_ident_like()?;
15705 method_name = Some(m.to_ascii_lowercase());
15706 match m.to_ascii_lowercase().as_str() {
15707 "hnsw" => IndexMethod::Hnsw,
15708 "btree" => IndexMethod::BTree,
15709 "brin" => IndexMethod::Brin,
15710 // v7.12.3 — real GIN inverted index over `tsvector`.
15711 // v7.9.26b's `USING gin` → BTree silent fallback is
15712 // gone; the engine validates that the indexed column
15713 // is `tsvector` at CREATE INDEX time.
15714 "gin" => IndexMethod::Gin,
15715 // v7.9.26b — PG `pg_dump` emits `USING gist` /
15716 // `USING spgist` / `USING hash` for their built-in
15717 // AMs that SPG doesn't have a matching
15718 // implementation for; degrade to BTree on the
15719 // leading column so the schema loads + the index
15720 // catalogue stays consistent. Operator pays the
15721 // planner cost only for the queries that would have
15722 // used the specialised AM.
15723 "gist" | "spgist" | "hash" => IndexMethod::BTree,
15724 // v7.11.3 — pgvector ships both `ivfflat` and
15725 // `hnsw`. Customers shouldn't have to choose
15726 // their on-disk index method based on what SPG
15727 // implements; accept `ivfflat` as a synonym for
15728 // `hnsw` so PG schemas using either method drop
15729 // in. The vector distance op (`<->` / `<#>` /
15730 // `<=>`) at query time still picks the metric.
15731 "ivfflat" => IndexMethod::Hnsw,
15732 other => {
15733 return Err(self.err(alloc::format!(
15734 "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
15735 )));
15736 }
15737 }
15738 } else {
15739 IndexMethod::BTree
15740 };
15741 if !matches!(self.peek(), Token::LParen) {
15742 return Err(self.err(format!(
15743 "expected '(' before indexed column, got {:?}",
15744 self.peek()
15745 )));
15746 }
15747 self.advance();
15748 // v6.8.2 — accept either a bare column ident (legacy) or
15749 // an expression `fn(col, …)` for expression indexes.
15750 // Distinguish by peeking the token *after* the current
15751 // ident: `ident )` is the legacy column-only path;
15752 // anything else triggers the Pratt expression parser.
15753 // (`advance()` uses `mem::replace` to nil out the current
15754 // slot, so we can't save+rewind cleanly — peek-ahead via
15755 // direct index avoids the mutation.)
15756 let mut opclass: Option<String> = None;
15757 let mut key_collation: Option<String> = None;
15758 let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
15759 // Single column with `)` immediately after — fast path.
15760 // v7.9.29 — also: bare column followed by `,` (the
15761 // multi-column form `(a, b, c)`). Without this branch
15762 // the leading ident gets pulled into `parse_expr`
15763 // which then sets `expression = Some(Column(a))` and
15764 // breaks Display round-trip on the multi-column shape.
15765 Token::Ident(s) | Token::QuotedIdent(s)
15766 if matches!(
15767 self.tokens.get(self.pos + 1),
15768 Some(Token::RParen | Token::Comma)
15769 ) =>
15770 {
15771 self.advance();
15772 (s, None)
15773 }
15774 // v7.9.22 — single column followed by a pgvector
15775 // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
15776 // v7.15.0 — capture the opclass instead of discarding
15777 // it so the engine can dispatch (e.g. `gin_trgm_ops`
15778 // → real trigram-shingle GIN over a TEXT column).
15779 // Vector/HNSW opclasses still take their distance
15780 // metric from the query operator (`<->` / `<#>` /
15781 // `<=>`), so for those callers the opclass stays
15782 // informational.
15783 // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
15784 // opclass: `(embedding public.vector_cosine_ops)`. Strip
15785 // the schema and dispatch on the bare opclass, the same
15786 // treatment table/type names get.
15787 Token::Ident(s) | Token::QuotedIdent(s)
15788 if matches!(
15789 self.tokens.get(self.pos + 1),
15790 Some(Token::Ident(_) | Token::QuotedIdent(_))
15791 ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
15792 && matches!(
15793 self.tokens.get(self.pos + 3),
15794 Some(Token::Ident(op) | Token::QuotedIdent(op))
15795 if is_vector_opclass_name(op)
15796 ) =>
15797 {
15798 self.advance(); // column name
15799 self.advance(); // schema qualifier
15800 self.advance(); // dot
15801 let op_tok = self.advance();
15802 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15803 opclass = Some(op.to_ascii_lowercase());
15804 }
15805 (s, None)
15806 }
15807 // r1038 — an operator class is recognised by its POSITION, not
15808 // by a list of names. It used to be `is_vector_opclass_name`,
15809 // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
15810 // sentori's migration wrote — was a syntax error while
15811 // `USING gin (doc)` parsed. Anything sitting between a column
15812 // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
15813 // two bare identifiers in a row are not valid there otherwise.
15814 Token::Ident(s) | Token::QuotedIdent(s)
15815 if matches!(
15816 self.tokens.get(self.pos + 1),
15817 Some(Token::Ident(op) | Token::QuotedIdent(op))
15818 if is_vector_opclass_name(op) || Self::opclass_position_follows(
15819 self.tokens.get(self.pos + 2)
15820 )
15821 ) =>
15822 {
15823 self.advance(); // column name
15824 // Capture the opclass token, lower-cased for
15825 // case-insensitive engine dispatch.
15826 let op_tok = self.advance();
15827 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15828 opclass = Some(op.to_ascii_lowercase());
15829 }
15830 (s, None)
15831 }
15832 Token::Ident(_) | Token::QuotedIdent(_) => {
15833 // v7.39 (round 538) — an explicit COLLATE on the key,
15834 // read by LOOKAHEAD because `parse_expr` absorbs the
15835 // clause as a no-op (SPG orders text by bytes, which is
15836 // the C collation, so it changes nothing to honour). PG
15837 // still PRINTS it: an explicitly written `"C"` and the
15838 // collation a column inherits are different collation
15839 // OBJECTS even where they sort identically, which is why
15840 // `(a COLLATE "C")` shows on a C-collation database too.
15841 if matches!(
15842 self.tokens.get(self.pos + 1),
15843 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
15844 ) {
15845 key_collation = match self.tokens.get(self.pos + 2) {
15846 Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
15847 Some(n.clone())
15848 }
15849 _ => None,
15850 };
15851 }
15852 let key_expr = self.parse_expr(0)?;
15853 let primary = extract_first_column(&key_expr).ok_or_else(|| {
15854 self.err("expression index key must reference at least one column".into())
15855 })?;
15856 (primary, Some(key_expr))
15857 }
15858 // v7.37.43-T4 — parenthesised expression index key
15859 // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
15860 // PG's CREATE INDEX requires the expression to be in
15861 // its own parens to disambiguate function calls from
15862 // column lists, so this `LParen` is the inner open-paren
15863 // of an expression key. parse_expr handles the recursive
15864 // descent and consumes the matching `RParen`.
15865 Token::LParen => {
15866 let key_expr = self.parse_expr(0)?;
15867 let primary = extract_first_column(&key_expr).ok_or_else(|| {
15868 self.err("expression index key must reference at least one column".into())
15869 })?;
15870 (primary, Some(key_expr))
15871 }
15872 other => {
15873 return Err(self.err(format!(
15874 "expected column ident or expression, got {other:?}"
15875 )));
15876 }
15877 };
15878 // v7.9.14 — accept extra comma-separated columns inside
15879 // the index key parens (`CREATE INDEX … (a, b, c)`).
15880 // mailrs F2. Each extra column may carry an optional
15881 // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
15882 // — parsed and discarded; SPG doesn't honour direction
15883 // on a BTree index today (column ordering is intrinsic
15884 // to the storage). v7.10 will widen to genuine composite
15885 // index keys.
15886 let mut extra_columns: Vec<String> = Vec::new();
15887 // The leading column may also have ASC/DESC after it — and that
15888 // one is the column SPG indexes, so its clause is kept.
15889 let key_order = self.consume_optional_index_column_qualifiers();
15890 while matches!(self.peek(), Token::Comma) {
15891 self.advance();
15892 let extra = self.expect_ident_like()?;
15893 let _ = self.consume_optional_index_column_qualifiers();
15894 extra_columns.push(extra);
15895 }
15896 if !matches!(self.peek(), Token::RParen) {
15897 return Err(self.err(format!(
15898 "expected ')' after indexed column / expression, got {:?}",
15899 self.peek()
15900 )));
15901 }
15902 self.advance();
15903 // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
15904 // index-only-scan annotation. Bare ident (not a reserved
15905 // keyword) so we test by case-insensitive string match.
15906 let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
15907 {
15908 self.advance();
15909 if !matches!(self.peek(), Token::LParen) {
15910 return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
15911 }
15912 self.advance();
15913 let mut cols = Vec::new();
15914 loop {
15915 cols.push(self.expect_ident_like()?);
15916 match self.peek() {
15917 Token::Comma => {
15918 self.advance();
15919 }
15920 Token::RParen => {
15921 self.advance();
15922 break;
15923 }
15924 other => {
15925 return Err(self.err(format!(
15926 "expected ',' or ')' in INCLUDE list, got {other:?}"
15927 )));
15928 }
15929 }
15930 }
15931 cols
15932 } else {
15933 Vec::new()
15934 };
15935 // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
15936 // storage parameters. pgvector emits `WITH (lists = N)` for
15937 // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
15938 // SPG's HNSW picks its own parameters today (tunable via
15939 // env vars), so the WITH clause is informational and dropped.
15940 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15941 self.advance();
15942 if !matches!(self.peek(), Token::LParen) {
15943 return Err(self.err(format!(
15944 "expected '(' after WITH in CREATE INDEX, got {:?}",
15945 self.peek()
15946 )));
15947 }
15948 self.advance();
15949 loop {
15950 if matches!(self.peek(), Token::RParen) {
15951 self.advance();
15952 break;
15953 }
15954 // Drain `key = value` or bare `key` tokens.
15955 let _ = self.advance(); // key
15956 if matches!(self.peek(), Token::Eq) {
15957 self.advance();
15958 let _ = self.advance(); // value (int / string / ident)
15959 }
15960 match self.peek() {
15961 Token::Comma => {
15962 self.advance();
15963 }
15964 Token::RParen => {
15965 self.advance();
15966 break;
15967 }
15968 other => {
15969 return Err(self.err(format!(
15970 "expected ',' or ')' in WITH (…) clause, got {other:?}"
15971 )));
15972 }
15973 }
15974 }
15975 }
15976 // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
15977 // which sits between the key list and the WHERE clause.
15978 let mut nulls_not_distinct = false;
15979 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15980 let n1 = self.tokens.get(self.pos + 1);
15981 let n2 = self.tokens.get(self.pos + 2);
15982 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
15983 self.advance(); // NULLS
15984 self.advance(); // NOT
15985 self.advance(); // DISTINCT
15986 nulls_not_distinct = true;
15987 } else if matches!(n1, Some(Token::Distinct)) {
15988 self.advance(); // NULLS
15989 self.advance(); // DISTINCT
15990 }
15991 }
15992 // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
15993 let partial_predicate = if matches!(self.peek(), Token::Where) {
15994 self.advance();
15995 Some(self.parse_expr(0)?)
15996 } else {
15997 None
15998 };
15999 // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16000 // sense: uniqueness over an ANN structure has no clean
16001 // semantics. Reject early. (BRIN UNIQUE is similarly
16002 // meaningless — block both.)
16003 if is_unique && !matches!(method, IndexMethod::BTree) {
16004 return Err(self.err(alloc::format!(
16005 "UNIQUE is only supported on BTree indexes, got USING {:?}",
16006 method
16007 )));
16008 }
16009 Ok(Statement::CreateIndex(CreateIndexStatement {
16010 concurrently,
16011 name,
16012 key_order,
16013 key_collation,
16014 table,
16015 column,
16016 nulls_not_distinct,
16017 method,
16018 if_not_exists,
16019 included_columns,
16020 partial_predicate,
16021 extra_columns: extra_columns.clone(),
16022 expression,
16023 is_unique,
16024 opclass,
16025 method_name,
16026 }))
16027 }
16028
16029 /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16030 /// column-level `REFERENCES ...` clause. The trailing FK is
16031 /// normalised into table-level shape (single-element columns +
16032 /// parent_columns) so the engine sees one uniform constraint list.
16033 fn parse_column_def_with_fk(
16034 &mut self,
16035 ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16036 let col = self.parse_column_def()?;
16037 // v7.39 (round 308, V29) — an explicitly named inline FK:
16038 // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16039 // loop leaves this spelling intact precisely so the name can be
16040 // kept here; PG reports it in violation messages and matches it
16041 // in `SET CONSTRAINTS`.
16042 let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16043 {
16044 self.advance();
16045 Some(self.expect_ident_like()?)
16046 } else {
16047 None
16048 };
16049 // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16050 let inline_references = matches!(
16051 self.peek(),
16052 Token::Ident(s) if s.eq_ignore_ascii_case("references")
16053 );
16054 if !inline_references {
16055 return Ok((col, None));
16056 }
16057 let (
16058 parent_table,
16059 parent_columns,
16060 on_delete,
16061 on_update,
16062 match_type,
16063 deferrable,
16064 initially_deferred,
16065 ) = self.parse_references_tail(1)?;
16066 let fk = ForeignKeyConstraint {
16067 name: declared_name,
16068 columns: vec![col.name.clone()],
16069 parent_table,
16070 parent_columns,
16071 on_delete,
16072 on_update,
16073 match_type,
16074 deferrable,
16075 initially_deferred,
16076 };
16077 Ok((col, Some(fk)))
16078 }
16079
16080 /// v7.13.0 — parse a column type (consuming the type ident and
16081 /// any trailing parameters / `[]`), without surrounding column
16082 /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16083 /// Returns the resolved `ColumnTypeName` plus implied
16084 /// `(auto_increment, not_null)` flags from PG SERIAL family
16085 /// shorthands — callers that don't expect those (ALTER COLUMN
16086 /// TYPE) can discard them.
16087 fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16088 let (ty, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16089 Ok(ty)
16090 }
16091
16092 #[allow(clippy::type_complexity)]
16093 fn parse_type_with_implied_flags(
16094 &mut self,
16095 ) -> Result<
16096 (
16097 ColumnTypeName,
16098 bool,
16099 bool,
16100 Option<String>,
16101 Collation,
16102 // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16103 bool,
16104 // v7.39 (round 676) — the collation NAME as written, which the
16105 // `Collation` enum above cannot carry.
16106 Option<String>,
16107 bool,
16108 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16109 // list captured at type-parse time. None for all
16110 // non-ENUM types.
16111 Option<Vec<String>>,
16112 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16113 // list. Distinct from ENUM (subset semantics).
16114 Option<Vec<String>>,
16115 // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16116 // width, lost when the type collapses to SmallInt / Int.
16117 Option<MysqlIntWidth>,
16118 // v7.39 (round 424) — declared fractional-seconds precision of a
16119 // MySQL temporal column (bare spelling = 0). None under PG.
16120 Option<u8>,
16121 ),
16122 ParseError,
16123 > {
16124 let mut ty_ident = match self.advance() {
16125 Token::Ident(s) => s,
16126 // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16127 // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16128 // '<span>'` literal grammar. As a column type it lands
16129 // here directly; downstream resolution still uses the
16130 // canonical lowercase string.
16131 Token::Interval => "interval".to_string(),
16132 other => {
16133 return Err(ParseError {
16134 message: format!("expected column type, got {other:?}"),
16135 token_pos: self.consumed_pos(),
16136 });
16137 }
16138 };
16139 // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16140 // pg_dump qualifies extension types (`public.vector(1024)`).
16141 // SPG is single-namespace; drop the schema and resolve the
16142 // bare type — same treatment table names already get.
16143 while matches!(self.peek(), Token::Dot) {
16144 self.advance();
16145 ty_ident = self.expect_ident_like()?;
16146 }
16147 let mut implied_auto_increment = false;
16148 let mut implied_not_null = false;
16149 let mut user_type_ref: Option<String> = None;
16150 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16151 // value list, captured here and bubbled up through the
16152 // ColumnDef so the engine can attach it to the column
16153 // schema (and validate INSERT cells against it).
16154 let mut inline_enum_variants: Option<Vec<String>> = None;
16155 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16156 let mut inline_set_variants: Option<Vec<String>> = None;
16157 // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16158 // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16159 // collapses to SmallInt / Int. Only under the MySQL dialect.
16160 let mut mysql_int_width: Option<MysqlIntWidth> = None;
16161 // v7.39 (round 424) — the declared fractional-seconds precision of a
16162 // MySQL temporal column. Set by the temporal arms below; stays None
16163 // for PG (whose temporal columns keep full microseconds).
16164 let mut mysql_fsp: Option<u8> = None;
16165 let mut ty = match ty_ident.as_str() {
16166 // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16167 "smallserial" | "serial2" => {
16168 implied_auto_increment = true;
16169 implied_not_null = true;
16170 ColumnTypeName::SmallInt
16171 }
16172 "serial" | "serial4" => {
16173 implied_auto_increment = true;
16174 implied_not_null = true;
16175 ColumnTypeName::Int
16176 }
16177 "bigserial" | "serial8" => {
16178 implied_auto_increment = true;
16179 implied_not_null = true;
16180 ColumnTypeName::BigInt
16181 }
16182 // MySQL flavours we accept by aliasing to the closest SPG
16183 // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16184 // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16185 // 24-bit) → INT. UNSIGNED modifiers are consumed below
16186 // without semantic effect.
16187 // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16188 // PG's internal type names; pg_dump and hand-written PG schemas
16189 // use them interchangeably with smallint / int / bigint (the cast
16190 // path already accepted them, only the column grammar didn't).
16191 "smallint" | "int2" => {
16192 // v7.14.0 — MySQL display-width on integers
16193 // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16194 // parenthesised number is purely cosmetic — it
16195 // doesn't change storage. Accept + discard.
16196 self.consume_optional_paren_size();
16197 ColumnTypeName::SmallInt
16198 }
16199 // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16200 // canonical encoding for BOOLEAN. Every MySQL driver
16201 // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16202 // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16203 // 4.3 SPG classified TINYINT(1) as SmallInt, which
16204 // gave the customer i16-shaped values where the app
16205 // expected bool — a Tier-A silent type drift on
16206 // mysqldump restores. Now: `TINYINT(1)` → Bool;
16207 // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16208 // stay SmallInt (the legacy width-agnostic path).
16209 "tinyint" => {
16210 let width = self.peek_optional_paren_size_value();
16211 self.consume_optional_paren_size();
16212 if width == Some(1) {
16213 ColumnTypeName::Bool
16214 } else {
16215 // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16216 // lost width so the write path can enforce -128..127.
16217 if self.mysql_dialect {
16218 mysql_int_width = Some(MysqlIntWidth::Tiny);
16219 }
16220 ColumnTypeName::SmallInt
16221 }
16222 }
16223 "mediumint" => {
16224 self.consume_optional_paren_size();
16225 // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16226 if self.mysql_dialect {
16227 mysql_int_width = Some(MysqlIntWidth::Medium);
16228 }
16229 ColumnTypeName::Int
16230 }
16231 "int" | "integer" | "int4" => {
16232 self.consume_optional_paren_size();
16233 ColumnTypeName::Int
16234 }
16235 "bigint" | "int8" => {
16236 self.consume_optional_paren_size();
16237 ColumnTypeName::BigInt
16238 }
16239 // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16240 // (mailrs round-5 G6). Consume the optional `PRECISION`
16241 // tail when the type keyword was `double` / `DOUBLE`.
16242 //
16243 // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16244 // FLOAT". `FLOAT(p)` picks the width the way PG does:
16245 // p in 1..=24 is real, 25..=53 is double precision, and
16246 // anything else is an error.
16247 "float" | "double" | "real" => {
16248 if ty_ident.eq_ignore_ascii_case("double")
16249 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16250 {
16251 self.advance();
16252 }
16253 if ty_ident.eq_ignore_ascii_case("real") {
16254 // v7.39 (round 274) — the two dialects genuinely
16255 // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16256 // synonym for DOUBLE (8-byte). Round 269 made REAL
16257 // 32-bit globally and thereby narrowed the stored
16258 // precision of every MySQL REAL column.
16259 if self.mysql_dialect {
16260 ColumnTypeName::Float
16261 } else {
16262 ColumnTypeName::Real
16263 }
16264 } else if ty_ident.eq_ignore_ascii_case("float")
16265 && self.mysql_dialect
16266 && matches!(self.peek(), Token::LParen)
16267 && self.peek_paren_has_comma()
16268 {
16269 // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16270 // display form (`FLOAT(10,2)`), which PG has no
16271 // equivalent of. It was `syntax error at or near ","`,
16272 // so the whole CREATE failed. The digits are a display
16273 // hint only; SPG stores the full double.
16274 self.consume_optional_paren_size();
16275 ColumnTypeName::Float
16276 } else if ty_ident.eq_ignore_ascii_case("float")
16277 && matches!(self.peek(), Token::LParen)
16278 {
16279 // PG words the two bounds differently, and
16280 // parse_paren_size already rejects a zero.
16281 let p = self.parse_paren_size("FLOAT")?;
16282 if p > 53 {
16283 return Err(self.err(String::from(
16284 "precision for type float must be less than 54 bits",
16285 )));
16286 }
16287 if p <= 24 {
16288 ColumnTypeName::Real
16289 } else {
16290 ColumnTypeName::Float
16291 }
16292 } else {
16293 ColumnTypeName::Float
16294 }
16295 }
16296 // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16297 "float4" => ColumnTypeName::Real,
16298 "float8" => ColumnTypeName::Float,
16299 "text" => ColumnTypeName::Text,
16300 // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16301 // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16302 // real MySQL schema and NONE of them existed: the CREATE
16303 // failed outright with `type "blob" does not exist`, so the
16304 // table was never made. The sizes differ only in MySQL's
16305 // maximum length, which SPG does not cap, so they collapse
16306 // onto TEXT and BYTEA the way the unsized spellings do.
16307 "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16308 "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16309 // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16310 // enforce, consumed so the declaration parses.
16311 "varbinary" | "binary" => {
16312 self.consume_optional_paren_size();
16313 ColumnTypeName::Bytes
16314 }
16315 "name" => ColumnTypeName::Name,
16316 "xid" => ColumnTypeName::Xid,
16317 "oid" => ColumnTypeName::Oid,
16318 "xid8" => ColumnTypeName::Xid8,
16319 "bool" | "boolean" => ColumnTypeName::Bool,
16320 // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16321 // an unbounded `character varying`, which the arm below has always
16322 // read as text. Only the short spelling demanded a length, so
16323 // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16324 // there is — failed on `VARCHAR type requires (N)` while the long
16325 // spelling of the same thing was accepted. The same asymmetry
16326 // round 613 closed on the CAST side, here on the DDL side.
16327 "varchar" => {
16328 if matches!(self.peek(), Token::LParen) {
16329 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16330 } else {
16331 ColumnTypeName::Text
16332 }
16333 }
16334 // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16335 // `character` below (SQL standard).
16336 "char" => {
16337 if matches!(self.peek(), Token::LParen) {
16338 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16339 } else {
16340 ColumnTypeName::Char(1)
16341 }
16342 }
16343 // pg_dump's canonical spellings: `character varying(n)` = varchar,
16344 // `character(n)` = char, bare `character` = char(1). Unbounded
16345 // `character varying` maps to text.
16346 "character" => {
16347 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16348 self.advance();
16349 if matches!(self.peek(), Token::LParen) {
16350 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16351 } else {
16352 ColumnTypeName::Text
16353 }
16354 } else if matches!(self.peek(), Token::LParen) {
16355 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16356 } else {
16357 ColumnTypeName::Char(1)
16358 }
16359 }
16360 "vector" => {
16361 let dim = self.parse_paren_size("VECTOR")?;
16362 let encoding = self.parse_optional_vector_encoding()?;
16363 ColumnTypeName::Vector { dim, encoding }
16364 }
16365 // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16366 // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16367 // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16368 // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16369 // DECIMAL(10,2))` — how nearly every money column is written,
16370 // in either dialect — was a syntax error and the table was
16371 // never created. `FIXED` is MySQL's alias alone, so it is
16372 // taken only in that dialect.
16373 "numeric" | "decimal" | "dec" => {
16374 let (precision, scale) = self.parse_optional_numeric_params()?;
16375 ColumnTypeName::Numeric(precision, scale)
16376 }
16377 "fixed" if self.mysql_dialect => {
16378 let (precision, scale) = self.parse_optional_numeric_params()?;
16379 ColumnTypeName::Numeric(precision, scale)
16380 }
16381 "date" => ColumnTypeName::Date,
16382 // MySQL's `DATETIME` is the same domain as standard
16383 // `TIMESTAMP` — accept both spellings.
16384 "timestamp" | "datetime" => {
16385 // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16386 // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16387 // TIME ZONE` clause, so consume it first.
16388 // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16389 // (it truncates on write and pads on render), so capture it;
16390 // a bare spelling means precision 0 there. PG stores µs always
16391 // and keeps `None`.
16392 let n = self.take_optional_paren_size();
16393 if self.mysql_dialect {
16394 mysql_fsp = Some(n.unwrap_or(0).min(6));
16395 }
16396 // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16397 // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16398 // the full form. SPG canonicalises:
16399 // - WITH TIME ZONE → Timestamptz
16400 // - WITHOUT TIME ZONE → Timestamp
16401 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16402 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16403 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16404 {
16405 self.advance(); // WITH
16406 self.advance(); // TIME
16407 self.advance(); // ZONE
16408 ColumnTypeName::Timestamptz
16409 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16410 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16411 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16412 {
16413 self.advance(); // WITHOUT
16414 self.advance(); // TIME
16415 self.advance(); // ZONE
16416 ColumnTypeName::Timestamp
16417 } else {
16418 // A second `(precision)` cannot legally follow, but the
16419 // old grammar tolerated it; keep that tolerance.
16420 self.consume_optional_paren_size();
16421 ColumnTypeName::Timestamp
16422 }
16423 }
16424 // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16425 // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16426 // only PG-wire OID differs.
16427 "timestamptz" => {
16428 self.consume_optional_paren_size();
16429 ColumnTypeName::Timestamptz
16430 }
16431 // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16432 // validation. We accept the JSONB spelling too because
16433 // most PG clients default to it; SPG doesn't distinguish
16434 // the two (no path-operator perf advantage to model).
16435 "json" => ColumnTypeName::Json,
16436 "jsonb" => ColumnTypeName::Jsonb,
16437 // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16438 // surface here. Same storage shape; mapping happens at
16439 // the engine side via the ColumnTypeName → DataType
16440 // resolver. Literal forms are handled at coerce_value
16441 // time so the lexer stays untouched.
16442 "bytea" | "bytes" => ColumnTypeName::Bytes,
16443 // v7.17.0 Phase 7 — PG network address types
16444 // v7.17.0 had a Text-backed fallback here for
16445 // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16446 // each to a first-class type; the keywords are
16447 // bound below in the ζ-A block.
16448 // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16449 // The actual `to_tsvector` / `@@` / `ts_rank` surface
16450 // arrives in v7.12.1+; the type itself loads here so
16451 // mailrs's `scripts/init-schema.sql` runs unmodified.
16452 "tsvector" => ColumnTypeName::TsVector,
16453 "tsquery" => ColumnTypeName::TsQuery,
16454 // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
16455 // surface for Django / Rails / Hibernate's default
16456 // PK pattern.
16457 "uuid" => ColumnTypeName::Uuid,
16458 // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
16459 // Storage = three-field {months, days, micros}, catalog
16460 // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
16461 // line `INTERVAL` was parser-rejected at CREATE TABLE.
16462 "interval" => {
16463 // pg_dump emits field-qualified forms like `INTERVAL DAY TO
16464 // SECOND` and an optional `(p)` precision. SPG stores the full
16465 // {months,days,micros}; consume + ignore the qualifier/precision.
16466 while matches!(self.peek(), Token::To)
16467 || matches!(self.peek(), Token::Ident(s) if matches!(
16468 s.to_ascii_lowercase().as_str(),
16469 "year" | "month" | "day" | "hour" | "minute" | "second"
16470 ))
16471 {
16472 self.advance();
16473 }
16474 self.consume_optional_paren_size();
16475 ColumnTypeName::Interval
16476 }
16477 // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
16478 // i64 microseconds since 00:00:00. Wire OID 1083.
16479 // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
16480 "time" => {
16481 // v7.39 (round 424) — MySQL TIME carries a semantic
16482 // fractional-seconds precision, bare meaning 0.
16483 let n = self.take_optional_paren_size();
16484 if self.mysql_dialect {
16485 mysql_fsp = Some(n.unwrap_or(0).min(6));
16486 }
16487 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16488 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16489 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16490 {
16491 self.advance();
16492 self.advance();
16493 self.advance();
16494 ColumnTypeName::TimeTz
16495 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16496 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16497 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16498 {
16499 self.advance();
16500 self.advance();
16501 self.advance();
16502 ColumnTypeName::Time
16503 } else {
16504 ColumnTypeName::Time
16505 }
16506 }
16507 // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
16508 // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
16509 "year" => ColumnTypeName::Year,
16510 // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
16511 // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
16512 "timetz" => ColumnTypeName::TimeTz,
16513 // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
16514 // Wire OID 790.
16515 "money" => ColumnTypeName::Money,
16516 // v7.17.0 Phase 3.P0-38 — PG range types.
16517 "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
16518 "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
16519 "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
16520 "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
16521 "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
16522 "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
16523 // v7.37.5 δ — PG 14+ multirange keywords.
16524 "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
16525 "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
16526 "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
16527 "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
16528 "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
16529 "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
16530 // v7.37.5 ε — PG geometry scalar keywords.
16531 "point" => ColumnTypeName::Point,
16532 "lseg" => ColumnTypeName::Lseg,
16533 "path" => ColumnTypeName::Path,
16534 "box" => ColumnTypeName::PgBox,
16535 "polygon" => ColumnTypeName::Polygon,
16536 "line" => ColumnTypeName::Line,
16537 "circle" => ColumnTypeName::Circle,
16538 // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
16539 "inet" => ColumnTypeName::Inet,
16540 "cidr" => ColumnTypeName::Cidr,
16541 "macaddr" => ColumnTypeName::Macaddr,
16542 "macaddr8" => ColumnTypeName::Macaddr8,
16543 // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
16544 // width in the value, so the optional `(N)` typmod is accepted and
16545 // ignored (the column stores whatever width it's given).
16546 "bit" => {
16547 let varying = matches!(
16548 self.peek(),
16549 Token::Ident(k) if k.eq_ignore_ascii_case("varying")
16550 );
16551 if varying {
16552 self.advance();
16553 }
16554 // v7.39 (round 281) — the length used to be parsed and
16555 // dropped, so `bit(3)` accepted a five-bit string.
16556 let n = if matches!(self.peek(), Token::LParen) {
16557 self.parse_paren_size("BIT")?
16558 } else {
16559 0
16560 };
16561 if varying {
16562 ColumnTypeName::BitVarying(n)
16563 } else {
16564 ColumnTypeName::Bit(n)
16565 }
16566 }
16567 "varbit" => {
16568 let n = if matches!(self.peek(), Token::LParen) {
16569 self.parse_paren_size("VARBIT")?
16570 } else {
16571 0
16572 };
16573 ColumnTypeName::BitVarying(n)
16574 }
16575 "xml" => ColumnTypeName::Xml,
16576 // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
16577 "hstore" => ColumnTypeName::Hstore,
16578 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
16579 // `ENUM('a','b','c')`. Storage is TEXT; the value
16580 // list lands on `inline_enum_variants` for the
16581 // engine to validate INSERT cells against. Empty
16582 // value list is a parse error (matches MySQL).
16583 "enum" => {
16584 // Expect the opening `(`.
16585 if !matches!(self.peek(), Token::LParen) {
16586 return Err(self.err(alloc::format!(
16587 "expected '(' after ENUM, got {:?}",
16588 self.peek()
16589 )));
16590 }
16591 self.advance();
16592 let mut variants: Vec<String> = Vec::new();
16593 loop {
16594 match self.advance() {
16595 Token::String(s) => variants.push(s),
16596 other => {
16597 return Err(self.err(alloc::format!(
16598 "ENUM(...) expects string literal variants, got {other:?}"
16599 )));
16600 }
16601 }
16602 match self.peek() {
16603 Token::Comma => {
16604 self.advance();
16605 continue;
16606 }
16607 Token::RParen => {
16608 self.advance();
16609 break;
16610 }
16611 other => {
16612 return Err(self.err(alloc::format!(
16613 "expected ',' or ')' in ENUM(...), got {other:?}"
16614 )));
16615 }
16616 }
16617 }
16618 if variants.is_empty() {
16619 return Err(self.err("ENUM(...) must declare at least one variant".into()));
16620 }
16621 inline_enum_variants = Some(variants);
16622 // Storage is plain TEXT; the variant list lives on
16623 // the ColumnSchema side.
16624 ColumnTypeName::Text
16625 }
16626 // v7.17.0 Phase 3.P0-37 — MySQL inline SET
16627 // `SET('a','b','c')`. Same parse shape as ENUM;
16628 // semantics differ (subset rather than pick-one).
16629 "set" => {
16630 if !matches!(self.peek(), Token::LParen) {
16631 return Err(self.err(alloc::format!(
16632 "expected '(' after SET, got {:?}",
16633 self.peek()
16634 )));
16635 }
16636 self.advance();
16637 let mut variants: Vec<String> = Vec::new();
16638 loop {
16639 match self.advance() {
16640 Token::String(s) => variants.push(s),
16641 other => {
16642 return Err(self.err(alloc::format!(
16643 "SET(...) expects string literal variants, got {other:?}"
16644 )));
16645 }
16646 }
16647 match self.peek() {
16648 Token::Comma => {
16649 self.advance();
16650 continue;
16651 }
16652 Token::RParen => {
16653 self.advance();
16654 break;
16655 }
16656 other => {
16657 return Err(self.err(alloc::format!(
16658 "expected ',' or ')' in SET(...), got {other:?}"
16659 )));
16660 }
16661 }
16662 }
16663 if variants.is_empty() {
16664 return Err(self.err("SET(...) must declare at least one variant".into()));
16665 }
16666 inline_set_variants = Some(variants);
16667 ColumnTypeName::Text
16668 }
16669 _other => {
16670 // v7.17.0 Phase 1.4 — unknown ident → defer
16671 // resolution to the engine. Stored as Text in
16672 // ColumnTypeName + the original name carried as
16673 // `user_type_ref` so CREATE TABLE can look up
16674 // user-defined enum / domain types.
16675 user_type_ref = Some(ty_ident.clone());
16676 ColumnTypeName::Text
16677 }
16678 };
16679 // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
16680 // right after the type keyword. Pre-4.4 SPG consumed +
16681 // discarded the keyword, leaving a customer column
16682 // declared `id INT UNSIGNED NOT NULL` silently accepting
16683 // negative values — a Tier-A correctness drift where
16684 // application invariants (auto-increment-IDs never
16685 // negative) silently broke on cutover. Now: capture as
16686 // a column flag, persist on the schema, enforce at
16687 // INSERT / UPDATE time.
16688 let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
16689 {
16690 self.advance();
16691 true
16692 } else {
16693 false
16694 };
16695 // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
16696 // `<type> COLLATE <name>` post-fixes on text columns. SPG
16697 // stores text as UTF-8 always so CHARACTER SET is still a
16698 // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
16699 // name: it gets classified into a `Collation` variant the
16700 // engine consults at WHERE-eval time. PG `default` /
16701 // `pg_catalog.default` / `C` / `POSIX` collations all
16702 // resolve to `Binary` (the prior behaviour); `_ci` /
16703 // `case_insensitive` / `nocase` shift to CaseInsensitive.
16704 // The schema-qualifier form (`pg_catalog.default`) lexes
16705 // as `Ident '.' Ident` — peek for the `.` and consume both
16706 // halves so it's treated as one collation name. PG's
16707 // `IDENT.IDENT` collation form (which can appear here) is
16708 // resolved by Collation::from_collation_name on the bare
16709 // identifier after the dot.
16710 let mut collation = Collation::Binary;
16711 // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
16712 // clause was written. The engine needs this to tell an explicit
16713 // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
16714 // clause at all: both resolve to `Collation::Binary`, but under the
16715 // MySQL dialect the latter takes the folding default collation.
16716 let mut collation_explicit = false;
16717 let mut collation_name: Option<alloc::string::String> = None;
16718 loop {
16719 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
16720 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
16721 {
16722 self.advance(); // CHARACTER
16723 self.advance(); // SET
16724 if matches!(
16725 self.peek(),
16726 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
16727 ) {
16728 self.advance();
16729 }
16730 continue;
16731 }
16732 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
16733 self.advance(); // COLLATE
16734 // Accept Ident / QuotedIdent / String AND the
16735 // keyword-tokenised `Default` (PG `pg_catalog.default`
16736 // and bare `DEFAULT` collation names — `default` is a
16737 // reserved word so the lexer hands back Token::Default
16738 // not Token::Ident).
16739 let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
16740 match this.peek().clone() {
16741 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
16742 this.advance();
16743 Some(s)
16744 }
16745 Token::Default => {
16746 this.advance();
16747 Some(alloc::string::String::from("default"))
16748 }
16749 _ => None,
16750 }
16751 };
16752 let raw = if let Some(head) = read_collation_atom(self) {
16753 // Schema-qualified PG form: `pg_catalog.default`.
16754 if matches!(self.peek(), Token::Dot) {
16755 self.advance();
16756 let tail = read_collation_atom(self).unwrap_or_default();
16757 alloc::format!("{head}.{tail}")
16758 } else {
16759 head
16760 }
16761 } else {
16762 alloc::string::String::new()
16763 };
16764 if !raw.is_empty() {
16765 collation_explicit = true;
16766 // v7.39 (round 676) — keep the name too. The enum below
16767 // folds C / POSIX / en_US / default into one value, and
16768 // `pg_attribute.attcollation` has to tell them apart.
16769 // The schema qualifier goes: PG's `pg_catalog.default`
16770 // and a bare `default` name the same collation.
16771 // v7.39 (round 679) — strip a SCHEMA qualifier, not an
16772 // encoding suffix. Round 676 used `rsplit('.')` for
16773 // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
16774 // PG writes `pg_catalog.default` (qualifier) and
16775 // `en_US.utf8` (locale + encoding) with the same
16776 // separator. Only `pg_catalog.` is a qualifier, and it
16777 // is the only one PG's own dumps emit.
16778 let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
16779 let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
16780 collation_name = Some(alloc::string::String::from(bare));
16781 let parsed = Collation::from_collation_name(&raw);
16782 // Last COLLATE clause wins, but `Binary` from a
16783 // bare keyword like `default` should not
16784 // silently downgrade a stronger one set earlier
16785 // on the same column. v7.17 only ships one
16786 // non-Binary variant so a simple OR is enough.
16787 if parsed != Collation::Binary {
16788 collation = parsed;
16789 }
16790 }
16791 continue;
16792 }
16793 break;
16794 }
16795 // v7.10.10 — postfix `[]` widens the base type to its array
16796 // type. PG accepts `TYPE[]` after any base type and so does
16797 // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
16798 // all through; the old "only TEXT[]" note was stale).
16799 if matches!(self.peek(), Token::LBracket) {
16800 self.advance();
16801 if !matches!(self.peek(), Token::RBracket) {
16802 return Err(self.err(alloc::format!(
16803 "TEXT[] takes no dimension; got {:?}",
16804 self.peek()
16805 )));
16806 }
16807 self.advance();
16808 // v7.11.13 — widened to INT[] and BIGINT[] in addition
16809 // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
16810 // still error here.
16811 ty = match ty {
16812 ColumnTypeName::Text => ColumnTypeName::TextArray,
16813 ColumnTypeName::Int => ColumnTypeName::IntArray,
16814 ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
16815 // v7.37.5 β-P4 — INTERVAL[] via the same postfix
16816 // `[]` grammar. Wire OID 1187.
16817 ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
16818 // v7.37.5 γ — full PG array-of-scalar family.
16819 ColumnTypeName::Bool => ColumnTypeName::BoolArray,
16820 ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
16821 ColumnTypeName::Float => ColumnTypeName::FloatArray,
16822 // NUMERIC(p, s) loses its precision params at the
16823 // array level (matches PG: `NUMERIC[]` is untyped,
16824 // per-element precision flows through values).
16825 ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
16826 ColumnTypeName::Date => ColumnTypeName::DateArray,
16827 ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
16828 ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
16829 ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
16830 ColumnTypeName::Json => ColumnTypeName::JsonArray,
16831 ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
16832 ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
16833 // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
16834 // the array level (matches PG semantics where the
16835 // element precision is per-row, not column-wide).
16836 ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
16837 ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
16838 // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
16839 // follow-up.
16840 ColumnTypeName::Money => ColumnTypeName::MoneyArray,
16841 other => {
16842 return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
16843 }
16844 };
16845 // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
16846 // for INT/TEXT/BIGINT. Anything else is an error.
16847 if matches!(self.peek(), Token::LBracket) {
16848 self.advance();
16849 if !matches!(self.peek(), Token::RBracket) {
16850 return Err(self.err(alloc::format!(
16851 "TYPE[][] second dimension takes no size; got {:?}",
16852 self.peek()
16853 )));
16854 }
16855 self.advance();
16856 ty = match ty {
16857 ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
16858 ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
16859 ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
16860 // v7.39 (read01 round 75) — bool[][].
16861 ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
16862 other => {
16863 return Err(self.err(alloc::format!(
16864 "v7.17 2D arrays support INT[][] / BIGINT[][] / \
16865 TEXT[][] only; got {other:?}"
16866 )));
16867 }
16868 };
16869 }
16870 }
16871 Ok((
16872 ty,
16873 implied_auto_increment,
16874 implied_not_null,
16875 user_type_ref,
16876 collation,
16877 collation_explicit,
16878 collation_name,
16879 is_unsigned,
16880 inline_enum_variants,
16881 inline_set_variants,
16882 mysql_int_width,
16883 mysql_fsp,
16884 ))
16885 }
16886
16887 fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
16888 // v7.20 — PG reserves the table-constraint keywords, so a
16889 // BARE `UNIQUE` / `PRIMARY` / … in column position is a
16890 // malformed constraint clause (e.g. `UNIQUE a` missing its
16891 // parens), not a column named "unique". Since v7.17's
16892 // unknown-type leniency (`user_type_ref`) such a clause
16893 // would otherwise parse as a column with a user-defined
16894 // type — silently accepting invalid DDL. Quoted
16895 // identifiers ("unique" / `unique`) remain valid names.
16896 if let Token::Ident(s) = self.peek()
16897 && [
16898 "unique",
16899 "primary",
16900 "foreign",
16901 "constraint",
16902 "check",
16903 "references",
16904 "exclude",
16905 ]
16906 .iter()
16907 .any(|kw| s.eq_ignore_ascii_case(kw))
16908 {
16909 return Err(self.err(alloc::format!(
16910 "unexpected reserved keyword '{s}' at start of column definition \
16911 (malformed table constraint?)"
16912 )));
16913 }
16914 let name = self.expect_ident_like()?;
16915 let (
16916 ty,
16917 implied_auto_increment,
16918 implied_not_null,
16919 user_type_ref,
16920 collation,
16921 collation_explicit,
16922 collation_name,
16923 is_unsigned,
16924 inline_enum_variants,
16925 inline_set_variants,
16926 mysql_int_width,
16927 mysql_fsp,
16928 ) = self.parse_type_with_implied_flags()?;
16929 // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
16930 // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
16931 // each at most once.
16932 let mut default: Option<Expr> = None;
16933 let mut nullable = !implied_not_null;
16934 let mut nullability_seen = implied_not_null;
16935 let mut auto_increment = implied_auto_increment;
16936 let mut is_primary_key = false;
16937 let mut is_unique = false;
16938 let mut unique_nulls_not_distinct = false;
16939 let mut constraint_deferrable = false;
16940 let mut constraint_initially_deferred = false;
16941 let mut check: Option<Expr> = None;
16942 let mut on_update_runtime: Option<Expr> = None;
16943 let mut generated_stored_expr: Option<Box<Expr>> = None;
16944 let mut identity_always = false;
16945 loop {
16946 // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
16947 // not-null constraints by name and pg_dump emits them
16948 // inline: `id bigint CONSTRAINT contacts_id_not_null1
16949 // NOT NULL`. Accept and discard the name; whatever
16950 // constraint follows is parsed by the arms below.
16951 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16952 // v7.39 (round 308, V29) — a name on an inline
16953 // REFERENCES belongs to the FOREIGN KEY, and the caller
16954 // (`parse_column_def_with_fk`) is what builds it, so
16955 // leave the whole clause for it. Dropping the name here
16956 // is what made `CONSTRAINT fk_a REFERENCES …` come back
16957 // as the synthesised `c_pid_fkey` — which then could
16958 // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
16959 // `advance()` takes tokens by `mem::replace`, so there
16960 // is no rewinding once consumed.
16961 if matches!(
16962 self.tokens.get(self.pos + 2),
16963 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
16964 ) {
16965 break;
16966 }
16967 self.advance();
16968 let _name = self.expect_ident_like()?;
16969 continue;
16970 }
16971 // v7.39 (round 379) — MySQL's SHORT generated-column form
16972 // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
16973 // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
16974 // below), but hand-written schemas and app migrations use this.
16975 // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
16976 // SPG computes-and-stores either way, like the long form.
16977 if matches!(self.peek(), Token::As) {
16978 self.advance();
16979 if !matches!(self.peek(), Token::LParen) {
16980 return Err(self.err(alloc::format!(
16981 "expected '(' after AS in a generated column, got {:?}",
16982 self.peek()
16983 )));
16984 }
16985 self.advance();
16986 let expr = self.parse_expr(0)?;
16987 if !matches!(self.peek(), Token::RParen) {
16988 return Err(self.err(alloc::format!(
16989 "expected ')' after AS (<expr>), got {:?}",
16990 self.peek()
16991 )));
16992 }
16993 self.advance();
16994 if matches!(self.peek(), Token::Ident(s)
16995 if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
16996 {
16997 self.advance();
16998 }
16999 generated_stored_expr = Some(alloc::boxed::Box::new(expr));
17000 continue;
17001 }
17002 // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
17003 // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
17004 // the modern replacement for SERIAL in hand-written
17005 // schemas). Both flavours map onto the auto-increment
17006 // machinery — SPG's serial semantics ≈ BY DEFAULT;
17007 // ALWAYS's reject-explicit-values nuance is documented
17008 // leniency. Generated EXPRESSION columns
17009 // (`AS (expr) STORED`) are not supported: error loudly
17010 // instead of silently storing NULLs.
17011 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
17012 self.advance();
17013 let mut saw_generated_always = false;
17014 match self.peek().clone() {
17015 Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
17016 self.advance();
17017 saw_generated_always = true;
17018 }
17019 Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
17020 self.advance();
17021 if !matches!(self.peek(), Token::Default) {
17022 return Err(self.err(alloc::format!(
17023 "expected DEFAULT after GENERATED BY, got {:?}",
17024 self.peek()
17025 )));
17026 }
17027 self.advance();
17028 }
17029 other => {
17030 return Err(self.err(alloc::format!(
17031 "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
17032 )));
17033 }
17034 }
17035 if !matches!(self.peek(), Token::As) {
17036 return Err(self.err(alloc::format!(
17037 "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
17038 self.peek()
17039 )));
17040 }
17041 self.advance();
17042 // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
17043 // ( <expr> ) STORED` stored computed-column. The
17044 // expression is captured for the engine to recompute
17045 // on every INSERT / UPDATE. v7.37.7 accepts the
17046 // STORED keyword only; PG also has VIRTUAL, which
17047 // v7.37.7 carves out (sentori only uses STORED).
17048 if matches!(self.peek(), Token::LParen) {
17049 self.advance();
17050 let expr = self.parse_expr(0)?;
17051 if !matches!(self.peek(), Token::RParen) {
17052 return Err(self.err(alloc::format!(
17053 "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
17054 self.peek()
17055 )));
17056 }
17057 self.advance();
17058 let stored = match self.peek() {
17059 Token::Ident(s) | Token::QuotedIdent(s)
17060 if s.eq_ignore_ascii_case("stored") =>
17061 {
17062 self.advance();
17063 true
17064 }
17065 // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
17066 // generated columns. SPG computes them on write and
17067 // persists like STORED; the two are observably
17068 // identical for query results (the value, recompute
17069 // on base-column change, and NOT NULL enforcement all
17070 // match), so a PG 18 schema/dump using VIRTUAL loads
17071 // and behaves correctly. The compute-on-read storage
17072 // saving is an invisible internal difference.
17073 Token::Ident(s) | Token::QuotedIdent(s)
17074 if s.eq_ignore_ascii_case("virtual") =>
17075 {
17076 self.advance();
17077 false
17078 }
17079 other => {
17080 return Err(self.err(alloc::format!(
17081 "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
17082 got {other:?}"
17083 )));
17084 }
17085 };
17086 let _ = stored; // STORED / VIRTUAL both compute-and-store.
17087 generated_stored_expr = Some(Box::new(expr));
17088 continue;
17089 }
17090 self.expect_keyword_ident("identity")?;
17091 // Optional `(START WITH 1 INCREMENT BY 1 …)` —
17092 // consume the balanced parens and discard (SPG's
17093 // auto-increment is max+1-scan based).
17094 if matches!(self.peek(), Token::LParen) {
17095 let mut depth = 0usize;
17096 loop {
17097 match self.advance() {
17098 Token::LParen => depth += 1,
17099 Token::RParen => {
17100 depth -= 1;
17101 if depth == 0 {
17102 break;
17103 }
17104 }
17105 Token::Eof => {
17106 return Err(self.err(
17107 "unterminated sequence-options parens after IDENTITY".into(),
17108 ));
17109 }
17110 _ => {}
17111 }
17112 }
17113 }
17114 auto_increment = true;
17115 // v7.38 (read01) — remember the ALWAYS flavour so the engine
17116 // can reject explicit non-DEFAULT INSERT values (unless
17117 // OVERRIDING SYSTEM VALUE) the way PG does.
17118 identity_always = saw_generated_always;
17119 // PG identity columns are implicitly NOT NULL.
17120 nullable = false;
17121 continue;
17122 }
17123 // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
17124 // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
17125 // is accepted today. The "ON" token is an Ident
17126 // (not reserved) — peek before consuming.
17127 if matches!(self.peek(), Token::On)
17128 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17129 {
17130 self.advance(); // ON
17131 self.advance(); // update
17132 // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17133 let next = self.peek().clone();
17134 match next {
17135 Token::Ident(s) | Token::QuotedIdent(s)
17136 if s.eq_ignore_ascii_case("current_timestamp") =>
17137 {
17138 self.advance();
17139 // Optional `(N)` precision.
17140 if matches!(self.peek(), Token::LParen) {
17141 self.advance();
17142 if !matches!(self.peek(), Token::Integer(_)) {
17143 return Err(self.err(alloc::format!(
17144 "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17145 self.peek()
17146 )));
17147 }
17148 self.advance();
17149 if !matches!(self.peek(), Token::RParen) {
17150 return Err(self.err(alloc::format!(
17151 "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17152 self.peek()
17153 )));
17154 }
17155 self.advance();
17156 }
17157 on_update_runtime = Some(Expr::FunctionCall {
17158 name: "now".into(),
17159 args: Vec::new(),
17160 });
17161 continue;
17162 }
17163 other => {
17164 return Err(self.err(alloc::format!(
17165 "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17166 )));
17167 }
17168 }
17169 }
17170 if matches!(self.peek(), Token::Default) {
17171 if default.is_some() {
17172 return Err(self.err("DEFAULT specified twice".into()));
17173 }
17174 self.advance();
17175 default = Some(self.parse_expr(0)?);
17176 continue;
17177 }
17178 // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17179 // token with NOT NULL and sits EARLIER in the loop than the
17180 // deferrability arm, so without the lookahead it was reported as
17181 // "NOT NULL specified twice" (or "expected NULL after NOT").
17182 if matches!(self.peek(), Token::Not)
17183 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17184 {
17185 // NOT DEFERRABLE — explicit immediate; nothing to carry.
17186 self.consume_optional_deferrable_clauses()?;
17187 continue;
17188 }
17189 if matches!(self.peek(), Token::Not) {
17190 if nullability_seen {
17191 return Err(self.err("NOT NULL specified twice".into()));
17192 }
17193 self.advance();
17194 if !matches!(self.peek(), Token::Null) {
17195 return Err(self.err(format!(
17196 "expected NULL after NOT in column def, got {:?}",
17197 self.peek()
17198 )));
17199 }
17200 self.advance();
17201 nullable = false;
17202 nullability_seen = true;
17203 continue;
17204 }
17205 // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17206 // "this column is nullable" marker (the default in
17207 // standard SQL anyway). mysqldump emits it routinely
17208 // (`col TYPE NULL DEFAULT NULL` for nullable
17209 // timestamps etc). Accept + no-op.
17210 if matches!(self.peek(), Token::Null) {
17211 if nullability_seen && !nullable {
17212 // v7.39 (round 761, F31 tranche 2 #31) — PG's
17213 // sentence, PG18-measured (the table name is the
17214 // caller's; the column half is exact).
17215 return Err(self.err(alloc::format!(
17216 "conflicting NULL/NOT NULL declarations for column \"{name}\""
17217 )));
17218 }
17219 self.advance();
17220 nullable = true;
17221 nullability_seen = true;
17222 continue;
17223 }
17224 // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17225 // arrives as a bare Ident. Match either, case-insensitive.
17226 if let Token::Ident(s) = self.peek()
17227 && (s.eq_ignore_ascii_case("auto_increment")
17228 || s.eq_ignore_ascii_case("autoincrement"))
17229 {
17230 if auto_increment {
17231 return Err(self.err("AUTO_INCREMENT specified twice".into()));
17232 }
17233 self.advance();
17234 auto_increment = true;
17235 continue;
17236 }
17237 // v7.9.13 — inline `PRIMARY KEY` column constraint
17238 // (mailrs F1). Implies `NOT NULL`. The engine creates
17239 // a BTree index for the PK column at CREATE TABLE time
17240 // so FK parent-side index lookups resolve.
17241 // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17242 // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17243 // spelling was a parse error, so a pg_dump carrying one stopped
17244 // mid-restore. The clauses are consumed by the same helper the FK
17245 // path has used since round 288 and recorded nowhere: SPG enforces
17246 // the constraint IMMEDIATELY either way, which fails earlier than
17247 // PG inside a transaction that violates-then-repairs — a refusal,
17248 // not a wrong answer. True deferral is the open remainder of F08.
17249 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17250 || (matches!(self.peek(), Token::Not)
17251 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
17252 {
17253 // v7.39 (round 711) — CARRIED now (the storing half of
17254 // F08); round 621 only consumed.
17255 let (d, idef) = self.consume_deferrable_clauses_timed()?;
17256 constraint_deferrable |= d;
17257 constraint_initially_deferred |= idef;
17258 continue;
17259 }
17260 if let Token::Ident(s) = self.peek()
17261 && s.eq_ignore_ascii_case("primary")
17262 {
17263 if is_primary_key {
17264 return Err(self.err("PRIMARY KEY specified twice".into()));
17265 }
17266 // Peek-ahead for the required `KEY` token.
17267 let next = self.tokens.get(self.pos + 1);
17268 let next_is_key = matches!(
17269 next,
17270 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17271 );
17272 if !next_is_key {
17273 return Err(self.err(format!(
17274 "expected KEY after PRIMARY in column def, got {:?}",
17275 next
17276 )));
17277 }
17278 self.advance(); // PRIMARY
17279 self.advance(); // KEY
17280 is_primary_key = true;
17281 if nullability_seen && nullable {
17282 return Err(self.err(
17283 "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17284 ));
17285 }
17286 nullable = false;
17287 nullability_seen = true;
17288 continue;
17289 }
17290 // v7.13.0 — inline `UNIQUE` column constraint
17291 // (mailrs round-5 G2). Fold into a single-column
17292 // table-level UNIQUE at CREATE TABLE post-process time.
17293 if let Token::Ident(s) = self.peek()
17294 && s.eq_ignore_ascii_case("unique")
17295 {
17296 if is_unique {
17297 return Err(self.err("UNIQUE specified twice".into()));
17298 }
17299 self.advance();
17300 is_unique = true;
17301 // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17302 // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17303 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17304 let n1 = self.tokens.get(self.pos + 1);
17305 let n2 = self.tokens.get(self.pos + 2);
17306 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17307 self.advance(); // NULLS
17308 self.advance(); // NOT
17309 self.advance(); // DISTINCT
17310 unique_nulls_not_distinct = true;
17311 } else if matches!(n1, Some(Token::Distinct)) {
17312 self.advance(); // NULLS
17313 self.advance(); // DISTINCT
17314 }
17315 }
17316 continue;
17317 }
17318 // v7.13.0 — inline `CHECK (<expr>)` column constraint
17319 // (mailrs round-5 G3). PG semantics: column-level
17320 // CHECK is equivalent to a table-level CHECK. Multiple
17321 // inline CHECKs on the same column AND together.
17322 if let Token::Ident(s) = self.peek()
17323 && s.eq_ignore_ascii_case("check")
17324 {
17325 self.advance();
17326 if !matches!(self.peek(), Token::LParen) {
17327 return Err(self.err(alloc::format!(
17328 "expected '(' after CHECK in column def, got {:?}",
17329 self.peek()
17330 )));
17331 }
17332 self.advance();
17333 let pred = self.parse_expr(0)?;
17334 if !matches!(self.peek(), Token::RParen) {
17335 return Err(self.err(alloc::format!(
17336 "expected ')' to close CHECK predicate, got {:?}",
17337 self.peek()
17338 )));
17339 }
17340 self.advance();
17341 check = Some(match check.take() {
17342 Some(prev) => Expr::Binary {
17343 op: BinOp::And,
17344 lhs: Box::new(prev),
17345 rhs: Box::new(pred),
17346 },
17347 None => pred,
17348 });
17349 continue;
17350 }
17351 break;
17352 }
17353 Ok(ColumnDef {
17354 name,
17355 ty,
17356 nullable,
17357 default,
17358 auto_increment,
17359 is_primary_key,
17360 is_unique,
17361 unique_nulls_not_distinct,
17362 constraint_deferrable,
17363 constraint_initially_deferred,
17364 check,
17365 user_type_ref,
17366 on_update_runtime,
17367 collation,
17368 collation_explicit,
17369 collation_name,
17370 is_unsigned,
17371 inline_enum_variants,
17372 inline_set_variants,
17373 generated_stored_expr,
17374 identity_always,
17375 mysql_int_width,
17376 mysql_fsp,
17377 })
17378 }
17379
17380 /// `NUMERIC` may appear without parameters, with one (precision
17381 /// only, scale=0), or with both. Returns `(precision, scale)` with
17382 /// 0 = unspecified for the bare form.
17383 fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17384 if !matches!(self.peek(), Token::LParen) {
17385 // Bare `NUMERIC` — PG treats this as "unlimited precision";
17386 // we surface it as precision=0 to mean "unconstrained" so
17387 // the engine doesn't need a separate variant.
17388 return Ok((0, 0));
17389 }
17390 self.advance();
17391 // v7.39 (round 272) — PG's declared precision runs to 1000, and
17392 // it words the out-of-range case with the value it saw. SPG
17393 // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17394 // accepts failed to parse at all; values wider than i128 are
17395 // carried by the arbitrary-precision form.
17396 let precision = match self.advance() {
17397 Token::Integer(n) if (1..=1000).contains(&n) => {
17398 u16::try_from(n).expect("range-checked")
17399 }
17400 Token::Integer(n) => {
17401 return Err(ParseError {
17402 message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17403 token_pos: self.consumed_pos(),
17404 });
17405 }
17406 other => {
17407 return Err(ParseError {
17408 message: format!(
17409 "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17410 ),
17411 token_pos: self.consumed_pos(),
17412 });
17413 }
17414 };
17415 // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17416 // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17417 // then overflows). A negative scale rounds to tens / hundreds / …
17418 let scale = if matches!(self.peek(), Token::Comma) {
17419 self.advance();
17420 let neg = if matches!(self.peek(), Token::Minus) {
17421 self.advance();
17422 true
17423 } else {
17424 false
17425 };
17426 match self.advance() {
17427 Token::Integer(n) => {
17428 let signed = if neg { -n } else { n };
17429 if !(-1000..=1000).contains(&signed) {
17430 return Err(ParseError {
17431 message: format!(
17432 "NUMERIC scale {signed} must be between -1000 and 1000"
17433 ),
17434 token_pos: self.consumed_pos(),
17435 });
17436 }
17437 i16::try_from(signed).expect("range-checked")
17438 }
17439 other => {
17440 return Err(ParseError {
17441 message: format!("NUMERIC scale must be an integer, got {other:?}"),
17442 token_pos: self.consumed_pos(),
17443 });
17444 }
17445 }
17446 } else {
17447 0
17448 };
17449 if !matches!(self.peek(), Token::RParen) {
17450 return Err(self.err(format!(
17451 "expected ')' to close NUMERIC params, got {:?}",
17452 self.peek()
17453 )));
17454 }
17455 self.advance();
17456 Ok((precision, scale))
17457 }
17458
17459 /// Parse `(N)` where `N` is a positive integer literal — used by the
17460 /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
17461 /// for the error message.
17462 /// v6.0.1: parse the optional `USING <encoding>` clause that
17463 /// follows `VECTOR(N)` in a column definition. Missing clause
17464 /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
17465 /// ident → `ParseError` listing the encodings recognised today.
17466 fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
17467 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
17468 return Ok(VecEncoding::F32);
17469 }
17470 // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
17471 // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
17472 // consume the token when the very next token is a known
17473 // vector-encoding keyword (SQ8 / HALF). Otherwise leave
17474 // `USING` for the caller — it's the rewrite-expression form.
17475 let n1 = self.tokens.get(self.pos + 1);
17476 let next_is_encoding = matches!(
17477 n1,
17478 Some(Token::Ident(s))
17479 if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
17480 );
17481 if !next_is_encoding {
17482 return Ok(VecEncoding::F32);
17483 }
17484 self.advance();
17485 let enc_ident = match self.advance() {
17486 Token::Ident(s) => s,
17487 other => {
17488 return Err(self.err(format!(
17489 "expected vector encoding after USING, got {other:?}"
17490 )));
17491 }
17492 };
17493 match enc_ident.to_ascii_lowercase().as_str() {
17494 "sq8" => Ok(VecEncoding::Sq8),
17495 // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
17496 // binary16 per-element storage.
17497 "half" => Ok(VecEncoding::F16),
17498 other => Err(self.err(format!(
17499 "unknown vector encoding {other:?}; supported: SQ8, HALF"
17500 ))),
17501 }
17502 }
17503
17504 /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
17505 /// without consuming it. Returns `Some(N)` when the next
17506 /// tokens are `( <int> )`; None otherwise. Used by the
17507 /// TINYINT classifier to decide whether to map to Bool or
17508 /// SmallInt.
17509 fn peek_optional_paren_size_value(&self) -> Option<i64> {
17510 if !matches!(self.peek(), Token::LParen) {
17511 return None;
17512 }
17513 let next = self.tokens.get(self.pos + 1)?;
17514 let n = match next {
17515 Token::Integer(n) => *n,
17516 _ => return None,
17517 };
17518 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17519 return None;
17520 }
17521 Some(n)
17522 }
17523
17524 /// v7.14.0 — consume an optional MySQL display-width
17525 /// parenthesised number after an integer type, returning
17526 /// nothing. `TINYINT(1)` etc.
17527 /// v7.39 (round 360) — does the parenthesised group ahead contain a
17528 /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
17529 fn peek_paren_has_comma(&self) -> bool {
17530 let mut i = self.pos + 1;
17531 let mut depth = 1usize;
17532 while depth > 0 {
17533 match self.tokens.get(i) {
17534 Some(Token::LParen) => depth += 1,
17535 Some(Token::RParen) => depth -= 1,
17536 Some(Token::Comma) if depth == 1 => return true,
17537 None | Some(Token::Eof) => return false,
17538 _ => {}
17539 }
17540 i += 1;
17541 }
17542 false
17543 }
17544
17545 /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
17546 /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
17547 /// fractional-seconds precision that drives write truncation and render
17548 /// padding, where `consume_optional_paren_size` throws it away.
17549 /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
17550 fn take_optional_paren_size(&mut self) -> Option<u8> {
17551 let Some(Token::Integer(n)) = self
17552 .tokens
17553 .get(self.pos + 1)
17554 .filter(|_| matches!(self.peek(), Token::LParen))
17555 .cloned()
17556 else {
17557 self.consume_optional_paren_size();
17558 return None;
17559 };
17560 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17561 self.consume_optional_paren_size();
17562 return None;
17563 }
17564 self.consume_optional_paren_size();
17565 u8::try_from(n).ok()
17566 }
17567
17568 fn consume_optional_paren_size(&mut self) {
17569 if !matches!(self.peek(), Token::LParen) {
17570 return;
17571 }
17572 self.advance();
17573 // Skip until matching RParen (allow nested or any tokens).
17574 let mut depth = 1usize;
17575 while depth > 0 {
17576 match self.peek() {
17577 Token::LParen => depth += 1,
17578 Token::RParen => depth -= 1,
17579 Token::Eof => return,
17580 _ => {}
17581 }
17582 self.advance();
17583 }
17584 }
17585
17586 fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
17587 if !matches!(self.peek(), Token::LParen) {
17588 return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
17589 }
17590 self.advance();
17591 let n = match self.advance() {
17592 Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
17593 message: format!("{label} size too large: {n}"),
17594 token_pos: self.consumed_pos(),
17595 })?,
17596 other => {
17597 return Err(ParseError {
17598 message: format!("expected positive integer {label} size, got {other:?}"),
17599 token_pos: self.consumed_pos(),
17600 });
17601 }
17602 };
17603 if !matches!(self.peek(), Token::RParen) {
17604 return Err(self.err(format!(
17605 "expected ')' after {label} size, got {:?}",
17606 self.peek()
17607 )));
17608 }
17609 self.advance();
17610 Ok(n)
17611 }
17612
17613 /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
17614 /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
17615 /// key, like MySQL) whose action skips conflicting rows.
17616 /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
17617 /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
17618 /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
17619 /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
17620 /// common bulk-upsert spellings —
17621 /// INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
17622 /// REPLACE INTO t SELECT …
17623 /// — were a parse error / a duplicate-key failure respectively.
17624 ///
17625 /// Precedence: an explicitly written clause beats a statement-level flag.
17626 /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
17627 /// implicit `REPLACE` and `IGNORE` lowerings.
17628 fn parse_insert_conflict_clause(
17629 &mut self,
17630 replace: bool,
17631 ignore: bool,
17632 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17633 if let Some(c) = self.parse_optional_on_duplicate_key()? {
17634 return Ok(Some(c));
17635 }
17636 if let Some(c) = self.parse_optional_on_conflict()? {
17637 return Ok(Some(c));
17638 }
17639 if replace {
17640 // REPLACE INTO = delete-then-insert, which PG spells as
17641 // `ON CONFLICT DO UPDATE SET` over every column; the engine
17642 // reads an empty assignment list as "take the incoming row".
17643 return Ok(Some(crate::ast::OnConflictClause {
17644 target_columns: Vec::new(),
17645 index_where: None,
17646 constraint_name: None,
17647 mysql_lowered: true,
17648 action: crate::ast::OnConflictAction::Update {
17649 assignments: Vec::new(),
17650 where_: None,
17651 },
17652 }));
17653 }
17654 if ignore {
17655 return Ok(Some(Self::insert_ignore_clause()));
17656 }
17657 Ok(None)
17658 }
17659
17660 /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
17661 /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
17662 /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
17663 /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
17664 fn parse_optional_on_duplicate_key(
17665 &mut self,
17666 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17667 if !(matches!(self.peek(), Token::On)
17668 && matches!(self.tokens.get(self.pos + 1),
17669 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
17670 {
17671 return Ok(None);
17672 }
17673 self.advance(); // ON
17674 self.advance(); // DUPLICATE
17675 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
17676 return Err(self.err(format!(
17677 "expected KEY after ON DUPLICATE, got {:?}",
17678 self.peek()
17679 )));
17680 }
17681 self.advance();
17682 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
17683 return Err(self.err(format!(
17684 "expected UPDATE after ON DUPLICATE KEY, got {:?}",
17685 self.peek()
17686 )));
17687 }
17688 self.advance();
17689 let mut assignments: Vec<(String, Expr)> = Vec::new();
17690 loop {
17691 let col = self.expect_ident_like()?;
17692 if !matches!(self.peek(), Token::Eq) {
17693 return Err(self.err(format!(
17694 "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
17695 self.peek()
17696 )));
17697 }
17698 self.advance();
17699 let mut expr = self.parse_expr(0)?;
17700 Self::rewrite_mysql_values_refs(&mut expr);
17701 assignments.push((col, expr));
17702 if matches!(self.peek(), Token::Comma) {
17703 self.advance();
17704 continue;
17705 }
17706 break;
17707 }
17708 Ok(Some(crate::ast::OnConflictClause {
17709 target_columns: Vec::new(),
17710 index_where: None,
17711 constraint_name: None,
17712 mysql_lowered: true,
17713 action: crate::ast::OnConflictAction::Update {
17714 assignments,
17715 where_: None,
17716 },
17717 }))
17718 }
17719
17720 fn insert_ignore_clause() -> crate::ast::OnConflictClause {
17721 crate::ast::OnConflictClause {
17722 target_columns: Vec::new(),
17723 index_where: None,
17724 constraint_name: None,
17725 mysql_lowered: true,
17726 action: crate::ast::OnConflictAction::Nothing,
17727 }
17728 }
17729
17730 fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
17731 debug_assert!(
17732 matches!(self.peek(), Token::Insert)
17733 || (replace
17734 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
17735 );
17736 self.advance();
17737 // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
17738 // would raise a duplicate-key error instead of failing the statement,
17739 // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
17740 // plain ident to the lexer; only the MySQL dialect accepts it here.
17741 let ignore = self.mysql_dialect
17742 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
17743 if ignore {
17744 self.advance();
17745 }
17746 if !matches!(self.peek(), Token::Into) {
17747 return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
17748 }
17749 self.advance();
17750 let table = self.expect_ident_like()?;
17751 // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
17752 // grammar requires the AS keyword here (a bare identifier would be
17753 // ambiguous with a column list). The alias is what the ON CONFLICT
17754 // DO UPDATE expressions refer to the target row by.
17755 let alias = if matches!(self.peek(), Token::As) {
17756 self.advance();
17757 Some(self.expect_ident_like()?)
17758 } else {
17759 None
17760 };
17761 // v7.39 (round 428) — MySQL's SET-form INSERT:
17762 // INSERT INTO t SET a = 1, b = 'x'
17763 // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
17764 // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
17765 // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
17766 // measured). So it lowers to the column list + one VALUES row and
17767 // rejoins the ordinary path, which already handles every one of
17768 // those. PG has no such spelling, hence the dialect gate.
17769 if self.mysql_dialect
17770 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
17771 {
17772 self.advance(); // SET
17773 let mut names = Vec::new();
17774 let mut values = Vec::new();
17775 loop {
17776 names.push(self.expect_ident_like()?);
17777 if !matches!(self.peek(), Token::Eq) {
17778 return Err(self.err(alloc::format!(
17779 "expected '=' in INSERT … SET, got {:?}",
17780 self.peek()
17781 )));
17782 }
17783 self.advance();
17784 // `SET a = DEFAULT` rides the same `__column_default` marker
17785 // the VALUES-row and UPDATE-SET paths use; the INSERT
17786 // executor resolves it against the target column.
17787 if matches!(self.peek(), Token::Default) {
17788 self.advance();
17789 values.push(Expr::FunctionCall {
17790 name: "__column_default".to_string(),
17791 args: Vec::new(),
17792 });
17793 } else {
17794 values.push(self.parse_expr(0)?);
17795 }
17796 if matches!(self.peek(), Token::Comma) {
17797 self.advance();
17798 continue;
17799 }
17800 break;
17801 }
17802 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17803 let returning = self.parse_optional_returning()?;
17804 return Ok(Statement::Insert(InsertStatement {
17805 ctes: Vec::new(),
17806 table,
17807 alias,
17808 columns: Some(names),
17809 rows: alloc::vec![values],
17810 select_source: None,
17811 // MySQL's SET form has no `OVERRIDING …` clause (that is
17812 // PG's identity-column spelling).
17813 overriding: Overriding::None,
17814 mysql_ignore: ignore,
17815 on_conflict,
17816 returning,
17817 }));
17818 }
17819 // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
17820 // v7.39 (round 151) — a SELECT or WITH right after the paren is
17821 // a parenthesized query source instead (PG select_with_parens:
17822 // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
17823 // both keywords are reserved in PG, so no column list can start
17824 // with them.
17825 let columns = if matches!(self.peek(), Token::LParen) {
17826 self.advance();
17827 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17828 let select_stmt = if self.peek_is_with_kw() {
17829 self.advance();
17830 self.parse_nested_with_select()?
17831 } else {
17832 match self.parse_select_stmt()? {
17833 Statement::Select(s) => s,
17834 other => {
17835 return Err(self.err(alloc::format!(
17836 "expected SELECT in parenthesized INSERT source, got {other:?}"
17837 )));
17838 }
17839 }
17840 };
17841 if !matches!(self.peek(), Token::RParen) {
17842 return Err(self.err(format!(
17843 "expected ')' after parenthesized INSERT source, got {:?}",
17844 self.peek()
17845 )));
17846 }
17847 self.advance();
17848 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17849 let returning = self.parse_optional_returning()?;
17850 return Ok(Statement::Insert(InsertStatement {
17851 ctes: Vec::new(),
17852 table,
17853 alias: alias.clone(),
17854 columns: None,
17855 rows: Vec::new(),
17856 select_source: Some(Box::new(select_stmt)),
17857 on_conflict,
17858 returning,
17859 overriding: Overriding::None,
17860 mysql_ignore: ignore,
17861 }));
17862 }
17863 let mut names = Vec::new();
17864 loop {
17865 names.push(self.expect_ident_like()?);
17866 match self.peek() {
17867 Token::Comma => {
17868 self.advance();
17869 }
17870 Token::RParen => {
17871 self.advance();
17872 break;
17873 }
17874 other => {
17875 return Err(self.err(format!(
17876 "expected ',' or ')' in INSERT column list, got {other:?}"
17877 )));
17878 }
17879 }
17880 }
17881 Some(names)
17882 } else {
17883 None
17884 };
17885 // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
17886 // OVERRIDING SYSTEM VALUE for its identity columns. The clause
17887 // is captured on the statement so the engine can apply PG's
17888 // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
17889 let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
17890 {
17891 self.advance();
17892 let which = self.expect_ident_like()?;
17893 let ov = if which.eq_ignore_ascii_case("system") {
17894 Overriding::System
17895 } else if which.eq_ignore_ascii_case("user") {
17896 Overriding::User
17897 } else {
17898 return Err(self.err(format!(
17899 "expected SYSTEM or USER after OVERRIDING, got {which:?}"
17900 )));
17901 };
17902 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
17903 return Err(self.err(format!(
17904 "expected VALUE after OVERRIDING {}, got {:?}",
17905 which.to_ascii_uppercase(),
17906 self.peek()
17907 )));
17908 }
17909 self.advance();
17910 ov
17911 } else {
17912 Overriding::None
17913 };
17914 // `INSERT INTO t DEFAULT VALUES` — a single row made
17915 // entirely of column defaults. Lower to the permuted
17916 // column-list path with an empty list: every schema column
17917 // is unmapped, so the engine fills each from its default
17918 // (serials advance, plain defaults evaluate, the rest NULL).
17919 if matches!(self.peek(), Token::Default) {
17920 self.advance();
17921 if !matches!(self.peek(), Token::Values) {
17922 return Err(self.err(format!(
17923 "expected VALUES after DEFAULT in INSERT, got {:?}",
17924 self.peek()
17925 )));
17926 }
17927 self.advance();
17928 if columns.is_some() {
17929 return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
17930 }
17931 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17932 let returning = self.parse_optional_returning()?;
17933 return Ok(Statement::Insert(InsertStatement {
17934 ctes: Vec::new(),
17935 table,
17936 alias: alias.clone(),
17937 columns: Some(Vec::new()),
17938 rows: alloc::vec![Vec::new()],
17939 select_source: None,
17940 on_conflict,
17941 returning,
17942 overriding,
17943 mysql_ignore: ignore,
17944 }));
17945 }
17946 // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
17947 // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
17948 // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
17949 // SELECT …`) heads the SOURCE select, as in PG (the statement's
17950 // own WITH comes before INSERT).
17951 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17952 let select_stmt = if self.peek_is_with_kw() {
17953 self.advance();
17954 self.parse_nested_with_select()?
17955 } else {
17956 match self.parse_select_stmt()? {
17957 Statement::Select(s) => s,
17958 other => {
17959 return Err(self.err(alloc::format!(
17960 "expected SELECT after INSERT INTO ... target, got {other:?}"
17961 )));
17962 }
17963 }
17964 };
17965 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17966 let returning = self.parse_optional_returning()?;
17967 return Ok(Statement::Insert(InsertStatement {
17968 ctes: Vec::new(),
17969 table,
17970 alias: alias.clone(),
17971 columns,
17972 rows: Vec::new(),
17973 select_source: Some(Box::new(select_stmt)),
17974 on_conflict,
17975 returning,
17976 overriding,
17977 mysql_ignore: ignore,
17978 }));
17979 }
17980 if !matches!(self.peek(), Token::Values) {
17981 return Err(self.err(format!(
17982 "expected VALUES or SELECT after table name, got {:?}",
17983 self.peek()
17984 )));
17985 }
17986 self.advance();
17987 if !matches!(self.peek(), Token::LParen) {
17988 return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
17989 }
17990 let mut rows = Vec::new();
17991 loop {
17992 // Each iteration consumes one `(expr, expr, …)` tuple.
17993 if !matches!(self.peek(), Token::LParen) {
17994 return Err(self.err(format!(
17995 "expected '(' for next VALUES tuple, got {:?}",
17996 self.peek()
17997 )));
17998 }
17999 self.advance();
18000 let mut tuple = Vec::new();
18001 loop {
18002 // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
18003 // the column's declared default for that slot. Rides out as the
18004 // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
18005 // path uses; the INSERT executor resolves it per target column.
18006 if matches!(self.peek(), Token::Default) {
18007 self.advance();
18008 tuple.push(Expr::FunctionCall {
18009 name: "__column_default".to_string(),
18010 args: Vec::new(),
18011 });
18012 } else {
18013 tuple.push(self.parse_expr(0)?);
18014 }
18015 match self.peek() {
18016 Token::Comma => {
18017 self.advance();
18018 }
18019 Token::RParen => {
18020 self.advance();
18021 break;
18022 }
18023 other => {
18024 return Err(self.err(format!(
18025 "expected ',' or ')' in VALUES tuple, got {other:?}"
18026 )));
18027 }
18028 }
18029 }
18030 if tuple.is_empty() {
18031 return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
18032 }
18033 rows.push(tuple);
18034 // Continue with comma-separated tuples.
18035 if matches!(self.peek(), Token::Comma) {
18036 self.advance();
18037 } else {
18038 break;
18039 }
18040 }
18041 // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
18042 // to ON CONFLICT DO UPDATE with an empty conflict target
18043 // (the engine picks the table's first unique index, which
18044 // matches MySQL's any-unique-key behaviour for the common
18045 // single-key case). `VALUES(col)` in the assignments is
18046 // MySQL's spelling of EXCLUDED.col.
18047 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18048 let returning = self.parse_optional_returning()?;
18049 Ok(Statement::Insert(InsertStatement {
18050 ctes: Vec::new(),
18051 table,
18052 alias,
18053 columns,
18054 rows,
18055 select_source: None,
18056 on_conflict,
18057 returning,
18058 overriding,
18059 mysql_ignore: ignore,
18060 }))
18061 }
18062
18063 /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
18064 /// the incoming row's value — exactly PG's EXCLUDED.col.
18065 fn rewrite_mysql_values_refs(e: &mut Expr) {
18066 match e {
18067 Expr::FunctionCall { name, args }
18068 if name.eq_ignore_ascii_case("values")
18069 && args.len() == 1
18070 && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
18071 {
18072 let Expr::Column(c) = &args[0] else {
18073 unreachable!("guarded above");
18074 };
18075 *e = Expr::Column(crate::ast::ColumnName {
18076 qualifier: Some("EXCLUDED".to_string()),
18077 name: c.name.clone(),
18078 });
18079 }
18080 Expr::FunctionCall { args, .. } => {
18081 for a in args {
18082 Self::rewrite_mysql_values_refs(a);
18083 }
18084 }
18085 Expr::Binary { lhs, rhs, .. } => {
18086 Self::rewrite_mysql_values_refs(lhs);
18087 Self::rewrite_mysql_values_refs(rhs);
18088 }
18089 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
18090 Self::rewrite_mysql_values_refs(expr);
18091 }
18092 Expr::Case {
18093 operand,
18094 branches,
18095 else_branch,
18096 } => {
18097 if let Some(op) = operand {
18098 Self::rewrite_mysql_values_refs(op);
18099 }
18100 for (w, t) in branches {
18101 Self::rewrite_mysql_values_refs(w);
18102 Self::rewrite_mysql_values_refs(t);
18103 }
18104 if let Some(el) = else_branch {
18105 Self::rewrite_mysql_values_refs(el);
18106 }
18107 }
18108 _ => {}
18109 }
18110 }
18111
18112 /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
18113 /// clause sitting between the INSERT body and the trailing
18114 /// RETURNING. All keywords come in as bare idents; `ON` is
18115 /// a reserved Token though.
18116 fn parse_optional_on_conflict(
18117 &mut self,
18118 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18119 if !matches!(self.peek(), Token::On) {
18120 return Ok(None);
18121 }
18122 // Peek further: we want exactly "ON CONFLICT ...". If the
18123 // next ident isn't "conflict", let some other parser handle.
18124 let next_is_conflict = matches!(
18125 self.tokens.get(self.pos + 1),
18126 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18127 );
18128 if !next_is_conflict {
18129 return Ok(None);
18130 }
18131 self.advance(); // ON
18132 self.advance(); // CONFLICT
18133 // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18134 // the constraint instead of listing columns (the pg_dump
18135 // form); the engine resolves it.
18136 let mut constraint_name: Option<String> = None;
18137 if matches!(self.peek(), Token::On) {
18138 self.advance(); // ON
18139 match self.advance() {
18140 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18141 }
18142 other => {
18143 return Err(self.err(alloc::format!(
18144 "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18145 )));
18146 }
18147 }
18148 constraint_name = Some(self.expect_ident_like()?);
18149 }
18150 // Optional `(col [, col]*)` target list.
18151 let mut target_columns: Vec<String> = Vec::new();
18152 if matches!(self.peek(), Token::LParen) {
18153 self.advance();
18154 loop {
18155 target_columns.push(self.expect_ident_like()?);
18156 match self.peek() {
18157 Token::Comma => {
18158 self.advance();
18159 }
18160 Token::RParen => {
18161 self.advance();
18162 break;
18163 }
18164 other => {
18165 return Err(self.err(alloc::format!(
18166 "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18167 )));
18168 }
18169 }
18170 }
18171 }
18172 // v7.39 (round 240) — optional index predicate after the target
18173 // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18174 // PARTIAL unique index; SPG's arbiters are full indexes, which
18175 // satisfy any predicate, so it is parsed and carried but not
18176 // consulted (recorded residual: partial-unique-index arbiters).
18177 let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18178 self.advance();
18179 Some(self.parse_expr(0)?)
18180 } else {
18181 None
18182 };
18183 // Required `DO`.
18184 match self.advance() {
18185 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18186 other => {
18187 return Err(self.err(alloc::format!(
18188 "expected DO after ON CONFLICT [(…)], got {other:?}"
18189 )));
18190 }
18191 }
18192 // Action: NOTHING | UPDATE SET …
18193 let action = match self.advance() {
18194 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18195 crate::ast::OnConflictAction::Nothing
18196 }
18197 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18198 self.parse_on_conflict_update_action()?
18199 }
18200 other => {
18201 return Err(self.err(alloc::format!(
18202 "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18203 )));
18204 }
18205 };
18206 Ok(Some(crate::ast::OnConflictClause {
18207 target_columns,
18208 index_where,
18209 constraint_name,
18210 mysql_lowered: false,
18211 action,
18212 }))
18213 }
18214
18215 /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18216 /// `SET col = expr [, …] [WHERE cond]`. Caller already
18217 /// consumed `UPDATE`.
18218 fn parse_on_conflict_update_action(
18219 &mut self,
18220 ) -> Result<crate::ast::OnConflictAction, ParseError> {
18221 // `SET`
18222 match self.advance() {
18223 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18224 other => {
18225 return Err(self.err(alloc::format!(
18226 "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18227 )));
18228 }
18229 }
18230 let mut assignments: Vec<(String, Expr)> = Vec::new();
18231 loop {
18232 let col = self.expect_ident_like()?;
18233 if !matches!(self.peek(), Token::Eq) {
18234 return Err(self.err(alloc::format!(
18235 "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18236 self.peek()
18237 )));
18238 }
18239 self.advance();
18240 let value = self.parse_expr(0)?;
18241 assignments.push((col, value));
18242 if matches!(self.peek(), Token::Comma) {
18243 self.advance();
18244 continue;
18245 }
18246 break;
18247 }
18248 let where_ = if matches!(self.peek(), Token::Where) {
18249 self.advance();
18250 Some(self.parse_expr(0)?)
18251 } else {
18252 None
18253 };
18254 Ok(crate::ast::OnConflictAction::Update {
18255 assignments,
18256 where_,
18257 })
18258 }
18259
18260 fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18261 let mut items = Vec::new();
18262 // v7.39 (round 341, V66) — PG's target list may be EMPTY
18263 // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18264 // answers one zero-column row per row of t, and a bare `SELECT`
18265 // answers a single zero-column row. SPG required at least one
18266 // item, so both were syntax errors. Recognised by the token that
18267 // follows — nothing that can start an expression appears here.
18268 if self.select_list_is_empty_here() {
18269 return Ok(items);
18270 }
18271 loop {
18272 items.push(self.parse_select_item()?);
18273 if matches!(self.peek(), Token::Comma) {
18274 self.advance();
18275 } else {
18276 break;
18277 }
18278 }
18279 Ok(items)
18280 }
18281
18282 /// Is the target list empty at this point — i.e. does the next token
18283 /// end the SELECT's item list rather than start an item?
18284 fn select_list_is_empty_here(&self) -> bool {
18285 match self.peek() {
18286 Token::From
18287 | Token::Where
18288 | Token::Group
18289 | Token::Having
18290 | Token::Order
18291 | Token::Limit
18292 | Token::Offset
18293 | Token::Semicolon
18294 | Token::RParen
18295 | Token::Union
18296 | Token::Except
18297 | Token::Eof => true,
18298 // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18299 // with unreserved keywords, so they arrive as plain idents.
18300 Token::Ident(s) => {
18301 s.eq_ignore_ascii_case("fetch")
18302 || s.eq_ignore_ascii_case("window")
18303 || s.eq_ignore_ascii_case("intersect")
18304 }
18305 _ => false,
18306 }
18307 }
18308
18309 fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18310 if matches!(self.peek(), Token::Star) {
18311 self.advance();
18312 return Ok(SelectItem::Wildcard);
18313 }
18314 // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18315 // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18316 // choke on the `*` ("expected identifier, got Star"). The lookahead is
18317 // `<ident> . *` with nothing binding tighter.
18318 if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18319 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18320 && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18321 {
18322 self.advance(); // qualifier
18323 self.advance(); // .
18324 self.advance(); // *
18325 return Ok(SelectItem::QualifiedWildcard(q));
18326 }
18327 }
18328 let start_tok = self.pos;
18329 let expr = self.parse_expr(0)?;
18330 let end_tok = self.consumed_pos();
18331 // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18332 // multi-column function returns into columns. Marked here and lowered in
18333 // `parse_bare_select`, where the FROM clause is in hand.
18334 if matches!(self.peek(), Token::Dot)
18335 && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18336 {
18337 self.advance(); // .
18338 self.advance(); // *
18339 return Ok(SelectItem::Expr {
18340 expr: Expr::FunctionCall {
18341 name: "__record_expand".to_string(),
18342 args: alloc::vec![expr],
18343 },
18344 alias: None,
18345 });
18346 }
18347 let alias = match self.parse_optional_alias()? {
18348 Some(a) => Some(a),
18349 None => self.mysql_item_label(&expr, start_tok, end_tok),
18350 };
18351 Ok(SelectItem::Expr { expr, alias })
18352 }
18353
18354 /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18355 /// carries no `AS`, filled in here so every downstream path reports it
18356 /// without knowing the rule. `None` leaves the item un-aliased, which is
18357 /// what a PG session always gets.
18358 ///
18359 /// Measured against MariaDB 11, three rules and no more:
18360 ///
18361 /// | item | label | why |
18362 /// |------------------|------------|------------------------------|
18363 /// | `lbl.a` | `a` | a column reports its name |
18364 /// | `'it''s'` | `it's` | a string reports its VALUE |
18365 /// | `a + b` | `a + b` | anything else, source text |
18366 ///
18367 /// The third is why this lives in the parser at all: the label is the
18368 /// text the client WROTE, down to the spacing, so it cannot be printed
18369 /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18370 ///
18371 /// Comments survive, and that is right: through a `mariadb` CLI both
18372 /// servers answer `a + b` for `SELECT a /* c */ + b`, but that is the
18373 /// CLIENT stripping the comment before it sends. Asked over the raw
18374 /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18375 /// produces.
18376 fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18377 if !self.mysql_dialect {
18378 return None;
18379 }
18380 match expr {
18381 // A column already reports its own name downstream; naming it
18382 // again here would only re-state the qualifier the label drops.
18383 Expr::Column(_) => None,
18384 Expr::Literal(Literal::String(v)) => Some(v.clone()),
18385 _ => self.source_span(start_tok, end_tok).map(str::to_string),
18386 }
18387 }
18388
18389 /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18390 /// consumed VALUES keyword. Each row lowers to a constant SELECT
18391 /// with PG's default column1..columnN names; subsequent rows
18392 /// chain as UNION ALL peers. Shared by the FROM-position
18393 /// `( VALUES … )` arm and the top-level bare VALUES statement.
18394 fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
18395 let mut row_selects: Vec<SelectStatement> = Vec::new();
18396 loop {
18397 if !matches!(self.peek(), Token::LParen) {
18398 return Err(self.err(alloc::format!(
18399 "expected '(' to start a VALUES row, got {:?}",
18400 self.peek()
18401 )));
18402 }
18403 self.advance(); // (
18404 let mut items: Vec<SelectItem> = Vec::new();
18405 loop {
18406 let expr = self.parse_expr(0)?;
18407 items.push(SelectItem::Expr {
18408 expr,
18409 alias: Some(alloc::format!("column{}", items.len() + 1)),
18410 });
18411 match self.peek() {
18412 Token::Comma => {
18413 self.advance();
18414 }
18415 Token::RParen => break,
18416 other => {
18417 return Err(self.err(alloc::format!(
18418 "expected ',' or ')' in VALUES row, got {other:?}"
18419 )));
18420 }
18421 }
18422 }
18423 self.advance(); // )
18424 row_selects.push(SelectStatement {
18425 locking: None,
18426 ctes: Vec::new(),
18427 distinct: false,
18428 distinct_on: Vec::new(),
18429 items,
18430 from: None,
18431 where_: None,
18432 group_by: None,
18433 group_by_all: false,
18434 having: None,
18435 unions: Vec::new(),
18436 order_by: Vec::new(),
18437 limit: None,
18438 offset: None,
18439 limit_with_ties: false,
18440 window_check_exprs: Vec::new(),
18441 });
18442 if matches!(self.peek(), Token::Comma) {
18443 self.advance();
18444 continue;
18445 }
18446 break;
18447 }
18448 let mut head = row_selects.remove(0);
18449 head.unions = row_selects
18450 .into_iter()
18451 .map(|s| (UnionKind::All, s))
18452 .collect();
18453 Ok(head)
18454 }
18455
18456 fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
18457 // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
18458 // children. It was read as a table NAMED `only`, so the query
18459 // failed on `relation "only" does not exist`.
18460 //
18461 // v7.39 (round 644) — and it is no longer a no-op. Round 621
18462 // absorbed the keyword, reasoning that SPG's children are
18463 // separate relations a plain scan does not descend into, so ONLY
18464 // already described the scan. That stopped being true when a
18465 // partition parent started unioning its children: measured,
18466 // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
18467 // where PG answers 0. The flag is carried now.
18468 let mut only = false;
18469 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
18470 && matches!(
18471 self.tokens.get(self.pos + 1),
18472 Some(Token::Ident(_) | Token::QuotedIdent(_))
18473 )
18474 {
18475 only = true;
18476 self.advance();
18477 }
18478 // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
18479 // for these SRFs the keyword is noise at parse time: the
18480 // join executor already substitutes outer-column references
18481 // into unnest_expr / generate_series_args per outer row
18482 // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
18483 // licences the correlation even without the keyword. Absorb
18484 // it and fall through to the SRF arms below.
18485 // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
18486 // just the four builtin SRFs: a user set-returning function on a JOIN's
18487 // right side is the whole point of LATERAL. The keyword stays noise at
18488 // parse time — the join executor substitutes the outer row into the
18489 // call's arguments per outer row.
18490 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18491 && matches!(
18492 self.tokens.get(self.pos + 1),
18493 // The json_each family has its OWN `LATERAL …` arm below, which
18494 // needs to see the keyword — absorbing it here would send those
18495 // calls down the generic table-function channel instead.
18496 Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
18497 )
18498 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18499 {
18500 self.advance(); // LATERAL
18501 }
18502 // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
18503 // set-returning function whose argument may reference a
18504 // preceding FROM item. We rewrite this to
18505 // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
18506 // AS __srf__) AS <alias>` so the existing LATERAL subquery
18507 // executor handles per-outer-row evaluation and the
18508 // SRF-primary jsonb_each_text path handles the inner
18509 // materialisation. Sentori 0067 backfill is the dogfood
18510 // shape.
18511 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18512 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
18513 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18514 {
18515 self.advance(); // LATERAL
18516 let each_fn = match self.peek() {
18517 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18518 _ => unreachable!(),
18519 };
18520 self.advance(); // jsonb_each[_text] / json_each[_text]
18521 self.advance(); // (
18522 let arg = self.parse_expr(0)?;
18523 if !matches!(self.peek(), Token::RParen) {
18524 return Err(self.err(alloc::format!(
18525 "expected ')' after LATERAL {each_fn}() argument, got {:?}",
18526 self.peek()
18527 )));
18528 }
18529 self.advance();
18530 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18531 let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18532 // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
18533 // FROM jsonb_each_text(<arg>) AS __srf__
18534 // PG's `AS kv(key, value)` column-alias list maps
18535 // positions to names; default to (key, value) when
18536 // omitted (matching the SRF's natural column names).
18537 let srf_alias = "__srf__".to_string();
18538 let key_alias = column_aliases
18539 .first()
18540 .cloned()
18541 .unwrap_or_else(|| "key".to_string());
18542 let value_alias = column_aliases
18543 .get(1)
18544 .cloned()
18545 .unwrap_or_else(|| "value".to_string());
18546 let inner_select = crate::ast::SelectStatement {
18547 locking: None,
18548 ctes: Vec::new(),
18549 distinct: false,
18550 distinct_on: Vec::new(),
18551 items: alloc::vec![
18552 crate::ast::SelectItem::Expr {
18553 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18554 qualifier: Some(srf_alias.clone()),
18555 name: "key".to_string(),
18556 }),
18557 alias: Some(key_alias),
18558 },
18559 crate::ast::SelectItem::Expr {
18560 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18561 qualifier: Some(srf_alias.clone()),
18562 name: "value".to_string(),
18563 }),
18564 alias: Some(value_alias),
18565 },
18566 ],
18567 from: Some(crate::ast::FromClause {
18568 primary: TableRef {
18569 name: srf_alias.clone(),
18570 alias: Some(srf_alias.clone()),
18571 only: false,
18572 as_of_segment: None,
18573 unnest_expr: None,
18574 unnest_column_aliases: Vec::new(),
18575 with_ordinality: false,
18576 generate_series_args: None,
18577 lateral_subquery: None,
18578 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18579 table_fn_call: None,
18580 rows_from: None,
18581 json_table: None,
18582 scalar_fn_item: false,
18583 },
18584 joins: Vec::new(),
18585 }),
18586 where_: None,
18587 group_by: None,
18588 group_by_all: false,
18589 having: None,
18590 unions: Vec::new(),
18591 order_by: Vec::new(),
18592 limit: None,
18593 offset: None,
18594 limit_with_ties: false,
18595 window_check_exprs: Vec::new(),
18596 };
18597 return Ok(TableRef {
18598 name: alias.clone(),
18599 alias: Some(alias),
18600 only: false,
18601 as_of_segment: None,
18602 unnest_expr: None,
18603 unnest_column_aliases: Vec::new(),
18604 with_ordinality: false,
18605 generate_series_args: None,
18606 lateral_subquery: Some(Box::new(inner_select)),
18607 jsonb_each_text_arg: None,
18608 table_fn_call: None,
18609 rows_from: None,
18610 json_table: None,
18611 scalar_fn_item: false,
18612 });
18613 }
18614 // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
18615 // without an explicit `LATERAL` keyword is the same shape
18616 // PG accepts (SRF naturally licences lateral correlation).
18617 // We mirror the LATERAL rewrite when the argument syntactic-
18618 // ally references an outer column (Column { qualifier:
18619 // Some(_), … }). For simplicity we apply the rewrite
18620 // whenever the SRF directly follows JOIN/CROSS JOIN/comma
18621 // in the FROM-list — caller-side join parsing positions
18622 // this peek correctly.
18623 // (Implementation note: detection lives below; the LATERAL
18624 // branch above already covers the explicit form; the bare
18625 // form falls through to the plain SRF arm and the engine
18626 // treats it as a constant-arg SRF if no outer reference is
18627 // present.)
18628 // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
18629 // table. Detect at the head so it claims precedence over
18630 // every other table-ref shape (unnest / generate_series /
18631 // bare ident); the lateral subquery itself follows the
18632 // regular SELECT grammar.
18633 // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
18634 // t(cols)`. Each row lowers to a constant SELECT with PG's
18635 // default column1..columnN names; subsequent rows chain as
18636 // UNION ALL peers. The result rides the derived-table
18637 // lateral_subquery channel — zero executor work.
18638 if matches!(self.peek(), Token::LParen)
18639 && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
18640 {
18641 self.advance(); // (
18642 self.advance(); // VALUES
18643 let head = self.parse_values_rows_body()?;
18644 if !matches!(self.peek(), Token::RParen) {
18645 return Err(self.err(alloc::format!(
18646 "expected ')' after VALUES list, got {:?}",
18647 self.peek()
18648 )));
18649 }
18650 self.advance();
18651 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18652 let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
18653 return Ok(TableRef {
18654 name,
18655 alias: alias_ident,
18656 only: false,
18657 as_of_segment: None,
18658 unnest_expr: None,
18659 unnest_column_aliases: column_aliases,
18660 with_ordinality: false,
18661 generate_series_args: None,
18662 lateral_subquery: Some(Box::new(head)),
18663 jsonb_each_text_arg: None,
18664 table_fn_call: None,
18665 rows_from: None,
18666 json_table: None,
18667 scalar_fn_item: false,
18668 });
18669 }
18670 // v7.37.17 (17.6 siblings) — plain derived table:
18671 // `FROM ( SELECT … ) [AS] alias`. Rides the same
18672 // lateral_subquery channel the explicit LATERAL form uses —
18673 // an uncorrelated inner SELECT executes identically. The
18674 // inner parse carries UNION tails (they live on
18675 // SelectStatement.unions).
18676 // v7.37 D.20 — the derived-table inner may itself be a
18677 // parenthesized set-operation group (`FROM ((SELECT…) UNION
18678 // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
18679 // bare `(SELECT …)`. parse_one_statement already routes a leading
18680 // `(` set-op group (its LParen arm) and a leading WITH
18681 // (parse_with_cte_then_select), so widen the second-token gate to
18682 // Select | LParen | WITH.
18683 // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
18684 // PG's spelling of `SELECT * FROM t` and is accepted wherever a
18685 // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
18686 // has existed since the shorthand landed and `parse_bare_select`
18687 // already routes it ("valid anywhere a SELECT head is"); what was
18688 // missing is this second-token gate, and the CTE body's dispatch
18689 // below. Round 868 found both by putting the shorthand in a
18690 // subquery — the top-level forms had been the only ones tested.
18691 if matches!(self.peek(), Token::LParen)
18692 && (matches!(
18693 self.tokens.get(self.pos + 1),
18694 Some(Token::Select | Token::LParen | Token::Table)
18695 ) || matches!(self.tokens.get(self.pos + 1),
18696 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
18697 {
18698 self.advance(); // (
18699 let inner = match self.parse_one_statement()? {
18700 Statement::Select(s) => s,
18701 other => {
18702 return Err(self.err(alloc::format!(
18703 "expected SELECT inside derived table ( … ), got {other:?}"
18704 )));
18705 }
18706 };
18707 if !matches!(self.peek(), Token::RParen) {
18708 return Err(self.err(alloc::format!(
18709 "expected ')' after derived-table subquery, got {:?}",
18710 self.peek()
18711 )));
18712 }
18713 self.advance();
18714 // `AS t(a, b)` column-alias list rides the
18715 // unnest_column_aliases field (same positional-rename
18716 // contract the unnest SRFs use).
18717 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18718 let name = alias_ident
18719 .clone()
18720 .unwrap_or_else(|| "subquery".to_string());
18721 return Ok(TableRef {
18722 name,
18723 alias: alias_ident,
18724 only: false,
18725 as_of_segment: None,
18726 unnest_expr: None,
18727 unnest_column_aliases: column_aliases,
18728 with_ordinality: false,
18729 generate_series_args: None,
18730 lateral_subquery: Some(Box::new(inner)),
18731 jsonb_each_text_arg: None,
18732 table_fn_call: None,
18733 rows_from: None,
18734 json_table: None,
18735 scalar_fn_item: false,
18736 });
18737 }
18738 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18739 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18740 {
18741 self.advance(); // LATERAL
18742 self.advance(); // (
18743 // Parse the inner SELECT.
18744 let inner = match self.parse_one_statement()? {
18745 Statement::Select(s) => s,
18746 other => {
18747 return Err(self.err(alloc::format!(
18748 "expected SELECT inside LATERAL ( … ), got {other:?}"
18749 )));
18750 }
18751 };
18752 if !matches!(self.peek(), Token::RParen) {
18753 return Err(self.err(alloc::format!(
18754 "expected ')' after LATERAL subquery, got {:?}",
18755 self.peek()
18756 )));
18757 }
18758 self.advance();
18759 // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
18760 // `(VALUES …) t(g)` derived table round-trips through view-body
18761 // Display, which renders on the lateral_subquery channel).
18762 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18763 let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
18764 return Ok(TableRef {
18765 name,
18766 alias: alias_ident,
18767 only: false,
18768 as_of_segment: None,
18769 unnest_expr: None,
18770 unnest_column_aliases: column_aliases,
18771 with_ordinality: false,
18772 generate_series_args: None,
18773 lateral_subquery: Some(Box::new(inner)),
18774 jsonb_each_text_arg: None,
18775 table_fn_call: None,
18776 rows_from: None,
18777 json_table: None,
18778 scalar_fn_item: false,
18779 });
18780 }
18781 // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
18782 // function as a FROM item. Emits one row per (key, value)
18783 // pair in the JSONB object argument as TEXT columns. May
18784 // be wrapped in CROSS JOIN LATERAL when the argument
18785 // references a preceding FROM item (sentori migration
18786 // 0067 backfill shape: `CROSS JOIN LATERAL
18787 // jsonb_each_text(t.json_col) AS kv(key, value)`).
18788 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
18789 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18790 {
18791 let each_fn = match self.peek() {
18792 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18793 _ => unreachable!(),
18794 };
18795 self.advance(); // jsonb_each[_text] / json_each[_text]
18796 self.advance(); // (
18797 let arg = self.parse_expr(0)?;
18798 if !matches!(self.peek(), Token::RParen) {
18799 return Err(self.err(alloc::format!(
18800 "expected ')' after {each_fn}() argument, got {:?}",
18801 self.peek()
18802 )));
18803 }
18804 self.advance();
18805 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18806 let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18807 return Ok(TableRef {
18808 name,
18809 alias: alias_ident,
18810 only: false,
18811 as_of_segment: None,
18812 unnest_expr: None,
18813 // `AS t(k, v)` renames key/value positionally, same as the
18814 // LATERAL-position form already does.
18815 unnest_column_aliases: column_aliases,
18816 with_ordinality: false,
18817 generate_series_args: None,
18818 lateral_subquery: None,
18819 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18820 table_fn_call: None,
18821 rows_from: None,
18822 json_table: None,
18823 scalar_fn_item: false,
18824 });
18825 }
18826 // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
18827 // (+ json_ variants) — record-returning JSON functions with a
18828 // column-definition list. Desugar to a derived table that
18829 // projects each declared column from the JSON via `->>` + a cast,
18830 // over `jsonb_array_elements(J)` for the *set (per-element) form.
18831 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
18832 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18833 {
18834 return self.parse_json_to_record_from();
18835 }
18836 // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
18837 // row is a text[] of capture groups, so it cannot desugar to unnest
18838 // (that would flatten the array). Wrap it as a derived table
18839 // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
18840 // SRF path already emits one text[] row per match. PG names the column
18841 // `regexp_matches`; an `AS a(col)` alias overrides it.
18842 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18843 if s.eq_ignore_ascii_case("regexp_matches"))
18844 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18845 {
18846 self.advance(); // fn name
18847 self.advance(); // (
18848 let mut fn_args: Vec<Expr> = Vec::new();
18849 loop {
18850 fn_args.push(self.parse_expr(0)?);
18851 if matches!(self.peek(), Token::Comma) {
18852 self.advance();
18853 continue;
18854 }
18855 break;
18856 }
18857 if !matches!(self.peek(), Token::RParen) {
18858 return Err(self.err(alloc::format!(
18859 "expected ')' after regexp_matches() arguments, got {:?}",
18860 self.peek()
18861 )));
18862 }
18863 self.advance();
18864 // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
18865 // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
18866 // it, so it died on the `with` token while every other table function
18867 // accepted it.
18868 let with_ordinality = self.absorb_with_ordinality();
18869 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18870 let table_alias = alias_ident
18871 .clone()
18872 .unwrap_or_else(|| "regexp_matches".to_string());
18873 // PG names a single-column function's output column after the ALIAS
18874 // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
18875 // `m` reads as that column and not as a whole-row composite. Naming
18876 // it after the function regardless made `SELECT m[1] FROM … AS m`
18877 // subscript a record.
18878 let col_name = column_aliases
18879 .first()
18880 .cloned()
18881 .or_else(|| alias_ident.clone())
18882 .unwrap_or_else(|| "regexp_matches".to_string());
18883 let inner = crate::ast::SelectStatement {
18884 locking: None,
18885 ctes: Vec::new(),
18886 distinct: false,
18887 distinct_on: Vec::new(),
18888 items: alloc::vec![SelectItem::Expr {
18889 expr: Expr::FunctionCall {
18890 name: "regexp_matches".to_string(),
18891 args: fn_args,
18892 },
18893 alias: Some(col_name),
18894 }],
18895 from: None,
18896 where_: None,
18897 group_by: None,
18898 group_by_all: false,
18899 having: None,
18900 unions: Vec::new(),
18901 order_by: Vec::new(),
18902 limit: None,
18903 offset: None,
18904 limit_with_ties: false,
18905 window_check_exprs: Vec::new(),
18906 };
18907 return Ok(TableRef {
18908 name: table_alias.clone(),
18909 alias: Some(table_alias),
18910 only: false,
18911 as_of_segment: None,
18912 unnest_expr: None,
18913 unnest_column_aliases: column_aliases,
18914 with_ordinality,
18915 generate_series_args: None,
18916 lateral_subquery: Some(Box::new(inner)),
18917 jsonb_each_text_arg: None,
18918 table_fn_call: None,
18919 rows_from: None,
18920 json_table: None,
18921 // regexp_matches returns text[], a base type: `SELECT m FROM
18922 // regexp_matches(…) AS m` is the array, not a composite wrapping it.
18923 scalar_fn_item: true,
18924 });
18925 }
18926 // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
18927 // / json_ variants as a FROM item. Rewritten into
18928 // `unnest(<same fn>(<expr>))`: the scalar form returns the
18929 // elements as a TEXT array, and the existing unnest SRF path
18930 // materialises one row per element. PG's natural column name
18931 // is `value`; an `AS a(col)` column-alias list overrides it.
18932 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18933 if s.eq_ignore_ascii_case("jsonb_array_elements")
18934 || s.eq_ignore_ascii_case("json_array_elements")
18935 || s.eq_ignore_ascii_case("jsonb_array_elements_text")
18936 || s.eq_ignore_ascii_case("json_array_elements_text")
18937 || s.eq_ignore_ascii_case("jsonb_object_keys")
18938 || s.eq_ignore_ascii_case("json_object_keys")
18939 || s.eq_ignore_ascii_case("jsonb_path_query")
18940 || s.eq_ignore_ascii_case("json_path_query")
18941 || s.eq_ignore_ascii_case("generate_subscripts")
18942 || s.eq_ignore_ascii_case("string_to_table")
18943 || s.eq_ignore_ascii_case("regexp_split_to_table"))
18944 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18945 {
18946 let fn_name = match self.peek() {
18947 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18948 _ => unreachable!(),
18949 };
18950 self.advance(); // fn name
18951 self.advance(); // (
18952 let mut fn_args: Vec<Expr> = Vec::new();
18953 loop {
18954 fn_args.push(self.parse_expr(0)?);
18955 if matches!(self.peek(), Token::Comma) {
18956 self.advance();
18957 continue;
18958 }
18959 break;
18960 }
18961 if !matches!(self.peek(), Token::RParen) {
18962 return Err(self.err(alloc::format!(
18963 "expected ')' after {fn_name}() arguments, got {:?}",
18964 self.peek()
18965 )));
18966 }
18967 self.advance();
18968 let with_ordinality = self.absorb_with_ordinality();
18969 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18970 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
18971 // PG's natural column name: the array-elements SRFs
18972 // declare an OUT parameter `value`; jsonb_object_keys
18973 // and generate_subscripts have none, so the column is
18974 // named after the function. A bare table alias on a
18975 // single-column SRF renames the column too (PG: `FROM
18976 // generate_subscripts(a, 1) AS s` projects column s) —
18977 // except for the OUT-parameter SRFs, whose column stays
18978 // `value` under a bare alias.
18979 let natural_col = if fn_name.ends_with("_array_elements")
18980 || fn_name.ends_with("_array_elements_text")
18981 {
18982 "value".to_string()
18983 } else {
18984 alias_ident.clone().unwrap_or_else(|| fn_name.clone())
18985 };
18986 let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
18987 // Keep any further entries — the second names the
18988 // ordinality column under WITH ORDINALITY.
18989 srf_cols.extend(column_aliases.into_iter().skip(1));
18990 // The *_to_table SRFs are row-streams over the existing
18991 // *_to_array scalars — map the call target; the display
18992 // name (alias / column defaults) keeps the SRF spelling.
18993 let call_name = match fn_name.as_str() {
18994 "string_to_table" => "string_to_array".to_string(),
18995 "regexp_split_to_table" => "regexp_split_to_array".to_string(),
18996 _ => fn_name,
18997 };
18998 // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
18999 // preceding FROM item (bare or qualified column) is correlated;
19000 // route it through the per-outer-row lateral channel.
19001 let expr = crate::ast::Expr::FunctionCall {
19002 name: call_name,
19003 args: fn_args,
19004 };
19005 let correlated = Self::expr_has_any_column(&expr);
19006 let tref = TableRef {
19007 name,
19008 alias: alias_ident,
19009 only: false,
19010 as_of_segment: None,
19011 unnest_expr: Some(Box::new(expr)),
19012 unnest_column_aliases: srf_cols,
19013 with_ordinality,
19014 generate_series_args: None,
19015 lateral_subquery: None,
19016 jsonb_each_text_arg: None,
19017 table_fn_call: None,
19018 rows_from: None,
19019 json_table: None,
19020 // Each of these returns a BASE type (jsonb / text / int), so the item's
19021 // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
19022 // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
19023 scalar_fn_item: !with_ordinality,
19024 };
19025 return Ok(if correlated {
19026 Self::wrap_correlated_srf(tref)
19027 } else {
19028 tref
19029 });
19030 }
19031 // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
19032 // explicit parallel-zip syntax. Each entry lowers to its
19033 // array-returning scalar form (unnest(x) → x itself; the
19034 // FROM-SRF rewrite family → their scalar array calls) and
19035 // the list rides the multi-arg unnest zip channel:
19036 // NULL-padded to the longest, WITH ORDINALITY appends the
19037 // counter. generate_series has no scalar array form and
19038 // errors honestly.
19039 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
19040 && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
19041 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19042 {
19043 self.advance(); // ROWS
19044 self.advance(); // FROM
19045 self.advance(); // (
19046 let mut entries: Vec<Expr> = Vec::new();
19047 // v7.39 (read01 round 74) — the generic channel, filled in parallel.
19048 // Used only when some entry has no array form.
19049 let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
19050 loop {
19051 let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
19052 if !matches!(self.peek(), Token::LParen) {
19053 return Err(self.err(alloc::format!(
19054 "expected '(' after {fn_name} in ROWS FROM, got {:?}",
19055 self.peek()
19056 )));
19057 }
19058 self.advance();
19059 let mut fn_args: Vec<Expr> = Vec::new();
19060 if !matches!(self.peek(), Token::RParen) {
19061 loop {
19062 fn_args.push(self.parse_expr(0)?);
19063 if matches!(self.peek(), Token::Comma) {
19064 self.advance();
19065 continue;
19066 }
19067 break;
19068 }
19069 }
19070 if !matches!(self.peek(), Token::RParen) {
19071 return Err(self.err(alloc::format!(
19072 "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
19073 self.peek()
19074 )));
19075 }
19076 self.advance();
19077 let entry = match fn_name.as_str() {
19078 "unnest" => {
19079 if fn_args.len() != 1 {
19080 return Err(
19081 self.err("unnest inside ROWS FROM takes exactly one array".into())
19082 );
19083 }
19084 fn_args.pop().expect("len checked")
19085 }
19086 "jsonb_array_elements"
19087 | "json_array_elements"
19088 | "jsonb_array_elements_text"
19089 | "json_array_elements_text"
19090 | "jsonb_object_keys"
19091 | "json_object_keys"
19092 | "generate_subscripts" => crate::ast::Expr::FunctionCall {
19093 name: fn_name,
19094 args: fn_args,
19095 },
19096 "string_to_table" => crate::ast::Expr::FunctionCall {
19097 name: "string_to_array".to_string(),
19098 args: fn_args,
19099 },
19100 "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
19101 name: "regexp_split_to_array".to_string(),
19102 args: fn_args,
19103 },
19104 // v7.39 (read01 round 74) — an SRF with no array form
19105 // (`generate_series`, a user `RETURNS SETOF` function) has no
19106 // scalar expression to zip, so the WHOLE list switches to the
19107 // rows_from channel, which runs each function and zips the
19108 // rows themselves. The all-array case keeps the old lowering:
19109 // it is well-trodden and this must not disturb it.
19110 _ => {
19111 generic.push((fn_name, fn_args));
19112 if matches!(self.peek(), Token::Comma) {
19113 self.advance();
19114 continue;
19115 }
19116 break;
19117 }
19118 };
19119 generic.push((
19120 // The array-able entries carry their lowered expr along, so a
19121 // MIXED list still works: the engine sees the scalar array
19122 // form and unnests it.
19123 "__array".to_string(),
19124 alloc::vec![entry.clone()],
19125 ));
19126 entries.push(entry);
19127 if matches!(self.peek(), Token::Comma) {
19128 self.advance();
19129 continue;
19130 }
19131 break;
19132 }
19133 if !matches!(self.peek(), Token::RParen) {
19134 return Err(self.err(alloc::format!(
19135 "expected ')' to close ROWS FROM, got {:?}",
19136 self.peek()
19137 )));
19138 }
19139 self.advance();
19140 let with_ordinality = self.absorb_with_ordinality();
19141 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19142 let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19143 // v7.39 (read01 round 74) — some entry had no array form, so the whole
19144 // list rides the generic channel.
19145 if generic.iter().any(|(n, _)| n != "__array") {
19146 let correlated = generic
19147 .iter()
19148 .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19149 let tref = TableRef {
19150 name,
19151 alias: alias_ident,
19152 only: false,
19153 as_of_segment: None,
19154 unnest_expr: None,
19155 unnest_column_aliases,
19156 with_ordinality,
19157 generate_series_args: None,
19158 lateral_subquery: None,
19159 jsonb_each_text_arg: None,
19160 table_fn_call: None,
19161 rows_from: Some(generic),
19162 json_table: None,
19163 scalar_fn_item: false,
19164 };
19165 return Ok(if correlated {
19166 Self::wrap_correlated_srf(tref)
19167 } else {
19168 tref
19169 });
19170 }
19171 let correlated = entries.iter().any(Self::expr_has_any_column);
19172 let expr = if entries.len() == 1 {
19173 entries.pop().expect("len checked")
19174 } else {
19175 crate::ast::Expr::FunctionCall {
19176 name: "__unnest_zip".to_string(),
19177 args: entries,
19178 }
19179 };
19180 let tref = TableRef {
19181 name,
19182 alias: alias_ident,
19183 only: false,
19184 as_of_segment: None,
19185 unnest_expr: Some(Box::new(expr)),
19186 unnest_column_aliases,
19187 with_ordinality,
19188 generate_series_args: None,
19189 lateral_subquery: None,
19190 jsonb_each_text_arg: None,
19191 table_fn_call: None,
19192 rows_from: None,
19193 json_table: None,
19194 scalar_fn_item: false,
19195 };
19196 return Ok(if correlated {
19197 Self::wrap_correlated_srf(tref)
19198 } else {
19199 tref
19200 });
19201 }
19202 // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19203 // source. Detect at the head before the bare-ident fallback;
19204 // unnest is not a reserved token.
19205 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19206 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19207 {
19208 self.advance(); // unnest
19209 self.advance(); // (
19210 let mut srf_args = alloc::vec![self.parse_expr(0)?];
19211 while matches!(self.peek(), Token::Comma) {
19212 self.advance();
19213 srf_args.push(self.parse_expr(0)?);
19214 }
19215 if !matches!(self.peek(), Token::RParen) {
19216 return Err(self.err(alloc::format!(
19217 "expected ')' after unnest() argument, got {:?}",
19218 self.peek()
19219 )));
19220 }
19221 self.advance();
19222 // Multi-arg unnest(a, b, …) zips the arrays in
19223 // parallel, NULL-padding to the longest (PG's ROWS
19224 // FROM shorthand). Lower onto the unnest channel as an
19225 // internal marker call the executors unpack.
19226 let expr = if srf_args.len() == 1 {
19227 srf_args.pop().expect("len checked")
19228 } else {
19229 crate::ast::Expr::FunctionCall {
19230 name: "__unnest_zip".to_string(),
19231 args: srf_args,
19232 }
19233 };
19234 let with_ordinality = self.absorb_with_ordinality();
19235 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19236 let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19237 let correlated = Self::expr_has_any_column(&expr);
19238 let tref = TableRef {
19239 name,
19240 alias: alias_ident,
19241 only: false,
19242 as_of_segment: None,
19243 unnest_expr: Some(Box::new(expr)),
19244 unnest_column_aliases,
19245 with_ordinality,
19246 generate_series_args: None,
19247 lateral_subquery: None,
19248 jsonb_each_text_arg: None,
19249 table_fn_call: None,
19250 rows_from: None,
19251 json_table: None,
19252 scalar_fn_item: false,
19253 };
19254 return Ok(if correlated {
19255 Self::wrap_correlated_srf(tref)
19256 } else {
19257 tref
19258 });
19259 }
19260 // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19261 // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19262 // generic table-fn arg parser can't read), so it is intercepted
19263 // here BEFORE the generic dispatch. The doc expr may reference
19264 // outer columns (implicit LATERAL) — same correlated-wrap rule.
19265 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19266 if s.eq_ignore_ascii_case("json_table"))
19267 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19268 {
19269 let tref = self.parse_json_table_ref()?;
19270 let correlated = tref
19271 .json_table
19272 .as_deref()
19273 .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19274 return Ok(if correlated {
19275 Self::wrap_correlated_srf(tref)
19276 } else {
19277 tref
19278 });
19279 }
19280 // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19281 // functions dispatched by name (`pg_partition_tree('t')`,
19282 // `pg_partition_ancestors('t')`). Same head-detection shape as
19283 // unnest; the engine executor owns the row shape per function.
19284 // v7.39 (read01 round 65) — and a USER function in FROM position
19285 // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19286 // (generate_series / unnest / the json_each family) keep it — their arms
19287 // sit further down, so they are excluded here by name rather than by
19288 // ordering. Anything else that is an ident followed by `(` is a table
19289 // function; the engine executor decides whether it is a builtin, a
19290 // set-returning user function, or an error.
19291 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19292 if !s.eq_ignore_ascii_case("generate_series")
19293 && !s.eq_ignore_ascii_case("unnest")
19294 && !is_json_each_name(s))
19295 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19296 {
19297 // Body out-of-line — this parse sits on the FROM/subquery
19298 // recursion chain (debug frame-cliff discipline).
19299 // v7.39 (read01 round 69) — a call whose arguments reference an outer
19300 // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19301 // outer row, so it rides the lateral channel. Same rule the unnest
19302 // arm uses.
19303 let tref = self.parse_table_fn_ref()?;
19304 let correlated = tref
19305 .table_fn_call
19306 .as_deref()
19307 .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19308 return Ok(if correlated {
19309 Self::wrap_correlated_srf(tref)
19310 } else {
19311 tref
19312 });
19313 }
19314 // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19315 // [, step])` set-returning source. Same shape as unnest:
19316 // detect at the head, parse the comma-separated arg list,
19317 // dispatch downstream through the engine's set-returning
19318 // path. Supports integer triplets (mailrs's `WITH row_no AS
19319 // (SELECT * FROM generate_series(1, N))` pattern) and
19320 // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19321 // date-range iteration pattern, which pre-3.10 had no
19322 // direct equivalent in SPG).
19323 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19324 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19325 {
19326 self.advance(); // generate_series
19327 self.advance(); // (
19328 let mut args: Vec<Expr> = Vec::new();
19329 loop {
19330 args.push(self.parse_expr(0)?);
19331 if matches!(self.peek(), Token::Comma) {
19332 self.advance();
19333 continue;
19334 }
19335 break;
19336 }
19337 if !matches!(self.peek(), Token::RParen) {
19338 return Err(self.err(alloc::format!(
19339 "expected ')' after generate_series() arguments, got {:?}",
19340 self.peek()
19341 )));
19342 }
19343 self.advance();
19344 if args.len() < 2 || args.len() > 3 {
19345 return Err(self.err(alloc::format!(
19346 "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19347 args.len()
19348 )));
19349 }
19350 let with_ordinality = self.absorb_with_ordinality();
19351 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19352 let name = alias_ident
19353 .clone()
19354 .unwrap_or_else(|| "generate_series".to_string());
19355 let correlated = args.iter().any(Self::expr_has_any_column);
19356 let tref = TableRef {
19357 name,
19358 alias: alias_ident,
19359 only: false,
19360 as_of_segment: None,
19361 unnest_expr: None,
19362 unnest_column_aliases: column_aliases,
19363 with_ordinality,
19364 generate_series_args: Some(args),
19365 lateral_subquery: None,
19366 jsonb_each_text_arg: None,
19367 table_fn_call: None,
19368 rows_from: None,
19369 json_table: None,
19370 scalar_fn_item: false,
19371 };
19372 return Ok(if correlated {
19373 Self::wrap_correlated_srf(tref)
19374 } else {
19375 tref
19376 });
19377 }
19378 // v7.16.2 — preserve information_schema / pg_catalog
19379 // qualifiers (mailrs round-10 A.3). The generic
19380 // `expect_ident_like` strip silently drops the schema;
19381 // we want the engine to recognise these PG meta tables
19382 // and synthesise rows from the live catalog. Produce a
19383 // synthetic name (`__spg_info_columns` etc.) so the
19384 // engine's SELECT-side router can dispatch without
19385 // clashing with any user-defined `columns` table.
19386 let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
19387 (synth, Some(orig))
19388 } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
19389 (synth, Some(orig))
19390 } else {
19391 (self.expect_ident_like()?, None)
19392 };
19393 // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
19394 // time-travel clause. Parse BEFORE the alias so the
19395 // alias can still ride at the tail (`tbl AS OF SEGMENT
19396 // '5' alias`). `AS` is a reserved keyword token, while
19397 // `OF` and `SEGMENT` are bare idents.
19398 let as_of_segment = if matches!(self.peek(), Token::As)
19399 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
19400 {
19401 self.advance(); // AS
19402 self.advance(); // OF
19403 let kw = match self.peek().clone() {
19404 Token::Ident(s) | Token::QuotedIdent(s) => s,
19405 other => {
19406 return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
19407 }
19408 };
19409 if !kw.eq_ignore_ascii_case("segment") {
19410 return Err(self.err(format!(
19411 "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
19412 )));
19413 }
19414 self.advance();
19415 // Segment id literal — accept either a string or
19416 // integer for operator ergonomics.
19417 let id = match self.advance() {
19418 Token::String(s) => s
19419 .parse::<u32>()
19420 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19421 Token::Integer(n) => u32::try_from(n)
19422 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19423 other => {
19424 return Err(self.err(format!(
19425 "expected segment id literal after AS OF SEGMENT, got {other:?}"
19426 )));
19427 }
19428 };
19429 Some(id)
19430 } else {
19431 None
19432 };
19433 // TABLESAMPLE is not a reserved token — keep the bare-ident
19434 // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
19435 let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
19436 {
19437 None
19438 } else {
19439 self.parse_optional_alias()?
19440 };
19441 // r1052 — a catalog name rewritten to its synthetic form keeps
19442 // the WRITTEN name as the relation's alias, so `pg_cast.oid`
19443 // still binds after `pg_cast` became `__spg_pg_cast`. PG
19444 // semantics: the visible name of `pg_catalog.pg_cast` IS
19445 // `pg_cast`. Without this, every table-name-qualified column
19446 // on a synthesised catalog answered "missing FROM-clause
19447 // entry" — which is the wall pg_dump hit on its first
19448 // pg_proc/pg_cast query.
19449 let alias = match (&alias, &meta_original) {
19450 (None, Some(orig)) if *orig != name => Some(orig.clone()),
19451 _ => alias,
19452 };
19453 // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
19454 // (PG grammar). BERNOULLI lowers to a per-row
19455 // `random() < p/100` conjunct on the enclosing SELECT's
19456 // WHERE — exact row-level Bernoulli semantics. SYSTEM
19457 // shares the lowering: SPG has no page structure to
19458 // sample, and the row-level form returns the same expected
19459 // fraction. REPEATABLE(seed) promises a deterministic
19460 // sample SPG cannot honour yet — honest error rather than
19461 // a silently ignored seed.
19462 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
19463 self.advance();
19464 let method = self.expect_ident_like()?;
19465 if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
19466 return Err(self.err(alloc::format!(
19467 "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
19468 )));
19469 }
19470 if !matches!(self.peek(), Token::LParen) {
19471 return Err(self.err(alloc::format!(
19472 "expected '(' after TABLESAMPLE {}, got {:?}",
19473 method.to_ascii_uppercase(),
19474 self.peek()
19475 )));
19476 }
19477 self.advance();
19478 let percent = self.parse_expr(0)?;
19479 if !matches!(self.peek(), Token::RParen) {
19480 return Err(self.err(alloc::format!(
19481 "expected ')' after TABLESAMPLE percentage, got {:?}",
19482 self.peek()
19483 )));
19484 }
19485 self.advance();
19486 // REPEATABLE(seed) → a deterministic per-row draw seeded by
19487 // `seed`, so the sample is stable across repeats and rescans.
19488 // Non-REPEATABLE keeps the non-deterministic `random()` draw.
19489 let mut sample_seed: Option<Expr> = None;
19490 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
19491 self.advance();
19492 if !matches!(self.peek(), Token::LParen) {
19493 return Err(self.err(alloc::format!(
19494 "expected '(' after REPEATABLE, got {:?}",
19495 self.peek()
19496 )));
19497 }
19498 self.advance();
19499 let seed = self.parse_expr(0)?;
19500 if !matches!(self.peek(), Token::RParen) {
19501 return Err(self.err(alloc::format!(
19502 "expected ')' after REPEATABLE seed, got {:?}",
19503 self.peek()
19504 )));
19505 }
19506 self.advance();
19507 sample_seed = Some(seed);
19508 }
19509 let draw = match sample_seed {
19510 Some(seed) => Expr::FunctionCall {
19511 name: "__tsm_fract".to_string(),
19512 args: alloc::vec![seed],
19513 },
19514 None => Expr::FunctionCall {
19515 name: "random".to_string(),
19516 args: Vec::new(),
19517 },
19518 };
19519 self.pending_sample_preds.push(Expr::Binary {
19520 lhs: Box::new(draw),
19521 op: crate::ast::BinOp::Lt,
19522 rhs: Box::new(Expr::Binary {
19523 lhs: Box::new(percent),
19524 op: crate::ast::BinOp::Div,
19525 rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
19526 }),
19527 });
19528 }
19529 Ok(TableRef {
19530 name,
19531 alias,
19532 only,
19533 as_of_segment,
19534 unnest_expr: None,
19535 unnest_column_aliases: Vec::new(),
19536 with_ordinality: false,
19537 generate_series_args: None,
19538 lateral_subquery: None,
19539 jsonb_each_text_arg: None,
19540 table_fn_call: None,
19541 rows_from: None,
19542 json_table: None,
19543 scalar_fn_item: false,
19544 })
19545 }
19546
19547 /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
19548 /// but also accepts `AS alias(col [, col, …])` — the
19549 /// PG-standard table-function column-list form. The column
19550 /// list is only honoured when paired with `UNNEST(...)` in
19551 /// the parent; other call sites currently discard it.
19552 /// True when the expression tree contains a qualified column
19553 /// reference (`t.col`) — the syntactic marker that an SRF
19554 /// argument correlates with a preceding FROM item.
19555 fn expr_has_qualified_column(e: &Expr) -> bool {
19556 match e {
19557 Expr::Column(c) => c.qualifier.is_some(),
19558 Expr::Binary { lhs, rhs, .. } => {
19559 Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
19560 }
19561 Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
19562 Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
19563 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
19564 Expr::Case {
19565 operand,
19566 branches,
19567 else_branch,
19568 } => {
19569 operand
19570 .as_deref()
19571 .is_some_and(Self::expr_has_qualified_column)
19572 || branches.iter().any(|(w, t)| {
19573 Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
19574 })
19575 || else_branch
19576 .as_deref()
19577 .is_some_and(Self::expr_has_qualified_column)
19578 }
19579 _ => false,
19580 }
19581 }
19582
19583 /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
19584 /// counts a bare (unqualified) column. A set-returning function has no
19585 /// input columns of its own, so ANY column in its arguments is an outer
19586 /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
19587 fn expr_has_any_column(e: &Expr) -> bool {
19588 match e {
19589 Expr::Column(_) => true,
19590 Expr::Binary { lhs, rhs, .. } => {
19591 Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
19592 }
19593 Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
19594 Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
19595 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
19596 // v7.39 (round 759, F31-B8b) — a column INSIDE an array
19597 // constructor or subscript fell to the `_ => false` arm, so
19598 // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
19599 // channel and the eager peer eval answered `column "x" does
19600 // not exist` (the substitution walker already recurses both
19601 // shapes; only this detector was blind to them).
19602 Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
19603 Expr::ArraySubscript { target, index } => {
19604 Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
19605 }
19606 Expr::Case {
19607 operand,
19608 branches,
19609 else_branch,
19610 } => {
19611 operand.as_deref().is_some_and(Self::expr_has_any_column)
19612 || branches
19613 .iter()
19614 .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
19615 || else_branch
19616 .as_deref()
19617 .is_some_and(Self::expr_has_any_column)
19618 }
19619 _ => false,
19620 }
19621 }
19622
19623 /// Wrap a correlated SRF table ref (`unnest(t.col)` /
19624 /// `generate_series(1, t.n)`) into the lateral_subquery
19625 /// channel: `SELECT * FROM <srf>` executes per outer row with
19626 /// outer references substituted (v7.37.43-T4.5 machinery).
19627 /// Uncorrelated SRFs stay on their plain channels.
19628 fn wrap_correlated_srf(srf: TableRef) -> TableRef {
19629 let name = srf.name.clone();
19630 let alias = srf.alias.clone();
19631 let inner = crate::ast::SelectStatement {
19632 locking: None,
19633 ctes: Vec::new(),
19634 distinct: false,
19635 distinct_on: Vec::new(),
19636 items: alloc::vec![crate::ast::SelectItem::Wildcard],
19637 from: Some(crate::ast::FromClause {
19638 primary: srf,
19639 joins: Vec::new(),
19640 }),
19641 where_: None,
19642 group_by: None,
19643 group_by_all: false,
19644 having: None,
19645 unions: Vec::new(),
19646 order_by: Vec::new(),
19647 limit: None,
19648 offset: None,
19649 limit_with_ties: false,
19650 window_check_exprs: Vec::new(),
19651 };
19652 TableRef {
19653 name,
19654 alias,
19655 only: false,
19656 as_of_segment: None,
19657 unnest_expr: None,
19658 unnest_column_aliases: Vec::new(),
19659 with_ordinality: false,
19660 generate_series_args: None,
19661 lateral_subquery: Some(Box::new(inner)),
19662 jsonb_each_text_arg: None,
19663 table_fn_call: None,
19664 rows_from: None,
19665 json_table: None,
19666 scalar_fn_item: false,
19667 }
19668 }
19669
19670 /// True when the expression tree contains an unresolved
19671 /// `OVER w` marker (see parse_over_clause).
19672 fn expr_has_named_window(e: &Expr) -> bool {
19673 match e {
19674 Expr::WindowFunction { partition_by, .. } => matches!(
19675 partition_by.as_slice(),
19676 [Expr::Column(c)] if matches!(
19677 c.qualifier.as_deref(),
19678 Some("__named_window__") | Some("__named_window_ref__")
19679 )
19680 ),
19681 Expr::Binary { lhs, rhs, .. } => {
19682 Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
19683 }
19684 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
19685 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
19686 Expr::Case {
19687 operand,
19688 branches,
19689 else_branch,
19690 } => {
19691 operand.as_deref().is_some_and(Self::expr_has_named_window)
19692 || branches.iter().any(|(w, t)| {
19693 Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
19694 })
19695 || else_branch
19696 .as_deref()
19697 .is_some_and(Self::expr_has_named_window)
19698 }
19699 _ => false,
19700 }
19701 }
19702
19703 /// v7.39 (round 705) — the NAMES the expression references through the
19704 /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
19705 /// definitions nothing referenced. Traversal mirrors
19706 /// `expr_has_named_window` above.
19707 fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
19708 match e {
19709 Expr::WindowFunction { partition_by, .. } => {
19710 if let [Expr::Column(c)] = partition_by.as_slice()
19711 && matches!(
19712 c.qualifier.as_deref(),
19713 Some("__named_window__") | Some("__named_window_ref__")
19714 )
19715 {
19716 into.push(c.name.clone());
19717 }
19718 }
19719 Expr::Binary { lhs, rhs, .. } => {
19720 Self::collect_named_window_refs(lhs, into);
19721 Self::collect_named_window_refs(rhs, into);
19722 }
19723 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19724 Self::collect_named_window_refs(expr, into);
19725 }
19726 Expr::FunctionCall { args, .. } => {
19727 for a in args {
19728 Self::collect_named_window_refs(a, into);
19729 }
19730 }
19731 Expr::Case {
19732 operand,
19733 branches,
19734 else_branch,
19735 } => {
19736 if let Some(o) = operand.as_deref() {
19737 Self::collect_named_window_refs(o, into);
19738 }
19739 for (w, t) in branches {
19740 Self::collect_named_window_refs(w, into);
19741 Self::collect_named_window_refs(t, into);
19742 }
19743 if let Some(eb) = else_branch.as_deref() {
19744 Self::collect_named_window_refs(eb, into);
19745 }
19746 }
19747 _ => {}
19748 }
19749 }
19750
19751 /// Inline named-window definitions into the `OVER w` markers.
19752 /// An unknown name errors (PG: window "w" does not exist).
19753 #[allow(clippy::type_complexity)]
19754 fn substitute_named_windows(
19755 e: &mut Expr,
19756 defs: &[(
19757 String,
19758 (
19759 Vec<Expr>,
19760 Vec<(Expr, bool, Option<bool>)>,
19761 Option<WindowFrame>,
19762 ),
19763 )],
19764 ) -> Result<(), String> {
19765 match e {
19766 Expr::WindowFunction {
19767 partition_by,
19768 order_by,
19769 frame,
19770 ..
19771 } => {
19772 // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
19773 // from the bare `OVER w1` (a plain reference).
19774 let named = match partition_by.as_slice() {
19775 [Expr::Column(c)] => match c.qualifier.as_deref() {
19776 Some("__named_window__") => Some((c.name.clone(), false)),
19777 Some("__named_window_ref__") => Some((c.name.clone(), true)),
19778 _ => None,
19779 },
19780 _ => None,
19781 };
19782 if let Some((wname, is_copy)) = named {
19783 let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
19784 else {
19785 return Err(alloc::format!("window {wname:?} does not exist"));
19786 };
19787 if !is_copy {
19788 *partition_by = def.0.clone();
19789 *order_by = def.1.clone();
19790 *frame = def.2.clone();
19791 return Ok(());
19792 }
19793 // v7.39 (round 229) — PG's copy rules, probed against
19794 // 18.4: a copy inherits the partitioning, may supply an
19795 // ordering only when the base has none, and may not copy
19796 // a base that already carries a frame (its own frame
19797 // would be ambiguous with the inherited one).
19798 if !def.1.is_empty() && !order_by.is_empty() {
19799 return Err(alloc::format!(
19800 "cannot override ORDER BY clause of window \"{wname}\""
19801 ));
19802 }
19803 if def.2.is_some() {
19804 return Err(alloc::format!(
19805 "cannot copy window \"{wname}\" because it has a frame clause"
19806 ));
19807 }
19808 *partition_by = def.0.clone();
19809 if order_by.is_empty() {
19810 *order_by = def.1.clone();
19811 }
19812 }
19813 Ok(())
19814 }
19815 Expr::Binary { lhs, rhs, .. } => {
19816 Self::substitute_named_windows(lhs, defs)?;
19817 Self::substitute_named_windows(rhs, defs)
19818 }
19819 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19820 Self::substitute_named_windows(expr, defs)
19821 }
19822 Expr::FunctionCall { args, .. } => {
19823 for a in args {
19824 Self::substitute_named_windows(a, defs)?;
19825 }
19826 Ok(())
19827 }
19828 Expr::Case {
19829 operand,
19830 branches,
19831 else_branch,
19832 } => {
19833 if let Some(op) = operand {
19834 Self::substitute_named_windows(op, defs)?;
19835 }
19836 for (w, t) in branches {
19837 Self::substitute_named_windows(w, defs)?;
19838 Self::substitute_named_windows(t, defs)?;
19839 }
19840 if let Some(el) = else_branch {
19841 Self::substitute_named_windows(el, defs)?;
19842 }
19843 Ok(())
19844 }
19845 _ => Ok(()),
19846 }
19847 }
19848
19849 /// SQL-standard `TABLE name` shorthand — builds the equivalent
19850 /// `SELECT * FROM name` head. Callers own set-op chain / tail
19851 /// composition.
19852 fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
19853 debug_assert!(matches!(self.peek(), Token::Table));
19854 self.advance(); // TABLE
19855 let tname = self.expect_ident_like()?;
19856 Ok(SelectStatement {
19857 locking: None,
19858 ctes: Vec::new(),
19859 distinct: false,
19860 distinct_on: Vec::new(),
19861 items: alloc::vec![SelectItem::Wildcard],
19862 from: Some(FromClause {
19863 primary: TableRef {
19864 name: tname,
19865 alias: None,
19866 only: false,
19867 as_of_segment: None,
19868 unnest_expr: None,
19869 unnest_column_aliases: Vec::new(),
19870 with_ordinality: false,
19871 generate_series_args: None,
19872 lateral_subquery: None,
19873 jsonb_each_text_arg: None,
19874 table_fn_call: None,
19875 rows_from: None,
19876 json_table: None,
19877 scalar_fn_item: false,
19878 },
19879 joins: Vec::new(),
19880 }),
19881 where_: None,
19882 group_by: None,
19883 group_by_all: false,
19884 having: None,
19885 unions: Vec::new(),
19886 order_by: Vec::new(),
19887 limit: None,
19888 offset: None,
19889 limit_with_ties: false,
19890 window_check_exprs: Vec::new(),
19891 })
19892 }
19893
19894 /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
19895 /// variants) → a derived table that reads each declared column out of
19896 /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
19897 /// `jsonb_array_elements(J)` (one row per element, column `value`);
19898 /// the scalar *record form projects a single row straight off `J`.
19899 /// Rides the existing lateral-subquery channel, so no new executor or
19900 /// AST is needed.
19901 fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
19902 use crate::ast::{
19903 BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
19904 };
19905 let fn_name = match self.peek() {
19906 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19907 _ => unreachable!("caller guarded is_json_to_record_name"),
19908 };
19909 self.advance(); // fn name
19910 self.advance(); // (
19911 let mut arg = self.parse_expr(0)?;
19912 // populate_record(base, json): the base only carries the record
19913 // type here — the JSON argument is the second expression.
19914 let mut base: Option<Expr> = None;
19915 if matches!(self.peek(), Token::Comma) {
19916 self.advance();
19917 base = Some(arg);
19918 arg = self.parse_expr(0)?;
19919 }
19920 if !matches!(self.peek(), Token::RParen) {
19921 return Err(self.err(alloc::format!(
19922 "expected ')' after {fn_name}() argument, got {:?}",
19923 self.peek()
19924 )));
19925 }
19926 self.advance(); // )
19927 let is_set = fn_name.ends_with("recordset");
19928 // `[AS] alias ( col type [, …] )` column-definition list.
19929 if matches!(self.peek(), Token::As) {
19930 self.advance();
19931 }
19932 let alias_opt = match self.peek() {
19933 Token::Ident(s) | Token::QuotedIdent(s) => {
19934 let a = s.clone();
19935 self.advance();
19936 Some(a)
19937 }
19938 _ => None,
19939 };
19940 // v7.39 (read01 round 76) — the populate family's canonical PG
19941 // spelling carries no column list at all: the row shape comes from
19942 // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
19943 // j)`). The parser has no catalog, so hand the two arguments to the
19944 // engine's table-function channel, which does. Only `*_to_record*`
19945 // (whose base is bare `record`) genuinely requires the list.
19946 if !matches!(self.peek(), Token::LParen) {
19947 if let Some(base_expr) = base {
19948 let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
19949 return Ok(TableRef {
19950 name: alias.clone(),
19951 alias: Some(alias),
19952 only: false,
19953 as_of_segment: None,
19954 unnest_expr: None,
19955 unnest_column_aliases: Vec::new(),
19956 with_ordinality: false,
19957 generate_series_args: None,
19958 lateral_subquery: None,
19959 jsonb_each_text_arg: None,
19960 table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
19961 rows_from: None,
19962 json_table: None,
19963 scalar_fn_item: false,
19964 });
19965 }
19966 return Err(self.err(alloc::format!(
19967 "expected '(' to start the {fn_name} column-definition list, got {:?}",
19968 self.peek()
19969 )));
19970 }
19971 let Some(alias) = alias_opt else {
19972 return Err(self.err(alloc::format!(
19973 "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
19974 )));
19975 };
19976 self.advance(); // (
19977 let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
19978 loop {
19979 let col = self.expect_ident_like()?;
19980 let ty = self.parse_cast_target()?;
19981 coldefs.push((col, ty));
19982 if matches!(self.peek(), Token::Comma) {
19983 self.advance();
19984 continue;
19985 }
19986 if matches!(self.peek(), Token::RParen) {
19987 self.advance();
19988 break;
19989 }
19990 return Err(self.err(alloc::format!(
19991 "expected ',' or ')' in {fn_name} column list, got {:?}",
19992 self.peek()
19993 )));
19994 }
19995 if coldefs.is_empty() {
19996 return Err(self.err(alloc::format!(
19997 "{fn_name} column-definition list must declare at least one column"
19998 )));
19999 }
20000 // Per column: (base ->> 'col')::type AS col. The base is the
20001 // per-element `value` column for the *set form, or the argument
20002 // itself for the scalar record form.
20003 let items: Vec<SelectItem> = coldefs
20004 .into_iter()
20005 .map(|(col, ty)| {
20006 let base = if is_set {
20007 Expr::Column(ColumnName {
20008 qualifier: None,
20009 name: "value".to_string(),
20010 })
20011 } else {
20012 arg.clone()
20013 };
20014 SelectItem::Expr {
20015 expr: Expr::Cast {
20016 expr: Box::new(Expr::Binary {
20017 lhs: Box::new(base),
20018 op: BinOp::JsonGetText,
20019 rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
20020 }),
20021 target: ty,
20022 },
20023 alias: Some(col),
20024 }
20025 })
20026 .collect();
20027 let from = if is_set {
20028 let elem_fn = if fn_name.starts_with("jsonb") {
20029 "jsonb_array_elements"
20030 } else {
20031 "json_array_elements"
20032 };
20033 Some(FromClause {
20034 primary: TableRef {
20035 name: "value".to_string(),
20036 alias: None,
20037 only: false,
20038 as_of_segment: None,
20039 unnest_expr: Some(Box::new(Expr::FunctionCall {
20040 name: elem_fn.to_string(),
20041 args: alloc::vec![arg],
20042 })),
20043 unnest_column_aliases: alloc::vec!["value".to_string()],
20044 with_ordinality: false,
20045 generate_series_args: None,
20046 lateral_subquery: None,
20047 jsonb_each_text_arg: None,
20048 table_fn_call: None,
20049 rows_from: None,
20050 json_table: None,
20051 scalar_fn_item: false,
20052 },
20053 joins: Vec::new(),
20054 })
20055 } else {
20056 None
20057 };
20058 let inner = SelectStatement {
20059 locking: None,
20060 ctes: Vec::new(),
20061 distinct: false,
20062 distinct_on: Vec::new(),
20063 items,
20064 from,
20065 where_: None,
20066 group_by: None,
20067 group_by_all: false,
20068 having: None,
20069 unions: Vec::new(),
20070 order_by: Vec::new(),
20071 limit: None,
20072 offset: None,
20073 limit_with_ties: false,
20074 window_check_exprs: Vec::new(),
20075 };
20076 Ok(TableRef {
20077 name: alias.clone(),
20078 alias: Some(alias),
20079 only: false,
20080 as_of_segment: None,
20081 unnest_expr: None,
20082 unnest_column_aliases: Vec::new(),
20083 with_ordinality: false,
20084 generate_series_args: None,
20085 lateral_subquery: Some(Box::new(inner)),
20086 jsonb_each_text_arg: None,
20087 table_fn_call: None,
20088 rows_from: None,
20089 json_table: None,
20090 scalar_fn_item: false,
20091 })
20092 }
20093
20094 /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
20095 /// Returns true when the clause was present. `WITH` alone (a
20096 /// CTE can never start here) is not enough — the ORDINALITY
20097 /// ident must follow, so a stray WITH still errors downstream.
20098 fn absorb_with_ordinality(&mut self) -> bool {
20099 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
20100 && matches!(self.tokens.get(self.pos + 1),
20101 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
20102 {
20103 self.advance();
20104 self.advance();
20105 true
20106 } else {
20107 false
20108 }
20109 }
20110
20111 /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
20112 /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
20113 /// Out-of-line: the caller sits on the FROM recursion chain.
20114 #[inline(never)]
20115 fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
20116 let fn_name = match self.advance() {
20117 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20118 _ => unreachable!("caller peeked an ident"),
20119 };
20120 self.advance(); // (
20121 let mut args: Vec<Expr> = Vec::new();
20122 if !matches!(self.peek(), Token::RParen) {
20123 loop {
20124 args.push(self.parse_expr(0)?);
20125 if matches!(self.peek(), Token::Comma) {
20126 self.advance();
20127 continue;
20128 }
20129 break;
20130 }
20131 }
20132 if !matches!(self.peek(), Token::RParen) {
20133 return Err(self.err(alloc::format!(
20134 "expected ')' after {fn_name}() arguments, got {:?}",
20135 self.peek()
20136 )));
20137 }
20138 self.advance();
20139 // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20140 // counter column rides after the function's own, and the alias list
20141 // names it.
20142 let with_ordinality = self.absorb_with_ordinality();
20143 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20144 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20145 Ok(TableRef {
20146 name,
20147 alias: alias_ident,
20148 only: false,
20149 as_of_segment: None,
20150 unnest_expr: None,
20151 unnest_column_aliases,
20152 with_ordinality,
20153 generate_series_args: None,
20154 lateral_subquery: None,
20155 jsonb_each_text_arg: None,
20156 table_fn_call: Some(Box::new((fn_name, args))),
20157 rows_from: None,
20158 json_table: None,
20159 scalar_fn_item: false,
20160 })
20161 }
20162
20163 /// v7.39 (round 205, JSON_TABLE) — parse
20164 /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20165 /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20166 /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20167 #[inline(never)]
20168 fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20169 self.advance(); // json_table
20170 self.advance(); // (
20171 let doc = Box::new(self.parse_expr(0)?);
20172 self.expect_comma_json_table()?;
20173 let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20174 // Optional `PASSING <expr> AS <name> [, …]`.
20175 let mut passing: Vec<(String, Expr)> = Vec::new();
20176 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20177 self.advance();
20178 loop {
20179 let e = self.parse_expr(0)?;
20180 if !matches!(self.peek(), Token::As) {
20181 return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20182 }
20183 self.advance();
20184 let vname = match self.advance() {
20185 Token::Ident(s) | Token::QuotedIdent(s) => s,
20186 other => {
20187 return Err(self.err(alloc::format!(
20188 "expected PASSING variable name, got {other:?}"
20189 )));
20190 }
20191 };
20192 passing.push((vname, e));
20193 if matches!(self.peek(), Token::Comma) {
20194 self.advance();
20195 continue;
20196 }
20197 break;
20198 }
20199 }
20200 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20201 return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20202 }
20203 self.advance();
20204 let columns = self.parse_json_table_columns()?;
20205 if !matches!(self.peek(), Token::RParen) {
20206 return Err(self.err(alloc::format!(
20207 "expected ')' to close JSON_TABLE, got {:?}",
20208 self.peek()
20209 )));
20210 }
20211 self.advance();
20212 let alias_ident = self.parse_optional_alias()?;
20213 let name = alias_ident
20214 .clone()
20215 .unwrap_or_else(|| String::from("json_table"));
20216 Ok(TableRef {
20217 name,
20218 alias: alias_ident,
20219 only: false,
20220 as_of_segment: None,
20221 unnest_expr: None,
20222 unnest_column_aliases: Vec::new(),
20223 with_ordinality: false,
20224 generate_series_args: None,
20225 lateral_subquery: None,
20226 jsonb_each_text_arg: None,
20227 table_fn_call: None,
20228 rows_from: None,
20229 json_table: Some(Box::new(crate::ast::JsonTable {
20230 doc,
20231 row_path,
20232 columns,
20233 passing,
20234 })),
20235 scalar_fn_item: false,
20236 })
20237 }
20238
20239 fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20240 if !matches!(self.peek(), Token::Comma) {
20241 return Err(self.err(alloc::format!(
20242 "expected ',' after JSON_TABLE document, got {:?}",
20243 self.peek()
20244 )));
20245 }
20246 self.advance();
20247 Ok(())
20248 }
20249
20250 fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20251 match self.advance() {
20252 Token::String(s) => Ok(s),
20253 other => Err(self.err(alloc::format!(
20254 "expected {what} string literal, got {other:?}"
20255 ))),
20256 }
20257 }
20258
20259 /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20260 #[inline(never)]
20261 fn parse_json_table_columns(
20262 &mut self,
20263 ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20264 if !matches!(self.peek(), Token::LParen) {
20265 return Err(self.err("expected '(' after COLUMNS".into()));
20266 }
20267 self.advance();
20268 let mut cols = Vec::new();
20269 loop {
20270 cols.push(self.parse_json_table_one_column()?);
20271 if matches!(self.peek(), Token::Comma) {
20272 self.advance();
20273 continue;
20274 }
20275 break;
20276 }
20277 if !matches!(self.peek(), Token::RParen) {
20278 return Err(self.err(alloc::format!(
20279 "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20280 self.peek()
20281 )));
20282 }
20283 self.advance();
20284 Ok(cols)
20285 }
20286
20287 fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20288 use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20289 // NESTED [PATH] '<p>' COLUMNS (...)
20290 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20291 self.advance();
20292 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20293 self.advance();
20294 }
20295 let path = self.parse_json_string_literal("NESTED PATH")?;
20296 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20297 return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20298 }
20299 self.advance();
20300 let columns = self.parse_json_table_columns()?;
20301 return Ok(JsonTableColumn::Nested { path, columns });
20302 }
20303 // <name> ...
20304 let name = match self.advance() {
20305 Token::Ident(s) | Token::QuotedIdent(s) => s,
20306 other => {
20307 return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20308 }
20309 };
20310 // <name> FOR ORDINALITY
20311 if matches!(self.peek(), Token::For) {
20312 self.advance();
20313 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20314 return Err(self.err("expected ORDINALITY after FOR".into()));
20315 }
20316 self.advance();
20317 return Ok(JsonTableColumn::Ordinality { name });
20318 }
20319 // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20320 let ty = self.parse_column_type_name()?;
20321 let mut format_json = false;
20322 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20323 self.advance();
20324 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20325 return Err(self.err("expected JSON after FORMAT".into()));
20326 }
20327 self.advance();
20328 format_json = true;
20329 }
20330 let mut exists = false;
20331 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20332 self.advance();
20333 exists = true;
20334 }
20335 let mut path = alloc::format!("$.{name}");
20336 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20337 self.advance();
20338 path = self.parse_json_string_literal("column PATH")?;
20339 }
20340 if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20341 // `FORMAT JSON` after PATH (alternate placement).
20342 self.advance();
20343 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20344 self.advance();
20345 }
20346 format_json = true;
20347 }
20348 let mut wrapper = false;
20349 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20350 self.advance();
20351 // optional CONDITIONAL/UNCONDITIONAL
20352 if matches!(self.peek(), Token::Ident(s)
20353 if s.eq_ignore_ascii_case("unconditional")
20354 || s.eq_ignore_ascii_case("conditional"))
20355 {
20356 self.advance();
20357 }
20358 if !matches!(self.peek(), Token::Ident(s)
20359 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20360 {
20361 return Err(self.err("expected WRAPPER after WITH".into()));
20362 }
20363 self.advance();
20364 // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20365 if matches!(self.peek(), Token::Ident(s)
20366 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20367 {
20368 self.advance();
20369 }
20370 wrapper = true;
20371 }
20372 // ON EMPTY / ON ERROR clauses (two, in any order).
20373 let mut on_empty = JsonTableOnBehavior::Null;
20374 let mut on_error = JsonTableOnBehavior::Null;
20375 for _ in 0..2 {
20376 let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20377 {
20378 self.advance();
20379 Some(JsonTableOnBehavior::Error)
20380 } else if matches!(self.peek(), Token::Null) {
20381 self.advance();
20382 Some(JsonTableOnBehavior::Null)
20383 } else if matches!(self.peek(), Token::Default) {
20384 self.advance();
20385 Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
20386 } else {
20387 None
20388 };
20389 let Some(behavior) = behavior else { break };
20390 // `ON {EMPTY|ERROR}`
20391 if !matches!(self.peek(), Token::On) {
20392 return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
20393 }
20394 self.advance();
20395 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
20396 self.advance();
20397 on_empty = behavior;
20398 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
20399 self.advance();
20400 on_error = behavior;
20401 } else {
20402 return Err(self.err("expected EMPTY or ERROR after ON".into()));
20403 }
20404 }
20405 Ok(JsonTableColumn::Regular {
20406 name,
20407 ty,
20408 path,
20409 exists,
20410 format_json,
20411 wrapper,
20412 on_empty,
20413 on_error,
20414 })
20415 }
20416
20417 fn parse_optional_alias_with_columns(
20418 &mut self,
20419 ) -> Result<(Option<String>, Vec<String>), ParseError> {
20420 let alias = self.parse_optional_alias()?;
20421 if alias.is_none() {
20422 return Ok((None, Vec::new()));
20423 }
20424 let mut cols: Vec<String> = Vec::new();
20425 if matches!(self.peek(), Token::LParen) {
20426 self.advance();
20427 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
20428 self.advance();
20429 cols.push(s);
20430 if matches!(self.peek(), Token::Comma) {
20431 self.advance();
20432 continue;
20433 }
20434 break;
20435 }
20436 if matches!(self.peek(), Token::RParen) {
20437 self.advance();
20438 }
20439 }
20440 Ok((alias, cols))
20441 }
20442
20443 /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
20444 /// whose keyword token was already consumed and whose `(` is the
20445 /// current token. Factored out of `parse_atom` (and marked
20446 /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
20447 /// recursive `parse_atom` frame — inlining them there enlarges the
20448 /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
20449 /// against, risking an overflow before the budget triggers.
20450 #[inline(never)]
20451 fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
20452 self.advance(); // (
20453 let mut args = Vec::new();
20454 if !matches!(self.peek(), Token::RParen) {
20455 loop {
20456 args.push(self.parse_expr(0)?);
20457 match self.peek() {
20458 Token::Comma => {
20459 self.advance();
20460 }
20461 Token::RParen => break,
20462 other => {
20463 return Err(self.err(alloc::format!(
20464 "expected ',' or ')' in {name}() args, got {other:?}"
20465 )));
20466 }
20467 }
20468 }
20469 }
20470 self.advance(); // )
20471 Ok(Expr::FunctionCall {
20472 name: name.into(),
20473 args,
20474 })
20475 }
20476
20477 /// FROM-clause: a primary table reference plus zero-or-more joined
20478 /// peers expressed via either `, <table>` (cross-product, no ON) or
20479 /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
20480 /// v1.10 keeps the join list flat (left-associative nested-loop
20481 /// semantics).
20482 fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
20483 let primary = self.parse_table_ref()?;
20484 let primary_qual = primary
20485 .alias
20486 .clone()
20487 .unwrap_or_else(|| primary.name.clone());
20488 let joins = self.parse_from_joins(&primary_qual)?;
20489 Ok(FromClause { primary, joins })
20490 }
20491
20492 /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
20493 /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
20494 /// SAME grammar after its target table has already been consumed.
20495 /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
20496 /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
20497 /// be parsed forward, once.)
20498 /// `left_primary_qual` is the qualifier (alias, else name) of whatever
20499 /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
20500 /// target in the MySQL multi-table form. It only feeds the `USING (…)`
20501 /// desugaring, which needs a name for the left side of each equality.
20502 fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
20503 let mut joins = Vec::new();
20504 loop {
20505 // `, <table>` — cross-product with no ON.
20506 if matches!(self.peek(), Token::Comma) {
20507 self.advance();
20508 let table = self.parse_table_ref()?;
20509 joins.push(FromJoin {
20510 kind: JoinKind::Cross,
20511 table,
20512 on: None,
20513 using_cols: None,
20514 natural: false,
20515 });
20516 continue;
20517 }
20518 // v7.37.16 — optional leading `NATURAL` before the join
20519 // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
20520 // not a lexer keyword (it arrives as a bare Ident), so match
20521 // it case-insensitively here. When present, no ON/USING
20522 // clause is allowed — the common columns are resolved at
20523 // execution time.
20524 let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
20525 if natural {
20526 self.advance();
20527 }
20528 // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
20529 // CROSS JOIN, and bare JOIN (defaults to INNER).
20530 let kind =
20531 match self.peek() {
20532 Token::Inner => {
20533 self.advance();
20534 if !matches!(self.peek(), Token::Join) {
20535 return Err(self
20536 .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
20537 }
20538 self.advance();
20539 JoinKind::Inner
20540 }
20541 Token::Left => {
20542 self.advance();
20543 if matches!(self.peek(), Token::Outer) {
20544 self.advance();
20545 }
20546 if !matches!(self.peek(), Token::Join) {
20547 return Err(self.err(format!(
20548 "expected JOIN after LEFT [OUTER], got {:?}",
20549 self.peek()
20550 )));
20551 }
20552 self.advance();
20553 JoinKind::Left
20554 }
20555 Token::Cross => {
20556 self.advance();
20557 if !matches!(self.peek(), Token::Join) {
20558 return Err(self
20559 .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
20560 }
20561 self.advance();
20562 JoinKind::Cross
20563 }
20564 // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
20565 Token::Right => {
20566 self.advance();
20567 if matches!(self.peek(), Token::Outer) {
20568 self.advance();
20569 }
20570 if !matches!(self.peek(), Token::Join) {
20571 return Err(self.err(format!(
20572 "expected JOIN after RIGHT [OUTER], got {:?}",
20573 self.peek()
20574 )));
20575 }
20576 self.advance();
20577 JoinKind::Right
20578 }
20579 // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
20580 Token::Full => {
20581 self.advance();
20582 if matches!(self.peek(), Token::Outer) {
20583 self.advance();
20584 }
20585 if !matches!(self.peek(), Token::Join) {
20586 return Err(self.err(format!(
20587 "expected JOIN after FULL [OUTER], got {:?}",
20588 self.peek()
20589 )));
20590 }
20591 self.advance();
20592 JoinKind::FullOuter
20593 }
20594 Token::Join => {
20595 self.advance();
20596 JoinKind::Inner
20597 }
20598 _ => break,
20599 };
20600 let table = self.parse_table_ref()?;
20601 // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
20602 // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
20603 // where prev_table is the most-recent left-side table
20604 // (the previous join's table if any, else the FROM primary).
20605 // PG semantics around column merging are richer (USING'd
20606 // cols become deduplicated single output columns); for
20607 // sugar purposes the predicate-only form covers the
20608 // baseline corpus shape and chained `… JOIN x USING (k)
20609 // JOIN y USING (k)` calls.
20610 // v7.37.16 — NATURAL joins carry no ON/USING clause; the
20611 // common columns resolve at execution time.
20612 if natural {
20613 joins.push(FromJoin {
20614 kind,
20615 table,
20616 on: None,
20617 using_cols: None,
20618 natural: true,
20619 });
20620 continue;
20621 }
20622 let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
20623 // v7.37.16 — capture the USING column list (in addition to
20624 // the ON desugar below) so the executor can perform PG's
20625 // column-merge on the output side.
20626 let mut using_cols: Option<Vec<String>> = None;
20627 let on = if matches!(self.peek(), Token::On) {
20628 self.advance();
20629 Some(self.parse_expr(0)?)
20630 } else if using_match {
20631 self.advance();
20632 if !matches!(self.peek(), Token::LParen) {
20633 return Err(
20634 self.err(format!("expected '(' after USING, got {:?}", self.peek()))
20635 );
20636 }
20637 self.advance();
20638 let mut cols: Vec<String> = Vec::new();
20639 loop {
20640 match self.peek().clone() {
20641 Token::Ident(s) | Token::QuotedIdent(s) => {
20642 self.advance();
20643 cols.push(s);
20644 }
20645 other => {
20646 return Err(self.err(format!(
20647 "expected column name inside USING (…), got {other:?}"
20648 )));
20649 }
20650 }
20651 match self.peek() {
20652 Token::Comma => {
20653 self.advance();
20654 continue;
20655 }
20656 Token::RParen => {
20657 self.advance();
20658 break;
20659 }
20660 other => {
20661 return Err(self.err(format!(
20662 "expected ',' or ')' inside USING (…), got {other:?}"
20663 )));
20664 }
20665 }
20666 }
20667 if cols.is_empty() {
20668 return Err(self.err("USING (…) requires at least one column".to_string()));
20669 }
20670 using_cols = Some(cols.clone());
20671 // Pick the left-side alias: prev join's table if any,
20672 // else FROM primary. Use alias when present, else
20673 // table name (PG-equivalent qualifier).
20674 let left_qual: String = joins
20675 .last()
20676 .map(|j| {
20677 j.table
20678 .alias
20679 .clone()
20680 .unwrap_or_else(|| j.table.name.clone())
20681 })
20682 .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
20683 let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
20684 let mut iter = cols.into_iter().map(|c| Expr::Binary {
20685 lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20686 qualifier: Some(left_qual.clone()),
20687 name: c.clone(),
20688 })),
20689 op: crate::ast::BinOp::Eq,
20690 rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20691 qualifier: Some(right_qual.clone()),
20692 name: c,
20693 })),
20694 });
20695 let first = iter.next().expect("at least one col");
20696 Some(iter.fold(first, |acc, pred| Expr::Binary {
20697 lhs: alloc::boxed::Box::new(acc),
20698 op: crate::ast::BinOp::And,
20699 rhs: alloc::boxed::Box::new(pred),
20700 }))
20701 } else if kind == JoinKind::Cross {
20702 None
20703 } else {
20704 return Err(self.err(format!(
20705 "expected ON or USING after {:?} JOIN, got {:?}",
20706 kind,
20707 self.peek()
20708 )));
20709 };
20710 joins.push(FromJoin {
20711 kind,
20712 table,
20713 on,
20714 using_cols,
20715 natural: false,
20716 });
20717 }
20718 Ok(joins)
20719 }
20720
20721 /// Optional alias after an expression or table:
20722 /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
20723 /// accepted (PG-style implicit alias). Returns `None` if the next token
20724 /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
20725 fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
20726 if matches!(self.peek(), Token::As) {
20727 self.advance();
20728 // v7.39 (round 340, V56) — after AS the next token MUST be an
20729 // identifier. This used to return None and "let the caller
20730 // surface the error on the next expectation", but when AS is
20731 // the LAST token there is no next expectation: `SELECT 1 AS`
20732 // parsed clean and silently dropped the alias. PG rejects it.
20733 if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
20734 return self.expect_ident_like().map(Some);
20735 }
20736 return Err(self.err(alloc::format!(
20737 "expected an alias after AS, got {:?}",
20738 self.peek()
20739 )));
20740 }
20741 // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
20742 // grammar reserves a long list of follow-keywords from the
20743 // alias slot. SPG's bareword approximation: skip a small
20744 // set of idents that would otherwise be swallowed as the
20745 // table alias and break trailing clauses like CREATE
20746 // MATERIALIZED VIEW … WITH [NO] DATA or future ON
20747 // CONFLICT WHERE shapes.
20748 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
20749 if is_alias_stopword(s) {
20750 return Ok(None);
20751 }
20752 return Ok(self.expect_ident_like().ok());
20753 }
20754 Ok(None)
20755 }
20756
20757 /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
20758 fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
20759 // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
20760 // error beats a stack overflow (an overflow aborts the
20761 // embedding host process).
20762 self.enter_nested()?;
20763 let r = self.parse_expr_inner(min_prec);
20764 self.nest_depth -= 1;
20765 r
20766 }
20767
20768 /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
20769 /// When the upcoming tokens form one, return the underlying
20770 /// operator token and the position just past the closing paren
20771 /// so the binary loop can dispatch on the plain operator.
20772 fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
20773 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
20774 return None;
20775 }
20776 if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
20777 return None;
20778 }
20779 let mut i = self.pos + 2;
20780 // Optional schema qualifier (pg_catalog.<op> etc.).
20781 if matches!(self.tokens.get(i), Some(Token::Ident(_)))
20782 && matches!(self.tokens.get(i + 1), Some(Token::Dot))
20783 {
20784 i += 2;
20785 }
20786 let op_tok = self.tokens.get(i)?.clone();
20787 if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
20788 return None;
20789 }
20790 Some((i + 2, op_tok))
20791 }
20792
20793 /// PG operator symbols that lower onto function calls in
20794 /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
20795 /// family → regexp_like, comparison rung), `^@` (starts_with,
20796 /// comparison rung), `^` (power, tighter than `*`), `#`
20797 /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
20798 /// subset of the OR bits so the subtraction never borrows).
20799 fn try_symbol_operator(
20800 &mut self,
20801 lhs: &Expr,
20802 min_prec: u8,
20803 ) -> Result<Option<Expr>, ParseError> {
20804 enum Sym {
20805 Regex { ci: bool, negated: bool },
20806 Like { ci: bool, negated: bool },
20807 StartsWith,
20808 Power,
20809 Xor,
20810 RangeAdjacent,
20811 }
20812 // v7.39 (IS-precedence knife) — the low-precedence postfix
20813 // predicates ride this existing leaf call (zero new frame slots
20814 // on the nesting chain).
20815 if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
20816 return Ok(Some(e));
20817 }
20818 let (sym, prec): (Sym, u8) = match self.peek() {
20819 Token::Tilde => (
20820 Sym::Regex {
20821 ci: false,
20822 negated: false,
20823 },
20824 5,
20825 ),
20826 Token::TildeStar => (
20827 Sym::Regex {
20828 ci: true,
20829 negated: false,
20830 },
20831 5,
20832 ),
20833 Token::NotTilde => (
20834 Sym::Regex {
20835 ci: false,
20836 negated: true,
20837 },
20838 5,
20839 ),
20840 Token::NotTildeStar => (
20841 Sym::Regex {
20842 ci: true,
20843 negated: true,
20844 },
20845 5,
20846 ),
20847 // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
20848 Token::DoubleTilde => (
20849 Sym::Like {
20850 ci: false,
20851 negated: false,
20852 },
20853 5,
20854 ),
20855 Token::DoubleTildeStar => (
20856 Sym::Like {
20857 ci: true,
20858 negated: false,
20859 },
20860 5,
20861 ),
20862 Token::NotDoubleTilde => (
20863 Sym::Like {
20864 ci: false,
20865 negated: true,
20866 },
20867 5,
20868 ),
20869 Token::NotDoubleTildeStar => (
20870 Sym::Like {
20871 ci: true,
20872 negated: true,
20873 },
20874 5,
20875 ),
20876 Token::CaretAt => (Sym::StartsWith, 5),
20877 // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
20878 // tighter than `* / & |`, which the prec-9 rung preserves —
20879 // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
20880 Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
20881 Token::Caret => (Sym::Power, 9),
20882 // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
20883 // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
20884 Token::Hash => (Sym::Xor, 6),
20885 Token::Adjacent => (Sym::RangeAdjacent, 5),
20886 _ => return Ok(None),
20887 };
20888 if prec < min_prec {
20889 return Ok(None);
20890 }
20891 self.advance();
20892 let rhs = self.parse_expr(prec + 1)?;
20893 let out = match sym {
20894 Sym::Regex { ci, negated } => {
20895 let mut args = alloc::vec![lhs.clone(), rhs];
20896 if ci {
20897 args.push(Expr::Literal(Literal::String(String::from("i"))));
20898 }
20899 maybe_not(
20900 Expr::FunctionCall {
20901 name: String::from("regexp_like"),
20902 args,
20903 },
20904 negated,
20905 )
20906 }
20907 Sym::Like { ci, negated } => Expr::Like {
20908 expr: alloc::boxed::Box::new(lhs.clone()),
20909 pattern: alloc::boxed::Box::new(rhs),
20910 negated,
20911 case_insensitive: ci,
20912 },
20913 Sym::StartsWith => Expr::FunctionCall {
20914 name: String::from("starts_with"),
20915 args: alloc::vec![lhs.clone(), rhs],
20916 },
20917 Sym::Power => Expr::FunctionCall {
20918 name: String::from("power"),
20919 args: alloc::vec![lhs.clone(), rhs],
20920 },
20921 // `#` bitwise XOR — a real operator now (was desugared to
20922 // `(a|b)-(a&b)`, algebraically identical for integers but
20923 // undefined for bit strings; the direct op handles both).
20924 Sym::Xor => Expr::Binary {
20925 lhs: Box::new(lhs.clone()),
20926 op: BinOp::BitXor,
20927 rhs: Box::new(rhs),
20928 },
20929 // range `-|-` "is adjacent to" — lowered to a catalog function.
20930 Sym::RangeAdjacent => Expr::FunctionCall {
20931 name: String::from("range_adjacent"),
20932 args: alloc::vec![lhs.clone(), rhs],
20933 },
20934 };
20935 Ok(Some(out))
20936 }
20937
20938 /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
20939 /// predicates, moved out of the tight postfix-cast loop: PG binds
20940 /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
20941 /// looser than EVERY binary operator (only NOT/AND/OR are looser),
20942 /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
20943 /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
20944 /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
20945 /// when nothing at this position belongs to the family. Out-of-line
20946 /// (`inline(never)`): the caller sits on the per-nesting-level frame
20947 /// chain that MAX_NEST_DEPTH is tuned against.
20948 #[inline(never)]
20949 fn parse_postfix_predicate(
20950 &mut self,
20951 lhs: &Expr,
20952 min_prec: u8,
20953 ) -> Result<Option<Expr>, ParseError> {
20954 // Reached through try_symbol_operator (an existing leaf call of
20955 // the binary loop) so NO new stack slots land on the per-nesting
20956 // frame chain; the lhs clones only when a predicate actually
20957 // consumes it.
20958 match self.peek() {
20959 // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
20960 // comparison family rung 5 (each +1 from the pre-XOR ladder).
20961 Token::Is if min_prec <= 4 => {}
20962 Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
20963 Token::Not
20964 if min_prec <= 5
20965 && matches!(
20966 self.tokens.get(self.pos + 1),
20967 Some(Token::Between | Token::In | Token::Like)
20968 ) => {}
20969 Token::Not | Token::Ident(_)
20970 if min_prec <= 5
20971 && (matches!(self.peek(), Token::Ident(s)
20972 if s.eq_ignore_ascii_case("ilike")
20973 || (self.mysql_dialect
20974 && (s.eq_ignore_ascii_case("regexp")
20975 || s.eq_ignore_ascii_case("rlike")))
20976 || (s.eq_ignore_ascii_case("similar")
20977 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
20978 || (matches!(self.peek(), Token::Not)
20979 && matches!(self.tokens.get(self.pos + 1),
20980 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
20981 || (self.mysql_dialect
20982 && (s.eq_ignore_ascii_case("regexp")
20983 || s.eq_ignore_ascii_case("rlike")))
20984 || s.eq_ignore_ascii_case("similar")))) => {}
20985 _ => return Ok(None),
20986 }
20987 let mut expr = lhs.clone();
20988 // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
20989 // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
20990 if min_prec <= 4 {
20991 if matches!(self.peek(), Token::Is) {
20992 self.advance();
20993 let negated = if matches!(self.peek(), Token::Not) {
20994 self.advance();
20995 true
20996 } else {
20997 false
20998 };
20999 // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
21000 // mailrs pg_dump.
21001 if matches!(self.peek(), Token::Distinct) {
21002 self.advance();
21003 if !matches!(self.peek(), Token::From) {
21004 return Err(self.err(format!(
21005 "expected FROM after IS{} DISTINCT, got {:?}",
21006 if negated { " NOT" } else { "" },
21007 self.peek()
21008 )));
21009 }
21010 self.advance();
21011 // Right-hand side: parse at the same precedence
21012 // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
21013 // groups as `x IS DISTINCT FROM (a + b)`.
21014 let rhs = self.parse_expr(5)?;
21015 let op = if negated {
21016 BinOp::IsNotDistinctFrom
21017 } else {
21018 BinOp::IsDistinctFrom
21019 };
21020 expr = Expr::Binary {
21021 op,
21022 lhs: Box::new(expr),
21023 rhs: Box::new(rhs),
21024 };
21025 {
21026 return Ok(Some(expr));
21027 }
21028 }
21029 // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
21030 // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
21031 // Lowers onto pg_is_json(x, kind); NOT wraps the
21032 // call in a logical negation.
21033 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21034 if s.eq_ignore_ascii_case("json"))
21035 {
21036 self.advance(); // JSON
21037 let kind = match self.peek() {
21038 Token::Ident(s) | Token::QuotedIdent(s)
21039 if matches!(
21040 s.to_ascii_lowercase().as_str(),
21041 "value" | "object" | "array" | "scalar"
21042 ) =>
21043 {
21044 let k = s.to_ascii_lowercase();
21045 self.advance();
21046 k
21047 }
21048 _ => "value".to_string(),
21049 };
21050 let call = Expr::FunctionCall {
21051 name: "pg_is_json".to_string(),
21052 args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
21053 };
21054 expr = if negated {
21055 Expr::Unary {
21056 op: UnOp::Not,
21057 expr: Box::new(call),
21058 }
21059 } else {
21060 call
21061 };
21062 {
21063 return Ok(Some(expr));
21064 }
21065 }
21066 // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
21067 // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
21068 // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
21069 {
21070 let form_kw = match self.peek() {
21071 Token::Ident(s) | Token::QuotedIdent(s)
21072 if matches!(
21073 s.to_ascii_uppercase().as_str(),
21074 "NFC" | "NFD" | "NFKC" | "NFKD"
21075 ) && matches!(
21076 self.tokens.get(self.pos + 1),
21077 Some(Token::Ident(n) | Token::QuotedIdent(n))
21078 if n.eq_ignore_ascii_case("normalized")
21079 ) =>
21080 {
21081 Some(s.to_ascii_uppercase())
21082 }
21083 _ => None,
21084 };
21085 let bare_normalized = form_kw.is_none()
21086 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21087 if s.eq_ignore_ascii_case("normalized"));
21088 if form_kw.is_some() || bare_normalized {
21089 if form_kw.is_some() {
21090 self.advance(); // form keyword
21091 }
21092 self.advance(); // NORMALIZED
21093 let mut args = alloc::vec![expr];
21094 if let Some(f) = form_kw {
21095 args.push(Expr::Literal(Literal::String(f)));
21096 }
21097 let call = Expr::FunctionCall {
21098 name: "is_normalized".to_string(),
21099 args,
21100 };
21101 expr = if negated {
21102 Expr::Unary {
21103 op: UnOp::Not,
21104 expr: Box::new(call),
21105 }
21106 } else {
21107 call
21108 };
21109 {
21110 return Ok(Some(expr));
21111 }
21112 }
21113 }
21114 // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
21115 // three-valued boolean tests. IS TRUE/FALSE never
21116 // return NULL, so they lower to CASE forms whose
21117 // ELSE catches the NULL branch; IS UNKNOWN on a
21118 // boolean is exactly IS NULL.
21119 if matches!(self.peek(), Token::True | Token::False)
21120 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
21121 {
21122 let tok = self.advance();
21123 let test = match tok {
21124 Token::True => Some(true),
21125 Token::False => Some(false),
21126 _ => None, // UNKNOWN
21127 };
21128 // v7.39 (round 328, V45) — kept as what the user
21129 // wrote. These used to be lowered here into `CASE` /
21130 // `IS NULL`; the semantics were right but the AST no
21131 // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
21132 // was echoed back as
21133 // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
21134 expr = Expr::BoolTest {
21135 expr: Box::new(expr),
21136 value: test,
21137 negated,
21138 };
21139 {
21140 return Ok(Some(expr));
21141 }
21142 }
21143 if !matches!(self.peek(), Token::Null) {
21144 return Err(self.err(format!(
21145 "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21146 if negated { " NOT" } else { "" },
21147 self.peek()
21148 )));
21149 }
21150 self.advance();
21151 expr = Expr::IsNull {
21152 expr: Box::new(expr),
21153 negated,
21154 };
21155 {
21156 return Ok(Some(expr));
21157 }
21158 }
21159 }
21160 // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21161 if min_prec <= 5 {
21162 // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21163 // Look one token ahead so a stray `NOT` not followed by any of
21164 // these flows through to the early return below untouched.
21165 let negated = if matches!(self.peek(), Token::Not) {
21166 let next = self.tokens.get(self.pos + 1);
21167 matches!(next, Some(Token::Between | Token::In | Token::Like))
21168 || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21169 || (self.mysql_dialect
21170 && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21171 || s.eq_ignore_ascii_case("similar"))
21172 } else {
21173 false
21174 };
21175 if negated {
21176 self.advance();
21177 }
21178 if matches!(self.peek(), Token::Between) {
21179 expr = self.parse_between_tail(expr, negated)?;
21180 {
21181 return Ok(Some(expr));
21182 }
21183 }
21184 if matches!(self.peek(), Token::In) {
21185 if self.suppress_in_tail && !negated {
21186 // POSITION(sub IN str) — IN belongs to the
21187 // enclosing function syntax; stop here.
21188 {
21189 return Ok(None);
21190 }
21191 }
21192 expr = self.parse_in_tail(expr, negated)?;
21193 {
21194 return Ok(Some(expr));
21195 }
21196 }
21197 // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21198 // lowers onto the internal __similar_to(expr, pat[, esc]) call
21199 // (the SQL→regex transform runs inside, in the backtracking-
21200 // friendly shape SPG's matcher needs).
21201 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21202 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21203 {
21204 self.advance(); // SIMILAR
21205 self.advance(); // TO
21206 let pattern = self.parse_expr(6)?;
21207 let mut args = alloc::vec![expr, pattern];
21208 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21209 self.advance();
21210 args.push(self.parse_expr(6)?);
21211 }
21212 let call = Expr::FunctionCall {
21213 name: "__similar_to".to_string(),
21214 args,
21215 };
21216 expr = maybe_not(call, negated);
21217 {
21218 return Ok(Some(expr));
21219 }
21220 }
21221 if matches!(self.peek(), Token::Like) {
21222 self.advance();
21223 // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21224 if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21225 expr = q;
21226 {
21227 return Ok(Some(expr));
21228 }
21229 }
21230 // Pattern at the same precedence as other comparison RHSes —
21231 // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21232 let mut pattern = self.parse_expr(6)?;
21233 // `ESCAPE 'c'` — rewrite a literal pattern to the
21234 // default backslash escape at parse time. Custom
21235 // escapes on non-literal patterns would need
21236 // matcher support; error honestly.
21237 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21238 self.advance();
21239 let esc = self.parse_expr(6)?;
21240 pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21241 }
21242 expr = Expr::Like {
21243 expr: Box::new(expr),
21244 pattern: Box::new(pattern),
21245 negated,
21246 case_insensitive: false,
21247 };
21248 {
21249 return Ok(Some(expr));
21250 }
21251 }
21252 // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21253 // keyword reaches us as a plain identifier.
21254 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21255 self.advance();
21256 if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21257 expr = q;
21258 {
21259 return Ok(Some(expr));
21260 }
21261 }
21262 let pattern = self.parse_expr(6)?;
21263 expr = Expr::Like {
21264 expr: Box::new(expr),
21265 pattern: Box::new(pattern),
21266 negated,
21267 case_insensitive: true,
21268 };
21269 {
21270 return Ok(Some(expr));
21271 }
21272 }
21273 // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21274 // operator (RLIKE is the alias). It is a keyword, not `~`, and
21275 // matches case-insensitively under the default collation, so it
21276 // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21277 // `~*` operator uses, wrapped in NOT when negated.
21278 if self.mysql_dialect
21279 && matches!(self.peek(), Token::Ident(s)
21280 if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21281 {
21282 self.advance();
21283 let pattern = self.parse_expr(6)?;
21284 let call = Expr::FunctionCall {
21285 name: String::from("regexp_like"),
21286 args: alloc::vec![
21287 expr,
21288 pattern,
21289 Expr::Literal(Literal::String(String::from("i"))),
21290 ],
21291 };
21292 return Ok(Some(maybe_not(call, negated)));
21293 }
21294 }
21295 let _ = expr;
21296 Ok(None)
21297 }
21298
21299 fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21300 let mut lhs = self.parse_unary()?;
21301 let mut chain_len = 0usize;
21302 loop {
21303 // OPERATOR([schema.]op) reduces to its underlying
21304 // operator token before the normal dispatch.
21305 let explicit = self.peek_explicit_operator();
21306 let dispatch = match &explicit {
21307 Some((_, tok)) => self.binop_here(tok),
21308 None => self.binop_here(self.peek()),
21309 };
21310 let Some((op, prec)) = dispatch else {
21311 // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21312 // of the symbol family. `binop_here` answers None for them
21313 // because they lower onto function calls rather than a
21314 // BinOp, and the fallback below reads `self.peek()` — the
21315 // word OPERATOR, not the operator. `pg_dump` writes every
21316 // catalog predicate this way, so its first query failed
21317 // and no dump ran:
21318 //
21319 // AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21320 //
21321 // Collapsing the wrapper to the operator it names puts the
21322 // token where the fallback already looks.
21323 if let Some((next, op_tok)) = explicit {
21324 self.tokens.splice(self.pos..next, [op_tok]);
21325 }
21326 if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21327 lhs = e;
21328 chain_len += 1;
21329 if chain_len > MAX_BINARY_CHAIN {
21330 return Err(self.err(alloc::format!(
21331 "more than {MAX_BINARY_CHAIN} chained binary operators"
21332 )));
21333 }
21334 continue;
21335 }
21336 break;
21337 };
21338 if prec < min_prec {
21339 break;
21340 }
21341 // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21342 // iteratively but evaluates and drops recursively;
21343 // depth beyond the budget overflows worker stacks.
21344 chain_len += 1;
21345 if chain_len > MAX_BINARY_CHAIN {
21346 return Err(self.err(alloc::format!(
21347 "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21348 )));
21349 }
21350 match explicit {
21351 Some((end_pos, _)) => self.pos = end_pos,
21352 None => {
21353 self.advance();
21354 }
21355 }
21356 // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21357 // ANY is a bare ident; ALL is a reserved Token. Both
21358 // require an immediate `(` to disambiguate from
21359 // identifier columns named `any` / `all`.
21360 let any_kind = match self.peek() {
21361 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21362 Some(false)
21363 }
21364 Token::Ident(s) | Token::QuotedIdent(s)
21365 if (s.eq_ignore_ascii_case("any")
21366 || s.eq_ignore_ascii_case("some")
21367 || s.eq_ignore_ascii_case("all"))
21368 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21369 {
21370 Some(!s.eq_ignore_ascii_case("all"))
21371 }
21372 _ => None,
21373 };
21374 if let Some(is_any) = any_kind {
21375 lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21376 continue;
21377 }
21378 let rhs = self.parse_expr(prec + 1)?;
21379 lhs = Expr::Binary {
21380 lhs: Box::new(lhs),
21381 op,
21382 rhs: Box::new(rhs),
21383 };
21384 }
21385 Ok(lhs)
21386 }
21387
21388 /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
21389 /// and the array form.
21390 ///
21391 /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
21392 /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
21393 /// this block's `Expr` temporaries and four `format!` sites slots in
21394 /// that frame on every level of `((((1))))`, which never reaches it.
21395 #[inline(never)]
21396 fn parse_any_all_rhs(
21397 &mut self,
21398 lhs: Expr,
21399 op: BinOp,
21400 is_any: bool,
21401 ) -> Result<Expr, ParseError> {
21402 self.advance(); // ident
21403 self.advance(); // (
21404 // `x op ANY (SELECT …)` — the quantified-subquery
21405 // form. `= ANY` is exactly IN; the other operators
21406 // lower onto EXISTS over the subquery as a derived
21407 // table, comparing against its single projection
21408 // aliased __v (x's columns resolve correlated).
21409 // ALL is the negated-EXISTS complement; a NULL
21410 // element makes PG return NULL where this lowering
21411 // returns true — the NOT NULL column case (the
21412 // practical one) is exact.
21413 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
21414 // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
21415 // legal PG too (round-151 sibling). Out-of-line
21416 // (#[inline(never)] helper) — this sits on
21417 // parse_expr's recursive frame and the two-armed
21418 // SELECT temporary blew the nesting-budget stack.
21419 let mut sub = self.parse_any_all_select_body()?;
21420 if !matches!(self.peek(), Token::RParen) {
21421 return Err(self.err(alloc::format!(
21422 "expected ')' after ANY/ALL subquery, got {:?}",
21423 self.peek()
21424 )));
21425 }
21426 self.advance();
21427 if sub.items.len() != 1 {
21428 return Err(self.err(alloc::format!(
21429 "ANY/ALL subquery must return one column, got {}",
21430 sub.items.len()
21431 )));
21432 }
21433 if is_any && matches!(op, BinOp::Eq) {
21434 return Ok(Expr::InSubquery {
21435 expr: Box::new(lhs),
21436 subquery: Box::new(sub),
21437 negated: false,
21438 });
21439 }
21440 // The engine's subquery resolvers materialise
21441 // the single-column result into an ARRAY the
21442 // existing AnyAll three-valued eval consumes.
21443 return Ok(Expr::AnyAll {
21444 expr: Box::new(lhs),
21445 op,
21446 array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
21447 is_any,
21448 });
21449 }
21450 let arr = self.parse_expr(0)?;
21451 if !matches!(self.peek(), Token::RParen) {
21452 return Err(self.err(alloc::format!(
21453 "expected ')' after ANY/ALL argument, got {:?}",
21454 self.peek()
21455 )));
21456 }
21457 self.advance();
21458 Ok(Expr::AnyAll {
21459 expr: Box::new(lhs),
21460 op,
21461 array: Box::new(arr),
21462 is_any,
21463 })
21464 }
21465
21466 /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
21467 /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
21468 #[inline(never)]
21469 fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
21470 self.advance();
21471 let e = self.parse_expr(9)?;
21472 Ok(build_center_call(e))
21473 }
21474
21475 /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
21476 /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
21477 /// unary minus.
21478 ///
21479 /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
21480 /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
21481 /// the Expr-sized local stays out of that frame.
21482 #[inline(never)]
21483 fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21484 self.advance();
21485 let e = self.parse_expr(9)?;
21486 Ok(Expr::FunctionCall {
21487 name: alloc::string::String::from(name),
21488 args: alloc::vec![e],
21489 })
21490 }
21491
21492 /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
21493 /// (horizontal). Out-of-line from `parse_unary` (frame budget).
21494 #[inline(never)]
21495 fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
21496 self.advance();
21497 let e = self.parse_expr(9)?;
21498 Ok(Expr::FunctionCall {
21499 name: alloc::string::String::from(if vertical {
21500 "isvertical"
21501 } else {
21502 "ishorizontal"
21503 }),
21504 args: alloc::vec![e],
21505 })
21506 }
21507
21508 /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
21509 /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
21510 /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
21511 #[inline(never)]
21512 fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
21513 self.advance();
21514 let e = self.parse_expr(9)?;
21515 Ok(Expr::Cast {
21516 expr: Box::new(e),
21517 target: CastTarget::Named("binary".to_string()),
21518 })
21519 }
21520
21521 /// The prefix operators that share one shape: take the token, parse
21522 /// an operand at `prec`, wrap it.
21523 ///
21524 /// `#[inline(never)]`, and one function instead of five arms, for the
21525 /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
21526 /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
21527 /// debug build gives EVERY arm's locals a slot in the frame, whichever
21528 /// arm runs. `((((1))))` reaches none of these arms and was carrying
21529 /// five `Expr`-sized locals per level for them anyway.
21530 #[inline(never)]
21531 fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
21532 self.advance();
21533 let e = self.parse_expr(prec)?;
21534 Ok(Expr::Unary {
21535 op,
21536 expr: Box::new(e),
21537 })
21538 }
21539
21540 /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
21541 /// and separate from it because of the literal folding below and the
21542 /// `format!` temporaries that folding needs.
21543 #[inline(never)]
21544 fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
21545 self.advance();
21546 // v7.39 (round 549) — fold the sign into an integer literal that
21547 // only fits once it is negative.
21548 //
21549 // `9223372036854775808` is one past i64::MAX, so the lexer hands
21550 // it over as a NUMERIC and `-` on a numeric stays numeric. PG
21551 // folds the sign first, so `-9223372036854775808` is a bigint
21552 // there — and `-9223372036854775808 - 1` raises "bigint out of
21553 // range" where SPG quietly answered -9223372036854775809, a value
21554 // no bigint can hold. The arithmetic itself was already checked;
21555 // only the literal's type was wrong.
21556 if let Token::Numeric(lit) = self.peek()
21557 && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
21558 {
21559 self.advance();
21560 return Ok(Expr::Literal(Literal::Integer(folded)));
21561 }
21562 // Unary minus binds tighter than `*`/`/` (now at prec 7 after
21563 // `<->` slotted into 5 and arithmetic shifted up).
21564 let e = self.parse_expr(9)?;
21565 Ok(Expr::Unary {
21566 op: UnOp::Neg,
21567 expr: Box::new(e),
21568 })
21569 }
21570
21571 /// tsquery `!!` prefix negation, lowered to the catalog function.
21572 /// Binds like unary minus. Out-of-line for the frame reason on
21573 /// `parse_unary_op`.
21574 #[inline(never)]
21575 fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
21576 self.advance();
21577 let e = self.parse_expr(9)?;
21578 Ok(Expr::FunctionCall {
21579 name: String::from("tsquery_not"),
21580 args: alloc::vec![e],
21581 })
21582 }
21583
21584 fn parse_unary(&mut self) -> Result<Expr, ParseError> {
21585 match self.peek() {
21586 // NOT binds tighter than AND / XOR / OR but looser than
21587 // comparisons — its operand takes everything ≥ the comparison
21588 // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
21589 // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
21590 // was rung 3, behaviour-identical when 3 was unused; AND now
21591 // occupies 3, so this must be 4 to keep NOT tighter than AND.)
21592 Token::Not => self.parse_unary_op(UnOp::Not, 4),
21593 // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
21594 // The body is out-of-line: `parse_unary` is one of the three
21595 // frames the parser's MAX_NEST_DEPTH is tuned against, and an
21596 // inline arm here overflowed the native stack in
21597 // `nesting_budget_errors_cleanly` — the guard test caught it,
21598 // exactly as the eval-side cliff did in rounds 346 and 351.
21599 Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
21600 self.parse_binary_prefix()
21601 }
21602 // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
21603 // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
21604 // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
21605 Token::Bang => self.parse_unary_op(UnOp::Not, 9),
21606 Token::Minus => self.parse_prefix_minus(),
21607 // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
21608 // worked only because the lexer reads it as one signed literal;
21609 // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
21610 // PG18 and MariaDB take all of them. Binds like unary minus.
21611 Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
21612 // Bitwise NOT binds like unary minus.
21613 Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
21614 // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
21615 // "center of" operator; desugars to center(x). The whole arm
21616 // is out-of-line: parse_unary sits on the per-nesting-level
21617 // frame chain that MAX_NEST_DEPTH is tuned against, so no
21618 // Expr-sized local may live in this frame.
21619 Token::TsMatch => self.parse_prefix_center(),
21620 // v7.39 (round 508) — the prefix operators that are named
21621 // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
21622 // is length. Out-of-line for the same nesting-frame reason as
21623 // parse_prefix_center — parse_unary sits on the recursive cycle
21624 // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
21625 // live in this frame.
21626 Token::At => self.parse_prefix_call("abs"),
21627 Token::Hash => self.parse_prefix_call("npoints"),
21628 Token::AtMinusAt => self.parse_prefix_call("length"),
21629 // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
21630 // "is horizontal" (lseg / line); desugars to the existing
21631 // isvertical()/ishorizontal() functions. Out-of-line for the
21632 // same nesting-frame reason as parse_prefix_center.
21633 Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
21634 Token::GeomHoriz => self.parse_prefix_geom_axis(false),
21635 Token::DoubleBang => self.parse_prefix_tsquery_not(),
21636 _ => self.parse_atom(),
21637 }
21638 }
21639
21640 /// Parse a parenthesised scalar subquery body after the caller has consumed
21641 /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
21642 /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
21643 /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
21644 /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
21645 /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
21646 /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
21647 /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
21648 /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
21649 /// which sits on the recursive nesting-budget cycle (a few extra bytes there
21650 /// tips the deep-nesting test into a stack overflow).
21651 #[inline(never)]
21652 fn array_subquery_ahead(&self) -> bool {
21653 if !matches!(self.peek(), Token::LParen) {
21654 return false;
21655 }
21656 matches!(
21657 self.tokens.get(self.pos + 1),
21658 Some(Token::Select | Token::Values)
21659 ) || matches!(
21660 self.tokens.get(self.pos + 1),
21661 Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
21662 )
21663 }
21664
21665 /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
21666 /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
21667 /// locals stay off parse_atom's recursive frame (round 105).
21668 #[inline(never)]
21669 fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
21670 self.advance(); // consume `[`
21671 let mut items: Vec<Expr> = Vec::new();
21672 if !matches!(self.peek(), Token::RBracket) {
21673 loop {
21674 // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
21675 // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
21676 if matches!(self.peek(), Token::LBracket) {
21677 items.push(self.parse_array_bracket_body()?);
21678 } else {
21679 items.push(self.parse_expr(0)?);
21680 }
21681 match self.peek() {
21682 Token::Comma => {
21683 self.advance();
21684 }
21685 Token::RBracket => break,
21686 other => {
21687 return Err(self.err(alloc::format!(
21688 "expected ',' or ']' in ARRAY literal, got {other:?}"
21689 )));
21690 }
21691 }
21692 }
21693 }
21694 self.advance(); // consume `]`
21695 Ok(Expr::Array(items))
21696 }
21697
21698 /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
21699 /// is already consumed; the current token is `(`. Desugars to a scalar
21700 /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
21701 /// the subquery's single-column rows in order — reusing the existing
21702 /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
21703 /// keeps the large `Statement` local off parse_atom's recursive frame.
21704 #[inline(never)]
21705 fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
21706 self.advance(); // consume `(`
21707 let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
21708 if w.eq_ignore_ascii_case("with"));
21709 let sub = if is_with {
21710 self.advance(); // WITH
21711 self.parse_with_cte_then_select()?
21712 } else {
21713 self.parse_select_stmt()?
21714 };
21715 if !matches!(self.peek(), Token::RParen) {
21716 return Err(self.err(alloc::format!(
21717 "expected ')' to close ARRAY(subquery), got {:?}",
21718 self.peek()
21719 )));
21720 }
21721 self.advance(); // consume `)`
21722 // Reuse the parser to build the array_agg wrapper from the subquery's
21723 // canonical text — avoids hand-constructing the derived-table AST.
21724 let wrapper = alloc::format!(
21725 "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
21726 );
21727 let stmt = parse_statement(&wrapper)
21728 .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
21729 let Statement::Select(sel) = stmt else {
21730 return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
21731 };
21732 Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
21733 }
21734
21735 #[inline(never)]
21736 fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
21737 let inner = if is_with {
21738 self.advance(); // WITH
21739 self.parse_with_cte_then_select()?
21740 } else {
21741 self.parse_select_stmt()?
21742 };
21743 match self.advance() {
21744 Token::RParen => {
21745 let Statement::Select(s) = inner else {
21746 return Err(ParseError {
21747 message: "scalar subquery body must be a SELECT".into(),
21748 token_pos: self.consumed_pos(),
21749 });
21750 };
21751 Ok(Expr::ScalarSubquery(Box::new(s)))
21752 }
21753 other => Err(ParseError {
21754 message: format!("expected ')' after scalar subquery, got {other:?}"),
21755 token_pos: self.consumed_pos(),
21756 }),
21757 }
21758 }
21759
21760 /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
21761 /// literals. The lexer splits them into an ident + string; recombine
21762 /// here. Out-of-line and returning `Option` so `parse_atom` — the
21763 /// recursive frame the 768 KiB stack budget is tuned against — pays no
21764 /// frame for the `body` / `bits` strings and their char loops (the
21765 /// round-367 frame cliff, M20).
21766 #[inline(never)]
21767 fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
21768 let is_hex = match self.peek() {
21769 Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
21770 Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
21771 _ => return None,
21772 };
21773 if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
21774 return None;
21775 }
21776 self.advance();
21777 let Token::String(body) = self.advance() else {
21778 unreachable!("guarded above");
21779 };
21780 // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
21781 // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
21782 // (hex pairs, even count required — MariaDB errors on an odd
21783 // count); `b'1010'` packs its bits big-endian, left-padded to a
21784 // byte. Lower both onto the bytea cast.
21785 if self.mysql_dialect {
21786 if is_hex {
21787 if body.len() % 2 == 1 {
21788 return Some(Err(self.err(alloc::format!(
21789 "invalid hex string literal X'{body}': odd digit count"
21790 ))));
21791 }
21792 for c in body.chars() {
21793 if !c.is_ascii_hexdigit() {
21794 return Some(Err(
21795 self.err(alloc::format!("invalid hexadecimal digit {c:?} in X'…'"))
21796 ));
21797 }
21798 }
21799 return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
21800 }
21801 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21802 return Some(Err(
21803 self.err(alloc::format!("invalid binary digit {bad:?} in b'…'"))
21804 ));
21805 }
21806 return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
21807 }
21808 let bits = if is_hex {
21809 let mut out = String::with_capacity(body.len() * 4);
21810 for c in body.chars() {
21811 let Some(d) = c.to_digit(16) else {
21812 return Some(Err(self.err(alloc::format!(
21813 "invalid hexadecimal digit {c:?} in X'…' bit string"
21814 ))));
21815 };
21816 out.push_str(&alloc::format!("{d:04b}"));
21817 }
21818 out
21819 } else {
21820 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21821 return Some(Err(self.err(alloc::format!(
21822 "invalid binary digit {bad:?} in B'…' bit string"
21823 ))));
21824 }
21825 body
21826 };
21827 // Route through the postfix-cast loop so a chained cast like
21828 // `B'1010'::int` attaches onto the implicit `::bit` cast instead
21829 // of erroring at the `::`.
21830 // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
21831 // literal keeps its exact length, while an explicit `::bit` cast is
21832 // bit(1) with pad/truncate semantics (PG).
21833 Some(self.finish_postfix_casts(Expr::Cast {
21834 expr: Box::new(Expr::Literal(Literal::String(bits))),
21835 target: CastTarget::Named("__bit_literal".to_string()),
21836 }))
21837 }
21838
21839 fn parse_atom(&mut self) -> Result<Expr, ParseError> {
21840 if let Some(res) = self.try_parse_bit_string_literal() {
21841 return res;
21842 }
21843 let tok_pos = self.pos;
21844 match self.advance() {
21845 Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
21846 Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
21847 // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
21848 // carrying the source mantissa + scale so no precision is lost. A
21849 // literal too wide for i128 falls back to double precision.
21850 // Out-of-line (#[inline(never)]) — this arm sits on the
21851 // parse_expr recursion chain; its expansion locals must not
21852 // widen the recursive frame (debug frame-cliff discipline).
21853 Token::Numeric(s) => match numeric_token_to_literal(s) {
21854 Ok(lit) => Ok(Expr::Literal(lit)),
21855 Err(msg) => Err(self.err(msg)),
21856 },
21857 Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
21858 // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
21859 // (the lexer only emits this token in the MySQL dialect). Lower
21860 // onto the existing bytea cast; out-of-line to keep this arm off
21861 // the parse recursion frame.
21862 Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
21863 Token::True => Ok(Expr::Literal(Literal::Bool(true))),
21864 Token::False => Ok(Expr::Literal(Literal::Bool(false))),
21865 Token::Null => Ok(Expr::Literal(Literal::Null)),
21866 // v6.1.1 — `$N` placeholder. The actual Value lookup
21867 // happens in the engine eval path against the prepared-
21868 // statement bind buffer.
21869 Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
21870 Token::LParen => {
21871 // v4.10: `(SELECT ...)` in expression position is a
21872 // scalar subquery; otherwise it's a parenthesised
21873 // expression. Peek for SELECT keyword to dispatch.
21874 // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
21875 // lexes as Ident("with") (not a reserved token). The subquery body
21876 // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
21877 // so its large `Statement` local stays out of parse_atom's stack
21878 // frame — parse_atom is on the recursive `((…))` cycle and the
21879 // nesting budget is tuned to its frame size).
21880 let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21881 if s.eq_ignore_ascii_case("with"));
21882 if matches!(self.peek(), Token::Select) || is_with {
21883 self.parse_paren_scalar_subquery(is_with)
21884 } else {
21885 let e = self.parse_expr(0)?;
21886 // `(a, b, …)` — a row constructor. Valid only
21887 // in front of a comparison operator or [NOT]
21888 // IN; both expand at parse time (lexicographic
21889 // comparison / OR'd row equalities).
21890 if matches!(self.peek(), Token::Comma) {
21891 let mut row = alloc::vec![e];
21892 while matches!(self.peek(), Token::Comma) {
21893 self.advance();
21894 row.push(self.parse_expr(0)?);
21895 }
21896 if !matches!(self.peek(), Token::RParen) {
21897 return Err(self.err(alloc::format!(
21898 "expected ')' after row constructor, got {:?}",
21899 self.peek()
21900 )));
21901 }
21902 self.advance();
21903 // A bare `(a, b, …)` row constructor can carry postfix
21904 // (`::text`, `.field`) just like `ROW(a, b, …)`; the
21905 // early return here skips parse_atom's tail postfix
21906 // pass, so fold casts in explicitly. For the
21907 // comparison / predicate forms nothing postfix follows,
21908 // so this is a no-op.
21909 return self
21910 .parse_row_comparison_tail(row)
21911 .and_then(|e| self.finish_postfix_casts(e));
21912 }
21913 match self.advance() {
21914 Token::RParen => Ok(e),
21915 other => Err(ParseError {
21916 message: format!("expected ')', got {other:?}"),
21917 token_pos: self.consumed_pos(),
21918 }),
21919 }
21920 }
21921 }
21922 Token::LBracket => self.parse_vector_literal_body(),
21923 Token::Extract => self.parse_extract_atom(),
21924 Token::Interval => self.parse_interval_atom(),
21925 // `LEFT` / `RIGHT` are reserved-keyword tokens because the
21926 // grammar dedicates arms for `LEFT [OUTER] JOIN` /
21927 // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
21928 // expression position calling the PG `left(string, n)` /
21929 // `right(string, n)` function; rebuild the AST as a regular
21930 // function call so the engine's apply_function dispatch picks
21931 // it up. Delegated to a #[inline(never)] helper so its locals
21932 // don't bloat this recursive `parse_atom` frame (the nesting
21933 // budget in `enter_nested` is tuned to parse_atom's size).
21934 Token::Left if matches!(self.peek(), Token::LParen) => {
21935 self.parse_lr_string_function_call("left")
21936 }
21937 Token::Right if matches!(self.peek(), Token::LParen) => {
21938 self.parse_lr_string_function_call("right")
21939 }
21940 // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
21941 // token; we match on the bare ident. NOT is a token
21942 // (consumed in the comparison rung), but `EXISTS (...)`
21943 // at the top of an expression starts here.
21944 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
21945 self.parse_exists_atom(false)
21946 }
21947 // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
21948 // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
21949 // CASE is a bare ident; we dispatch on lowercase match.
21950 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
21951 self.parse_case_atom()
21952 }
21953 // v7.37.17 (17.6 siblings) — PG typed datetime literals:
21954 // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
21955 // '…'`. Lower onto the ::cast node so the existing
21956 // runtime text→date/timestamp paths do the parsing. The
21957 // string must follow immediately, else the ident stays a
21958 // plain column reference.
21959 Token::Ident(s)
21960 if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
21961 && matches!(self.peek(), Token::String(_)) =>
21962 {
21963 let target =
21964 typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
21965 let Token::String(lit) = self.advance() else {
21966 unreachable!("peek guaranteed a string token");
21967 };
21968 Ok(Expr::Cast {
21969 expr: Box::new(Expr::Literal(Literal::String(lit))),
21970 target,
21971 })
21972 }
21973 // v7.39 (round 221) — the SQL-standard long spellings:
21974 // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
21975 // TIME ZONE '…'`. Consume the modifier and lower to the same
21976 // typed-literal cast (`timetz` / `timestamptz` for WITH).
21977 Token::Ident(s)
21978 if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
21979 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
21980 || w.eq_ignore_ascii_case("without"))
21981 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
21982 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
21983 && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
21984 {
21985 let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
21986 self.advance(); // WITH / WITHOUT
21987 self.advance(); // TIME
21988 self.advance(); // ZONE
21989 let Token::String(lit) = self.advance() else {
21990 unreachable!("guard checked a string token");
21991 };
21992 let base = s.to_ascii_lowercase();
21993 let target = match (base.as_str(), with_tz) {
21994 ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
21995 ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
21996 (_, true) => CastTarget::Timestamptz,
21997 (_, false) => CastTarget::Timestamp,
21998 };
21999 Ok(Expr::Cast {
22000 expr: Box::new(Expr::Literal(Literal::String(lit))),
22001 target,
22002 })
22003 }
22004 // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
22005 // gathers the subquery's single-column rows (in its row order)
22006 // into an array. Desugared to `array_agg` over the subquery as a
22007 // derived table; out-of-line to keep parse_atom's frame small (it
22008 // sits on the recursive nesting-budget cycle).
22009 Token::Ident(s) | Token::QuotedIdent(s)
22010 if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
22011 {
22012 self.parse_array_subquery()
22013 }
22014 // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
22015 // is not a reserved token; we match by case-insensitive
22016 // ident. The opening `[` must follow immediately. v7.39 (read01
22017 // round 105) — the body moved out-of-line so its `Vec`/loop locals
22018 // leave parse_atom's frame (which sits on the nesting-budget cycle).
22019 Token::Ident(s) | Token::QuotedIdent(s)
22020 if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
22021 {
22022 self.parse_array_literal_body()
22023 }
22024 // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
22025 // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
22026 // We special-case before the generic ident dispatch so
22027 // the AGAINST clause never reaches the function-call
22028 // loop (which would mis-read `(cols) AGAINST` as a
22029 // call with no trailing modifier). The shape is
22030 // rewritten to a Boolean OR over per-column
22031 // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
22032 // term)` so the existing FTS evaluator handles
22033 // semantics — the fulltext-GIN built at CREATE TABLE
22034 // time is currently a "real index that survives dump
22035 // round-trip"; the planner hook that actually uses
22036 // it for posting-list intersection lands in a later
22037 // sub-phase (Phase 2.2b) without touching this surface.
22038 Token::Ident(s) | Token::QuotedIdent(s)
22039 if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
22040 {
22041 self.parse_match_against_atom()
22042 }
22043 Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
22044 // v7.37.43-T4 — PG-unreserved keywords are legal column /
22045 // alias names in expression context too. `release` appears
22046 // in sentori `0003_partition_events.sql` as both a column
22047 // reference (SELECT … release …) and an INSERT column list
22048 // entry. Mirrors `expect_ident_like`'s expansion of the
22049 // identifier set.
22050 other if unreserved_keyword_text(&other).is_some() => {
22051 let s = unreserved_keyword_text(&other).unwrap();
22052 self.finish_ident_atom(s)
22053 }
22054 // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
22055 // only inside `SET` before, so `SELECT @@autocommit` — which
22056 // every MySQL connector asks at handshake — was a parse error.
22057 // MariaDB accepts the bare, `@@session.` and `@@global.`
22058 // spellings alike and answers from the session's own value.
22059 Token::SessionVar(v) => {
22060 // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
22061 // has nothing to do with a `@@` engine setting: its own
22062 // per-session namespace, and an unset one reads NULL instead
22063 // of raising. Stripping every `@` (as this did) made `@x` and
22064 // `@@x` the same node, so `SELECT @x` answered "Unknown
22065 // system variable".
22066 Ok(variable_ref_atom(&v))
22067 }
22068 other => Err(ParseError {
22069 message: format!("unexpected token {other:?} in expression"),
22070 token_pos: tok_pos,
22071 }),
22072 }
22073 // After parsing the atom, fold any postfix `::vector` casts.
22074 .and_then(|atom| self.finish_postfix_casts(atom))
22075 }
22076
22077 /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
22078 /// Both bind tighter than any binary op.
22079 /// Shared cast-target parser for postfix `::TYPE` and the
22080 /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
22081 /// If the next tokens are `( N )`, consume them and return the canonical
22082 /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
22083 /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
22084 fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
22085 if !matches!(self.peek(), Token::LParen) {
22086 return None;
22087 }
22088 self.advance(); // (
22089 let n = match self.advance() {
22090 Token::Integer(n) => n,
22091 _ => return Some(base.to_string()), // malformed → drop precision
22092 };
22093 if matches!(self.peek(), Token::RParen) {
22094 self.advance();
22095 }
22096 Some(alloc::format!("{base}({n})"))
22097 }
22098
22099 fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
22100 // r1052 — `::pg_catalog.regproc` and friends: pg_dump
22101 // schema-qualifies every cast target, and `pg_catalog.X` names
22102 // exactly the builtin type X. Consume the qualifier and let
22103 // the ordinary target parse decide.
22104 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
22105 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
22106 {
22107 self.advance();
22108 self.advance();
22109 }
22110 let target = match self.advance() {
22111 Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
22112 "int" | "integer" | "int4" => {
22113 if matches!(self.peek(), Token::LBracket)
22114 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22115 {
22116 self.advance();
22117 self.advance();
22118 CastTarget::IntArray
22119 } else {
22120 CastTarget::Int
22121 }
22122 }
22123 "bigint" | "int8" => {
22124 if matches!(self.peek(), Token::LBracket)
22125 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22126 {
22127 self.advance();
22128 self.advance();
22129 CastTarget::BigIntArray
22130 } else {
22131 CastTarget::BigInt
22132 }
22133 }
22134 "float" | "double" => CastTarget::Float,
22135 "text" => {
22136 // v7.10.11 — `::TEXT[]` widens to TextArray.
22137 if matches!(self.peek(), Token::LBracket)
22138 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22139 {
22140 self.advance();
22141 self.advance();
22142 CastTarget::TextArray
22143 } else {
22144 CastTarget::Text
22145 }
22146 }
22147 "bool" | "boolean" => CastTarget::Bool,
22148 "vector" => CastTarget::Vector,
22149 "date" => CastTarget::Date,
22150 // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22151 // seconds precision through the Named path (the engine rounds
22152 // the sub-second field); bare `::timestamp` keeps the fast arm.
22153 "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22154 Some(named) => CastTarget::Named(named),
22155 None => CastTarget::Timestamp,
22156 },
22157 "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22158 Some(named) => CastTarget::Named(named),
22159 None => CastTarget::Timestamptz,
22160 },
22161 "interval" => CastTarget::Interval,
22162 "json" => CastTarget::Json,
22163 "jsonb" => CastTarget::Jsonb,
22164 // v7.39 (round 694) — these have dedicated CastTarget
22165 // variants, so they never reached the postfix `[]` handling
22166 // further down and `::regtype[]` was a SYNTAX error at the
22167 // `]`. PG has an array type for every scalar; take the
22168 // suffix here and hand the canonical `<ty>_array` name to
22169 // the engine, the same shape every other array cast uses.
22170 "regtype" if self.peek_postfix_array_brackets() => {
22171 self.advance();
22172 self.advance();
22173 CastTarget::Named(alloc::string::String::from("regtype_array"))
22174 }
22175 "regclass" if self.peek_postfix_array_brackets() => {
22176 self.advance();
22177 self.advance();
22178 CastTarget::Named(alloc::string::String::from("regclass_array"))
22179 }
22180 "regtype" => CastTarget::RegType,
22181 "regclass" => CastTarget::RegClass,
22182 // v7.12.0 — `::tsvector` / `::tsquery`.
22183 // Engine decodes the LHS text via the PG
22184 // external form parser.
22185 // v7.39 (round 352, M8) — MySQL's own cast targets.
22186 // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22187 // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22188 // such type, so they are taken only in that dialect and
22189 // fall through to the "type does not exist" arm otherwise.
22190 "signed" | "unsigned" if self.mysql_dialect => {
22191 if matches!(self.peek(), Token::Ident(k)
22192 if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22193 {
22194 self.advance();
22195 }
22196 CastTarget::Named(s.to_ascii_lowercase())
22197 }
22198 // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22199 // in MySQL: MariaDB answers '123' where the SQL-standard
22200 // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22201 // Truncating a number to its first digit is a wrong answer
22202 // with no error, so the MySQL session gets MySQL's reading.
22203 "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22204 CastTarget::Text
22205 }
22206 "tsvector" => CastTarget::TsVector,
22207 "tsquery" => CastTarget::TsQuery,
22208 // v7.17.0 — `::uuid`. Engine decodes the LHS
22209 // text via `spg_storage::parse_uuid_str`.
22210 "uuid" => CastTarget::Uuid,
22211 // v7.18 — `::bytea`. Engine decodes the LHS
22212 // text via the PG hex form (`'\xdeadbeef'`)
22213 // or escape form (`'\\x05\\x00'`). Closes
22214 // mailrs D-pre #3 reverse-acceptance gap.
22215 "bytea" => CastTarget::Bytea,
22216 // v7.37.5 ship triage — generic typed-cast escape.
22217 // Anything the long-tail PG type ident table knows
22218 // about(network/bit/geometry/multirange/etc.)flows
22219 // through `CastTarget::Named(canonical)`; the engine
22220 // resolves via `column_type_to_data_type` and dispatches
22221 // through the typed `coerce_value` path. Truly
22222 // unrecognised idents still hit the error arm below
22223 // because the engine rejects them.
22224 other => {
22225 // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22226 // `::varchar(255)`, etc. Capture into the canonical
22227 // `name(p,s)` form so `type_name_to_data_type` can
22228 // reconstruct the `DataType::Numeric { precision,
22229 // scale }` (and similar param-carrying types).
22230 let mut name = other.to_string();
22231 // v7.39 (round 281) — `::bit varying(3)` is two
22232 // words; fold the tail in so the typmod reaches the
22233 // type resolver instead of tripping the parser.
22234 if name.eq_ignore_ascii_case("bit")
22235 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22236 {
22237 self.advance();
22238 name = alloc::string::String::from("varbit");
22239 }
22240 // v7.39 (round 613) — `::character varying` is the same
22241 // two-word shape and had no fold, so the `varying` was
22242 // left behind and the cast became a bare `character`,
22243 // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22244 // `a` where PG answers `ab`. Silently, and for a spelling
22245 // pg_dump writes.
22246 if name.eq_ignore_ascii_case("character")
22247 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22248 {
22249 self.advance();
22250 name = alloc::string::String::from("varchar");
22251 }
22252 if matches!(self.peek(), Token::LParen) {
22253 let mut buf = alloc::string::String::from("(");
22254 let mut depth = 0usize;
22255 loop {
22256 match self.advance() {
22257 Token::LParen => {
22258 depth += 1;
22259 if depth > 1 {
22260 buf.push('(');
22261 }
22262 }
22263 Token::RParen => {
22264 depth -= 1;
22265 if depth == 0 {
22266 buf.push(')');
22267 break;
22268 }
22269 buf.push(')');
22270 }
22271 Token::Comma => buf.push(','),
22272 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22273 // v7.39 (round 273) — a minus used to fall
22274 // into the catch-all below and vanish, so
22275 // `::numeric(10,-2)` reached the engine as
22276 // the text `numeric(10,2)` and silently
22277 // rounded to two DECIMALS instead of to
22278 // hundreds. A dropped token is not a
22279 // no-op when it carries a sign.
22280 Token::Minus => buf.push('-'),
22281 Token::Eof => break,
22282 _ => {}
22283 }
22284 }
22285 name.push_str(&buf);
22286 }
22287 // Optional postfix `[]` widens to the array form —
22288 // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22289 // The engine's `type_name_to_data_type` recognises
22290 // the canonical `<ty>_array` form.
22291 if matches!(self.peek(), Token::LBracket)
22292 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22293 {
22294 self.advance();
22295 self.advance();
22296 name.push_str("_array");
22297 }
22298 CastTarget::Named(name)
22299 }
22300 },
22301 Token::Interval => CastTarget::Interval,
22302 // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22303 // "char" (oid 18, SPG Char1 — distinct from bare `char`
22304 // = char(1)); other quoted names resolve like idents.
22305 Token::QuotedIdent(q) => {
22306 if q.eq_ignore_ascii_case("char") {
22307 CastTarget::Named("char1".into())
22308 } else {
22309 CastTarget::Named(q.to_ascii_lowercase())
22310 }
22311 }
22312 other => {
22313 return Err(ParseError {
22314 message: format!("expected type ident after `::`, got {other:?}"),
22315 token_pos: self.consumed_pos(),
22316 });
22317 }
22318 };
22319 // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22320 // target to its array sibling. Closed-enum arms (Bool /
22321 // SmallInt / Numeric / Float / Date / …) didn't carry the
22322 // explicit widening that Text / Int / BigInt did, so
22323 // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22324 // error. The widening here mirrors the per-arm Text /
22325 // Int / BigInt logic above + folds the new ζ-A first-class
22326 // types through `CastTarget::Named("<ty>_array")`.
22327 if matches!(self.peek(), Token::LBracket)
22328 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22329 {
22330 let widened = match &target {
22331 CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22332 CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22333 // v7.39 (round 326, V43) — the two temporal types stay
22334 // distinct. Both used to widen to `timestamptz_array`, so
22335 // `::timestamp[]` named the wrong target in its own error
22336 // message and lost the zone-less identity on the way.
22337 CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22338 CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22339 CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22340 CastTarget::Json | CastTarget::Jsonb => {
22341 Some(CastTarget::Named("jsonb_array".to_string()))
22342 }
22343 CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22344 CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22345 CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22346 CastTarget::Named(name) => {
22347 let mut a = name.clone();
22348 a.push_str("_array");
22349 Some(CastTarget::Named(a))
22350 }
22351 // Int / BigInt / Text / Vector / TsVector / TsQuery /
22352 // RegType / RegClass / TextArray / IntArray /
22353 // BigIntArray already finalised — leave as is.
22354 _ => None,
22355 };
22356 if let Some(w) = widened {
22357 self.advance();
22358 self.advance();
22359 return Ok(w);
22360 }
22361 }
22362 Ok(target)
22363 }
22364
22365 fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
22366 loop {
22367 // v7.38 (read01, T9) — composite field access `(expr).field`.
22368 // A bare `a.b` is consumed as a qualified column inside the ident
22369 // atom, so a Dot only survives to this postfix position when the
22370 // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
22371 // `.*` whole-row expansion is not handled here (projection-level).
22372 if matches!(self.peek(), Token::Dot)
22373 && matches!(
22374 self.tokens.get(self.pos + 1),
22375 Some(Token::Ident(_) | Token::QuotedIdent(_))
22376 )
22377 {
22378 self.advance(); // .
22379 let field = match self.advance() {
22380 Token::Ident(s) | Token::QuotedIdent(s) => s,
22381 other => {
22382 return Err(
22383 self.err(format!("expected a field name after '.', got {other:?}"))
22384 );
22385 }
22386 };
22387 expr = Expr::FieldAccess {
22388 base: Box::new(expr),
22389 field,
22390 };
22391 continue;
22392 }
22393 if matches!(self.peek(), Token::DoubleColon) {
22394 self.advance();
22395 // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
22396 // target set to include INTERVAL (reserved Token),
22397 // TIMESTAMPTZ, and PG catalog regtype / regclass.
22398 // mailrs follow-up H3a + H3b.
22399 let target = self.parse_cast_target()?;
22400 expr = Expr::Cast {
22401 expr: Box::new(expr),
22402 target,
22403 };
22404 continue;
22405 }
22406 // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
22407 // returns NULL for out-of-range. Multiple subscripts
22408 // chain: `a[i][j]` parses left-to-right.
22409 if matches!(self.peek(), Token::LBracket) {
22410 self.advance();
22411 // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
22412 // bare index stays a subscript.
22413 let lo = if matches!(self.peek(), Token::Colon) {
22414 None
22415 } else {
22416 Some(self.parse_expr(0)?)
22417 };
22418 if matches!(self.peek(), Token::Colon) {
22419 self.advance();
22420 let hi = if matches!(self.peek(), Token::RBracket) {
22421 None
22422 } else {
22423 Some(Box::new(self.parse_expr(0)?))
22424 };
22425 if !matches!(self.peek(), Token::RBracket) {
22426 return Err(self.err(alloc::format!(
22427 "expected ']' after array slice, got {:?}",
22428 self.peek()
22429 )));
22430 }
22431 self.advance();
22432 expr = Expr::ArraySlice {
22433 target: Box::new(expr),
22434 lo: lo.map(Box::new),
22435 hi,
22436 };
22437 continue;
22438 }
22439 let index = lo.expect("non-colon branch parsed an index");
22440 if !matches!(self.peek(), Token::RBracket) {
22441 return Err(self.err(alloc::format!(
22442 "expected ']' after array index, got {:?}",
22443 self.peek()
22444 )));
22445 }
22446 self.advance();
22447 expr = Expr::ArraySubscript {
22448 target: Box::new(expr),
22449 index: Box::new(index),
22450 };
22451 continue;
22452 }
22453 // `expr AT TIME ZONE zone` — lowers to PG's own function
22454 // form timezone(zone, expr); the scalar implements the
22455 // offset shift (named zones error there — no tzdata).
22456 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
22457 && matches!(self.tokens.get(self.pos + 1),
22458 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
22459 && matches!(self.tokens.get(self.pos + 2),
22460 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
22461 {
22462 self.advance(); // AT
22463 self.advance(); // TIME
22464 self.advance(); // ZONE
22465 // Zone at comparison precedence so AND/OR stay out.
22466 let zone = self.parse_expr(6)?;
22467 expr = Expr::FunctionCall {
22468 name: "timezone".to_string(),
22469 args: alloc::vec![zone, expr],
22470 };
22471 continue;
22472 }
22473 // `expr COLLATE "name"` — SPG's single text ordering IS
22474 // byte order, i.e. the C collation. The byte-order
22475 // spellings absorb as no-ops; a locale collation would
22476 // silently sort differently from PG, so it errors
22477 // honestly instead.
22478 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
22479 self.advance();
22480 let mut cname = match self.advance() {
22481 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22482 other => {
22483 return Err(self.err(alloc::format!(
22484 "expected collation name after COLLATE, got {other:?}"
22485 )));
22486 }
22487 };
22488 // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
22489 // is how `pg_dump` writes the default one:
22490 // `… COLLATE pg_catalog.default`. Reading a single token
22491 // left the SCHEMA as the name, so the clause was refused
22492 // as an unsupported locale collation and no dump ran.
22493 if matches!(self.peek(), Token::Dot) {
22494 self.advance();
22495 cname = match self.advance() {
22496 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22497 // `default` lexes as a KEYWORD, and it is the name
22498 // pg_dump writes — the same trap round 535 hit with
22499 // TABLE / INDEX / FULL.
22500 Token::Default => alloc::string::String::from("default"),
22501 other => {
22502 return Err(self.err(alloc::format!(
22503 "expected collation name after COLLATE, got {other:?}"
22504 )));
22505 }
22506 };
22507 }
22508 let lc = cname.to_ascii_lowercase();
22509 // v7.39 (round 371, M4 P4b) — a per-expression MySQL
22510 // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
22511 // family / `binary`) forces byte-wise, which is exactly
22512 // what `BINARY expr` does — lower onto that so every fold
22513 // site (comparison, LIKE, ORDER BY) suppresses via
22514 // `is_binary_coerced`. A `_ci` family override folds, and
22515 // under the MySQL dialect the default already folds, so it
22516 // absorbs as a no-op; likewise the C / byte-order spellings.
22517 if self.mysql_dialect && (lc.ends_with("_bin") || lc == "binary") {
22518 expr = Expr::Cast {
22519 expr: alloc::boxed::Box::new(expr),
22520 target: CastTarget::Named("binary".to_string()),
22521 };
22522 continue;
22523 }
22524 let mysql_ci = self.mysql_dialect
22525 && (lc.ends_with("_ci")
22526 || matches!(lc.as_str(), "case_insensitive" | "nocase"));
22527 // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
22528 // goes to the lowering channel, the byte-order spellings
22529 // included. Round 691 recorded only the names the old
22530 // allow-list rejected, which left `ORDER BY a COLLATE "C"`
22531 // absorbed as a no-op — and once a column could declare a
22532 // collation, absorbing the clause meant the COLUMN's
22533 // collation won where the query had asked for bytes.
22534 if self.in_order_by_key && !mysql_ci {
22535 self.order_key_collation = Some(cname);
22536 continue;
22537 }
22538 if !matches!(
22539 lc.as_str(),
22540 "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
22541 ) && !mysql_ci
22542 {
22543 return Err(self.err(alloc::format!(
22544 "COLLATE {cname:?}: SPG orders text by bytes (the C \
22545 collation); locale collations are not supported yet — \
22546 use COLLATE \"C\" or drop the clause"
22547 )));
22548 }
22549 continue;
22550 }
22551 return Ok(expr);
22552 }
22553 }
22554
22555 /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
22556 /// the first token that is not one. Schema qualifiers collapse to the
22557 /// last part, which is what every other name path here does (SPG is
22558 /// single-schema).
22559 fn take_comma_separated_names(&mut self) -> Vec<String> {
22560 let mut out = Vec::new();
22561 while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
22562 self.advance();
22563 let mut last = n;
22564 while matches!(self.peek(), Token::Dot) {
22565 self.advance();
22566 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
22567 last = t;
22568 }
22569 }
22570 out.push(last);
22571 if matches!(self.peek(), Token::Comma) {
22572 self.advance();
22573 } else {
22574 break;
22575 }
22576 }
22577 out
22578 }
22579
22580 /// v7.39 (round 694) — is the next token pair a postfix `[]`?
22581 ///
22582 /// The general cast-target path tests this inline; the types with their
22583 /// own `CastTarget` variant need it as a guard on their match arm,
22584 /// which is what this exists for.
22585 fn peek_postfix_array_brackets(&self) -> bool {
22586 matches!(self.peek(), Token::LBracket)
22587 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22588 }
22589
22590 /// Parse the operator tail after a `(a, b, …)` row constructor
22591 /// and expand at parse time. `=` is the conjunction of element
22592 /// equalities; `<>` its negation; the order operators expand
22593 /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
22594 /// equalities. Anything else (a bare row value, a subquery
22595 /// RHS) errors honestly — SPG has no composite runtime value.
22596 fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
22597 fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
22598 let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
22599 lhs: Box::new(l.clone()),
22600 op: BinOp::Eq,
22601 rhs: Box::new(r.clone()),
22602 });
22603 let first = it.next().expect("row has at least two elements");
22604 it.fold(first, |acc, e| Expr::Binary {
22605 lhs: Box::new(acc),
22606 op: BinOp::And,
22607 rhs: Box::new(e),
22608 })
22609 }
22610 // Lexicographic (a,b) OP (c,d):
22611 // a STRICT c OR (a = c AND (b OP d)) — recursing right.
22612 fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
22613 if lhs.len() == 1 {
22614 return Expr::Binary {
22615 lhs: Box::new(lhs[0].clone()),
22616 op: last,
22617 rhs: Box::new(rhs[0].clone()),
22618 };
22619 }
22620 let head_strict = Expr::Binary {
22621 lhs: Box::new(lhs[0].clone()),
22622 op: strict,
22623 rhs: Box::new(rhs[0].clone()),
22624 };
22625 let head_eq = Expr::Binary {
22626 lhs: Box::new(lhs[0].clone()),
22627 op: BinOp::Eq,
22628 rhs: Box::new(rhs[0].clone()),
22629 };
22630 Expr::Binary {
22631 lhs: Box::new(head_strict),
22632 op: BinOp::Or,
22633 rhs: Box::new(Expr::Binary {
22634 lhs: Box::new(head_eq),
22635 op: BinOp::And,
22636 rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
22637 }),
22638 }
22639 }
22640 let negated_in = if matches!(self.peek(), Token::Not)
22641 && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
22642 {
22643 self.advance();
22644 true
22645 } else {
22646 false
22647 };
22648 if matches!(self.peek(), Token::In) {
22649 self.advance();
22650 if !matches!(self.peek(), Token::LParen) {
22651 return Err(self.err(alloc::format!(
22652 "expected '(' after row IN, got {:?}",
22653 self.peek()
22654 )));
22655 }
22656 self.advance();
22657 // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
22658 // not a list of literal rows. Row-vs-list decomposes to
22659 // OR-of-AND above, but the subquery's rows are only known at
22660 // runtime, so keep it as a RowInSubquery node.
22661 if matches!(self.peek(), Token::Select) {
22662 let inner = self.parse_select_stmt()?;
22663 if !matches!(self.peek(), Token::RParen) {
22664 return Err(self.err(alloc::format!(
22665 "expected ')' after row IN-subquery, got {:?}",
22666 self.peek()
22667 )));
22668 }
22669 self.advance();
22670 let Statement::Select(s) = inner else {
22671 unreachable!("parse_select_stmt always returns Statement::Select")
22672 };
22673 return Ok(Expr::RowInSubquery {
22674 row,
22675 subquery: Box::new(s),
22676 negated: negated_in,
22677 });
22678 }
22679 let mut alternatives: Vec<Expr> = Vec::new();
22680 loop {
22681 // Optional ROW keyword before the paren row.
22682 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22683 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22684 {
22685 self.advance();
22686 }
22687 if !matches!(self.peek(), Token::LParen) {
22688 return Err(self.err(alloc::format!(
22689 "expected '(' to open a row inside IN, got {:?}",
22690 self.peek()
22691 )));
22692 }
22693 self.advance();
22694 let mut rhs = alloc::vec![self.parse_expr(0)?];
22695 while matches!(self.peek(), Token::Comma) {
22696 self.advance();
22697 rhs.push(self.parse_expr(0)?);
22698 }
22699 if !matches!(self.peek(), Token::RParen) {
22700 return Err(self.err(alloc::format!(
22701 "expected ')' after row inside IN, got {:?}",
22702 self.peek()
22703 )));
22704 }
22705 self.advance();
22706 if rhs.len() != row.len() {
22707 return Err(self.err(alloc::format!(
22708 "row IN arity mismatch: left has {}, right has {}",
22709 row.len(),
22710 rhs.len()
22711 )));
22712 }
22713 alternatives.push(row_eq(&row, &rhs));
22714 if matches!(self.peek(), Token::Comma) {
22715 self.advance();
22716 continue;
22717 }
22718 break;
22719 }
22720 if !matches!(self.peek(), Token::RParen) {
22721 return Err(self.err(alloc::format!(
22722 "expected ')' to close row IN list, got {:?}",
22723 self.peek()
22724 )));
22725 }
22726 self.advance();
22727 let mut it = alternatives.into_iter();
22728 let first = it.next().expect("IN list has at least one row");
22729 let combined = it.fold(first, |acc, e| Expr::Binary {
22730 lhs: Box::new(acc),
22731 op: BinOp::Or,
22732 rhs: Box::new(e),
22733 });
22734 return Ok(maybe_not(combined, negated_in));
22735 }
22736 // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
22737 // two periods share at least one time point. Each pair is
22738 // normalised with least/greatest (PG accepts the endpoints
22739 // in either order), then lowered to the standard
22740 // `start1 < end2 AND start2 < end1` form.
22741 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
22742 if row.len() != 2 {
22743 return Err(self.err(alloc::format!(
22744 "OVERLAPS needs (start, end) pairs; left side has {} elements",
22745 row.len()
22746 )));
22747 }
22748 self.advance();
22749 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22750 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22751 {
22752 self.advance();
22753 }
22754 if !matches!(self.peek(), Token::LParen) {
22755 return Err(self.err(alloc::format!(
22756 "expected '(' after OVERLAPS, got {:?}",
22757 self.peek()
22758 )));
22759 }
22760 self.advance();
22761 let r0 = self.parse_expr(0)?;
22762 if !matches!(self.peek(), Token::Comma) {
22763 return Err(self.err(alloc::format!(
22764 "OVERLAPS needs (start, end) on the right, got {:?}",
22765 self.peek()
22766 )));
22767 }
22768 self.advance();
22769 let r1 = self.parse_expr(0)?;
22770 if !matches!(self.peek(), Token::RParen) {
22771 return Err(self.err(alloc::format!(
22772 "expected ')' after OVERLAPS pair, got {:?}",
22773 self.peek()
22774 )));
22775 }
22776 self.advance();
22777 let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
22778 name: String::from(name),
22779 args: alloc::vec![a.clone(), b.clone()],
22780 };
22781 let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
22782 lhs: Box::new(lhs),
22783 op: BinOp::Lt,
22784 rhs: Box::new(rhs),
22785 };
22786 return Ok(Expr::Binary {
22787 lhs: Box::new(lt(
22788 pair_fn("least", &row[0], &row[1]),
22789 pair_fn("greatest", &r0, &r1),
22790 )),
22791 op: BinOp::And,
22792 rhs: Box::new(lt(
22793 pair_fn("least", &r0, &r1),
22794 pair_fn("greatest", &row[0], &row[1]),
22795 )),
22796 });
22797 }
22798 // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
22799 // PG, `IS NULL` is true only when EVERY field is NULL, and
22800 // `IS NOT NULL` is true only when every field is non-NULL — the
22801 // latter is NOT the negation of the former (a mixed row is
22802 // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
22803 // which reproduces exactly that all-fields semantics.
22804 if matches!(self.peek(), Token::Is) {
22805 self.advance();
22806 let negated = if matches!(self.peek(), Token::Not) {
22807 self.advance();
22808 true
22809 } else {
22810 false
22811 };
22812 if !matches!(self.peek(), Token::Null) {
22813 return Err(self.err(alloc::format!(
22814 "expected NULL after row IS [NOT], got {:?}",
22815 self.peek()
22816 )));
22817 }
22818 self.advance();
22819 let mut it = row.iter().map(|e| Expr::IsNull {
22820 expr: Box::new(e.clone()),
22821 negated,
22822 });
22823 let first = it.next().expect("row has at least two elements");
22824 return Ok(it.fold(first, |acc, e| Expr::Binary {
22825 lhs: Box::new(acc),
22826 op: BinOp::And,
22827 rhs: Box::new(e),
22828 }));
22829 }
22830 let op = match self.peek() {
22831 Token::Eq => BinOp::Eq,
22832 Token::NotEq => BinOp::NotEq,
22833 Token::Lt => BinOp::Lt,
22834 Token::LtEq => BinOp::LtEq,
22835 Token::Gt => BinOp::Gt,
22836 Token::GtEq => BinOp::GtEq,
22837 // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
22838 // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
22839 // constructor value, identical to the `ROW(a, b, …)` keyword form:
22840 // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
22841 // (`::text`, `.field`) applies at the caller just as it does for the
22842 // ROW(...) node. All the comparison / predicate forms returned above.
22843 _ => {
22844 return Ok(Expr::FunctionCall {
22845 name: String::from("row"),
22846 args: row,
22847 });
22848 }
22849 };
22850 self.advance();
22851 // Optional ROW keyword before the paren row.
22852 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22853 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22854 {
22855 self.advance();
22856 }
22857 if !matches!(self.peek(), Token::LParen) {
22858 return Err(self.err(alloc::format!(
22859 "expected '(' to open the right-hand row, got {:?}",
22860 self.peek()
22861 )));
22862 }
22863 self.advance();
22864 // `(a, b) <op> (SELECT x, y)` — compare against a single-row
22865 // subquery. Kept as a node (the subquery's row is a runtime value);
22866 // the literal-RHS form below still decomposes at parse time.
22867 if matches!(self.peek(), Token::Select) {
22868 let inner = self.parse_select_stmt()?;
22869 if !matches!(self.peek(), Token::RParen) {
22870 return Err(self.err(alloc::format!(
22871 "expected ')' after row comparison subquery, got {:?}",
22872 self.peek()
22873 )));
22874 }
22875 self.advance();
22876 let Statement::Select(s) = inner else {
22877 unreachable!("parse_select_stmt always returns Statement::Select")
22878 };
22879 return Ok(Expr::RowCmpSubquery {
22880 row,
22881 op,
22882 subquery: Box::new(s),
22883 });
22884 }
22885 let mut rhs = alloc::vec![self.parse_expr(0)?];
22886 while matches!(self.peek(), Token::Comma) {
22887 self.advance();
22888 rhs.push(self.parse_expr(0)?);
22889 }
22890 if !matches!(self.peek(), Token::RParen) {
22891 return Err(self.err(alloc::format!(
22892 "expected ')' after right-hand row, got {:?}",
22893 self.peek()
22894 )));
22895 }
22896 self.advance();
22897 if rhs.len() != row.len() {
22898 // v7.39 (round 239) — PG's wording (42601).
22899 return Err(self.err("unequal number of entries in row expressions".to_string()));
22900 }
22901 Ok(match op {
22902 BinOp::Eq => row_eq(&row, &rhs),
22903 BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
22904 BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
22905 BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
22906 BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
22907 BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
22908 _ => unreachable!("op restricted above"),
22909 })
22910 }
22911
22912 /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
22913 /// escape character becomes the matcher's default backslash:
22914 /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
22915 /// → the char itself, and any pre-existing backslash escapes
22916 /// itself so it stays literal. Both operands must be string
22917 /// literals — a runtime pattern would need matcher support.
22918 fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
22919 let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
22920 (&pattern, &esc)
22921 else {
22922 return Err(
22923 "LIKE ... ESCAPE requires string-literal pattern and escape \
22924 (runtime escape characters are not supported yet)"
22925 .into(),
22926 );
22927 };
22928 // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
22929 // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
22930 // multi-character escape is an error.
22931 let esc_ch: Option<char> = {
22932 let mut ch_iter = e.chars();
22933 match (ch_iter.next(), ch_iter.next()) {
22934 (Some(c), None) => Some(c),
22935 (None, _) => None,
22936 (Some(_), Some(_)) => {
22937 return Err(alloc::format!(
22938 "ESCAPE must be a single character, got {e:?}"
22939 ));
22940 }
22941 }
22942 };
22943 let mut out = String::with_capacity(p.len() + 4);
22944 let mut chars = p.chars();
22945 while let Some(c) = chars.next() {
22946 if Some(c) == esc_ch {
22947 match chars.next() {
22948 // Escaped wildcard / escaped escape → keep the
22949 // next char literal via backslash.
22950 Some(next) => {
22951 out.push('\\');
22952 out.push(next);
22953 }
22954 None => {
22955 return Err("LIKE pattern ends with the escape character".into());
22956 }
22957 }
22958 } else if c == '\\' && esc_ch != Some('\\') {
22959 // A raw backslash is literal under a custom (or absent) escape
22960 // — escape it for the backslash-based matcher.
22961 out.push('\\');
22962 out.push('\\');
22963 } else {
22964 out.push(c);
22965 }
22966 }
22967 Ok(Expr::Literal(Literal::String(out)))
22968 }
22969
22970 /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
22971 /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
22972 /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
22973 /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
22974 /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
22975 /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
22976 /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
22977 /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
22978 /// array expression errors honestly rather than silently mismatching.
22979 fn try_like_any_all(
22980 &mut self,
22981 base: &Expr,
22982 negated: bool,
22983 case_insensitive: bool,
22984 ) -> Result<Option<Expr>, ParseError> {
22985 let is_any = match self.peek() {
22986 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
22987 Token::Ident(s)
22988 if s.eq_ignore_ascii_case("any")
22989 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
22990 {
22991 true
22992 }
22993 _ => return Ok(None),
22994 };
22995 self.advance(); // ANY / ALL
22996 self.advance(); // '('
22997 let arr = self.parse_expr(0)?;
22998 if !matches!(self.peek(), Token::RParen) {
22999 return Err(self.err(format!(
23000 "expected ')' after LIKE {} argument, got {:?}",
23001 if is_any { "ANY" } else { "ALL" },
23002 self.peek()
23003 )));
23004 }
23005 self.advance(); // ')'
23006 let Expr::Array(items) = arr else {
23007 return Err(self.err(
23008 "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
23009 ));
23010 };
23011 let mut clauses = items.into_iter().map(|p| Expr::Like {
23012 expr: Box::new(base.clone()),
23013 pattern: Box::new(p),
23014 negated,
23015 case_insensitive,
23016 });
23017 let Some(first) = clauses.next() else {
23018 // ANY(empty) = FALSE, ALL(empty) = TRUE.
23019 return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
23020 };
23021 let op = if is_any { BinOp::Or } else { BinOp::And };
23022 let combined = clauses.fold(first, |acc, c| Expr::Binary {
23023 lhs: Box::new(acc),
23024 op,
23025 rhs: Box::new(c),
23026 });
23027 Ok(Some(combined))
23028 }
23029
23030 /// `x BETWEEN low AND high` → `(x >= low) AND (x <= high)`, wrapped in
23031 /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
23032 /// `AND` is not swallowed.
23033 fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23034 self.advance(); // BETWEEN
23035 // SYMMETRIC — the bounds may arrive in either order; both
23036 // orientations OR together. ASYMMETRIC is the default and
23037 // absorbs as noise.
23038 let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
23039 {
23040 self.advance();
23041 true
23042 } else {
23043 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
23044 self.advance();
23045 }
23046 false
23047 };
23048 let low = self.parse_expr(6)?;
23049 if !matches!(self.peek(), Token::And) {
23050 return Err(self.err(format!(
23051 "expected AND after BETWEEN low bound, got {:?}",
23052 self.peek()
23053 )));
23054 }
23055 self.advance();
23056 let high = self.parse_expr(6)?;
23057 let target = Box::new(expr);
23058 let range = |lo: Expr, hi: Expr| Expr::Binary {
23059 lhs: Box::new(Expr::Binary {
23060 lhs: target.clone(),
23061 op: BinOp::GtEq,
23062 rhs: Box::new(lo),
23063 }),
23064 op: BinOp::And,
23065 rhs: Box::new(Expr::Binary {
23066 lhs: target.clone(),
23067 op: BinOp::LtEq,
23068 rhs: Box::new(hi),
23069 }),
23070 };
23071 let combined = if symmetric {
23072 Expr::Binary {
23073 lhs: Box::new(range(low.clone(), high.clone())),
23074 op: BinOp::Or,
23075 rhs: Box::new(range(high, low)),
23076 }
23077 } else {
23078 range(low, high)
23079 };
23080 Ok(maybe_not(combined, negated))
23081 }
23082
23083 /// `x IN (a, b, c)` → chained OR of equalities. Empty list collapses
23084 /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
23085 /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
23086 /// Caller already consumed the leading `WITH` ident.
23087 /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
23088 /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
23089 /// self-reference that appears more than once in a single term.
23090 fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
23091 use crate::ast::{CteBody, SelectStatement};
23092 if !cte.recursive {
23093 return Ok(());
23094 }
23095 let CteBody::Select(body) = &cte.body else {
23096 return Ok(());
23097 };
23098 // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
23099 // check the anchor and every peer term.
23100 let has_order = |s: &SelectStatement| !s.order_by.is_empty();
23101 let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
23102 if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
23103 return Err(self.err(String::from(
23104 "ORDER BY in a recursive query is not implemented",
23105 )));
23106 }
23107 if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
23108 return Err(self.err(String::from(
23109 "LIMIT in a recursive query is not implemented",
23110 )));
23111 }
23112 let self_refs = |s: &SelectStatement| -> usize {
23113 let Some(from) = &s.from else {
23114 return 0;
23115 };
23116 let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
23117 for j in &from.joins {
23118 if j.table.name.eq_ignore_ascii_case(&cte.name) {
23119 n += 1;
23120 }
23121 }
23122 n
23123 };
23124 if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
23125 return Err(self.err(alloc::format!(
23126 "recursive reference to query \"{}\" must not appear more than once",
23127 cte.name
23128 )));
23129 }
23130 // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
23131 // apply only when the body actually references itself (a non-self-
23132 // referencing CTE under WITH RECURSIVE may use any set-op shape).
23133 let anchor_refs = self_refs(body);
23134 let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
23135 if anchor_refs > 0 || union_refs {
23136 // Shape: the top level must be UNION [ALL] arms only. A self-ref
23137 // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
23138 // "does not have the form" error — SPG used to compute a value.
23139 if body.unions.is_empty()
23140 || body.unions.iter().any(|(k, _)| {
23141 !matches!(
23142 k,
23143 crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
23144 )
23145 })
23146 {
23147 return Err(self.err(alloc::format!(
23148 "recursive query \"{}\" does not have the form non-recursive-term \
23149 UNION [ALL] recursive-term",
23150 cte.name
23151 )));
23152 }
23153 if anchor_refs > 0 {
23154 return Err(self.err(alloc::format!(
23155 "recursive reference to query \"{}\" must not appear within its non-recursive term",
23156 cte.name
23157 )));
23158 }
23159 }
23160 let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23161 for (_, u) in &body.unions {
23162 if self_refs(u) == 0 {
23163 continue;
23164 }
23165 // The self-reference must not sit on the nullable side of an outer
23166 // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23167 if let Some(from) = &u.from {
23168 for (i, j) in from.joins.iter().enumerate() {
23169 let left_has_self = is_self(&from.primary)
23170 || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23171 let violated = match j.kind {
23172 crate::ast::JoinKind::Left => is_self(&j.table),
23173 crate::ast::JoinKind::Right => left_has_self,
23174 crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23175 _ => false,
23176 };
23177 if violated {
23178 return Err(self.err(alloc::format!(
23179 "recursive reference to query \"{}\" must not appear within an outer join",
23180 cte.name
23181 )));
23182 }
23183 }
23184 }
23185 // No aggregates at the top level of the recursive term (SPG used
23186 // to run them and surface a misleading downstream error).
23187 let mut items_and_having: Vec<&Expr> = Vec::new();
23188 for it in &u.items {
23189 if let crate::ast::SelectItem::Expr { expr, .. } = it {
23190 items_and_having.push(expr);
23191 }
23192 }
23193 if let Some(h) = &u.having {
23194 items_and_having.push(h);
23195 }
23196 for e in items_and_having {
23197 if expr_has_toplevel_aggregate(e) {
23198 return Err(self.err(String::from(
23199 "aggregate functions are not allowed in a recursive query's recursive term",
23200 )));
23201 }
23202 }
23203 }
23204 // A self-reference inside a sublink expression (EXISTS / IN / scalar
23205 // subquery) anywhere in the body is rejected; a plain FROM derived
23206 // table is legal in PG and untouched here.
23207 let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23208 all_terms.extend(body.unions.iter().map(|(_, u)| u));
23209 for term in all_terms {
23210 if select_has_self_ref_in_sublink(term, &cte.name) {
23211 return Err(self.err(alloc::format!(
23212 "recursive reference to query \"{}\" must not appear within a subquery",
23213 cte.name
23214 )));
23215 }
23216 }
23217 Ok(())
23218 }
23219
23220 /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23221 /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23222 /// right after parse so the engine sees a plain recursive CTE with the
23223 /// tracking columns already projected. DEPTH FIRST and CYCLE are
23224 /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23225 /// text-rendered rows can't provide, and errors honestly.
23226 fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23227 use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23228 if cte.search.is_none() && cte.cycle.is_none() {
23229 return Ok(());
23230 }
23231 let cte_name = cte.name.clone();
23232 let col_names = cte.column_overrides.clone();
23233 if col_names.is_empty() {
23234 return Err(
23235 self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23236 );
23237 }
23238 let search = cte.search.take();
23239 let cycle = cte.cycle.take();
23240 let mut extra_cols: Vec<String> = Vec::new();
23241 let col_ref = |name: &str| {
23242 Expr::Column(ColumnName {
23243 qualifier: Some(cte_name.clone()),
23244 name: name.to_string(),
23245 })
23246 };
23247 // Position of a SEARCH/CYCLE column within the CTE's column list.
23248 let pos_of = |name: &str| -> Result<usize, ParseError> {
23249 col_names
23250 .iter()
23251 .position(|c| c.eq_ignore_ascii_case(name))
23252 .ok_or_else(|| {
23253 self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23254 })
23255 };
23256 let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23257 let mut args = Vec::with_capacity(positions.len());
23258 for &p in positions {
23259 match items.get(p) {
23260 Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23261 _ => {
23262 return Err(self.err(
23263 "SEARCH/CYCLE column maps to a non-expression select item".into(),
23264 ));
23265 }
23266 }
23267 }
23268 Ok(Expr::FunctionCall {
23269 name: "row".into(),
23270 args,
23271 })
23272 };
23273 let CteBody::Select(body) = &mut cte.body else {
23274 return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23275 };
23276 if body.unions.is_empty() {
23277 return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23278 }
23279 let rec = body.unions.len() - 1; // recursive term = last UNION peer
23280
23281 if let Some(srch) = search {
23282 // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23283 // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23284 // no typed `record[]`, but element-wise array ORDER BY is correct
23285 // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23286 // exactly onto a typed array: DEPTH is the root→node path
23287 // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23288 // orders numerically (multi-digit keys included), matching PG.
23289 //
23290 // A multi-column BY would need a record[] to keep the per-node key
23291 // tuple orderable, which SPG can't express — error honestly there
23292 // rather than mis-order.
23293 if srch.by_columns.len() != 1 {
23294 return Err(self.err(
23295 "SEARCH … BY with multiple columns needs typed record[] ordering \
23296 SPG doesn't have yet; a single BY column is supported"
23297 .into(),
23298 ));
23299 }
23300 let key_pos = pos_of(&srch.by_columns[0])?;
23301 let base_key = match body.items.get(key_pos) {
23302 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23303 _ => {
23304 return Err(
23305 self.err("SEARCH BY column maps to a non-expression select item".into())
23306 );
23307 }
23308 };
23309 let rec_key = match body.unions[rec].1.items.get(key_pos) {
23310 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23311 _ => {
23312 return Err(
23313 self.err("SEARCH BY column maps to a non-expression select item".into())
23314 );
23315 }
23316 };
23317 if srch.depth_first {
23318 // base: ARRAY[key]; rec: array_append(cte.set, key).
23319 body.items.push(SelectItem::Expr {
23320 expr: Expr::Array(alloc::vec![base_key]),
23321 alias: Some(srch.set_column.clone()),
23322 });
23323 body.unions[rec].1.items.push(SelectItem::Expr {
23324 expr: Expr::FunctionCall {
23325 name: "array_append".into(),
23326 args: alloc::vec![col_ref(&srch.set_column), rec_key],
23327 },
23328 alias: Some(srch.set_column.clone()),
23329 });
23330 } else {
23331 // BREADTH: [depth, key]; depth starts at 0 and increments. The
23332 // leading depth element dominates the element-wise comparison,
23333 // so shallower rows sort first, then by key — PG's (depth, key).
23334 body.items.push(SelectItem::Expr {
23335 expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
23336 alias: Some(srch.set_column.clone()),
23337 });
23338 // rec depth = cte.set[1] + 1.
23339 let parent_depth = Expr::ArraySubscript {
23340 target: Box::new(col_ref(&srch.set_column)),
23341 index: Box::new(Expr::Literal(Literal::Integer(1))),
23342 };
23343 body.unions[rec].1.items.push(SelectItem::Expr {
23344 expr: Expr::Array(alloc::vec![
23345 Expr::Binary {
23346 lhs: Box::new(parent_depth),
23347 op: BinOp::Add,
23348 rhs: Box::new(Expr::Literal(Literal::Integer(1))),
23349 },
23350 rec_key,
23351 ]),
23352 alias: Some(srch.set_column.clone()),
23353 });
23354 }
23355 extra_cols.push(srch.set_column);
23356 }
23357
23358 if let Some(cyc) = cycle {
23359 let positions: Vec<usize> = cyc
23360 .columns
23361 .iter()
23362 .map(|c| pos_of(c))
23363 .collect::<Result<_, _>>()?;
23364 // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
23365 // cast it to text for the cycle path: membership only needs equality,
23366 // and the record text form gives SPG a TextArray path (SPG has no
23367 // typed record[] array). Cycle detection is unaffected.
23368 let base_row = Expr::Cast {
23369 expr: Box::new(row_of(&body.items, &positions)?),
23370 target: CastTarget::Text,
23371 };
23372 let rec_row = Expr::Cast {
23373 expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
23374 target: CastTarget::Text,
23375 };
23376 let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
23377 let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
23378 // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
23379 body.items.push(SelectItem::Expr {
23380 expr: Expr::Literal(dflt.clone()),
23381 alias: Some(cyc.mark_column.clone()),
23382 });
23383 body.items.push(SelectItem::Expr {
23384 expr: Expr::Array(alloc::vec![base_row]),
23385 alias: Some(cyc.path_column.clone()),
23386 });
23387 // rec mark: ROW(cols) already in the path → cycle.
23388 let hit = Expr::AnyAll {
23389 expr: Box::new(rec_row.clone()),
23390 op: BinOp::Eq,
23391 array: Box::new(col_ref(&cyc.path_column)),
23392 is_any: true,
23393 };
23394 let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
23395 Expr::Case {
23396 operand: None,
23397 branches: alloc::vec![(hit, Expr::Literal(mark))],
23398 else_branch: Some(Box::new(Expr::Literal(dflt))),
23399 }
23400 } else {
23401 hit
23402 };
23403 body.unions[rec].1.items.push(SelectItem::Expr {
23404 expr: mark_expr,
23405 alias: Some(cyc.mark_column.clone()),
23406 });
23407 // rec path: array_append(cte.path, ROW(cols)).
23408 body.unions[rec].1.items.push(SelectItem::Expr {
23409 expr: Expr::FunctionCall {
23410 name: "array_append".into(),
23411 args: alloc::vec![col_ref(&cyc.path_column), rec_row],
23412 },
23413 alias: Some(cyc.path_column.clone()),
23414 });
23415 // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
23416 let stop = Expr::Unary {
23417 op: UnOp::Not,
23418 expr: Box::new(col_ref(&cyc.mark_column)),
23419 };
23420 let w = &mut body.unions[rec].1.where_;
23421 *w = Some(match w.take() {
23422 Some(prev) => Expr::Binary {
23423 lhs: Box::new(prev),
23424 op: BinOp::And,
23425 rhs: Box::new(stop),
23426 },
23427 None => stop,
23428 });
23429 extra_cols.push(cyc.mark_column);
23430 extra_cols.push(cyc.path_column);
23431 }
23432 cte.column_overrides.extend(extra_cols);
23433 Ok(())
23434 }
23435
23436 /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
23437 /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
23438 fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
23439 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
23440 return Ok(None);
23441 }
23442 self.advance(); // SEARCH
23443 let depth_first = match self.peek() {
23444 Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
23445 Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
23446 other => {
23447 return Err(self.err(format!(
23448 "expected DEPTH or BREADTH after SEARCH, got {other:?}"
23449 )));
23450 }
23451 };
23452 self.advance();
23453 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
23454 return Err(self.err(format!(
23455 "expected FIRST after SEARCH mode, got {:?}",
23456 self.peek()
23457 )));
23458 }
23459 self.advance();
23460 if !self.peek_is_by() {
23461 return Err(self.err(format!(
23462 "expected BY after SEARCH … FIRST, got {:?}",
23463 self.peek()
23464 )));
23465 }
23466 self.advance();
23467 let mut by_columns = alloc::vec![self.expect_ident_like()?];
23468 while matches!(self.peek(), Token::Comma) {
23469 self.advance();
23470 by_columns.push(self.expect_ident_like()?);
23471 }
23472 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23473 return Err(self.err(format!(
23474 "expected SET in SEARCH clause, got {:?}",
23475 self.peek()
23476 )));
23477 }
23478 self.advance();
23479 let set_column = self.expect_ident_like()?;
23480 Ok(Some(crate::ast::SearchClause {
23481 depth_first,
23482 by_columns,
23483 set_column,
23484 }))
23485 }
23486
23487 /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
23488 /// USING pathcol`. Returns None when the next token isn't CYCLE.
23489 fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
23490 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
23491 return Ok(None);
23492 }
23493 self.advance(); // CYCLE
23494 let mut columns = alloc::vec![self.expect_ident_like()?];
23495 while matches!(self.peek(), Token::Comma) {
23496 self.advance();
23497 columns.push(self.expect_ident_like()?);
23498 }
23499 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23500 return Err(self.err(format!(
23501 "expected SET in CYCLE clause, got {:?}",
23502 self.peek()
23503 )));
23504 }
23505 self.advance();
23506 let mark_column = self.expect_ident_like()?;
23507 let mut mark_value = None;
23508 let mut default_value = None;
23509 if matches!(self.peek(), Token::To) {
23510 self.advance();
23511 mark_value = Some(self.parse_cycle_literal()?);
23512 if !matches!(self.peek(), Token::Default) {
23513 return Err(self.err(format!(
23514 "expected DEFAULT after CYCLE … TO, got {:?}",
23515 self.peek()
23516 )));
23517 }
23518 self.advance();
23519 default_value = Some(self.parse_cycle_literal()?);
23520 }
23521 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
23522 return Err(self.err(format!(
23523 "expected USING in CYCLE clause, got {:?}",
23524 self.peek()
23525 )));
23526 }
23527 self.advance();
23528 let path_column = self.expect_ident_like()?;
23529 Ok(Some(crate::ast::CycleClause {
23530 columns,
23531 mark_column,
23532 mark_value,
23533 default_value,
23534 path_column,
23535 }))
23536 }
23537
23538 /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
23539 /// literal (string / bool / number) in PG.
23540 fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
23541 match self.parse_expr(0)? {
23542 Expr::Literal(l) => Ok(l),
23543 other => Err(self.err(format!(
23544 "CYCLE mark/default value must be a literal, got {other:?}"
23545 ))),
23546 }
23547 }
23548
23549 fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
23550 // v4.22: WITH RECURSIVE — optional keyword right after WITH.
23551 // Comes through as an identifier; consume it if present and
23552 // mark every CTE in the clause as recursive (PG semantics —
23553 // the flag is per-WITH, not per-CTE).
23554 let mut recursive = false;
23555 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
23556 && s.eq_ignore_ascii_case("recursive")
23557 {
23558 self.advance();
23559 recursive = true;
23560 }
23561 let mut ctes = Vec::new();
23562 loop {
23563 let name = self.expect_ident_like()?;
23564 // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
23565 // PG uses these to rename the body's output columns; we
23566 // do the same below by overriding `columns[i].name`.
23567 let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
23568 self.advance();
23569 let mut names = Vec::new();
23570 loop {
23571 names.push(self.expect_ident_like()?);
23572 if matches!(self.peek(), Token::Comma) {
23573 self.advance();
23574 continue;
23575 }
23576 break;
23577 }
23578 if !matches!(self.peek(), Token::RParen) {
23579 return Err(self.err(format!(
23580 "expected ')' to close CTE column list, got {:?}",
23581 self.peek()
23582 )));
23583 }
23584 self.advance();
23585 names
23586 } else {
23587 Vec::new()
23588 };
23589 // AS is a reserved Token::As (used by SELECT-item / FROM
23590 // aliasing) — handle it specially rather than as a bare
23591 // ident.
23592 if !matches!(self.peek(), Token::As) {
23593 return Err(self.err(format!(
23594 "expected AS after CTE name {name:?}, got {:?}",
23595 self.peek()
23596 )));
23597 }
23598 self.advance();
23599 // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
23600 // MATERIALIZED` optimizer hints. SPG materialises every
23601 // CTE, so both spellings are accepted and absorbed.
23602 if matches!(self.peek(), Token::Not) {
23603 self.advance(); // NOT
23604 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23605 if s.eq_ignore_ascii_case("materialized"))
23606 {
23607 self.advance();
23608 } else {
23609 return Err(self.err(format!(
23610 "expected MATERIALIZED after AS NOT, got {:?}",
23611 self.peek()
23612 )));
23613 }
23614 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23615 if s.eq_ignore_ascii_case("materialized"))
23616 {
23617 self.advance();
23618 }
23619 if !matches!(self.peek(), Token::LParen) {
23620 return Err(self.err(format!(
23621 "expected '(' after AS in WITH clause, got {:?}",
23622 self.peek()
23623 )));
23624 }
23625 self.advance();
23626 // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
23627 // RETURNING) as the CTE body in addition to SELECT.
23628 // PG writable CTE semantics. UPDATE / DELETE come in as
23629 // bare Idents (lexer keeps SELECT / INSERT as reserved
23630 // tokens but treats the rest of DML as case-insensitive
23631 // idents).
23632 let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23633 let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23634 let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23635 let body = match self.peek() {
23636 Token::Select => {
23637 let inner = self.parse_select_stmt()?;
23638 let Statement::Select(s) = inner else {
23639 unreachable!("parse_select_stmt returns Select");
23640 };
23641 crate::ast::CteBody::Select(s)
23642 }
23643 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23644 // `SELECT * FROM t` this way and accepts it wherever a
23645 // SELECT goes, so the CTE body dispatch needs its own
23646 // arm: this match is keyed on the FIRST token, and
23647 // `Token::Table` fell through to a tail that then
23648 // rejected what it got. `parse_table_shorthand` has
23649 // returned a desugared SelectStatement since the
23650 // shorthand landed — only the routing was missing.
23651 // Round 868 found this by putting the shorthand in a
23652 // subquery; every earlier check used a top-level form.
23653 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23654 // `SELECT * FROM t` this way and accepts it wherever a
23655 // SELECT goes, so the CTE body dispatch needs its own
23656 // arm: this match is keyed on the FIRST token, and
23657 // `Token::Table` fell through to a tail that rejected
23658 // what it got. `parse_table_shorthand` has returned a
23659 // desugared SelectStatement since the shorthand landed —
23660 // only the routing was missing, here and in the derived
23661 // table's second-token gate. Round 868 found both by
23662 // putting the shorthand in a subquery; every earlier
23663 // check had used a top-level form.
23664 Token::Table
23665 if matches!(
23666 self.tokens.get(self.pos + 1),
23667 Some(Token::Ident(_) | Token::QuotedIdent(_))
23668 ) =>
23669 {
23670 let mut head = self.parse_table_shorthand()?;
23671 self.parse_setop_chain_into(&mut head)?;
23672 self.parse_select_tail_into(&mut head)?;
23673 crate::ast::CteBody::Select(head)
23674 }
23675 // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
23676 // WITH t(a) AS (VALUES (1), (2)) … lowers through
23677 // the shared rows helper onto a Select body.
23678 Token::Values => {
23679 self.advance(); // VALUES
23680 let mut head = self.parse_values_rows_body()?;
23681 // A VALUES seed can head a set-operation chain —
23682 // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
23683 // SELECT n+1 FROM t …). Attach any trailing
23684 // UNION / INTERSECT / EXCEPT peers so the
23685 // recursive-CTE body parses like the SELECT seed.
23686 self.parse_setop_chain_into(&mut head)?;
23687 crate::ast::CteBody::Select(head)
23688 }
23689 Token::Insert => {
23690 let inner = self.parse_one_statement()?;
23691 let Statement::Insert(s) = inner else {
23692 unreachable!("Token::Insert routes to Insert");
23693 };
23694 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23695 }
23696 _ if is_update_kw => {
23697 let inner = self.parse_one_statement()?;
23698 let Statement::Update(s) = inner else {
23699 return Err(
23700 self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
23701 );
23702 };
23703 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23704 }
23705 _ if is_delete_kw => {
23706 let inner = self.parse_one_statement()?;
23707 let Statement::Delete(s) = inner else {
23708 return Err(
23709 self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
23710 );
23711 };
23712 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23713 }
23714 // v7.39 (round 149) — PG 17 allows MERGE as a
23715 // data-modifying CTE body.
23716 _ if is_merge_kw => {
23717 let inner = self.parse_one_statement()?;
23718 let Statement::Merge(s) = inner else {
23719 return Err(
23720 self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
23721 );
23722 };
23723 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23724 }
23725 // v7.39 (round 151) — a CTE body may itself be
23726 // WITH-headed (PG grammar: PreparableStmt carries its
23727 // own with_clause). The nested statement keeps its own
23728 // ctes; the modifying-CTE-at-top-level rule is enforced
23729 // at execution.
23730 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
23731 self.advance(); // WITH
23732 match self.parse_with_cte_then_select()? {
23733 Statement::Select(s) => crate::ast::CteBody::Select(s),
23734 Statement::Insert(s) => {
23735 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23736 }
23737 Statement::Update(s) => {
23738 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23739 }
23740 Statement::Delete(s) => {
23741 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23742 }
23743 Statement::Merge(s) => {
23744 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23745 }
23746
23747 other => {
23748 return Err(self.err(format!(
23749 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23750 )));
23751 }
23752 }
23753 }
23754 other => {
23755 return Err(self.err(format!(
23756 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23757 )));
23758 }
23759 };
23760 if !matches!(self.peek(), Token::RParen) {
23761 return Err(self.err(format!(
23762 "expected ')' after CTE body, got {:?}",
23763 self.peek()
23764 )));
23765 }
23766 self.advance();
23767 // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
23768 // CTE, desugared into extra body columns by the engine.
23769 let search = self.parse_cte_search_clause()?;
23770 let cycle = self.parse_cte_cycle_clause()?;
23771 let mut cte = crate::ast::Cte {
23772 name,
23773 body,
23774 recursive,
23775 column_overrides,
23776 search,
23777 cycle,
23778 };
23779 self.validate_recursive_cte(&cte)?;
23780 self.desugar_cte_search_cycle(&mut cte)?;
23781 ctes.push(cte);
23782 if matches!(self.peek(), Token::Comma) {
23783 self.advance();
23784 continue;
23785 }
23786 break;
23787 }
23788 // v7.37.43-T4.4 — the outer body may be SELECT (classical),
23789 // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
23790 // the parsed CTEs to whichever statement the body produces.
23791 let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23792 let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23793 let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23794 match self.peek() {
23795 Token::Select => {
23796 let body_stmt = self.parse_select_stmt()?;
23797 let Statement::Select(mut body) = body_stmt else {
23798 unreachable!()
23799 };
23800 body.ctes = ctes;
23801 Ok(Statement::Select(body))
23802 }
23803 Token::Insert => {
23804 let body_stmt = self.parse_one_statement()?;
23805 let Statement::Insert(mut body) = body_stmt else {
23806 unreachable!()
23807 };
23808 body.ctes = ctes;
23809 Ok(Statement::Insert(body))
23810 }
23811 _ if outer_is_update => {
23812 let body_stmt = self.parse_one_statement()?;
23813 let Statement::Update(mut body) = body_stmt else {
23814 return Err(self.err(format!("expected UPDATE after WITH clause")));
23815 };
23816 body.ctes = ctes;
23817 Ok(Statement::Update(body))
23818 }
23819 _ if outer_is_delete => {
23820 let body_stmt = self.parse_one_statement()?;
23821 let Statement::Delete(mut body) = body_stmt else {
23822 return Err(self.err(format!("expected DELETE after WITH clause")));
23823 };
23824 body.ctes = ctes;
23825 Ok(Statement::Delete(body))
23826 }
23827 // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
23828 // WITH RECURSIVE is rejected with PG's exact message
23829 // (parse analysis, transformWithClause).
23830 _ if outer_is_merge => {
23831 if recursive {
23832 return Err(self.err(String::from(
23833 "WITH RECURSIVE is not supported for MERGE statement",
23834 )));
23835 }
23836 let body_stmt = self.parse_one_statement()?;
23837 let Statement::Merge(mut body) = body_stmt else {
23838 return Err(self.err(format!("expected MERGE after WITH clause")));
23839 };
23840 body.ctes = ctes;
23841 Ok(Statement::Merge(body))
23842 }
23843 other => Err(self.err(format!(
23844 "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
23845 ))),
23846 }
23847 }
23848
23849 /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
23850 /// already consumed the leading `EXISTS` ident via
23851 /// `self.advance()`.
23852 /// v7.13.0 — parse the rest of a `CASE … END` expression after
23853 /// the leading `CASE` ident has been consumed (mailrs round-5
23854 /// G9). Supports both the searched form
23855 /// (`CASE WHEN cond THEN val …`) and the simple form
23856 /// (`CASE operand WHEN val THEN val …`).
23857 fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
23858 // Disambiguate searched vs simple form: if the next token
23859 // is `WHEN`, we're in the searched form. Otherwise the
23860 // intervening expression is the operand.
23861 let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
23862 None
23863 } else {
23864 Some(Box::new(self.parse_expr(0)?))
23865 };
23866 let mut branches: Vec<(Expr, Expr)> = Vec::new();
23867 loop {
23868 match self.peek() {
23869 Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
23870 self.advance();
23871 let cond = self.parse_expr(0)?;
23872 match self.peek() {
23873 Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
23874 self.advance();
23875 }
23876 other => {
23877 return Err(self.err(alloc::format!(
23878 "expected THEN after CASE WHEN <expr>, got {other:?}"
23879 )));
23880 }
23881 }
23882 let value = self.parse_expr(0)?;
23883 branches.push((cond, value));
23884 }
23885 _ => break,
23886 }
23887 }
23888 if branches.is_empty() {
23889 return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
23890 }
23891 let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
23892 {
23893 self.advance();
23894 Some(Box::new(self.parse_expr(0)?))
23895 } else {
23896 None
23897 };
23898 match self.peek() {
23899 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
23900 self.advance();
23901 }
23902 other => {
23903 return Err(self.err(alloc::format!(
23904 "expected END to close CASE expression, got {other:?}"
23905 )));
23906 }
23907 }
23908 Ok(Expr::Case {
23909 operand,
23910 branches,
23911 else_branch,
23912 })
23913 }
23914
23915 /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
23916 /// query-source position (EXISTS / IN / INSERT source / CTE body /
23917 /// view body). Caller consumed the WITH keyword. Only a SELECT
23918 /// outer is grammatical here; the data-modifying-CTE-at-top-level
23919 /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
23920 /// maps correctly.
23921 fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
23922 let inner = self.parse_with_cte_then_select()?;
23923 match inner {
23924 Statement::Select(s) => Ok(s),
23925 other => Err(self.err(format!(
23926 "expected SELECT after WITH in a subquery, got {other:?}"
23927 ))),
23928 }
23929 }
23930
23931 /// True when the next token is the (unquoted) WITH keyword. WITH is
23932 /// reserved in PG, so a bare `with` can never be a column reference
23933 /// in these positions; a quoted `"with"` stays an identifier.
23934 fn peek_is_with_kw(&self) -> bool {
23935 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
23936 }
23937
23938 /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
23939 /// `#[inline(never)]` keeps the large SelectStatement temporaries
23940 /// off parse_expr's recursive frame (the nesting-budget stack
23941 /// cliff — see the round-153 gate regression).
23942 #[inline(never)]
23943 fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
23944 if self.peek_is_with_kw() {
23945 self.advance();
23946 self.parse_nested_with_select()
23947 } else {
23948 match self.parse_select_stmt()? {
23949 Statement::Select(s) => Ok(s),
23950 other => Err(self.err(alloc::format!(
23951 "expected SELECT inside ANY/ALL, got {other:?}"
23952 ))),
23953 }
23954 }
23955 }
23956
23957 fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
23958 if !matches!(self.peek(), Token::LParen) {
23959 return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
23960 }
23961 self.advance();
23962 // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
23963 let s = if self.peek_is_with_kw() {
23964 self.advance();
23965 self.parse_nested_with_select()?
23966 } else {
23967 let inner = self.parse_select_stmt()?;
23968 let Statement::Select(s) = inner else {
23969 unreachable!("parse_select_stmt returns Select")
23970 };
23971 s
23972 };
23973 if !matches!(self.peek(), Token::RParen) {
23974 return Err(self.err(format!(
23975 "expected ')' after EXISTS-subquery, got {:?}",
23976 self.peek()
23977 )));
23978 }
23979 self.advance();
23980 Ok(Expr::Exists {
23981 subquery: Box::new(s),
23982 negated,
23983 })
23984 }
23985
23986 fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23987 self.advance(); // IN
23988 if !matches!(self.peek(), Token::LParen) {
23989 return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
23990 }
23991 self.advance();
23992 // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
23993 // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
23994 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
23995 let s = if self.peek_is_with_kw() {
23996 self.advance();
23997 self.parse_nested_with_select()?
23998 } else {
23999 let inner = self.parse_select_stmt()?;
24000 let Statement::Select(s) = inner else {
24001 unreachable!("parse_select_stmt always returns Statement::Select")
24002 };
24003 s
24004 };
24005 if !matches!(self.peek(), Token::RParen) {
24006 return Err(self.err(format!(
24007 "expected ')' after IN-subquery, got {:?}",
24008 self.peek()
24009 )));
24010 }
24011 self.advance();
24012 return Ok(Expr::InSubquery {
24013 expr: Box::new(expr),
24014 subquery: Box::new(s),
24015 negated,
24016 });
24017 }
24018 let mut elements = Vec::new();
24019 if !matches!(self.peek(), Token::RParen) {
24020 loop {
24021 elements.push(self.parse_expr(0)?);
24022 match self.peek() {
24023 Token::Comma => {
24024 self.advance();
24025 }
24026 Token::RParen => break,
24027 other => {
24028 return Err(
24029 self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
24030 );
24031 }
24032 }
24033 }
24034 }
24035 self.advance(); // ')'
24036 // v7.30.2 (mailrs round-25) — flat InList node instead of a
24037 // left-deep OR-Eq chain: chain depth scaled with the element
24038 // count and overflowed the stack (eval + drop are recursive).
24039 if elements.is_empty() {
24040 return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
24041 }
24042 Ok(Expr::InList {
24043 expr: Box::new(expr),
24044 list: elements,
24045 negated,
24046 })
24047 }
24048
24049 /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
24050 /// already consumed by the caller. Elements must be numeric literals
24051 /// (with optional unary `-`); any compound expression is rejected at
24052 /// parse time so the runtime never needs to evaluate inside a vector.
24053 /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
24054 /// has already consumed the `EXTRACT` token before calling us —
24055 /// we pick up at the opening `(`.
24056 /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
24057 /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
24058 /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
24059 /// per-column OR-fold of
24060 /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
24061 /// term)` so the existing FTS evaluator handles semantics.
24062 ///
24063 /// The mode modifier is accepted-and-ignored at v7.17 — all
24064 /// modes map to the same `plainto_tsquery` rewrite. Boolean-
24065 /// mode operators (`+foo -bar`) would need their own parser
24066 /// (Phase 2.2c); customers who hit them today already get a
24067 /// correct lexeme-match against the bare term, only without
24068 /// the +/- precedence the customer asked for.
24069 fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
24070 // Already at `MATCH`-consumed position; the dispatcher
24071 // confirmed the next token is `(`.
24072 if !matches!(self.peek(), Token::LParen) {
24073 return Err(self.err(alloc::format!(
24074 "expected '(' after MATCH, got {:?}",
24075 self.peek()
24076 )));
24077 }
24078 self.advance();
24079 let mut cols: Vec<Expr> = Vec::new();
24080 loop {
24081 cols.push(self.parse_expr(0)?);
24082 match self.peek() {
24083 Token::Comma => {
24084 self.advance();
24085 }
24086 Token::RParen => break,
24087 other => {
24088 return Err(self.err(alloc::format!(
24089 "expected ',' or ')' in MATCH column list, got {other:?}"
24090 )));
24091 }
24092 }
24093 }
24094 self.advance(); // ')'
24095 // Expect AGAINST.
24096 match self.peek() {
24097 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
24098 self.advance();
24099 }
24100 other => {
24101 return Err(self.err(alloc::format!(
24102 "expected AGAINST after MATCH column list, got {other:?}"
24103 )));
24104 }
24105 }
24106 if !matches!(self.peek(), Token::LParen) {
24107 return Err(self.err(alloc::format!(
24108 "expected '(' after AGAINST, got {:?}",
24109 self.peek()
24110 )));
24111 }
24112 self.advance();
24113 // Read AGAINST's argument as a single primary token —
24114 // string literal, placeholder, or column-ref ident. We
24115 // can't call `parse_expr` / `parse_unary` here because
24116 // the postfix chain inside `parse_atom` would greedily
24117 // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
24118 // and fail at "expected '(' after IN". Customers always
24119 // write a literal or bound parameter in AGAINST, so this
24120 // restriction is non-blocking; the error path explains
24121 // the limit if a more complex expression shows up.
24122 let term = match self.advance() {
24123 Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
24124 Token::Placeholder(n) => Expr::Placeholder(n),
24125 Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
24126 qualifier: None,
24127 name: s,
24128 }),
24129 other => {
24130 return Err(self.err(alloc::format!(
24131 "MATCH ... AGAINST(<term>) expects a string literal, \
24132 bound parameter, or column ref, got {other:?}"
24133 )));
24134 }
24135 };
24136 // Optional mode tail — accept-and-ignore at v7.17:
24137 // IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
24138 // IN BOOLEAN MODE
24139 // WITH QUERY EXPANSION
24140 loop {
24141 match self.peek() {
24142 // IN lexes as a reserved Token::In, not an ident,
24143 // so it gets its own arm.
24144 Token::In => {
24145 self.advance();
24146 }
24147 Token::Ident(s) | Token::QuotedIdent(s)
24148 if s.eq_ignore_ascii_case("natural")
24149 || s.eq_ignore_ascii_case("language")
24150 || s.eq_ignore_ascii_case("boolean")
24151 || s.eq_ignore_ascii_case("mode")
24152 || s.eq_ignore_ascii_case("with")
24153 || s.eq_ignore_ascii_case("query")
24154 || s.eq_ignore_ascii_case("expansion") =>
24155 {
24156 self.advance();
24157 }
24158 _ => break,
24159 }
24160 }
24161 if !matches!(self.peek(), Token::RParen) {
24162 return Err(self.err(alloc::format!(
24163 "expected ')' to close AGAINST, got {:?}",
24164 self.peek()
24165 )));
24166 }
24167 self.advance();
24168 // Build per-column `to_tsvector('simple', col) @@
24169 // plainto_tsquery('simple', term)` and OR-fold.
24170 let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24171 let plainto = Expr::FunctionCall {
24172 name: String::from("plainto_tsquery"),
24173 args: alloc::vec![simple_lit(), term.clone()],
24174 };
24175 let mut folded: Option<Expr> = None;
24176 for col in cols {
24177 let to_tsv = Expr::FunctionCall {
24178 name: String::from("to_tsvector"),
24179 args: alloc::vec![simple_lit(), col],
24180 };
24181 let leaf = Expr::Binary {
24182 lhs: Box::new(to_tsv),
24183 op: crate::ast::BinOp::TsMatch,
24184 rhs: Box::new(plainto.clone()),
24185 };
24186 folded = Some(match folded {
24187 None => leaf,
24188 Some(prev) => Expr::Binary {
24189 lhs: Box::new(prev),
24190 op: crate::ast::BinOp::Or,
24191 rhs: Box::new(leaf),
24192 },
24193 });
24194 }
24195 match folded {
24196 Some(e) => Ok(e),
24197 None => Err(self.err(String::from(
24198 "MATCH(...) AGAINST(...) requires at least one column",
24199 ))),
24200 }
24201 }
24202
24203 fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24204 if !matches!(self.peek(), Token::LParen) {
24205 return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24206 }
24207 self.advance();
24208 let field_name = self.expect_ident_like()?;
24209 let field = match field_name.to_ascii_lowercase().as_str() {
24210 // PG accepts the plural spellings (years/months/…/millenniums) as
24211 // aliases for the singular fields — its datetime unit table has both.
24212 // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24213 "year" | "years" => ExtractField::Year,
24214 "month" | "months" => ExtractField::Month,
24215 "day" | "days" => ExtractField::Day,
24216 "hour" | "hours" => ExtractField::Hour,
24217 "minute" | "minutes" => ExtractField::Minute,
24218 "second" | "seconds" => ExtractField::Second,
24219 "microsecond" | "microseconds" => ExtractField::Microsecond,
24220 "epoch" => ExtractField::Epoch,
24221 "dow" => ExtractField::Dow,
24222 "isodow" => ExtractField::Isodow,
24223 "doy" => ExtractField::Doy,
24224 "week" | "weeks" => ExtractField::Week,
24225 "isoyear" => ExtractField::Isoyear,
24226 "quarter" => ExtractField::Quarter,
24227 "decade" | "decades" => ExtractField::Decade,
24228 "century" | "centuries" => ExtractField::Century,
24229 "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24230 "julian" => ExtractField::Julian,
24231 "millisecond" | "milliseconds" => ExtractField::Millisecond,
24232 "timezone" => ExtractField::Timezone,
24233 "timezone_hour" => ExtractField::TimezoneHour,
24234 "timezone_minute" => ExtractField::TimezoneMinute,
24235 // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24236 // reports an unknown one with the source type (22023); carry the
24237 // raw name so eval can word it.
24238 other => ExtractField::Other(alloc::string::String::from(other)),
24239 };
24240 if !matches!(self.peek(), Token::From) {
24241 return Err(self.err(format!(
24242 "expected FROM after EXTRACT field, got {:?}",
24243 self.peek()
24244 )));
24245 }
24246 self.advance();
24247 let source = self.parse_expr(0)?;
24248 if !matches!(self.peek(), Token::RParen) {
24249 return Err(self.err(format!(
24250 "expected ')' to close EXTRACT, got {:?}",
24251 self.peek()
24252 )));
24253 }
24254 self.advance();
24255 Ok(Expr::Extract {
24256 field,
24257 source: Box::new(source),
24258 })
24259 }
24260
24261 /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24262 /// is already consumed; we expect a single string literal next and
24263 /// resolve it into `Literal::Interval` at parse time so the engine
24264 /// never has to re-tokenise inside the string.
24265 /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24266 /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24267 /// is the SQL-standard form and is left to the path below.
24268 fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24269 // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24270 let (offset, sign) = match self.peek() {
24271 Token::Minus => (1, "-"),
24272 _ => (0, ""),
24273 };
24274 let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24275 return None;
24276 };
24277 self.tokens
24278 .get(self.pos + offset + 1)
24279 .filter(|t| mysql_interval_unit(t).is_some())?;
24280 Some((alloc::format!("{sign}{n}"), offset + 1))
24281 }
24282
24283 /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24284 /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24285 /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24286 ///
24287 /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24288 /// this by parsing the group and then restoring `self.pos` — which could
24289 /// never have worked, because `advance()` DESTROYS the token it returns
24290 /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24291 /// inert only because both branches errored back then.
24292 fn interval_paren_is_quantity(&self) -> bool {
24293 let mut depth = 0usize;
24294 let mut saw_top_level_comma = false;
24295 let mut i = self.pos;
24296 while let Some(tok) = self.tokens.get(i) {
24297 match tok {
24298 Token::LParen => depth += 1,
24299 Token::RParen => {
24300 depth = depth.saturating_sub(1);
24301 if depth == 0 {
24302 return !saw_top_level_comma
24303 && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
24304 .is_some();
24305 }
24306 }
24307 // A comma directly inside the outermost parens means the
24308 // argument list of the INTERVAL() function.
24309 Token::Comma if depth == 1 => saw_top_level_comma = true,
24310 Token::Eof => return false,
24311 _ => {}
24312 }
24313 i += 1;
24314 }
24315 false
24316 }
24317
24318 fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
24319 // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
24320 // (the index of the last Ni ≤ N), distinct from the interval literal.
24321 // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
24322 // is decided by a non-destructive lookahead (round 422) before either
24323 // branch consumes anything. MySQL only.
24324 if self.mysql_dialect
24325 && matches!(self.peek(), Token::LParen)
24326 && !self.interval_paren_is_quantity()
24327 {
24328 self.advance(); // (
24329 let mut args = Vec::new();
24330 if !matches!(self.peek(), Token::RParen) {
24331 loop {
24332 args.push(self.parse_expr(0)?);
24333 if matches!(self.peek(), Token::Comma) {
24334 self.advance();
24335 continue;
24336 }
24337 break;
24338 }
24339 }
24340 if !matches!(self.peek(), Token::RParen) {
24341 return Err(self.err(alloc::format!(
24342 "expected ')' after INTERVAL() arguments, got {:?}",
24343 self.peek()
24344 )));
24345 }
24346 self.advance(); // )
24347 return Ok(Expr::FunctionCall {
24348 name: alloc::string::String::from("interval"),
24349 args,
24350 });
24351 }
24352 // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
24353 // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
24354 // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
24355 // writes every date arithmetic there is, and it did not parse at
24356 // all. PG rejects the unquoted form outright (`syntax error at or
24357 // near "1"`, measured), so it is taken only in the MySQL dialect —
24358 // PG's own `INTERVAL '1' DAY` is untouched below.
24359 if self.mysql_dialect
24360 && let Some((text, consume)) = self.peek_unquoted_interval_count()
24361 {
24362 for _ in 0..consume {
24363 self.advance(); // the optional `-` and the number
24364 }
24365 let Some(unit) = mysql_interval_unit(self.peek()) else {
24366 return Err(self.err(alloc::format!(
24367 "expected an interval unit after INTERVAL {text}, got {:?}",
24368 self.peek()
24369 )));
24370 };
24371 self.advance(); // the unit
24372 let (months, days, micros) = scale_mysql_interval(&text, unit)
24373 .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
24374 return Ok(Expr::Literal(Literal::Interval {
24375 months,
24376 days,
24377 micros,
24378 // The canonical rendering, so Display round-trips into a
24379 // form both dialects read back.
24380 text: alloc::format!("{text} {unit}"),
24381 }));
24382 }
24383 // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
24384 // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
24385 // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
24386 // Those cannot fold into a compile-time `Literal::Interval`, so they
24387 // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
24388 // builtin, which builds the value at run time (and yields NULL for a
24389 // NULL quantity, as MariaDB does). The literal path above still folds
24390 // the constant case — it is cheaper and round-trips through Display.
24391 //
24392 // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
24393 // MySQL's quoted spelling) keep the qualifier path below.
24394 if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
24395 let qty = self.parse_expr(0)?;
24396 let Some(unit) = mysql_interval_unit(self.peek()) else {
24397 return Err(self.err(alloc::format!(
24398 "expected an interval unit after INTERVAL <expr>, got {:?}",
24399 self.peek()
24400 )));
24401 };
24402 self.advance(); // the unit
24403 return Ok(make_interval_call(qty, unit));
24404 }
24405 let tok = self.advance();
24406 let Token::String(text) = tok else {
24407 return Err(self.err(format!(
24408 "expected string literal after INTERVAL, got {tok:?}"
24409 )));
24410 };
24411 // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
24412 // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
24413 // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
24414 // bare number means and the leading/trailing precision.
24415 let field1 = interval_field_of(self.peek());
24416 let qualifier = if let Some(f1) = field1 {
24417 self.advance();
24418 let f2 = if matches!(self.peek(), Token::To) {
24419 self.advance();
24420 let Some(f) = interval_field_of(self.peek()) else {
24421 return Err(self.err(format!(
24422 "expected an interval field after TO, got {:?}",
24423 self.peek()
24424 )));
24425 };
24426 self.advance();
24427 Some(f)
24428 } else {
24429 None
24430 };
24431 Some((f1, f2))
24432 } else {
24433 None
24434 };
24435 let (months, days, micros) = match qualifier {
24436 Some(q) => interpret_qualified_interval(&text, q),
24437 None => parse_interval_text(&text),
24438 }
24439 .ok_or_else(|| ParseError {
24440 message: format!(
24441 "cannot parse INTERVAL {text:?}; \
24442 expected `<n> <unit> [<n> <unit> ...]` with units \
24443 microsecond[s], millisecond[s], second[s], minute[s], \
24444 hour[s], day[s], week[s], month[s], year[s]"
24445 ),
24446 token_pos: self.consumed_pos(),
24447 })?;
24448 Ok(Expr::Literal(Literal::Interval {
24449 months,
24450 days,
24451 micros,
24452 text,
24453 }))
24454 }
24455
24456 /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
24457 /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
24458 /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
24459 /// than a pgvector literal.
24460 fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
24461 self.advance(); // consume `[`
24462 let mut items: Vec<Expr> = Vec::new();
24463 if !matches!(self.peek(), Token::RBracket) {
24464 loop {
24465 if matches!(self.peek(), Token::LBracket) {
24466 items.push(self.parse_array_bracket_body()?);
24467 } else {
24468 items.push(self.parse_expr(0)?);
24469 }
24470 match self.peek() {
24471 Token::Comma => {
24472 self.advance();
24473 }
24474 Token::RBracket => break,
24475 other => {
24476 return Err(self.err(alloc::format!(
24477 "expected ',' or ']' in array literal, got {other:?}"
24478 )));
24479 }
24480 }
24481 }
24482 }
24483 self.advance(); // consume `]`
24484 Ok(Expr::Array(items))
24485 }
24486
24487 fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
24488 let mut elems = Vec::new();
24489 if matches!(self.peek(), Token::RBracket) {
24490 self.advance();
24491 return Ok(Expr::Literal(Literal::Vector(elems)));
24492 }
24493 loop {
24494 let e = self.parse_expr(0)?;
24495 let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
24496 message: format!("vector element must be a numeric literal, got {e:?}"),
24497 token_pos: self.pos,
24498 })?;
24499 elems.push(x);
24500 match self.peek() {
24501 Token::Comma => {
24502 self.advance();
24503 }
24504 Token::RBracket => {
24505 self.advance();
24506 break;
24507 }
24508 other => {
24509 return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
24510 }
24511 }
24512 }
24513 Ok(Expr::Literal(Literal::Vector(elems)))
24514 }
24515
24516 /// Atom that started with an identifier: could be `t.col`, `col`, or
24517 /// `func(arg, ...)`. Detect each shape by looking at the next token.
24518 /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
24519 /// [, ...])`. Caller has already consumed `OVER`. Either clause
24520 /// is optional; an empty `()` is also legal (PG semantics).
24521 /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
24522 /// modifier between `name(args)` and `OVER (...)`. Default is
24523 /// `Respect`. Unrecognised idents leave the stream unchanged.
24524 fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
24525 let Token::Ident(s) = self.peek().clone() else {
24526 return NullTreatment::Respect;
24527 };
24528 let is_ignore = s.eq_ignore_ascii_case("ignore");
24529 let is_respect = s.eq_ignore_ascii_case("respect");
24530 if !is_ignore && !is_respect {
24531 return NullTreatment::Respect;
24532 }
24533 // Lookahead for NULLS — only consume both tokens together.
24534 // pos+1 must hold a "nulls" ident.
24535 if self.pos + 1 < self.tokens.len()
24536 && let Token::Ident(s2) = &self.tokens[self.pos + 1]
24537 && s2.eq_ignore_ascii_case("nulls")
24538 {
24539 self.advance();
24540 self.advance();
24541 return if is_ignore {
24542 NullTreatment::Ignore
24543 } else {
24544 NullTreatment::Respect
24545 };
24546 }
24547 NullTreatment::Respect
24548 }
24549
24550 /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
24551 /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
24552 /// (same shape as the `OVER` tail). Consumes the whole clause and
24553 /// returns the predicate; returns `None` when no `FILTER` follows.
24554 fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
24555 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24556 return Ok(None);
24557 };
24558 if !s.eq_ignore_ascii_case("filter") {
24559 return Ok(None);
24560 }
24561 self.advance(); // FILTER
24562 if !matches!(self.peek(), Token::LParen) {
24563 return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
24564 }
24565 self.advance(); // (
24566 if !matches!(self.peek(), Token::Where) {
24567 return Err(self.err(format!(
24568 "expected WHERE inside FILTER (...), got {:?}",
24569 self.peek()
24570 )));
24571 }
24572 self.advance(); // WHERE
24573 let cond = self.parse_expr(0)?;
24574 if !matches!(self.peek(), Token::RParen) {
24575 return Err(self.err(format!(
24576 "expected ')' to close FILTER (WHERE ...), got {:?}",
24577 self.peek()
24578 )));
24579 }
24580 self.advance(); // )
24581 Ok(Some(Box::new(cond)))
24582 }
24583
24584 /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
24585 /// the separator as the aggregate's second argument, which is the
24586 /// shape `string_agg` already takes. Returns whether one was there.
24587 fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
24588 if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
24589 return Ok(false);
24590 }
24591 self.advance();
24592 let Token::String(sep) = self.peek().clone() else {
24593 return Err(self.err(alloc::format!(
24594 "expected a string literal after SEPARATOR, got {:?}",
24595 self.peek()
24596 )));
24597 };
24598 self.advance();
24599 args.push(Expr::Literal(Literal::String(sep)));
24600 Ok(true)
24601 }
24602
24603 /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
24604 /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
24605 /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
24606 /// keys, or an empty vec when no `WITHIN GROUP` follows.
24607 fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
24608 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24609 return Ok(Vec::new());
24610 };
24611 if !s.eq_ignore_ascii_case("within") {
24612 return Ok(Vec::new());
24613 }
24614 self.advance(); // WITHIN
24615 if !matches!(self.peek(), Token::Group) {
24616 return Err(self.err(format!(
24617 "expected GROUP after WITHIN, got {:?}",
24618 self.peek()
24619 )));
24620 }
24621 self.advance(); // GROUP
24622 if !matches!(self.peek(), Token::LParen) {
24623 return Err(self.err(format!(
24624 "expected '(' after WITHIN GROUP, got {:?}",
24625 self.peek()
24626 )));
24627 }
24628 self.advance(); // (
24629 if !matches!(self.peek(), Token::Order) {
24630 return Err(self.err(format!(
24631 "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
24632 self.peek()
24633 )));
24634 }
24635 self.advance(); // ORDER
24636 if !self.peek_is_by() {
24637 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24638 }
24639 self.advance(); // BY
24640 let mut keys: Vec<OrderBy> = Vec::new();
24641 loop {
24642 // v7.39 (round 691) — save/restore, the discipline this parser
24643 // already uses around `pending_sample_preds`, so a subquery inside
24644 // a key neither inherits nor leaks the channel.
24645 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
24646 let saved_coll = self.order_key_collation.take();
24647 let parsed = self.parse_expr(0);
24648 self.in_order_by_key = saved_flag;
24649 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
24650 let expr = parsed?;
24651 let desc = if matches!(self.peek(), Token::Desc) {
24652 self.advance();
24653 true
24654 } else if matches!(self.peek(), Token::Asc) {
24655 self.advance();
24656 false
24657 } else {
24658 false
24659 };
24660 let nulls_first = self.parse_optional_nulls_placement()?;
24661 keys.push(OrderBy {
24662 expr,
24663 desc,
24664 nulls_first,
24665 collation,
24666 });
24667 if matches!(self.peek(), Token::Comma) {
24668 self.advance();
24669 } else {
24670 break;
24671 }
24672 }
24673 if !matches!(self.peek(), Token::RParen) {
24674 return Err(self.err(format!(
24675 "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
24676 self.peek()
24677 )));
24678 }
24679 self.advance(); // )
24680 Ok(keys)
24681 }
24682
24683 /// No frame clause is supported.
24684 #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
24685 fn parse_over_clause(
24686 &mut self,
24687 ) -> Result<
24688 (
24689 Vec<Expr>,
24690 Vec<(Expr, bool, Option<bool>)>,
24691 Option<WindowFrame>,
24692 ),
24693 ParseError,
24694 > {
24695 // `OVER w` — a named-window reference. The WINDOW clause
24696 // parses after the select list, so the name rides out as a
24697 // marker in partition_by; parse_bare_select substitutes the
24698 // definition once the clause is known.
24699 if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
24700 let name = w.clone();
24701 self.advance();
24702 return Ok((
24703 alloc::vec![Expr::Column(crate::ast::ColumnName {
24704 qualifier: Some("__named_window__".to_string()),
24705 name,
24706 })],
24707 Vec::new(),
24708 None,
24709 ));
24710 }
24711 if !matches!(self.peek(), Token::LParen) {
24712 return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
24713 }
24714 self.advance();
24715 let mut partition_by = Vec::new();
24716 let mut order_by = Vec::new();
24717 // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
24718 // window, refined in place. PG's rules (probed against 18.4) differ
24719 // from the bare `OVER w1` form, so the reference rides out under its
24720 // own marker and `substitute_named_windows` applies them. The base
24721 // name is any leading identifier that isn't a window-spec keyword.
24722 let base_window = match self.peek() {
24723 Token::Ident(s) | Token::QuotedIdent(s)
24724 if !s.eq_ignore_ascii_case("partition")
24725 && !s.eq_ignore_ascii_case("rows")
24726 && !s.eq_ignore_ascii_case("range")
24727 && !s.eq_ignore_ascii_case("groups") =>
24728 {
24729 let n = s.clone();
24730 self.advance();
24731 Some(n)
24732 }
24733 _ => None,
24734 };
24735 // PARTITION BY ?
24736 // v7.37.6-B promoted PARTITION to a reserved keyword
24737 // (Token::Partition); pre-7.37.6-B catalogs lexed it as
24738 // `Token::Ident("partition")`. Accept both so older sources
24739 // and the new lexer surface land on the same path.
24740 let is_partition_kw = match self.peek() {
24741 Token::Partition => true,
24742 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
24743 _ => false,
24744 };
24745 if is_partition_kw {
24746 self.advance();
24747 if !self.peek_is_by() {
24748 return Err(self.err(format!(
24749 "expected BY after PARTITION, got {:?}",
24750 self.peek()
24751 )));
24752 }
24753 self.advance();
24754 loop {
24755 partition_by.push(self.parse_expr(0)?);
24756 if matches!(self.peek(), Token::Comma) {
24757 self.advance();
24758 continue;
24759 }
24760 break;
24761 }
24762 }
24763 // ORDER BY ?
24764 if matches!(self.peek(), Token::Order) {
24765 self.advance();
24766 if !self.peek_is_by() {
24767 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24768 }
24769 self.advance();
24770 loop {
24771 let e = self.parse_expr(0)?;
24772 let desc = if matches!(self.peek(), Token::Desc) {
24773 self.advance();
24774 true
24775 } else if matches!(self.peek(), Token::Asc) {
24776 self.advance();
24777 false
24778 } else {
24779 false
24780 };
24781 // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
24782 let nulls_first = self.parse_optional_nulls_placement()?;
24783 order_by.push((e, desc, nulls_first));
24784 if matches!(self.peek(), Token::Comma) {
24785 self.advance();
24786 continue;
24787 }
24788 break;
24789 }
24790 }
24791 // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
24792 // Both keywords come through the lexer as identifiers; match
24793 // case-insensitively.
24794 let mut frame: Option<WindowFrame> = None;
24795 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
24796 let kind = if s.eq_ignore_ascii_case("rows") {
24797 Some(FrameKind::Rows)
24798 } else if s.eq_ignore_ascii_case("range") {
24799 Some(FrameKind::Range)
24800 } else if s.eq_ignore_ascii_case("groups") {
24801 // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
24802 Some(FrameKind::Groups)
24803 } else {
24804 None
24805 };
24806 if let Some(kind) = kind {
24807 self.advance();
24808 frame = Some(self.parse_frame_tail(kind)?);
24809 }
24810 }
24811 if !matches!(self.peek(), Token::RParen) {
24812 return Err(self.err(format!(
24813 "expected ')' to close OVER clause, got {:?}",
24814 self.peek()
24815 )));
24816 }
24817 self.advance();
24818 if let Some(base) = base_window {
24819 // A copy may refine but never override the base's partitioning
24820 // (PG rejects it outright, before looking the name up).
24821 if !partition_by.is_empty() {
24822 return Err(self.err(alloc::format!(
24823 "cannot override PARTITION BY clause of window \"{base}\""
24824 )));
24825 }
24826 partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
24827 qualifier: Some("__named_window_ref__".to_string()),
24828 name: base,
24829 })];
24830 }
24831 Ok((partition_by, order_by, frame))
24832 }
24833
24834 /// v4.20: parse the tail of an explicit frame, given the `ROWS`
24835 /// or `RANGE` keyword was just consumed. Accepts both
24836 /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
24837 /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
24838 /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
24839 fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
24840 let (start, end) = if matches!(self.peek(), Token::Between) {
24841 self.advance();
24842 let start = self.parse_frame_bound()?;
24843 if !matches!(self.peek(), Token::And) {
24844 return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
24845 }
24846 self.advance();
24847 let end = self.parse_frame_bound()?;
24848 (start, Some(end))
24849 } else {
24850 (self.parse_frame_bound()?, None)
24851 };
24852 let exclude = self.parse_frame_exclusion()?;
24853 Ok(WindowFrame {
24854 kind,
24855 start,
24856 end,
24857 exclude,
24858 })
24859 }
24860
24861 /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
24862 /// after a frame spec. NO OTHERS is the default no-op.
24863 fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
24864 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
24865 return Ok(FrameExclusion::NoOthers);
24866 }
24867 self.advance(); // EXCLUDE
24868 match self.peek() {
24869 Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
24870 self.advance();
24871 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
24872 return Err(self.err(format!(
24873 "expected ROW after EXCLUDE CURRENT, got {:?}",
24874 self.peek()
24875 )));
24876 }
24877 self.advance();
24878 Ok(FrameExclusion::CurrentRow)
24879 }
24880 // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
24881 // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
24882 // Without this arm it fell to the catch-all, whose message
24883 // self-contradictingly listed GROUP as expected.
24884 Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
24885 self.advance();
24886 Ok(FrameExclusion::Group)
24887 }
24888 Token::Group => {
24889 self.advance();
24890 Ok(FrameExclusion::Group)
24891 }
24892 Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
24893 self.advance();
24894 Ok(FrameExclusion::Ties)
24895 }
24896 Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
24897 self.advance();
24898 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
24899 return Err(self.err(format!(
24900 "expected OTHERS after EXCLUDE NO, got {:?}",
24901 self.peek()
24902 )));
24903 }
24904 self.advance();
24905 Ok(FrameExclusion::NoOthers)
24906 }
24907 other => Err(self.err(format!(
24908 "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
24909 ))),
24910 }
24911 }
24912
24913 /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
24914 /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
24915 /// `UNBOUNDED FOLLOWING`.
24916 fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
24917 // Interval-typed offset for a value-based RANGE frame over a
24918 // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
24919 // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
24920 // PRECEDING`.
24921 if let Some((months, days, micros)) = self.try_take_interval_offset()? {
24922 let dir = self.expect_ident_like()?;
24923 return if dir.eq_ignore_ascii_case("preceding") {
24924 Ok(FrameBound::IntervalPreceding {
24925 months,
24926 days,
24927 micros,
24928 })
24929 } else if dir.eq_ignore_ascii_case("following") {
24930 Ok(FrameBound::IntervalFollowing {
24931 months,
24932 days,
24933 micros,
24934 })
24935 } else {
24936 Err(self.err(format!(
24937 "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
24938 )))
24939 };
24940 }
24941 // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
24942 if let Token::Integer(n) = *self.peek() {
24943 self.advance();
24944 let n: u64 = u64::try_from(n).map_err(|_| {
24945 self.err(format!(
24946 "invalid frame offset {n} — expected non-negative integer"
24947 ))
24948 })?;
24949 let dir = self.expect_ident_like()?;
24950 return if dir.eq_ignore_ascii_case("preceding") {
24951 Ok(FrameBound::OffsetPreceding(n))
24952 } else if dir.eq_ignore_ascii_case("following") {
24953 Ok(FrameBound::OffsetFollowing(n))
24954 } else {
24955 Err(self.err(format!(
24956 "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
24957 )))
24958 };
24959 }
24960 let first = self.expect_ident_like()?;
24961 if first.eq_ignore_ascii_case("unbounded") {
24962 let dir = self.expect_ident_like()?;
24963 return if dir.eq_ignore_ascii_case("preceding") {
24964 Ok(FrameBound::UnboundedPreceding)
24965 } else if dir.eq_ignore_ascii_case("following") {
24966 Ok(FrameBound::UnboundedFollowing)
24967 } else {
24968 Err(self.err(format!(
24969 "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
24970 )))
24971 };
24972 }
24973 if first.eq_ignore_ascii_case("current") {
24974 let row = self.expect_ident_like()?;
24975 if !row.eq_ignore_ascii_case("row") {
24976 return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
24977 }
24978 return Ok(FrameBound::CurrentRow);
24979 }
24980 Err(self.err(format!(
24981 "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
24982 )))
24983 }
24984
24985 /// Detect and consume a leading interval offset in a frame bound —
24986 /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
24987 /// `(months, days, micros)`. Leaves the cursor on the trailing
24988 /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
24989 /// when the next tokens are not an interval offset.
24990 fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
24991 // Shape A — `INTERVAL '1 day'`.
24992 if matches!(self.peek(), Token::Interval) {
24993 self.advance(); // INTERVAL
24994 let atom = self.parse_interval_atom()?;
24995 if let Expr::Literal(Literal::Interval {
24996 months,
24997 days,
24998 micros,
24999 ..
25000 }) = atom
25001 {
25002 return Ok(Some((months, days, micros)));
25003 }
25004 return Err(self.err("expected an interval literal in frame offset".to_string()));
25005 }
25006 // Shape B — `'1 day'::interval`. Look ahead for the exact
25007 // string / `::` / interval-target triple before committing.
25008 if let Token::String(text) = self.peek() {
25009 let target_is_interval = match self.tokens.get(self.pos + 2) {
25010 Some(Token::Interval) => true,
25011 Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
25012 _ => false,
25013 };
25014 let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
25015 && target_is_interval;
25016 if is_cast {
25017 let text = text.clone();
25018 self.advance(); // string
25019 self.advance(); // ::
25020 self.advance(); // interval
25021 let parts = parse_interval_text(&text).ok_or_else(|| {
25022 self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
25023 })?;
25024 return Ok(Some(parts));
25025 }
25026 }
25027 Ok(None)
25028 }
25029
25030 fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
25031 if matches!(self.peek(), Token::Dot) {
25032 self.advance();
25033 let name = self.expect_ident_like()?;
25034 // v7.14.0 — schema-qualified function call
25035 // `<schema>.<fn>(args)`. PG dumps emit
25036 // `pg_catalog.set_config(...)` in the preamble. SPG
25037 // is single-namespace: drop the schema prefix and
25038 // route the dispatch on the bare function name.
25039 if matches!(self.peek(), Token::LParen) {
25040 return self.finish_ident_atom(name);
25041 }
25042 return Ok(Expr::Column(ColumnName {
25043 qualifier: Some(first),
25044 name,
25045 }));
25046 }
25047 if matches!(self.peek(), Token::LParen) {
25048 self.advance();
25049 // `COUNT(*)` — special-cased here because `*` isn't a normal
25050 // expression token. Lower-case match on `first` since the lexer
25051 // folds identifiers.
25052 if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
25053 self.advance();
25054 if !matches!(self.peek(), Token::RParen) {
25055 return Err(self.err(format!(
25056 "expected ')' after COUNT(*), got {:?}",
25057 self.peek()
25058 )));
25059 }
25060 self.advance();
25061 // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
25062 let filter = self.parse_filter_clause()?;
25063 // v4.12: COUNT(*) OVER (...) — same window tail.
25064 let null_treatment = self.parse_null_treatment_modifier();
25065 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25066 && s.eq_ignore_ascii_case("over")
25067 {
25068 self.advance();
25069 let (partition_by, order_by, frame) = self.parse_over_clause()?;
25070 return Ok(Expr::WindowFunction {
25071 name: "count_star".into(),
25072 args: Vec::new(),
25073 partition_by,
25074 order_by,
25075 frame,
25076 null_treatment,
25077 filter,
25078 });
25079 }
25080 if let Some(filter) = filter {
25081 return Ok(Expr::AggregateOrdered {
25082 call: Box::new(Expr::FunctionCall {
25083 name: "count_star".into(),
25084 args: Vec::new(),
25085 }),
25086 order_by: Vec::new(),
25087 distinct: false,
25088 filter: Some(filter),
25089 });
25090 }
25091 return Ok(Expr::FunctionCall {
25092 name: "count_star".into(),
25093 args: Vec::new(),
25094 });
25095 }
25096 // Function call. PG-style: zero-or-more comma-separated args.
25097 let mut args = Vec::new();
25098 // v7.38 (read01, T14) — named-argument notation `argname => value`.
25099 // Names are collected in lock-step with `args` and resolved to
25100 // positional order after the loop (the AST stays positional).
25101 let mut arg_names: Vec<Option<String>> = Vec::new();
25102 let mut agg_order_by: Vec<OrderBy> = Vec::new();
25103 // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
25104 // seen, so the value arguments before it can be folded.
25105 let mut saw_separator = false;
25106 // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
25107 // v7.32 (round-29) — accept the dual `ALL` quantifier too
25108 // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
25109 let agg_distinct = if matches!(self.peek(), Token::Distinct) {
25110 self.advance();
25111 true
25112 } else if matches!(self.peek(), Token::All) {
25113 self.advance();
25114 false
25115 } else {
25116 false
25117 };
25118 // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
25119 // TIMESTAMPDIFF take a bare unit keyword as the first
25120 // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
25121 // bare type keyword (DATE / TIME / DATETIME); lower them
25122 // onto string literals so the evaluator sees plain text.
25123 if ((first.eq_ignore_ascii_case("timestampadd")
25124 || first.eq_ignore_ascii_case("timestampdiff"))
25125 && matches!(self.peek(), Token::Ident(u) if matches!(
25126 u.to_ascii_lowercase().as_str(),
25127 "microsecond" | "second" | "minute" | "hour" | "day"
25128 | "week" | "month" | "quarter" | "year"
25129 )))
25130 || (first.eq_ignore_ascii_case("get_format")
25131 && matches!(self.peek(), Token::Ident(u) if matches!(
25132 u.to_ascii_lowercase().as_str(),
25133 "date" | "time" | "datetime" | "timestamp"
25134 )))
25135 {
25136 if let Token::Ident(u) = self.peek() {
25137 args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
25138 }
25139 self.advance();
25140 if matches!(self.peek(), Token::Comma) {
25141 self.advance();
25142 }
25143 }
25144 // `ROW(a, b, …)` keyword constructor. Followed by a
25145 // comparison operator or [NOT] IN it joins the paren
25146 // row-constructor machinery (fieldwise parse-time
25147 // expansion); bare, it stays a `row` call the evaluator
25148 // renders as PG record text.
25149 if first.eq_ignore_ascii_case("row") {
25150 let mut row_items = Vec::new();
25151 if !matches!(self.peek(), Token::RParen) {
25152 loop {
25153 row_items.push(self.parse_expr(0)?);
25154 match self.peek() {
25155 Token::Comma => {
25156 self.advance();
25157 }
25158 Token::RParen => break,
25159 other => {
25160 return Err(self.err(format!(
25161 "expected ',' or ')' in ROW(...), got {other:?}"
25162 )));
25163 }
25164 }
25165 }
25166 }
25167 self.advance(); // ')'
25168 let comparison_follows = matches!(
25169 self.peek(),
25170 Token::Eq
25171 | Token::NotEq
25172 | Token::Lt
25173 | Token::LtEq
25174 | Token::Gt
25175 | Token::GtEq
25176 | Token::In
25177 ) || (matches!(self.peek(), Token::Not)
25178 && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25179 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25180 if comparison_follows && !row_items.is_empty() {
25181 return self.parse_row_comparison_tail(row_items);
25182 }
25183 return Ok(Expr::FunctionCall {
25184 name: String::from("row"),
25185 args: row_items,
25186 });
25187 }
25188 // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25189 // the parse-mode keyword introduces the source text. SPG
25190 // carries XML as text, so both modes lower to __xmlparse(expr)
25191 // which validates well-formedness and returns Value::Xml.
25192 if first.eq_ignore_ascii_case("xmlparse")
25193 && matches!(self.peek(), Token::Ident(kw)
25194 if kw.eq_ignore_ascii_case("document")
25195 || kw.eq_ignore_ascii_case("content"))
25196 {
25197 let mode = match self.advance() {
25198 Token::Ident(kw) => kw.to_ascii_lowercase(),
25199 _ => unreachable!("peeked an ident"),
25200 };
25201 let src = self.parse_expr(0)?;
25202 if !matches!(self.peek(), Token::RParen) {
25203 return Err(self.err(format!(
25204 "expected ')' to close XMLPARSE, got {:?}",
25205 self.peek()
25206 )));
25207 }
25208 self.advance();
25209 return Ok(Expr::FunctionCall {
25210 name: String::from("__xmlparse"),
25211 args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25212 });
25213 }
25214 // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25215 // keyword introduces the element name (a bare or quoted
25216 // identifier), then optional content expressions. Lower to a
25217 // plain `xmlelement(name_text, content …)` call.
25218 if first.eq_ignore_ascii_case("xmlelement")
25219 && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25220 {
25221 self.advance(); // consume NAME
25222 let elem_name = match self.peek().clone() {
25223 Token::Ident(n) | Token::QuotedIdent(n) => {
25224 self.advance();
25225 n
25226 }
25227 other => {
25228 return Err(self.err(format!(
25229 "expected element name after XMLELEMENT NAME, got {other:?}"
25230 )));
25231 }
25232 };
25233 let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25234 while matches!(self.peek(), Token::Comma) {
25235 self.advance();
25236 args.push(self.parse_expr(0)?);
25237 }
25238 if !matches!(self.peek(), Token::RParen) {
25239 return Err(self.err(format!(
25240 "expected ')' to close XMLELEMENT, got {:?}",
25241 self.peek()
25242 )));
25243 }
25244 self.advance();
25245 return Ok(Expr::FunctionCall {
25246 name: String::from("xmlelement"),
25247 args,
25248 });
25249 }
25250 // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
25251 // becomes a `<name>value</name>` element; a bare column infers its
25252 // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
25253 if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
25254 let mut args: Vec<Expr> = Vec::new();
25255 loop {
25256 let val = self.parse_expr(0)?;
25257 let name = if matches!(self.peek(), Token::As) {
25258 self.advance();
25259 match self.peek().clone() {
25260 Token::Ident(n) | Token::QuotedIdent(n) => {
25261 self.advance();
25262 n
25263 }
25264 other => {
25265 return Err(self.err(format!(
25266 "expected name after AS in XMLFOREST, got {other:?}"
25267 )));
25268 }
25269 }
25270 } else if let Expr::Column(c) = &val {
25271 c.name.clone()
25272 } else {
25273 return Err(
25274 self.err("XMLFOREST element without a column name needs AS".into())
25275 );
25276 };
25277 args.push(Expr::Literal(Literal::String(name)));
25278 args.push(val);
25279 if matches!(self.peek(), Token::Comma) {
25280 self.advance();
25281 } else {
25282 break;
25283 }
25284 }
25285 if !matches!(self.peek(), Token::RParen) {
25286 return Err(self.err(format!(
25287 "expected ')' to close XMLFOREST, got {:?}",
25288 self.peek()
25289 )));
25290 }
25291 self.advance();
25292 return Ok(Expr::FunctionCall {
25293 name: String::from("xmlforest"),
25294 args,
25295 });
25296 }
25297 // SQL-standard `POSITION(sub IN str)` — lowers onto
25298 // strpos(str, sub). IN is the argument separator here,
25299 // so the needle parses with the IN-tail suppressed.
25300 if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
25301 let saved = self.suppress_in_tail;
25302 self.suppress_in_tail = true;
25303 let needle = self.parse_expr(0);
25304 self.suppress_in_tail = saved;
25305 let needle = needle?;
25306 if matches!(self.peek(), Token::In) {
25307 self.advance();
25308 let haystack = self.parse_expr(0)?;
25309 if !matches!(self.peek(), Token::RParen) {
25310 return Err(self.err(format!(
25311 "expected ')' to close POSITION, got {:?}",
25312 self.peek()
25313 )));
25314 }
25315 self.advance();
25316 return Ok(Expr::FunctionCall {
25317 name: String::from("strpos"),
25318 args: alloc::vec![haystack, needle],
25319 });
25320 }
25321 // position(sub, str) comma form (incl. bytea) —
25322 // hand the parsed first arg to the generic list.
25323 args.push(needle);
25324 if matches!(self.peek(), Token::Comma) {
25325 self.advance();
25326 }
25327 }
25328 // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
25329 // FROM str)` — lowers onto btrim / ltrim / rtrim. The
25330 // plain comma forms TRIM(str) / TRIM(str, chars) keep
25331 // riding the generic argument list below.
25332 if first.eq_ignore_ascii_case("trim") {
25333 let mode = match self.peek() {
25334 Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
25335 self.advance();
25336 Some("btrim")
25337 }
25338 Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
25339 self.advance();
25340 Some("ltrim")
25341 }
25342 Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
25343 self.advance();
25344 Some("rtrim")
25345 }
25346 _ => None,
25347 };
25348 if mode.is_some() || matches!(self.peek(), Token::From) {
25349 // TRIM([mode] FROM str) — no strip-chars.
25350 let chars = if matches!(self.peek(), Token::From) {
25351 None
25352 } else {
25353 Some(self.parse_expr(0)?)
25354 };
25355 if !matches!(self.peek(), Token::From) {
25356 return Err(self.err(format!(
25357 "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
25358 self.peek()
25359 )));
25360 }
25361 self.advance();
25362 let target = self.parse_expr(0)?;
25363 if !matches!(self.peek(), Token::RParen) {
25364 return Err(
25365 self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
25366 );
25367 }
25368 self.advance();
25369 let mut trim_args = alloc::vec![target];
25370 if let Some(c) = chars {
25371 trim_args.push(c);
25372 }
25373 return Ok(Expr::FunctionCall {
25374 name: String::from(mode.unwrap_or("btrim")),
25375 args: trim_args,
25376 });
25377 }
25378 }
25379 if !matches!(self.peek(), Token::RParen) {
25380 loop {
25381 // v7.38 (read01, T14) — `argname => value` names this arg.
25382 // v7.39 (read01 round 77) — `argname := value` is the same
25383 // thing, and it is the spelling PG's own docs lead with. It
25384 // was simply never lexed here, so every `f(x := 1)` died in
25385 // the parser regardless of what `f` was.
25386 let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
25387 (
25388 Token::Ident(n) | Token::QuotedIdent(n),
25389 Some(Token::FatArrow | Token::ColonEq),
25390 ) => {
25391 let name = n.clone();
25392 self.advance(); // name
25393 self.advance(); // => / :=
25394 Some(name)
25395 }
25396 _ => None,
25397 };
25398 // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
25399 // array's elements into a variadic call's trailing args
25400 // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
25401 // reserved, so it arrives as a bare ident before the arg.
25402 let is_variadic = this_name.is_none()
25403 && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
25404 if is_variadic {
25405 self.advance();
25406 }
25407 let arg = self.parse_expr(0)?;
25408 args.push(match &this_name {
25409 // The callee's parameter names decide the slot, and a
25410 // user function's live in the catalog. Carry the name
25411 // to eval rather than guessing here.
25412 Some(n) => Expr::NamedArg {
25413 name: n.clone(),
25414 expr: Box::new(arg),
25415 },
25416 None if is_variadic => Expr::Variadic(Box::new(arg)),
25417 None => arg,
25418 });
25419 arg_names.push(this_name);
25420 // v7.25 (round-17) — standard `CAST(expr AS type)`.
25421 // The `::` cast already worked; this lowers the
25422 // function form onto the same Expr::Cast node.
25423 if first.eq_ignore_ascii_case("cast")
25424 && args.len() == 1
25425 && matches!(self.peek(), Token::As)
25426 {
25427 self.advance();
25428 let target = self.parse_cast_target()?;
25429 if !matches!(self.peek(), Token::RParen) {
25430 return Err(self.err(format!(
25431 "expected ')' to close CAST, got {:?}",
25432 self.peek()
25433 )));
25434 }
25435 self.advance();
25436 return Ok(Expr::Cast {
25437 expr: Box::new(args.pop().expect("one arg")),
25438 target,
25439 });
25440 }
25441 // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
25442 // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
25443 // keywords; SPG's lexer makes them plain idents (so they'd be
25444 // read as column refs). Lower the keyword to the string form
25445 // the evaluator already accepts.
25446 if first.eq_ignore_ascii_case("normalize")
25447 && args.len() == 1
25448 && matches!(self.peek(), Token::Comma)
25449 {
25450 let form = match self.tokens.get(self.pos + 1) {
25451 Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
25452 let up = f.to_ascii_uppercase();
25453 matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
25454 }
25455 _ => None,
25456 };
25457 if let Some(up) = form {
25458 self.advance(); // comma
25459 self.advance(); // form keyword
25460 args.push(Expr::Literal(Literal::String(up)));
25461 }
25462 }
25463 // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
25464 // form. Desugars to the comma-list shape evaluator already
25465 // handles. Triggered after the first arg when the function
25466 // name is substring / substr and the next token is FROM
25467 // (a reserved keyword in PG; SPG also reserves it).
25468 // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
25469 // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
25470 // internal __substring_similar(str, pat, esc) call.
25471 if (first.eq_ignore_ascii_case("substring")
25472 || first.eq_ignore_ascii_case("substr"))
25473 && args.len() == 1
25474 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
25475 {
25476 self.advance(); // SIMILAR
25477 let pattern = self.parse_expr(0)?;
25478 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
25479 {
25480 return Err(self.err(format!(
25481 "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
25482 self.peek()
25483 )));
25484 }
25485 self.advance(); // ESCAPE
25486 let esc = self.parse_expr(0)?;
25487 if !matches!(self.peek(), Token::RParen) {
25488 return Err(self.err(format!(
25489 "expected ')' to close substring(... SIMILAR ...), got {:?}",
25490 self.peek()
25491 )));
25492 }
25493 self.advance();
25494 args.push(pattern);
25495 args.push(esc);
25496 return Ok(Expr::FunctionCall {
25497 name: "__substring_similar".to_string(),
25498 args,
25499 });
25500 }
25501 if (first.eq_ignore_ascii_case("substring")
25502 || first.eq_ignore_ascii_case("substr"))
25503 && args.len() == 1
25504 && matches!(self.peek(), Token::From | Token::For)
25505 {
25506 // `substring(str FROM pos [FOR len])`, or the FOR-only
25507 // `substring(str FOR len)` which PG treats as FROM 1.
25508 if matches!(self.peek(), Token::From) {
25509 self.advance();
25510 let start = self.parse_expr(0)?;
25511 args.push(start);
25512 } else {
25513 args.push(Expr::Literal(Literal::Integer(1)));
25514 }
25515 if matches!(self.peek(), Token::For) {
25516 self.advance();
25517 let length = self.parse_expr(0)?;
25518 args.push(length);
25519 }
25520 if !matches!(self.peek(), Token::RParen) {
25521 return Err(self.err(format!(
25522 "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
25523 self.peek()
25524 )));
25525 }
25526 self.advance();
25527 return Ok(Expr::FunctionCall {
25528 name: first.to_ascii_lowercase(),
25529 args,
25530 });
25531 }
25532 // PG `overlay(str PLACING repl FROM n [FOR len])`
25533 // syntactic form. Desugars to the `overlay(str,
25534 // repl, n[, len])` comma-list shape the evaluator
25535 // already implements. `PLACING` is not a reserved
25536 // token in SPG, so it arrives as a bare Ident.
25537 if first.eq_ignore_ascii_case("overlay")
25538 && args.len() == 1
25539 && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
25540 {
25541 self.advance(); // consume PLACING
25542 args.push(self.parse_expr(0)?); // replacement
25543 if !matches!(self.peek(), Token::From) {
25544 return Err(self.err(format!(
25545 "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
25546 self.peek()
25547 )));
25548 }
25549 self.advance();
25550 args.push(self.parse_expr(0)?); // start position
25551 if matches!(self.peek(), Token::For) {
25552 self.advance();
25553 args.push(self.parse_expr(0)?); // length
25554 }
25555 if !matches!(self.peek(), Token::RParen) {
25556 return Err(self.err(format!(
25557 "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
25558 self.peek()
25559 )));
25560 }
25561 self.advance();
25562 return Ok(Expr::FunctionCall {
25563 name: String::from("overlay"),
25564 args,
25565 });
25566 }
25567 // `TRIM(chars FROM str)` — the keyword-less
25568 // spelling lands here after the chars parse
25569 // (the keyword forms return earlier).
25570 if first.eq_ignore_ascii_case("trim")
25571 && args.len() == 1
25572 && matches!(self.peek(), Token::From)
25573 {
25574 self.advance();
25575 let target = self.parse_expr(0)?;
25576 if !matches!(self.peek(), Token::RParen) {
25577 return Err(self.err(format!(
25578 "expected ')' to close TRIM(chars FROM str), got {:?}",
25579 self.peek()
25580 )));
25581 }
25582 self.advance();
25583 let chars = args.pop().expect("one arg");
25584 return Ok(Expr::FunctionCall {
25585 name: String::from("btrim"),
25586 args: alloc::vec![target, chars],
25587 });
25588 }
25589 // v7.24 (round-16 A) — aggregate-internal
25590 // ordering: `array_agg(x ORDER BY y DESC NULLS
25591 // LAST)`. Keys close the argument list.
25592 if matches!(self.peek(), Token::Order) {
25593 self.advance();
25594 if !self.peek_is_by() {
25595 return Err(self.err(format!(
25596 "expected BY after ORDER in aggregate args, got {:?}",
25597 self.peek()
25598 )));
25599 }
25600 self.advance();
25601 loop {
25602 // v7.39 (round 691) — save/restore, the discipline this parser
25603 // already uses around `pending_sample_preds`, so a subquery inside
25604 // a key neither inherits nor leaks the channel.
25605 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25606 let saved_coll = self.order_key_collation.take();
25607 let parsed = self.parse_expr(0);
25608 self.in_order_by_key = saved_flag;
25609 let collation =
25610 core::mem::replace(&mut self.order_key_collation, saved_coll);
25611 let expr = parsed?;
25612 let desc = if matches!(self.peek(), Token::Desc) {
25613 self.advance();
25614 true
25615 } else if matches!(self.peek(), Token::Asc) {
25616 self.advance();
25617 false
25618 } else {
25619 false
25620 };
25621 let nulls_first = self.parse_optional_nulls_placement()?;
25622 agg_order_by.push(OrderBy {
25623 expr,
25624 desc,
25625 nulls_first,
25626 collation,
25627 });
25628 if matches!(self.peek(), Token::Comma) {
25629 self.advance();
25630 } else {
25631 break;
25632 }
25633 }
25634 // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
25635 // follow the ORDER BY inside GROUP_CONCAT.
25636 if self.consume_group_concat_separator(&mut args)? {
25637 saw_separator = true;
25638 }
25639 if !matches!(self.peek(), Token::RParen) {
25640 return Err(self.err(format!(
25641 "expected ')' after aggregate ORDER BY, got {:?}",
25642 self.peek()
25643 )));
25644 }
25645 break;
25646 }
25647 // v7.39 (round 354, M12) — …or directly after the
25648 // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
25649 // own spelling of what PG passes as string_agg's second
25650 // argument; it was a parse error, so every MySQL query
25651 // that names its own separator failed outright.
25652 if self.consume_group_concat_separator(&mut args)? {
25653 saw_separator = true;
25654 break;
25655 }
25656 match self.peek() {
25657 Token::Comma => {
25658 self.advance();
25659 }
25660 Token::RParen => break,
25661 other => {
25662 return Err(self.err(format!(
25663 "expected ',' or ')' in function args, got {other:?}"
25664 )));
25665 }
25666 }
25667 }
25668 }
25669 // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
25670 // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
25671 // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
25672 // meaning a separator — that is what the explicit SEPARATOR
25673 // tail is for. Fold them into one `concat(...)` so the
25674 // aggregate keeps its single value argument.
25675 if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
25676 let values = args.len() - usize::from(saw_separator);
25677 if values > 1 {
25678 let sep_arg = if saw_separator { args.pop() } else { None };
25679 let folded = Expr::FunctionCall {
25680 name: "concat".to_string(),
25681 args: core::mem::take(&mut args),
25682 };
25683 args.push(folded);
25684 if let Some(sep) = sep_arg {
25685 args.push(sep);
25686 }
25687 }
25688 }
25689 self.advance(); // consume ')'
25690 // v7.39 (read01 round 77) — named arguments are NOT reordered here
25691 // any more. The parser has no catalog, so it could only ever resolve
25692 // the handful of `make_*` builtins whose parameter names were baked
25693 // into a table right here — every user function got
25694 // "does not support named arguments", though the catalog has been
25695 // storing its parameter names all along. Reordering happens in eval,
25696 // in one place, for builtins and user functions alike.
25697 // v7.32 (round-29) — ordered-set aggregate tail
25698 // `name(direct_args) WITHIN GROUP (ORDER BY …)`
25699 // (percentile_cont / percentile_disc / mode). The sort spec
25700 // lands in the same `order_by` slot a decorated aggregate
25701 // uses; the executor dispatches on the function name. WITHIN
25702 // GROUP and an intra-argument ORDER BY are mutually
25703 // exclusive (PG rejects both).
25704 let within_group_order = self.parse_within_group_clause()?;
25705 if !within_group_order.is_empty() && !agg_order_by.is_empty() {
25706 return Err(self.err(
25707 "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
25708 .into(),
25709 ));
25710 }
25711 let within_group_seen = !within_group_order.is_empty();
25712 let agg_order_by = if within_group_order.is_empty() {
25713 agg_order_by
25714 } else {
25715 within_group_order
25716 };
25717 // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
25718 let filter = self.parse_filter_clause()?;
25719 // v4.12: window-function tail — `name(args) OVER (...)`.
25720 // Promotes the just-parsed FunctionCall into a
25721 // WindowFunction node carrying partition + order.
25722 // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
25723 // / `RESPECT NULLS OVER (...)` between the closing paren
25724 // and `OVER`.
25725 let null_treatment = self.parse_null_treatment_modifier();
25726 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25727 && s.eq_ignore_ascii_case("over")
25728 {
25729 self.advance();
25730 // v7.39 (round 230) — PG implements neither modifier for a
25731 // windowed call and says so (0A000). Both used to be parsed
25732 // and then silently dropped here, so `count(DISTINCT v)
25733 // OVER (…)` quietly answered the non-distinct count.
25734 if agg_distinct {
25735 return Err(
25736 self.err("DISTINCT is not implemented for window functions".to_string())
25737 );
25738 }
25739 if !agg_order_by.is_empty() {
25740 // PG separates the two shapes that land here: a
25741 // WITHIN GROUP call is an ordered-set aggregate and gets
25742 // its own message naming the aggregate; a plain
25743 // `agg(x ORDER BY y)` gets the generic one.
25744 let msg = if within_group_seen {
25745 alloc::format!("OVER is not supported for ordered-set aggregate {first}")
25746 } else {
25747 "aggregate ORDER BY is not implemented for window functions".to_string()
25748 };
25749 return Err(self.err(msg));
25750 }
25751 let (partition_by, order_by, frame) = self.parse_over_clause()?;
25752 return Ok(Expr::WindowFunction {
25753 name: first,
25754 args,
25755 partition_by,
25756 order_by,
25757 frame,
25758 null_treatment,
25759 filter,
25760 });
25761 }
25762 if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
25763 return Ok(Expr::AggregateOrdered {
25764 call: Box::new(Expr::FunctionCall { name: first, args }),
25765 order_by: agg_order_by,
25766 distinct: agg_distinct,
25767 filter,
25768 });
25769 }
25770 // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
25771 // over TIMESTAMPTZ and has no timestamp overload, so a
25772 // timestamp argument is coerced on the way in and the answer
25773 // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
25774 // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
25775 // zone`. SPG answered `timestamp without time zone`, dropping
25776 // the offset from every rendering.
25777 //
25778 // Writing the coercion PG performs makes the existing
25779 // argument-driven typing (the one `date_trunc` uses) reach the
25780 // right answer, rather than teaching the type layer a second
25781 // rule. MySQL's DATE_ADD is a different function that returns
25782 // DATE or DATETIME, so this is PG-dialect only.
25783 //
25784 // Out-of-line because this sits on the RECURSIVE descent
25785 // frame: an inline block with locals here costs every nesting
25786 // level, and the suite's deep-nesting sentinel overflowed the
25787 // 512 KiB parser stack the moment one was added (round 430's
25788 // lesson, in the same shape).
25789 if !self.mysql_dialect {
25790 lift_date_add_arg_to_timestamptz(&first, &mut args);
25791 }
25792 return Ok(Expr::FunctionCall { name: first, args });
25793 }
25794 // v7.9.20 — SQL-standard parenless keyword expressions
25795 // (PG treats these as functions called without parens).
25796 // Resolve to a synthetic FunctionCall so the engine's
25797 // eval path reuses the existing function-call routing.
25798 // mailrs G3.
25799 let lc = first.to_ascii_lowercase();
25800 if matches!(
25801 lc.as_str(),
25802 "current_date"
25803 | "current_time"
25804 | "current_timestamp"
25805 | "localtimestamp"
25806 | "localtime"
25807 // v7.37.17 (17.6 siblings) — session-identity SQL-
25808 // standard parenless keywords. current_user /
25809 // session_user / user were already caught by the
25810 // pgwire canned-response shortcut but bare-select
25811 // in the embedded engine went through Expr::Column
25812 // and errored. Adding them here so the parser
25813 // resolves to a synthetic FunctionCall that reuses
25814 // the existing eval/functions.rs dispatch.
25815 | "current_user"
25816 | "session_user"
25817 | "current_role"
25818 | "current_catalog"
25819 | "current_schema"
25820 | "current_database"
25821 // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
25822 | "system_user"
25823 ) {
25824 return Ok(Expr::FunctionCall {
25825 name: lc,
25826 args: Vec::new(),
25827 });
25828 }
25829 Ok(Expr::Column(ColumnName {
25830 qualifier: None,
25831 name: first,
25832 }))
25833 }
25834}
25835
25836/// v7.39 (round 522) — write the coercion PG's `date_add` /
25837/// `date_subtract` signature performs.
25838///
25839/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
25840/// timestamp argument is cast on the way in and the answer is
25841/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
25842/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
25843/// `timestamp without time zone`, dropping the offset from every
25844/// rendering of the result.
25845///
25846/// Writing the cast the signature implies lets the existing
25847/// argument-driven typing (the one `date_trunc` uses) reach the right
25848/// answer instead of teaching the type layer a second rule. MySQL's
25849/// DATE_ADD is a different function returning DATE or DATETIME, so the
25850/// caller applies this in PG dialect only.
25851///
25852/// A free function, and not a block at the call site, because the caller
25853/// is on the recursive-descent frame chain.
25854#[inline(never)]
25855fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
25856 if args.len() != 2
25857 || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
25858 {
25859 return;
25860 }
25861 let base = args.remove(0);
25862 args.insert(
25863 0,
25864 Expr::Cast {
25865 expr: Box::new(base),
25866 target: CastTarget::Timestamptz,
25867 },
25868 );
25869}
25870
25871/// v6.8.2 — walk an expression tree and return the first column
25872/// reference's bare name. Used by `parse_create_index_stmt_after_create`
25873/// to derive `CreateIndexStatement.column` from an expression
25874/// key (so downstream planner code resolving a primary column
25875/// position keeps working with expression indexes). Returns
25876/// `None` when the expression has no column ref at all — caller
25877/// surfaces that as a parse error.
25878fn extract_first_column(expr: &Expr) -> Option<String> {
25879 match expr {
25880 Expr::Column(cn) => Some(cn.name.clone()),
25881 Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
25882 Expr::Binary { lhs, rhs, .. } => {
25883 extract_first_column(lhs).or_else(|| extract_first_column(rhs))
25884 }
25885 Expr::Unary { expr: e, .. } => extract_first_column(e),
25886 // v7.39 (read01 round 93) — a cast wraps its operand: a common
25887 // expression-index key is `lower(col::text)`, where the column
25888 // sits under the `::text` cast inside the function arg. Without
25889 // descending here the key was rejected as "references no column".
25890 Expr::Cast { expr: e, .. } => extract_first_column(e),
25891 _ => None,
25892 }
25893}
25894
25895fn maybe_not(expr: Expr, negated: bool) -> Expr {
25896 if negated {
25897 Expr::Unary {
25898 op: UnOp::Not,
25899 expr: Box::new(expr),
25900 }
25901 } else {
25902 expr
25903 }
25904}
25905
25906/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
25907/// things in the two dialects, and SPG read all three PG's way:
25908///
25909/// | token | PG (and SPG) | MySQL, measured |
25910/// |---|---|---|
25911/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
25912/// | `&&` | inet / array overlap | **AND** |
25913/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
25914///
25915/// `1 || 0` answering the string '10' on a MySQL session is a wrong
25916/// answer with no error, which is why they are routed here rather than
25917/// left to the shared table.
25918impl Parser {
25919 fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
25920 if self.mysql_dialect {
25921 // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
25922 // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
25923 // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
25924 if let Token::Ident(w) = tok
25925 && w.eq_ignore_ascii_case("div")
25926 {
25927 return Some((BinOp::IntDiv, 8));
25928 }
25929 // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
25930 // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
25931 // the lexer; the `MOD(x, y)` function form is unaffected (MOD
25932 // there sits in operand position, not infix).
25933 if let Token::Ident(w) = tok
25934 && w.eq_ignore_ascii_case("mod")
25935 {
25936 return Some((BinOp::Mod, 8));
25937 }
25938 // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
25939 // plain ident to the lexer. Its precedence sits between OR (1)
25940 // and AND (3) — hence rung 2, the slot freed by moving AND up.
25941 if let Token::Ident(w) = tok
25942 && w.eq_ignore_ascii_case("xor")
25943 {
25944 return Some((BinOp::LogicalXor, 2));
25945 }
25946 match tok {
25947 Token::Concat => return Some((BinOp::Or, 1)),
25948 // MySQL's `&&` is logical AND, sharing AND's rung (3).
25949 Token::InetOverlap => return Some((BinOp::And, 3)),
25950 // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
25951 Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
25952 _ => {}
25953 }
25954 }
25955 binop_from(tok)
25956 }
25957}
25958
25959// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
25960// (which sits strictly between OR and AND), every level from AND upward was
25961// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
25962// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
25963// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
25964// the *relative* order of every PG operator is unchanged by the shift.
25965fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
25966 let pair = match tok {
25967 Token::Or => (BinOp::Or, 1),
25968 Token::And => (BinOp::And, 3),
25969 Token::Eq => (BinOp::Eq, 5),
25970 Token::NotEq => (BinOp::NotEq, 5),
25971 Token::Lt => (BinOp::Lt, 5),
25972 Token::LtEq => (BinOp::LtEq, 5),
25973 Token::Gt => (BinOp::Gt, 5),
25974 Token::GtEq => (BinOp::GtEq, 5),
25975 // pgvector distance ops all sit on the same rung — tighter than
25976 // comparisons (5) so `col <-> v < threshold` parses correctly.
25977 Token::L2Distance => (BinOp::L2Distance, 6),
25978 // v7.39 (read01 geo_ops.c) — geometric predicates ride the
25979 // comparison rung.
25980 Token::GeomParallel => (BinOp::GeomParallel, 5),
25981 // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
25982 // comparison rung.
25983 Token::OverLeft => (BinOp::OverLeft, 5),
25984 Token::OverRight => (BinOp::OverRight, 5),
25985 Token::GeomPerp => (BinOp::GeomPerp, 5),
25986 Token::GeomSameAs => (BinOp::GeomSameAs, 5),
25987 Token::ClosestPoint => (BinOp::ClosestPoint, 6),
25988 Token::GeomHoriz => (BinOp::GeomHoriz, 5),
25989 Token::InnerProduct => (BinOp::InnerProduct, 6),
25990 Token::CosineDistance => (BinOp::CosineDistance, 6),
25991 Token::Plus => (BinOp::Add, 7),
25992 Token::Minus => (BinOp::Sub, 7),
25993 // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
25994 // binds every "other" operator (`||`, `|`, `&`, `#`, the
25995 // pgvector distances above) BETWEEN additive (7) and the
25996 // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
25997 // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
25998 // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
25999 // ("matches PG conceptually" — the round-753 audit measured it
26000 // false; the old rung errored on `'a' || 1 + 1` with
26001 // `text + integer`). Same-level chains left-fold, as PG does.
26002 Token::Concat => (BinOp::Concat, 6),
26003 Token::Pipe => (BinOp::BitOr, 6),
26004 Token::Amp => (BinOp::BitAnd, 6),
26005 Token::Star => (BinOp::Mul, 8),
26006 Token::Slash => (BinOp::Div, 8),
26007 Token::Percent => (BinOp::Mod, 8),
26008 // v4.14: JSON path ops bind tighter than comparisons (5)
26009 // and additive (7) so `doc->'k' = 'v'` parses correctly.
26010 // Same rung as the multiplicative ops.
26011 Token::JsonGet => (BinOp::JsonGet, 8),
26012 Token::JsonGetText => (BinOp::JsonGetText, 8),
26013 Token::JsonGetPath => (BinOp::JsonGetPath, 8),
26014 Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
26015 Token::JsonContains => (BinOp::JsonContains, 8),
26016 Token::JsonPathExists => (BinOp::JsonPathExists, 8),
26017 Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
26018 Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
26019 Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
26020 Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
26021 Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
26022 // v7.12.2 — `@@` binds at the comparison rung (looser than
26023 // arithmetic, tighter than AND / OR). PG places `@@` at
26024 // the same precedence as `=` / `<`, so we follow.
26025 Token::TsMatch => (BinOp::TsMatch, 5),
26026 // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
26027 // PG places these at the comparison rung (same level as `=`),
26028 // so we follow.
26029 Token::InetContainedBy => (BinOp::InetContainedBy, 5),
26030 Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
26031 Token::InetContains => (BinOp::InetContains, 5),
26032 Token::InetContainsEq => (BinOp::InetContainsEq, 5),
26033 Token::InetOverlap => (BinOp::InetOverlap, 5),
26034 // v7.39 (round 508) — the geometric and pattern-order predicates
26035 // ride the comparison rung, as every other predicate does.
26036 Token::Intersects => (BinOp::Intersects, 5),
26037 Token::IsBelow => (BinOp::IsBelow, 5),
26038 Token::IsAbove => (BinOp::IsAbove, 5),
26039 Token::PatternLt => (BinOp::PatternLt, 5),
26040 Token::PatternLtEq => (BinOp::PatternLtEq, 5),
26041 Token::PatternGt => (BinOp::PatternGt, 5),
26042 Token::PatternGtEq => (BinOp::PatternGtEq, 5),
26043 // `@@@` is the old spelling of `@@` and means exactly it.
26044 Token::TsMatchOld => (BinOp::TsMatch, 5),
26045 _ => return None,
26046 };
26047 Some(pair)
26048}
26049
26050#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26051// `as f32` here is intentional: vector elements widen / narrow into f32 on
26052// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
26053// past ~15 decimal digits — both are acceptable for a fixed-precision
26054// pgvector column.
26055/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
26056/// implicit table alias and break trailing clauses. WITH lands
26057/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
26058/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
26059/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
26060/// / VALUES / FOR / LATERAL — all of which would otherwise be
26061/// silently swallowed by `parse_optional_alias`.
26062fn is_alias_stopword(s: &str) -> bool {
26063 matches!(
26064 s.to_ascii_lowercase().as_str(),
26065 "with"
26066 | "on"
26067 | "where"
26068 | "having"
26069 | "group"
26070 | "order"
26071 | "limit"
26072 | "offset"
26073 | "union"
26074 | "except"
26075 | "intersect"
26076 | "returning"
26077 | "set"
26078 | "values"
26079 | "for"
26080 | "window"
26081 | "tablesample"
26082 | "lateral"
26083 | "left"
26084 | "right"
26085 | "inner"
26086 | "outer"
26087 | "full"
26088 | "cross"
26089 | "join"
26090 | "natural"
26091 | "using"
26092 | "fetch"
26093 )
26094}
26095
26096fn extract_numeric_literal(e: &Expr) -> Option<f32> {
26097 match e {
26098 Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
26099 Expr::Literal(Literal::Float(x)) => Some(*x as f32),
26100 // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
26101 // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
26102 // so scale the divisor by hand instead of `f32::powi`.)
26103 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26104 let mut div = 1.0f32;
26105 for _ in 0..*scale {
26106 div *= 10.0;
26107 }
26108 Some(*unscaled as f32 / div)
26109 }
26110 Expr::Unary {
26111 op: UnOp::Neg,
26112 expr,
26113 } => extract_numeric_literal(expr).map(|x| -x),
26114 _ => None,
26115 }
26116}
26117
26118/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
26119/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
26120/// negative. Returns `None` if any pair fails to parse or no pair is found.
26121///
26122/// Recognised units (case-insensitive, optional trailing `s`):
26123/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
26124/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
26125/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
26126/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
26127/// (PG-canonical: DST and month-boundary semantics depend on this).
26128/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
26129/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
26130/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
26131#[allow(clippy::cast_possible_truncation)]
26132fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
26133 let mut months: i64 = 0;
26134 let mut days: i64 = 0;
26135 let mut micros: i64 = 0;
26136 let mut in_time = false;
26137 let mut num = alloc::string::String::new();
26138 for ch in rest.chars() {
26139 if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
26140 num.push(ch);
26141 continue;
26142 }
26143 if ch == 'T' || ch == 't' {
26144 if !num.is_empty() {
26145 return None;
26146 }
26147 in_time = true;
26148 continue;
26149 }
26150 let n: f64 = num.parse().ok()?;
26151 num.clear();
26152 match (ch, in_time) {
26153 ('Y' | 'y', false) => months += (n * 12.0) as i64,
26154 ('M', false) => months += n as i64,
26155 ('W' | 'w', false) => days += (n * 7.0) as i64,
26156 ('D' | 'd', false) => days += n as i64,
26157 ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26158 ('M', true) => micros += (n * 60_000_000.0) as i64,
26159 ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26160 _ => return None,
26161 }
26162 }
26163 if !num.is_empty() {
26164 return None;
26165 }
26166 Some((
26167 i32::try_from(months).ok()?,
26168 i32::try_from(days).ok()?,
26169 micros,
26170 ))
26171}
26172
26173/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
26174/// leading `-` negates the whole value). Rejects date-like strings.
26175fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
26176 let (neg, body) = match s.strip_prefix('-') {
26177 Some(b) => (true, b),
26178 None => (false, s),
26179 };
26180 let (y, m) = body.split_once('-')?;
26181 let years: i32 = y.parse().ok()?;
26182 let mons: i32 = m.parse().ok()?;
26183 if years < 0 || mons < 0 {
26184 return None;
26185 }
26186 let total = years.checked_mul(12)?.checked_add(mons)?;
26187 Some((if neg { -total } else { total }, 0, 0))
26188}
26189
26190/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
26191/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
26192fn parse_interval_clock(tok: &str) -> Option<i64> {
26193 let (neg, body) = match tok.strip_prefix('-') {
26194 Some(r) => (true, r),
26195 None => (false, tok.strip_prefix('+').unwrap_or(tok)),
26196 };
26197 let mut it = body.split(':');
26198 let h: i64 = it.next()?.parse().ok()?;
26199 let m: i64 = it.next()?.parse().ok()?;
26200 let s_tok = it.next().unwrap_or("0");
26201 if it.next().is_some() {
26202 return None;
26203 }
26204 let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
26205 let sec: i64 = sec.parse().ok()?;
26206 let mut f = alloc::string::String::from(frac);
26207 while f.len() < 6 {
26208 f.push('0');
26209 }
26210 f.truncate(6);
26211 let fus: i64 = f.parse().ok()?;
26212 sec.checked_mul(1_000_000)?.checked_add(fus)?
26213 } else {
26214 s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
26215 };
26216 let total = h
26217 .checked_mul(3_600_000_000)?
26218 .checked_add(m.checked_mul(60_000_000)?)?
26219 .checked_add(sec_us)?;
26220 Some(if neg { -total } else { total })
26221}
26222
26223/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
26224/// every spelling PG accepts (measured against live PG18.4, not guessed):
26225/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
26226/// Before this, the unit table matched long names only, with an ad-hoc
26227/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
26228/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
26229/// INTERVAL", and it had also grown arms for the debris that stripping leaves
26230/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
26231/// fractional) both read from this one table now.
26232fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
26233 let u = raw.to_ascii_lowercase();
26234 Some(match u.as_str() {
26235 "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
26236 "microsecond"
26237 }
26238 "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
26239 "millisecond"
26240 }
26241 "second" | "seconds" | "sec" | "secs" | "s" => "second",
26242 "minute" | "minutes" | "min" | "mins" | "m" => "minute",
26243 "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
26244 "day" | "days" | "d" => "day",
26245 "week" | "weeks" | "w" => "week",
26246 "month" | "months" | "mon" | "mons" => "month",
26247 "year" | "years" | "yr" | "yrs" | "y" => "year",
26248 "decade" | "decades" | "dec" | "decs" => "decade",
26249 "century" | "centuries" | "cent" | "c" => "century",
26250 "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
26251 _ => return None,
26252 })
26253}
26254
26255/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
26256/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
26257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26258pub(crate) enum IntervalField {
26259 Year,
26260 Month,
26261 Day,
26262 Hour,
26263 Minute,
26264 Second,
26265}
26266
26267/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
26268/// spellings aren't standard for the qualifier position, so only the singular
26269/// forms are accepted.
26270/// v7.39 (round 350, M7) — MySQL's interval units, measured against
26271/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
26272/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
26273/// take a `'1 2'` style literal — are not read here; they stay a parse
26274/// error rather than being silently misread.)
26275/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
26276///
26277/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
26278/// to do with a `@@` engine setting, and an unset one reads NULL rather
26279/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
26280/// were the same node and `SELECT @x` answered "Unknown system variable".)
26281/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
26282/// not see a session override — measured, after `SET autocommit=0`,
26283/// `@@global.autocommit` is still 1.
26284///
26285/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
26286/// the parser's nesting budget is tuned against, and building these
26287/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
26288/// wall `parse_left_right_atom` and friends were factored out for).
26289#[inline(never)]
26290fn variable_ref_atom(raw: &str) -> Expr {
26291 let user_var = !raw.starts_with("@@");
26292 let bare = raw.trim_start_matches('@').to_ascii_lowercase();
26293 Expr::FunctionCall {
26294 name: String::from(if user_var {
26295 "__spg_user_var"
26296 } else {
26297 "__spg_session_var"
26298 }),
26299 args: alloc::vec![Expr::Literal(Literal::String(bare))],
26300 }
26301}
26302
26303fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
26304 let Token::Ident(s) = tok else { return None };
26305 Some(match () {
26306 () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
26307 () if s.eq_ignore_ascii_case("second") => "second",
26308 () if s.eq_ignore_ascii_case("minute") => "minute",
26309 () if s.eq_ignore_ascii_case("hour") => "hour",
26310 () if s.eq_ignore_ascii_case("day") => "day",
26311 () if s.eq_ignore_ascii_case("week") => "week",
26312 () if s.eq_ignore_ascii_case("month") => "month",
26313 () if s.eq_ignore_ascii_case("quarter") => "quarter",
26314 () if s.eq_ignore_ascii_case("year") => "year",
26315 () => return None,
26316 })
26317}
26318
26319/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
26320/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
26321/// which constructs the value at run time. Only the slot the unit names
26322/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
26323/// slot the builtin has (months and fractional seconds respectively).
26324fn make_interval_call(qty: Expr, unit: &str) -> Expr {
26325 let zero = || Expr::Literal(Literal::Integer(0));
26326 let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
26327 lhs: alloc::boxed::Box::new(qty.clone()),
26328 op,
26329 rhs: alloc::boxed::Box::new(by),
26330 };
26331 // (years, months, weeks, days, hours, mins, secs)
26332 let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
26333 match unit {
26334 "year" => args[0] = qty,
26335 "quarter" => {
26336 args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
26337 }
26338 "month" => args[1] = qty,
26339 "week" => args[2] = qty,
26340 "day" => args[3] = qty,
26341 "hour" => args[4] = qty,
26342 "minute" => args[5] = qty,
26343 "second" => args[6] = qty,
26344 // The builtin's seconds slot takes a fraction, so microseconds ride
26345 // it scaled down; the divisor is a NUMERIC literal so the division
26346 // stays exact rather than going through a float.
26347 "microsecond" => {
26348 args[6] = scaled(
26349 crate::ast::BinOp::Div,
26350 Expr::Literal(Literal::Numeric {
26351 unscaled: 1_000_000,
26352 scale: 0,
26353 }),
26354 );
26355 }
26356 _ => args[3] = qty,
26357 }
26358 Expr::FunctionCall {
26359 name: alloc::string::String::from("make_interval"),
26360 args,
26361 }
26362}
26363
26364/// `(count, unit)` → `(months, days, micros)`.
26365fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
26366 let n: i64 = count.trim().parse().ok()?;
26367 Some(match unit {
26368 "microsecond" => (0, 0, n),
26369 "second" => (0, 0, n.checked_mul(1_000_000)?),
26370 "minute" => (0, 0, n.checked_mul(60_000_000)?),
26371 "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
26372 "day" => (0, i32::try_from(n).ok()?, 0),
26373 "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
26374 "month" => (i32::try_from(n).ok()?, 0, 0),
26375 "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
26376 "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
26377 _ => return None,
26378 })
26379}
26380
26381fn interval_field_of(tok: &Token) -> Option<IntervalField> {
26382 let Token::Ident(s) = tok else { return None };
26383 Some(match () {
26384 () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
26385 () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
26386 () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
26387 () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
26388 () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
26389 () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
26390 () => return None,
26391 })
26392}
26393
26394/// v7.39 (read01 round 102) — interpret an interval literal under a field
26395/// qualifier. Returns `(months, days, micros)`.
26396///
26397/// * A single field applied to a bare number sets which unit the number means,
26398/// truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
26399/// SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
26400/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
26401/// * Every other range, and any literal a single field can't read as a plain
26402/// number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
26403/// interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
26404/// like PG, and the qualifier there only bounds precision.
26405fn interpret_qualified_interval(
26406 text: &str,
26407 (f1, f2): (IntervalField, Option<IntervalField>),
26408) -> Option<(i32, i32, i64)> {
26409 if let Some(f2) = f2 {
26410 if f1 == IntervalField::Year && f2 == IntervalField::Month {
26411 if let Some(m) = parse_year_month_literal(text) {
26412 return Some((m, 0, 0));
26413 }
26414 }
26415 return parse_interval_text(text);
26416 }
26417 // Single field: reinterpret a bare number; otherwise the default parse.
26418 let trimmed = text.trim();
26419 if let Ok(val) = trimmed.parse::<f64>() {
26420 // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
26421 #[allow(clippy::cast_possible_truncation)]
26422 let whole = val as i64;
26423 #[allow(clippy::cast_possible_truncation)]
26424 let secs_micros = {
26425 let m = val * 1_000_000.0;
26426 if m >= 0.0 {
26427 (m + 0.5) as i64
26428 } else {
26429 (m - 0.5) as i64
26430 }
26431 };
26432 return Some(match f1 {
26433 IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
26434 IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
26435 IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
26436 IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
26437 IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
26438 IntervalField::Second => (0, 0, secs_micros),
26439 });
26440 }
26441 parse_interval_text(text)
26442}
26443
26444/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
26445fn parse_year_month_literal(text: &str) -> Option<i32> {
26446 let t = text.trim();
26447 let (neg, body) = match t.strip_prefix('-') {
26448 Some(r) => (true, r),
26449 None => (false, t.strip_prefix('+').unwrap_or(t)),
26450 };
26451 let mut it = body.split('-');
26452 let years: i32 = it.next()?.trim().parse().ok()?;
26453 let months: i32 = match it.next() {
26454 Some(m) => m.trim().parse().ok()?,
26455 None => 0,
26456 };
26457 if it.next().is_some() {
26458 return None;
26459 }
26460 let total = years.checked_mul(12)?.checked_add(months)?;
26461 Some(if neg { -total } else { total })
26462}
26463
26464pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
26465 // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
26466 // `@` is decorative; a trailing `ago` negates the whole interval.
26467 let mut trimmed = s.trim();
26468 trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
26469 let mut negate = false;
26470 if let Some(rest) = trimmed
26471 .strip_suffix("ago")
26472 .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
26473 {
26474 negate = true;
26475 trimmed = rest.trim();
26476 }
26477 let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
26478 let (mo, d, us) = v?;
26479 if negate {
26480 Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
26481 } else {
26482 Some((mo, d, us))
26483 }
26484 };
26485 let s = trimmed;
26486 // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
26487 // are single tokens, not the `<n> <unit>` pair form handled below.
26488 if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
26489 return finish(parse_iso8601_interval(rest));
26490 }
26491 if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
26492 if let Some(iv) = parse_year_month_interval(trimmed) {
26493 return finish(Some(iv));
26494 }
26495 }
26496 // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
26497 // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
26498 // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
26499 if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
26500 if let Ok(n) = trimmed.parse::<i64>() {
26501 return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
26502 }
26503 if let Ok(f) = trimmed.parse::<f64>() {
26504 if f.is_finite() {
26505 #[allow(clippy::cast_possible_truncation)]
26506 return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
26507 }
26508 }
26509 }
26510 // v7.39 (round 243) — PG accepts the number and unit run together
26511 // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
26512 // the `<n> <unit>` pair loop below sees them as two.
26513 let raw_parts: Vec<&str> = s.split_whitespace().collect();
26514 let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
26515 for p in raw_parts {
26516 let boundary = p
26517 .char_indices()
26518 .find(|(i, c)| {
26519 *i > 0
26520 && c.is_ascii_alphabetic()
26521 && p[..*i]
26522 .chars()
26523 .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
26524 && p[..*i].chars().any(|d| d.is_ascii_digit())
26525 })
26526 .map(|(i, _)| i);
26527 match boundary {
26528 Some(i) => {
26529 parts.push(&p[..i]);
26530 parts.push(&p[i..]);
26531 }
26532 None => parts.push(p),
26533 }
26534 }
26535 // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
26536 // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
26537 // remains is the `<n> <unit>` pair form handled below.
26538 let mut clock_us: i64 = 0;
26539 let mut had_clock = false;
26540 if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
26541 clock_us = parse_interval_clock(parts[pos])?;
26542 parts.remove(pos);
26543 had_clock = true;
26544 }
26545 // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
26546 // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
26547 let mut lone_days: i32 = 0;
26548 if had_clock && parts.len() == 1 {
26549 if let Ok(n) = parts[0].parse::<i64>() {
26550 lone_days = i32::try_from(n).ok()?;
26551 parts.clear();
26552 }
26553 }
26554 if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
26555 return None;
26556 }
26557 let mut months: i32 = 0;
26558 let mut days: i32 = lone_days;
26559 let mut micros: i64 = clock_us;
26560 let mut i = 0;
26561 while i < parts.len() {
26562 let unit_stripped = canonical_interval_unit(parts[i + 1])?;
26563 if let Ok(n) = parts[i].parse::<i64>() {
26564 match unit_stripped {
26565 "microsecond" => micros = micros.checked_add(n)?,
26566 "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
26567 "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
26568 "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
26569 "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
26570 "day" => {
26571 let n32 = i32::try_from(n).ok()?;
26572 days = days.checked_add(n32)?;
26573 }
26574 "week" => {
26575 let n32 = i32::try_from(n).ok()?;
26576 days = days.checked_add(n32.checked_mul(7)?)?;
26577 }
26578 "month" => {
26579 let n32 = i32::try_from(n).ok()?;
26580 months = months.checked_add(n32)?;
26581 }
26582 "year" => {
26583 let n32 = i32::try_from(n).ok()?;
26584 months = months.checked_add(n32.checked_mul(12)?)?;
26585 }
26586 // v7.39 (read01 timestamp.c) — the larger calendar units.
26587 "decade" => {
26588 let n32 = i32::try_from(n).ok()?;
26589 months = months.checked_add(n32.checked_mul(120)?)?;
26590 }
26591 "century" => {
26592 let n32 = i32::try_from(n).ok()?;
26593 months = months.checked_add(n32.checked_mul(1200)?)?;
26594 }
26595 "millennium" => {
26596 let n32 = i32::try_from(n).ok()?;
26597 months = months.checked_add(n32.checked_mul(12000)?)?;
26598 }
26599 _ => return None,
26600 }
26601 } else if let Ok(f) = parts[i].parse::<f64>() {
26602 // Fractional units cascade down to the next-finer field the way
26603 // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
26604 // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
26605 // no_std: f64 has no trunc/fract/round methods, so do them with
26606 // casts (toward-zero) + explicit round-half-away-from-zero.
26607 #[allow(clippy::cast_possible_truncation)]
26608 fn round_i64(x: f64) -> i64 {
26609 if x >= 0.0 {
26610 (x + 0.5) as i64
26611 } else {
26612 (x - 0.5) as i64
26613 }
26614 }
26615 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26616 fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
26617 const DAY_US: f64 = 86_400_000_000.0;
26618 let whole = d as i64; // truncates toward zero
26619 let frac = d - whole as f64;
26620 *days = days.checked_add(i32::try_from(whole).ok()?)?;
26621 *micros = micros.checked_add(round_i64(frac * DAY_US))?;
26622 Some(())
26623 }
26624 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26625 match unit_stripped {
26626 "microsecond" => micros = micros.checked_add(round_i64(f))?,
26627 "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
26628 "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
26629 "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
26630 "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
26631 "day" => add_days_frac(&mut days, &mut micros, f)?,
26632 "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
26633 "month" => {
26634 let whole = f as i64;
26635 months = months.checked_add(i32::try_from(whole).ok()?)?;
26636 add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
26637 }
26638 "year" => {
26639 let m = f * 12.0;
26640 let whole = m as i64;
26641 months = months.checked_add(i32::try_from(whole).ok()?)?;
26642 add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
26643 }
26644 _ => return None,
26645 }
26646 } else {
26647 return None;
26648 }
26649 i += 2;
26650 }
26651 finish(Some((months, days, micros)))
26652}
26653
26654/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
26655/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
26656/// `interval` is intentionally absent (handled by its own parser arm).
26657/// Returns `None` for names that aren't sensible as a bare typed literal, so
26658/// the caller falls back to treating the ident as a column reference.
26659fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
26660 Some(match ident {
26661 "date" => CastTarget::Date,
26662 "timestamp" | "datetime" => CastTarget::Timestamp,
26663 "timestamptz" => CastTarget::Timestamptz,
26664 "bool" | "boolean" => CastTarget::Bool,
26665 "int" | "integer" | "int4" => CastTarget::Int,
26666 "bigint" | "int8" => CastTarget::BigInt,
26667 "float8" | "double precision" => CastTarget::Float,
26668 "uuid" => CastTarget::Uuid,
26669 "bytea" => CastTarget::Bytea,
26670 "json" => CastTarget::Json,
26671 "jsonb" => CastTarget::Jsonb,
26672 // Types without a dedicated CastTarget variant flow through the
26673 // generic Named path (engine resolves via column_type_to_data_type).
26674 "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
26675 | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
26676 | "money" | "bit" | "varbit"
26677 // Geometric types accept the `TYPE 'literal'` prefix spelling too.
26678 | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
26679 // Range / multirange types likewise.
26680 | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
26681 | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
26682 | "datemultirange" | "tsmultirange" | "tstzmultirange"
26683 // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
26684 | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
26685 CastTarget::Named(alloc::string::String::from(ident))
26686 }
26687 _ => return None,
26688 })
26689}
26690
26691/// v7.12.4 — map a bare type-name identifier (the form that
26692/// appears in a function arg list or RETURNS clause) to a
26693/// [`ColumnTypeName`]. Returns `None` for unknown / extension
26694/// types so the caller can preserve them as
26695/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
26696///
26697/// Subset of the full column-type grammar — we deliberately
26698/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
26699/// here because function-arg types in v7.12.4 are mostly the
26700/// bare form (`text`, `int`, `bytea`, …).
26701/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
26702/// than being `name TYPE`?
26703///
26704/// The multi-word spellings SQL allows for a bare argument type, each
26705/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
26706///
26707/// NOTE this list also exists in `spg-storage`, which computes the
26708/// signature key from the rendered argument text and has to reach the
26709/// same verdict. The two crates are siblings — neither depends on the
26710/// other — and each already carries its own table of type spellings
26711/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
26712/// there), so this follows the structure rather than inventing new
26713/// duplication. Recorded as V49.
26714pub fn is_multiword_type_phrase(phrase: &str) -> bool {
26715 let t = phrase.trim().to_ascii_lowercase();
26716 let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
26717 matches!(
26718 base,
26719 "double precision"
26720 | "character varying"
26721 | "bit varying"
26722 | "timestamp with time zone"
26723 | "timestamp without time zone"
26724 | "time with time zone"
26725 | "time without time zone"
26726 | "national character"
26727 | "national character varying"
26728 )
26729}
26730
26731fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
26732 Some(match ident.to_ascii_lowercase().as_str() {
26733 "smallint" | "tinyint" => ColumnTypeName::SmallInt,
26734 "int" | "integer" | "mediumint" => ColumnTypeName::Int,
26735 "bigint" => ColumnTypeName::BigInt,
26736 "float" | "double" => ColumnTypeName::Float,
26737 // v7.39 (round 269) — real is 32-bit.
26738 "real" | "float4" => ColumnTypeName::Real,
26739 "text" => ColumnTypeName::Text,
26740 "bool" | "boolean" => ColumnTypeName::Bool,
26741 "date" => ColumnTypeName::Date,
26742 "timestamp" | "datetime" => ColumnTypeName::Timestamp,
26743 "timestamptz" => ColumnTypeName::Timestamptz,
26744 "json" => ColumnTypeName::Json,
26745 "jsonb" => ColumnTypeName::Jsonb,
26746 "bytea" | "bytes" => ColumnTypeName::Bytes,
26747 "tsvector" => ColumnTypeName::TsVector,
26748 "tsquery" => ColumnTypeName::TsQuery,
26749 "uuid" => ColumnTypeName::Uuid,
26750 "interval" => ColumnTypeName::Interval,
26751 "time" => ColumnTypeName::Time,
26752 "year" => ColumnTypeName::Year,
26753 "timetz" => ColumnTypeName::TimeTz,
26754 "money" => ColumnTypeName::Money,
26755 _ => return None,
26756 })
26757}
26758
26759/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
26760/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
26761///
26762/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
26763/// / embedded SQL land in v7.12.5+):
26764///
26765/// ```text
26766/// body := [ws] block [ws]
26767/// block := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
26768/// stmt := assign | return
26769/// assign := assign_target := expr
26770/// assign_target := ( NEW | OLD ) . ident | ident
26771/// return := RETURN ( NEW | OLD | NULL | expr )
26772/// ```
26773///
26774/// `expr` is parsed by recursing into the regular `Parser` — so a
26775/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
26776/// NEW.subject || ' ' || NEW.sender)` body shape works without
26777/// the body parser knowing what `to_tsvector` is.
26778///
26779/// Errors here cause the caller to fall back to
26780/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
26781/// successful, but the executor will refuse to invoke the
26782/// function with an "unparseable body" error.
26783/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
26784/// from the crate root as `spg_sql::parse_function_body`.
26785pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26786 parse_plpgsql_body(body)
26787}
26788
26789fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26790 // Use the regular lexer on the body text. The trailing
26791 // `END;` may or may not have a semicolon; the lexer treats
26792 // both forms identically.
26793 let tokens = lexer::tokenize(body).map_err(|e| ParseError {
26794 message: alloc::format!("plpgsql body lex error: {e}"),
26795 token_pos: 0,
26796 })?;
26797 let mut parser = Parser::new(tokens);
26798 parser.parse_plpgsql_block()
26799}
26800
26801/// v7.39 (GUC) — the textual body of a SET value, for list joining.
26802fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
26803 match v {
26804 crate::ast::SetValue::String(s)
26805 | crate::ast::SetValue::Ident(s)
26806 | crate::ast::SetValue::Number(s) => s.clone(),
26807 crate::ast::SetValue::Default => "DEFAULT".into(),
26808 }
26809}
26810
26811/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
26812/// contains an aggregate call at ITS OWN query level (recursion stops at
26813/// sublink boundaries — a sublink's aggregates belong to the sublink).
26814/// Backs the "aggregate functions are not allowed in a recursive query's
26815/// recursive term" well-formedness check.
26816fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
26817 const AGG_NAMES: &[&str] = &[
26818 "count",
26819 "sum",
26820 "min",
26821 "max",
26822 "avg",
26823 "string_agg",
26824 "array_agg",
26825 "bool_and",
26826 "bool_or",
26827 "every",
26828 "any_value",
26829 "json_agg",
26830 "jsonb_agg",
26831 "json_object_agg",
26832 "jsonb_object_agg",
26833 "bit_and",
26834 "bit_or",
26835 "bit_xor",
26836 "var_pop",
26837 "var_samp",
26838 "variance",
26839 "stddev",
26840 "stddev_pop",
26841 "stddev_samp",
26842 "range_agg",
26843 "range_intersect_agg",
26844 "percentile_cont",
26845 "percentile_disc",
26846 "mode",
26847 "corr",
26848 "covar_pop",
26849 "covar_samp",
26850 ];
26851 match e {
26852 Expr::AggregateOrdered { .. } => true,
26853 Expr::FunctionCall { name, args } => {
26854 AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
26855 || args.iter().any(expr_has_toplevel_aggregate)
26856 }
26857 Expr::NamedArg { expr, .. }
26858 | Expr::Variadic(expr)
26859 | Expr::Unary { expr, .. }
26860 | Expr::Cast { expr, .. }
26861 | Expr::IsNull { expr, .. }
26862 | Expr::FieldAccess { base: expr, .. }
26863 | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
26864 Expr::Binary { lhs, rhs, .. } => {
26865 expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
26866 }
26867 Expr::Like { expr, pattern, .. } => {
26868 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
26869 }
26870 Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
26871 Expr::InList { expr, list, .. } => {
26872 expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
26873 }
26874 Expr::ArraySubscript { target, index } => {
26875 expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
26876 }
26877 Expr::ArraySlice { target, lo, hi } => {
26878 expr_has_toplevel_aggregate(target)
26879 || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
26880 || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
26881 }
26882 Expr::AnyAll { expr, array, .. } => {
26883 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
26884 }
26885 Expr::Case {
26886 operand,
26887 branches,
26888 else_branch,
26889 } => {
26890 operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
26891 || branches
26892 .iter()
26893 .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
26894 || else_branch
26895 .as_deref()
26896 .is_some_and(expr_has_toplevel_aggregate)
26897 }
26898 // The outer-level operands of a sublink can aggregate; the sublink's
26899 // own body cannot leak its aggregates up here.
26900 Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
26901 Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
26902 row.iter().any(expr_has_toplevel_aggregate)
26903 }
26904 _ => false,
26905 }
26906}
26907
26908/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
26909/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
26910/// named table anywhere in its subtree. A plain FROM derived table is NOT a
26911/// sublink and is legal in a recursive term, so it is not walked here.
26912fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
26913 let mut exprs: Vec<&Expr> = Vec::new();
26914 for it in &s.items {
26915 if let crate::ast::SelectItem::Expr { expr, .. } = it {
26916 exprs.push(expr);
26917 }
26918 }
26919 if let Some(w) = &s.where_ {
26920 exprs.push(w);
26921 }
26922 if let Some(h) = &s.having {
26923 exprs.push(h);
26924 }
26925 if let Some(g) = &s.group_by {
26926 exprs.extend(g.iter());
26927 }
26928 if let Some(from) = &s.from {
26929 for j in &from.joins {
26930 if let Some(on) = &j.on {
26931 exprs.push(on);
26932 }
26933 }
26934 }
26935 exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
26936}
26937
26938/// Does this expression contain a sublink whose subquery mentions `name`?
26939fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
26940 match e {
26941 Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
26942 Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
26943 Expr::InSubquery { expr, subquery, .. } => {
26944 expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
26945 }
26946 Expr::RowInSubquery { row, subquery, .. } => {
26947 row.iter().any(|x| expr_sublink_mentions(x, name))
26948 || select_mentions_table(subquery, name)
26949 }
26950 Expr::RowCmpSubquery { row, subquery, .. } => {
26951 row.iter().any(|x| expr_sublink_mentions(x, name))
26952 || select_mentions_table(subquery, name)
26953 }
26954 Expr::NamedArg { expr, .. }
26955 | Expr::Variadic(expr)
26956 | Expr::Unary { expr, .. }
26957 | Expr::Cast { expr, .. }
26958 | Expr::IsNull { expr, .. }
26959 | Expr::FieldAccess { base: expr, .. }
26960 | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
26961 Expr::Binary { lhs, rhs, .. } => {
26962 expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
26963 }
26964 Expr::Like { expr, pattern, .. } => {
26965 expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
26966 }
26967 Expr::FunctionCall { args, .. } | Expr::Array(args) => {
26968 args.iter().any(|x| expr_sublink_mentions(x, name))
26969 }
26970 Expr::InList { expr, list, .. } => {
26971 expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
26972 }
26973 Expr::ArraySubscript { target, index } => {
26974 expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
26975 }
26976 Expr::ArraySlice { target, lo, hi } => {
26977 expr_sublink_mentions(target, name)
26978 || lo
26979 .as_deref()
26980 .is_some_and(|x| expr_sublink_mentions(x, name))
26981 || hi
26982 .as_deref()
26983 .is_some_and(|x| expr_sublink_mentions(x, name))
26984 }
26985 Expr::AnyAll { expr, array, .. } => {
26986 expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
26987 }
26988 Expr::Case {
26989 operand,
26990 branches,
26991 else_branch,
26992 } => {
26993 operand
26994 .as_deref()
26995 .is_some_and(|x| expr_sublink_mentions(x, name))
26996 || branches
26997 .iter()
26998 .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
26999 || else_branch
27000 .as_deref()
27001 .is_some_and(|x| expr_sublink_mentions(x, name))
27002 }
27003 _ => false,
27004 }
27005}
27006
27007/// Does this SELECT (in full — FROM tables, derived tables, its own
27008/// sublinks, and union arms) mention the named table?
27009fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
27010 if let Some(from) = &s.from {
27011 if from.primary.name.eq_ignore_ascii_case(name) {
27012 return true;
27013 }
27014 if let Some(sub) = &from.primary.lateral_subquery
27015 && select_mentions_table(sub, name)
27016 {
27017 return true;
27018 }
27019 for j in &from.joins {
27020 if j.table.name.eq_ignore_ascii_case(name) {
27021 return true;
27022 }
27023 if let Some(sub) = &j.table.lateral_subquery
27024 && select_mentions_table(sub, name)
27025 {
27026 return true;
27027 }
27028 }
27029 }
27030 if select_has_self_ref_in_sublink(s, name) {
27031 return true;
27032 }
27033 s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
27034}
27035
27036/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
27037/// row count, the way PG evaluates one before applying it.
27038///
27039/// `None` = not a constant (a column, a subquery, a function call).
27040/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
27041/// message stands in for LIMIT / OFFSET, which the caller substitutes.
27042/// All wordings were read off live PG 18.4.
27043fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
27044 use crate::ast::{BinOp, Expr, Literal, UnOp};
27045 match e {
27046 Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
27047 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27048 Some(Ok(round_scaled_half_away(*unscaled, *scale)))
27049 }
27050 // PG coerces a string by its CONTENT, and fails on the value.
27051 Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
27052 |_| {
27053 Err(alloc::format!(
27054 "invalid input syntax for type bigint: \"{t}\""
27055 ))
27056 },
27057 |n| Ok(i128::from(n)),
27058 )),
27059 Expr::Literal(Literal::Bool(_)) => Some(Err(
27060 "argument of {L} must be type bigint, not type boolean".into(),
27061 )),
27062 Expr::Unary {
27063 op: UnOp::Neg,
27064 expr,
27065 } => match fold_limit_constant(expr)? {
27066 Ok(v) => Some(Ok(-v)),
27067 e @ Err(_) => Some(e),
27068 },
27069 Expr::Binary { lhs, op, rhs } => {
27070 let a = match fold_limit_constant(lhs)? {
27071 Ok(v) => v,
27072 e @ Err(_) => return Some(e),
27073 };
27074 let b = match fold_limit_constant(rhs)? {
27075 Ok(v) => v,
27076 e @ Err(_) => return Some(e),
27077 };
27078 let out = match op {
27079 BinOp::Add => a.checked_add(b),
27080 BinOp::Sub => a.checked_sub(b),
27081 BinOp::Mul => a.checked_mul(b),
27082 BinOp::Div if b != 0 => a.checked_div(b),
27083 BinOp::Div => return Some(Err("division by zero".into())),
27084 BinOp::Mod if b != 0 => a.checked_rem(b),
27085 BinOp::Mod => return Some(Err("division by zero".into())),
27086 _ => return None,
27087 };
27088 // PG evaluates the arithmetic in the operand's own type, so an
27089 // int-by-int product that leaves int range fails there — before
27090 // the row count is ever looked at.
27091 match out {
27092 Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
27093 Some(Err("integer out of range".into()))
27094 }
27095 Some(v) => Some(Ok(v)),
27096 None => Some(Err("integer out of range".into())),
27097 }
27098 }
27099 _ => None,
27100 }
27101}
27102
27103/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
27104/// cast, which is what makes `LIMIT 2.5` keep three rows.
27105fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
27106 if scale == 0 {
27107 return unscaled;
27108 }
27109 let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
27110 return 0;
27111 };
27112 let neg = unscaled < 0;
27113 let mag = unscaled.unsigned_abs() as i128;
27114 let rounded = (mag + div / 2) / div;
27115 if neg { -rounded } else { rounded }
27116}
27117
27118#[cfg(test)]
27119mod tests {
27120 use super::*;
27121 use alloc::string::ToString;
27122
27123 fn parse(s: &str) -> Statement {
27124 parse_statement(s).expect("parse ok")
27125 }
27126
27127 // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
27128 // `tables`, `partition`, etc. are unreserved keywords per PG's
27129 // `pg_get_keywords()` and MUST be usable as column / table /
27130 // alias names. Pre-T4 every drop-in user whose schema had one
27131 // of these as a column name (sentori events.release, mailrs
27132 // messages.index in some forks) blew the parser up at CREATE
27133 // TABLE time with "expected identifier, got Release". The
27134 // generalisation lives in `unreserved_keyword_text` + the
27135 // `expect_ident_like` and `parse_atom` arms that consult it.
27136 #[test]
27137 fn release_usable_as_column_name_in_create_table() {
27138 let stmt =
27139 parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
27140 if let Statement::CreateTable(t) = stmt {
27141 let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
27142 assert_eq!(names, alloc::vec!["id", "release", "payload"]);
27143 } else {
27144 panic!("expected CreateTable");
27145 }
27146 }
27147
27148 #[test]
27149 fn release_usable_as_column_ref_in_select_projection() {
27150 // The sentori `0003_partition_events.sql` INSERT-SELECT
27151 // walk references `release` in both column lists; the
27152 // projection-side use exercises `parse_atom`'s relaxed
27153 // identifier set.
27154 parse("SELECT id, release, payload FROM events WHERE id = 1");
27155 }
27156
27157 #[test]
27158 fn release_usable_as_column_ref_in_insert_column_list() {
27159 // INSERT INTO t (id, release, payload) VALUES (…)
27160 parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
27161 }
27162
27163 #[test]
27164 fn alter_column_drop_not_null_uses_keyword_drop_token() {
27165 // Sentori `0013_audit_tombstone.sql` issues
27166 // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
27167 // emits Token::Drop (not Ident("drop")); the parser must
27168 // accept both in the ALTER COLUMN sub-dispatch.
27169 parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
27170 }
27171
27172 #[test]
27173 fn create_index_accepts_parenthesised_expression_key() {
27174 // sentori `0040_events_bundle_idx.sql` shape — JSONB
27175 // expression index. Pre-T4 the parser bailed at the
27176 // inner `(` with "expected column ident or expression,
27177 // got LParen". The Token::LParen arm in CREATE INDEX
27178 // routes through the expression parser instead.
27179 parse(
27180 "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
27181 ON events ((payload->'bundle'->>'id'))",
27182 );
27183 }
27184
27185 // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
27186 // surface as parse errors, never stack overflows (embed hosts
27187 // abort on overflow).
27188 /// The nesting budget is a COUNT; what it has to fit inside is a
27189 /// number of BYTES, and only one of those two is stable across
27190 /// compiler versions. Round 847 measured 30,336 bytes per level
27191 /// after a toolchain move, which puts 64 levels at 1.94 MB and
27192 /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
27193 /// aborted instead of erroring, which is precisely the outcome it
27194 /// exists to rule out.
27195 ///
27196 /// So the budget is metered rather than assumed. The ceiling leaves
27197 /// the depth SPG advertises fitting in a default 2 MiB thread with
27198 /// room to spare, in the debug build, where frames are widest.
27199 #[test]
27200 fn nesting_frame_cost_stays_under_ceiling() {
27201 // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
27202 // thread keeps a margin for whatever called the parser.
27203 const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
27204
27205 frame_meter::reset();
27206 let depth = frame_meter::SAMPLE_HI + 8;
27207 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27208 parse(&sql);
27209
27210 let per_level = frame_meter::bytes_per_level();
27211 {
27212 extern crate std;
27213 std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
27214 }
27215 assert!(
27216 per_level <= CEILING,
27217 "{per_level} bytes per nesting level exceeds {CEILING}; \
27218 {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
27219 in parse_expr_inner / parse_unary rather than lowering the \
27220 depth or widening the stack.",
27221 per_level * MAX_NEST_DEPTH
27222 );
27223 }
27224
27225 #[test]
27226 fn nesting_budget_errors_cleanly() {
27227 let depth = MAX_NEST_DEPTH + 50;
27228 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27229 let err = parse_statement(&sql).expect_err("must reject");
27230 assert!(err.message.contains("nests deeper"), "{err:?}");
27231 // Within budget still parses.
27232 let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
27233 parse(&sql);
27234 }
27235
27236 #[test]
27237 fn binary_chain_budget_errors_cleanly() {
27238 let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
27239 let err = parse_statement(&sql).expect_err("must reject");
27240 assert!(err.message.contains("chained binary"), "{err:?}");
27241 // Within budget still parses (chain depth ≤ budget is safe
27242 // for recursive eval/drop on 2 MiB stacks).
27243 let sql = format!("SELECT 1{}", " + 1".repeat(200));
27244 parse(&sql);
27245 }
27246
27247 #[test]
27248 fn in_list_unaffected_by_chain_budget() {
27249 // Flat InList: 20k elements parse fine and stay flat.
27250 let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
27251 let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
27252 let Statement::Select(s) = parse(&sql) else {
27253 panic!("expected select")
27254 };
27255 let Some(Expr::InList { list, negated, .. }) = s.where_ else {
27256 panic!("expected flat InList, got {:?}", s.where_)
27257 };
27258 assert_eq!(list.len(), 20_000);
27259 assert!(!negated);
27260 }
27261
27262 fn lit_int(n: i64) -> Expr {
27263 Expr::Literal(Literal::Integer(n))
27264 }
27265
27266 fn col(name: &str) -> Expr {
27267 Expr::Column(ColumnName {
27268 qualifier: None,
27269 name: name.into(),
27270 })
27271 }
27272
27273 #[test]
27274 fn select_single_integer() {
27275 let s = parse("SELECT 1");
27276 let Statement::Select(s) = s else {
27277 panic!("expected SELECT")
27278 };
27279 assert_eq!(s.items.len(), 1);
27280 assert!(s.from.is_none());
27281 assert!(s.where_.is_none());
27282 }
27283
27284 #[test]
27285 fn select_multiple_literal_kinds() {
27286 let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
27287 let Statement::Select(s) = s else {
27288 panic!("expected SELECT")
27289 };
27290 assert_eq!(s.items.len(), 5);
27291 }
27292
27293 #[test]
27294 fn select_wildcard_from_table() {
27295 let s = parse("SELECT * FROM users");
27296 let Statement::Select(s) = s else {
27297 panic!("expected SELECT")
27298 };
27299 assert!(matches!(s.items[..], [SelectItem::Wildcard]));
27300 assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
27301 }
27302
27303 #[test]
27304 fn select_with_table_alias() {
27305 let s = parse("SELECT * FROM users AS u");
27306 let Statement::Select(s) = s else {
27307 panic!("expected SELECT")
27308 };
27309 let t = &s.from.as_ref().unwrap().primary;
27310 assert_eq!(t.name, "users");
27311 assert_eq!(t.alias.as_deref(), Some("u"));
27312 }
27313
27314 #[test]
27315 fn select_with_where_eq() {
27316 let s = parse("SELECT a FROM t WHERE a = 1");
27317 let Statement::Select(s) = s else {
27318 panic!("expected SELECT")
27319 };
27320 let w = s.where_.unwrap();
27321 assert_eq!(
27322 w,
27323 Expr::Binary {
27324 lhs: Box::new(col("a")),
27325 op: BinOp::Eq,
27326 rhs: Box::new(lit_int(1)),
27327 }
27328 );
27329 }
27330
27331 #[test]
27332 fn arithmetic_precedence() {
27333 let s = parse("SELECT 1 + 2 * 3");
27334 let Statement::Select(s) = s else {
27335 panic!("expected SELECT")
27336 };
27337 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27338 panic!("wildcard?")
27339 };
27340 assert_eq!(
27341 expr,
27342 &Expr::Binary {
27343 lhs: Box::new(lit_int(1)),
27344 op: BinOp::Add,
27345 rhs: Box::new(Expr::Binary {
27346 lhs: Box::new(lit_int(2)),
27347 op: BinOp::Mul,
27348 rhs: Box::new(lit_int(3)),
27349 }),
27350 }
27351 );
27352 }
27353
27354 #[test]
27355 fn parentheses_override_precedence() {
27356 let s = parse("SELECT (1 + 2) * 3");
27357 let Statement::Select(s) = s else {
27358 panic!("expected SELECT")
27359 };
27360 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27361 panic!()
27362 };
27363 assert_eq!(
27364 expr,
27365 &Expr::Binary {
27366 lhs: Box::new(Expr::Binary {
27367 lhs: Box::new(lit_int(1)),
27368 op: BinOp::Add,
27369 rhs: Box::new(lit_int(2)),
27370 }),
27371 op: BinOp::Mul,
27372 rhs: Box::new(lit_int(3)),
27373 }
27374 );
27375 }
27376
27377 #[test]
27378 fn not_binds_below_comparison() {
27379 // `NOT a = 1` should parse as `NOT (a = 1)`.
27380 let s = parse("SELECT NOT a = 1 FROM t");
27381 let Statement::Select(s) = s else {
27382 panic!("expected SELECT")
27383 };
27384 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27385 panic!()
27386 };
27387 assert_eq!(
27388 expr,
27389 &Expr::Unary {
27390 op: UnOp::Not,
27391 expr: Box::new(Expr::Binary {
27392 lhs: Box::new(col("a")),
27393 op: BinOp::Eq,
27394 rhs: Box::new(lit_int(1)),
27395 }),
27396 }
27397 );
27398 }
27399
27400 #[test]
27401 fn unary_minus_binds_above_multiplication() {
27402 // `-a * 2` should be `(-a) * 2`.
27403 let s = parse("SELECT -a * 2 FROM t");
27404 let Statement::Select(s) = s else {
27405 panic!("expected SELECT")
27406 };
27407 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27408 panic!()
27409 };
27410 assert_eq!(
27411 expr,
27412 &Expr::Binary {
27413 lhs: Box::new(Expr::Unary {
27414 op: UnOp::Neg,
27415 expr: Box::new(col("a")),
27416 }),
27417 op: BinOp::Mul,
27418 rhs: Box::new(lit_int(2)),
27419 }
27420 );
27421 }
27422
27423 #[test]
27424 fn qualified_column() {
27425 let s = parse("SELECT t.col FROM t");
27426 let Statement::Select(s) = s else {
27427 panic!("expected SELECT")
27428 };
27429 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27430 panic!()
27431 };
27432 assert_eq!(
27433 expr,
27434 &Expr::Column(ColumnName {
27435 qualifier: Some("t".into()),
27436 name: "col".into()
27437 })
27438 );
27439 }
27440
27441 #[test]
27442 fn select_item_alias_with_as() {
27443 let s = parse("SELECT a AS y FROM t");
27444 let Statement::Select(s) = s else {
27445 panic!("expected SELECT")
27446 };
27447 let SelectItem::Expr { alias, .. } = &s.items[0] else {
27448 panic!()
27449 };
27450 assert_eq!(alias.as_deref(), Some("y"));
27451 }
27452
27453 #[test]
27454 fn trailing_semicolon_accepted() {
27455 let s = parse("SELECT 1;");
27456 let Statement::Select(s) = s else {
27457 panic!("expected SELECT")
27458 };
27459 assert_eq!(s.items.len(), 1);
27460 }
27461
27462 #[test]
27463 fn boolean_chain_with_and_or_not() {
27464 // (NOT a) OR (b AND (NOT c))
27465 let s = parse("SELECT NOT a OR b AND NOT c FROM t");
27466 let Statement::Select(s) = s else {
27467 panic!("expected SELECT")
27468 };
27469 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27470 panic!()
27471 };
27472 let expected = Expr::Binary {
27473 lhs: Box::new(Expr::Unary {
27474 op: UnOp::Not,
27475 expr: Box::new(col("a")),
27476 }),
27477 op: BinOp::Or,
27478 rhs: Box::new(Expr::Binary {
27479 lhs: Box::new(col("b")),
27480 op: BinOp::And,
27481 rhs: Box::new(Expr::Unary {
27482 op: UnOp::Not,
27483 expr: Box::new(col("c")),
27484 }),
27485 }),
27486 };
27487 assert_eq!(expr, &expected);
27488 }
27489
27490 #[test]
27491 fn empty_input_errors() {
27492 // v7.14.0 — pg_dump preambles emit several comment-only
27493 // / blank-line statements that collapse to Statement::
27494 // Empty rather than a parse error. The old "SELECT in
27495 // message" assertion is stale; verify the new contract:
27496 // empty / whitespace / comment-only input parses to
27497 // Statement::Empty.
27498 assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
27499 assert!(matches!(
27500 parse_statement(" \n\t ").unwrap(),
27501 Statement::Empty
27502 ));
27503 // Sanity: malformed-but-non-empty still errors.
27504 assert!(parse_statement("SELECT FROM WHERE").is_err());
27505 }
27506
27507 #[test]
27508 fn unmatched_paren_errors() {
27509 assert!(parse_statement("SELECT (1 + 2").is_err());
27510 }
27511
27512 #[test]
27513 fn display_round_trip_simple_select() {
27514 let original = parse("SELECT a + 1 FROM t WHERE a > 0");
27515 let text = original.to_string();
27516 let again = parse_statement(&text).expect("re-parse");
27517 assert_eq!(original, again);
27518 }
27519
27520 // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
27521
27522 #[test]
27523 fn create_table_single_column() {
27524 let s = parse("CREATE TABLE foo (a INT)");
27525 let Statement::CreateTable(c) = s else {
27526 panic!("expected CreateTable")
27527 };
27528 assert_eq!(c.name, "foo");
27529 assert_eq!(c.columns.len(), 1);
27530 assert_eq!(c.columns[0].name, "a");
27531 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27532 assert!(c.columns[0].nullable);
27533 }
27534
27535 #[test]
27536 fn create_table_multi_column_with_not_null_mix() {
27537 let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
27538 let Statement::CreateTable(c) = s else {
27539 panic!()
27540 };
27541 assert_eq!(c.columns.len(), 4);
27542 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27543 assert!(!c.columns[0].nullable);
27544 assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
27545 assert!(c.columns[1].nullable);
27546 assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
27547 assert!(!c.columns[2].nullable);
27548 assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
27549 }
27550
27551 #[test]
27552 fn create_table_bigint_supported() {
27553 let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
27554 let Statement::CreateTable(c) = s else {
27555 panic!()
27556 };
27557 assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
27558 }
27559
27560 #[test]
27561 fn create_table_vector_default_is_f32() {
27562 let s = parse("CREATE TABLE t (v VECTOR(128))");
27563 let Statement::CreateTable(c) = s else {
27564 panic!()
27565 };
27566 assert_eq!(
27567 c.columns[0].ty,
27568 ColumnTypeName::Vector {
27569 dim: 128,
27570 encoding: VecEncoding::F32,
27571 },
27572 );
27573 }
27574
27575 #[test]
27576 fn create_table_vector_using_sq8() {
27577 // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
27578 // Case-insensitive on both `USING` and the encoding name.
27579 for sql in [
27580 "CREATE TABLE t (v VECTOR(128) USING SQ8)",
27581 "CREATE TABLE t (v VECTOR(128) using sq8)",
27582 ] {
27583 let s = parse(sql);
27584 let Statement::CreateTable(c) = s else {
27585 panic!()
27586 };
27587 assert_eq!(
27588 c.columns[0].ty,
27589 ColumnTypeName::Vector {
27590 dim: 128,
27591 encoding: VecEncoding::Sq8,
27592 },
27593 "{sql}",
27594 );
27595 }
27596 }
27597
27598 #[test]
27599 fn create_table_vector_using_unknown_errors() {
27600 // v7.16.1 — the inline `USING <encoding>` shape on
27601 // CREATE TABLE column defs was withdrawn before
27602 // v7.14.0 in favour of `CREATE INDEX … USING hnsw
27603 // (col vector_<metric>_ops)`; the parser now rejects
27604 // USING at column-list position with a clearer
27605 // "expected ',' or ')'" message. Test asserts the
27606 // current rejection, not the old "unknown vector
27607 // encoding" string.
27608 let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
27609 assert!(
27610 err.message.contains("USING")
27611 || err.message.contains("using")
27612 || err.message.contains("')'")
27613 || err.message.contains("','"),
27614 "expected USING/column-list rejection, got: {}",
27615 err.message
27616 );
27617 }
27618
27619 #[test]
27620 fn vector_using_sq8_display_roundtrips() {
27621 // The Display impl must produce text that re-parses to the
27622 // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
27623 let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
27624 let Statement::CreateTable(c) = s else {
27625 panic!()
27626 };
27627 assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
27628 }
27629
27630 #[test]
27631 fn parser_recognises_placeholders() {
27632 use crate::ast::{Expr, SelectItem, Statement};
27633 // $N in expression position parses as Expr::Placeholder(N).
27634 let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
27635 let Statement::Select(sel) = s else { panic!() };
27636 assert!(matches!(
27637 sel.items[0],
27638 SelectItem::Expr {
27639 expr: Expr::Placeholder(1),
27640 alias: None
27641 }
27642 ));
27643 // $2 + 1
27644 let SelectItem::Expr {
27645 expr: Expr::Binary { lhs, rhs, .. },
27646 ..
27647 } = &sel.items[1]
27648 else {
27649 panic!()
27650 };
27651 assert!(matches!(**lhs, Expr::Placeholder(2)));
27652 assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
27653 // WHERE x = $3
27654 let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
27655 panic!()
27656 };
27657 assert!(matches!(**rhs, Expr::Placeholder(3)));
27658 }
27659
27660 #[test]
27661 fn parser_rejects_dollar_zero() {
27662 // $0 is not valid in PG; the lexer rejects it.
27663 assert!(parse_statement("SELECT $0").is_err());
27664 }
27665
27666 #[test]
27667 fn placeholder_display_roundtrips() {
27668 // The Display impl must produce text that re-lexes to the
27669 // same Placeholder token.
27670 let s = parse("SELECT $42 FROM t");
27671 let printed = s.to_string();
27672 assert!(printed.contains("$42"));
27673 let again = parse(&printed);
27674 assert_eq!(s, again);
27675 }
27676
27677 #[test]
27678 fn alter_index_rebuild_bare() {
27679 use crate::ast::{AlterIndexTarget, Statement};
27680 let s = parse("ALTER INDEX my_idx REBUILD");
27681 let Statement::AlterIndex(a) = s else {
27682 panic!("expected AlterIndex, got {s:?}")
27683 };
27684 assert_eq!(a.name, "my_idx");
27685 assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
27686 }
27687
27688 #[test]
27689 fn alter_index_rebuild_with_encoding() {
27690 use crate::ast::{AlterIndexTarget, Statement};
27691 for (sql, want) in [
27692 (
27693 "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
27694 VecEncoding::F32,
27695 ),
27696 (
27697 "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
27698 VecEncoding::Sq8,
27699 ),
27700 (
27701 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27702 VecEncoding::F16,
27703 ),
27704 ] {
27705 let s = parse(sql);
27706 let Statement::AlterIndex(a) = s else {
27707 panic!("{sql}: expected AlterIndex")
27708 };
27709 assert_eq!(a.name, "my_idx");
27710 assert_eq!(
27711 a.target,
27712 AlterIndexTarget::Rebuild {
27713 encoding: Some(want)
27714 },
27715 "{sql}"
27716 );
27717 }
27718 }
27719
27720 #[test]
27721 fn alter_index_rebuild_unknown_encoding_errors() {
27722 let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
27723 assert!(
27724 err.message.contains("unknown vector encoding"),
27725 "got: {}",
27726 err.message
27727 );
27728 }
27729
27730 #[test]
27731 fn alter_index_rebuild_display_roundtrips() {
27732 for (input, want) in [
27733 ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
27734 (
27735 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27736 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27737 ),
27738 (
27739 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27740 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27741 ),
27742 ] {
27743 let s = parse(input);
27744 assert_eq!(s.to_string(), want);
27745 }
27746 }
27747
27748 #[test]
27749 fn create_table_unknown_type_defers_to_engine() {
27750 // v4.9 picked XML as a parse-time "unsupported column
27751 // type" probe. v7.17.0 Phase 1.4 changed the contract:
27752 // an unknown type ident parses as Text + `user_type_ref`
27753 // so CREATE TABLE can resolve user-defined enum / domain
27754 // types — rejection of truly-unknown types moved to the
27755 // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
27756 // to a first-class built-in, so this probe switched to a
27757 // synthetic name nothing in the lexer will ever recognise.
27758 let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
27759 let Statement::CreateTable(t) = stmt else {
27760 panic!("expected CreateTable");
27761 };
27762 assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
27763 }
27764
27765 #[test]
27766 fn create_table_missing_table_keyword_errors() {
27767 assert!(parse_statement("CREATE x (a INT)").is_err());
27768 }
27769
27770 // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
27771 // `PARTITION OF parent <bounds>` child parse + Display round-trip.
27772
27773 #[test]
27774 fn parse_create_table_partition_by_range() {
27775 use crate::ast::{PartitionBySpec, PartitionKindAst};
27776 let stmt = parse_statement(
27777 "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
27778 payload JSONB) PARTITION BY RANGE (ts)",
27779 )
27780 .unwrap();
27781 let Statement::CreateTable(t) = stmt else {
27782 panic!("expected CreateTable");
27783 };
27784 assert!(t.partition_of.is_none(), "parent has no partition_of");
27785 assert_eq!(t.columns.len(), 3);
27786 let by = t.partition_by.as_ref().expect("expected PARTITION BY");
27787 assert_eq!(
27788 by,
27789 &PartitionBySpec {
27790 kind: PartitionKindAst::Range,
27791 key_columns: alloc::vec!["ts".to_string()],
27792 }
27793 );
27794 // Display round-trip preserves the suffix. `quote_ident`
27795 // only adds double quotes when the ident needs escaping, so
27796 // a plain `ts` survives bare here.
27797 assert!(
27798 t.to_string().contains("PARTITION BY RANGE (ts)"),
27799 "Display lost PARTITION BY suffix: {t}"
27800 );
27801 }
27802
27803 #[test]
27804 fn parse_create_table_partition_of_range() {
27805 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
27806 let stmt = parse_statement(
27807 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
27808 FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
27809 )
27810 .unwrap();
27811 let Statement::CreateTable(t) = stmt else {
27812 panic!("expected CreateTable");
27813 };
27814 assert!(t.columns.is_empty(), "child inherits columns from parent");
27815 assert!(t.partition_by.is_none());
27816 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27817 assert_eq!(of.parent_name, "events_partitioned");
27818 let PartitionOfSpec { bounds, .. } = of.clone();
27819 match bounds {
27820 PartitionOfBoundsAst::Range { lower, upper } => {
27821 assert!(lower.to_string().contains("2026-06-01"));
27822 assert!(upper.to_string().contains("2026-07-01"));
27823 }
27824 other => panic!("expected Range, got {other:?}"),
27825 }
27826 // Display round-trip emits the FOR VALUES tail. `quote_ident`
27827 // skips quotes when not required, so the parent name appears
27828 // bare here.
27829 let s = t.to_string();
27830 assert!(
27831 s.contains("PARTITION OF events_partitioned"),
27832 "Display lost PARTITION OF: {s}"
27833 );
27834 assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
27835 assert!(s.contains(") TO ("), "Display lost TO: {s}");
27836 }
27837
27838 #[test]
27839 fn parse_create_table_partition_of_default() {
27840 use crate::ast::PartitionOfBoundsAst;
27841 let stmt =
27842 parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
27843 .unwrap();
27844 let Statement::CreateTable(t) = stmt else {
27845 panic!("expected CreateTable");
27846 };
27847 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27848 assert_eq!(of.parent_name, "events_partitioned");
27849 assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
27850 assert!(
27851 t.to_string()
27852 .contains("PARTITION OF events_partitioned DEFAULT"),
27853 "Display lost DEFAULT: {t}"
27854 );
27855 }
27856
27857 #[test]
27858 fn parse_create_table_partition_by_list() {
27859 // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
27860 // child with `FOR VALUES IN (lit, lit, …)`.
27861 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27862 let parent =
27863 parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
27864 .unwrap();
27865 let Statement::CreateTable(t) = parent else {
27866 panic!("expected CreateTable");
27867 };
27868 let Some(PartitionBySpec {
27869 kind,
27870 ref key_columns,
27871 }) = t.partition_by
27872 else {
27873 panic!("expected PARTITION BY");
27874 };
27875 assert_eq!(kind, PartitionKindAst::List);
27876 assert_eq!(*key_columns, vec!["region".to_string()]);
27877 assert!(t.to_string().contains("PARTITION BY LIST (region)"));
27878
27879 let child = parse_statement(
27880 "CREATE TABLE events_apac PARTITION OF events_listed \
27881 FOR VALUES IN ('jp', 'kr', 'tw')",
27882 )
27883 .unwrap();
27884 let Statement::CreateTable(c) = child else {
27885 panic!("expected CreateTable");
27886 };
27887 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27888 let PartitionOfBoundsAst::List { values } = &of.bounds else {
27889 panic!("expected List bounds, got {:?}", of.bounds);
27890 };
27891 assert_eq!(values.len(), 3);
27892 let disp = c.to_string();
27893 assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
27894 }
27895
27896 #[test]
27897 fn parse_create_table_partition_by_hash() {
27898 // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
27899 // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
27900 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27901 let parent =
27902 parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
27903 let Statement::CreateTable(t) = parent else {
27904 panic!("expected CreateTable");
27905 };
27906 let Some(PartitionBySpec {
27907 kind,
27908 ref key_columns,
27909 }) = t.partition_by
27910 else {
27911 panic!("expected PARTITION BY");
27912 };
27913 assert_eq!(kind, PartitionKindAst::Hash);
27914 assert_eq!(*key_columns, vec!["id".to_string()]);
27915 assert!(t.to_string().contains("PARTITION BY HASH (id)"));
27916
27917 let child = parse_statement(
27918 "CREATE TABLE orders_h_0 PARTITION OF orders_h \
27919 FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
27920 )
27921 .unwrap();
27922 let Statement::CreateTable(c) = child else {
27923 panic!("expected CreateTable");
27924 };
27925 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27926 let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
27927 panic!("expected Hash bounds");
27928 };
27929 assert_eq!(modulus, 4);
27930 assert_eq!(remainder, 0);
27931 let disp = c.to_string();
27932 assert!(
27933 disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
27934 "Display lost HASH bounds: {disp}"
27935 );
27936
27937 // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
27938 let bad = parse_statement(
27939 "CREATE TABLE orders_h_bad PARTITION OF orders_h \
27940 FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
27941 );
27942 let msg = format!("{}", bad.unwrap_err());
27943 assert!(
27944 msg.contains("REMAINDER") && msg.contains("MODULUS"),
27945 "expected REMAINDER/MODULUS validation error: {msg}"
27946 );
27947 }
27948
27949 #[test]
27950 fn parse_create_table_partition_of_rejects_columns() {
27951 // v7.37.6-B contract: PARTITION OF children inherit columns
27952 // from the parent; an explicit list MUST surface as a parse
27953 // error rather than getting silently ignored.
27954 let err = parse_statement(
27955 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
27956 FOR VALUES FROM ('a') TO ('b')",
27957 );
27958 assert!(err.is_err(), "expected parse error for explicit columns");
27959 let msg = format!("{}", err.unwrap_err());
27960 assert!(
27961 msg.contains("PARTITION OF") && msg.contains("column"),
27962 "error should mention PARTITION OF + columns: {msg}"
27963 );
27964 }
27965
27966 #[test]
27967 fn insert_single_value() {
27968 let s = parse("INSERT INTO foo VALUES (42)");
27969 let Statement::Insert(i) = s else {
27970 panic!("expected Insert")
27971 };
27972 assert_eq!(i.table, "foo");
27973 assert_eq!(i.rows.len(), 1);
27974 assert_eq!(i.rows[0].len(), 1);
27975 assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
27976 }
27977
27978 #[test]
27979 fn insert_multi_value_with_mixed_literals() {
27980 let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
27981 let Statement::Insert(i) = s else { panic!() };
27982 assert_eq!(i.rows.len(), 1);
27983 assert_eq!(i.rows[0].len(), 5);
27984 }
27985
27986 #[test]
27987 fn insert_missing_into_errors() {
27988 assert!(parse_statement("INSERT foo VALUES (1)").is_err());
27989 }
27990
27991 #[test]
27992 fn create_table_round_trip() {
27993 let original =
27994 parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
27995 let text = original.to_string();
27996 let again = parse_statement(&text).expect("re-parse");
27997 assert_eq!(original, again);
27998 }
27999
28000 #[test]
28001 fn insert_round_trip_with_negation_and_string() {
28002 let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
28003 let text = original.to_string();
28004 let again = parse_statement(&text).expect("re-parse");
28005 assert_eq!(original, again);
28006 }
28007
28008 #[test]
28009 fn unknown_keyword_at_statement_start_errors() {
28010 // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
28011 // the top-level dispatch still has no branch to take.
28012 let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
28013 assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
28014 }
28015
28016 // --- v0.8 CREATE INDEX --------------------------------------------------
28017
28018 #[test]
28019 fn create_index_basic() {
28020 let s = parse("CREATE INDEX idx_id ON users (id)");
28021 let Statement::CreateIndex(c) = s else {
28022 panic!("expected CreateIndex")
28023 };
28024 assert_eq!(c.name, "idx_id");
28025 assert_eq!(c.table, "users");
28026 assert_eq!(c.column, "id");
28027 }
28028
28029 #[test]
28030 fn create_index_missing_on_errors() {
28031 assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
28032 }
28033
28034 #[test]
28035 fn create_index_missing_paren_errors() {
28036 assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
28037 }
28038
28039 #[test]
28040 fn create_index_round_trip() {
28041 let original = parse("CREATE INDEX by_name ON users (name)");
28042 let again = parse_statement(&original.to_string()).unwrap();
28043 assert_eq!(original, again);
28044 }
28045
28046 // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
28047
28048 #[test]
28049 fn create_unique_index_basic() {
28050 let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
28051 let Statement::CreateIndex(c) = s else {
28052 panic!("expected CreateIndex");
28053 };
28054 assert!(c.is_unique);
28055 assert_eq!(c.column, "a");
28056 assert!(c.partial_predicate.is_none());
28057 }
28058
28059 #[test]
28060 fn create_unique_index_partial() {
28061 // mailrs's email_templates "one default per user" shape.
28062 let s = parse(
28063 "CREATE UNIQUE INDEX idx_email_templates_user_default \
28064 ON email_templates (user_address) WHERE is_default = true",
28065 );
28066 let Statement::CreateIndex(c) = s else {
28067 panic!("expected CreateIndex");
28068 };
28069 assert!(c.is_unique);
28070 assert_eq!(c.table, "email_templates");
28071 assert_eq!(c.column, "user_address");
28072 assert!(c.partial_predicate.is_some());
28073 }
28074
28075 #[test]
28076 fn create_unique_index_composite_with_predicate() {
28077 // mailrs's calendar_events instance: composite columns.
28078 let s = parse(
28079 "CREATE UNIQUE INDEX uq_calendar_events_instance \
28080 ON calendar_events (calendar_id, uid, recurrence_id) \
28081 WHERE recurrence_id IS NOT NULL",
28082 );
28083 let Statement::CreateIndex(c) = s else {
28084 panic!("expected CreateIndex");
28085 };
28086 assert!(c.is_unique);
28087 assert_eq!(c.column, "calendar_id");
28088 assert_eq!(
28089 c.extra_columns,
28090 vec!["uid".to_string(), "recurrence_id".to_string()]
28091 );
28092 assert!(c.partial_predicate.is_some());
28093 }
28094
28095 #[test]
28096 fn create_unique_index_using_btree_ok() {
28097 let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
28098 assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
28099 }
28100
28101 #[test]
28102 fn create_unique_index_using_hnsw_rejected() {
28103 let err =
28104 parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
28105 assert!(err.message.contains("UNIQUE"), "{}", err.message);
28106 }
28107
28108 #[test]
28109 fn create_unique_index_round_trip() {
28110 let original = parse(
28111 "CREATE UNIQUE INDEX uq_calendar_events_master \
28112 ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
28113 );
28114 let again = parse_statement(&original.to_string()).unwrap();
28115 assert_eq!(original, again);
28116 }
28117
28118 #[test]
28119 fn create_unique_without_index_errors() {
28120 let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
28121 // v7.39 (round 340, V56) — PG 18.4, verbatim.
28122 assert_eq!(err.message, "syntax error at or near \"TABLE\"");
28123 }
28124
28125 // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
28126
28127 #[test]
28128 fn create_table_bytea_column() {
28129 let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
28130 let Statement::CreateTable(c) = s else {
28131 panic!("expected CreateTable");
28132 };
28133 assert_eq!(c.columns.len(), 2);
28134 assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
28135 assert!(!c.columns[1].nullable);
28136 }
28137
28138 #[test]
28139 fn create_table_bytes_alias_column() {
28140 let s = parse("CREATE TABLE t (blob BYTES)");
28141 let Statement::CreateTable(c) = s else {
28142 panic!("expected CreateTable");
28143 };
28144 assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
28145 }
28146
28147 #[test]
28148 fn bytea_round_trip_display() {
28149 let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
28150 let again = parse_statement(&original.to_string()).unwrap();
28151 assert_eq!(original, again);
28152 }
28153
28154 // --- v0.9 transactions -------------------------------------------------
28155
28156 #[test]
28157 fn begin_commit_rollback_parse_as_unit_variants() {
28158 assert_eq!(parse("BEGIN"), Statement::Begin(None));
28159 assert_eq!(parse("COMMIT"), Statement::Commit);
28160 // r1066 — PG synonyms pgbench's tpcb script relies on.
28161 assert_eq!(parse("END"), Statement::Commit);
28162 assert_eq!(parse("END TRANSACTION"), Statement::Commit);
28163 assert_eq!(parse("COMMIT WORK"), Statement::Commit);
28164 assert_eq!(parse("ROLLBACK"), Statement::Rollback);
28165 // Trailing semicolons accepted too.
28166 assert_eq!(parse("BEGIN;"), Statement::Begin(None));
28167 // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
28168 // statement (with or without the WORK/TRANSACTION noise word).
28169 assert_eq!(
28170 parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
28171 Statement::Begin(Some(IsolationLevel::RepeatableRead))
28172 );
28173 assert_eq!(
28174 parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
28175 Statement::Begin(Some(IsolationLevel::Serializable))
28176 );
28177 // A non-isolation mode keeps the session default (None).
28178 assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
28179 }
28180
28181 // --- v1.2: pgvector distance ops + ::vector cast --------------------
28182
28183 #[test]
28184 fn inner_product_binop_parses() {
28185 let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
28186 let Statement::Select(s) = s else { panic!() };
28187 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28188 panic!()
28189 };
28190 assert!(matches!(
28191 expr,
28192 Expr::Binary {
28193 op: BinOp::InnerProduct,
28194 ..
28195 }
28196 ));
28197 }
28198
28199 #[test]
28200 fn cosine_distance_binop_parses() {
28201 let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
28202 let Statement::Select(s) = s else { panic!() };
28203 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28204 panic!()
28205 };
28206 assert!(matches!(
28207 expr,
28208 Expr::Binary {
28209 op: BinOp::CosineDistance,
28210 ..
28211 }
28212 ));
28213 }
28214
28215 #[test]
28216 fn vector_cast_postfix_wraps_string_literal() {
28217 let s = parse("SELECT '[1,2,3]'::vector FROM t");
28218 let Statement::Select(s) = s else { panic!() };
28219 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28220 panic!()
28221 };
28222 assert!(matches!(
28223 expr,
28224 Expr::Cast {
28225 target: CastTarget::Vector,
28226 ..
28227 }
28228 ));
28229 }
28230
28231 #[test]
28232 fn unsupported_cast_target_errors() {
28233 // v7.37.5 ship triage promoted the parser to accept every
28234 // ident as a `CastTarget::Named(canonical)`; the engine
28235 // surfaces the "unsupported cast target" error at eval
28236 // time when `type_name_to_data_type` can't resolve it.
28237 // Parser-side error now requires a NON-ident after `::`
28238 // (e.g. a punctuation token).
28239 let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
28240 assert_eq!(err.message, "syntax error at or near \",\"");
28241 }
28242
28243 #[test]
28244 fn tx_statements_round_trip() {
28245 for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
28246 let original = parse(q);
28247 let again = parse_statement(&original.to_string()).unwrap();
28248 assert_eq!(original, again);
28249 }
28250 }
28251
28252 #[test]
28253 fn interval_text_parsing_units() {
28254 // v7.37.5 β — three-field shape `(months, days, micros)` so
28255 // `'1 day'` and `'24 hours'` no longer collide (PG parity).
28256 // Single unit.
28257 assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
28258 assert_eq!(
28259 parse_interval_text("24 hours"),
28260 Some((0, 0, 86_400_000_000))
28261 );
28262 assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
28263 assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
28264 assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
28265 assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
28266 // Compound spans accumulate per-dimension.
28267 assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
28268 assert_eq!(
28269 parse_interval_text("1 day 2 hours"),
28270 Some((0, 1, 7_200_000_000))
28271 );
28272 // Negative numbers carry through per-dimension.
28273 assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
28274 // Bad shapes return None.
28275 assert_eq!(parse_interval_text(""), None);
28276 assert_eq!(parse_interval_text("garbage"), None);
28277 assert_eq!(parse_interval_text("1 fortnight"), None);
28278 // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
28279 // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
28280 assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
28281 assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
28282 assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
28283 }
28284
28285 #[test]
28286 fn interval_literal_roundtrips_via_display() {
28287 let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
28288 let s = parsed.to_string();
28289 // Display preserves the original text verbatim.
28290 assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
28291 // And re-parsing yields a structurally equal statement.
28292 let again = parse_statement(&s).unwrap();
28293 assert_eq!(parsed, again);
28294 }
28295
28296 // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
28297
28298 #[test]
28299 fn parser_recognises_create_publication_bare() {
28300 let s = parse("CREATE PUBLICATION pub_a");
28301 let Statement::CreatePublication(p) = s else {
28302 panic!("expected CreatePublication, got {s:?}")
28303 };
28304 assert_eq!(p.name, "pub_a");
28305 assert_eq!(p.scope, PublicationScope::AllTables);
28306 }
28307
28308 #[test]
28309 fn parser_recognises_create_publication_for_all_tables() {
28310 let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
28311 let Statement::CreatePublication(p) = s else {
28312 panic!("expected CreatePublication, got {s:?}")
28313 };
28314 assert_eq!(p.name, "pub_a");
28315 assert_eq!(p.scope, PublicationScope::AllTables);
28316 }
28317
28318 #[test]
28319 fn parser_recognises_drop_publication() {
28320 let s = parse("DROP PUBLICATION pub_a");
28321 let Statement::DropPublication { name, .. } = s else {
28322 panic!("expected DropPublication, got {s:?}")
28323 };
28324 assert_eq!(name, "pub_a");
28325 }
28326
28327 #[test]
28328 fn parser_recognises_for_table_list() {
28329 let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
28330 let Statement::CreatePublication(p) = s else {
28331 panic!("expected CreatePublication, got {s:?}")
28332 };
28333 assert_eq!(p.name, "pub_a");
28334 let PublicationScope::ForTables(ts) = p.scope else {
28335 panic!("expected ForTables scope")
28336 };
28337 assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
28338 }
28339
28340 #[test]
28341 fn parser_rejects_bare_for_tables_and_takes_in_schema() {
28342 // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
28343 // is rejected (`invalid publication object list`; the old
28344 // test pinned an unverifiable "PG 19 accepts both" claim);
28345 // TABLES pairs with IN SCHEMA.
28346 let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
28347 .expect_err("bare FOR TABLES must reject");
28348 assert!(
28349 alloc::format!("{err}").contains("invalid publication object list"),
28350 "got: {err}"
28351 );
28352 let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
28353 let Statement::CreatePublication(p) = s else {
28354 panic!("expected CreatePublication, got {s:?}")
28355 };
28356 let PublicationScope::TablesInSchema(schema) = p.scope else {
28357 panic!("expected TablesInSchema")
28358 };
28359 assert_eq!(schema, "public");
28360 }
28361
28362 #[test]
28363 fn parser_recognises_for_all_tables_except_list() {
28364 let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
28365 let Statement::CreatePublication(p) = s else {
28366 panic!()
28367 };
28368 let PublicationScope::AllTablesExcept(ts) = p.scope else {
28369 panic!("expected AllTablesExcept")
28370 };
28371 assert_eq!(ts, alloc::vec!["t1", "t2"]);
28372 }
28373
28374 #[test]
28375 fn parser_rejects_for_table_with_empty_list() {
28376 // `FOR TABLE` with nothing after is a parse error.
28377 let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
28378 .expect_err("must error on empty list");
28379 // No specific message asserted — the call falls through to
28380 // expect_ident_like which yields "expected identifier, got …".
28381 assert!(!err.message.is_empty());
28382 }
28383
28384 #[test]
28385 fn parser_recognises_show_publications() {
28386 // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
28387 // bare ident in this position, NOT a reserved keyword.
28388 let s = parse("SHOW PUBLICATIONS");
28389 assert!(matches!(s, Statement::ShowPublications));
28390 }
28391
28392 // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
28393
28394 #[test]
28395 fn parser_recognises_create_subscription_single_publication() {
28396 let s = parse(
28397 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
28398 );
28399 let Statement::CreateSubscription(c) = s else {
28400 panic!("expected CreateSubscription, got {s:?}")
28401 };
28402 assert_eq!(c.name, "sub_a");
28403 assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
28404 assert_eq!(c.publications, alloc::vec!["pub_a"]);
28405 }
28406
28407 #[test]
28408 fn parser_recognises_create_subscription_multi_publication() {
28409 let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
28410 let Statement::CreateSubscription(c) = s else {
28411 panic!()
28412 };
28413 assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
28414 }
28415
28416 #[test]
28417 fn parser_rejects_create_subscription_missing_connection() {
28418 let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
28419 .expect_err("must error on missing CONNECTION");
28420 assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
28421 }
28422
28423 #[test]
28424 fn parser_rejects_create_subscription_missing_publication() {
28425 let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
28426 .expect_err("must error on missing PUBLICATION");
28427 assert_eq!(err.message, "syntax error at end of input");
28428 }
28429
28430 #[test]
28431 fn parser_recognises_drop_subscription() {
28432 let s = parse("DROP SUBSCRIPTION sub_a");
28433 let Statement::DropSubscription { name, .. } = s else {
28434 panic!("expected DropSubscription, got {s:?}")
28435 };
28436 assert_eq!(name, "sub_a");
28437 }
28438
28439 #[test]
28440 fn parser_recognises_show_subscriptions() {
28441 let s = parse("SHOW SUBSCRIPTIONS");
28442 assert!(matches!(s, Statement::ShowSubscriptions));
28443 }
28444
28445 #[test]
28446 fn parser_recognises_wait_for_wal_position_no_timeout() {
28447 let s = parse("WAIT FOR WAL POSITION 12345");
28448 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28449 panic!("expected WaitForWalPosition, got {s:?}")
28450 };
28451 assert_eq!(pos, 12345);
28452 assert!(timeout_ms.is_none());
28453 }
28454
28455 #[test]
28456 fn parser_recognises_wait_for_wal_position_with_timeout() {
28457 let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
28458 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28459 panic!()
28460 };
28461 assert_eq!(pos, 67890);
28462 assert_eq!(timeout_ms, Some(5000));
28463 }
28464
28465 #[test]
28466 fn parser_rejects_wait_with_negative_position() {
28467 // The lexer treats `-` as a token; `expect_u64_literal`
28468 // only sees the Integer that follows, so the negative
28469 // arrives as a unary-minus expression at higher levels.
28470 // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
28471 // parse error one way or another.
28472 let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
28473 assert!(!err.message.is_empty());
28474 }
28475
28476 #[test]
28477 fn parser_recognises_bare_analyze() {
28478 let s = parse("ANALYZE");
28479 assert!(matches!(s, Statement::Analyze(None)));
28480 }
28481
28482 #[test]
28483 fn parser_recognises_analyze_with_table() {
28484 let s = parse("ANALYZE users");
28485 let Statement::Analyze(Some(name)) = s else {
28486 panic!("expected Analyze, got {s:?}")
28487 };
28488 assert_eq!(name, "users");
28489 }
28490
28491 #[test]
28492 fn parser_recognises_analyze_with_quoted_table() {
28493 let s = parse("ANALYZE \"Mixed Case\"");
28494 let Statement::Analyze(Some(name)) = s else {
28495 panic!()
28496 };
28497 assert_eq!(name, "Mixed Case");
28498 }
28499
28500 #[test]
28501 fn parser_rejects_analyze_with_garbage_token() {
28502 let err = parse_statement("ANALYZE 42").expect_err("must error");
28503 assert!(!err.message.is_empty());
28504 }
28505
28506 #[test]
28507 fn analyze_display_roundtrips() {
28508 for sql in ["ANALYZE", "ANALYZE users"] {
28509 let s = parse(sql);
28510 let printed = s.to_string();
28511 let again = parse_statement(&printed)
28512 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28513 assert_eq!(s, again);
28514 }
28515 }
28516
28517 #[test]
28518 fn wait_for_display_roundtrips() {
28519 for sql in [
28520 "WAIT FOR WAL POSITION 12345",
28521 "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
28522 ] {
28523 let s = parse(sql);
28524 let printed = s.to_string();
28525 let again = parse_statement(&printed)
28526 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28527 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28528 }
28529 }
28530
28531 #[test]
28532 fn subscription_ddl_display_roundtrips() {
28533 for sql in [
28534 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
28535 "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
28536 "DROP SUBSCRIPTION sub_a",
28537 "SHOW SUBSCRIPTIONS",
28538 ] {
28539 let s = parse(sql);
28540 let printed = s.to_string();
28541 let again = parse_statement(&printed)
28542 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28543 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28544 }
28545 }
28546
28547 #[test]
28548 fn parser_drop_dispatches_user_vs_publication() {
28549 // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
28550 // tokenises DROP. Both targets must still parse.
28551 let s = parse("DROP USER 'alice'");
28552 let Statement::DropUser { name, .. } = s else {
28553 panic!("expected DropUser, got {s:?}")
28554 };
28555 assert_eq!(name, "alice");
28556 // And DROP PUBLICATION lands the new variant.
28557 let s = parse("DROP PUBLICATION p1");
28558 assert!(matches!(s, Statement::DropPublication { .. }));
28559 }
28560
28561 #[test]
28562 fn publication_ddl_display_roundtrips() {
28563 // Every CREATE PUBLICATION variant must Display → parse →
28564 // same AST. v6.1.3 covers all three scope shapes.
28565 for sql in [
28566 "CREATE PUBLICATION pub_a",
28567 "CREATE PUBLICATION pub_a FOR ALL TABLES",
28568 "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
28569 "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
28570 "DROP PUBLICATION pub_a",
28571 "SHOW PUBLICATIONS",
28572 ] {
28573 let s = parse(sql);
28574 let printed = s.to_string();
28575 let again = parse_statement(&printed)
28576 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28577 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28578 }
28579 }
28580
28581 // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
28582
28583 #[test]
28584 fn create_function_returns_trigger_plpgsql_minimal() {
28585 let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
28586 let s = parse(sql);
28587 let Statement::CreateFunction(f) = s else {
28588 panic!("expected CreateFunction");
28589 };
28590 assert_eq!(f.name, "noop");
28591 assert!(!f.or_replace);
28592 assert!(f.args.is_empty());
28593 assert!(matches!(f.returns, FunctionReturn::Trigger));
28594 assert_eq!(f.language, "plpgsql");
28595 let FunctionBody::PlPgSql(block) = f.body else {
28596 panic!("expected PlPgSql body");
28597 };
28598 assert_eq!(block.statements.len(), 1);
28599 assert!(matches!(
28600 block.statements[0],
28601 PlPgSqlStmt::Return(ReturnTarget::New)
28602 ));
28603 }
28604
28605 #[test]
28606 fn create_function_or_replace_with_assignment() {
28607 // mailrs-shape trigger function: NEW.col := to_tsvector(...);
28608 // RETURN NEW.
28609 let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
28610BEGIN
28611 NEW.search_vector := to_tsvector('english', NEW.subject);
28612 RETURN NEW;
28613END;
28614$$";
28615 let s = parse(sql);
28616 let Statement::CreateFunction(f) = s else {
28617 panic!("expected CreateFunction");
28618 };
28619 assert!(f.or_replace);
28620 let FunctionBody::PlPgSql(block) = &f.body else {
28621 panic!("expected PlPgSql body");
28622 };
28623 assert_eq!(block.statements.len(), 2);
28624 // First statement: NEW.search_vector := to_tsvector(...)
28625 let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
28626 panic!("expected Assign as first stmt");
28627 };
28628 match target {
28629 AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
28630 other => panic!("expected NEW.col, got {other:?}"),
28631 }
28632 // Second statement: RETURN NEW
28633 assert!(matches!(
28634 block.statements[1],
28635 PlPgSqlStmt::Return(ReturnTarget::New)
28636 ));
28637 }
28638
28639 #[test]
28640 fn create_trigger_after_insert_or_update() {
28641 let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
28642 let s = parse(sql);
28643 let Statement::CreateTrigger(t) = s else {
28644 panic!("expected CreateTrigger");
28645 };
28646 assert_eq!(t.name, "tg");
28647 assert_eq!(t.table, "messages");
28648 assert_eq!(t.timing, TriggerTiming::After);
28649 assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
28650 assert_eq!(t.for_each, TriggerForEach::Row);
28651 assert_eq!(t.function, "update_sv");
28652 }
28653
28654 #[test]
28655 fn create_trigger_before_delete_execute_procedure_alias() {
28656 // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
28657 let sql =
28658 "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
28659 let s = parse(sql);
28660 let Statement::CreateTrigger(t) = s else {
28661 panic!("expected CreateTrigger");
28662 };
28663 assert_eq!(t.timing, TriggerTiming::Before);
28664 assert_eq!(t.events, vec![TriggerEvent::Delete]);
28665 }
28666
28667 #[test]
28668 fn drop_trigger_if_exists_round_trips() {
28669 // No parser support for DROP TRIGGER yet — added in v7.12.5
28670 // alongside the broader DROP …{IF EXISTS} cleanup. The
28671 // AST + Display impls are in place so we round-trip via
28672 // construction:
28673 let s = Statement::DropTrigger {
28674 name: "tg".into(),
28675 table: "messages".into(),
28676 if_exists: true,
28677 };
28678 assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
28679 }
28680
28681 #[test]
28682 fn trigger_ddl_display_roundtrips_through_parser() {
28683 // CREATE TRIGGER + its referenced CREATE FUNCTION must
28684 // Display → parse → same AST (modulo PL/pgSQL body
28685 // formatting which is parser-canonicalised).
28686 for sql in [
28687 "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
28688 "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
28689 ] {
28690 let s = parse(sql);
28691 let printed = s.to_string();
28692 let again = parse_statement(&printed)
28693 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28694 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28695 }
28696 }
28697}