spg_sql/parser.rs
1//! Recursive-descent parser with a Pratt (precedence-climbing) sub-parser for
2//! expressions.
3//!
4//! Precedence (lowest → highest binding):
5//! `OR` (1) `<` `AND` (2) `<` `NOT` unary (3) `<`
6//! comparisons `=` `<>` `<` `<=` `>` `>=` (4) `<`
7//! `+` `-` (5) `<` `*` `/` (6) `<` unary `-` (7) `<` parens / atom.
8//!
9//! This matches PG's behaviour for the operators we support — e.g. `NOT a = b`
10//! parses as `NOT (a = b)` and `-a * b` as `(-a) * b`.
11
12use alloc::boxed::Box;
13use alloc::format;
14use alloc::string::{String, ToString};
15use alloc::vec;
16use alloc::vec::Vec;
17use core::fmt;
18use core::mem;
19
20use crate::ast::{
21 AssignTarget, BinOp, CastTarget, Collation, ColumnDef, ColumnName, ColumnTypeName,
22 CreateFunctionStatement, CreateIndexStatement, CreatePublicationStatement,
23 CreateSubscriptionStatement, CreateTableStatement, CreateTriggerStatement, DiscardTarget, Expr,
24 ExtractField, FkAction, ForeignKeyConstraint, FrameBound, FrameExclusion, FrameKind,
25 FromClause, FromJoin, FunctionArg, FunctionArgMode, FunctionArgType, FunctionAttrs,
26 FunctionBody, FunctionParallel, FunctionReturn, FunctionVolatility, GrantObject, GrantPriv,
27 GrantStatement, IndexMethod, InsertStatement, IsolationLevel, JoinKind, Literal, MysqlIntWidth,
28 NullTreatment, OrderBy, Overriding, PlPgSqlBlock, PlPgSqlDeclare, PlPgSqlStmt,
29 PublicationScope, RaiseLevel, RangeKindAst, ReturnTarget, SelectItem, SelectStatement,
30 Statement, TableRef, TriggerEvent, TriggerForEach, TriggerTiming, UnOp, UnionKind, VecEncoding,
31 WindowFrame,
32};
33use crate::lexer::{self, LexError, Token};
34
35/// v7.38 — a `WINDOW w AS (…)` definition body:
36/// `(PARTITION BY exprs, ORDER BY (expr, desc, nulls_first), frame)`.
37type WindowDef = (
38 Vec<Expr>,
39 Vec<(Expr, bool, Option<bool>)>,
40 Option<WindowFrame>,
41);
42
43/// v7.14.0 — true when the leading keyword of a top-level
44/// statement is one of the dump-emitted DDL forms SPG accepts
45/// as a no-op (no behavioural effect on the single-schema /
46/// single-database model). These statements are consumed up to
47/// the next `;` / EOF and returned as `Statement::Empty`.
48/// v7.39 (read01 round 57) — wrap a parsed GRANT body in the right statement.
49fn finish_grant(grant: bool, g: GrantStatement) -> Statement {
50 if grant {
51 Statement::Grant(g)
52 } else {
53 Statement::Revoke(g)
54 }
55}
56
57fn is_dump_noise_statement(lc: &str) -> bool {
58 matches!(
59 lc,
60 // v7.39 (read01 round 50): "comment" moved OUT — COMMENT ON is now a
61 // real statement with a real store. v7.39 (read01 round 57): "grant" /
62 // "revoke" moved OUT — table privileges are now REAL (stored in
63 // `relacl`, enforced against the session role); a grant on any other
64 // object class still parses and no-ops so dumps restore.
65 // MySQL bulk-load brackets.
66 "unlock"
67 // MySQL OPTIMIZE / ANALYZE TABLE / CHECK TABLE
68 // diagnostics that pg_dump-style tools also emit
69 // post-restore.
70 | "optimize"
71 | "check"
72 | "use"
73 // PG psql backslash meta-commands that newer
74 // pg_dump versions emit unescaped (\restrict /
75 // \unrestrict). Real psql intercepts these; SPG's
76 // PG-wire sees them as raw text.
77 | "\\restrict"
78 | "\\unrestrict"
79 // v7.17.0 Phase 4.1 — MySQL `DELIMITER //` and
80 // `DELIMITER ;` directives. Technically client-side
81 // (the `mysql` CLI uses them to set the statement
82 // terminator), not SQL — but mysqldump and stored-
83 // procedure scripts emit them inline. SPG's parser
84 // sees one statement at a time and doesn't care
85 // about the terminator, so consume DELIMITER lines
86 // as Empty.
87 | "delimiter"
88 // v7.37.17 (17.6 siblings) — additional PG maintenance /
89 // session-state statements pg_dump + application startup
90 // scripts emit. SPG has no matching session-state to
91 // discard (no prepared-plan cache surface, no temp
92 // sequences), no matching security-label / storage-
93 // option to apply, no separate CREATE/DROP CAST that
94 // affects execution.
95 // v7.37.17 (17.6 siblings) — PG role-cleanup statements
96 // pg_dump / pg_dumpall emit around DROP ROLE:
97 // REASSIGN OWNED BY <role> [, ...] TO <newrole>
98 // DROP OWNED BY <role> [, ...] [CASCADE | RESTRICT]
99 // Both operate on the role's owned objects; SPG has no
100 // role-owner model, so accept-and-no-op.
101 // v7.37.17 (17.6 sibling) — LOAD '<library>'. pg_dump
102 // + extension scripts use LOAD to preload shared
103 // libraries. SPG doesn't have a shared-library extension
104 // point today (extensions ship as first-class crates
105 // linked at build time); accept as a no-op.
106 | "load"
107 )
108}
109
110/// v7.37.43-T4 — PG-unreserved keywords that are legal identifiers
111/// per `pg_get_keywords()`. SPG tokenizes these as named variants
112/// so the parser can dispatch on them in their owning contexts
113/// (`RELEASE SAVEPOINT`, `SHOW name`, `BEGIN`/`COMMIT`/`ROLLBACK`,
114/// `CREATE INDEX`, etc.), but they MUST stay usable as table /
115/// column / alias names — that's the PG contract for unreserved
116/// keywords (see PG docs Appendix C.1).
117///
118/// Before this generalisation, sentori migration 0001_init.sql
119/// `release TEXT NOT NULL` blew up the parser with "expected
120/// identifier, got Release", and the same gap stalked every
121/// SPG drop-in user whose schema had a column / alias named
122/// `release` / `index` / `tables` / `show` / `savepoint` /
123/// `begin` / `commit` / `rollback` / `drop` / `insert` / `values`
124/// / `limit` / `partition`. PG accepts all of them as identifiers
125/// when unquoted, so SPG must too.
126///
127/// Returns the canonical lowercase identifier text when the token
128/// belongs to PG's unreserved class, `None` otherwise. Used by
129/// `expect_ident_like` (column / table / alias names) so the
130/// generalisation applies everywhere an identifier may appear,
131/// not just in the contexts these tokens were introduced for.
132fn unreserved_keyword_text(tok: &Token) -> Option<String> {
133 let s = match tok {
134 // PG keyword class: unreserved or col_name.
135 //
136 Token::Release => "release",
137 Token::Savepoint => "savepoint",
138 Token::Show => "show",
139 Token::Index => "index",
140 Token::Begin => "begin",
141 Token::Commit => "commit",
142 Token::Rollback => "rollback",
143 Token::Drop => "drop",
144 Token::Insert => "insert",
145 Token::Values => "values",
146 Token::Limit => "limit",
147 Token::Partition => "partition",
148 Token::Tables => "tables",
149 Token::Connection => "connection",
150 Token::Publication => "publication",
151 Token::Subscription => "subscription",
152 Token::Interval => "interval",
153 // `extract` is non-reserved in PG too (it's a function the
154 // parser dispatches via context — outside that context it's
155 // a plain identifier).
156 Token::Extract => "extract",
157 Token::Offset => "offset",
158 // `to` is reserved in PG (used in many "AS … TO …" forms), so
159 // it is NOT relaxed here. Same for `from`, `where`, `as`,
160 // `select`, `not`, `and`, `or`, `null`, `true`, `false`,
161 // `create`, `table`, `into`, `on`, `order`, `by`, `having`,
162 // `group`, `distinct`, `union`, `all`, `join`, `inner`,
163 // `left`, `cross`, `outer`, `default`, `is`, `between`,
164 // `in`, `like`, `for`, `except`, `desc`, `asc`, `partition`
165 // (partial — keep partition as unreserved per modern PG).
166 _ => return None,
167 };
168 Some(s.to_string())
169}
170
171/// v7.9.22 — recognise pgvector / SPG vector-index opclass names
172/// in CREATE INDEX. SPG's HNSW already routes by query operator;
173/// the opclass is accepted for `pg_dump` compatibility (mailrs
174/// migration follow-up G5).
175/// v7.13.0 — extended to recognise PG built-in / pg_trgm opclasses
176/// (mailrs round-5 G5). These are tokens-only acceptance — SPG
177/// doesn't change index behaviour based on them.
178/// v7.37.17 (17.6 siblings) — the four PG `each` SRFs share one
179/// FROM-clause pipeline; the stored name tells the executor whether
180/// the value column keeps JSON rendering (`jsonb_each` / `json_each`)
181/// or unwraps to text (`*_each_text`).
182fn is_json_each_name(s: &str) -> bool {
183 s.eq_ignore_ascii_case("jsonb_each_text")
184 || s.eq_ignore_ascii_case("jsonb_each")
185 || s.eq_ignore_ascii_case("json_each_text")
186 || s.eq_ignore_ascii_case("json_each")
187}
188
189/// v7.38 (read01, T14) — resolve named function arguments (`argname => value`)
190/// to positional order for the `make_*` family (the AST stays positional).
191/// Positional args fill slots left-to-right; a named arg goes to its registered
192/// slot; unfilled slots default to integer 0 (PG's optional make_interval
193/// fields — the make_date/time arity is still checked at eval time).
194fn reorder_named_args(
195 fname: &str,
196 args: Vec<Expr>,
197 names: &[Option<String>],
198) -> Result<Vec<Expr>, String> {
199 let params: &[&str] = match fname.to_ascii_lowercase().as_str() {
200 "make_date" => &["year", "month", "day"],
201 "make_time" => &["hour", "min", "sec"],
202 "make_timestamp" | "make_timestamptz" => &["year", "month", "mday", "hour", "min", "sec"],
203 "make_interval" => &["years", "months", "weeks", "days", "hours", "mins", "secs"],
204 other => {
205 return Err(alloc::format!(
206 "function {other}(...) does not support named arguments"
207 ));
208 }
209 };
210 let mut slots: Vec<Option<Expr>> = (0..params.len()).map(|_| None).collect();
211 let mut next_positional = 0usize;
212 for (arg, name) in args.into_iter().zip(names.iter()) {
213 let idx = match name {
214 Some(n) => params
215 .iter()
216 .position(|p| p.eq_ignore_ascii_case(n))
217 .ok_or_else(|| alloc::format!("{fname}(...) has no argument named \"{n}\""))?,
218 None => {
219 let i = next_positional;
220 next_positional += 1;
221 i
222 }
223 };
224 if idx >= slots.len() {
225 return Err(alloc::format!("too many arguments for {fname}(...)"));
226 }
227 if slots[idx].is_some() {
228 return Err(alloc::format!(
229 "argument \"{}\" specified more than once",
230 params[idx]
231 ));
232 }
233 slots[idx] = Some(arg);
234 }
235 Ok(slots
236 .into_iter()
237 .map(|s| s.unwrap_or(Expr::Literal(Literal::Integer(0))))
238 .collect())
239}
240
241/// v7.38 (read01) — parse a lexer `Token::Numeric` source string (digits with
242/// an optional single `.`, no sign, no exponent) into `(unscaled, scale)` for
243/// `Literal::Numeric`. Returns `None` if the mantissa overflows i128.
244/// v7.39 (read01 numeric.c) — the result of expanding an `1.5e3`-style
245/// scientific literal into PG's plain NUMERIC decimal form.
246#[derive(Debug)]
247pub enum SciExpanded {
248 /// Plain decimal string ("1.5e3" → "1500", "1e-5" → "0.00001").
249 Expanded(String),
250 /// Exponent pushes the value outside PG's numeric format
251 /// (more than 131072 integer digits or 16383 fractional digits).
252 Overflow,
253 /// Not a `[±]digits[.digits]e[±]digits` literal at all.
254 NotScientific,
255}
256
257/// Expand scientific notation into a plain decimal string by moving the
258/// decimal point — no float round-trip, so the value stays exact. PG treats
259/// such literals as NUMERIC; the digit-count caps mirror PG's numeric format
260/// limits ("value overflows numeric format").
261pub fn expand_scientific_literal(s: &str) -> SciExpanded {
262 let s = s.trim();
263 let Some(epos) = s.find(['e', 'E']) else {
264 return SciExpanded::NotScientific;
265 };
266 let (mant, exp_str) = (&s[..epos], &s[epos + 1..]);
267 let Ok(exp) = exp_str.parse::<i64>() else {
268 return SciExpanded::NotScientific;
269 };
270 let (neg, mant) = match mant.strip_prefix('-') {
271 Some(r) => (true, r),
272 None => (false, mant.strip_prefix('+').unwrap_or(mant)),
273 };
274 let (int_part, frac_part) = match mant.split_once('.') {
275 Some((i, f)) => (i, f),
276 None => (mant, ""),
277 };
278 if (int_part.is_empty() && frac_part.is_empty())
279 || !int_part.bytes().all(|b| b.is_ascii_digit())
280 || !frac_part.bytes().all(|b| b.is_ascii_digit())
281 {
282 return SciExpanded::NotScientific;
283 }
284 let mut digits = String::with_capacity(int_part.len() + frac_part.len());
285 digits.push_str(int_part);
286 digits.push_str(frac_part);
287 // Decimal point position within `digits` after applying the exponent.
288 let new_point = int_part.len() as i64 + exp;
289 // PG's numeric format: up to 131072 digits before the point, 16383 after.
290 if new_point > 131_072 {
291 return SciExpanded::Overflow;
292 }
293 if (digits.len() as i64 - new_point) > 16_383 {
294 return SciExpanded::Overflow;
295 }
296 let sign = if neg { "-" } else { "" };
297 let plain = if new_point <= 0 {
298 let mut out = String::with_capacity(digits.len() + 2 + (-new_point) as usize);
299 out.push_str("0.");
300 for _ in 0..(-new_point) {
301 out.push('0');
302 }
303 out.push_str(&digits);
304 out
305 } else if (new_point as usize) >= digits.len() {
306 let mut out = digits;
307 for _ in 0..(new_point as usize - out.len()) {
308 out.push('0');
309 }
310 out
311 } else {
312 let mut out = String::with_capacity(digits.len() + 1);
313 out.push_str(&digits[..new_point as usize]);
314 out.push('.');
315 out.push_str(&digits[new_point as usize..]);
316 out
317 };
318 SciExpanded::Expanded(alloc::format!("{sign}{plain}"))
319}
320
321/// v7.39 (round 367, M20) — lower a MySQL hexadecimal binary-string
322/// literal (`0x…` / `X'…'`) onto the existing bytea cast. The hex digits
323/// are left-padded to an even count (`0x123` → byte string `01 23`, per
324/// MariaDB) and handed to the PG bytea input format (`\x…`).
325#[inline(never)]
326fn hex_literal_to_bytea_expr(hex: &str) -> Expr {
327 let padded = if hex.len() % 2 == 1 {
328 alloc::format!("0{hex}")
329 } else {
330 hex.to_string()
331 };
332 Expr::Cast {
333 expr: alloc::boxed::Box::new(Expr::Literal(Literal::String(alloc::format!(
334 "\\x{padded}"
335 )))),
336 target: CastTarget::Named("bytea".to_string()),
337 }
338}
339
340/// v7.39 (round 367, M20) — lower a MySQL bit-value literal (`b'1010'`)
341/// onto the bytea cast. The bits are read big-endian and left-padded to a
342/// whole number of bytes (`b'1010'` → one byte `0x0A`, per MariaDB).
343#[inline(never)]
344fn bits_literal_to_bytea_expr(bits: &str) -> Expr {
345 let pad = (8 - bits.len() % 8) % 8;
346 let mut hex = String::with_capacity((bits.len() + pad).div_ceil(4));
347 let padded: String = core::iter::repeat_n('0', pad).chain(bits.chars()).collect();
348 for nibble in padded.as_bytes().chunks(4) {
349 let mut v = 0u8;
350 for &b in nibble {
351 v = (v << 1) | (b - b'0');
352 }
353 hex.push(char::from_digit(u32::from(v), 16).unwrap_or('0'));
354 }
355 hex_literal_to_bytea_expr(&hex)
356}
357
358/// Resolve a lexer `Token::Numeric` into its literal. PG semantics: a dotted
359/// or over-i64 literal is exact NUMERIC; scientific notation is NUMERIC too
360/// (expanded to the plain decimal form); only a fractional depth beyond SPG's
361/// scale width (u8) falls back to double precision (recorded delta).
362/// Kept out of the parse_expr recursion frame — see the call site.
363#[inline(never)]
364fn numeric_token_to_literal(s: String) -> Result<Literal, String> {
365 match parse_decimal_literal(&s) {
366 Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
367 // v7.38 (read01, T3.C3) — a plain decimal too wide for i128 keeps
368 // its exact value as a NumericBig.
369 None if !s.contains(['e', 'E']) => Ok(Literal::NumericBig(s)),
370 // v7.39 (read01 numeric.c) — expand the exponent form.
371 None => match expand_scientific_literal(&s) {
372 SciExpanded::Expanded(plain) => match parse_decimal_literal(&plain) {
373 Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
374 None if plain
375 .split_once('.')
376 .is_none_or(|(_, f)| u8::try_from(f.len()).is_ok()) =>
377 {
378 Ok(Literal::NumericBig(plain))
379 }
380 None => s
381 .parse::<f64>()
382 .map(Literal::Float)
383 .map_err(|_| format!("invalid numeric literal {s:?}")),
384 },
385 SciExpanded::Overflow => Err("value overflows numeric format".to_string()),
386 SciExpanded::NotScientific => s
387 .parse::<f64>()
388 .map(Literal::Float)
389 .map_err(|_| format!("invalid numeric literal {s:?}")),
390 },
391 }
392}
393
394fn parse_decimal_literal(s: &str) -> Option<(i128, u16)> {
395 let (int_part, frac_part) = match s.split_once('.') {
396 Some((i, f)) => (i, f),
397 None => (s, ""),
398 };
399 // v7.39 (round 271) — was u8::MAX. A literal with 256 decimal
400 // places fell out of the numeric path here, which is why
401 // `pg_typeof(1e-256)` answered double precision and a plain
402 // 256-place decimal aborted the query in the big-decimal converter.
403 if frac_part.len() > u16::MAX as usize {
404 return None;
405 }
406 let mut digits = String::with_capacity(int_part.len() + frac_part.len());
407 digits.push_str(int_part);
408 digits.push_str(frac_part);
409 let mantissa: i128 = digits.parse().ok()?;
410 #[allow(clippy::cast_possible_truncation)]
411 Some((mantissa, frac_part.len() as u16))
412}
413
414/// `jsonb_to_record` / `jsonb_to_recordset` (+ `json_` variants) — the
415/// record-returning JSON functions that take a `AS alias(col type, …)`
416/// column-definition list in FROM position.
417fn is_json_to_record_name(s: &str) -> bool {
418 s.eq_ignore_ascii_case("jsonb_to_recordset")
419 || s.eq_ignore_ascii_case("jsonb_to_record")
420 // v7.39 (read01 jsonfuncs.c) — the populate family with an AS
421 // column-definition list desugars identically (the record base
422 // argument only carries the type; a non-NULL base's field
423 // defaults are a recorded delta).
424 || s.eq_ignore_ascii_case("json_populate_record")
425 || s.eq_ignore_ascii_case("jsonb_populate_record")
426 || s.eq_ignore_ascii_case("json_populate_recordset")
427 || s.eq_ignore_ascii_case("jsonb_populate_recordset")
428 || s.eq_ignore_ascii_case("json_to_recordset")
429 || s.eq_ignore_ascii_case("json_to_record")
430}
431
432impl Parser {
433 /// Whether what follows an identifier ends an index key, which is how
434 /// an operator class is told from anything else in that position.
435 fn opclass_position_follows(next: Option<&Token>) -> bool {
436 match next {
437 // `ASC` / `DESC` have their own tokens; matching them as
438 // identifiers named "asc" / "desc" — which the first version of
439 // this did — never fires, and `(c text_pattern_ops DESC)` (which
440 // PG18.4 accepts, verified) went on failing to parse.
441 Some(Token::Comma | Token::RParen | Token::Asc | Token::Desc) => true,
442 Some(Token::Ident(w)) => {
443 w.eq_ignore_ascii_case("nulls") || w.eq_ignore_ascii_case("collate")
444 }
445 _ => false,
446 }
447 }
448}
449
450fn is_vector_opclass_name(name: &str) -> bool {
451 let lc = name.to_ascii_lowercase();
452 matches!(
453 lc.as_str(),
454 "vector_cosine_ops"
455 | "vector_l2_ops"
456 | "vector_ip_ops"
457 | "halfvec_cosine_ops"
458 | "halfvec_l2_ops"
459 | "halfvec_ip_ops"
460 | "sq8_cosine_ops"
461 | "sq8_l2_ops"
462 | "sq8_ip_ops"
463 // pg_trgm — trigram operator class. SPG's GIN index
464 // already uses tsvector tokens; trigram-style LIKE
465 // pattern matching still routes through a sequential
466 // scan, but the opclass name is accepted so PG schemas
467 // load.
468 | "gin_trgm_ops"
469 | "gist_trgm_ops"
470 // PG built-in btree opclasses occasionally appear in
471 // pg_dump output for column types with multiple
472 // sort orders (text_pattern_ops, varchar_pattern_ops,
473 // bpchar_pattern_ops).
474 | "text_pattern_ops"
475 | "varchar_pattern_ops"
476 | "bpchar_pattern_ops"
477 | "int4_ops"
478 | "int8_ops"
479 | "text_ops"
480 )
481}
482
483#[derive(Debug, Clone, PartialEq, Eq)]
484pub struct ParseError {
485 pub message: String,
486 /// Index into the token stream where parsing tripped. Not a byte offset.
487 /// v7.39 (read01 round 95) — the byte/char position is NOT stored here: a
488 /// field would grow every `Result<_, ParseError>` slot on the deeply
489 /// recursive parse stack and tip the nesting-budget frame cliff. PG's
490 /// 1-based char position is recovered on the cold error path by
491 /// [`syntax_error_position`], which re-tokenizes to map this token index.
492 pub token_pos: usize,
493}
494
495impl fmt::Display for ParseError {
496 /// v7.39 (round 322/V24) — the message ALONE. It used to be prefixed
497 /// with `parse error at token #N: `, which PG has no equivalent of:
498 /// the message bodies are already PG's verbatim (`LIMIT must not be
499 /// negative`, `invalid input syntax for type bigint: "abc"`), and the
500 /// prefix was SPG's internal token index leaking into every one of
501 /// them. `token_pos` stays a field — the wire recovers PG's 1-based
502 /// character position from it for the ErrorResponse `P`.
503 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504 f.write_str(&self.message)
505 }
506}
507
508impl From<LexError> for ParseError {
509 fn from(e: LexError) -> Self {
510 Self {
511 message: format!("lex: {e}"),
512 token_pos: 0,
513 }
514 }
515}
516
517/// v7.9.30 — parse a single expression (no trailing junk). Used by
518/// the engine to re-hydrate stored partial-index / unique-index
519/// predicates from their canonical Display form. The same Pratt
520/// parser the statement path uses; this entry point just skips the
521/// statement dispatch.
522pub fn parse_expression(input: &str) -> Result<Expr, ParseError> {
523 let (tokens, offsets) =
524 lexer::tokenize_with_offsets(input, false).map_err(|e| shape_lex_error(&e, input))?;
525 let mut p = Parser::new(tokens);
526 let expr = p
527 .parse_expr(0)
528 .and_then(|e| p.expect_eof().map(|()| e))
529 .map_err(|e| shape_syntax_error(e, input, &offsets))?;
530 Ok(expr)
531}
532
533/// Parse exactly one statement, swallow an optional trailing `;`, and require
534/// the token stream to end there. PG string semantics.
535pub fn parse_statement(input: &str) -> Result<Statement, ParseError> {
536 parse_statement_with(input, false)
537}
538
539/// v7.22 (round-13 T3) — dialect-aware entry: `backslash_escapes`
540/// selects MySQL-style string lexing (see `lexer::tokenize_with`).
541/// The engine threads its session flag through here.
542pub fn parse_statement_with(input: &str, backslash_escapes: bool) -> Result<Statement, ParseError> {
543 let (tokens, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes)
544 .map_err(|e| shape_lex_error(&e, input))?;
545 // The same session flag names the dialect for both the lexer and
546 // the type mapping.
547 let mut p = Parser::new_with_dialect(tokens, backslash_escapes).with_source(input, &offsets);
548 let stmt = (|| {
549 let stmt = p.parse_one_statement()?;
550 if matches!(p.peek(), Token::Semicolon) {
551 p.advance();
552 }
553 p.expect_eof()?;
554 Ok(stmt)
555 })()
556 .map_err(|e: ParseError| shape_syntax_error(e, input, &offsets))?;
557 Ok(stmt)
558}
559
560/// v7.39 (round 340, V56) — PG has exactly two syntax-error wordings:
561/// `syntax error at or near "<token>"` and `syntax error at end of input`
562/// (measured on 18.4 across a dozen shapes). SPG wrote its own per-site
563/// prose — `expected identifier, got Eof`, `unexpected token From in
564/// expression`, `expected end of input, got Ident("with")` — which named
565/// internal token types and, in the Debug forms, leaked the parser's own
566/// enum into a message clients read.
567///
568/// Applied once on the way out, so every construction site is covered and
569/// the token named is the one the error itself points at. Messages whose
570/// bodies are already PG's verbatim (`LIMIT must not be negative`,
571/// `invalid input syntax for type bigint: "abc"`) are left alone — those
572/// are PG's own errors, not its syntax error.
573fn shape_syntax_error(e: ParseError, input: &str, offsets: &[usize]) -> ParseError {
574 if !(e.message.starts_with("expected ") || e.message.starts_with("unexpected token ")) {
575 return e;
576 }
577 let message = match offending_lexeme(input, offsets, e.token_pos) {
578 Some(tok) => alloc::format!("syntax error at or near \"{tok}\""),
579 None => "syntax error at end of input".into(),
580 };
581 ParseError {
582 message,
583 token_pos: e.token_pos,
584 }
585}
586
587/// v7.39 (round 340, V56) — a lexer-level failure the way PG words it.
588/// Measured on 18.4: `unterminated quoted string at or near "'abc"`,
589/// `unterminated quoted identifier at or near ""abc"`, `unterminated /*
590/// comment at or near "/* x"` — the quoted part runs from the opening
591/// delimiter to the end of the input. SPG reported its own internal
592/// shape instead (`unterminated string literal at byte 7`), which named
593/// a byte offset no client can use.
594fn shape_lex_error(e: &lexer::LexError, input: &str) -> ParseError {
595 use lexer::LexErrorKind as K;
596 let from_here = input.get(e.pos..).map(str::trim_end).unwrap_or("");
597 let message = match &e.kind {
598 K::UnterminatedString => {
599 alloc::format!("unterminated quoted string at or near \"{from_here}\"")
600 }
601 K::UnterminatedQuotedIdent => {
602 alloc::format!("unterminated quoted identifier at or near \"{from_here}\"")
603 }
604 K::UnterminatedBlockComment => {
605 alloc::format!("unterminated /* comment at or near \"{from_here}\"")
606 }
607 // PG has no "unknown character" error of its own — the character
608 // is skipped and the parser reports the next token. SPG stops at
609 // the character itself and names it, which is the same shape.
610 K::UnknownChar(c) => alloc::format!("syntax error at or near \"{c}\""),
611 // The number-literal kinds already carry PG's `at or near` form.
612 other => alloc::format!(
613 "{}",
614 lexer::LexError {
615 kind: other.clone(),
616 pos: e.pos,
617 }
618 ),
619 };
620 ParseError {
621 message,
622 token_pos: 0,
623 }
624}
625
626/// The offending token exactly as it appears in the input, or `None` at
627/// end of input. PG echoes the source spelling — a lower-case `frm`
628/// reports as `frm`, not as a canonicalised keyword.
629fn offending_lexeme<'a>(input: &'a str, offsets: &[usize], token_pos: usize) -> Option<&'a str> {
630 let start = *offsets.get(token_pos)?;
631 if start >= input.len() {
632 return None;
633 }
634 let end = offsets
635 .get(token_pos + 1)
636 .copied()
637 .unwrap_or(input.len())
638 .min(input.len());
639 let seg = input.get(start..end)?.trim();
640 if seg.is_empty() {
641 return None;
642 }
643 // A quoted literal / identifier keeps its inner spaces; anything else
644 // ends at the first whitespace (the segment runs to the NEXT token's
645 // start, which may swallow a comment).
646 if seg.starts_with('\'') || seg.starts_with('"') || seg.starts_with('`') {
647 Some(seg)
648 } else {
649 seg.split_whitespace().next()
650 }
651}
652
653/// v7.39 (read01 round 95) — recover PG's 1-based CHARACTER error position for
654/// a [`ParseError::token_pos`]. Kept off the `ParseError` struct (and so off
655/// every recursive `Result` slot) to protect the nesting-budget frame cliff:
656/// this re-tokenizes `input` on the cold error path to map the failing token
657/// index to its start byte, then to a character offset. `backslash_escapes`
658/// must match the parse that produced `token_pos` (it barely shifts offsets,
659/// but stay consistent). Returns `None` when the index has no offset or the
660/// byte isn't a char boundary. The wire attaches it as the ErrorResponse `P`.
661#[must_use]
662pub fn syntax_error_position(
663 input: &str,
664 backslash_escapes: bool,
665 token_pos: usize,
666) -> Option<usize> {
667 let (_, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes).ok()?;
668 let byte_off = *offsets.get(token_pos)?;
669 if byte_off > input.len() || !input.is_char_boundary(byte_off) {
670 return None;
671 }
672 Some(input[..byte_off].chars().count() + 1)
673}
674
675struct Parser {
676 tokens: Vec<Token>,
677 pos: usize,
678 /// v7.39 (round 274) — the session's dialect, carried by the same
679 /// signal that drives string-literal escaping: `SET sql_mode` (only
680 /// MySQL clients and mysqldump preambles emit it) turns it on,
681 /// `SET standard_conforming_strings` (every pg_dump preamble) turns
682 /// it off. Needed here because the two dialects disagree about what
683 /// `REAL` means — see the type mapping below.
684 mysql_dialect: bool,
685 /// v7.30.2 (mailrs round-25 ask 2) — live nesting depth of the
686 /// mutually recursive expr/select parsers. Bounded so a deeply
687 /// nested input returns a parse error instead of overflowing
688 /// the stack (embed hosts die on overflow — it is an abort,
689 /// not a catchable error).
690 nest_depth: usize,
691 /// TABLESAMPLE lowering channel: the table-ref parser pushes a
692 /// `random() < p/100` predicate here; the enclosing SELECT
693 /// drains the list after its WHERE parses and ANDs the
694 /// predicates in. parse_bare_select save/restores around its
695 /// FROM+WHERE so nested selects only drain their own.
696 pending_sample_preds: Vec<Expr>,
697 /// v7.39 (round 691) — collation lowering channel, the same shape as
698 /// `pending_sample_preds` above. `expr COLLATE "name"` is ORDERING
699 /// information, and `ast::OrderBy` is where this parser keeps ordering
700 /// information (`desc`, `nulls_first`); the alternative — a new `Expr`
701 /// variant — puts a new arm on `eval_expr`, which this repo has
702 /// measured to overflow the debug stack. So while an ORDER BY KEY is
703 /// being parsed the postfix loop drops the name here instead of
704 /// refusing it, and the key's parser takes it.
705 ///
706 /// Only inside an ORDER BY key: everywhere else an unperformable
707 /// collation still errors, because accepting one at a COMPARISON and
708 /// ignoring it is the defect F36 exists to close.
709 in_order_by_key: bool,
710 order_key_collation: Option<String>,
711 /// POSITION(sub IN str) — while parsing the needle, the IN
712 /// keyword is the argument separator, not a membership test.
713 /// The postfix loop leaves IN unconsumed when this is set.
714 suppress_in_tail: bool,
715 /// Index of the token the last `advance()` returned — see
716 /// [`Parser::consumed_pos`].
717 last_consumed: usize,
718 /// v7.39 (round 506) — the statement's own text and the byte each token
719 /// starts at, so a MySQL projection item can report the SOURCE TEXT
720 /// MariaDB reports: `SELECT a + b` names its column `a + b`,
721 /// spacing and all. Only filled for a MySQL session — a PG one names
722 /// columns from the parsed shape and pays nothing for this.
723 src: Option<(String, Vec<usize>)>,
724}
725
726/// Max expr/select parser nesting (parens, subqueries, CASE, …).
727/// Real SQL nests a few dozen levels at the extreme. Each nesting level
728/// costs a parse_expr→parse_unary→parse_atom frame chain, so the budget
729/// exists to turn a deep statement into a catchable parse ERROR: a stack
730/// overflow is an abort, and in the server it does not fail one query, it
731/// takes the process down and every other connection with it.
732///
733/// v7.39 (round 507) — measured, because the figure here used to be a
734/// guess ("over 10 KiB in debug … comfortably inside a 2 MiB worker stack
735/// in BOTH debug and release"), and the debug half of that is wrong by
736/// more than an order of magnitude:
737///
738/// * RELEASE, on a 2 MiB worker stack: every recursive shape reaches
739/// this budget and errors. Verified against a live server for nested
740/// derived tables, parens, calls, CASE, IN-subqueries, scalar
741/// subqueries, NOT and unary minus — the server stayed up through all
742/// of them. This is the contract that matters, and it holds.
743/// * DEBUG: nested derived tables cost roughly 235 KiB of stack PER
744/// LEVEL, so parsing alone aborts around 35 levels on an 8 MiB stack
745/// and executing aborts around 8 inside a test thread. The budget is
746/// simply unreachable there, which is why a deep-nesting test has to
747/// ask for a large stack of its own — see `nesting_budget_errors_at`
748/// in the parser tests.
749/// v7.39 (round 541) — the pg_catalog relations SPG synthesises, in
750/// one place.
751///
752/// There were two copies of this fact: a curated list, used for BARE
753/// names, and — in `try_peek_meta_qualified` — no list at all, which
754/// rewrote `pg_catalog.<anything>` to `__spg_pg_<anything>` and left
755/// the engine to complain about a view it could not materialise. So
756/// writing the schema qualifier CHANGED THE ANSWER: `pg_stat_activity`
757/// had rows, `pg_catalog.pg_stat_activity` was an error.
758///
759/// PG puts `pg_catalog` at the implicit front of every search_path, so
760/// the two spellings name the same relation and must resolve the same
761/// way. Names NOT here (`pg_stat_activity`, `pg_locks`,
762/// `pg_stat_statements`, `pg_statio_user_tables`) route through the
763/// meta_view_result path under their own names and must not be
764/// rewritten; a name that is neither reaches the ordinary resolver,
765/// which reports that the relation does not exist — PG's answer.
766const SYNTHESISED_PG_CATALOGS: &[&str] = &[
767 "pg_am",
768 "pg_attrdef",
769 "pg_attribute",
770 "pg_cast",
771 "pg_db_role_setting",
772 "pg_conversion",
773 "pg_default_acl",
774 "pg_shadow",
775 "pg_sequences",
776 "pg_range",
777 "pg_partitioned_table",
778 "pg_language",
779 "pg_group",
780 "pg_authid",
781 "pg_class",
782 "pg_collation",
783 "pg_constraint",
784 "pg_database",
785 "pg_depend",
786 "pg_amop",
787 "pg_amproc",
788 "pg_opclass",
789 "pg_opfamily",
790 // v7.39 (read01 round 50) — COMMENT ON store, PG's pg_description.
791 "pg_description",
792 "pg_enum",
793 "pg_extension",
794 // v7.39 (round 541) — pg_dump reads it for every relation of kind
795 // 'f'. SPG has no foreign tables, so it is empty, which is also
796 // what PG reports on a database that has none.
797 "pg_foreign_table",
798 // v7.39 (round 541) — the empty-by-truth family; see
799 // EMPTY_PG_CATALOGS in spg-engine::system_catalog.
800 "pg_event_trigger",
801 "pg_file_settings",
802 "pg_foreign_data_wrapper",
803 "pg_foreign_server",
804 "pg_hba_file_rules",
805 "pg_ident_file_mappings",
806 "pg_init_privs",
807 "pg_parameter_acl",
808 "pg_prepared_xacts",
809 "pg_publication_namespace",
810 "pg_publication_rel",
811 "pg_publication_tables",
812 "pg_replication_origin",
813 "pg_replication_origin_status",
814 "pg_seclabel",
815 "pg_seclabels",
816 "pg_shdepend",
817 "pg_shdescription",
818 "pg_shmem_allocations",
819 "pg_shmem_allocations_numa",
820 "pg_shseclabel",
821 "pg_statistic_ext_data",
822 "pg_stats_ext",
823 "pg_stats_ext_exprs",
824 "pg_subscription_rel",
825 "pg_transform",
826 "pg_user_mapping",
827 "pg_user_mappings",
828 "pg_index",
829 "pg_indexes",
830 "pg_inherits",
831 // v7.39 (round 650) — the text-search catalogs SPG can fill
832 // honestly. `pg_ts_config_map` is deliberately NOT here: it maps
833 // token types to dictionaries and SPG has no token-type model,
834 // the same gap that leaves `ts_token_type` / `ts_debug` unbuilt.
835 "pg_ts_config",
836 "pg_ts_config_map",
837 "pg_ts_dict",
838 "pg_ts_parser",
839 "pg_ts_template",
840 "pg_matviews",
841 "pg_namespace",
842 // v7.39 (round 621)
843 "pg_operator",
844 "pg_policies",
845 "pg_policy",
846 "pg_proc",
847 "pg_publication",
848 "pg_replication_slots",
849 "pg_roles",
850 // v7.39 (round 143) — the rewrite-rule listing view.
851 // v7.39 (round 312) — and the rule catalogue itself, which
852 // `pg_get_ruledef(oid)` resolves against.
853 "pg_rewrite",
854 "pg_rules",
855 "pg_sequence",
856 "pg_settings",
857 "pg_stat_archiver",
858 "pg_stat_bgwriter",
859 "pg_stat_checkpointer",
860 "pg_stat_database",
861 "pg_stat_io",
862 "pg_stat_progress_analyze",
863 "pg_auth_members",
864 "pg_stat_progress_create_index",
865 "pg_stat_progress_vacuum",
866 "pg_stat_replication",
867 "pg_stat_slru",
868 "pg_stat_subscription_stats",
869 "pg_stat_user_functions",
870 "pg_stat_user_indexes",
871 "pg_stat_user_tables",
872 "pg_stat_wal",
873 "pg_prepared_statements",
874 "pg_largeobject",
875 "pg_largeobject_metadata",
876 "pg_statistic",
877 "pg_statistic_ext",
878 "pg_subscription",
879 "pg_tables",
880 "pg_tablespace",
881 // v7.39 (round 502) — the timezone catalogues. SPG resolved
882 // named zones correctly but could not list them, so a client
883 // populating a timezone picker got "relation does not exist".
884 "pg_timezone_abbrevs",
885 "pg_timezone_names",
886 "pg_trigger",
887 "pg_type",
888 "pg_user",
889 "pg_views",
890];
891
892const MAX_NEST_DEPTH: usize = 64;
893
894/// Stack accounting for the nesting budget, test-only.
895///
896/// `MAX_NEST_DEPTH` is a fixed count calibrated against a frame size
897/// that MOVES: a compiler upgrade grew the parser's debug frames and
898/// silently ate the margin until `nesting_budget_errors_cleanly` went
899/// from erroring cleanly to aborting on a stack overflow. A count
900/// cannot notice that on its own, so the budget is measured here and
901/// held to a ceiling.
902///
903/// The reading has to come from a helper whose OWN frame is the same at
904/// every call: debug slot placement does not follow source order, so a
905/// local's address inside the function under test is not that
906/// function's frame boundary. Two earlier probes were wrong that way —
907/// one read `&self.nest_depth`, which is the `Parser`'s address and
908/// never moves at all.
909#[cfg(test)]
910mod frame_meter {
911 extern crate std;
912 use std::cell::Cell;
913
914 // Per-THREAD, not global. `cargo test` runs tests in parallel and
915 // plenty of them parse nested expressions, so shared statics get
916 // stack addresses from several threads at once and the subtraction
917 // below turns into noise — it read 229,772 bytes per level that way,
918 // while passing when the test was run on its own.
919 std::thread_local! {
920 static AT_LO: Cell<usize> = const { Cell::new(0) };
921 static AT_HI: Cell<usize> = const { Cell::new(0) };
922 }
923
924 pub(super) const SAMPLE_LO: usize = 4;
925 pub(super) const SAMPLE_HI: usize = 24;
926
927 #[inline(never)]
928 pub(super) fn record(depth: usize) {
929 let anchor = 0u8;
930 let at = core::ptr::from_ref(&anchor) as usize;
931 if depth == SAMPLE_LO {
932 AT_LO.with(|c| c.set(at));
933 } else if depth == SAMPLE_HI {
934 AT_HI.with(|c| c.set(at));
935 }
936 }
937
938 /// Bytes of stack one nesting level costs, averaged over the span.
939 pub(super) fn bytes_per_level() -> usize {
940 let lo = AT_LO.with(Cell::get);
941 let hi = AT_HI.with(Cell::get);
942 assert!(lo > 0 && hi > 0, "meter never sampled: lo={lo} hi={hi}");
943 assert!(lo > hi, "stack grew upwards? lo={lo} hi={hi}");
944 (lo - hi) / (SAMPLE_HI - SAMPLE_LO)
945 }
946
947 pub(super) fn reset() {
948 AT_LO.with(|c| c.set(0));
949 AT_HI.with(|c| c.set(0));
950 }
951}
952
953/// v7.39 (read01 geo_ops.c) — prefix `@@` desugar target, out-of-line so
954/// the constructor's temporaries stay off `parse_unary`'s recursion frame.
955#[inline(never)]
956fn build_center_call(e: Expr) -> Expr {
957 Expr::FunctionCall {
958 name: alloc::string::String::from("center"),
959 args: alloc::vec![e],
960 }
961}
962
963/// Max consecutive binary operators at ONE precedence level
964/// (`a OR b OR c …`, `1+1+1…`). The chain builds iteratively at
965/// parse time but evaluates and drops recursively — depth beyond
966/// this overflows 2 MiB worker stacks (debug eval frames run
967/// multiple KiB). `IN (…)` lists are flat and unaffected.
968const MAX_BINARY_CHAIN: usize = 256;
969
970/// v7.22 (round-13 gap 5) — the kind keyword after `CONSTRAINT
971/// <name>` in a CREATE TABLE column list. FOREIGN KEY is not here:
972/// it keeps its dedicated path (`parse_table_level_fk`).
973enum NamedTableConstraintKind {
974 Check,
975 Unique,
976 PrimaryKey,
977 Exclude,
978}
979
980impl Parser {
981 fn new(tokens: Vec<Token>) -> Self {
982 Self::new_with_dialect(tokens, false)
983 }
984
985 fn new_with_dialect(tokens: Vec<Token>, mysql_dialect: bool) -> Self {
986 Self {
987 tokens,
988 mysql_dialect,
989 in_order_by_key: false,
990 order_key_collation: None,
991 pos: 0,
992 nest_depth: 0,
993 pending_sample_preds: Vec::new(),
994 suppress_in_tail: false,
995 last_consumed: 0,
996 src: None,
997 }
998 }
999
1000 /// Hand the parser the text it is parsing, for [`Parser::source_span`].
1001 fn with_source(mut self, input: &str, offsets: &[usize]) -> Self {
1002 if self.mysql_dialect {
1003 self.src = Some((input.to_string(), offsets.to_vec()));
1004 }
1005 self
1006 }
1007
1008 /// The source text spanning tokens `start ..= end`, trimmed.
1009 ///
1010 /// The offsets are token STARTS, so the span runs to the start of the
1011 /// token after `end` and gives back the whitespace between them —
1012 /// trimming is what makes `a + b FROM t` end at `b`.
1013 fn source_span(&self, start: usize, end: usize) -> Option<&str> {
1014 let (text, offsets) = self.src.as_ref()?;
1015 let from = *offsets.get(start)?;
1016 let to = *offsets.get(end + 1)?;
1017 text.get(from..to).map(str::trim_end)
1018 }
1019
1020 /// v7.30.2 (mailrs round-25 ask 2) — bump the expr/select
1021 /// nesting depth, erroring out cleanly past the budget.
1022 fn enter_nested(&mut self) -> Result<(), ParseError> {
1023 self.nest_depth += 1;
1024 #[cfg(test)]
1025 frame_meter::record(self.nest_depth);
1026 if self.nest_depth > MAX_NEST_DEPTH {
1027 self.nest_depth -= 1;
1028 return Err(self.err(alloc::format!(
1029 "statement nests deeper than {MAX_NEST_DEPTH} levels"
1030 )));
1031 }
1032 Ok(())
1033 }
1034
1035 fn peek(&self) -> &Token {
1036 // tokens always ends with Eof; pos is clamped in advance().
1037 &self.tokens[self.pos]
1038 }
1039
1040 fn advance(&mut self) -> Token {
1041 let t = mem::replace(&mut self.tokens[self.pos], Token::Eof);
1042 self.last_consumed = self.pos;
1043 if self.pos + 1 < self.tokens.len() {
1044 self.pos += 1;
1045 }
1046 t
1047 }
1048
1049 /// v7.39 (round 340, V56) — the index of the token `advance()` just
1050 /// returned. It was computed as `pos - 1`, which is wrong at both
1051 /// ends: `advance()` parks on the final Eof rather than running off
1052 /// the end (so `SELECT * FROM` named `FROM` where PG says `at end of
1053 /// input`), and after backtracking `pos` is no longer one past the
1054 /// token that failed. Recorded by `advance()` itself instead.
1055 const fn consumed_pos(&self) -> usize {
1056 self.last_consumed
1057 }
1058
1059 fn err(&self, message: String) -> ParseError {
1060 ParseError {
1061 message,
1062 token_pos: self.pos,
1063 }
1064 }
1065
1066 fn expect_eof(&self) -> Result<(), ParseError> {
1067 if matches!(self.peek(), Token::Eof) {
1068 Ok(())
1069 } else {
1070 Err(self.err(format!("expected end of input, got {:?}", self.peek())))
1071 }
1072 }
1073
1074 /// v7.14.0 — swallow every token up to (but not including) the
1075 /// next semicolon / EOF. Used by the dump-noise dispatcher
1076 /// to consume `COMMENT ON …`, `GRANT …`, `LOCK TABLES …`,
1077 /// etc. without modeling each grammar.
1078 /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
1079 /// Kinds SPG stores by name: TABLE / COLUMN / INDEX / VIEW / SEQUENCE /
1080 /// SCHEMA / TYPE / DATABASE / FUNCTION. Anything else (and the multi-word
1081 /// `CONSTRAINT c ON t` / `MATERIALIZED VIEW` forms) keeps the old
1082 /// swallow-as-no-op behaviour so a pg_dump tail still loads.
1083 fn parse_comment_on(&mut self) -> Result<Statement, ParseError> {
1084 let start = self.pos;
1085 self.advance(); // COMMENT
1086 if !matches!(self.peek(), Token::On) {
1087 self.pos = start;
1088 self.consume_until_statement_boundary();
1089 return Ok(Statement::Empty);
1090 }
1091 self.advance(); // ON
1092 let kind = match self.peek() {
1093 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
1094 Token::Table => "table".into(),
1095 _ => {
1096 self.consume_until_statement_boundary();
1097 return Ok(Statement::Empty);
1098 }
1099 };
1100 if !matches!(
1101 kind.as_str(),
1102 "table"
1103 | "column"
1104 | "index"
1105 | "view"
1106 | "sequence"
1107 | "schema"
1108 | "type"
1109 | "database"
1110 | "function"
1111 ) {
1112 self.consume_until_statement_boundary();
1113 return Ok(Statement::Empty);
1114 }
1115 self.advance(); // the kind keyword
1116 // The object name. ⚠️ `expect_ident_like` strips a leading
1117 // `<schema>.` qualifier and returns only the trailing ident (SPG is
1118 // single-schema), which would turn `COMMENT ON COLUMN t.c` into just
1119 // `c`. Read the dotted parts from raw tokens instead, then let a
1120 // 3-part `schema.t.c` drop its leading schema like everywhere else.
1121 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
1122 loop {
1123 match self.advance() {
1124 Token::Ident(s) | Token::QuotedIdent(s) => parts.push(s),
1125 other if unreserved_keyword_text(&other).is_some() => {
1126 parts.push(unreserved_keyword_text(&other).unwrap());
1127 }
1128 other => {
1129 return Err(ParseError {
1130 message: alloc::format!("expected identifier, got {other:?}"),
1131 token_pos: self.consumed_pos(),
1132 });
1133 }
1134 }
1135 if matches!(self.peek(), Token::Dot) {
1136 self.advance();
1137 } else {
1138 break;
1139 }
1140 }
1141 // COLUMN wants `table.column`; every other kind wants a bare name.
1142 let want = if kind == "column" { 2 } else { 1 };
1143 while parts.len() > want {
1144 parts.remove(0);
1145 }
1146 let name = parts.join(".");
1147 // v7.39 (round 710) — `COMMENT ON FUNCTION f(int, text) IS …`.
1148 // pg_dump writes the SIGNATURE, and the paren list was a syntax
1149 // error here — a dump carrying one function comment failed to
1150 // restore. The list is consumed (the comment store keys by name;
1151 // overload-precise comments are the function-predicate follow-up).
1152 if matches!(self.peek(), Token::LParen)
1153 && matches!(
1154 kind.as_str(),
1155 "function" | "procedure" | "aggregate" | "routine"
1156 )
1157 {
1158 let mut depth = 0usize;
1159 loop {
1160 match self.advance() {
1161 Token::LParen => depth += 1,
1162 Token::RParen => {
1163 depth -= 1;
1164 if depth == 0 {
1165 break;
1166 }
1167 }
1168 Token::Eof => {
1169 return Err(self.err(alloc::string::String::from(
1170 "unterminated argument list in COMMENT ON",
1171 )));
1172 }
1173 _ => {}
1174 }
1175 }
1176 }
1177 // `IS`
1178 if !matches!(self.peek(), Token::Is) {
1179 self.expect_keyword_ident("is")?;
1180 } else {
1181 self.advance();
1182 }
1183 let comment = match self.peek() {
1184 Token::Null => {
1185 self.advance();
1186 None
1187 }
1188 _ => Some(self.expect_string_literal()?),
1189 };
1190 Ok(Statement::CommentOn {
1191 kind,
1192 name,
1193 comment,
1194 })
1195 }
1196
1197 /// v7.39 (read01 round 57) — `GRANT <privs> ON <obj> TO <roles> [WITH GRANT
1198 /// OPTION]` / `REVOKE [GRANT OPTION FOR] <privs> ON <obj> FROM <roles>
1199 /// [CASCADE|RESTRICT]`.
1200 ///
1201 /// TABLE privileges are the real ones (stored, enforced, introspectable).
1202 /// Every other object class — SCHEMA / SEQUENCE / DATABASE / FUNCTION / …,
1203 /// and the no-ON `GRANT role TO role` membership form — parses into
1204 /// `GrantObject::Other` and no-ops in the engine, so a pg_dump that grants
1205 /// on them still restores.
1206 fn parse_grant_or_revoke(&mut self, grant: bool) -> Result<Statement, ParseError> {
1207 self.advance(); // GRANT / REVOKE
1208 // REVOKE's optional `GRANT OPTION FOR` prefix.
1209 let mut grant_option = false;
1210 if !grant
1211 && self.peek_keyword_ident("grant")
1212 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("option"))
1213 {
1214 self.advance(); // GRANT
1215 self.advance(); // OPTION
1216 self.expect_keyword_ident("for")?;
1217 grant_option = true;
1218 }
1219 // The privilege list: `ALL [PRIVILEGES] [(cols)]`, or comma-separated
1220 // words each with an optional COLUMN list.
1221 let mut privileges: Vec<GrantPriv> = Vec::new();
1222 if matches!(self.peek(), Token::All) {
1223 self.advance();
1224 if self.peek_keyword_ident("privileges") {
1225 self.advance();
1226 }
1227 // `GRANT ALL (col) ON t TO r` — every column privilege, on that
1228 // column only.
1229 if matches!(self.peek(), Token::LParen) {
1230 let columns = self.parse_grant_column_list()?;
1231 privileges.push(GrantPriv {
1232 word: "ALL".into(),
1233 columns,
1234 });
1235 }
1236 } else {
1237 loop {
1238 // SELECT and INSERT lex as reserved tokens, so they never
1239 // reach `expect_ident_like` as plain idents; the rest
1240 // (UPDATE / DELETE / TRUNCATE / REFERENCES / TRIGGER /
1241 // MAINTAIN) are ordinary identifiers.
1242 let w = match self.peek() {
1243 Token::Select => {
1244 self.advance();
1245 "SELECT".to_string()
1246 }
1247 Token::Insert => {
1248 self.advance();
1249 "INSERT".to_string()
1250 }
1251 // v7.39 (read01 round 60) — CREATE is a privilege word on a
1252 // schema / database, and it lexes as a reserved token.
1253 Token::Create => {
1254 self.advance();
1255 "CREATE".to_string()
1256 }
1257 // NOT upper-cased: in the no-ON shape (`GRANT devs TO
1258 // alice`) these "privilege words" are ROLE NAMES, and a
1259 // role name is case-sensitive. `priv_from_word` folds case
1260 // itself when they really are privileges.
1261 _ => self.expect_ident_like()?,
1262 };
1263 // v7.39 (read01 round 59) — the optional per-privilege COLUMN
1264 // list: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
1265 let columns = if matches!(self.peek(), Token::LParen) {
1266 self.parse_grant_column_list()?
1267 } else {
1268 Vec::new()
1269 };
1270 privileges.push(GrantPriv { word: w, columns });
1271 if matches!(self.peek(), Token::Comma) {
1272 self.advance();
1273 } else {
1274 break;
1275 }
1276 }
1277 }
1278 // v7.39 (read01 round 58) — no ON clause at all = `GRANT devs TO alice`:
1279 // role MEMBERSHIP. The words parsed as "privileges" are the role names.
1280 if !matches!(self.peek(), Token::On) {
1281 let roles: Vec<String> = core::mem::take(&mut privileges)
1282 .into_iter()
1283 .map(|p| p.word)
1284 .collect();
1285 let grantees = self.parse_grantee_list(grant)?;
1286 // `WITH ADMIN OPTION` / `GRANTED BY x` — accepted, ignored (SPG has
1287 // no admin-option layer: a member cannot re-grant).
1288 self.consume_until_statement_boundary();
1289 return Ok(finish_grant(
1290 grant,
1291 GrantStatement {
1292 privileges: Vec::new(),
1293 object: GrantObject::Roles(roles),
1294 grantees,
1295 grant_option,
1296 },
1297 ));
1298 }
1299 self.advance(); // ON
1300 // An optional object-class keyword. `TABLE` (or no keyword at all) is
1301 // the enforced case; anything else parses and no-ops.
1302 let mut class = "TABLE";
1303 match self.peek() {
1304 Token::Table => {
1305 self.advance();
1306 }
1307 Token::All => {
1308 // v7.39 (read01 round 61) — `ALL TABLES IN SCHEMA x` expands to
1309 // every table at GRANT time, like PG. `ALL SEQUENCES/FUNCTIONS
1310 // IN SCHEMA` stay no-ops and keep their own object class.
1311 self.advance(); // ALL
1312 let kind = match self.peek() {
1313 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
1314 // TABLES has its own token (SHOW TABLES owns it).
1315 Token::Tables | Token::Table => "tables".to_string(),
1316 _ => String::new(),
1317 };
1318 if !kind.is_empty() {
1319 self.advance();
1320 }
1321 // `IN SCHEMA <name>`
1322 if matches!(self.peek(), Token::In) {
1323 self.advance();
1324 if self.peek_keyword_ident("schema") {
1325 self.advance();
1326 let _schema = self.expect_ident_like()?;
1327 }
1328 }
1329 if kind != "tables" {
1330 self.consume_until_statement_boundary();
1331 return Ok(finish_grant(
1332 grant,
1333 GrantStatement {
1334 privileges,
1335 object: GrantObject::Other("ALL … IN SCHEMA".into()),
1336 grantees: Vec::new(),
1337 grant_option,
1338 },
1339 ));
1340 }
1341 let grantees = self.parse_grantee_list(grant)?;
1342 self.consume_until_statement_boundary();
1343 return Ok(finish_grant(
1344 grant,
1345 GrantStatement {
1346 privileges,
1347 object: GrantObject::AllTablesInSchema,
1348 grantees,
1349 grant_option,
1350 },
1351 ));
1352 }
1353 Token::Ident(w) | Token::QuotedIdent(w) => {
1354 let lc = w.to_ascii_lowercase();
1355 // v7.39 (read01 round 60) — SEQUENCE / SCHEMA / DATABASE are
1356 // real objects with real ACLs now.
1357 if matches!(lc.as_str(), "sequence" | "schema" | "database") {
1358 self.advance();
1359 let mut names: Vec<String> = Vec::new();
1360 loop {
1361 let mut parts: Vec<String> = Vec::new();
1362 loop {
1363 parts.push(self.expect_ident_like()?);
1364 if matches!(self.peek(), Token::Dot) {
1365 self.advance();
1366 } else {
1367 break;
1368 }
1369 }
1370 names.push(parts.pop().expect("at least one part"));
1371 if matches!(self.peek(), Token::Comma) {
1372 self.advance();
1373 } else {
1374 break;
1375 }
1376 }
1377 let grantees = self.parse_grantee_list(grant)?;
1378 let mut grant_option = grant_option;
1379 if grant && self.peek_keyword_ident("with") {
1380 self.advance();
1381 self.expect_keyword_ident("grant")?;
1382 self.expect_keyword_ident("option")?;
1383 grant_option = true;
1384 }
1385 self.consume_until_statement_boundary();
1386 let object = match lc.as_str() {
1387 "sequence" => GrantObject::Sequences(names),
1388 "schema" => GrantObject::Schemas(names),
1389 _ => GrantObject::Databases(names),
1390 };
1391 return Ok(finish_grant(
1392 grant,
1393 GrantStatement {
1394 privileges,
1395 object,
1396 grantees,
1397 grant_option,
1398 },
1399 ));
1400 }
1401 // v7.39 (read01 round 61) — `ON FUNCTION f(int)` is real. SPG
1402 // keys functions by NAME, so the argument list parses and is
1403 // dropped (an overload set shares one ACL — recorded residual).
1404 if matches!(lc.as_str(), "function" | "procedure" | "routine") {
1405 self.advance();
1406 let mut names: Vec<(String, Option<Vec<String>>)> = Vec::new();
1407 loop {
1408 let mut parts: Vec<String> = Vec::new();
1409 loop {
1410 parts.push(self.expect_ident_like()?);
1411 if matches!(self.peek(), Token::Dot) {
1412 self.advance();
1413 } else {
1414 break;
1415 }
1416 }
1417 let fname = parts.pop().expect("at least one part");
1418 // v7.39 (read01 round 62) — the signature picks the
1419 // overload, so it is captured.
1420 let sig = if matches!(self.peek(), Token::LParen) {
1421 Some(self.parse_function_signature_types()?)
1422 } else {
1423 None
1424 };
1425 names.push((fname, sig));
1426 if matches!(self.peek(), Token::Comma) {
1427 self.advance();
1428 } else {
1429 break;
1430 }
1431 }
1432 let grantees = self.parse_grantee_list(grant)?;
1433 self.consume_until_statement_boundary();
1434 return Ok(finish_grant(
1435 grant,
1436 GrantStatement {
1437 privileges,
1438 object: GrantObject::Functions(names),
1439 grantees,
1440 grant_option,
1441 },
1442 ));
1443 }
1444 if matches!(
1445 lc.as_str(),
1446 "type"
1447 | "domain"
1448 | "language"
1449 | "tablespace"
1450 | "large"
1451 | "foreign"
1452 | "parameter"
1453 ) {
1454 self.consume_until_statement_boundary();
1455 return Ok(finish_grant(
1456 grant,
1457 GrantStatement {
1458 privileges,
1459 object: GrantObject::Other(lc.to_ascii_uppercase()),
1460 grantees: Vec::new(),
1461 grant_option,
1462 },
1463 ));
1464 }
1465 class = "TABLE";
1466 }
1467 _ => {}
1468 }
1469 let _ = class;
1470 // The table list. Schema-qualified names drop their qualifier (SPG is
1471 // single-schema) — but read the dotted parts from raw tokens, since
1472 // `expect_ident_like` would silently swallow the leading part.
1473 let mut tables: Vec<String> = Vec::new();
1474 loop {
1475 let mut parts: Vec<String> = Vec::new();
1476 loop {
1477 parts.push(self.expect_ident_like()?);
1478 if matches!(self.peek(), Token::Dot) {
1479 self.advance();
1480 } else {
1481 break;
1482 }
1483 }
1484 tables.push(parts.pop().expect("at least one part"));
1485 if matches!(self.peek(), Token::Comma) {
1486 self.advance();
1487 } else {
1488 break;
1489 }
1490 }
1491 let grantees = self.parse_grantee_list(grant)?;
1492 if grant && self.peek_keyword_ident("with") {
1493 self.advance();
1494 self.expect_keyword_ident("grant")?;
1495 self.expect_keyword_ident("option")?;
1496 grant_option = true;
1497 }
1498 // REVOKE's trailing CASCADE / RESTRICT — SPG has no dependent grants
1499 // to cascade to (no re-granting), so both are accepted and ignored.
1500 if !grant && (self.peek_keyword_ident("cascade") || self.peek_keyword_ident("restrict")) {
1501 self.advance();
1502 }
1503 Ok(finish_grant(
1504 grant,
1505 GrantStatement {
1506 privileges,
1507 object: GrantObject::Tables(tables),
1508 grantees,
1509 grant_option,
1510 },
1511 ))
1512 }
1513
1514 /// v7.39 (read01 round 62) — the argument TYPES in a function signature:
1515 /// `(int, text)` or `(x int, y text)` (PG accepts either). Returns the type
1516 /// words; the caller normalises them into a signature key.
1517 fn parse_function_signature_types(&mut self) -> Result<Vec<String>, ParseError> {
1518 self.advance(); // (
1519 let mut types: Vec<String> = Vec::new();
1520 if matches!(self.peek(), Token::RParen) {
1521 self.advance();
1522 return Ok(types);
1523 }
1524 loop {
1525 // Collect the words of one argument up to a comma / close paren.
1526 let mut words: Vec<String> = Vec::new();
1527 loop {
1528 match self.peek() {
1529 Token::Comma | Token::RParen | Token::Eof => break,
1530 _ => {}
1531 }
1532 let tok = self.advance();
1533 match tok {
1534 Token::Ident(w) | Token::QuotedIdent(w) => words.push(w),
1535 other => {
1536 if let Some(w) = unreserved_keyword_text(&other) {
1537 words.push(w);
1538 }
1539 }
1540 }
1541 }
1542 // `name TYPE` or a bare `TYPE`. Several of PG's type names are
1543 // themselves several words (`double precision`, `character
1544 // varying`, `timestamp with time zone`), so "two words means the
1545 // first is a parameter name" reads the type off `f(double
1546 // precision)` as `precision`. v7.39 (round 282): recognise the
1547 // multi-word spellings first — a leading word that STARTS one of
1548 // them is part of the type, not a name.
1549 let joined = words.join(" ");
1550 let ty = if words.len() >= 2 && is_multiword_type_phrase(&joined) {
1551 joined
1552 } else if words.len() >= 2 {
1553 words[1..].join(" ")
1554 } else {
1555 words.first().cloned().unwrap_or_default()
1556 };
1557 types.push(ty);
1558 if matches!(self.peek(), Token::Comma) {
1559 self.advance();
1560 } else {
1561 break;
1562 }
1563 }
1564 if matches!(self.peek(), Token::RParen) {
1565 self.advance();
1566 }
1567 Ok(types)
1568 }
1569
1570 /// v7.39 (read01 round 59) — `( col, col, … )` after a privilege word.
1571 fn parse_grant_column_list(&mut self) -> Result<Vec<String>, ParseError> {
1572 self.advance(); // (
1573 let mut cols = Vec::new();
1574 loop {
1575 cols.push(self.expect_ident_like()?);
1576 if matches!(self.peek(), Token::Comma) {
1577 self.advance();
1578 } else {
1579 break;
1580 }
1581 }
1582 if !matches!(self.peek(), Token::RParen) {
1583 return Err(self.err(alloc::format!(
1584 "expected ')' to close the column list, got {:?}",
1585 self.peek()
1586 )));
1587 }
1588 self.advance(); // )
1589 Ok(cols)
1590 }
1591
1592 /// `TO <roles>` (grant) / `FROM <roles>` (revoke). An empty-string entry is
1593 /// PUBLIC.
1594 fn parse_grantee_list(&mut self, grant: bool) -> Result<Vec<String>, ParseError> {
1595 if grant {
1596 if matches!(self.peek(), Token::To) {
1597 self.advance();
1598 } else {
1599 self.expect_keyword_ident("to")?;
1600 }
1601 } else if matches!(self.peek(), Token::From) {
1602 self.advance();
1603 } else {
1604 self.expect_keyword_ident("from")?;
1605 }
1606 let mut grantees: Vec<String> = Vec::new();
1607 loop {
1608 // `GROUP name` is the legacy spelling of a plain role name.
1609 if self.peek_keyword_ident("group") {
1610 self.advance();
1611 }
1612 if self.peek_keyword_ident("public") {
1613 self.advance();
1614 grantees.push(String::new()); // PUBLIC
1615 } else {
1616 grantees.push(self.expect_ident_like()?);
1617 }
1618 if matches!(self.peek(), Token::Comma) {
1619 self.advance();
1620 } else {
1621 break;
1622 }
1623 }
1624 Ok(grantees)
1625 }
1626
1627 /// v7.39 (round 277) — `PREPARE <name> [(type, …)] AS <statement>`.
1628 /// The body keeps its `$N` placeholders; substitution happens at
1629 /// EXECUTE. The declared types are recorded for
1630 /// `pg_prepared_statements.parameter_types` but are not enforced —
1631 /// PG infers when the list is omitted, and SPG resolves the values
1632 /// at substitution time either way.
1633 fn parse_prepare(&mut self) -> Result<Statement, ParseError> {
1634 let start = self.pos;
1635 self.advance(); // PREPARE
1636 // v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'` is 2PC, a
1637 // different statement that happens to share the keyword. PG
1638 // ships with `max_prepared_transactions = 0` and reports it
1639 // this way; SPG has no prepared-transaction registry, so the
1640 // same wording is the accurate answer rather than a dodge.
1641 // Round 277 turned this from a silent no-op into a confusing
1642 // "expected AS in PREPARE" parse error.
1643 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
1644 self.advance();
1645 let gid = match self.advance() {
1646 Token::String(g) => g,
1647 other => {
1648 return Err(self.err(alloc::format!(
1649 "expected a transaction identifier after PREPARE TRANSACTION, got {other:?}"
1650 )));
1651 }
1652 };
1653 return Ok(Statement::PrepareTransaction(gid));
1654 }
1655 let name = self.expect_ident_like()?;
1656 let mut param_types = Vec::new();
1657 if matches!(self.peek(), Token::LParen) {
1658 self.advance();
1659 loop {
1660 let mut ty = self.expect_ident_like()?;
1661 // A parameterised type name (`numeric(10,2)`,
1662 // `varchar(20)`) keeps its argument list in the text.
1663 if matches!(self.peek(), Token::LParen) {
1664 let mut depth = 0usize;
1665 let mut buf = String::from("(");
1666 loop {
1667 match self.advance() {
1668 Token::LParen => {
1669 depth += 1;
1670 if depth > 1 {
1671 buf.push('(');
1672 }
1673 }
1674 Token::RParen => {
1675 depth -= 1;
1676 buf.push(')');
1677 if depth == 0 {
1678 break;
1679 }
1680 }
1681 Token::Comma => buf.push(','),
1682 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
1683 Token::Eof => break,
1684 _ => {}
1685 }
1686 }
1687 ty.push_str(&buf);
1688 }
1689 // r1049 — `PREPARE p(bigint[]) AS …`: the sixth `[]`
1690 // position, same family as the parameter list above.
1691 let array_suffix = self.consume_array_suffix();
1692 ty.push_str(&array_suffix);
1693 param_types.push(ty);
1694 match self.peek() {
1695 Token::Comma => {
1696 self.advance();
1697 }
1698 Token::RParen => {
1699 self.advance();
1700 break;
1701 }
1702 other => {
1703 return Err(self.err(alloc::format!(
1704 "expected ',' or ')' in PREPARE parameter list, got {other:?}"
1705 )));
1706 }
1707 }
1708 }
1709 }
1710 if !matches!(self.peek(), Token::As) {
1711 return Err(self.err(alloc::format!(
1712 "expected AS in PREPARE, got {:?}",
1713 self.peek()
1714 )));
1715 }
1716 self.advance();
1717 let body = self.parse_one_statement()?;
1718 // The Parser holds tokens, not the source text, so the
1719 // statement PG reports in `pg_prepared_statements.statement`
1720 // is rebuilt from the AST rather than sliced from the input.
1721 let _ = start;
1722 let mut source = alloc::format!("PREPARE {}", crate::ast::quote_ident(&name));
1723 if !param_types.is_empty() {
1724 source.push_str(" (");
1725 source.push_str(¶m_types.join(", "));
1726 source.push(')');
1727 }
1728 source.push_str(" AS ");
1729 source.push_str(&alloc::format!("{body}"));
1730 Ok(Statement::Prepare {
1731 name,
1732 param_types,
1733 body: alloc::boxed::Box::new(body),
1734 source,
1735 })
1736 }
1737
1738 /// v7.39 (round 277) — `EXECUTE <name> [(<expr>, …)]`.
1739 fn parse_execute(&mut self) -> Result<Statement, ParseError> {
1740 self.advance(); // EXECUTE
1741 let name = self.expect_ident_like()?;
1742 let mut args = Vec::new();
1743 if matches!(self.peek(), Token::LParen) {
1744 self.advance();
1745 if matches!(self.peek(), Token::RParen) {
1746 self.advance();
1747 } else {
1748 loop {
1749 args.push(self.parse_expr(0)?);
1750 match self.advance() {
1751 Token::Comma => {}
1752 Token::RParen => break,
1753 other => {
1754 return Err(self.err(alloc::format!(
1755 "expected ',' or ')' in EXECUTE arguments, got {other:?}"
1756 )));
1757 }
1758 }
1759 }
1760 }
1761 }
1762 Ok(Statement::Execute { name, args })
1763 }
1764
1765 /// v7.39 (round 277) — `DEALLOCATE {[PREPARE] <name> | ALL}`.
1766 /// v7.39 (round 278) — `CALL <proc>([args])`. There is no
1767 /// procedure catalog yet, so this reports PG's not-found error
1768 /// (with its HINT) rather than pretending the call ran.
1769 /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
1770 /// Bare `DISCARD` is a syntax error in PG; so it is here.
1771 fn parse_discard(&mut self) -> Result<Statement, ParseError> {
1772 self.advance(); // DISCARD
1773 let target = match self.advance() {
1774 Token::All => DiscardTarget::All,
1775 Token::Ident(w) | Token::QuotedIdent(w) => match w.to_ascii_lowercase().as_str() {
1776 "all" => DiscardTarget::All,
1777 "plans" => DiscardTarget::Plans,
1778 "sequences" => DiscardTarget::Sequences,
1779 "temp" | "temporary" => DiscardTarget::Temp,
1780 other => {
1781 return Err(self.err(format!(
1782 "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1783 )));
1784 }
1785 },
1786 other => {
1787 return Err(self.err(format!(
1788 "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1789 )));
1790 }
1791 };
1792 Ok(Statement::Discard(target))
1793 }
1794
1795 /// v7.39 (round 318, V51) — MySQL `KILL [HARD|SOFT] [CONNECTION|QUERY]
1796 /// <expr>`. MariaDB accepts an expression for the id (its own docs use
1797 /// `KILL connection_id()`), and the HARD / SOFT prefixes only pick how
1798 /// aggressively the server interrupts, which SPG does not distinguish.
1799 /// Bare `KILL <id>` means CONNECTION.
1800 fn parse_kill(&mut self) -> Result<Statement, ParseError> {
1801 self.advance(); // KILL
1802 let mut query_only = false;
1803 loop {
1804 // CONNECTION is a reserved keyword token (it also opens
1805 // `CREATE SUBSCRIPTION … CONNECTION '…'`), so it arrives as
1806 // `Token::Connection` rather than a bare ident.
1807 if matches!(self.peek(), Token::Connection) {
1808 self.advance();
1809 break;
1810 }
1811 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
1812 break;
1813 };
1814 match w.to_ascii_lowercase().as_str() {
1815 "hard" | "soft" => {
1816 self.advance();
1817 }
1818 "query" => {
1819 self.advance();
1820 query_only = true;
1821 break;
1822 }
1823 _ => break,
1824 }
1825 }
1826 let id = self.parse_expr(0)?;
1827 Ok(Statement::Kill {
1828 query_only,
1829 id: Box::new(id),
1830 })
1831 }
1832
1833 fn parse_call(&mut self) -> Result<Statement, ParseError> {
1834 self.advance(); // CALL
1835 let name = self.expect_ident_like()?;
1836 self.consume_until_statement_boundary();
1837 Ok(Statement::Call(name))
1838 }
1839
1840 fn parse_deallocate(&mut self) -> Result<Statement, ParseError> {
1841 self.advance(); // DEALLOCATE
1842 // PG accepts an optional noise `PREPARE` keyword here.
1843 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("prepare")) {
1844 self.advance();
1845 }
1846 if matches!(self.peek(), Token::All) {
1847 self.advance();
1848 return Ok(Statement::Deallocate(None));
1849 }
1850 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("all")) {
1851 self.advance();
1852 return Ok(Statement::Deallocate(None));
1853 }
1854 let name = self.expect_ident_like()?;
1855 Ok(Statement::Deallocate(Some(name)))
1856 }
1857
1858 fn consume_until_statement_boundary(&mut self) {
1859 loop {
1860 match self.peek() {
1861 Token::Semicolon | Token::Eof => return,
1862 _ => self.advance(),
1863 };
1864 }
1865 }
1866
1867 /// v7.22 (round-13 T2) — consume to the statement boundary like
1868 /// `consume_until_statement_boundary`, but pick out the sequence
1869 /// name on the way: either `SEQUENCE NAME <ident>` (identity
1870 /// columns) or the first string literal (`nextval('<seq>')`).
1871 /// Schema qualifiers and `::regclass` casts are stripped.
1872 fn scan_sequence_name_until_boundary(&mut self) -> Option<String> {
1873 let mut seq: Option<String> = None;
1874 let mut after_sequence_kw = false;
1875 let mut after_name_kw = false;
1876 loop {
1877 match self.peek().clone() {
1878 Token::Semicolon | Token::Eof => break,
1879 Token::Ident(s) | Token::QuotedIdent(s) => {
1880 if after_name_kw && seq.is_none() {
1881 self.advance();
1882 let mut name = s;
1883 // `SEQUENCE NAME public.groups_id_seq` — keep
1884 // the bare name, drop qualifiers.
1885 while matches!(self.peek(), Token::Dot) {
1886 self.advance();
1887 if let Token::Ident(n) | Token::QuotedIdent(n) = self.advance() {
1888 name = n;
1889 }
1890 }
1891 seq = Some(name);
1892 after_name_kw = false;
1893 continue;
1894 }
1895 if after_sequence_kw && s.eq_ignore_ascii_case("name") {
1896 after_name_kw = true;
1897 after_sequence_kw = false;
1898 } else {
1899 after_sequence_kw = s.eq_ignore_ascii_case("sequence");
1900 }
1901 self.advance();
1902 }
1903 Token::String(s) => {
1904 if seq.is_none() {
1905 // `nextval('public.groups_id_seq'::regclass)`
1906 let bare = s
1907 .rsplit_once('.')
1908 .map_or_else(|| s.clone(), |(_, b)| b.to_string());
1909 seq = Some(bare);
1910 }
1911 self.advance();
1912 }
1913 _ => {
1914 after_sequence_kw = false;
1915 after_name_kw = false;
1916 self.advance();
1917 }
1918 }
1919 }
1920 seq
1921 }
1922
1923 /// v7.39 (round 621) — is the next token the keyword `BY`?
1924 ///
1925 /// `pg_get_keywords()` classes `by` as `U` (unreserved), so it is a legal
1926 /// column, table and alias name — and SPG lexed it into a dedicated
1927 /// `Token::By`, which made it unusable as a name ANYWHERE. Of the seven
1928 /// two-letter keywords the lexer knew, this was the only one PG leaves
1929 /// unreserved (`as`, `in`, `on`, `or`, `to` are reserved and `is` is `T`).
1930 ///
1931 /// The token is gone; the three clauses that own the word — GROUP BY,
1932 /// ORDER BY, PARTITION BY — and the handful of other places that expect it
1933 /// ask this instead. Adding it to the unreserved-identifier table was not
1934 /// enough on its own: identifier positions that match the token shape
1935 /// directly (an index's column list, a table alias) never consult that
1936 /// table, so `CREATE INDEX … ON t(by)` and `FROM t AS by` still failed.
1937 /// Not lexing it as a keyword closes the whole class rather than the two
1938 /// positions that happened to be noticed.
1939 fn peek_is_by(&self) -> bool {
1940 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by"))
1941 }
1942
1943 /// v7.39 (round 621) — the optional `CASCADE` / `RESTRICT` trailer a DROP
1944 /// takes. Accepted and dropped: SPG tracks no dependents to cascade to,
1945 /// which is what `DROP TABLE` and `DROP INDEX` have done since v7.14.
1946 fn consume_drop_behaviour(&mut self) {
1947 if matches!(
1948 self.peek(),
1949 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") || s.eq_ignore_ascii_case("restrict")
1950 ) {
1951 self.advance();
1952 }
1953 }
1954
1955 fn expect_ident_like(&mut self) -> Result<String, ParseError> {
1956 let first = match self.advance() {
1957 Token::Ident(s) | Token::QuotedIdent(s) => s,
1958 // v7.37.43-T4 — PG-unreserved keywords are legal identifiers
1959 // per PG's `pg_get_keywords()` classification. SPG tokenizes
1960 // these as named variants for parsing leverage in the
1961 // contexts that own them (`RELEASE SAVEPOINT`, `SHOW name`,
1962 // `BEGIN`, etc.), but they MUST still be usable as table /
1963 // column / alias names in DDL+DML. Sentori migrations like
1964 // 0001_init.sql ship `release TEXT NOT NULL` in the events
1965 // table — the `events.release` column carries the release
1966 // identifier string. Pre-T4 this triggered "expected
1967 // identifier, got Release" and blocked every drop-in user
1968 // whose schema had a column / alias with one of these names.
1969 other if unreserved_keyword_text(&other).is_some() => {
1970 unreserved_keyword_text(&other).unwrap()
1971 }
1972 other => {
1973 return Err(ParseError {
1974 message: format!("expected identifier, got {other:?}"),
1975 token_pos: self.consumed_pos(),
1976 });
1977 }
1978 };
1979 // v7.14.0 — strip optional `<schema>.` prefix. PG dumps
1980 // qualify every name with `public.` (and pg_catalog.* for
1981 // functions); SPG is single-schema so we discard the
1982 // prefix and return only the trailing ident. Same shape
1983 // also handles MySQL `db.tbl` cross-database refs (SPG
1984 // ignores the db part).
1985 if matches!(self.peek(), Token::Dot) {
1986 self.advance();
1987 match self.advance() {
1988 Token::Ident(s) | Token::QuotedIdent(s) => return Ok(s),
1989 other if unreserved_keyword_text(&other).is_some() => {
1990 return Ok(unreserved_keyword_text(&other).unwrap());
1991 }
1992 other => {
1993 return Err(ParseError {
1994 message: format!("expected identifier after '{first}.', got {other:?}"),
1995 token_pos: self.consumed_pos(),
1996 });
1997 }
1998 }
1999 }
2000 Ok(first)
2001 }
2002
2003 #[allow(clippy::too_many_lines)]
2004 fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
2005 // v7.14.0 — empty / comment-only / semicolon-only input
2006 // (after the lexer strips line + block + MySQL
2007 // conditional comments) lands as Statement::Empty.
2008 // pg_dump and mysqldump emit several wrappers that
2009 // collapse to nothing after stripping (`/*!40101 SET …
2010 // */;`, blank lines between statements); the engine
2011 // returns CommandOk no-op so the dump loads cleanly.
2012 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
2013 return Ok(Statement::Empty);
2014 }
2015 // v7.14.0 — pg_dump / mysqldump "noise" statements:
2016 // catalog / metadata DDL that has no behavioural effect
2017 // on SPG's single-schema, single-database, single-user
2018 // model. Consume the whole statement up to the next
2019 // semicolon / EOF and return Empty. This is broader than
2020 // the per-keyword DROP / SET / COMMENT arms but lets the
2021 // long tail of `LOCK TABLES`, `UNLOCK TABLES`, `GRANT`,
2022 // `REVOKE`, `ALTER OWNER TO`, `\restrict`, `\unrestrict`,
2023 // `BEGIN; COMMIT;` wrappers, etc. all pass through.
2024 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
2025 let lc = s.to_ascii_lowercase();
2026 // v7.39 (read01 round 50) — COMMENT ON is a real statement now.
2027 if lc == "comment" {
2028 return self.parse_comment_on();
2029 }
2030 // v7.39 (read01 round 57) — so is GRANT / REVOKE.
2031 if lc == "grant" || lc == "revoke" {
2032 return self.parse_grant_or_revoke(lc == "grant");
2033 }
2034 // v7.39 (round 277) — the SQL-level prepared-statement
2035 // surface is REAL now. It used to be accepted and dropped
2036 // on the theory that "real execution still happens via the
2037 // extended-query flow" — true only for a driver that uses
2038 // that flow; a plain SQL PREPARE / EXECUTE returned no
2039 // rows at all.
2040 if lc == "prepare" {
2041 return self.parse_prepare();
2042 }
2043 if lc == "execute" {
2044 return self.parse_execute();
2045 }
2046 if lc == "deallocate" {
2047 return self.parse_deallocate();
2048 }
2049 // v7.39 (round 278) — `CALL <proc>(<args>)` used to be
2050 // accepted and dropped, so an application's stored-procedure
2051 // invocation reported success and did nothing. SPG has no
2052 // procedure catalog, so every CALL names a procedure that
2053 // does not exist — which is exactly what PG says.
2054 if lc == "call" {
2055 return self.parse_call();
2056 }
2057 // v7.39 (round 318, V51) — MySQL `KILL`. Not dump noise: it
2058 // names one connection and acts on it.
2059 if lc == "kill" {
2060 return self.parse_kill();
2061 }
2062 if lc == "discard" {
2063 return self.parse_discard();
2064 }
2065 // v7.39 (round 696) — REASSIGN OWNED BY <role> [, …] TO <role>.
2066 // Still performs nothing; the roles are carried out so a name
2067 // that does not exist is refused, as PG18 refuses it.
2068 if lc == "reassign" {
2069 self.advance();
2070 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("owned")) {
2071 self.advance();
2072 }
2073 if self.peek_is_by() {
2074 self.advance();
2075 }
2076 // Only the roles BEFORE the TO are the ones that must
2077 // exist — `TO` names the new owner, which PG checks as
2078 // well, so both lists are collected.
2079 let mut names = self.take_comma_separated_names();
2080 if matches!(self.peek(), Token::To) {
2081 self.advance();
2082 names.extend(self.take_comma_separated_names());
2083 }
2084 self.consume_until_statement_boundary();
2085 return Ok(Statement::ValidateOnly {
2086 kind: crate::ast::ValidateOnlyKind::RoleName,
2087 names,
2088 });
2089 }
2090 // v7.39 (round 696) — SECURITY LABEL. PG18 refuses it
2091 // unconditionally with `no security label providers have been
2092 // loaded`, whatever object it names, because none is loaded.
2093 // SPG has none either; accepting it told the caller a label had
2094 // been applied when nothing anywhere records one.
2095 if lc == "security" {
2096 self.consume_until_statement_boundary();
2097 return Ok(Statement::ValidateOnly {
2098 kind: crate::ast::ValidateOnlyKind::SecurityLabel,
2099 names: Vec::new(),
2100 });
2101 }
2102 if is_dump_noise_statement(&lc) {
2103 self.consume_until_statement_boundary();
2104 return Ok(Statement::Empty);
2105 }
2106 }
2107 match self.peek() {
2108 Token::Select => self.parse_select_stmt(),
2109 // v7.37.17 (17.6 siblings) — a statement opening with a
2110 // parenthesized query group: `(SELECT … UNION …)
2111 // INTERSECT …`. parse_bare_select's group arm consumes
2112 // the parens; the select parser handles the outer chain
2113 // and tail.
2114 Token::LParen
2115 if matches!(
2116 self.tokens.get(self.pos + 1),
2117 Some(Token::Select | Token::LParen | Token::Values)
2118 ) =>
2119 {
2120 self.parse_select_stmt()
2121 }
2122 // v7.37.17 (17.6 siblings) — top-level bare VALUES
2123 // statement (`VALUES (1), (2) [ORDER BY …] [LIMIT …]`).
2124 // Lowers to the same UNION ALL chain the FROM-position
2125 // form uses, then reuses the shared SELECT tail.
2126 Token::Values => {
2127 self.advance(); // VALUES
2128 let mut head = self.parse_values_rows_body()?;
2129 self.parse_select_tail_into(&mut head)?;
2130 Ok(Statement::Select(head))
2131 }
2132 // SQL-standard `TABLE name` shorthand for
2133 // `SELECT * FROM name` — pg_dump never emits it, but
2134 // psql users and PG docs use it constantly. Set-op
2135 // chains and the ORDER BY/LIMIT tail compose like any
2136 // SELECT head.
2137 Token::Table
2138 if matches!(
2139 self.tokens.get(self.pos + 1),
2140 Some(Token::Ident(_) | Token::QuotedIdent(_))
2141 ) =>
2142 {
2143 let mut head = self.parse_table_shorthand()?;
2144 self.parse_setop_chain_into(&mut head)?;
2145 self.parse_select_tail_into(&mut head)?;
2146 Ok(Statement::Select(head))
2147 }
2148 // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2149 // body is a dollar-quoted plpgsql block (lexer already
2150 // collapsed `$$…$$` into a single Token::String).
2151 // v7.16.2 — mailrs round-10 A.2: parse the body as a
2152 // real PlPgSqlBlock so the engine can EXECUTE it at
2153 // top level instead of silently swallowing. Pre-
2154 // v7.16.2 the parser threw the body away and the
2155 // engine returned CommandOk for the entire DO; that
2156 // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2157 // $$` into a SEV-1 silent no-op (the IF + the rename
2158 // were both invisible — mailrs's migrate-042 didn't
2159 // actually run). Now the body parses + executes;
2160 // EmbeddedSql inside the block runs immediately
2161 // against the engine (not deferred — we're at top
2162 // level, not inside a trigger row-write loop).
2163 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2164 self.advance();
2165 let body_text = match self.advance() {
2166 Token::String(s) => s,
2167 other => {
2168 return Err(self.err(alloc::format!(
2169 "expected dollar-quoted body after DO, got {other:?}"
2170 )));
2171 }
2172 };
2173 // Optional `LANGUAGE <name>` trailer (idents only).
2174 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2175 self.advance();
2176 let _ = self.expect_ident_like()?;
2177 }
2178 // Parse the body — same shape CREATE FUNCTION
2179 // uses for trigger function bodies. If the body
2180 // doesn't parse cleanly we surface the error
2181 // (better than silent no-op).
2182 let block = parse_plpgsql_body(&body_text)?;
2183 Ok(Statement::DoBlock(block))
2184 }
2185 // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2186 // WITH isn't a reserved token in our lexer — comes through
2187 // as `Token::Ident("with")` (case-insensitive).
2188 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2189 self.advance();
2190 self.parse_with_cte_then_select()
2191 }
2192 // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2193 // an identifier — not a reserved keyword.
2194 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2195 self.advance();
2196 let mut analyze = false;
2197 let mut suggest = false;
2198 let mut costs_off = false;
2199 let mut buffers = false;
2200 let mut timing_off = false;
2201 let mut settings = false;
2202 let mut wal = false;
2203 let mut summary_off = false;
2204 let mut format = crate::ast::ExplainFormat::Text;
2205 // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2206 // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2207 // options are comma-separated. Booleans default to ON
2208 // when the value token is omitted (matches PG).
2209 if matches!(self.peek(), Token::LParen) {
2210 self.advance();
2211 loop {
2212 let opt = match self.peek().clone() {
2213 Token::Ident(s) | Token::QuotedIdent(s) => s,
2214 other => {
2215 return Err(self.err(format!(
2216 "expected option keyword inside EXPLAIN (…), got {other:?}"
2217 )));
2218 }
2219 };
2220 self.advance();
2221 if opt.eq_ignore_ascii_case("suggest") {
2222 suggest = true;
2223 // SUGGEST takes no explicit value today.
2224 } else if opt.eq_ignore_ascii_case("costs") {
2225 // PG syntax: `COSTS [ON | OFF]`. Default
2226 // when value omitted is ON, so plain
2227 // `COSTS` is a no-op. `COSTS OFF` flips.
2228 // `ON` lexes to `Token::On` (reserved
2229 // keyword in JOIN ... ON contexts); accept
2230 // it alongside the bare Ident form so the
2231 // grammar matches PG verbatim.
2232 let value = match self.peek().clone() {
2233 Token::On => {
2234 self.advance();
2235 true
2236 }
2237 Token::Ident(v) | Token::QuotedIdent(v)
2238 if v.eq_ignore_ascii_case("off") =>
2239 {
2240 self.advance();
2241 false
2242 }
2243 Token::Ident(v) | Token::QuotedIdent(v)
2244 if v.eq_ignore_ascii_case("true") =>
2245 {
2246 self.advance();
2247 true
2248 }
2249 _ => true,
2250 };
2251 costs_off = !value;
2252 } else if opt.eq_ignore_ascii_case("analyze")
2253 || opt.eq_ignore_ascii_case("analyse")
2254 {
2255 // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2256 // Same default-ON rule as ANALYZE keyword form.
2257 let value = match self.peek().clone() {
2258 Token::On => {
2259 self.advance();
2260 true
2261 }
2262 Token::Ident(v) | Token::QuotedIdent(v)
2263 if v.eq_ignore_ascii_case("off") =>
2264 {
2265 self.advance();
2266 false
2267 }
2268 Token::Ident(v) | Token::QuotedIdent(v)
2269 if v.eq_ignore_ascii_case("true") =>
2270 {
2271 self.advance();
2272 true
2273 }
2274 _ => true,
2275 };
2276 analyze = value;
2277 } else if opt.eq_ignore_ascii_case("buffers") {
2278 // v7.37.22 — `BUFFERS [ON|OFF]`.
2279 let value = match self.peek().clone() {
2280 Token::On => {
2281 self.advance();
2282 true
2283 }
2284 Token::Ident(v) | Token::QuotedIdent(v)
2285 if v.eq_ignore_ascii_case("off") =>
2286 {
2287 self.advance();
2288 false
2289 }
2290 Token::Ident(v) | Token::QuotedIdent(v)
2291 if v.eq_ignore_ascii_case("true") =>
2292 {
2293 self.advance();
2294 true
2295 }
2296 _ => true,
2297 };
2298 buffers = value;
2299 } else if opt.eq_ignore_ascii_case("timing") {
2300 // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2301 // the measured wall-clock annotation.
2302 let value = match self.peek().clone() {
2303 Token::On => {
2304 self.advance();
2305 true
2306 }
2307 Token::Ident(v) | Token::QuotedIdent(v)
2308 if v.eq_ignore_ascii_case("off") =>
2309 {
2310 self.advance();
2311 false
2312 }
2313 Token::Ident(v) | Token::QuotedIdent(v)
2314 if v.eq_ignore_ascii_case("true") =>
2315 {
2316 self.advance();
2317 true
2318 }
2319 _ => true,
2320 };
2321 timing_off = !value;
2322 } else if opt.eq_ignore_ascii_case("settings") {
2323 settings = true;
2324 } else if opt.eq_ignore_ascii_case("wal") {
2325 wal = true;
2326 } else if opt.eq_ignore_ascii_case("summary") {
2327 // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2328 // gates the trailing Planning/Execution Time
2329 // lines now (was accept-and-no-op).
2330 let value = match self.peek().clone() {
2331 Token::On => {
2332 self.advance();
2333 true
2334 }
2335 Token::Ident(v) | Token::QuotedIdent(v)
2336 if v.eq_ignore_ascii_case("off") =>
2337 {
2338 self.advance();
2339 false
2340 }
2341 Token::Ident(v) | Token::QuotedIdent(v)
2342 if v.eq_ignore_ascii_case("true") =>
2343 {
2344 self.advance();
2345 true
2346 }
2347 _ => true,
2348 };
2349 summary_off = !value;
2350 } else if opt.eq_ignore_ascii_case("verbose")
2351 || opt.eq_ignore_ascii_case("format")
2352 {
2353 // v7.37.22 — accept-but-no-op the remaining
2354 // PG options so EXPLAIN-using clients
2355 // (pgAdmin / DataGrip) don't see syntax
2356 // errors. FORMAT takes a value (text /
2357 // json / yaml / xml); skip the next token
2358 // if it's an ident.
2359 if opt.eq_ignore_ascii_case("format") {
2360 if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2361 {
2362 self.advance();
2363 format = match v.to_ascii_lowercase().as_str() {
2364 "text" => crate::ast::ExplainFormat::Text,
2365 "json" => crate::ast::ExplainFormat::Json,
2366 "xml" => crate::ast::ExplainFormat::Xml,
2367 "yaml" => crate::ast::ExplainFormat::Yaml,
2368 other => {
2369 return Err(self.err(format!(
2370 "EXPLAIN (FORMAT …): unknown format {other:?}; \
2371 supports text, json, xml, yaml"
2372 )));
2373 }
2374 };
2375 }
2376 } else {
2377 // VERBOSE / SUMMARY take optional ON/OFF;
2378 // consume if present.
2379 if matches!(self.peek(), Token::On) {
2380 self.advance();
2381 } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2382 self.peek().clone()
2383 && (v.eq_ignore_ascii_case("off")
2384 || v.eq_ignore_ascii_case("true"))
2385 {
2386 self.advance();
2387 let _ = v;
2388 }
2389 }
2390 } else {
2391 return Err(self.err(format!(
2392 "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2393 )));
2394 }
2395 if matches!(self.peek(), Token::Comma) {
2396 self.advance();
2397 continue;
2398 }
2399 break;
2400 }
2401 if !matches!(self.peek(), Token::RParen) {
2402 return Err(self.err(format!(
2403 "expected ')' after EXPLAIN options, got {:?}",
2404 self.peek()
2405 )));
2406 }
2407 self.advance();
2408 } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2409 && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2410 {
2411 self.advance();
2412 analyze = true;
2413 }
2414 // v7.39 (round 224) — the body may open with WITH (CTEs);
2415 // route through the same CTE-then-SELECT path the top-level
2416 // WITH statement uses. v7.39 (round 225) — DML bodies parse
2417 // too (PG explains INSERT / UPDATE / DELETE).
2418 let inner = match self.peek().clone() {
2419 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2420 self.advance();
2421 self.parse_with_cte_then_select()?
2422 }
2423 Token::Insert => self.parse_insert_stmt(false)?,
2424 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2425 self.advance();
2426 self.parse_update_after_keyword()?
2427 }
2428 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2429 self.advance();
2430 self.parse_delete_after_keyword()?
2431 }
2432 _ => self.parse_select_stmt()?,
2433 };
2434 if !matches!(
2435 inner,
2436 Statement::Select(_)
2437 | Statement::Insert(_)
2438 | Statement::Update(_)
2439 | Statement::Delete(_)
2440 ) {
2441 return Err(self.err(format!(
2442 "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2443 )));
2444 }
2445 Ok(Statement::Explain(crate::ast::ExplainStatement {
2446 analyze,
2447 inner: Box::new(inner),
2448 suggest,
2449 costs_off,
2450 buffers,
2451 timing_off,
2452 settings,
2453 wal,
2454 summary_off,
2455 format,
2456 }))
2457 }
2458 Token::Create => self.parse_create_stmt(),
2459 Token::Insert => self.parse_insert_stmt(false),
2460 // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2461 // spelling; route to the same handler. DESC is the
2462 // reserved ORDER BY token, so it gets its own arm.
2463 Token::Ident(s)
2464 if s.eq_ignore_ascii_case("describe")
2465 && matches!(
2466 self.tokens.get(self.pos + 1),
2467 Some(Token::Ident(_) | Token::QuotedIdent(_))
2468 ) =>
2469 {
2470 self.advance();
2471 let table = self.expect_ident_like()?;
2472 Ok(Statement::ShowColumns(table))
2473 }
2474 Token::Desc
2475 if matches!(
2476 self.tokens.get(self.pos + 1),
2477 Some(Token::Ident(_) | Token::QuotedIdent(_))
2478 ) =>
2479 {
2480 self.advance();
2481 let table = self.expect_ident_like()?;
2482 Ok(Statement::ShowColumns(table))
2483 }
2484 // `COPY table [(cols)] TO STDOUT` — the export half of
2485 // pg_dump's COPY pair (the FROM stdin half rides the
2486 // embed import path). Options need a format design and
2487 // error honestly.
2488 Token::Ident(s)
2489 if s.eq_ignore_ascii_case("copy")
2490 && matches!(
2491 self.tokens.get(self.pos + 1),
2492 Some(Token::Ident(_) | Token::QuotedIdent(_))
2493 ) =>
2494 {
2495 self.advance(); // COPY
2496 let table = self.expect_ident_like()?;
2497 let columns = if matches!(self.peek(), Token::LParen) {
2498 self.advance();
2499 let mut cols = alloc::vec![self.expect_ident_like()?];
2500 while matches!(self.peek(), Token::Comma) {
2501 self.advance();
2502 cols.push(self.expect_ident_like()?);
2503 }
2504 if !matches!(self.peek(), Token::RParen) {
2505 return Err(self.err(format!(
2506 "expected ')' after COPY column list, got {:?}",
2507 self.peek()
2508 )));
2509 }
2510 self.advance();
2511 Some(cols)
2512 } else {
2513 None
2514 };
2515 // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2516 // endpoint. (FROM STDIN still rides the wire/import path —
2517 // its data arrives out of band.)
2518 if matches!(self.peek(), Token::From)
2519 && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2520 {
2521 self.advance(); // FROM
2522 let Token::String(path) = self.advance() else {
2523 unreachable!()
2524 };
2525 let options = self.parse_copy_to_options()?;
2526 return Ok(Statement::CopyFromFile {
2527 table,
2528 columns,
2529 path,
2530 options,
2531 });
2532 }
2533 if !matches!(self.peek(), Token::To) {
2534 return Err(self.err(format!(
2535 "COPY: only TO STDOUT is supported here (FROM stdin \
2536 rides the import path); got {:?}",
2537 self.peek()
2538 )));
2539 }
2540 self.advance();
2541 if matches!(self.peek(), Token::String(_)) {
2542 let Token::String(path) = self.advance() else { unreachable!() };
2543 let options = self.parse_copy_to_options()?;
2544 return Ok(Statement::CopyToFile {
2545 table,
2546 columns,
2547 query: None,
2548 path,
2549 options,
2550 });
2551 }
2552 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2553 return Err(self.err(format!(
2554 "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2555 self.peek()
2556 )));
2557 }
2558 self.advance();
2559 let options = self.parse_copy_to_options()?;
2560 Ok(Statement::CopyTo {
2561 table,
2562 columns,
2563 query: None,
2564 options,
2565 })
2566 }
2567 // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2568 // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2569 // result set is streamed in COPY format (PG's query form).
2570 Token::Ident(s)
2571 if s.eq_ignore_ascii_case("copy")
2572 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2573 {
2574 self.advance(); // COPY
2575 self.advance(); // (
2576 let query = self.parse_select_stmt()?;
2577 if !matches!(self.peek(), Token::RParen) {
2578 return Err(self.err(format!(
2579 "expected ')' after COPY query, got {:?}",
2580 self.peek()
2581 )));
2582 }
2583 self.advance(); // )
2584 if !matches!(self.peek(), Token::To) {
2585 return Err(self.err(format!(
2586 "COPY (query): only TO STDOUT is supported, got {:?}",
2587 self.peek()
2588 )));
2589 }
2590 self.advance();
2591 if matches!(self.peek(), Token::String(_)) {
2592 let Token::String(path) = self.advance() else { unreachable!() };
2593 let options = self.parse_copy_to_options()?;
2594 return Ok(Statement::CopyToFile {
2595 table: String::new(),
2596 columns: None,
2597 query: Some(alloc::boxed::Box::new(query)),
2598 path,
2599 options,
2600 });
2601 }
2602 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2603 return Err(self.err(format!(
2604 "COPY (query): TO supports STDOUT only, got {:?}",
2605 self.peek()
2606 )));
2607 }
2608 self.advance();
2609 let options = self.parse_copy_to_options()?;
2610 Ok(Statement::CopyTo {
2611 table: String::new(),
2612 columns: None,
2613 query: Some(alloc::boxed::Box::new(query)),
2614 options,
2615 })
2616 }
2617 // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2618 // Shares the INSERT body; the replace flag lowers it
2619 // onto ON CONFLICT DO UPDATE with an empty assignment
2620 // list (engine: replace the whole row).
2621 Token::Ident(s)
2622 if s.eq_ignore_ascii_case("replace")
2623 && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2624 {
2625 self.parse_insert_stmt(true)
2626 }
2627 Token::Begin => {
2628 self.advance();
2629 // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2630 // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2631 // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2632 // is consumed first, then the trailing modes — including the
2633 // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2634 // WORK/TRANSACTION). The explicit level, when present, rides the
2635 // statement so `exec_begin` applies it for this transaction.
2636 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2637 {
2638 self.advance();
2639 }
2640 let iso = self.parse_isolation_level_clauses()?;
2641 Ok(Statement::Begin(iso))
2642 }
2643 // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2644 // for BEGIN. START is contextual in PG too; pattern-match
2645 // on the ident here. Iso clauses are parse-and-ignored,
2646 // same as BEGIN above.
2647 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2648 self.advance();
2649 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2650 {
2651 return Err(self.err(alloc::format!(
2652 "expected TRANSACTION after START, got {:?}",
2653 self.peek()
2654 )));
2655 }
2656 self.advance();
2657 let iso = self.parse_isolation_level_clauses()?;
2658 Ok(Statement::Begin(iso))
2659 }
2660 Token::Commit => {
2661 self.advance();
2662 // PG: `COMMIT [WORK | TRANSACTION]`.
2663 if let Token::Ident(w) = self.peek()
2664 && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2665 {
2666 self.advance();
2667 }
2668 Ok(Statement::Commit)
2669 }
2670 // r1066 (7.38 S5.1) — `END [WORK | TRANSACTION]` is PG's
2671 // COMMIT synonym; pgbench's builtin tpcb-like script closes
2672 // every transaction with `END;` and the drop-in aborted on
2673 // it. Only reachable at statement start (CASE … END lives
2674 // inside expressions), so no ambiguity.
2675 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
2676 self.advance();
2677 if let Token::Ident(w) = self.peek()
2678 && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2679 {
2680 self.advance();
2681 }
2682 Ok(Statement::Commit)
2683 }
2684 Token::Rollback => {
2685 self.advance();
2686 // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2687 // savepoint without ending the transaction. Bare
2688 // `ROLLBACK` drops the whole TX.
2689 if matches!(self.peek(), Token::To) {
2690 self.advance();
2691 if matches!(self.peek(), Token::Savepoint) {
2692 self.advance();
2693 }
2694 let name = self.expect_ident_like()?;
2695 Ok(Statement::RollbackToSavepoint(name))
2696 } else {
2697 Ok(Statement::Rollback)
2698 }
2699 }
2700 Token::Savepoint => {
2701 self.advance();
2702 let name = self.expect_ident_like()?;
2703 Ok(Statement::Savepoint(name))
2704 }
2705 Token::Release => {
2706 self.advance();
2707 // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2708 // is optional in standard SQL.
2709 if matches!(self.peek(), Token::Savepoint) {
2710 self.advance();
2711 }
2712 let name = self.expect_ident_like()?;
2713 Ok(Statement::ReleaseSavepoint(name))
2714 }
2715 Token::Show => {
2716 self.advance();
2717 // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2718 // v6.1.2 promoted TABLES to a reserved keyword (for
2719 // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2720 // arrives as `Token::Tables` rather than a bare ident.
2721 // USERS / COLUMNS remain bare idents.
2722 let target = match self.advance() {
2723 Token::Tables => "tables".to_string(),
2724 // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2725 // keyword token; recognise it as the SHOW CREATE
2726 // dispatch keyword too.
2727 Token::Create => "create".to_string(),
2728 // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2729 // keyword too; let SHOW INDEX FROM parse.
2730 Token::Index => "index".to_string(),
2731 // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2732 // reserved (used in aggregate function calls);
2733 // recognise it here so the parser dispatches
2734 // to ShowParameter("all") — the engine returns
2735 // the curated parameter inventory.
2736 Token::All => "all".to_string(),
2737 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2738 other => {
2739 return Err(self.err(format!(
2740 "expected SHOW target, got {other:?}"
2741 )));
2742 }
2743 };
2744 match target.as_str() {
2745 "tables" => Ok(Statement::ShowTables),
2746 "users" => Ok(Statement::ShowUsers),
2747 // v7.38 轴 4 — `SHOW transaction_isolation`
2748 // returns the currently-selected isolation level.
2749 "transaction_isolation" => Ok(Statement::ShowParameter(
2750 "transaction_isolation".to_string(),
2751 )),
2752 // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2753 // TABLE <t>` returns a 2-column row: (Table,
2754 // Create Table). mysqldump emits this for every
2755 // table at scrape time; without it the dump
2756 // round-trip stalls.
2757 // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2758 // FROM <t>` (also spelled `SHOW INDEX` and
2759 // `SHOW KEYS`). admin / mysqldump probes use
2760 // it to list per-table indexes.
2761 "indexes" | "index" | "keys" => {
2762 if !matches!(self.peek(), Token::From) {
2763 return Err(self.err(format!(
2764 "expected FROM after SHOW INDEXES, got {:?}",
2765 self.peek()
2766 )));
2767 }
2768 self.advance();
2769 let table = self.expect_ident_like()?;
2770 Ok(Statement::ShowIndexes(table))
2771 }
2772 // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2773 // `SHOW VARIABLES`. Both return a 2-column row
2774 // set listing server-side state; clients probe
2775 // them at connect time.
2776 "status" => Ok(Statement::ShowStatus),
2777 "variables" => {
2778 // r1067 — `SHOW VARIABLES LIKE 'pat'`.
2779 if matches!(self.peek(), Token::Like) {
2780 self.advance();
2781 let pat = match self.advance() {
2782 Token::String(p) => p,
2783 other => {
2784 return Err(self.err(format!(
2785 "SHOW VARIABLES LIKE expects a quoted pattern, got {other:?}"
2786 )));
2787 }
2788 };
2789 return Ok(Statement::ShowVariablesLike(pat));
2790 }
2791 Ok(Statement::ShowVariables)
2792 }
2793 // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2794 "processlist" => Ok(Statement::ShowProcesslist),
2795 "create" => {
2796 // SHOW CREATE TABLE / VIEW / DATABASE — only
2797 // TABLE is supported in v7.17.
2798 let kind = match self.advance() {
2799 Token::Ident(s) | Token::QuotedIdent(s) => s,
2800 Token::Table => "table".to_string(),
2801 other => {
2802 return Err(self.err(format!(
2803 "expected TABLE after SHOW CREATE, got {other:?}"
2804 )));
2805 }
2806 };
2807 if !kind.eq_ignore_ascii_case("table") {
2808 return Err(self.err(format!(
2809 "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2810 )));
2811 }
2812 let name = self.expect_ident_like()?;
2813 Ok(Statement::ShowCreateTable(name))
2814 }
2815 // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2816 // (and `SHOW SCHEMAS` alias). The mysql client uses
2817 // it to populate the database selector at connect
2818 // time; without it `mysql -p` errors before the
2819 // first user query.
2820 "databases" | "schemas" => Ok(Statement::ShowDatabases),
2821 // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2822 // keyword on its own; it lands here as a bare
2823 // ident. Returning all publications + their
2824 // scope summary.
2825 "publications" => Ok(Statement::ShowPublications),
2826 // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2827 "subscriptions" => Ok(Statement::ShowSubscriptions),
2828 "columns" => {
2829 if !matches!(self.peek(), Token::From) {
2830 return Err(self.err(format!(
2831 "expected FROM after SHOW COLUMNS, got {:?}",
2832 self.peek()
2833 )));
2834 }
2835 self.advance();
2836 let table = self.expect_ident_like()?;
2837 Ok(Statement::ShowColumns(table))
2838 }
2839 // v7.38 轴 4 surface — `SHOW <param>` for any
2840 // remaining session / preset parameter name
2841 // (server_version, search_path, client_encoding,
2842 // …). The engine's ShowParameter handler does the
2843 // dispatch; unrecognised names error there with
2844 // a pointer to pg_settings, not at parse time —
2845 // so a driver that issues `SHOW spam_setting`
2846 // gets a clear runtime error instead of a
2847 // confusing "unknown SHOW target".
2848 other => {
2849 // v7.38 (read01 P3.20) — a custom namespaced GUC
2850 // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
2851 // consume the dotted tail so it round-trips with
2852 // `SET app.foo` / `current_setting('app.foo')`.
2853 let mut full = other.to_string();
2854 while matches!(self.peek(), Token::Dot) {
2855 self.advance();
2856 let seg = self.expect_ident_like()?;
2857 full.push('.');
2858 full.push_str(&seg.to_ascii_lowercase());
2859 }
2860 Ok(Statement::ShowParameter(full))
2861 }
2862 }
2863 }
2864 // v6.1.2: `DROP` is now a reserved keyword (it dispatches
2865 // to DROP USER and DROP PUBLICATION today; DROP TABLE /
2866 // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
2867 // arrived as a bare ident; tokenising it dedicatedly
2868 // keeps the dispatch tree small.
2869 Token::Drop => {
2870 self.advance();
2871 match self.peek() {
2872 // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
2873 // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
2874 // around DROP ROLE cleanup. SPG has no role-owner
2875 // model, so consume to boundary as a no-op.
2876 Token::Ident(s) | Token::QuotedIdent(s)
2877 if s.eq_ignore_ascii_case("owned") =>
2878 {
2879 // v7.39 (round 696) — still a no-op (SPG has no
2880 // role-owner model), but the ROLE is carried out so
2881 // the engine can refuse one that does not exist,
2882 // which is what PG18 does.
2883 self.advance();
2884 if self.peek_is_by() {
2885 self.advance();
2886 }
2887 let names = self.take_comma_separated_names();
2888 self.consume_until_statement_boundary();
2889 Ok(Statement::ValidateOnly {
2890 kind: crate::ast::ValidateOnlyKind::RoleName,
2891 names,
2892 })
2893 }
2894 // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
2895 // It drops only a TEMPORARY table, and name resolution
2896 // already prefers the session's own, so the keyword is
2897 // consumed and the ordinary DROP TABLE path runs.
2898 Token::Ident(s) | Token::QuotedIdent(s)
2899 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
2900 {
2901 self.advance();
2902 if !matches!(self.peek(), Token::Table) {
2903 return Err(self.err(alloc::format!(
2904 "expected TABLE after DROP TEMPORARY, got {:?}",
2905 self.peek()
2906 )));
2907 }
2908 self.parse_drop_table_after_keyword()
2909 }
2910 Token::Publication => {
2911 self.advance();
2912 // v7.39 (round 754, F31-B4) — the round-753
2913 // audit probe tripped over the missing
2914 // `IF EXISTS` here (syntax error).
2915 let if_exists = self.consume_if_exists();
2916 let name = self.expect_ident_or_string()?;
2917 Ok(Statement::DropPublication { name, if_exists })
2918 }
2919 Token::Subscription => {
2920 self.advance();
2921 let if_exists = self.consume_if_exists();
2922 let name = self.expect_ident_or_string()?;
2923 Ok(Statement::DropSubscription { name, if_exists })
2924 }
2925 Token::Ident(s) | Token::QuotedIdent(s)
2926 if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
2927 {
2928 self.advance();
2929 // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
2930 // login user IS a role in PG, and SPG's store holds
2931 // both. `IF EXISTS` is accepted on either spelling.
2932 let if_exists = self.consume_if_exists();
2933 let name = self.expect_ident_or_string()?;
2934 Ok(Statement::DropUser { name, if_exists })
2935 }
2936 // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
2937 // CREATE DATABASE has parsed since v7.14 and this did
2938 // not, so `DROP DATABASE IF EXISTS x` — what every
2939 // teardown script and pg_dumpall preamble opens with —
2940 // came back as a syntax error, which IF EXISTS cannot
2941 // soften. The name is carried so the engine can answer
2942 // the way PG does; PG never lets this succeed on a
2943 // single-database server, since the name is either
2944 // unknown ("database … does not exist", or a notice
2945 // under IF EXISTS) or the one you are connected to
2946 // ("cannot drop the currently open database").
2947 Token::Ident(s) | Token::QuotedIdent(s)
2948 if s.eq_ignore_ascii_case("database") =>
2949 {
2950 self.advance();
2951 let if_exists = self.consume_if_exists();
2952 let name = self.expect_ident_or_string()?;
2953 self.consume_until_statement_boundary();
2954 Ok(Statement::DropDatabase { name, if_exists })
2955 }
2956 // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
2957 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
2958 self.advance();
2959 let if_exists = self.consume_if_exists();
2960 let name = self.expect_ident_like()?;
2961 // ON <table>
2962 if !matches!(self.peek(), Token::On) {
2963 return Err(self.err(alloc::format!(
2964 "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
2965 self.peek()
2966 )));
2967 }
2968 self.advance();
2969 let table = self.expect_ident_like()?;
2970 Ok(Statement::DropTrigger {
2971 name,
2972 table,
2973 if_exists,
2974 })
2975 }
2976 // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
2977 // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
2978 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
2979 self.advance();
2980 let if_exists = self.consume_if_exists();
2981 let name = self.expect_ident_like()?;
2982 if !matches!(self.peek(), Token::On) {
2983 return Err(self.err(alloc::format!(
2984 "expected ON <table> after DROP RULE {name:?}, got {:?}",
2985 self.peek()
2986 )));
2987 }
2988 self.advance();
2989 let table = self.expect_ident_like()?;
2990 // Optional CASCADE / RESTRICT — accepted, no effect.
2991 self.consume_until_statement_boundary();
2992 Ok(Statement::DropRule {
2993 name,
2994 table,
2995 if_exists,
2996 })
2997 }
2998 // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
2999 // v7.12.4 ignores any optional arg-list (signature-
3000 // based overload disambiguation lands in v7.12.5+).
3001 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
3002 self.advance();
3003 let if_exists = self.consume_if_exists();
3004 let name = self.expect_ident_like()?;
3005 // v7.39 (read01 round 62) — the argument list identifies
3006 // WHICH overload to drop, so it is captured, not
3007 // discarded. `DROP FUNCTION f` (no list) is legal when
3008 // the name is unambiguous; the engine enforces that.
3009 let args = if matches!(self.peek(), Token::LParen) {
3010 Some(self.parse_function_signature_types()?)
3011 } else {
3012 None
3013 };
3014 // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
3015 // trailer, which `DROP TABLE` and `DROP INDEX` have
3016 // accepted since v7.14 and this one refused outright.
3017 // pg_dump writes it, so refusing was a parse error in
3018 // the middle of a restore. SPG drops the function
3019 // either way — it tracks no dependents to cascade to —
3020 // which is the same reading the other two give it.
3021 self.consume_drop_behaviour();
3022 Ok(Statement::DropFunction {
3023 name,
3024 args,
3025 if_exists,
3026 })
3027 }
3028 // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
3029 // [CASCADE|RESTRICT]. pg_dump and mysqldump both
3030 // emit DROP TABLE IF EXISTS at the head of every
3031 // CREATE TABLE block so re-importing a dump
3032 // overwrites prior state. SPG accepts and removes
3033 // matching tables; CASCADE/RESTRICT trailers
3034 // accepted silently.
3035 Token::Table => self.parse_drop_table_after_keyword(),
3036 // v7.14.0 — DROP INDEX [IF EXISTS] name
3037 // [CASCADE|RESTRICT]. PG / mysqldump emit this
3038 // for partial-index renames and pgvector
3039 // migrations. SPG removes the matching index;
3040 // IF EXISTS makes the drop idempotent.
3041 Token::Index => {
3042 self.advance();
3043 let if_exists = self.consume_if_exists();
3044 let name = self.expect_ident_like()?;
3045 if matches!(
3046 self.peek(),
3047 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3048 || s.eq_ignore_ascii_case("restrict")
3049 ) {
3050 self.advance();
3051 }
3052 Ok(Statement::DropIndex { name, if_exists })
3053 }
3054 // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3055 // [CASCADE|RESTRICT]. SPG is single-database;
3056 // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3057 // name [, name…] [CASCADE | RESTRICT]. Real
3058 // unregister (was silent no-op pre-v7.17).
3059 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3060 self.advance();
3061 let if_exists = self.consume_if_exists();
3062 let mut names = vec![self.expect_ident_like()?];
3063 while matches!(self.peek(), Token::Comma) {
3064 self.advance();
3065 names.push(self.expect_ident_like()?);
3066 }
3067 if matches!(
3068 self.peek(),
3069 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3070 || s.eq_ignore_ascii_case("restrict")
3071 ) {
3072 self.advance();
3073 }
3074 Ok(Statement::DropSchema { names, if_exists })
3075 }
3076 // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3077 // name [, name…] [CASCADE|RESTRICT].
3078 Token::Ident(s) | Token::QuotedIdent(s)
3079 if s.eq_ignore_ascii_case("type") =>
3080 {
3081 self.advance();
3082 let if_exists = self.consume_if_exists();
3083 let mut names = vec![self.expect_ident_like()?];
3084 while matches!(self.peek(), Token::Comma) {
3085 self.advance();
3086 names.push(self.expect_ident_like()?);
3087 }
3088 if matches!(
3089 self.peek(),
3090 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3091 || s.eq_ignore_ascii_case("restrict")
3092 ) {
3093 self.advance();
3094 }
3095 Ok(Statement::DropType { names, if_exists })
3096 }
3097 // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3098 // name [, name…] [CASCADE|RESTRICT].
3099 Token::Ident(s) | Token::QuotedIdent(s)
3100 if s.eq_ignore_ascii_case("domain") =>
3101 {
3102 self.advance();
3103 let if_exists = self.consume_if_exists();
3104 let mut names = vec![self.expect_ident_like()?];
3105 while matches!(self.peek(), Token::Comma) {
3106 self.advance();
3107 names.push(self.expect_ident_like()?);
3108 }
3109 if matches!(
3110 self.peek(),
3111 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3112 || s.eq_ignore_ascii_case("restrict")
3113 ) {
3114 self.advance();
3115 }
3116 Ok(Statement::DropDomain { names, if_exists })
3117 }
3118 // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3119 // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3120 Token::Ident(s) | Token::QuotedIdent(s)
3121 if s.eq_ignore_ascii_case("materialized") =>
3122 {
3123 self.advance();
3124 let nxt = self.peek().clone();
3125 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3126 {
3127 return Err(self.err(alloc::format!(
3128 "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3129 )));
3130 }
3131 self.advance();
3132 let if_exists = self.consume_if_exists();
3133 let mut names = vec![self.expect_ident_like()?];
3134 while matches!(self.peek(), Token::Comma) {
3135 self.advance();
3136 names.push(self.expect_ident_like()?);
3137 }
3138 if matches!(
3139 self.peek(),
3140 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3141 || s.eq_ignore_ascii_case("restrict")
3142 ) {
3143 self.advance();
3144 }
3145 Ok(Statement::DropMaterializedView { names, if_exists })
3146 }
3147 // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3148 // name [, name…] [CASCADE|RESTRICT].
3149 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3150 self.advance();
3151 let if_exists = self.consume_if_exists();
3152 let mut names = vec![self.expect_ident_like()?];
3153 while matches!(self.peek(), Token::Comma) {
3154 self.advance();
3155 names.push(self.expect_ident_like()?);
3156 }
3157 if matches!(
3158 self.peek(),
3159 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3160 || s.eq_ignore_ascii_case("restrict")
3161 ) {
3162 self.advance();
3163 }
3164 Ok(Statement::DropView { names, if_exists })
3165 }
3166 // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3167 // [CASCADE|RESTRICT]. Real removal from catalog
3168 // (was a silent no-op pre-v7.17).
3169 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3170 self.advance();
3171 let if_exists = self.consume_if_exists();
3172 let mut names = vec![self.expect_ident_like()?];
3173 while matches!(self.peek(), Token::Comma) {
3174 self.advance();
3175 names.push(self.expect_ident_like()?);
3176 }
3177 if matches!(
3178 self.peek(),
3179 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3180 || s.eq_ignore_ascii_case("restrict")
3181 ) {
3182 self.advance();
3183 }
3184 Ok(Statement::DropSequence { names, if_exists })
3185 }
3186 // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3187 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3188 self.advance();
3189 self.parse_drop_policy_after_keyword()
3190 }
3191 // v7.37.17 (17.6 siblings) — DROP <target> for
3192 // targets SPG doesn't natively track. pg_dump
3193 // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3194 // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3195 // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3196 // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3197 // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3198 // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3199 // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3200 // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3201 // etc. — accept + Empty-return so pg_dump tails
3202 // load through. Materialized-view drop dispatches
3203 // to the existing DropTable path when the token
3204 // is Materialized-View-shaped (elsewhere in
3205 // this parser).
3206 Token::Ident(s) | Token::QuotedIdent(s)
3207 if s.eq_ignore_ascii_case("text")
3208 // The DROP dispatch matches on PEEK — `text` is
3209 // not yet consumed, so SEARCH/CONFIGURATION sit
3210 // at pos+1/pos+2 (the round-695 trap's mirror).
3211 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3212 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3213 {
3214 // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3215 // validates the name; DICTIONARY / PARSER / TEMPLATE
3216 // stay in the noise arm below.
3217 self.advance(); // TEXT
3218 self.advance(); // SEARCH
3219 self.advance(); // CONFIGURATION
3220 let if_exists = self.consume_if_exists();
3221 let names = self.take_comma_separated_names();
3222 self.consume_until_statement_boundary();
3223 if if_exists {
3224 return Ok(Statement::Empty);
3225 }
3226 Ok(Statement::ValidateOnly {
3227 kind: crate::ast::ValidateOnlyKind::TsConfigName,
3228 names,
3229 })
3230 }
3231 Token::Ident(s) | Token::QuotedIdent(s)
3232 if matches!(
3233 s.to_ascii_lowercase().as_str(),
3234 "type"
3235 | "domain"
3236 | "operator"
3237 | "cast"
3238 // `text` = TEXT SEARCH DICTIONARY / PARSER /
3239 // TEMPLATE (CONFIGURATION intercepted above).
3240 | "text"
3241 | "materialized"
3242 | "large"
3243 | "role"
3244 | "access"
3245 | "procedure"
3246 | "routine"
3247 ) =>
3248 {
3249 self.consume_until_statement_boundary();
3250 Ok(Statement::Empty)
3251 }
3252 // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3253 // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3254 // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3255 // foreign-data warning family (round 706) so a
3256 // CREATE→DROP sequence in a dump stays consistent.
3257 Token::Ident(s) | Token::QuotedIdent(s)
3258 if s.eq_ignore_ascii_case("server")
3259 || s.eq_ignore_ascii_case("foreign") =>
3260 {
3261 self.advance();
3262 self.consume_until_statement_boundary();
3263 Ok(Statement::ValidateOnly {
3264 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3265 names: Vec::new(),
3266 })
3267 }
3268 Token::Ident(s) | Token::QuotedIdent(s)
3269 if s.eq_ignore_ascii_case("collation")
3270 || s.eq_ignore_ascii_case("tablespace") =>
3271 {
3272 let kind = if s.eq_ignore_ascii_case("collation") {
3273 crate::ast::ValidateOnlyKind::CollationName
3274 } else {
3275 crate::ast::ValidateOnlyKind::TablespaceName
3276 };
3277 self.advance();
3278 let if_exists = self.consume_if_exists();
3279 let names = self.take_comma_separated_names();
3280 self.consume_until_statement_boundary();
3281 if if_exists {
3282 return Ok(Statement::Empty);
3283 }
3284 Ok(Statement::ValidateOnly { kind, names })
3285 }
3286 Token::Ident(s) | Token::QuotedIdent(s)
3287 if s.eq_ignore_ascii_case("event") =>
3288 {
3289 self.advance();
3290 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3291 {
3292 self.advance();
3293 }
3294 let if_exists = self.consume_if_exists();
3295 let names = self.take_comma_separated_names();
3296 self.consume_until_statement_boundary();
3297 if if_exists {
3298 return Ok(Statement::Empty);
3299 }
3300 Ok(Statement::ValidateOnly {
3301 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3302 names,
3303 })
3304 }
3305 // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3306 // leave the noise list; see the ValidateOnly kinds.
3307 Token::Ident(s) | Token::QuotedIdent(s)
3308 if s.eq_ignore_ascii_case("conversion")
3309 || s.eq_ignore_ascii_case("language")
3310 // `DROP PROCEDURAL LANGUAGE` puts the modifier
3311 // FIRST — the first draft looked for it after.
3312 || s.eq_ignore_ascii_case("procedural") =>
3313 {
3314 let kind = if s.eq_ignore_ascii_case("conversion") {
3315 crate::ast::ValidateOnlyKind::ConversionName
3316 } else {
3317 crate::ast::ValidateOnlyKind::LanguageName
3318 };
3319 self.advance();
3320 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3321 {
3322 self.advance();
3323 }
3324 let if_exists = self.consume_if_exists();
3325 let names = self.take_comma_separated_names();
3326 self.consume_until_statement_boundary();
3327 if if_exists {
3328 return Ok(Statement::Empty);
3329 }
3330 Ok(Statement::ValidateOnly { kind, names })
3331 }
3332 // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3333 // name(argtypes)[, …]`. Parsed for real so the engine
3334 // can answer as PG does; see Statement::DropAggregate.
3335 Token::Ident(s) | Token::QuotedIdent(s)
3336 if s.eq_ignore_ascii_case("aggregate") =>
3337 {
3338 self.advance();
3339 let if_exists = self.consume_if_exists();
3340 let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3341 loop {
3342 let name = self.expect_ident_like()?;
3343 if !matches!(self.peek(), Token::LParen) {
3344 return Err(self.err(alloc::format!(
3345 "expected argument list after DROP AGGREGATE {name}"
3346 )));
3347 }
3348 self.advance();
3349 let mut args: Vec<String> = Vec::new();
3350 let mut star = false;
3351 loop {
3352 match self.peek().clone() {
3353 Token::RParen => {
3354 self.advance();
3355 break;
3356 }
3357 Token::Star => {
3358 self.advance();
3359 star = true;
3360 }
3361 Token::Comma => {
3362 self.advance();
3363 }
3364 _ => {
3365 // A type name may be multi-token
3366 // (`double precision`); glue idents
3367 // until , or ).
3368 let mut t = self.expect_ident_like()?;
3369 while let Token::Ident(nx) = self.peek() {
3370 let nx = nx.clone();
3371 self.advance();
3372 t.push(' ');
3373 t.push_str(&nx);
3374 }
3375 args.push(t);
3376 }
3377 }
3378 }
3379 items.push((name, if star { None } else { Some(args) }));
3380 if matches!(self.peek(), Token::Comma) {
3381 self.advance();
3382 } else {
3383 break;
3384 }
3385 }
3386 self.consume_until_statement_boundary();
3387 Ok(Statement::DropAggregate { if_exists, items })
3388 }
3389 // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3390 // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3391 // installed; `IF EXISTS` is the spelling that says do
3392 // not, and it keeps the no-op.
3393 Token::Ident(s) | Token::QuotedIdent(s)
3394 if s.eq_ignore_ascii_case("extension") =>
3395 {
3396 self.advance();
3397 let if_exists = self.consume_if_exists();
3398 let names = self.take_comma_separated_names();
3399 self.consume_until_statement_boundary();
3400 if if_exists {
3401 return Ok(Statement::Empty);
3402 }
3403 Ok(Statement::ValidateOnly {
3404 kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3405 names,
3406 })
3407 }
3408 Token::Ident(s) | Token::QuotedIdent(s)
3409 if s.eq_ignore_ascii_case("statistics") =>
3410 {
3411 self.parse_drop_statistics_after_drop()
3412 }
3413 other => Err(self.err(format!(
3414 "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3415 SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3416 ))),
3417 }
3418 }
3419 // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3420 // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3421 // and accepted before the view name. SPG materialised
3422 // views re-evaluate on read (always-fresh semantics), so
3423 // the CONCURRENTLY-vs-serial distinction has no runtime
3424 // effect — the refresh body does not block readers either
3425 // way. Same accept-and-no-op pattern as DETACH PARTITION
3426 // CONCURRENTLY (16.5).
3427 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3428 self.advance();
3429 let nxt = self.peek().clone();
3430 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3431 {
3432 return Err(self.err(alloc::format!(
3433 "expected MATERIALIZED after REFRESH, got {nxt:?}"
3434 )));
3435 }
3436 self.advance();
3437 let nxt2 = self.peek().clone();
3438 if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3439 {
3440 return Err(self.err(alloc::format!(
3441 "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3442 )));
3443 }
3444 self.advance();
3445 // Optional CONCURRENTLY noise word — consumed without
3446 // changing semantics.
3447 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3448 {
3449 self.advance();
3450 }
3451 let name = self.expect_ident_like()?;
3452 let with_data = self.parse_optional_with_data(true)?;
3453 Ok(Statement::RefreshMaterializedView { name, with_data })
3454 }
3455 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3456 self.advance();
3457 self.parse_update_after_keyword()
3458 }
3459 // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3460 // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3461 // [CASCADE | RESTRICT]. Clears every row from each named
3462 // table. Parses at the top level; the engine dispatcher
3463 // walks Statement::Truncate.
3464 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3465 self.advance();
3466 // Optional TABLE noise word — PG accepts both the reserved
3467 // token and the bare identifier spelling.
3468 if matches!(self.peek(), Token::Table)
3469 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3470 {
3471 self.advance();
3472 }
3473 // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3474 // not absorbed. The lookahead keeps a table genuinely
3475 // called `only` working: the keyword is a keyword only
3476 // when a name follows it.
3477 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3478 if s.eq_ignore_ascii_case("only"))
3479 && matches!(
3480 self.tokens.get(self.pos + 1),
3481 Some(Token::Ident(_) | Token::QuotedIdent(_))
3482 );
3483 if only {
3484 self.advance();
3485 }
3486 // Table names (comma-separated).
3487 let mut tables = Vec::new();
3488 loop {
3489 tables.push(self.expect_ident_like()?);
3490 if matches!(self.peek(), Token::Comma) {
3491 self.advance();
3492 continue;
3493 }
3494 break;
3495 }
3496 // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3497 let mut restart_identity = false;
3498 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3499 {
3500 self.advance();
3501 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3502 {
3503 self.advance();
3504 restart_identity = true;
3505 }
3506 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3507 {
3508 self.advance();
3509 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3510 {
3511 self.advance();
3512 }
3513 }
3514 // Optional CASCADE / RESTRICT.
3515 let mut cascade = false;
3516 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3517 {
3518 self.advance();
3519 cascade = true;
3520 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3521 {
3522 self.advance();
3523 }
3524 Ok(Statement::Truncate {
3525 tables,
3526 restart_identity,
3527 cascade,
3528 only,
3529 })
3530 }
3531 // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3532 // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3533 // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3534 // rows change so the index tree is always up-to-date;
3535 // REINDEX is a strict no-op. Accept the whole statement
3536 // shape to boundary for pg_dump round-trip compatibility.
3537 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3538 // v7.39 (round 535) — the target is CARRIED now. SPG has no
3539 // index bloat to rebuild, so the work stays a no-op, but PG
3540 // validates what it was pointed at and this swallowed the
3541 // name at parse time — `REINDEX TABLE typo` reported
3542 // success. Measured on PG18: INDEX / TABLE name a relation,
3543 // SCHEMA a schema, SYSTEM nothing.
3544 self.advance();
3545 self.parse_reindex_tail()
3546 }
3547 // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3548 // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3549 // SPG has no MVCC bloat today (Phase D visibility map
3550 // queues with v7.38); the freezer collapses hot-tier
3551 // rows into cold segments automatically. VACUUM is a
3552 // no-op — pg_dump maintenance scripts and Discourse's
3553 // periodic-maintenance path both emit it.
3554 // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3555 // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3556 // actual bloat, so the pre-MVCC accept-and-ignore posture
3557 // became a silent no-op on a customer's manual reclaim.
3558 // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3559 // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3560 // ANALYZE is captured, the optional table name is captured.
3561 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3562 self.advance();
3563 // Parenthesised option list: absorb it.
3564 if matches!(self.peek(), Token::LParen) {
3565 let mut depth = 0usize;
3566 loop {
3567 match self.advance() {
3568 Token::LParen => depth += 1,
3569 Token::RParen => {
3570 depth -= 1;
3571 if depth == 0 {
3572 break;
3573 }
3574 }
3575 Token::Eof => break,
3576 _ => {}
3577 }
3578 }
3579 }
3580 let mut analyze = false;
3581 let mut table: Option<String> = None;
3582 loop {
3583 match self.peek() {
3584 // v7.39 (round 535) — `FULL` lexes as a keyword, not
3585 // an identifier, so the loop below broke out on it and
3586 // dropped the table name: `VACUUM FULL nosuch` was
3587 // accepted where `VACUUM nosuch` was refused.
3588 Token::Full => {
3589 self.advance();
3590 }
3591 Token::Ident(w) | Token::QuotedIdent(w) => {
3592 let wl = w.to_ascii_lowercase();
3593 match wl.as_str() {
3594 "full" | "freeze" | "verbose" => {
3595 self.advance();
3596 }
3597 "analyze" | "analyse" => {
3598 analyze = true;
3599 self.advance();
3600 }
3601 _ => {
3602 table = Some(self.expect_ident_like()?);
3603 break;
3604 }
3605 }
3606 }
3607 _ => break,
3608 }
3609 }
3610 // Optional trailing column list / anything else to the
3611 // statement boundary (PG accepts per-column ANALYZE).
3612 self.consume_until_statement_boundary();
3613 Ok(Statement::Vacuum { table, analyze })
3614 }
3615 // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3616 // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3617 // <index>. PG stores rows in physical order matching
3618 // an index; SPG's hot-tier is append-only + cold-tier
3619 // is segment-frozen, so clustering has no persistent
3620 // effect. Accept-and-no-op for pg_dump compat.
3621 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3622 // v7.39 (round 535) — same as REINDEX above: the relation is
3623 // carried so the engine can refuse one that does not exist.
3624 // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3625 self.advance();
3626 self.parse_cluster_tail()
3627 }
3628 // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3629 // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3630 // optional string payload; UNLISTEN takes a channel or `*`.
3631 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3632 self.advance();
3633 let ch = match self.advance() {
3634 Token::Ident(c) | Token::QuotedIdent(c) => c,
3635 other => {
3636 return Err(self.err(format!(
3637 "expected channel name after LISTEN, got {other:?}"
3638 )));
3639 }
3640 };
3641 Ok(Statement::Listen(ch))
3642 }
3643 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3644 self.advance();
3645 let channel = match self.advance() {
3646 Token::Ident(c) | Token::QuotedIdent(c) => c,
3647 other => {
3648 return Err(self.err(format!(
3649 "expected channel name after NOTIFY, got {other:?}"
3650 )));
3651 }
3652 };
3653 let payload = if matches!(self.peek(), Token::Comma) {
3654 self.advance();
3655 match self.advance() {
3656 Token::String(p) => Some(p),
3657 other => {
3658 return Err(self.err(format!(
3659 "expected string payload after NOTIFY <channel>, got {other:?}"
3660 )));
3661 }
3662 }
3663 } else {
3664 None
3665 };
3666 Ok(Statement::Notify { channel, payload })
3667 }
3668 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3669 self.advance();
3670 match self.advance() {
3671 Token::Star => Ok(Statement::Unlisten(None)),
3672 Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3673 other => Err(self.err(format!(
3674 "expected channel name or * after UNLISTEN, got {other:?}"
3675 ))),
3676 }
3677 }
3678 // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3679 // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3680 // process-wide write lock today; explicit LOCK has no
3681 // effect. Accept-and-no-op for pg_dump / migration
3682 // compat.
3683 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3684 self.advance();
3685 // v7.39 (round 696) — the LOCK still has no effect (SPG's
3686 // engine holds a process-wide write lock), but the TABLE
3687 // NAME is now carried out so the engine can refuse one that
3688 // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3689 // READ|WRITE` is a different statement with the same first
3690 // word; it keeps the old no-op, because a MySQL dump's
3691 // bracket names tables it is about to create.
3692 let mysql_tables = matches!(self.peek(), Token::Ident(k)
3693 if k.eq_ignore_ascii_case("tables"));
3694 if mysql_tables {
3695 self.consume_until_statement_boundary();
3696 return Ok(Statement::Empty);
3697 }
3698 if matches!(self.peek(), Token::Table) {
3699 self.advance();
3700 }
3701 let names = self.take_comma_separated_names();
3702 self.consume_until_statement_boundary();
3703 Ok(Statement::ValidateOnly {
3704 kind: crate::ast::ValidateOnlyKind::LockTable,
3705 names,
3706 })
3707 }
3708 // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3709 // durability marker + snapshot in PG. SPG has WAL
3710 // checkpointing on a byte / time schedule (v7.37.10
3711 // 60s / 4 MiB defaults). The bare statement parses to
3712 // `Statement::Empty` here (the no_std engine owns no
3713 // WAL / snapshot); v7.37 Epic Du wires the HOST
3714 // (embedded `Database::execute_buffered`, via
3715 // `sql_is_checkpoint`) to force an immediate synchronous
3716 // checkpoint through `Database::checkpoint` — a real
3717 // durability barrier, matching PG.
3718 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3719 self.advance();
3720 self.consume_until_statement_boundary();
3721 Ok(Statement::Empty)
3722 }
3723 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3724 self.advance();
3725 self.parse_delete_after_keyword()
3726 }
3727 // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3728 // ALTER is not a reserved keyword in the lexer — handled
3729 // as a bare ident here.
3730 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3731 self.advance();
3732 self.parse_alter_after_keyword()
3733 }
3734 // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3735 // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3736 // additions needed.
3737 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3738 self.advance();
3739 self.parse_wait_after_keyword()
3740 }
3741 // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3742 // Bare ANALYZE → analyse every user table; ANALYZE
3743 // <name> → re-stats one. The argument is an optional
3744 // ident (or quoted ident); anything else is a parse
3745 // error.
3746 // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3747 // `WHERE` filter (carved out per V6_7_DESIGN.md
3748 // STABILITY). Lex order: identifier "compact" → "cold"
3749 // → "segments". Anything else after `COMPACT` is a
3750 // parse error.
3751 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3752 self.advance();
3753 let next = self.peek().clone();
3754 let cold = match next {
3755 Token::Ident(s) | Token::QuotedIdent(s) => s,
3756 _ => {
3757 return Err(
3758 self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3759 );
3760 }
3761 };
3762 if !cold.eq_ignore_ascii_case("cold") {
3763 return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3764 }
3765 self.advance();
3766 let next = self.peek().clone();
3767 let segments = match next {
3768 Token::Ident(s) | Token::QuotedIdent(s) => s,
3769 _ => {
3770 return Err(self.err(format!(
3771 "expected SEGMENTS after COMPACT COLD, got {:?}",
3772 self.peek()
3773 )));
3774 }
3775 };
3776 if !segments.eq_ignore_ascii_case("segments") {
3777 return Err(self.err(format!(
3778 "expected SEGMENTS after COMPACT COLD, got {segments:?}"
3779 )));
3780 }
3781 self.advance();
3782 Ok(Statement::CompactColdSegments)
3783 }
3784 // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
3785 // Parsed as a case-insensitive identifier since MERGE
3786 // isn't a reserved lexer keyword (collides with the
3787 // mysqldump `ALGORITHM = MERGE` view clause if it
3788 // were); the inner parser drives the rest of the
3789 // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
3790 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
3791 self.advance();
3792 self.parse_merge_after_keyword()
3793 }
3794 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
3795 self.advance();
3796 let target = match self.peek() {
3797 Token::Eof | Token::Semicolon => None,
3798 Token::Ident(_) | Token::QuotedIdent(_) => {
3799 Some(self.expect_ident_like()?)
3800 }
3801 other => {
3802 return Err(self.err(format!(
3803 "expected table name or end of statement after ANALYZE, got {other:?}"
3804 )));
3805 }
3806 };
3807 // v7.39 (round 776, F31 J7) — the per-column form
3808 // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
3809 // here while the VACUUM arm already consumed it; SPG
3810 // analyzes whole tables, so the list parses and is
3811 // accepted like the VACUUM path's.
3812 if target.is_some() && matches!(self.peek(), Token::LParen) {
3813 self.advance();
3814 loop {
3815 let _ = self.expect_ident_like()?;
3816 match self.peek() {
3817 Token::Comma => {
3818 self.advance();
3819 }
3820 Token::RParen => {
3821 self.advance();
3822 break;
3823 }
3824 other => {
3825 return Err(self.err(format!(
3826 "expected ',' or ')' in ANALYZE column list, got {other:?}"
3827 )));
3828 }
3829 }
3830 }
3831 }
3832 Ok(Statement::Analyze(target))
3833 }
3834 // v7.12.1 — `SET <name> [TO|=] <value>`. The
3835 // `default_text_search_config` parameter is consumed
3836 // by the FTS function dispatcher; other parameter
3837 // names are recorded but treated as a no-op so PG
3838 // dump output loads.
3839 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
3840 self.advance();
3841 // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
3842 // adds `SET GLOBAL` too (and the alias `SET @@global.name =
3843 // …` which the SessionVar path handles). `LOCAL` is the only
3844 // one that changes semantics — it scopes the change to the
3845 // current transaction — so capture it; SESSION / GLOBAL are
3846 // accepted and treated as the default session scope.
3847 let mut set_local = false;
3848 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
3849 let q = s.to_ascii_lowercase();
3850 if q == "local" || q == "session" || q == "global" {
3851 set_local = q == "local";
3852 self.advance();
3853 }
3854 }
3855 // 7.38.1 S5.2 — PG `SET [SESSION] AUTHORIZATION
3856 // { DEFAULT | <role> }`. pg_dump's ACL section switches
3857 // to the object owner with it. SPG maps it onto the
3858 // session-role machinery (recorded delta: PG moves
3859 // session_user too; SPG moves the effective role).
3860 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3861 if s.eq_ignore_ascii_case("authorization"))
3862 {
3863 self.advance(); // AUTHORIZATION
3864 let role = match self.peek().clone() {
3865 Token::Default => {
3866 self.advance();
3867 None
3868 }
3869 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
3870 self.advance();
3871 Some(s)
3872 }
3873 _ => None,
3874 };
3875 return Ok(Statement::SetRole(role));
3876 }
3877 // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
3878 // <collation>]` — change the connection client
3879 // charset. SPG stores UTF-8 always and orders
3880 // bytewise; accept as a no-op.
3881 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
3882 {
3883 self.advance();
3884 // Charset ident-or-string.
3885 if matches!(
3886 self.peek(),
3887 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3888 ) {
3889 self.advance();
3890 }
3891 // Optional `COLLATE <name>`.
3892 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
3893 {
3894 self.advance();
3895 if matches!(
3896 self.peek(),
3897 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3898 ) {
3899 self.advance();
3900 }
3901 }
3902 return Ok(Statement::Empty);
3903 }
3904 // v7.37.17 (17.6 sibling) — PG `SET ROLE
3905 // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
3906 // uses this to switch to the object owner before
3907 // recreating tables. SPG has no role system so this
3908 // is a no-op.
3909 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
3910 {
3911 self.advance(); // ROLE
3912 // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
3913 // reset to the login identity; a name / string sets the
3914 // effective role that drives current_user + RLS.
3915 let role = match self.peek().clone() {
3916 Token::Default => {
3917 self.advance();
3918 None
3919 }
3920 Token::Ident(s) | Token::QuotedIdent(s)
3921 if s.eq_ignore_ascii_case("none") =>
3922 {
3923 self.advance();
3924 None
3925 }
3926 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
3927 self.advance();
3928 Some(s)
3929 }
3930 _ => None,
3931 };
3932 return Ok(Statement::SetRole(role));
3933 }
3934 // v7.37.17 (17.6 sibling) — PG `SET SESSION
3935 // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
3936 // ISO SQL surface). pg_dump prepends this to fix
3937 // the isolation level for the restore session. SPG
3938 // defaults to READ COMMITTED and doesn't yet honor
3939 // session-set isolation across statements — accept
3940 // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
3941 // per-tx form is handled elsewhere.
3942 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
3943 {
3944 self.advance(); // CHARACTERISTICS
3945 self.consume_until_statement_boundary();
3946 return Ok(Statement::Empty);
3947 }
3948 // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
3949 // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
3950 // pg_dump emits this to control the deferrability of
3951 // FK / UNIQUE constraints across a bulk restore. SPG
3952 // has no deferrable-constraint machinery today; the
3953 // FK checker is strict-immediate. Accept-and-no-op
3954 // for pg_dump round-trip compatibility.
3955 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
3956 {
3957 self.advance(); // CONSTRAINTS
3958 // v7.39 (round 288) — no longer a no-op: the trailing
3959 // DEFERRED / IMMEDIATE sets the transaction's timing.
3960 // v7.39 (round 308, V29) — and the names are kept.
3961 // They used to be skipped over on the way to the
3962 // DEFERRED keyword, so a named form silently behaved
3963 // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
3964 // every deferrable constraint in the transaction.
3965 let mut names: alloc::vec::Vec<alloc::string::String> =
3966 alloc::vec::Vec::new();
3967 if matches!(self.peek(), Token::All) {
3968 self.advance();
3969 } else {
3970 loop {
3971 let mut n = self.expect_ident_like()?;
3972 // A schema-qualified name (`public.fk_a`)
3973 // identifies the same constraint; PG resolves
3974 // it by the trailing segment.
3975 while matches!(self.peek(), Token::Dot) {
3976 self.advance();
3977 n = self.expect_ident_like()?;
3978 }
3979 names.push(n);
3980 if matches!(self.peek(), Token::Comma) {
3981 self.advance();
3982 } else {
3983 break;
3984 }
3985 }
3986 }
3987 let deferred = match self.peek() {
3988 Token::Ident(s) | Token::QuotedIdent(s)
3989 if s.eq_ignore_ascii_case("deferred") =>
3990 {
3991 true
3992 }
3993 Token::Ident(s) | Token::QuotedIdent(s)
3994 if s.eq_ignore_ascii_case("immediate") =>
3995 {
3996 false
3997 }
3998 other => {
3999 return Err(self.err(alloc::format!(
4000 "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
4001 )));
4002 }
4003 };
4004 self.advance();
4005 return Ok(Statement::SetConstraints { names, deferred });
4006 }
4007 // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
4008 // { DEFAULT | '<role>' | <ident> }` (mailrs
4009 // round-10 A.1). pg_dump preamble emits the
4010 // `DEFAULT` form to reset session authorization.
4011 //
4012 // v7.39 (round 697) — this said "SPG has no role system so
4013 // this is a strict no-op". SPG has had one since round 58;
4014 // the comment outlived it, and with it the reason a name
4015 // that is not a role was accepted here. It still switches
4016 // no authorization — what it does now is refuse a role
4017 // that does not exist, as PG18 does. PG also accepts `RESET SESSION
4018 // AUTHORIZATION` (handled by the RESET parser
4019 // elsewhere). Reference:
4020 // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
4021 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
4022 {
4023 self.advance(); // AUTHORIZATION
4024 match self.peek().clone() {
4025 Token::Default => {
4026 self.advance();
4027 }
4028 Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4029 self.advance();
4030 return Ok(Statement::ValidateOnly {
4031 kind: crate::ast::ValidateOnlyKind::RoleName,
4032 names: alloc::vec![r],
4033 });
4034 }
4035 other => {
4036 return Err(self.err(alloc::format!(
4037 "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4038 )));
4039 }
4040 }
4041 return Ok(Statement::Empty);
4042 }
4043 // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4044 // ISOLATION LEVEL { READ COMMITTED | READ
4045 // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4046 // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4047 // PG-standard surface. v7.37.8 accepts the syntax
4048 // and tracks the selected level on
4049 // `Engine::current_isolation_level()`; the actual
4050 // MVCC / SSI semantics implementation lands in
4051 // the 轴 4 isolation framework (separate train).
4052 // PG itself maps READ UNCOMMITTED to READ COMMITTED
4053 // internally; SPG behaves the same (effectively
4054 // READ COMMITTED at every level today).
4055 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4056 {
4057 self.advance(); // TRANSACTION
4058 let level = self.parse_isolation_level_clauses()?.unwrap_or_default();
4059 return Ok(Statement::SetTransaction { isolation: level });
4060 }
4061 // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4062 // alias — same accept-as-no-op as SET NAMES.
4063 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4064 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4065 {
4066 self.advance(); // CHARACTER
4067 self.advance(); // SET
4068 if matches!(
4069 self.peek(),
4070 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4071 ) {
4072 self.advance();
4073 }
4074 return Ok(Statement::Empty);
4075 }
4076 // v7.39 (GUC) — PG spells the timezone GUC as two
4077 // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4078 // where <value> is a string/ident or the LOCAL /
4079 // DEFAULT keyword (both mean "back to the default").
4080 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4081 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4082 {
4083 self.advance(); // TIME
4084 self.advance(); // ZONE
4085 let value = match self.peek().clone() {
4086 Token::Ident(s)
4087 if s.eq_ignore_ascii_case("local")
4088 || s.eq_ignore_ascii_case("default") =>
4089 {
4090 self.advance();
4091 crate::ast::SetValue::Default
4092 }
4093 Token::Default => {
4094 self.advance();
4095 crate::ast::SetValue::Default
4096 }
4097 _ => self.parse_set_value()?,
4098 };
4099 return Ok(Statement::SetParameter {
4100 name: "timezone".into(),
4101 value,
4102 local: set_local,
4103 });
4104 }
4105 // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4106 // MySQL USER-variable assignment: its own per-session
4107 // namespace, an arbitrary expression on the right, and `:=`
4108 // as a second spelling of `=`. It used to fall into the
4109 // session-PARAMETER list below, whose values are literals and
4110 // whose store nothing reads back under a `@` name — so the
4111 // assignment reported success and vanished.
4112 //
4113 // A `@@`-prefixed LHS is a real engine setting and keeps the
4114 // old path.
4115 if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4116 return self.parse_set_user_vars();
4117 }
4118 // v7.14.0 — multi-assignment form
4119 // `SET a = 1, b = 2, …`. Single-assignment is the
4120 // 1-element case. Each LHS may be a regular ident
4121 // or a SessionVar (`@VAR` / `@@VAR`).
4122 let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4123 loop {
4124 let lhs = match self.peek().clone() {
4125 Token::SessionVar(s) => {
4126 self.advance();
4127 s
4128 }
4129 Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4130 other => {
4131 return Err(self.err(format!(
4132 "expected parameter name after SET, got {other:?}"
4133 )));
4134 }
4135 };
4136 // Accept either `=` or the bare `TO` keyword.
4137 match self.peek() {
4138 Token::Eq => {
4139 self.advance();
4140 }
4141 Token::To => {
4142 self.advance();
4143 }
4144 other => {
4145 return Err(self.err(format!(
4146 "expected `=` or TO after SET {lhs}, got {other:?}"
4147 )));
4148 }
4149 }
4150 let mut value = self.parse_set_value()?;
4151 // v7.39 (GUC) — disambiguate the comma: `, name =` /
4152 // `, name TO` continues a MySQL-style multi-assign,
4153 // anything else is a PG list VALUE
4154 // (`SET search_path = myschema, public`) folded into
4155 // one comma-joined string.
4156 while matches!(self.peek(), Token::Comma) {
4157 let is_assign = matches!(
4158 self.tokens.get(self.pos + 1),
4159 Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4160 ) && matches!(
4161 self.tokens.get(self.pos + 2),
4162 Some(Token::Eq | Token::To)
4163 );
4164 if is_assign {
4165 break;
4166 }
4167 self.advance(); // comma
4168 let next = self.parse_set_value()?;
4169 let joined = alloc::format!(
4170 "{}, {}",
4171 set_value_text(&value),
4172 set_value_text(&next)
4173 );
4174 value = crate::ast::SetValue::String(joined);
4175 }
4176 pairs.push((lhs, value));
4177 if matches!(self.peek(), Token::Comma) {
4178 self.advance();
4179 continue;
4180 }
4181 break;
4182 }
4183 if pairs.len() == 1 {
4184 let (name, value) = pairs.into_iter().next().unwrap();
4185 Ok(Statement::SetParameter {
4186 name,
4187 value,
4188 local: set_local,
4189 })
4190 } else {
4191 Ok(Statement::SetParameterList(pairs))
4192 }
4193 }
4194 // v7.12.1 — `RESET <name>` / `RESET ALL`.
4195 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4196 self.advance();
4197 match self.peek().clone() {
4198 Token::All => {
4199 self.advance();
4200 Ok(Statement::ResetParameter(None))
4201 }
4202 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4203 self.advance();
4204 Ok(Statement::ResetParameter(None))
4205 }
4206 // v7.39 (RLS) — `RESET ROLE` clears the session role.
4207 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4208 self.advance();
4209 Ok(Statement::SetRole(None))
4210 }
4211 // 7.38.1 S5.2 — `RESET SESSION AUTHORIZATION`
4212 // (pg_dump's return from the owner switch).
4213 Token::Ident(s) | Token::QuotedIdent(s)
4214 if s.eq_ignore_ascii_case("session")
4215 && matches!(
4216 self.tokens.get(self.pos + 1),
4217 Some(Token::Ident(a) | Token::QuotedIdent(a))
4218 if a.eq_ignore_ascii_case("authorization")
4219 ) =>
4220 {
4221 self.advance(); // SESSION
4222 self.advance(); // AUTHORIZATION
4223 Ok(Statement::SetRole(None))
4224 }
4225 _ => {
4226 let name = self.parse_set_param_name()?;
4227 Ok(Statement::ResetParameter(Some(name)))
4228 }
4229 }
4230 }
4231 // v7.39 (round 218) — server-side cursors.
4232 Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4233 Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4234 Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4235 Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4236 self.advance();
4237 match self.peek().clone() {
4238 Token::All => {
4239 self.advance();
4240 Ok(Statement::CloseCursor { name: None })
4241 }
4242 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4243 self.advance();
4244 Ok(Statement::CloseCursor { name: None })
4245 }
4246 Token::Ident(n) | Token::QuotedIdent(n) => {
4247 self.advance();
4248 Ok(Statement::CloseCursor { name: Some(n) })
4249 }
4250 other => Err(self.err(format!(
4251 "expected cursor name or ALL after CLOSE, got {other:?}"
4252 ))),
4253 }
4254 }
4255 other => Err(self.err(format!(
4256 "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4257 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4258 ))),
4259 }
4260 }
4261
4262 /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4263 /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4264 /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4265 /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4266 fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4267 self.advance(); // DECLARE
4268 let name = match self.advance() {
4269 Token::Ident(n) | Token::QuotedIdent(n) => n,
4270 other => {
4271 return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4272 }
4273 };
4274 let mut scroll: Option<bool> = None;
4275 loop {
4276 match self.peek() {
4277 Token::Ident(s)
4278 if s.eq_ignore_ascii_case("binary")
4279 || s.eq_ignore_ascii_case("insensitive")
4280 || s.eq_ignore_ascii_case("asensitive") =>
4281 {
4282 self.advance();
4283 }
4284 Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4285 self.advance();
4286 scroll = Some(true);
4287 }
4288 Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4289 {
4290 self.advance(); // NO
4291 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4292 return Err(self.err(format!(
4293 "expected SCROLL after NO in DECLARE, got {:?}",
4294 self.peek()
4295 )));
4296 }
4297 self.advance();
4298 scroll = Some(false);
4299 }
4300 _ => break,
4301 }
4302 }
4303 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4304 return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4305 }
4306 self.advance();
4307 let mut hold = false;
4308 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4309 self.advance();
4310 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4311 return Err(self.err(format!(
4312 "expected HOLD after WITH in DECLARE, got {:?}",
4313 self.peek()
4314 )));
4315 }
4316 self.advance();
4317 hold = true;
4318 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4319 self.advance();
4320 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4321 return Err(self.err(format!(
4322 "expected HOLD after WITHOUT in DECLARE, got {:?}",
4323 self.peek()
4324 )));
4325 }
4326 self.advance();
4327 }
4328 if !matches!(self.peek(), Token::For) {
4329 return Err(self.err(format!(
4330 "expected FOR before the cursor query, got {:?}",
4331 self.peek()
4332 )));
4333 }
4334 self.advance();
4335 let query = self.parse_one_statement()?;
4336 Ok(Statement::DeclareCursor {
4337 name,
4338 scroll,
4339 hold,
4340 query: alloc::boxed::Box::new(query),
4341 })
4342 }
4343
4344 /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4345 /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4346 /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4347 fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4348 use crate::ast::CursorDirection as D;
4349 self.advance(); // FETCH / MOVE
4350 let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4351 let neg = if matches!(this.peek(), Token::Minus) {
4352 this.advance();
4353 true
4354 } else {
4355 false
4356 };
4357 match this.advance() {
4358 Token::Integer(v) => Ok(if neg { -v } else { v }),
4359 other => Err(this.err(format!("expected count, got {other:?}"))),
4360 }
4361 };
4362 let direction = match self.peek().clone() {
4363 Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4364 self.advance();
4365 D::Next
4366 }
4367 Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4368 self.advance();
4369 D::Prior
4370 }
4371 Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4372 self.advance();
4373 D::First
4374 }
4375 Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4376 self.advance();
4377 D::Last
4378 }
4379 Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4380 self.advance();
4381 D::Absolute(signed_count(self)?)
4382 }
4383 Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4384 self.advance();
4385 D::Relative(signed_count(self)?)
4386 }
4387 Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4388 self.advance();
4389 match self.peek().clone() {
4390 Token::All => {
4391 self.advance();
4392 D::All
4393 }
4394 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4395 self.advance();
4396 D::All
4397 }
4398 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4399 _ => D::Next, // bare FORWARD = FORWARD 1
4400 }
4401 }
4402 Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4403 self.advance();
4404 match self.peek().clone() {
4405 Token::All => {
4406 self.advance();
4407 D::BackwardAll
4408 }
4409 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4410 self.advance();
4411 D::BackwardAll
4412 }
4413 Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4414 _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4415 }
4416 }
4417 Token::All => {
4418 self.advance();
4419 D::All
4420 }
4421 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4422 self.advance();
4423 D::All
4424 }
4425 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4426 // Bare `FETCH <name>` — direction defaults to NEXT.
4427 _ => D::Next,
4428 };
4429 // Optional FROM / IN.
4430 if matches!(self.peek(), Token::From)
4431 || matches!(self.peek(), Token::In)
4432 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4433 {
4434 self.advance();
4435 }
4436 let name = match self.advance() {
4437 Token::Ident(n) | Token::QuotedIdent(n) => n,
4438 other => {
4439 return Err(self.err(format!("expected cursor name, got {other:?}")));
4440 }
4441 };
4442 Ok(if is_move {
4443 Statement::MoveCursor { name, direction }
4444 } else {
4445 Statement::FetchCursor { name, direction }
4446 })
4447 }
4448
4449 /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4450 /// [(kind, …)] ON <col>, … FROM <table>`.
4451 fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4452 self.advance(); // STATISTICS
4453 // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4454 let mut if_not_exists = false;
4455 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4456 && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4457 {
4458 self.advance();
4459 self.advance();
4460 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4461 self.advance();
4462 if_not_exists = true;
4463 }
4464 }
4465 let name = self.expect_ident_like()?;
4466 let mut kinds = Vec::new();
4467 if matches!(self.peek(), Token::LParen) {
4468 self.advance();
4469 loop {
4470 let k = self.expect_ident_like()?;
4471 // PG stores the single letters; accept the spelled-out
4472 // names the SQL uses and record what PG records.
4473 kinds.push(match k.to_ascii_lowercase().as_str() {
4474 "ndistinct" => String::from("d"),
4475 "dependencies" => String::from("f"),
4476 "mcv" => String::from("m"),
4477 other => {
4478 return Err(
4479 self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4480 );
4481 }
4482 });
4483 match self.advance() {
4484 Token::Comma => {}
4485 Token::RParen => break,
4486 other => {
4487 return Err(self.err(alloc::format!(
4488 "expected ',' or ')' in statistics kind list, got {other:?}"
4489 )));
4490 }
4491 }
4492 }
4493 }
4494 if !matches!(self.peek(), Token::On) {
4495 return Err(self.err(alloc::format!(
4496 "expected ON in CREATE STATISTICS, got {:?}",
4497 self.peek()
4498 )));
4499 }
4500 self.advance();
4501 let mut columns = Vec::new();
4502 loop {
4503 columns.push(self.expect_ident_like()?);
4504 if matches!(self.peek(), Token::Comma) {
4505 self.advance();
4506 } else {
4507 break;
4508 }
4509 }
4510 if !matches!(self.peek(), Token::From) {
4511 return Err(self.err(alloc::format!(
4512 "expected FROM in CREATE STATISTICS, got {:?}",
4513 self.peek()
4514 )));
4515 }
4516 self.advance();
4517 let table = self.expect_ident_like()?;
4518 Ok(Statement::CreateStatistics {
4519 name,
4520 if_not_exists,
4521 kinds,
4522 columns,
4523 table,
4524 })
4525 }
4526
4527 /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4528 /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4529 /// entered with the `TABLE` keyword still unconsumed. Extracted so
4530 /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4531 /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4532 /// forward call.
4533 fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4534 self.advance(); // TABLE
4535 let if_exists = self.consume_if_exists();
4536 let mut names: Vec<String> = Vec::new();
4537 loop {
4538 names.push(self.expect_ident_like()?);
4539 if matches!(self.peek(), Token::Comma) {
4540 self.advance();
4541 continue;
4542 }
4543 break;
4544 }
4545 if matches!(
4546 self.peek(),
4547 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4548 || s.eq_ignore_ascii_case("restrict")
4549 ) {
4550 self.advance();
4551 }
4552 Ok(Statement::DropTable { names, if_exists })
4553 }
4554
4555 fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4556 self.advance(); // STATISTICS
4557 let mut if_exists = false;
4558 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4559 && matches!(self.tokens.get(self.pos + 1),
4560 Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4561 {
4562 self.advance();
4563 self.advance();
4564 if_exists = true;
4565 }
4566 let name = self.expect_ident_like()?;
4567 Ok(Statement::DropStatistics { name, if_exists })
4568 }
4569
4570 fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4571 debug_assert!(matches!(self.peek(), Token::Create));
4572 self.advance();
4573 match self.peek() {
4574 Token::Table => self.parse_create_table_stmt_after_create(),
4575 Token::Index => self.parse_create_index_stmt_after_create(false),
4576 // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4577 // object now. It used to be consumed by the CREATE-noise
4578 // arm, so a pg_dump that declares extended statistics
4579 // restored silently without them and reflection showed
4580 // nothing.
4581 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4582 self.parse_create_statistics_after_create()
4583 }
4584 // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4585 // The `UNIQUE` modifier turns a partial index into a
4586 // partial-uniqueness invariant (only rows matching the
4587 // WHERE predicate are checked for duplicates). mailrs
4588 // K1 (3 hits: email_templates default, calendar_events
4589 // master, calendar_events instance).
4590 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4591 self.advance();
4592 if !matches!(self.peek(), Token::Index) {
4593 return Err(self.err(alloc::format!(
4594 "expected INDEX after CREATE UNIQUE, got {:?}",
4595 self.peek()
4596 )));
4597 }
4598 self.parse_create_index_stmt_after_create(true)
4599 }
4600 Token::Publication => {
4601 self.advance();
4602 self.parse_create_publication_after_keyword()
4603 }
4604 Token::Subscription => {
4605 self.advance();
4606 self.parse_create_subscription_after_keyword()
4607 }
4608 // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4609 // USER isn't a reserved keyword — we look for the bare
4610 // identifier so the lexer doesn't have to grow a token.
4611 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4612 self.advance();
4613 self.parse_create_user_after_keyword(true)
4614 }
4615 // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4616 // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4617 // the default of the LOGIN attribute.
4618 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4619 self.advance();
4620 self.parse_create_user_after_keyword(false)
4621 }
4622 // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4623 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4624 self.advance();
4625 self.parse_create_policy_after_keyword()
4626 }
4627 // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4628 // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4629 // no-op. mailrs follow-up F3.
4630 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4631 self.advance();
4632 self.parse_create_extension_after_keyword()
4633 }
4634 // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4635 // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4636 // optional; absorb it here and forward to the
4637 // per-kind parsers with the flag. OR is a reserved
4638 // keyword token.
4639 Token::Or => {
4640 self.advance();
4641 let next = self.peek();
4642 let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4643 return Err(self.err(alloc::format!(
4644 "expected REPLACE after CREATE OR, got {next:?}"
4645 )));
4646 };
4647 if !s2.eq_ignore_ascii_case("replace") {
4648 return Err(self.err(alloc::format!(
4649 "expected REPLACE after CREATE OR, got {s2:?}"
4650 )));
4651 }
4652 self.advance();
4653 self.parse_create_function_or_trigger_after_or_replace(true)
4654 }
4655 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4656 self.advance();
4657 self.parse_create_function_after_keyword(false)
4658 }
4659 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4660 self.advance();
4661 self.parse_create_trigger_after_keyword(false)
4662 }
4663 // v7.39 (round 139) — CREATE RULE …
4664 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4665 self.advance();
4666 self.parse_create_rule_after_keyword(false)
4667 }
4668 // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4669 // trigger is a row-level AFTER trigger that additionally carries
4670 // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4671 // path already tolerates and skips those clauses, so consuming the
4672 // CONSTRAINT keyword and reusing it makes the statement parse and the
4673 // trigger fire. (The deferral timing itself is not yet honoured —
4674 // SPG fires it as a plain AFTER trigger, which is correct behaviour
4675 // for every non-deferred use.)
4676 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4677 self.advance();
4678 if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4679 if t.eq_ignore_ascii_case("trigger"))
4680 {
4681 return Err(self.err(alloc::format!(
4682 "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4683 self.peek()
4684 )));
4685 }
4686 self.advance();
4687 self.parse_create_trigger_after_keyword(false)
4688 }
4689 // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4690 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4691 self.advance();
4692 self.parse_create_sequence_after_keyword(false)
4693 }
4694 // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4695 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4696 self.advance();
4697 self.parse_create_view_after_keyword(false, false, false)
4698 }
4699 // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4700 // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4701 // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4702 // appear (in any order) between `CREATE` and `VIEW` in
4703 // every mysqldump-emitted view. Pre-2.6 the parser
4704 // rejected the prefix and the customer's whole view
4705 // backup failed on the first view. The hints are pure
4706 // planner / permission metadata; SPG's view-rewrite
4707 // path is semantically equivalent for all three
4708 // algorithms in v7.17 (TEMPTABLE differs only in
4709 // perf for huge views — out of v7.17 scope), and
4710 // DEFINER / SQL SECURITY are pure single-user
4711 // permissioning that SPG ignores by design.
4712 Token::Ident(s) | Token::QuotedIdent(s)
4713 if s.eq_ignore_ascii_case("algorithm")
4714 || s.eq_ignore_ascii_case("definer")
4715 || s.eq_ignore_ascii_case("sql") =>
4716 {
4717 self.consume_mysql_view_prefix()?;
4718 // After absorbing ALGORITHM / DEFINER / SQL SECURITY
4719 // (in any order, in any combination), the next
4720 // keyword must be VIEW. mysqldump never emits these
4721 // prefixes on non-view statements.
4722 let next = self.peek().clone();
4723 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
4724 if s2.eq_ignore_ascii_case("view"))
4725 {
4726 self.advance();
4727 self.parse_create_view_after_keyword(false, false, false)
4728 } else {
4729 Err(self.err(alloc::format!(
4730 "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
4731 )))
4732 }
4733 }
4734 // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
4735 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
4736 self.advance();
4737 self.parse_create_type_after_keyword()
4738 }
4739 // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
4740 // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
4741 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
4742 self.advance();
4743 self.parse_create_domain_after_keyword()
4744 }
4745 // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
4746 // name [AUTHORIZATION user]. Real catalog registry
4747 // (was silent-no-op'd pre-v7.17).
4748 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
4749 self.advance();
4750 let if_not_exists = self.parse_if_not_exists();
4751 let name = self.expect_ident_like()?;
4752 // Optional `AUTHORIZATION <user>` trailer — accepted,
4753 // ignored (single-user catalog).
4754 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4755 if s.eq_ignore_ascii_case("authorization"))
4756 {
4757 self.advance();
4758 let _ = self.expect_ident_like()?;
4759 }
4760 Ok(Statement::CreateSchema { name, if_not_exists })
4761 }
4762 // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
4763 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
4764 self.advance();
4765 let next = self.peek().clone();
4766 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4767 {
4768 self.advance();
4769 self.parse_create_materialized_view_after_keyword()
4770 } else {
4771 Err(self.err(alloc::format!(
4772 "expected VIEW after CREATE MATERIALIZED, got {next:?}"
4773 )))
4774 }
4775 }
4776 // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
4777 // no-op below), an UNLOGGED table is a real, fully-usable table in
4778 // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
4779 // durability optimisation is a follow-up), so a dump / app that
4780 // declares UNLOGGED tables works instead of failing to parse.
4781 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
4782 self.advance(); // UNLOGGED
4783 if matches!(self.peek(), Token::Table) {
4784 self.parse_create_table_stmt_after_create()
4785 } else {
4786 Err(self.err(format!(
4787 "expected TABLE after CREATE UNLOGGED, got {:?}",
4788 self.peek()
4789 )))
4790 }
4791 }
4792 Token::Ident(s) | Token::QuotedIdent(s)
4793 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
4794 {
4795 self.advance();
4796 // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
4797 let next = self.peek().clone();
4798 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
4799 {
4800 self.advance();
4801 self.parse_create_sequence_after_keyword(true)
4802 } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4803 {
4804 self.advance();
4805 self.parse_create_view_after_keyword(false, false, true)
4806 } else {
4807 // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
4808 // consumed and answered OK while creating nothing, so
4809 // every statement that touched the table afterwards failed
4810 // with "table not found" — the DDL itself lied. It is a
4811 // real CREATE TABLE now, marked temporary so the executor
4812 // puts it in the session's own namespace. An optional
4813 // TABLE keyword may or may not be present (`CREATE TEMP t`
4814 // is not legal, but the keyword is consumed by the
4815 // CREATE TABLE parser itself).
4816 let stmt = self.parse_create_table_stmt_after_create()?;
4817 match stmt {
4818 Statement::CreateTable(mut c) => {
4819 c.temporary = true;
4820 Ok(Statement::CreateTable(c))
4821 }
4822 // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
4823 // CTAS node, which needs the same session namespace.
4824 Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
4825 m.temporary = true;
4826 Ok(Statement::CreateMaterializedView(m))
4827 }
4828 other => Ok(other),
4829 }
4830 }
4831 }
4832 // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
4833 // BEGIN <body> END`. The body may reference `@var`
4834 // session variables, SET statements, internal `;`
4835 // terminators, etc. SPG has no procedure runtime, so
4836 // consume the whole `CREATE PROCEDURE … END` block as
4837 // a no-op so mysqldump scripts that include stored
4838 // routines load through. The matching-END consumer
4839 // tracks BEGIN/END nesting depth to handle nested
4840 // BEGIN blocks correctly.
4841 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
4842 self.consume_mysql_routine_body();
4843 Ok(Statement::Empty)
4844 }
4845 // v7.14.0 — pg_dump / mysqldump emit
4846 // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
4847 // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
4848 // SPG is single-schema / single-database; these have
4849 // no behavioural effect, so consume + return Empty.
4850 // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
4851 // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
4852 // moved up to real parser branches. DATABASE / ROLE /
4853 // POLICY / OPERATOR stay no-op forever
4854 // (single-database, hardcoded roles).
4855 Token::Ident(s) | Token::QuotedIdent(s)
4856 if matches!(
4857 s.to_ascii_lowercase().as_str(),
4858 "database"
4859 | "role"
4860 | "operator"
4861 | "cast"
4862 | "aggregate"
4863 | "language"
4864 | "collation"
4865 | "conversion"
4866 // v7.17.0 Phase 8 (audit N6) — rarely-
4867 // emitted pg_dump shapes that should
4868 // load through without a parser error.
4869 // SPG has no planner statistics catalog,
4870 // no event-trigger hooks, no foreign-
4871 // data-wrapper infrastructure; consume
4872 // + return Empty.
4873 | "statistics"
4874 | "event"
4875 // v7.37.17 (17.6 siblings) — additional CREATE
4876 // targets pg_dump / operator install scripts
4877 // may emit that SPG has no matching machinery
4878 // for. Consume + Empty-return.
4879 | "text"
4880 | "tablespace"
4881 | "access"
4882 | "large"
4883 ) =>
4884 {
4885 // DATABASE is the one member of this list PG refuses
4886 // inside a transaction block; the rest (ROLE, CAST,
4887 // TABLESPACE, …) it runs there quite happily, so only
4888 // this one is named. Still a no-op otherwise — SPG is
4889 // single-database.
4890 let is_database = s.eq_ignore_ascii_case("database");
4891 self.consume_until_statement_boundary();
4892 if is_database {
4893 return Ok(Statement::NoOpPreventedInTransaction {
4894 what: String::from("CREATE DATABASE"),
4895 });
4896 }
4897 Ok(Statement::Empty)
4898 }
4899 // v7.39 (round 706) — the foreign-data family leaves the silent
4900 // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
4901 // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
4902 // FDW machinery), but the ENGINE now warns, so a restore log
4903 // says what will not function instead of reporting success.
4904 Token::Ident(s) | Token::QuotedIdent(s)
4905 if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
4906 {
4907 self.consume_until_statement_boundary();
4908 Ok(Statement::ValidateOnly {
4909 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
4910 names: Vec::new(),
4911 })
4912 }
4913 other => Err(self.err(format!(
4914 "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
4915 ))),
4916 }
4917 }
4918
4919 /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
4920 /// keyword decides whether we parse a function or trigger
4921 /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
4922 /// PROCEDURE) — those land in later releases.
4923 fn parse_create_function_or_trigger_after_or_replace(
4924 &mut self,
4925 or_replace: bool,
4926 ) -> Result<Statement, ParseError> {
4927 let tok = self.peek();
4928 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
4929 return Err(self.err(alloc::format!(
4930 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
4931 )));
4932 };
4933 if s.eq_ignore_ascii_case("function") {
4934 self.advance();
4935 self.parse_create_function_after_keyword(or_replace)
4936 } else if s.eq_ignore_ascii_case("trigger") {
4937 self.advance();
4938 self.parse_create_trigger_after_keyword(or_replace)
4939 } else if s.eq_ignore_ascii_case("rule") {
4940 // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
4941 self.advance();
4942 self.parse_create_rule_after_keyword(or_replace)
4943 } else if s.eq_ignore_ascii_case("view") {
4944 // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
4945 self.advance();
4946 self.parse_create_view_after_keyword(or_replace, false, false)
4947 } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
4948 // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
4949 self.advance();
4950 let nxt = self.peek().clone();
4951 if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
4952 {
4953 self.advance();
4954 self.parse_create_view_after_keyword(or_replace, false, true)
4955 } else {
4956 Err(self.err(alloc::format!(
4957 "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
4958 )))
4959 }
4960 } else {
4961 Err(self.err(alloc::format!(
4962 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
4963 )))
4964 }
4965 }
4966
4967 /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
4968 /// SPG doesn't have a registry; pgvector / similar are
4969 /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
4970 /// the syntax lets dual-target schemas keep the line.
4971 fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
4972 // Optional `IF NOT EXISTS`.
4973 self.consume_if_not_exists();
4974 let name = self.expect_ident_like()?;
4975 // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
4976 // CASCADE / FROM '<v>' clauses; we don't model them.
4977 loop {
4978 match self.peek() {
4979 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
4980 self.advance();
4981 continue;
4982 }
4983 Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
4984 self.advance();
4985 let _ = self.expect_ident_like()?;
4986 continue;
4987 }
4988 Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
4989 self.advance();
4990 // String or ident literal.
4991 let _ = self.advance();
4992 continue;
4993 }
4994 Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
4995 self.advance();
4996 let _ = self.advance();
4997 continue;
4998 }
4999 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
5000 self.advance();
5001 continue;
5002 }
5003 _ => break,
5004 }
5005 }
5006 // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
5007 // nosuch` reported success and `pg_extension` then did not list it,
5008 // which is the accept-and-do-nothing shape F31 exists to find.
5009 Ok(Statement::ValidateOnly {
5010 kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
5011 names: alloc::vec![name],
5012 })
5013 }
5014
5015 /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
5016 /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
5017 /// already been consumed by the caller. Grammar accepted:
5018 ///
5019 /// name `(` arg-list `)`
5020 /// `RETURNS` return-type
5021 /// [ `LANGUAGE` ident ]
5022 /// `AS` $$ body $$
5023 /// [ `LANGUAGE` ident ]
5024 ///
5025 /// Either `LANGUAGE` position is allowed; PG accepts both.
5026 fn parse_create_function_after_keyword(
5027 &mut self,
5028 or_replace: bool,
5029 ) -> Result<Statement, ParseError> {
5030 let name = self.expect_ident_like()?;
5031 // Argument list. v7.12.4 commonly sees the empty `()`
5032 // (trigger functions); typed args parse and round-trip
5033 // but the executor only invokes nullary functions.
5034 if !matches!(self.peek(), Token::LParen) {
5035 return Err(self.err(alloc::format!(
5036 "expected '(' after function name {name:?}, got {:?}",
5037 self.peek()
5038 )));
5039 }
5040 self.advance();
5041 let args = self.parse_function_arg_list()?;
5042 // RETURNS clause.
5043 let tok = self.peek();
5044 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5045 return Err(self.err(alloc::format!(
5046 "expected RETURNS after function arg list, got {tok:?}"
5047 )));
5048 };
5049 if !s.eq_ignore_ascii_case("returns") {
5050 return Err(self.err(alloc::format!(
5051 "expected RETURNS after function arg list, got {s:?}"
5052 )));
5053 }
5054 self.advance();
5055 let returns = self.parse_function_return()?;
5056 // Optional LANGUAGE clause (PG also accepts after AS — we'll
5057 // re-check after the body too).
5058 let mut language: Option<String> = self.parse_optional_language()?;
5059 // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5060 // either side of the body and in any order, interleaved with
5061 // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5062 // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5063 // PG's own pg_dump output did not restore.
5064 let mut attrs = FunctionAttrs::default();
5065 loop {
5066 let before = self.pos;
5067 self.parse_function_attrs_into(&mut attrs)?;
5068 if language.is_none() {
5069 language = self.parse_optional_language()?;
5070 }
5071 if self.pos == before {
5072 break;
5073 }
5074 }
5075 // `AS` followed by a $$-quoted body (lexer already
5076 // collapses both `$$…$$` and `$tag$…$tag$` to a single
5077 // Token::String). AS is a reserved keyword (Token::As).
5078 if !matches!(self.peek(), Token::As) {
5079 return Err(self.err(alloc::format!(
5080 "expected AS before function body, got {:?}",
5081 self.peek()
5082 )));
5083 }
5084 self.advance();
5085 let body_text = match self.peek() {
5086 Token::String(s) => {
5087 let body = s.clone();
5088 self.advance();
5089 body
5090 }
5091 other => {
5092 return Err(self.err(alloc::format!(
5093 "expected $$-quoted function body after AS, got {other:?}"
5094 )));
5095 }
5096 };
5097 // Trailing clauses — PG's other accepted position for both the
5098 // LANGUAGE and the attributes.
5099 loop {
5100 let before = self.pos;
5101 self.parse_function_attrs_into(&mut attrs)?;
5102 if language.is_none() {
5103 language = self.parse_optional_language()?;
5104 }
5105 if self.pos == before {
5106 break;
5107 }
5108 }
5109 let language = language.unwrap_or_else(|| String::from("sql"));
5110 // PL/pgSQL bodies get structure-parsed. Other languages
5111 // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5112 // recognise) round-trip as Raw text — the executor errors
5113 // when invoked with a clear unsupported message.
5114 let body = if language.eq_ignore_ascii_case("plpgsql") {
5115 match parse_plpgsql_body(&body_text) {
5116 Ok(block) => FunctionBody::PlPgSql(block),
5117 // Best-effort: if the body parser doesn't yet
5118 // support a construct used inside, fall back to
5119 // raw — keeps `CREATE FUNCTION` itself working
5120 // (catalogue accepts), executor errors on
5121 // invocation only.
5122 Err(_) => FunctionBody::Raw(body_text),
5123 }
5124 } else {
5125 FunctionBody::Raw(body_text)
5126 };
5127 Ok(Statement::CreateFunction(CreateFunctionStatement {
5128 name,
5129 or_replace,
5130 args,
5131 returns,
5132 language,
5133 body,
5134 attrs,
5135 }))
5136 }
5137
5138 /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5139 /// attribute clauses into `attrs`, stopping at the first token that
5140 /// is not one. Measured against PG 18.4, which accepts them in any
5141 /// order and on either side of the body.
5142 fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5143 loop {
5144 let word = match self.peek() {
5145 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5146 // NOT LEAKPROOF — NOT is a reserved keyword token.
5147 Token::Not
5148 if matches!(
5149 self.tokens.get(self.pos + 1),
5150 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5151 ) =>
5152 {
5153 self.advance();
5154 self.advance();
5155 attrs.leakproof = false;
5156 continue;
5157 }
5158 _ => return Ok(()),
5159 };
5160 match word.as_str() {
5161 "immutable" => {
5162 self.advance();
5163 attrs.volatility = FunctionVolatility::Immutable;
5164 }
5165 "stable" => {
5166 self.advance();
5167 attrs.volatility = FunctionVolatility::Stable;
5168 }
5169 "volatile" => {
5170 self.advance();
5171 attrs.volatility = FunctionVolatility::Volatile;
5172 }
5173 "strict" => {
5174 self.advance();
5175 attrs.strict = true;
5176 }
5177 "leakproof" => {
5178 self.advance();
5179 attrs.leakproof = true;
5180 }
5181 // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5182 // spelled-out forms of STRICT and its opposite.
5183 "returns" | "called" => {
5184 let strict = word == "returns";
5185 let mut probe = self.pos + 1;
5186 if strict {
5187 // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5188 // is not ours.
5189 match self.tokens.get(probe) {
5190 Some(Token::Null) => probe += 1,
5191 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5192 _ => return Ok(()),
5193 }
5194 }
5195 let ok = matches!(self.tokens.get(probe), Some(Token::On))
5196 || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5197 if !ok {
5198 return Ok(());
5199 }
5200 probe += 1;
5201 match self.tokens.get(probe) {
5202 Some(Token::Null) => probe += 1,
5203 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5204 _ => return Ok(()),
5205 }
5206 match self.tokens.get(probe) {
5207 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5208 _ => return Ok(()),
5209 }
5210 self.pos = probe;
5211 attrs.strict = strict;
5212 }
5213 "security" | "external" => {
5214 // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5215 let mut probe = self.pos + 1;
5216 if word == "external" {
5217 match self.tokens.get(probe) {
5218 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5219 probe += 1;
5220 }
5221 _ => return Ok(()),
5222 }
5223 }
5224 let definer = match self.tokens.get(probe) {
5225 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5226 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5227 _ => return Ok(()),
5228 };
5229 self.pos = probe + 1;
5230 attrs.security_definer = definer;
5231 }
5232 "parallel" => {
5233 let level = match self.tokens.get(self.pos + 1) {
5234 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5235 FunctionParallel::Safe
5236 }
5237 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5238 FunctionParallel::Restricted
5239 }
5240 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5241 FunctionParallel::Unsafe
5242 }
5243 _ => return Ok(()),
5244 };
5245 self.pos += 2;
5246 attrs.parallel = level;
5247 }
5248 "cost" | "rows" => {
5249 let Some(n) = self.peek_number_at(self.pos + 1) else {
5250 return Ok(());
5251 };
5252 self.pos += 2;
5253 if word == "cost" {
5254 attrs.cost = Some(n);
5255 } else {
5256 attrs.rows = Some(n);
5257 }
5258 }
5259 _ => return Ok(()),
5260 }
5261 }
5262 }
5263
5264 /// The numeric literal at `idx`, if there is one.
5265 fn peek_number_at(&self, idx: usize) -> Option<f64> {
5266 match self.tokens.get(idx)? {
5267 Token::Integer(n) => Some(*n as f64),
5268 Token::Float(f) => Some(*f),
5269 Token::Numeric(t) => t.parse::<f64>().ok(),
5270 _ => None,
5271 }
5272 }
5273
5274 /// Closing `)`-terminated argument list. v7.12.4 commonly
5275 /// sees the empty `()`; typed args round-trip but the
5276 /// executor (yet) doesn't invoke them.
5277 /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5278 /// it away, which is what PG does with one on a function parameter.
5279 fn skip_type_modifier(&mut self) {
5280 if !matches!(self.peek(), Token::LParen) {
5281 return;
5282 }
5283 // Only a numeric modifier — anything else is not one, and eating
5284 // it would swallow real grammar.
5285 let mut i = self.pos + 1;
5286 let mut seen_number = false;
5287 loop {
5288 match self.tokens.get(i) {
5289 Some(Token::Integer(_)) => seen_number = true,
5290 Some(Token::Comma) => {}
5291 Some(Token::RParen) => break,
5292 _ => return,
5293 }
5294 i += 1;
5295 }
5296 if !seen_number {
5297 return;
5298 }
5299 while self.pos <= i {
5300 self.advance();
5301 }
5302 }
5303
5304 fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5305 let mut args: Vec<FunctionArg> = Vec::new();
5306 if matches!(self.peek(), Token::RParen) {
5307 self.advance();
5308 return Ok(args);
5309 }
5310 loop {
5311 // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5312 // a reserved token; OUT / INOUT are bare idents.
5313 let mode = if matches!(self.peek(), Token::In) {
5314 self.advance();
5315 FunctionArgMode::In
5316 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5317 {
5318 self.advance();
5319 FunctionArgMode::Out
5320 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5321 {
5322 self.advance();
5323 FunctionArgMode::InOut
5324 } else {
5325 FunctionArgMode::In
5326 };
5327 // Optional name. The next token is either a name
5328 // (followed by a type ident) or the type itself.
5329 // Disambiguate by peeking ahead: if the token after
5330 // the next ident is also an ident, we treat the
5331 // first as the name.
5332 // v7.39 (round 315, V19) — take EVERY ident-like word up to
5333 // the comma or paren, then decide. Reading at most two of
5334 // them could not spell `x double precision` at all, and
5335 // silently mis-read the bare `double precision` as a
5336 // parameter named "double" — which is what made the same
5337 // signature key two different ways.
5338 let (name, ty_token) = {
5339 let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5340 while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5341 words.push(self.expect_ident_like()?);
5342 }
5343 // v7.39 (round 344) — a length / precision modifier on the
5344 // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5345 // accepts it and DROPS it — `pg_get_function_arguments`
5346 // reports plain `character varying` / `numeric`, measured on
5347 // 18.4 — but SPG raised `syntax error at or near "("`,
5348 // because the modifier's parens were never consumed.
5349 self.skip_type_modifier();
5350 // r1049 — `f(v bigint[])`. The array suffix parsed in
5351 // the column position, the cast position and (r1038)
5352 // the RETURNS position, but not here: the fifth
5353 // member of the same family, reported by sentori as
5354 // presumably the same code. It is now.
5355 let array_suffix = self.consume_array_suffix();
5356 let whole = words.join(" ");
5357 let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5358 {
5359 (Some(words[0].clone()), words[1..].join(" "))
5360 } else {
5361 (None, whole)
5362 };
5363 ty_token.push_str(&array_suffix);
5364 (name, ty_token)
5365 };
5366 // Type — try to map to ColumnTypeName, else Raw.
5367 let ty = match map_type_ident_to_column_type_name(&ty_token) {
5368 Some(t) => FunctionArgType::Typed(t),
5369 None => FunctionArgType::Raw(ty_token),
5370 };
5371 args.push(FunctionArg { mode, name, ty });
5372 match self.peek() {
5373 Token::Comma => {
5374 self.advance();
5375 continue;
5376 }
5377 Token::RParen => {
5378 self.advance();
5379 return Ok(args);
5380 }
5381 other => {
5382 return Err(self.err(alloc::format!(
5383 "expected , or ) in function arg list, got {other:?}"
5384 )));
5385 }
5386 }
5387 }
5388 }
5389
5390 fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5391 // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5392 // function whose row shape is named inline.
5393 if matches!(self.peek(), Token::Table)
5394 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5395 {
5396 self.advance(); // TABLE
5397 self.advance(); // (
5398 let mut cols: Vec<String> = Vec::new();
5399 loop {
5400 let cname = self.expect_ident_like()?;
5401 let mut ty: Vec<String> = Vec::new();
5402 loop {
5403 match self.peek() {
5404 Token::Comma | Token::RParen | Token::Eof => break,
5405 _ => {}
5406 }
5407 match self.advance() {
5408 Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5409 other => {
5410 if let Some(w) = unreserved_keyword_text(&other) {
5411 ty.push(w);
5412 }
5413 }
5414 }
5415 }
5416 cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5417 if matches!(self.peek(), Token::Comma) {
5418 self.advance();
5419 } else {
5420 break;
5421 }
5422 }
5423 if matches!(self.peek(), Token::RParen) {
5424 self.advance();
5425 }
5426 return Ok(FunctionReturn::Other(alloc::format!(
5427 "TABLE({})",
5428 cols.join(", ")
5429 )));
5430 }
5431 let ident = self.expect_ident_like()?;
5432 // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5433 if ident.eq_ignore_ascii_case("setof") {
5434 let inner = self.expect_ident_like()?;
5435 let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5436 return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5437 }
5438 if ident.eq_ignore_ascii_case("trigger") {
5439 return Ok(FunctionReturn::Trigger);
5440 }
5441 if ident.eq_ignore_ascii_case("void") {
5442 return Ok(FunctionReturn::Void);
5443 }
5444 // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5445 // RETURN position did not, so the `[` was a syntax error and the
5446 // whole migration stopped. sentori worked around it by returning
5447 // zero-padded text.
5448 let suffix = self.consume_array_suffix();
5449 if !suffix.is_empty() {
5450 return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5451 }
5452 match map_type_ident_to_column_type_name(&ident) {
5453 Some(t) => Ok(FunctionReturn::Type(t)),
5454 None => Ok(FunctionReturn::Other(ident)),
5455 }
5456 }
5457
5458 /// Consume any `[]` / `[N]` array markers after a type name and give
5459 /// back their text. Empty when there are none.
5460 fn consume_array_suffix(&mut self) -> String {
5461 let mut out = String::new();
5462 while matches!(self.peek(), Token::LBracket) {
5463 self.advance();
5464 // `[N]` is accepted and, as in PG, the length is not enforced.
5465 if let Token::Integer(n) = self.peek().clone() {
5466 self.advance();
5467 out.push_str(&alloc::format!("[{n}]"));
5468 } else {
5469 out.push_str("[]");
5470 }
5471 if matches!(self.peek(), Token::RBracket) {
5472 self.advance();
5473 }
5474 }
5475 out
5476 }
5477
5478 fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5479 match self.peek() {
5480 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5481 self.advance();
5482 let lang = self.expect_ident_like()?;
5483 Ok(Some(lang.to_ascii_lowercase()))
5484 }
5485 _ => Ok(None),
5486 }
5487 }
5488
5489 /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5490 /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5491 /// (expr)]*`. The `DOMAIN` keyword has already been
5492 /// consumed. PG allows the trailing constraints in any
5493 /// order; we approximate with a small loop.
5494 fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5495 let name = self.expect_ident_like()?;
5496 // Optional `AS`.
5497 if matches!(self.peek(), Token::As) {
5498 self.advance();
5499 }
5500 // v7.39 (round 259) — keep the raw type NAME when the base is not
5501 // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5502 // parent domain.
5503 let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _) =
5504 self.parse_type_with_implied_flags()?;
5505 let mut default: Option<Expr> = None;
5506 let mut not_null = false;
5507 let mut checks: Vec<Expr> = Vec::new();
5508 loop {
5509 match self.peek() {
5510 Token::Default => {
5511 if default.is_some() {
5512 return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5513 }
5514 self.advance();
5515 default = Some(self.parse_expr(0)?);
5516 }
5517 Token::Not => {
5518 self.advance();
5519 if !matches!(self.peek(), Token::Null) {
5520 return Err(self.err(alloc::format!(
5521 "expected NULL after NOT in DOMAIN, got {:?}",
5522 self.peek()
5523 )));
5524 }
5525 self.advance();
5526 not_null = true;
5527 }
5528 Token::Null => {
5529 self.advance();
5530 // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5531 // is the default-nullable marker (PG accepts it),
5532 // but AFTER a NOT NULL it is a conflict PG refuses
5533 // (`conflicting NULL/NOT NULL constraints`,
5534 // PG18-measured); the old arm no-opped both ways.
5535 if not_null {
5536 return Err(self.err(alloc::string::String::from(
5537 "conflicting NULL/NOT NULL constraints",
5538 )));
5539 }
5540 }
5541 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5542 self.advance();
5543 if !matches!(self.peek(), Token::LParen) {
5544 return Err(self.err(alloc::format!(
5545 "expected '(' after CHECK in DOMAIN, got {:?}",
5546 self.peek()
5547 )));
5548 }
5549 self.advance();
5550 let expr = self.parse_expr(0)?;
5551 if !matches!(self.peek(), Token::RParen) {
5552 return Err(self.err(alloc::format!(
5553 "expected ')' after CHECK expr, got {:?}",
5554 self.peek()
5555 )));
5556 }
5557 self.advance();
5558 checks.push(expr);
5559 }
5560 // CONSTRAINT <name> CHECK (…) — PG accepts a name
5561 // prefix on the constraint; we drop the name and
5562 // recurse into the constraint parsing.
5563 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5564 self.advance();
5565 let _ = self.expect_ident_like()?;
5566 }
5567 _ => break,
5568 }
5569 }
5570 Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5571 name,
5572 base_type,
5573 base_domain: base_user_ref,
5574 default,
5575 not_null,
5576 checks,
5577 }))
5578 }
5579
5580 /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5581 /// ('a', 'b', …)`. The `TYPE` keyword has already been
5582 /// consumed.
5583 fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5584 let name = self.expect_ident_like()?;
5585 // Required `AS`.
5586 if !matches!(self.peek(), Token::As) {
5587 return Err(self.err(alloc::format!(
5588 "expected AS after CREATE TYPE {name:?}, got {:?}",
5589 self.peek()
5590 )));
5591 }
5592 self.advance();
5593 // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5594 // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5595 // on the next token: `(` = composite, ident `ENUM` = enum.
5596 if matches!(self.peek(), Token::LParen) {
5597 self.advance();
5598 let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5599 let mut field_user_types: Vec<Option<String>> = Vec::new();
5600 // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5601 // is legal PG (an attribute-less composite; measured — the old
5602 // e2e note claimed PG requires at least one attribute).
5603 if matches!(self.peek(), Token::RParen) {
5604 self.advance();
5605 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5606 name,
5607 kind: crate::ast::TypeKind::Composite {
5608 fields,
5609 field_user_types,
5610 },
5611 }));
5612 }
5613 loop {
5614 let field_name = self.expect_ident_like()?;
5615 // v7.39 (round 264) — keep the raw type name when it is not
5616 // a builtin: that is how a NESTED composite field records
5617 // which composite it holds.
5618 let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _) =
5619 self.parse_type_with_implied_flags()?;
5620 fields.push((field_name, field_type));
5621 field_user_types.push(field_user_ref);
5622 if matches!(self.peek(), Token::Comma) {
5623 self.advance();
5624 continue;
5625 }
5626 if matches!(self.peek(), Token::RParen) {
5627 self.advance();
5628 break;
5629 }
5630 return Err(self.err(alloc::format!(
5631 "expected , or ) in composite field list, got {:?}",
5632 self.peek()
5633 )));
5634 }
5635 if fields.is_empty() {
5636 return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5637 }
5638 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5639 name,
5640 kind: crate::ast::TypeKind::Composite {
5641 fields,
5642 field_user_types,
5643 },
5644 }));
5645 }
5646 // Required `ENUM` ident.
5647 let kind_ident = match self.peek().clone() {
5648 Token::Ident(s) | Token::QuotedIdent(s) => s,
5649 other => {
5650 return Err(self.err(alloc::format!(
5651 "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5652 )));
5653 }
5654 };
5655 if !kind_ident.eq_ignore_ascii_case("enum") {
5656 return Err(self.err(alloc::format!(
5657 "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5658 )));
5659 }
5660 self.advance();
5661 if !matches!(self.peek(), Token::LParen) {
5662 return Err(self.err(alloc::format!(
5663 "expected '(' after ENUM, got {:?}",
5664 self.peek()
5665 )));
5666 }
5667 self.advance();
5668 let mut labels: Vec<String> = Vec::new();
5669 loop {
5670 match self.peek().clone() {
5671 Token::String(s) => {
5672 self.advance();
5673 labels.push(s);
5674 }
5675 other => {
5676 return Err(
5677 self.err(alloc::format!("expected enum label string, got {other:?}"))
5678 );
5679 }
5680 }
5681 if matches!(self.peek(), Token::Comma) {
5682 self.advance();
5683 continue;
5684 }
5685 if matches!(self.peek(), Token::RParen) {
5686 self.advance();
5687 break;
5688 }
5689 return Err(self.err(alloc::format!(
5690 "expected , or ) in ENUM label list, got {:?}",
5691 self.peek()
5692 )));
5693 }
5694 if labels.is_empty() {
5695 return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5696 }
5697 Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5698 name,
5699 kind: crate::ast::TypeKind::Enum { labels },
5700 }))
5701 }
5702
5703 /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
5704 /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
5705 /// The `CREATE MATERIALIZED VIEW` keywords have already been
5706 /// consumed.
5707 fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
5708 let if_not_exists = self.parse_if_not_exists();
5709 let name = self.expect_ident_like()?;
5710 let mut columns: Vec<String> = Vec::new();
5711 if matches!(self.peek(), Token::LParen) {
5712 self.advance();
5713 loop {
5714 let c = self.expect_ident_like()?;
5715 columns.push(c);
5716 if matches!(self.peek(), Token::Comma) {
5717 self.advance();
5718 continue;
5719 }
5720 if matches!(self.peek(), Token::RParen) {
5721 self.advance();
5722 break;
5723 }
5724 return Err(self.err(alloc::format!(
5725 "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
5726 self.peek()
5727 )));
5728 }
5729 }
5730 if !matches!(self.peek(), Token::As) {
5731 return Err(self.err(alloc::format!(
5732 "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
5733 self.peek()
5734 )));
5735 }
5736 self.advance();
5737 // v7.39 (round 151) — a WITH-headed body is legal (read-only
5738 // CTEs only; the engine rejects data-modifying ones with PG's
5739 // message). A trailing `WITH [NO] DATA` can't START the body,
5740 // so WITH here heads the query.
5741 let body = if self.peek_is_with_kw() {
5742 self.advance();
5743 self.parse_nested_with_select()?
5744 } else {
5745 let body_stmt = self.parse_select_stmt()?;
5746 let Statement::Select(body) = body_stmt else {
5747 return Err(self.err(alloc::format!(
5748 "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
5749 )));
5750 };
5751 body
5752 };
5753 // Optional trailing `WITH [NO] DATA`.
5754 let with_data = self.parse_optional_with_data(true)?;
5755 Ok(Statement::CreateMaterializedView(
5756 crate::ast::CreateMaterializedViewStatement {
5757 temporary: false,
5758 name,
5759 if_not_exists,
5760 columns,
5761 body,
5762 with_data,
5763 as_plain_table: false,
5764 },
5765 ))
5766 }
5767
5768 /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
5769 /// `default_when_absent` is what to return if the tail is
5770 /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
5771 /// WITH DATA).
5772 fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
5773 let save = self.pos;
5774 // `WITH` is an Ident (not reserved in the lexer).
5775 let is_with = match self.peek() {
5776 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
5777 _ => false,
5778 };
5779 if !is_with {
5780 return Ok(default_when_absent);
5781 }
5782 self.advance();
5783 // Optional `NO`.
5784 let mut with_data = true;
5785 let is_no = match self.peek() {
5786 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
5787 _ => false,
5788 };
5789 if is_no {
5790 self.advance();
5791 with_data = false;
5792 }
5793 // Required `DATA` ident.
5794 let is_data = match self.peek() {
5795 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
5796 _ => false,
5797 };
5798 if is_data {
5799 self.advance();
5800 Ok(with_data)
5801 } else {
5802 // Caller's WITH wasn't WITH-DATA — rewind so the outer
5803 // parser can interpret it.
5804 self.pos = save;
5805 Ok(default_when_absent)
5806 }
5807 }
5808
5809 /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
5810 /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
5811 /// All keyword prefixes have already been consumed; the flags
5812 /// say which were present.
5813 fn parse_create_view_after_keyword(
5814 &mut self,
5815 or_replace: bool,
5816 _materialized_unused: bool,
5817 temporary: bool,
5818 ) -> Result<Statement, ParseError> {
5819 let if_not_exists = self.parse_if_not_exists();
5820 let name = self.expect_ident_like()?;
5821 // Optional `(col, col, …)` rename list.
5822 let mut columns: Vec<String> = Vec::new();
5823 if matches!(self.peek(), Token::LParen) {
5824 self.advance();
5825 loop {
5826 let c = self.expect_ident_like()?;
5827 columns.push(c);
5828 if matches!(self.peek(), Token::Comma) {
5829 self.advance();
5830 continue;
5831 }
5832 if matches!(self.peek(), Token::RParen) {
5833 self.advance();
5834 break;
5835 }
5836 return Err(self.err(alloc::format!(
5837 "expected , or ) in VIEW column list, got {:?}",
5838 self.peek()
5839 )));
5840 }
5841 }
5842 // Required `AS`.
5843 if !matches!(self.peek(), Token::As) {
5844 return Err(self.err(alloc::format!(
5845 "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
5846 self.peek()
5847 )));
5848 }
5849 self.advance();
5850 // Body: a regular SELECT statement. v7.39 (round 151) — a
5851 // WITH-headed body is legal too (read-only CTEs only; the
5852 // engine rejects data-modifying ones with PG's message).
5853 // Disambiguation vs `WITH CHECK OPTION`: a body can't START
5854 // with the check-option clause, so WITH here heads the query.
5855 let body = if self.peek_is_with_kw() {
5856 self.advance();
5857 self.parse_nested_with_select()?
5858 } else {
5859 let body_stmt = self.parse_select_stmt()?;
5860 let Statement::Select(body) = body_stmt else {
5861 return Err(self.err(alloc::format!(
5862 "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
5863 )));
5864 };
5865 body
5866 };
5867 // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
5868 // The SELECT parser stops before a trailing WITH, so it lands here.
5869 let check_option = if matches!(self.peek(),
5870 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
5871 {
5872 self.advance(); // WITH
5873 let opt = match self.peek() {
5874 Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
5875 self.advance();
5876 crate::ast::ViewCheckOption::Local
5877 }
5878 Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
5879 self.advance();
5880 crate::ast::ViewCheckOption::Cascaded
5881 }
5882 // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
5883 _ => crate::ast::ViewCheckOption::Cascaded,
5884 };
5885 if !matches!(self.peek(),
5886 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
5887 {
5888 return Err(self.err(alloc::format!(
5889 "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
5890 self.peek()
5891 )));
5892 }
5893 self.advance(); // CHECK
5894 if !matches!(self.peek(),
5895 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
5896 {
5897 return Err(self.err(alloc::format!(
5898 "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
5899 self.peek()
5900 )));
5901 }
5902 self.advance(); // OPTION
5903 Some(opt)
5904 } else {
5905 None
5906 };
5907 Ok(Statement::CreateView(crate::ast::CreateViewStatement {
5908 name,
5909 or_replace,
5910 if_not_exists,
5911 temporary,
5912 columns,
5913 body,
5914 check_option,
5915 }))
5916 }
5917
5918 /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
5919 /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
5920 /// consumed; `temporary` carries whether TEMPORARY was seen.
5921 fn parse_create_sequence_after_keyword(
5922 &mut self,
5923 temporary: bool,
5924 ) -> Result<Statement, ParseError> {
5925 let if_not_exists = self.parse_if_not_exists();
5926 let name = self.expect_ident_like()?;
5927 // Optional `AS data_type`.
5928 let data_type = if matches!(self.peek(), Token::As) {
5929 self.advance();
5930 Some(self.parse_sequence_data_type()?)
5931 } else {
5932 None
5933 };
5934 let options = self.parse_sequence_options(/* allow_restart = */ false)?;
5935 Ok(Statement::CreateSequence(
5936 crate::ast::CreateSequenceStatement {
5937 name,
5938 if_not_exists,
5939 temporary,
5940 data_type,
5941 options,
5942 },
5943 ))
5944 }
5945
5946 /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
5947 /// already been consumed; this is reached after `SEQUENCE`.
5948 /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
5949 fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5950 use crate::ast::AlterDomainAction as A;
5951 let name = self.expect_ident_like()?;
5952 // DROP / SET / ADD lex as reserved keyword tokens, not idents.
5953 let kw = match self.peek() {
5954 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
5955 Token::Drop => alloc::string::String::from("drop"),
5956 Token::Default => alloc::string::String::from("default"),
5957 other => {
5958 return Err(self.err(alloc::format!(
5959 "expected an ALTER DOMAIN action, got {other:?}"
5960 )));
5961 }
5962 };
5963 let action = match kw.as_str() {
5964 "add" => {
5965 self.advance();
5966 let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
5967 {
5968 self.advance();
5969 Some(self.expect_ident_like()?)
5970 } else {
5971 None
5972 };
5973 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
5974 return Err(self.err(alloc::format!(
5975 "ALTER DOMAIN ADD supports CHECK only, got {:?}",
5976 self.peek()
5977 )));
5978 }
5979 self.advance();
5980 if !matches!(self.peek(), Token::LParen) {
5981 return Err(self.err("expected '(' after CHECK".into()));
5982 }
5983 self.advance();
5984 let check = self.parse_expr(0)?;
5985 if !matches!(self.peek(), Token::RParen) {
5986 return Err(self.err("expected ')' after CHECK expression".into()));
5987 }
5988 self.advance();
5989 A::AddConstraint { name: cname, check }
5990 }
5991 "drop" => {
5992 self.advance();
5993 match self.peek() {
5994 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
5995 self.advance();
5996 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
5997 {
5998 self.advance();
5999 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
6000 {
6001 return Err(self.err("expected EXISTS after IF".into()));
6002 }
6003 self.advance();
6004 true
6005 } else {
6006 false
6007 };
6008 let cn = self.expect_ident_like()?;
6009 A::DropConstraint {
6010 name: cn,
6011 if_exists,
6012 }
6013 }
6014 Token::Default => {
6015 self.advance();
6016 A::DropDefault
6017 }
6018 Token::Not => {
6019 self.advance();
6020 if !matches!(self.peek(), Token::Null) {
6021 return Err(self.err("expected NULL after NOT".into()));
6022 }
6023 self.advance();
6024 A::DropNotNull
6025 }
6026 other => {
6027 return Err(self.err(alloc::format!(
6028 "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
6029 )));
6030 }
6031 }
6032 }
6033 "set" => {
6034 self.advance();
6035 match self.peek() {
6036 Token::Default => {
6037 self.advance();
6038 A::SetDefault(self.parse_expr(0)?)
6039 }
6040 Token::Not => {
6041 self.advance();
6042 if !matches!(self.peek(), Token::Null) {
6043 return Err(self.err("expected NULL after NOT".into()));
6044 }
6045 self.advance();
6046 A::SetNotNull
6047 }
6048 other => {
6049 return Err(self.err(alloc::format!(
6050 "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6051 )));
6052 }
6053 }
6054 }
6055 "rename" => {
6056 self.advance();
6057 if !matches!(self.peek(), Token::To) {
6058 return Err(self.err("expected TO after RENAME".into()));
6059 }
6060 self.advance();
6061 A::RenameTo(self.expect_ident_like()?)
6062 }
6063 other => {
6064 return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6065 }
6066 };
6067 Ok(Statement::AlterDomain { name, action })
6068 }
6069
6070 fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6071 let if_exists = self.parse_if_exists();
6072 let name = self.expect_ident_like()?;
6073 // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6074 // the option list (PG allows only one or the other).
6075 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6076 self.advance();
6077 if matches!(self.peek(), Token::To) {
6078 self.advance();
6079 } else {
6080 self.expect_keyword_ident("to")?;
6081 }
6082 let new = self.expect_ident_like()?;
6083 return Ok(Statement::AlterSequence(
6084 crate::ast::AlterSequenceStatement {
6085 name,
6086 if_exists,
6087 options: crate::ast::SequenceOptions::default(),
6088 rename_to: Some(new),
6089 },
6090 ));
6091 }
6092 let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6093 Ok(Statement::AlterSequence(
6094 crate::ast::AlterSequenceStatement {
6095 name,
6096 if_exists,
6097 options,
6098 rename_to: None,
6099 },
6100 ))
6101 }
6102
6103 fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6104 let kw = self.expect_ident_like()?;
6105 match kw.to_ascii_lowercase().as_str() {
6106 "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6107 "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6108 "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6109 other => Err(self.err(alloc::format!(
6110 "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6111 ))),
6112 }
6113 }
6114
6115 fn parse_sequence_options(
6116 &mut self,
6117 allow_restart: bool,
6118 ) -> Result<crate::ast::SequenceOptions, ParseError> {
6119 use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6120 let mut opts = SequenceOptions::default();
6121 #[allow(clippy::while_let_loop)]
6122 loop {
6123 // Match an ident; stop at any non-ident token (sentinel,
6124 // semicolon, end of statement).
6125 let kw_lc = match self.peek() {
6126 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6127 _ => break,
6128 };
6129 match kw_lc.as_str() {
6130 "increment" => {
6131 self.advance();
6132 // Optional BY.
6133 if self.peek_is_by() {
6134 self.advance();
6135 }
6136 opts.increment = Some(self.expect_signed_int()?);
6137 }
6138 "minvalue" => {
6139 self.advance();
6140 opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6141 }
6142 "maxvalue" => {
6143 self.advance();
6144 opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6145 }
6146 "no" => {
6147 self.advance();
6148 let what = self.expect_ident_like()?;
6149 match what.to_ascii_lowercase().as_str() {
6150 "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6151 "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6152 "cycle" => opts.cycle = Some(false),
6153 other => {
6154 return Err(self.err(alloc::format!(
6155 "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6156 )));
6157 }
6158 }
6159 }
6160 "start" => {
6161 self.advance();
6162 // Optional WITH.
6163 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6164 if s.eq_ignore_ascii_case("with"))
6165 {
6166 self.advance();
6167 }
6168 opts.start = Some(self.expect_signed_int()?);
6169 }
6170 "restart" if allow_restart => {
6171 self.advance();
6172 // Optional WITH n; bare RESTART means restart at START.
6173 let mut with_val: Option<i64> = None;
6174 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6175 if s.eq_ignore_ascii_case("with"))
6176 {
6177 self.advance();
6178 with_val = Some(self.expect_signed_int()?);
6179 } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6180 with_val = Some(self.expect_signed_int()?);
6181 }
6182 opts.restart = Some(with_val);
6183 }
6184 "cache" => {
6185 self.advance();
6186 opts.cache = Some(self.expect_signed_int()?);
6187 }
6188 "cycle" => {
6189 self.advance();
6190 opts.cycle = Some(true);
6191 }
6192 "owned" => {
6193 self.advance();
6194 match self.peek() {
6195 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6196 self.advance();
6197 }
6198 other => {
6199 return Err(
6200 self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6201 );
6202 }
6203 }
6204 // OWNED BY {NONE | tab.col}. Read just one ident
6205 // (NOT expect_ident_like which would auto-strip
6206 // a schema prefix and consume the `.col` we need).
6207 let first = match self.advance() {
6208 Token::Ident(s) | Token::QuotedIdent(s) => s,
6209 other => {
6210 return Err(self.err(alloc::format!(
6211 "expected identifier or NONE after OWNED BY, got {other:?}"
6212 )));
6213 }
6214 };
6215 if first.eq_ignore_ascii_case("none") {
6216 opts.owned_by = Some(SequenceOwnedBy::None);
6217 } else if matches!(self.peek(), Token::Dot) {
6218 self.advance();
6219 let second = match self.advance() {
6220 Token::Ident(s) | Token::QuotedIdent(s) => s,
6221 other => {
6222 return Err(self.err(alloc::format!(
6223 "expected column name after OWNED BY {first}., got {other:?}"
6224 )));
6225 }
6226 };
6227 // v7.17 dump-compat fix — pg_dump emits
6228 // OWNED BY clauses as
6229 // `schema.table.column` (three segments).
6230 // If a third `.<ident>` follows, treat the
6231 // first ident as schema (drop it; SPG is
6232 // single-schema) and the middle / last
6233 // pair as table.column. Otherwise it's
6234 // the two-segment form table.column.
6235 if matches!(self.peek(), Token::Dot) {
6236 self.advance();
6237 let third = match self.advance() {
6238 Token::Ident(s) | Token::QuotedIdent(s) => s,
6239 other => {
6240 return Err(self.err(alloc::format!(
6241 "expected column name after OWNED BY {first}.{second}., got {other:?}"
6242 )));
6243 }
6244 };
6245 let _ = first; // schema prefix discarded
6246 opts.owned_by = Some(SequenceOwnedBy::Column {
6247 table: second,
6248 column: third,
6249 });
6250 } else {
6251 opts.owned_by = Some(SequenceOwnedBy::Column {
6252 table: first,
6253 column: second,
6254 });
6255 }
6256 } else {
6257 return Err(self.err(alloc::format!(
6258 "expected table.column or NONE after OWNED BY, got {first:?}"
6259 )));
6260 }
6261 }
6262 _ => break,
6263 }
6264 }
6265 Ok(opts)
6266 }
6267
6268 fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6269 let neg = if matches!(self.peek(), Token::Minus) {
6270 self.advance();
6271 true
6272 } else {
6273 false
6274 };
6275 match self.peek() {
6276 Token::Integer(n) => {
6277 let v = *n;
6278 self.advance();
6279 Ok(if neg { -v } else { v })
6280 }
6281 other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6282 }
6283 }
6284
6285 /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6286 /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6287 /// clause is fully accepted and discarded — SPG always runs
6288 /// constraint checks immediately (single-writer model). The
6289 /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6290 /// in either order (per the SQL spec they're independent),
6291 /// though pg_dump always emits them in the canonical
6292 /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6293 /// Stops at the first token that isn't part of the clause.
6294 fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6295 self.consume_deferrable_clauses_timed().map(|_| ())
6296 }
6297
6298 /// v7.39 (round 288) — the same scan, but reporting what it saw:
6299 /// `(deferrable, initially_deferred)`. The clauses were parsed and
6300 /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6301 /// NOT DEFERRABLE and a circular-FK migration could not load.
6302 fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6303 let mut deferrable = false;
6304 let mut initially_deferred = false;
6305 loop {
6306 // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6307 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6308 self.advance();
6309 deferrable = true;
6310 if self.consume_optional_initially_clause()? {
6311 initially_deferred = true;
6312 }
6313 continue;
6314 }
6315 // `NOT DEFERRABLE` — already worked pre-3.1.
6316 if matches!(self.peek(), Token::Not) {
6317 let look = self.tokens.get(self.pos + 1);
6318 if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6319 self.advance(); // NOT
6320 self.advance(); // DEFERRABLE
6321 deferrable = false;
6322 initially_deferred = false;
6323 let _ = self.consume_optional_initially_clause()?;
6324 continue;
6325 }
6326 break;
6327 }
6328 // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6329 // accepts this without a leading [NOT] DEFERRABLE
6330 // (the timing keyword alone). pg_dump occasionally
6331 // emits it on FK constraints that inherit timing.
6332 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6333 if self.consume_optional_initially_clause()? {
6334 initially_deferred = true;
6335 // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6336 deferrable = true;
6337 }
6338 continue;
6339 }
6340 break;
6341 }
6342 Ok((deferrable, initially_deferred))
6343 }
6344
6345 /// Helper for [`consume_optional_deferrable_clauses`]. When the
6346 /// next token is `INITIALLY`, consume it plus the required
6347 /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6348 /// Returns true when the timing seen was `DEFERRED`.
6349 fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6350 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6351 return Ok(false);
6352 }
6353 self.advance(); // INITIALLY
6354 match self.advance() {
6355 Token::Ident(s)
6356 if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6357 {
6358 Ok(s.eq_ignore_ascii_case("deferred"))
6359 }
6360 other => Err(self.err(alloc::format!(
6361 "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6362 ))),
6363 }
6364 }
6365
6366 /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6367 /// in its entirety so the parser returns Empty without
6368 /// touching the runtime. The CREATE+PROCEDURE keywords are
6369 /// already consumed; this swallows everything from the
6370 /// procedure name through the matching `END`, including
6371 /// nested `BEGIN`/`END` blocks, internal `;` terminators
6372 /// (DELIMITER `//` makes the script splitter forward the
6373 /// whole block as one statement), `@var` session-variable
6374 /// references, and the trailing terminator.
6375 ///
6376 /// Tracks nesting depth so:
6377 /// BEGIN
6378 /// IF cond THEN
6379 /// BEGIN ... END;
6380 /// END IF;
6381 /// END
6382 /// terminates at the outer END.
6383 fn consume_mysql_routine_body(&mut self) {
6384 // Outer skeleton: name, (...), optional clauses, BEGIN
6385 // <body> END [;]. Scan for the first BEGIN — anything
6386 // before it is signature decoration we don't care about.
6387 // Once inside BEGIN, count up on BEGIN, down on END.
6388 let mut depth: i32 = 0;
6389 let mut started = false;
6390 loop {
6391 match self.peek().clone() {
6392 Token::Begin => {
6393 self.advance();
6394 depth += 1;
6395 started = true;
6396 }
6397 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6398 self.advance();
6399 if started {
6400 depth -= 1;
6401 if depth <= 0 {
6402 // Optional trailing ident (`END IF`,
6403 // `END LOOP`, `END WHILE`, `END CASE`,
6404 // `END label_name`) — eat the next
6405 // ident if present so we don't
6406 // mistake `END IF;` for the outer
6407 // close.
6408 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6409 // If the next token is one of the
6410 // PL/SQL block-closer keywords,
6411 // the END belongs to an inner
6412 // block; bump depth back up.
6413 let is_inner_close = matches!(
6414 self.peek(),
6415 Token::Ident(s) | Token::QuotedIdent(s)
6416 if matches!(
6417 s.to_ascii_lowercase().as_str(),
6418 "if" | "loop" | "while" | "case" | "repeat"
6419 )
6420 );
6421 if is_inner_close {
6422 self.advance();
6423 depth += 1;
6424 continue;
6425 }
6426 }
6427 // Eat optional trailing `;`.
6428 if matches!(self.peek(), Token::Semicolon) {
6429 self.advance();
6430 }
6431 return;
6432 }
6433 }
6434 }
6435 Token::Eof => return,
6436 _ => {
6437 self.advance();
6438 }
6439 }
6440 }
6441 }
6442
6443 /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6444 /// that appear between `CREATE` and `VIEW` in mysqldump output:
6445 ///
6446 /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6447 /// * `DEFINER = <user>` (user may be a quoted string, a bare
6448 /// ident, or `ident @ ident-or-quoted-string` host form)
6449 /// * `SQL SECURITY {DEFINER|INVOKER}`
6450 ///
6451 /// Each clause may appear at most once but in any order.
6452 /// The hints are pure planner / permission metadata that
6453 /// SPG's view-rewrite engine handles uniformly; we accept
6454 /// and discard. Returns `Ok(())` once a non-clause token is
6455 /// peeked (the caller then checks for the `VIEW` keyword).
6456 fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6457 loop {
6458 match self.peek().clone() {
6459 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6460 self.advance(); // ALGORITHM
6461 // Optional `=`. MySQL spec requires it but be
6462 // generous.
6463 if matches!(self.peek(), Token::Eq) {
6464 self.advance();
6465 }
6466 // UNDEFINED / MERGE / TEMPTABLE — accept any
6467 // bare ident; unknown values still parse so
6468 // future MySQL versions don't break.
6469 if matches!(
6470 self.peek(),
6471 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6472 ) {
6473 self.advance();
6474 }
6475 }
6476 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6477 self.advance(); // DEFINER
6478 if matches!(self.peek(), Token::Eq) {
6479 self.advance();
6480 }
6481 // User: quoted string, ident, OR ident @ host
6482 // (host may itself be quoted or bare).
6483 match self.peek().clone() {
6484 Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6485 self.advance();
6486 // Optional `@host`.
6487 if matches!(self.peek(), Token::At) {
6488 self.advance();
6489 if matches!(
6490 self.peek(),
6491 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6492 ) {
6493 self.advance();
6494 }
6495 }
6496 }
6497 _ => {}
6498 }
6499 }
6500 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6501 // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6502 // when followed by SECURITY — the dispatcher must
6503 // not consume a bare `SQL` token (it's not a
6504 // legal CREATE prefix on its own).
6505 let save = self.pos;
6506 self.advance(); // SQL
6507 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6508 if s2.eq_ignore_ascii_case("security"))
6509 {
6510 self.advance(); // SECURITY
6511 // DEFINER / INVOKER trailing ident.
6512 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6513 self.advance();
6514 }
6515 } else {
6516 // Not a SQL SECURITY clause — roll back and
6517 // bail; the caller will error out cleanly.
6518 self.pos = save;
6519 return Ok(());
6520 }
6521 }
6522 _ => return Ok(()),
6523 }
6524 }
6525 }
6526
6527 fn parse_if_not_exists(&mut self) -> bool {
6528 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6529 {
6530 let save = self.pos;
6531 self.advance();
6532 if matches!(self.peek(), Token::Not) {
6533 self.advance();
6534 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6535 {
6536 self.advance();
6537 return true;
6538 }
6539 }
6540 self.pos = save;
6541 }
6542 false
6543 }
6544
6545 fn parse_if_exists(&mut self) -> bool {
6546 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6547 {
6548 let save = self.pos;
6549 self.advance();
6550 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6551 {
6552 self.advance();
6553 return true;
6554 }
6555 self.pos = save;
6556 }
6557 false
6558 }
6559
6560 /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6561 /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6562 /// been consumed.
6563 fn parse_create_trigger_after_keyword(
6564 &mut self,
6565 or_replace: bool,
6566 ) -> Result<Statement, ParseError> {
6567 let name = self.expect_ident_like()?;
6568 let timing = {
6569 let ident = self.expect_ident_like()?;
6570 if ident.eq_ignore_ascii_case("before") {
6571 TriggerTiming::Before
6572 } else if ident.eq_ignore_ascii_case("after") {
6573 TriggerTiming::After
6574 } else if ident.eq_ignore_ascii_case("instead") {
6575 let next = self.expect_ident_like()?;
6576 if !next.eq_ignore_ascii_case("of") {
6577 return Err(self.err(alloc::format!(
6578 "expected OF after INSTEAD in trigger timing, got {next:?}"
6579 )));
6580 }
6581 TriggerTiming::InsteadOf
6582 } else {
6583 return Err(self.err(alloc::format!(
6584 "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6585 )));
6586 }
6587 };
6588 // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6589 // OR is a reserved keyword token (Token::Or), not an Ident.
6590 // v7.13.0 — after an UPDATE event we may optionally see
6591 // `OF col, col, …` (mailrs round-5 G7). Columns are
6592 // captured into `update_columns` once across the whole
6593 // events list; multiple `UPDATE OF` clauses are rejected.
6594 let mut events: Vec<TriggerEvent> = Vec::new();
6595 let mut update_columns: Vec<String> = Vec::new();
6596 let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6597 events.push(first_ev);
6598 if !first_cols.is_empty() {
6599 update_columns = first_cols;
6600 }
6601 while matches!(self.peek(), Token::Or) {
6602 self.advance();
6603 let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6604 events.push(ev);
6605 if !cols.is_empty() {
6606 if !update_columns.is_empty() {
6607 return Err(
6608 self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6609 );
6610 }
6611 update_columns = cols;
6612 }
6613 }
6614 // ON <table>
6615 let tok = self.peek();
6616 let Token::On = tok else {
6617 return Err(self.err(alloc::format!(
6618 "expected ON after trigger events, got {tok:?}"
6619 )));
6620 };
6621 self.advance();
6622 let table = self.expect_ident_like()?;
6623 // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6624 // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6625 // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6626 // the trigger as a plain AFTER trigger (correct for every non-deferred
6627 // use; deferral timing is not yet honoured).
6628 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6629 if s.eq_ignore_ascii_case("from"))
6630 {
6631 self.advance();
6632 let _reftable = self.expect_ident_like()?;
6633 }
6634 self.consume_optional_deferrable_clauses()?;
6635 // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6636 // keyword (Token::For); EACH / ROW / STATEMENT are bare
6637 // idents.
6638 if !matches!(self.peek(), Token::For) {
6639 return Err(self.err(alloc::format!(
6640 "expected FOR EACH ROW / STATEMENT, got {:?}",
6641 self.peek()
6642 )));
6643 }
6644 self.advance();
6645 let for_each = {
6646 let e = self.expect_ident_like()?;
6647 if !e.eq_ignore_ascii_case("each") {
6648 return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6649 }
6650 let unit = self.expect_ident_like()?;
6651 if unit.eq_ignore_ascii_case("row") {
6652 TriggerForEach::Row
6653 } else if unit.eq_ignore_ascii_case("statement") {
6654 TriggerForEach::Statement
6655 } else {
6656 return Err(self.err(alloc::format!(
6657 "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6658 )));
6659 }
6660 };
6661 // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6662 let when_condition = if matches!(self.peek(),
6663 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6664 {
6665 self.advance();
6666 Some(self.parse_paren_expr("WHEN")?)
6667 } else {
6668 None
6669 };
6670 // EXECUTE FUNCTION/PROCEDURE name(...)
6671 let exec = self.expect_ident_like()?;
6672 if !exec.eq_ignore_ascii_case("execute") {
6673 return Err(self.err(alloc::format!(
6674 "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6675 )));
6676 }
6677 let fn_or_proc = self.expect_ident_like()?;
6678 if !(fn_or_proc.eq_ignore_ascii_case("function")
6679 || fn_or_proc.eq_ignore_ascii_case("procedure"))
6680 {
6681 return Err(self.err(alloc::format!(
6682 "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6683 )));
6684 }
6685 let function = self.expect_ident_like()?;
6686 // Optional empty arg list `()`.
6687 if matches!(self.peek(), Token::LParen) {
6688 self.advance();
6689 if !matches!(self.peek(), Token::RParen) {
6690 return Err(self.err(alloc::format!(
6691 "v7.12.4 trigger function calls take no args; got {:?}",
6692 self.peek()
6693 )));
6694 }
6695 self.advance();
6696 }
6697 Ok(Statement::CreateTrigger(CreateTriggerStatement {
6698 name,
6699 or_replace,
6700 timing,
6701 events,
6702 table,
6703 for_each,
6704 function,
6705 update_columns,
6706 when_condition,
6707 }))
6708 }
6709
6710 /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
6711 /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
6712 fn parse_create_rule_after_keyword(
6713 &mut self,
6714 or_replace: bool,
6715 ) -> Result<Statement, ParseError> {
6716 let name = self.expect_ident_like()?;
6717 if !matches!(self.peek(), Token::As) {
6718 return Err(self.err(alloc::format!(
6719 "expected AS in CREATE RULE, got {:?}",
6720 self.peek()
6721 )));
6722 }
6723 self.advance();
6724 if !matches!(self.peek(), Token::On) {
6725 return Err(self.err(alloc::format!(
6726 "expected ON in CREATE RULE, got {:?}",
6727 self.peek()
6728 )));
6729 }
6730 self.advance();
6731 let event = self.parse_rule_event()?;
6732 if !matches!(self.peek(), Token::To)
6733 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
6734 {
6735 return Err(self.err(alloc::format!(
6736 "expected TO after rule event, got {:?}",
6737 self.peek()
6738 )));
6739 }
6740 self.advance();
6741 let table = self.expect_ident_like()?;
6742 // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
6743 let when_condition = if matches!(self.peek(), Token::Where) {
6744 self.advance();
6745 Some(self.parse_expr(0)?)
6746 } else {
6747 None
6748 };
6749 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
6750 {
6751 return Err(self.err(alloc::format!(
6752 "expected DO in CREATE RULE, got {:?}",
6753 self.peek()
6754 )));
6755 }
6756 self.advance();
6757 // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
6758 let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
6759 {
6760 self.advance();
6761 true
6762 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
6763 self.advance();
6764 false
6765 } else {
6766 false
6767 };
6768 // `NOTHING` | `( cmd; … )` | `cmd`.
6769 let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
6770 {
6771 self.advance();
6772 Vec::new()
6773 } else if matches!(self.peek(), Token::LParen) {
6774 self.advance();
6775 let mut cmds = Vec::new();
6776 loop {
6777 cmds.push(self.parse_one_statement()?);
6778 if matches!(self.peek(), Token::Semicolon) {
6779 self.advance();
6780 if matches!(self.peek(), Token::RParen) {
6781 break;
6782 }
6783 continue;
6784 }
6785 break;
6786 }
6787 if !matches!(self.peek(), Token::RParen) {
6788 return Err(self.err(alloc::format!(
6789 "expected ) closing the CREATE RULE command list, got {:?}",
6790 self.peek()
6791 )));
6792 }
6793 self.advance();
6794 cmds
6795 } else {
6796 alloc::vec![self.parse_one_statement()?]
6797 };
6798 Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
6799 name,
6800 or_replace,
6801 event,
6802 table,
6803 instead,
6804 when_condition,
6805 commands,
6806 }))
6807 }
6808
6809 /// v7.39 (round 139) — a rule event keyword → uppercase string.
6810 fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
6811 if matches!(self.peek(), Token::Insert) {
6812 self.advance();
6813 return Ok(alloc::string::String::from("INSERT"));
6814 }
6815 if matches!(self.peek(), Token::Select) {
6816 self.advance();
6817 return Ok(alloc::string::String::from("SELECT"));
6818 }
6819 match self.peek() {
6820 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
6821 self.advance();
6822 Ok(alloc::string::String::from("UPDATE"))
6823 }
6824 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
6825 self.advance();
6826 Ok(alloc::string::String::from("DELETE"))
6827 }
6828 other => Err(self.err(alloc::format!(
6829 "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
6830 ))),
6831 }
6832 }
6833
6834 /// v7.13.0 — parse one trigger event, then optionally consume
6835 /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
6836 /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
6837 fn parse_trigger_event_with_optional_of(
6838 &mut self,
6839 ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
6840 let ev = self.parse_trigger_event()?;
6841 if !matches!(ev, TriggerEvent::Update) {
6842 return Ok((ev, Vec::new()));
6843 }
6844 // `OF` is a bare ident.
6845 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
6846 return Ok((ev, Vec::new()));
6847 }
6848 self.advance(); // OF
6849 let mut cols: Vec<String> = Vec::new();
6850 loop {
6851 cols.push(self.expect_ident_like()?);
6852 if matches!(self.peek(), Token::Comma) {
6853 self.advance();
6854 continue;
6855 }
6856 break;
6857 }
6858 if cols.is_empty() {
6859 return Err(
6860 self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
6861 );
6862 }
6863 Ok((ev, cols))
6864 }
6865
6866 /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
6867 /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
6868 /// before `BEGIN`, and IF / RAISE / embedded SQL statements
6869 /// inside the body.
6870 /// Called by [`parse_plpgsql_body`] after the body's tokens
6871 /// have been lexed into this temporary parser.
6872 pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
6873 // v7.12.6 — optional DECLARE prelude.
6874 let declarations = if matches!(
6875 self.peek(),
6876 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
6877 ) {
6878 self.advance();
6879 self.parse_plpgsql_declare_block()?
6880 } else {
6881 Vec::new()
6882 };
6883 // BEGIN keyword (PL/pgSQL — distinct from the SQL
6884 // `BEGIN` transaction-start, but we can reuse the
6885 // reserved Token::Begin since the body is a separate
6886 // lex/parse context).
6887 if !matches!(self.peek(), Token::Begin) {
6888 return Err(self.err(alloc::format!(
6889 "expected BEGIN at start of plpgsql block, got {:?}",
6890 self.peek()
6891 )));
6892 }
6893 self.advance();
6894 let statements = self.parse_plpgsql_stmt_list_until_end()?;
6895 // v7.37.20 (20.10) — optional EXCEPTION clause between the
6896 // body's last statement and the trailing END. When present
6897 // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
6898 // arms terminated by END.
6899 let exception_handlers = if matches!(
6900 self.peek(),
6901 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
6902 ) {
6903 self.advance();
6904 self.parse_plpgsql_exception_handlers()?
6905 } else {
6906 Vec::new()
6907 };
6908 Ok(PlPgSqlBlock {
6909 declarations,
6910 statements,
6911 exception_handlers,
6912 })
6913 }
6914
6915 /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
6916 /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
6917 fn parse_plpgsql_exception_handlers(
6918 &mut self,
6919 ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
6920 let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
6921 loop {
6922 // Stop at END — the block-level trailing END LOOP / END;
6923 // is handled by the caller.
6924 if matches!(
6925 self.peek(),
6926 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
6927 ) {
6928 return Ok(out);
6929 }
6930 // WHEN <cond> [OR <cond>]* THEN <body>
6931 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6932 {
6933 return Err(self.err(alloc::format!(
6934 "expected WHEN or END inside EXCEPTION clause, got {:?}",
6935 self.peek()
6936 )));
6937 }
6938 self.advance();
6939 let mut conditions: Vec<String> = Vec::new();
6940 conditions.push(self.expect_ident_like()?);
6941 while matches!(self.peek(), Token::Or) {
6942 self.advance();
6943 conditions.push(self.expect_ident_like()?);
6944 }
6945 let then_kw = self.expect_ident_like()?;
6946 if !then_kw.eq_ignore_ascii_case("then") {
6947 return Err(self.err(alloc::format!(
6948 "expected THEN after WHEN condition list, got {then_kw:?}"
6949 )));
6950 }
6951 let body = self.parse_plpgsql_stmt_list_until_end()?;
6952 out.push(crate::ast::ExceptionHandler { conditions, body });
6953 }
6954 }
6955
6956 /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
6957 /// prelude. Caller has already consumed `DECLARE`. We stop
6958 /// reading entries when we hit `BEGIN`.
6959 fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
6960 let mut out: Vec<PlPgSqlDeclare> = Vec::new();
6961 loop {
6962 if matches!(self.peek(), Token::Begin) {
6963 return Ok(out);
6964 }
6965 let name = self.expect_ident_like()?;
6966 // v7.37.20 (20.7) — type inference: if the next token is
6967 // `:=` or `=` (no explicit type), infer from the default
6968 // expression. Otherwise the ident that follows is the
6969 // declared type.
6970 //
6971 // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
6972 // (PG-standard). SPG parse-accepts and treats identically
6973 // to inference — the eventual runtime value determines
6974 // the local's type, which is faithful to how SPG handles
6975 // untyped locals today (see 20.7). Full compile-time
6976 // catalog lookup queues with v7.40 PL/pgSQL epic.
6977 let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
6978 // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
6979 // downstream declaration walker to type the local by
6980 // the runtime type of the default expression.
6981 FunctionArgType::Raw("_infer_".into())
6982 } else {
6983 let ty_token = self.expect_ident_like()?;
6984 // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
6985 // consume optional `.<ident>` qualifier + `%<KW>`
6986 // suffix. Both qualifier and suffix map to _infer_.
6987 if matches!(self.peek(), Token::Dot) {
6988 self.advance();
6989 let _ = self.expect_ident_like()?;
6990 }
6991 if matches!(self.peek(), Token::Percent) {
6992 self.advance();
6993 // Consume the trailing TYPE / ROWTYPE ident.
6994 let _ = self.expect_ident_like()?;
6995 FunctionArgType::Raw("_infer_".into())
6996 } else {
6997 match map_type_ident_to_column_type_name(&ty_token) {
6998 Some(t) => FunctionArgType::Typed(t),
6999 None => FunctionArgType::Raw(ty_token),
7000 }
7001 }
7002 };
7003 let default = match self.peek() {
7004 Token::ColonEq => {
7005 self.advance();
7006 Some(self.parse_expr(0)?)
7007 }
7008 Token::Eq => {
7009 // PL/pgSQL also accepts `=` for the
7010 // DECLARE default (PG treats them the same
7011 // in this position).
7012 self.advance();
7013 Some(self.parse_expr(0)?)
7014 }
7015 _ => None,
7016 };
7017 // Mandatory `;` between declarations.
7018 if !matches!(self.peek(), Token::Semicolon) {
7019 return Err(self.err(alloc::format!(
7020 "expected ; after DECLARE entry for {name:?}, got {:?}",
7021 self.peek()
7022 )));
7023 }
7024 self.advance();
7025 out.push(PlPgSqlDeclare { name, ty, default });
7026 }
7027 }
7028
7029 /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
7030 /// the terminating `END;` (or `END IF;` etc — handled by the
7031 /// per-construct sub-parsers). Used by both the outer block
7032 /// and the IF/ELSE branch bodies.
7033 fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
7034 let mut statements: Vec<PlPgSqlStmt> = Vec::new();
7035 loop {
7036 // Allow trailing semicolons + END.
7037 while matches!(self.peek(), Token::Semicolon) {
7038 self.advance();
7039 }
7040 // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7041 if matches!(
7042 self.peek(),
7043 Token::Ident(s) | Token::QuotedIdent(s)
7044 if s.eq_ignore_ascii_case("end")
7045 || s.eq_ignore_ascii_case("else")
7046 || s.eq_ignore_ascii_case("elsif")
7047 || s.eq_ignore_ascii_case("elseif")
7048 || s.eq_ignore_ascii_case("exception")
7049 || s.eq_ignore_ascii_case("when")
7050 ) {
7051 return Ok(statements);
7052 }
7053 // Otherwise: one statement, then expect `;` or
7054 // a block-terminator keyword.
7055 let stmt = self.parse_plpgsql_stmt()?;
7056 statements.push(stmt);
7057 match self.peek() {
7058 Token::Semicolon => {
7059 self.advance();
7060 }
7061 Token::Ident(s) | Token::QuotedIdent(s)
7062 if s.eq_ignore_ascii_case("end")
7063 || s.eq_ignore_ascii_case("else")
7064 || s.eq_ignore_ascii_case("elsif")
7065 || s.eq_ignore_ascii_case("elseif")
7066 || s.eq_ignore_ascii_case("exception")
7067 || s.eq_ignore_ascii_case("when") =>
7068 {
7069 // Final statement of the block without `;`.
7070 }
7071 other => {
7072 return Err(self.err(alloc::format!(
7073 "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7074 )));
7075 }
7076 }
7077 }
7078 }
7079
7080 fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7081 // RETURN keyword?
7082 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7083 {
7084 self.advance();
7085 return self.parse_plpgsql_return();
7086 }
7087 // v7.12.6 — IF block.
7088 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7089 {
7090 self.advance();
7091 return self.parse_plpgsql_if();
7092 }
7093 // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7094 // Detected by peeking that token pos+3 is Ident("execute").
7095 if matches!(self.peek(), Token::For)
7096 && matches!(
7097 self.tokens.get(self.pos + 1),
7098 Some(Token::Ident(_) | Token::QuotedIdent(_))
7099 )
7100 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7101 && matches!(
7102 self.tokens.get(self.pos + 3),
7103 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7104 )
7105 {
7106 self.advance(); // FOR
7107 let var = self.expect_ident_like()?;
7108 self.advance(); // IN
7109 self.advance(); // EXECUTE
7110 // Prescan for LOOP at paren depth 0 so parse_expr stops
7111 // before the LOOP keyword (same trick as the bare-SELECT
7112 // ForQuery arm).
7113 let mut depth: i32 = 0;
7114 let mut loop_pos: Option<usize> = None;
7115 let mut scan = self.pos;
7116 while scan < self.tokens.len() {
7117 match self.tokens.get(scan) {
7118 Some(Token::LParen) => depth += 1,
7119 Some(Token::RParen) => depth -= 1,
7120 Some(Token::Ident(s) | Token::QuotedIdent(s))
7121 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7122 {
7123 loop_pos = Some(scan);
7124 break;
7125 }
7126 _ => {}
7127 }
7128 scan += 1;
7129 }
7130 let loop_pos = loop_pos.ok_or_else(|| {
7131 self.err(alloc::format!(
7132 "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7133 ))
7134 })?;
7135 let saved_loop = self.tokens[loop_pos].clone();
7136 self.tokens[loop_pos] = Token::Semicolon;
7137 let expr_result = self.parse_expr(0);
7138 self.tokens[loop_pos] = saved_loop;
7139 let sql_expr = expr_result?;
7140 let loop_kw = self.expect_ident_like()?;
7141 if !loop_kw.eq_ignore_ascii_case("loop") {
7142 return Err(self.err(alloc::format!(
7143 "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7144 )));
7145 }
7146 let body = self.parse_plpgsql_stmt_list_until_end()?;
7147 let end_kw = self.expect_ident_like()?;
7148 if !end_kw.eq_ignore_ascii_case("end") {
7149 return Err(self.err(alloc::format!(
7150 "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7151 )));
7152 }
7153 let loop_kw2 = self.expect_ident_like()?;
7154 if !loop_kw2.eq_ignore_ascii_case("loop") {
7155 return Err(self.err(alloc::format!(
7156 "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7157 )));
7158 }
7159 return Ok(PlPgSqlStmt::ForExecute {
7160 var,
7161 sql_expr,
7162 body,
7163 });
7164 }
7165 // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7166 //
7167 // Two syntactic forms:
7168 // FOR var IN SELECT ... ORDER BY ... LOOP ...
7169 // FOR var IN (SELECT ...) LOOP ...
7170 //
7171 // Bare-SELECT form: to keep parse_select_stmt from swallowing
7172 // the trailing `LOOP` keyword as a table alias, we prescan
7173 // forward to find LOOP at paren depth 0, splice a fake
7174 // Semicolon at that position (so SELECT parses cleanly),
7175 // then re-splice LOOP back in.
7176 //
7177 // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7178 // LOOP directly — no scan required.
7179 if matches!(self.peek(), Token::For)
7180 && matches!(
7181 self.tokens.get(self.pos + 1),
7182 Some(Token::Ident(_) | Token::QuotedIdent(_))
7183 )
7184 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7185 && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7186 || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7187 {
7188 self.advance(); // FOR
7189 let var = self.expect_ident_like()?;
7190 // IN
7191 self.advance();
7192 let query = if matches!(self.peek(), Token::LParen) {
7193 // Paren-wrapped SELECT.
7194 self.advance();
7195 let inner = self.parse_select_stmt()?;
7196 let Statement::Select(q) = inner else {
7197 return Err(self.err(alloc::format!(
7198 "expected SELECT inside (…), got {:?}",
7199 self.peek()
7200 )));
7201 };
7202 if !matches!(self.peek(), Token::RParen) {
7203 return Err(self.err(alloc::format!(
7204 "expected ')' after FOR-IN-SELECT body, got {:?}",
7205 self.peek()
7206 )));
7207 }
7208 self.advance();
7209 q
7210 } else {
7211 // Bare SELECT: prescan to find the LOOP boundary.
7212 let mut depth: i32 = 0;
7213 let mut loop_pos: Option<usize> = None;
7214 let mut scan = self.pos;
7215 while scan < self.tokens.len() {
7216 match self.tokens.get(scan) {
7217 Some(Token::LParen) => depth += 1,
7218 Some(Token::RParen) => depth -= 1,
7219 Some(Token::Ident(s) | Token::QuotedIdent(s))
7220 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7221 {
7222 loop_pos = Some(scan);
7223 break;
7224 }
7225 _ => {}
7226 }
7227 scan += 1;
7228 }
7229 let loop_pos = loop_pos.ok_or_else(|| {
7230 self.err(alloc::format!(
7231 "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7232 ))
7233 })?;
7234 // Swap the LOOP token with a synthetic Semicolon so
7235 // parse_select_stmt stops there, then restore afterward.
7236 let saved_loop = self.tokens[loop_pos].clone();
7237 self.tokens[loop_pos] = Token::Semicolon;
7238 let parse_result = self.parse_select_stmt();
7239 self.tokens[loop_pos] = saved_loop;
7240 let inner = parse_result?;
7241 let Statement::Select(q) = inner else {
7242 return Err(self.err(alloc::format!(
7243 "expected SELECT after FOR <var> IN, got {:?}",
7244 self.peek()
7245 )));
7246 };
7247 q
7248 };
7249 let loop_kw = self.expect_ident_like()?;
7250 if !loop_kw.eq_ignore_ascii_case("loop") {
7251 return Err(self.err(alloc::format!(
7252 "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7253 )));
7254 }
7255 let body = self.parse_plpgsql_stmt_list_until_end()?;
7256 let end_kw = self.expect_ident_like()?;
7257 if !end_kw.eq_ignore_ascii_case("end") {
7258 return Err(self.err(alloc::format!(
7259 "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7260 )));
7261 }
7262 let loop_kw2 = self.expect_ident_like()?;
7263 if !loop_kw2.eq_ignore_ascii_case("loop") {
7264 return Err(self.err(alloc::format!(
7265 "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7266 )));
7267 }
7268 return Ok(PlPgSqlStmt::ForQuery {
7269 var,
7270 query: Box::new(query),
7271 body,
7272 });
7273 }
7274 // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7275 // FOR is a reserved keyword token (Token::For).
7276 if matches!(self.peek(), Token::For)
7277 && matches!(
7278 self.tokens.get(self.pos + 1),
7279 Some(Token::Ident(_) | Token::QuotedIdent(_))
7280 )
7281 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7282 {
7283 self.advance(); // FOR
7284 let var = self.expect_ident_like()?;
7285 if !matches!(self.peek(), Token::In) {
7286 return Err(self.err(alloc::format!(
7287 "expected IN after FOR <var>, got {:?}",
7288 self.peek()
7289 )));
7290 }
7291 self.advance();
7292 let reverse = matches!(
7293 self.peek(),
7294 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7295 );
7296 if reverse {
7297 self.advance();
7298 }
7299 let start = self.parse_expr(0)?;
7300 if !matches!(self.peek(), Token::DotDot) {
7301 return Err(self.err(alloc::format!(
7302 "expected '..' between FOR loop bounds, got {:?}",
7303 self.peek()
7304 )));
7305 }
7306 self.advance();
7307 let end = self.parse_expr(0)?;
7308 let loop_kw = self.expect_ident_like()?;
7309 if !loop_kw.eq_ignore_ascii_case("loop") {
7310 return Err(self.err(alloc::format!(
7311 "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7312 )));
7313 }
7314 let body = self.parse_plpgsql_stmt_list_until_end()?;
7315 let end_kw = self.expect_ident_like()?;
7316 if !end_kw.eq_ignore_ascii_case("end") {
7317 return Err(self.err(alloc::format!(
7318 "expected END LOOP after FOR body, got {end_kw:?}"
7319 )));
7320 }
7321 let loop_kw2 = self.expect_ident_like()?;
7322 if !loop_kw2.eq_ignore_ascii_case("loop") {
7323 return Err(self.err(alloc::format!(
7324 "expected END LOOP after FOR body, got END {loop_kw2:?}"
7325 )));
7326 }
7327 return Ok(PlPgSqlStmt::ForRange {
7328 var,
7329 start,
7330 end,
7331 reverse,
7332 body,
7333 });
7334 }
7335 // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7336 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7337 {
7338 self.advance();
7339 let body = self.parse_plpgsql_stmt_list_until_end()?;
7340 let end_kw = self.expect_ident_like()?;
7341 if !end_kw.eq_ignore_ascii_case("end") {
7342 return Err(self.err(alloc::format!(
7343 "expected END LOOP after LOOP body, got {end_kw:?}"
7344 )));
7345 }
7346 let loop_kw = self.expect_ident_like()?;
7347 if !loop_kw.eq_ignore_ascii_case("loop") {
7348 return Err(self.err(alloc::format!(
7349 "expected END LOOP after LOOP body, got END {loop_kw:?}"
7350 )));
7351 }
7352 return Ok(PlPgSqlStmt::Loop { body });
7353 }
7354 // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7355 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7356 {
7357 self.advance();
7358 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7359 {
7360 self.advance();
7361 Some(self.parse_expr(0)?)
7362 } else {
7363 None
7364 };
7365 return Ok(PlPgSqlStmt::Exit { when });
7366 }
7367 // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7368 // already-parsed Statement or a runtime-computed SQL string.
7369 // The disambiguator vs the extended-query-protocol `EXECUTE
7370 // <stmt_name>` (which is a top-level Statement, not a
7371 // plpgsql line) is that inside a DO block / trigger body the
7372 // EXECUTE keyword ALWAYS refers to dynamic SQL.
7373 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7374 {
7375 self.advance();
7376 let sql = self.parse_expr(0)?;
7377 return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7378 }
7379 // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7380 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7381 {
7382 self.advance();
7383 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7384 {
7385 self.advance();
7386 Some(self.parse_expr(0)?)
7387 } else {
7388 None
7389 };
7390 return Ok(PlPgSqlStmt::Continue { when });
7391 }
7392 // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7393 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7394 {
7395 self.advance();
7396 let condition = self.parse_expr(0)?;
7397 let loop_kw = self.expect_ident_like()?;
7398 if !loop_kw.eq_ignore_ascii_case("loop") {
7399 return Err(self.err(alloc::format!(
7400 "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7401 )));
7402 }
7403 let body = self.parse_plpgsql_stmt_list_until_end()?;
7404 // Expect END LOOP.
7405 let end_kw = self.expect_ident_like()?;
7406 if !end_kw.eq_ignore_ascii_case("end") {
7407 return Err(self.err(alloc::format!(
7408 "expected END LOOP after WHILE body, got {end_kw:?}"
7409 )));
7410 }
7411 let loop_kw2 = self.expect_ident_like()?;
7412 if !loop_kw2.eq_ignore_ascii_case("loop") {
7413 return Err(self.err(alloc::format!(
7414 "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7415 )));
7416 }
7417 return Ok(PlPgSqlStmt::While { condition, body });
7418 }
7419 // v7.12.6 — RAISE.
7420 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7421 {
7422 self.advance();
7423 return self.parse_plpgsql_raise();
7424 }
7425 // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7426 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7427 {
7428 self.advance();
7429 let condition = self.parse_expr(0)?;
7430 let message = if matches!(self.peek(), Token::Comma) {
7431 self.advance();
7432 Some(self.parse_expr(0)?)
7433 } else {
7434 None
7435 };
7436 return Ok(PlPgSqlStmt::Assert { condition, message });
7437 }
7438 // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7439 // "PERFORM is equivalent to SELECT but discards the
7440 // result." Side effects (function calls, RAISE inside
7441 // SQL functions, etc.) still execute. We desugar to
7442 // `SELECT <body>` and wrap in EmbeddedSql so the engine's
7443 // existing embedded-statement path handles execution +
7444 // result-discard cleanly. The result is naturally
7445 // discarded because EmbeddedSql doesn't propagate row
7446 // sets back to the plpgsql interpreter.
7447 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7448 {
7449 self.advance();
7450 // Splice a synthetic Token::Select into the stream at
7451 // the current position so parse_select_stmt parses the
7452 // remainder as a normal SELECT body. Token-stream
7453 // surgery mirrors the try_parse_plpgsql_select_into
7454 // pattern used for SELECT … INTO desugaring.
7455 self.tokens.insert(self.pos, Token::Select);
7456 let select = self.parse_select_stmt()?;
7457 let Statement::Select(s) = select else {
7458 return Err(self.err(alloc::format!(
7459 "expected SELECT body after PERFORM, got {:?}",
7460 self.peek()
7461 )));
7462 };
7463 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7464 }
7465 // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7466 // plpgsql-specific shape (mailrs round-10 migrate-042).
7467 // PG's SELECT INTO at top-level SQL would CREATE a new
7468 // table; inside plpgsql it ASSIGNS the query result to
7469 // a local variable. We detect the INTO at paren-depth
7470 // 0 between SELECT and the statement boundary; if
7471 // found, split the token stream into "pre-INTO
7472 // projection" + "var" + "post-INTO FROM/WHERE…" and
7473 // rebuild as a SelectInto with a regular SELECT body
7474 // (no INTO clause).
7475 if matches!(self.peek(), Token::Select)
7476 && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7477 {
7478 return Ok(PlPgSqlStmt::SelectInto {
7479 var: var_name,
7480 body: Box::new(select_body),
7481 });
7482 }
7483 // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7484 // SELECT can appear directly inside a trigger body; we
7485 // recurse into the regular Statement parser, which will
7486 // stop at the trailing `;` (which our caller then
7487 // consumes).
7488 // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7489 // also embed ALTER / CREATE / DROP statements; route
7490 // those through the same parser so the DO body parses
7491 // cleanly.
7492 if matches!(self.peek(), Token::Insert)
7493 || matches!(self.peek(), Token::Select)
7494 || matches!(self.peek(), Token::Create)
7495 || matches!(self.peek(), Token::Drop)
7496 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7497 if s.eq_ignore_ascii_case("update")
7498 || s.eq_ignore_ascii_case("delete")
7499 || s.eq_ignore_ascii_case("alter"))
7500 {
7501 let stmt = self.parse_one_statement()?;
7502 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7503 }
7504 // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7505 // followed by `:=` and an expression.
7506 let target = self.parse_plpgsql_assign_target()?;
7507 // PL/pgSQL assignment uses `:=`. The lexer represents
7508 // this as a colon followed by `=`; check both shapes.
7509 match self.peek() {
7510 Token::ColonEq => {
7511 self.advance();
7512 }
7513 Token::Colon => {
7514 self.advance();
7515 if !matches!(self.peek(), Token::Eq) {
7516 return Err(self.err(alloc::format!(
7517 "expected := after plpgsql assign target, got `:` then {:?}",
7518 self.peek()
7519 )));
7520 }
7521 self.advance();
7522 }
7523 other => {
7524 return Err(self.err(alloc::format!(
7525 "expected := after plpgsql assign target, got {other:?}"
7526 )));
7527 }
7528 }
7529 let value = self.parse_expr(0)?;
7530 Ok(PlPgSqlStmt::Assign { target, value })
7531 }
7532
7533 /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7534 /// [ELSE body] END IF`. `IF` keyword already consumed.
7535 fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7536 let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7537 let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7538 loop {
7539 // <expr> THEN
7540 let cond = self.parse_expr(0)?;
7541 let then_kw = self.expect_ident_like()?;
7542 if !then_kw.eq_ignore_ascii_case("then") {
7543 return Err(self.err(alloc::format!(
7544 "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7545 )));
7546 }
7547 let body = self.parse_plpgsql_stmt_list_until_end()?;
7548 branches.push((cond, body));
7549 // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7550 match self.peek() {
7551 Token::Ident(s) | Token::QuotedIdent(s)
7552 if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7553 {
7554 self.advance();
7555 continue;
7556 }
7557 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7558 self.advance();
7559 else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7560 break;
7561 }
7562 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7563 break;
7564 }
7565 other => {
7566 return Err(self.err(alloc::format!(
7567 "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7568 )));
7569 }
7570 }
7571 }
7572 // Expect `END IF` (the END keyword is the one we're
7573 // looking at right now).
7574 let end_kw = self.expect_ident_like()?;
7575 if !end_kw.eq_ignore_ascii_case("end") {
7576 return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7577 }
7578 let if_kw = self.expect_ident_like()?;
7579 if !if_kw.eq_ignore_ascii_case("if") {
7580 return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7581 }
7582 Ok(PlPgSqlStmt::If {
7583 branches,
7584 else_branch,
7585 })
7586 }
7587
7588 /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7589 /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7590 /// is already consumed.
7591 fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7592 let lvl_ident = self.expect_ident_like()?;
7593 let level = match lvl_ident.to_ascii_lowercase().as_str() {
7594 "notice" => RaiseLevel::Notice,
7595 "warning" => RaiseLevel::Warning,
7596 "info" => RaiseLevel::Info,
7597 "log" => RaiseLevel::Log,
7598 "debug" => RaiseLevel::Debug,
7599 "exception" => RaiseLevel::Exception,
7600 other => {
7601 return Err(self.err(alloc::format!(
7602 "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7603 )));
7604 }
7605 };
7606 // Message: required for v7.12.6. PG accepts a bare
7607 // RAISE-rethrow form (no message), reserved for future
7608 // RAISE-no-args support.
7609 let Token::String(msg) = self.peek() else {
7610 return Err(self.err(alloc::format!(
7611 "expected RAISE message string, got {:?}",
7612 self.peek()
7613 )));
7614 };
7615 let message = msg.clone();
7616 self.advance();
7617 // Optional comma-separated args (PG `%` format substitution).
7618 let mut args: Vec<Expr> = Vec::new();
7619 while matches!(self.peek(), Token::Comma) {
7620 self.advance();
7621 args.push(self.parse_expr(0)?);
7622 }
7623 Ok(PlPgSqlStmt::Raise {
7624 level,
7625 message,
7626 args,
7627 })
7628 }
7629
7630 /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7631 /// <projection> INTO <var> [FROM …]` (mailrs round-10
7632 /// migrate-042). Returns `(rebuilt_select_without_into,
7633 /// var_name)` when the pattern matches; `None` for
7634 /// regular SELECTs (those go through the embedded-SQL
7635 /// path). Token-stream surgery so the rebuilt SELECT
7636 /// parses through the regular `parse_select_stmt`.
7637 #[allow(clippy::too_many_lines)]
7638 fn try_parse_plpgsql_select_into(
7639 &mut self,
7640 ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7641 // Scan forward from `self.pos + 1` (past Token::Select)
7642 // for Token::Into at paren-depth 0, stopping at the
7643 // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7644 // end the plpgsql statement.
7645 let start = self.pos;
7646 let mut into_pos: Option<usize> = None;
7647 let mut depth: i32 = 0;
7648 let mut i = start + 1;
7649 while i < self.tokens.len() {
7650 match &self.tokens[i] {
7651 Token::LParen => depth += 1,
7652 Token::RParen => depth -= 1,
7653 Token::Semicolon if depth == 0 => break,
7654 Token::Ident(s)
7655 if depth == 0
7656 && (s.eq_ignore_ascii_case("end")
7657 || s.eq_ignore_ascii_case("else")
7658 || s.eq_ignore_ascii_case("elsif")) =>
7659 {
7660 break;
7661 }
7662 Token::Into if depth == 0 => {
7663 into_pos = Some(i);
7664 break;
7665 }
7666 _ => {}
7667 }
7668 i += 1;
7669 }
7670 let Some(into_at) = into_pos else {
7671 return Ok(None);
7672 };
7673 // The token immediately after INTO must be the target
7674 // var ident; anything else (e.g. INSERT INTO table)
7675 // ruled out by the depth-0 check above. Capture it.
7676 let var = match self.tokens.get(into_at + 1) {
7677 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7678 other => {
7679 return Err(self.err(alloc::format!(
7680 "expected variable name after SELECT … INTO, got {other:?}"
7681 )));
7682 }
7683 };
7684 // Find the end of the plpgsql SELECT INTO statement —
7685 // same boundary rules as the depth-0 scan above.
7686 let mut end = into_at + 2;
7687 let mut depth2: i32 = 0;
7688 while end < self.tokens.len() {
7689 match &self.tokens[end] {
7690 Token::LParen => depth2 += 1,
7691 Token::RParen => depth2 -= 1,
7692 Token::Semicolon if depth2 == 0 => break,
7693 Token::Ident(s)
7694 if depth2 == 0
7695 && (s.eq_ignore_ascii_case("end")
7696 || s.eq_ignore_ascii_case("else")
7697 || s.eq_ignore_ascii_case("elsif")) =>
7698 {
7699 break;
7700 }
7701 _ => {}
7702 }
7703 end += 1;
7704 }
7705 // Rebuild a token stream that represents the SELECT
7706 // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
7707 // post-var tokens up to statement end]. Run the
7708 // regular `parse_select_stmt` against it.
7709 let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
7710 for j in start..into_at {
7711 rebuilt.push(self.tokens[j].clone());
7712 }
7713 for j in (into_at + 2)..end {
7714 rebuilt.push(self.tokens[j].clone());
7715 }
7716 rebuilt.push(Token::Eof);
7717 let saved_pos = self.pos;
7718 let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
7719 self.pos = 0;
7720 // parse_select_stmt → parse_bare_select consumes Token::Select itself.
7721 if !matches!(self.peek(), Token::Select) {
7722 self.tokens = saved_tokens;
7723 self.pos = saved_pos;
7724 return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
7725 }
7726 let sel = self.parse_select_stmt();
7727 self.tokens = saved_tokens;
7728 self.pos = end;
7729 let sel = sel?;
7730 let Statement::Select(body) = sel else {
7731 return Err(self.err(alloc::format!(
7732 "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
7733 )));
7734 };
7735 Ok(Some((body, var)))
7736 }
7737
7738 fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
7739 // v7.16.1 — read the head token DIRECTLY rather than
7740 // via `expect_ident_like`. The v7.14.0 schema-qualifier
7741 // strip (`public.t` → `t`) inside `expect_ident_like`
7742 // greedily consumes any `ident . ident` pair, which
7743 // silently turned every `NEW.col := …` /
7744 // `OLD.col := …` plpgsql assignment into a Local("col")
7745 // assignment — the head "new"/"old" was eaten as if it
7746 // were a schema name and the Dot was consumed too, so
7747 // this function's own `peek() == Token::Dot` check
7748 // below never fired. Every BEFORE trigger that rewrote
7749 // a NEW cell was a silent no-op for two major releases
7750 // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
7751 // gate failures were investigated as v7.16.1 backlog.
7752 let head = match self.advance() {
7753 Token::Ident(s) | Token::QuotedIdent(s) => s,
7754 other => {
7755 return Err(self.err(alloc::format!(
7756 "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
7757 )));
7758 }
7759 };
7760 if matches!(self.peek(), Token::Dot) {
7761 self.advance();
7762 let col = self.expect_ident_like()?;
7763 if head.eq_ignore_ascii_case("new") {
7764 return Ok(AssignTarget::NewColumn(col));
7765 }
7766 if head.eq_ignore_ascii_case("old") {
7767 return Ok(AssignTarget::OldColumn(col));
7768 }
7769 return Err(self.err(alloc::format!(
7770 "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
7771 got {head:?}.<col>"
7772 )));
7773 }
7774 Ok(AssignTarget::Local(head))
7775 }
7776
7777 fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7778 // RETURN NEW / OLD / NULL — bare-ident forms.
7779 match self.peek() {
7780 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
7781 self.advance();
7782 return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
7783 }
7784 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
7785 self.advance();
7786 return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
7787 }
7788 Token::Null => {
7789 self.advance();
7790 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7791 }
7792 // Bare `RETURN;` (no value) — treated as `RETURN NULL`
7793 // per PL/pgSQL convention.
7794 Token::Semicolon => {
7795 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7796 }
7797 _ => {}
7798 }
7799 // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
7800 // EXECUTE <expr>. In a DO block context RETURN QUERY has no
7801 // caller-visible effect (blocks don't return sets), so we
7802 // desugar it identically to PERFORM: parse the SELECT (or
7803 // EXECUTE dynamic) as embedded SQL that runs for side
7804 // effects and discards the result. RETURN NEXT <expr>
7805 // (single-row accumulator) queues with v7.40 SETOF function
7806 // infrastructure.
7807 // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
7808 // and keep going.
7809 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
7810 {
7811 self.advance();
7812 let e = self.parse_expr(0)?;
7813 return Ok(PlPgSqlStmt::ReturnNext(e));
7814 }
7815 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
7816 {
7817 self.advance();
7818 // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
7819 // rows go to the set, like the static form. It used to desugar to a
7820 // bare ExecuteDynamic, whose result was DISCARDED.
7821 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7822 {
7823 self.advance();
7824 let sql = self.parse_expr(0)?;
7825 return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
7826 }
7827 // Bare RETURN QUERY <select>. If the current token is
7828 // not already SELECT (e.g., the user wrote `RETURN QUERY
7829 // <projection> FROM ...` in a shorthand — rare but PG
7830 // accepts a bare projection here), splice one in. Same
7831 // trick as PERFORM.
7832 if !matches!(self.peek(), Token::Select) {
7833 self.tokens.insert(self.pos, Token::Select);
7834 }
7835 let select = self.parse_select_stmt()?;
7836 let Statement::Select(s) = select else {
7837 return Err(self.err(alloc::format!(
7838 "expected SELECT body after RETURN QUERY, got {:?}",
7839 self.peek()
7840 )));
7841 };
7842 // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
7843 // to an embedded side-effect SELECT whose rows were DISCARDED, which
7844 // in a SETOF function is the entire answer thrown away.
7845 return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
7846 }
7847 // Fall through: parse a full expression.
7848 let e = self.parse_expr(0)?;
7849 Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
7850 }
7851
7852 fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
7853 // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
7854 // are ident-shaped (the parser keys off case-insensitive
7855 // match — same shape used by the top-level Update / Delete
7856 // dispatchers at parse_one_statement).
7857 if matches!(self.peek(), Token::Insert) {
7858 self.advance();
7859 return Ok(TriggerEvent::Insert);
7860 }
7861 match self.peek() {
7862 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7863 self.advance();
7864 Ok(TriggerEvent::Update)
7865 }
7866 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7867 self.advance();
7868 Ok(TriggerEvent::Delete)
7869 }
7870 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
7871 self.advance();
7872 Ok(TriggerEvent::Truncate)
7873 }
7874 other => Err(self.err(alloc::format!(
7875 "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
7876 ))),
7877 }
7878 }
7879
7880 /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
7881 /// - (no clause) → implicit `FOR ALL TABLES`
7882 /// - `FOR ALL TABLES`
7883 /// - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
7884 /// - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
7885 /// accepted as an SPG lenience. PG18-measured (round 753): PG
7886 /// REJECTS the bare plural (`invalid publication object list`,
7887 /// TABLES only pairs with IN SCHEMA); the old note claimed an
7888 /// unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
7889 fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
7890 let name = self.expect_ident_or_string()?;
7891 // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
7892 // shape so existing publications keep parsing identically.
7893 let scope = if matches!(self.peek(), Token::For) {
7894 self.advance();
7895 if matches!(self.peek(), Token::All) {
7896 self.advance();
7897 if !matches!(self.peek(), Token::Tables) {
7898 return Err(self.err(format!(
7899 "expected TABLES after FOR ALL, got {:?}",
7900 self.peek()
7901 )));
7902 }
7903 self.advance();
7904 if matches!(self.peek(), Token::Except) {
7905 self.advance();
7906 let tables = self.parse_publication_table_list()?;
7907 PublicationScope::AllTablesExcept(tables)
7908 } else {
7909 PublicationScope::AllTables
7910 }
7911 } else if matches!(self.peek(), Token::Table) {
7912 self.advance();
7913 let tables = self.parse_publication_table_list()?;
7914 PublicationScope::ForTables(tables)
7915 } else if matches!(self.peek(), Token::Tables) {
7916 // v7.39 (round 754, F31-B5) — PG18-measured: the bare
7917 // plural (`FOR TABLES t`) is REJECTED (`invalid
7918 // publication object list`); TABLES only pairs with
7919 // `IN SCHEMA`. The old arm accepted it on an
7920 // unverifiable "PG 19 accepts both" claim.
7921 self.advance();
7922 if !matches!(self.peek(), Token::In) {
7923 return Err(self.err(alloc::string::String::from(
7924 "invalid publication object list",
7925 )));
7926 }
7927 self.advance();
7928 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
7929 return Err(self.err(format!(
7930 "expected SCHEMA after FOR TABLES IN, got {:?}",
7931 self.peek()
7932 )));
7933 }
7934 self.advance();
7935 let schema = self.expect_ident_or_string()?;
7936 PublicationScope::TablesInSchema(schema)
7937 } else {
7938 return Err(self.err(format!(
7939 "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
7940 self.peek()
7941 )));
7942 }
7943 } else {
7944 PublicationScope::AllTables
7945 };
7946 Ok(Statement::CreatePublication(CreatePublicationStatement {
7947 name,
7948 scope,
7949 }))
7950 }
7951
7952 /// v6.1.3 — Comma-separated identifier list for the publication
7953 /// FOR-clause. Requires at least one entry; empty list is a
7954 /// parse error (PG behaviour). Quoted idents are accepted; the
7955 /// names round-trip through `Display` as `quote_ident(name)`.
7956 ///
7957 /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
7958 /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
7959 /// pg_dump output. SPG's publication state today is per-table
7960 /// only (matching the pre-PG-15 surface); the col list + WHERE
7961 /// are parsed so dumps load through and the table name reaches
7962 /// `PublicationScope::ForTables`, but the filter is not enforced
7963 /// at publish time. Re-open when a customer dogfood gate
7964 /// requires per-row-filter or column-subset publish semantics
7965 /// (which gates on persistent slot state landing first, 21.12).
7966 fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
7967 let first = self.parse_publication_table_entry()?;
7968 let mut out = alloc::vec![first];
7969 while matches!(self.peek(), Token::Comma) {
7970 self.advance();
7971 out.push(self.parse_publication_table_entry()?);
7972 }
7973 Ok(out)
7974 }
7975
7976 /// One table entry inside a FOR TABLE clause:
7977 /// tab_name [ (col, col, …) ] [ WHERE (predicate) ]
7978 /// Returns just the table name; the column list + WHERE predicate
7979 /// are consumed and discarded per the parse-accept-discard
7980 /// commitment above.
7981 fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
7982 let name = self.expect_ident_like()?;
7983 // Optional column list — `(col, col, …)`.
7984 if matches!(self.peek(), Token::LParen) {
7985 self.advance();
7986 // Empty parens are a PG error too; require ≥ 1 column.
7987 let _ = self.expect_ident_like()?;
7988 while matches!(self.peek(), Token::Comma) {
7989 self.advance();
7990 let _ = self.expect_ident_like()?;
7991 }
7992 if !matches!(self.peek(), Token::RParen) {
7993 return Err(self.err(alloc::format!(
7994 "expected ')' to close publication column list, got {:?}",
7995 self.peek()
7996 )));
7997 }
7998 self.advance();
7999 }
8000 // Optional row filter — `WHERE (predicate)`.
8001 if matches!(self.peek(), Token::Where) {
8002 self.advance();
8003 if !matches!(self.peek(), Token::LParen) {
8004 return Err(self.err(alloc::format!(
8005 "expected '(' after WHERE in publication row filter, got {:?}",
8006 self.peek()
8007 )));
8008 }
8009 self.advance();
8010 let _ = self.parse_expr(0)?;
8011 if !matches!(self.peek(), Token::RParen) {
8012 return Err(self.err(alloc::format!(
8013 "expected ')' to close publication WHERE filter, got {:?}",
8014 self.peek()
8015 )));
8016 }
8017 self.advance();
8018 }
8019 Ok(name)
8020 }
8021
8022 /// v6.1.4 — `CREATE SUBSCRIPTION <name>
8023 /// CONNECTION '<conn>'
8024 /// PUBLICATION <pub> [, <pub> ...]`.
8025 ///
8026 /// The clause order is fixed (CONNECTION first, then
8027 /// PUBLICATION) to match PG. No WITH-options accepted in
8028 /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
8029 fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
8030 let name = self.expect_ident_or_string()?;
8031 if !matches!(self.peek(), Token::Connection) {
8032 return Err(self.err(format!(
8033 "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
8034 self.peek()
8035 )));
8036 }
8037 self.advance();
8038 let conn_str = self.expect_string_literal()?;
8039 if !matches!(self.peek(), Token::Publication) {
8040 return Err(self.err(format!(
8041 "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8042 self.peek()
8043 )));
8044 }
8045 self.advance();
8046 // Reuse the publication FOR-list parser shape: at least one
8047 // identifier, comma-separated.
8048 let first = self.expect_ident_like()?;
8049 let mut publications = alloc::vec![first];
8050 while matches!(self.peek(), Token::Comma) {
8051 self.advance();
8052 publications.push(self.expect_ident_like()?);
8053 }
8054 Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8055 name,
8056 conn_str,
8057 publications,
8058 }))
8059 }
8060
8061 /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8062 /// All keywords after `WAIT` are bare idents in v6.1.x; no
8063 /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8064 /// that fit `u64`.
8065 /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8066 /// qualifier is a *namespace* the app owns (`app.user_id`,
8067 /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8068 /// to discard. So parse the raw segments here instead of
8069 /// `expect_ident_like`, which strips a leading `schema.` qualifier
8070 /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8071 /// a single segment and round-trip unchanged.
8072 fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8073 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8074 loop {
8075 let seg = match self.advance() {
8076 Token::Ident(s) | Token::QuotedIdent(s) => s,
8077 other if unreserved_keyword_text(&other).is_some() => {
8078 unreserved_keyword_text(&other).unwrap()
8079 }
8080 other => {
8081 return Err(ParseError {
8082 message: format!("expected parameter name, got {other:?}"),
8083 token_pos: self.consumed_pos(),
8084 });
8085 }
8086 };
8087 parts.push(seg);
8088 if matches!(self.peek(), Token::Dot) {
8089 self.advance();
8090 continue;
8091 }
8092 break;
8093 }
8094 Ok(parts.join(".").to_ascii_lowercase())
8095 }
8096
8097 fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8098 Self::parse_set_value_inner(self)
8099 }
8100
8101 fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8102 match self.advance() {
8103 Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8104 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8105 Ok(crate::ast::SetValue::Default)
8106 }
8107 Token::Ident(s) | Token::QuotedIdent(s) => {
8108 let mut accum = s;
8109 while matches!(self.peek(), Token::Dot) {
8110 self.advance();
8111 let next = self.expect_ident_like()?;
8112 accum.push('.');
8113 accum.push_str(&next);
8114 }
8115 Ok(crate::ast::SetValue::Ident(accum))
8116 }
8117 Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8118 Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8119 // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8120 // spellings that lex as keyword tokens, not idents:
8121 // `SET standard_conforming_strings = on` is in every
8122 // pg_dump preamble (`off` already lexes as an ident).
8123 // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8124 // DEFAULT lexes as its keyword token, so the ident arm above
8125 // never saw it and the everyday reset form was a syntax error.
8126 Token::Default => Ok(crate::ast::SetValue::Default),
8127 Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8128 Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8129 Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8130 // v7.14.0 — MySQL session/user variable RHS
8131 // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8132 // Wrap as Ident so the SET handler can record it; the
8133 // engine treats `@VAR` / `@@VAR` values as opaque
8134 // strings.
8135 Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8136 // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8137 // is the common MySQL preamble shape. Allow a `+` or
8138 // `-` prefix on negative numerics for parity with PG
8139 // (some param defaults are negative).
8140 Token::Minus => match self.advance() {
8141 Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8142 Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8143 other => Err(self.err(format!(
8144 "expected numeric after `-` in SET value, got {other:?}"
8145 ))),
8146 },
8147 other => Err(self.err(format!(
8148 "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8149 ))),
8150 }
8151 }
8152
8153 /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8154 /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8155 /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8156 /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8157 /// present). Modes are comma-separated per PG; SPG also
8158 /// accepts space-separated for tolerance. READ ONLY / WRITE
8159 /// / DEFERRABLE are parsed-and-ignored (recorded for future
8160 /// surface but not behaviorally honoured today).
8161 /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8162 /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8163 /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8164 /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8165 /// session default rather than forcing READ COMMITTED.
8166 fn parse_isolation_level_clauses(&mut self) -> Result<Option<IsolationLevel>, ParseError> {
8167 let mut level = IsolationLevel::default();
8168 let mut have_level = false;
8169 loop {
8170 // ISOLATION LEVEL …
8171 let saw_isolation =
8172 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8173 if saw_isolation {
8174 self.advance(); // ISOLATION
8175 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8176 return Err(self.err(alloc::format!(
8177 "expected LEVEL after ISOLATION, got {:?}",
8178 self.peek()
8179 )));
8180 }
8181 self.advance(); // LEVEL
8182 // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8183 let w1 = self
8184 .expect_ident_like()
8185 .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8186 let lc = w1.to_ascii_lowercase();
8187 level = match lc.as_str() {
8188 "serializable" => IsolationLevel::Serializable,
8189 "repeatable" => {
8190 // Expect READ
8191 let w2 = self
8192 .expect_ident_like()
8193 .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8194 if !w2.eq_ignore_ascii_case("read") {
8195 return Err(self.err(alloc::format!(
8196 "expected READ after REPEATABLE, got {w2:?}"
8197 )));
8198 }
8199 IsolationLevel::RepeatableRead
8200 }
8201 "read" => {
8202 let w2 = self
8203 .expect_ident_like()
8204 .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8205 match w2.to_ascii_lowercase().as_str() {
8206 "committed" => IsolationLevel::ReadCommitted,
8207 "uncommitted" => IsolationLevel::ReadUncommitted,
8208 other => {
8209 return Err(self.err(alloc::format!(
8210 "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8211 )));
8212 }
8213 }
8214 }
8215 other => {
8216 return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8217 }
8218 };
8219 have_level = true;
8220 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8221 // READ ONLY | READ WRITE — parsed, not behaviorally honoured.
8222 self.advance();
8223 match self.peek().clone() {
8224 Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8225 self.advance();
8226 }
8227 Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8228 self.advance();
8229 }
8230 other => {
8231 return Err(self.err(alloc::format!(
8232 "expected ONLY or WRITE after READ, got {other:?}"
8233 )));
8234 }
8235 }
8236 } else if matches!(self.peek(), Token::Not) {
8237 // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8238 self.advance();
8239 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8240 return Err(self.err(alloc::format!(
8241 "expected DEFERRABLE after NOT, got {:?}",
8242 self.peek()
8243 )));
8244 }
8245 self.advance();
8246 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8247 {
8248 self.advance();
8249 } else {
8250 break;
8251 }
8252 // Optional comma between modes.
8253 if matches!(self.peek(), Token::Comma) {
8254 self.advance();
8255 }
8256 }
8257 Ok(have_level.then_some(level))
8258 }
8259
8260 fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8261 // FOR is a v6.1.2-reserved keyword (Token::For). The
8262 // other two are bare idents — they've never needed lexer
8263 // support and we keep it that way.
8264 if !matches!(self.peek(), Token::For) {
8265 return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8266 }
8267 self.advance();
8268 self.expect_keyword_ident("wal")?;
8269 self.expect_keyword_ident("position")?;
8270 let pos = self.expect_u64_literal()?;
8271 let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8272 {
8273 self.advance();
8274 self.expect_keyword_ident("timeout")?;
8275 Some(self.expect_u64_literal()?)
8276 } else {
8277 None
8278 };
8279 Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8280 }
8281
8282 /// v6.1.7 helper — consume a `Token::Integer` and check it
8283 /// fits `u64`. WAL positions and millisecond timeouts are
8284 /// non-negative.
8285 fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8286 match self.advance() {
8287 Token::Integer(n) if n >= 0 => Ok(n as u64),
8288 Token::Integer(n) => Err(ParseError {
8289 message: format!("expected non-negative integer, got {n}"),
8290 token_pos: self.consumed_pos(),
8291 }),
8292 other => Err(ParseError {
8293 message: format!("expected integer literal, got {other:?}"),
8294 token_pos: self.consumed_pos(),
8295 }),
8296 }
8297 }
8298
8299 /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8300 /// ROLE '<role>' (defaults to readonly). All string slots accept
8301 /// either a quoted ident or a quoted string literal.
8302 /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8303 /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8304 ///
8305 /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8306 /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8307 /// wire role) still parses — it is a different axis from the PG attributes.
8308 /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8309 /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8310 /// or RESET, so the plain attribute forms keep their old path.
8311 fn peeks_db_role_setting(&self) -> bool {
8312 let mut i = self.pos + 1; // past the object's name
8313 let word = |p: usize| -> Option<String> {
8314 match self.tokens.get(p) {
8315 Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8316 Some(Token::In) => Some(String::from("in")),
8317 _ => None,
8318 }
8319 };
8320 if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8321 i += 3; // IN DATABASE <name>
8322 }
8323 matches!(word(i).as_deref(), Some("set" | "reset"))
8324 }
8325
8326 fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8327 use crate::ast::SetDbRoleSettingStatement;
8328 // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8329 // identifier, so the ordinary name reader refuses it. Same trap
8330 // as TABLE / INDEX / FULL / DEFAULT before it.
8331 let name = if matches!(self.peek(), Token::All) {
8332 self.advance();
8333 String::from("all")
8334 } else {
8335 self.expect_ident_or_string()?
8336 };
8337 // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8338 let all = name.eq_ignore_ascii_case("all");
8339 let (mut database, mut role) = if is_database {
8340 (Some(name), None)
8341 } else if all {
8342 (None, None)
8343 } else {
8344 (None, Some(name))
8345 };
8346 if matches!(self.peek(), Token::In) {
8347 self.advance();
8348 self.advance(); // DATABASE
8349 database = Some(self.expect_ident_or_string()?);
8350 }
8351 let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8352 self.advance(); // SET | RESET
8353 if resetting && matches!(self.peek(), Token::All) {
8354 self.advance();
8355 self.consume_until_statement_boundary();
8356 return Ok(Statement::SetDbRoleSetting(Box::new(
8357 SetDbRoleSettingStatement {
8358 database,
8359 role,
8360 param: None,
8361 value: None,
8362 },
8363 )));
8364 }
8365 let param = self.expect_ident_like()?;
8366 let value = if resetting {
8367 None
8368 } else {
8369 // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8370 // KEYWORD, so the ident-only check missed it and consumed
8371 // the word itself as the value — the same trap as ALL, one
8372 // clause over.
8373 if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8374 self.advance();
8375 }
8376 Some(self.take_guc_value())
8377 };
8378 self.consume_until_statement_boundary();
8379 Ok(Statement::SetDbRoleSetting(Box::new(
8380 SetDbRoleSettingStatement {
8381 database,
8382 role,
8383 param: Some(param),
8384 value,
8385 },
8386 )))
8387 }
8388
8389 /// The remainder of a `SET <p> = …` clause as PG renders it back:
8390 /// a quoted literal loses its quotes, a bare word or number does not.
8391 fn take_guc_value(&mut self) -> String {
8392 match self.advance() {
8393 Token::String(s) => s,
8394 Token::Integer(n) => format!("{n}"),
8395 Token::Float(f) => format!("{f}"),
8396 Token::Ident(s) | Token::QuotedIdent(s) => s,
8397 other => format!("{other:?}"),
8398 }
8399 }
8400
8401 fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8402 let name = self.expect_ident_or_string()?;
8403 if self.peek_keyword_ident("with") {
8404 self.advance();
8405 }
8406 let mut password = String::new();
8407 let mut role = String::new();
8408 let mut login: Option<bool> = None;
8409 let mut inherit: Option<bool> = None;
8410 let mut superuser: Option<bool> = None;
8411 // Not a `while let`: the pattern would borrow `self` across the
8412 // body, which calls `self.advance()` / `self.expect_*` (&mut).
8413 #[allow(clippy::while_let_loop)]
8414 loop {
8415 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8416 break;
8417 };
8418 match w.to_ascii_lowercase().as_str() {
8419 "password" => {
8420 self.advance();
8421 password = self.expect_string_literal()?;
8422 }
8423 // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8424 // is the same slot.
8425 "encrypted" => {
8426 self.advance();
8427 self.expect_keyword_ident("password")?;
8428 password = self.expect_string_literal()?;
8429 }
8430 "login" => {
8431 self.advance();
8432 login = Some(true);
8433 }
8434 "nologin" => {
8435 self.advance();
8436 login = Some(false);
8437 }
8438 "inherit" => {
8439 self.advance();
8440 inherit = Some(true);
8441 }
8442 "noinherit" => {
8443 self.advance();
8444 inherit = Some(false);
8445 }
8446 "superuser" => {
8447 self.advance();
8448 superuser = Some(true);
8449 }
8450 "nosuperuser" => {
8451 self.advance();
8452 superuser = Some(false);
8453 }
8454 // SPG's own coarse wire role: `ROLE 'readwrite'`.
8455 "role" => {
8456 self.advance();
8457 role = self.expect_string_literal()?;
8458 }
8459 // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8460 // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8461 // accepted and ignored so a pg_dump role block restores. They
8462 // gate capabilities SPG does not have.
8463 "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8464 | "noreplication" | "bypassrls" | "nobypassrls" => {
8465 self.advance();
8466 }
8467 "connection" => {
8468 self.advance();
8469 self.expect_keyword_ident("limit")?;
8470 self.advance(); // the number
8471 }
8472 "valid" => {
8473 self.advance();
8474 self.expect_keyword_ident("until")?;
8475 self.expect_string_literal()?;
8476 }
8477 _ => break,
8478 }
8479 }
8480 if role.is_empty() {
8481 role = "readonly".to_string();
8482 }
8483 Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8484 name,
8485 password,
8486 role,
8487 login,
8488 inherit,
8489 superuser,
8490 is_user,
8491 }))
8492 }
8493
8494 /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8495 /// consumed the USING / WITH CHECK keyword.
8496 fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8497 if !matches!(self.peek(), Token::LParen) {
8498 return Err(self.err(alloc::format!(
8499 "expected '(' after {clause}, got {:?}",
8500 self.peek()
8501 )));
8502 }
8503 self.advance();
8504 let e = self.parse_expr(0)?;
8505 if !matches!(self.peek(), Token::RParen) {
8506 return Err(self.err(alloc::format!(
8507 "expected ')' to close {clause}, got {:?}",
8508 self.peek()
8509 )));
8510 }
8511 self.advance();
8512 Ok(e)
8513 }
8514
8515 /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8516 fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8517 let mut roles = Vec::new();
8518 loop {
8519 roles.push(self.expect_ident_like()?);
8520 if matches!(self.peek(), Token::Comma) {
8521 self.advance();
8522 } else {
8523 break;
8524 }
8525 }
8526 Ok(roles)
8527 }
8528
8529 /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8530 /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8531 /// `CREATE POLICY`.
8532 fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8533 use crate::ast::PolicyCmd;
8534 let name = self.expect_ident_like()?;
8535 if !matches!(self.peek(), Token::On) {
8536 return Err(self.err(alloc::format!(
8537 "expected ON after CREATE POLICY name, got {:?}",
8538 self.peek()
8539 )));
8540 }
8541 self.advance();
8542 let table = self.expect_ident_like()?;
8543
8544 let mut permissive = true;
8545 if matches!(self.peek(), Token::As) {
8546 self.advance();
8547 let w = self.expect_ident_like()?;
8548 permissive = if w.eq_ignore_ascii_case("permissive") {
8549 true
8550 } else if w.eq_ignore_ascii_case("restrictive") {
8551 false
8552 } else {
8553 return Err(self.err(alloc::format!(
8554 "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8555 )));
8556 };
8557 }
8558
8559 let mut cmd = PolicyCmd::All;
8560 if matches!(self.peek(), Token::For) {
8561 self.advance();
8562 cmd = self.parse_policy_cmd()?;
8563 }
8564
8565 let mut roles = Vec::new();
8566 if matches!(self.peek(), Token::To) {
8567 self.advance();
8568 roles = self.parse_policy_roles()?;
8569 }
8570
8571 let mut using = None;
8572 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8573 {
8574 self.advance();
8575 using = Some(self.parse_paren_expr("USING")?);
8576 }
8577
8578 let mut with_check = None;
8579 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8580 {
8581 self.advance();
8582 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8583 {
8584 return Err(self.err(alloc::format!(
8585 "expected CHECK after WITH, got {:?}",
8586 self.peek()
8587 )));
8588 }
8589 self.advance();
8590 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8591 }
8592
8593 // Clause-per-command matrix (PG wording).
8594 match cmd {
8595 PolicyCmd::Insert => {
8596 if using.is_some() {
8597 return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8598 }
8599 }
8600 PolicyCmd::Select | PolicyCmd::Delete => {
8601 if with_check.is_some() {
8602 return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8603 }
8604 }
8605 PolicyCmd::Update | PolicyCmd::All => {}
8606 }
8607
8608 Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8609 name,
8610 table,
8611 permissive,
8612 cmd,
8613 roles,
8614 using,
8615 with_check,
8616 }))
8617 }
8618
8619 /// v7.39 (RLS) — the command word after `FOR`.
8620 fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8621 use crate::ast::PolicyCmd;
8622 match self.peek().clone() {
8623 Token::All => {
8624 self.advance();
8625 Ok(PolicyCmd::All)
8626 }
8627 Token::Select => {
8628 self.advance();
8629 Ok(PolicyCmd::Select)
8630 }
8631 Token::Insert => {
8632 self.advance();
8633 Ok(PolicyCmd::Insert)
8634 }
8635 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8636 self.advance();
8637 Ok(PolicyCmd::Update)
8638 }
8639 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8640 self.advance();
8641 Ok(PolicyCmd::Delete)
8642 }
8643 other => Err(self.err(alloc::format!(
8644 "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8645 ))),
8646 }
8647 }
8648
8649 /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8650 /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8651 fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8652 let name = self.expect_ident_like()?;
8653 if !matches!(self.peek(), Token::On) {
8654 return Err(self.err(alloc::format!(
8655 "expected ON after ALTER POLICY name, got {:?}",
8656 self.peek()
8657 )));
8658 }
8659 self.advance();
8660 let table = self.expect_ident_like()?;
8661
8662 // RENAME TO new
8663 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8664 {
8665 self.advance();
8666 if !matches!(self.peek(), Token::To) {
8667 return Err(self.err(alloc::format!(
8668 "expected TO after RENAME, got {:?}",
8669 self.peek()
8670 )));
8671 }
8672 self.advance();
8673 let new = self.expect_ident_like()?;
8674 return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8675 name,
8676 table,
8677 rename_to: Some(new),
8678 roles: None,
8679 using: None,
8680 with_check: None,
8681 }));
8682 }
8683
8684 let mut roles = None;
8685 if matches!(self.peek(), Token::To) {
8686 self.advance();
8687 roles = Some(self.parse_policy_roles()?);
8688 }
8689 let mut using = None;
8690 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8691 {
8692 self.advance();
8693 using = Some(self.parse_paren_expr("USING")?);
8694 }
8695 let mut with_check = None;
8696 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8697 {
8698 self.advance();
8699 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8700 {
8701 return Err(self.err(alloc::format!(
8702 "expected CHECK after WITH, got {:?}",
8703 self.peek()
8704 )));
8705 }
8706 self.advance();
8707 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8708 }
8709 Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8710 name,
8711 table,
8712 rename_to: None,
8713 roles,
8714 using,
8715 with_check,
8716 }))
8717 }
8718
8719 /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
8720 /// `DROP POLICY`.
8721 fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8722 let if_exists = self.consume_if_exists();
8723 let name = self.expect_ident_like()?;
8724 if !matches!(self.peek(), Token::On) {
8725 return Err(self.err(alloc::format!(
8726 "expected ON after DROP POLICY name, got {:?}",
8727 self.peek()
8728 )));
8729 }
8730 self.advance();
8731 let table = self.expect_ident_like()?;
8732 Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
8733 name,
8734 table,
8735 if_exists,
8736 }))
8737 }
8738}
8739fn wrap_from_leaves(
8740 e: &mut Expr,
8741 names: &[String],
8742 make: &dyn Fn(Expr) -> Expr,
8743 refs: &dyn Fn(&Expr) -> bool,
8744) {
8745 if let Expr::Column(c) = e {
8746 if c.qualifier
8747 .as_deref()
8748 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
8749 {
8750 let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
8751 *e = make(taken);
8752 }
8753 return;
8754 }
8755 match e {
8756 Expr::Binary { lhs, rhs, .. } => {
8757 wrap_from_leaves(lhs, names, make, refs);
8758 wrap_from_leaves(rhs, names, make, refs);
8759 }
8760 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
8761 wrap_from_leaves(expr, names, make, refs)
8762 }
8763 Expr::FunctionCall { args, .. } => {
8764 for a in args.iter_mut() {
8765 wrap_from_leaves(a, names, make, refs);
8766 }
8767 }
8768 Expr::Case {
8769 operand,
8770 branches,
8771 else_branch,
8772 } => {
8773 if let Some(o) = operand.as_deref_mut() {
8774 wrap_from_leaves(o, names, make, refs);
8775 }
8776 for (w, t) in branches.iter_mut() {
8777 wrap_from_leaves(w, names, make, refs);
8778 wrap_from_leaves(t, names, make, refs);
8779 }
8780 if let Some(el) = else_branch.as_deref_mut() {
8781 wrap_from_leaves(el, names, make, refs);
8782 }
8783 }
8784 // Compound variants the walk doesn't decompose: keep the
8785 // pre-D.30 behavior — wrap the whole sub-expr if it touches
8786 // a source table, so nothing regresses.
8787 other => {
8788 if refs(other) {
8789 let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
8790 *other = make(taken);
8791 }
8792 }
8793 }
8794}
8795
8796/// v7.39 (round 241) — does this expression reference any of the FROM /
8797/// USING table names (shared by the UPDATE…FROM and DELETE…USING
8798/// lowerings)?
8799fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
8800 match e {
8801 Expr::Column(c) => c
8802 .qualifier
8803 .as_deref()
8804 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
8805 Expr::Binary { lhs, rhs, .. } => {
8806 expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
8807 }
8808 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
8809 Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
8810 Expr::Case {
8811 operand,
8812 branches,
8813 else_branch,
8814 } => {
8815 operand
8816 .as_deref()
8817 .is_some_and(|o| expr_refs_tables(o, names))
8818 || branches
8819 .iter()
8820 .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
8821 || else_branch
8822 .as_deref()
8823 .is_some_and(|el| expr_refs_tables(el, names))
8824 }
8825 _ => false,
8826 }
8827}
8828
8829impl Parser {
8830 /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
8831 /// Caller already consumed the leading `UPDATE` ident.
8832 /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
8833 /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
8834 /// after the target name has been read. `JOIN` is a reserved token;
8835 /// the qualifiers are bare idents.
8836 fn peek_is_update_join_start(&self) -> bool {
8837 match self.peek() {
8838 // JOIN and its qualifiers are reserved lexer tokens (the grammar
8839 // dedicates arms to `LEFT [OUTER] JOIN` and friends).
8840 Token::Join
8841 | Token::Inner
8842 | Token::Left
8843 | Token::Right
8844 | Token::Cross
8845 | Token::Full => true,
8846 // NATURAL / STRAIGHT_JOIN arrive as bare idents.
8847 Token::Ident(s) | Token::QuotedIdent(s) => {
8848 matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
8849 }
8850 _ => false,
8851 }
8852 }
8853
8854 /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
8855 /// USER-variable assignment. Its own per-session namespace, an arbitrary
8856 /// expression on the right, and `:=` as a second spelling of `=`.
8857 ///
8858 /// Out-of-line (`inline(never)`): the statement-parse frame it is called
8859 /// from sits on the nesting recursion chain (a CTE body, a subquery),
8860 /// and holding this loop's `Vec` + `String` locals there overflowed the
8861 /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
8862 #[inline(never)]
8863 fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
8864 let mut assigns: Vec<(String, Expr)> = Vec::new();
8865 let mut settings: Vec<(String, Expr)> = Vec::new();
8866 loop {
8867 // v7.39 (round 554) — a plain NAME here is a session
8868 // setting, not a user variable. mysqldump writes the two in
8869 // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
8870 // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
8871 // changes it — and this refused the mixture outright, so no
8872 // dump could be restored past its preamble.
8873 if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
8874 self.advance();
8875 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8876 return Err(self.err(alloc::format!(
8877 "expected `=` after {name}, got {:?}",
8878 self.peek()
8879 )));
8880 }
8881 self.advance();
8882 let value = self.parse_expr(0)?;
8883 settings.push((name.to_ascii_lowercase(), value));
8884 if matches!(self.peek(), Token::Comma) {
8885 self.advance();
8886 continue;
8887 }
8888 break;
8889 }
8890 let Token::SessionVar(raw) = self.peek().clone() else {
8891 return Err(self.err(alloc::format!(
8892 "expected a user variable after SET, got {:?}",
8893 self.peek()
8894 )));
8895 };
8896 if raw.starts_with("@@") {
8897 return Err(self.err(alloc::string::String::from(
8898 "cannot mix `@@` settings with `@` user variables in one SET",
8899 )));
8900 }
8901 self.advance();
8902 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8903 return Err(self.err(alloc::format!(
8904 "expected `=` or `:=` after {raw}, got {:?}",
8905 self.peek()
8906 )));
8907 }
8908 self.advance();
8909 let value = self.parse_expr(0)?;
8910 assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
8911 if matches!(self.peek(), Token::Comma) {
8912 self.advance();
8913 continue;
8914 }
8915 break;
8916 }
8917 Ok(Statement::SetUserVars(assigns, settings))
8918 }
8919
8920 fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
8921 // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
8922 // NAMED `only` until now, which failed on `relation "only" does
8923 // not exist`. The lookahead is what keeps a table actually
8924 // called `only` working: the keyword is only a keyword when a
8925 // TABLE NAME follows it — and `SET` arrives as an identifier
8926 // here, so `UPDATE only SET a = 2` would otherwise take `SET`
8927 // for the table and die on the `=`. Measured by the pin.
8928 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
8929 if s.eq_ignore_ascii_case("only"))
8930 && matches!(
8931 self.tokens.get(self.pos + 1),
8932 Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
8933 );
8934 if only {
8935 self.advance();
8936 }
8937 let table = self.expect_ident_like()?;
8938 // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
8939 // bare spelling; a bare identifier that is the SET keyword itself
8940 // is the clause, not an alias.
8941 // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
8942 // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
8943 // multi-table form, and swallowing `LEFT` as `a`'s alias made the
8944 // following JOIN a syntax error.
8945 let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
8946 let alias = if matches!(self.peek(), Token::As) {
8947 self.advance();
8948 Some(self.expect_ident_like()?)
8949 } else {
8950 match self.peek() {
8951 Token::Ident(s) | Token::QuotedIdent(s)
8952 if !s.eq_ignore_ascii_case("set") && !starts_join =>
8953 {
8954 let a = s.clone();
8955 self.advance();
8956 Some(a)
8957 }
8958 _ => None,
8959 }
8960 };
8961 // v7.39 (round 420) — MySQL's multi-table UPDATE:
8962 // UPDATE a, b SET a.v = b.v WHERE a.id = b.id
8963 // UPDATE a JOIN b ON a.id = b.id SET a.v = b.v + 1
8964 // UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
8965 // The FIRST table is the mutation target and the rest are sources —
8966 // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
8967 // SPG already lowers onto correlated subqueries. So rewind, let
8968 // `parse_from_clause` read the whole list (it handles aliases, comma
8969 // lists, and every JOIN form), then peel the target off the front.
8970 let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
8971 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
8972 {
8973 // NOTE: `advance()` destroys the tokens it returns
8974 // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
8975 // is NOT possible — the tail is read forward, once, through the
8976 // same grammar `parse_from_clause` uses after its primary.
8977 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
8978 let mut joins = self.parse_from_joins(&target_qual)?;
8979 if joins.is_empty() {
8980 return Err(self.err(alloc::string::String::from(
8981 "multi-table UPDATE needs at least one source table",
8982 )));
8983 }
8984 let head = joins.remove(0);
8985 // A LEFT join keeps every target row (the unmatched ones see NULL
8986 // on the source side), so it must NOT get the EXISTS row filter
8987 // the inner / comma forms use.
8988 let outer = matches!(head.kind, crate::ast::JoinKind::Left);
8989 let src = FromClause {
8990 primary: head.table,
8991 joins,
8992 };
8993 (Some(src), head.on, outer)
8994 } else {
8995 (None, None, false)
8996 };
8997 self.expect_keyword_ident("set")?;
8998 let mut assignments = Vec::new();
8999 loop {
9000 // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
9001 // …)` — the parenthesized multi-assignment. Expressions
9002 // assign positionally; a subquery RHS clones per column
9003 // keeping only the Nth projection item.
9004 if matches!(self.peek(), Token::LParen) {
9005 self.advance();
9006 let mut cols = alloc::vec![self.expect_ident_like()?];
9007 while matches!(self.peek(), Token::Comma) {
9008 self.advance();
9009 cols.push(self.expect_ident_like()?);
9010 }
9011 if !matches!(self.peek(), Token::RParen) {
9012 return Err(self.err(format!(
9013 "expected ')' after SET column list, got {:?}",
9014 self.peek()
9015 )));
9016 }
9017 self.advance();
9018 if !matches!(self.peek(), Token::Eq) {
9019 return Err(self.err(format!(
9020 "expected `=` after SET column list, got {:?}",
9021 self.peek()
9022 )));
9023 }
9024 self.advance();
9025 if !matches!(self.peek(), Token::LParen) {
9026 return Err(self.err(format!(
9027 "expected '(' after SET (…) =, got {:?}",
9028 self.peek()
9029 )));
9030 }
9031 self.advance();
9032 if matches!(self.peek(), Token::Select) {
9033 let inner = match self.parse_select_stmt()? {
9034 Statement::Select(s) => s,
9035 other => {
9036 return Err(self.err(alloc::format!(
9037 "expected SELECT in SET (…) = (SELECT …), got {other:?}"
9038 )));
9039 }
9040 };
9041 if !matches!(self.peek(), Token::RParen) {
9042 return Err(self.err(format!(
9043 "expected ')' after SET subquery, got {:?}",
9044 self.peek()
9045 )));
9046 }
9047 self.advance();
9048 if inner.items.len() != cols.len() {
9049 return Err(self.err(alloc::format!(
9050 "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9051 cols.len(),
9052 inner.items.len()
9053 )));
9054 }
9055 for (i, col) in cols.into_iter().enumerate() {
9056 let mut sub = inner.clone();
9057 sub.items = alloc::vec![sub.items[i].clone()];
9058 assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9059 }
9060 } else {
9061 let mut exprs = alloc::vec![self.parse_expr(0)?];
9062 while matches!(self.peek(), Token::Comma) {
9063 self.advance();
9064 exprs.push(self.parse_expr(0)?);
9065 }
9066 if !matches!(self.peek(), Token::RParen) {
9067 return Err(self.err(format!(
9068 "expected ')' after SET row values, got {:?}",
9069 self.peek()
9070 )));
9071 }
9072 self.advance();
9073 if exprs.len() != cols.len() {
9074 return Err(self.err(alloc::format!(
9075 "SET (…) = (…) arity mismatch: {} columns, {} values",
9076 cols.len(),
9077 exprs.len()
9078 )));
9079 }
9080 for (col, e) in cols.into_iter().zip(exprs) {
9081 assignments.push((col, e));
9082 }
9083 }
9084 if matches!(self.peek(), Token::Comma) {
9085 self.advance();
9086 continue;
9087 }
9088 break;
9089 }
9090 // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9091 // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9092 // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9093 // `public.` dump qualifiers), so the qualifier has to be read off
9094 // the token stream first — otherwise `SET b.v = 888` would write
9095 // to the TARGET table's `v` while naming a source table, a
9096 // silent-wrong. A qualifier naming a SOURCE table means a
9097 // multi-TARGET update — mutating two tables in one statement —
9098 // which SPG does not model, so it is refused loudly.
9099 let set_qual: Option<String> = if mysql_from.is_some()
9100 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9101 {
9102 match self.peek() {
9103 Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9104 _ => None,
9105 }
9106 } else {
9107 None
9108 };
9109 let col = self.expect_ident_like()?;
9110 if let Some(q) = set_qual {
9111 let names_target = q.eq_ignore_ascii_case(&table)
9112 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9113 if !names_target {
9114 return Err(self.err(alloc::format!(
9115 "multi-table UPDATE can only assign to its first table \
9116 ({table}); `{q}.{col}` targets another table"
9117 )));
9118 }
9119 }
9120 // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9121 // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9122 // `__column_default` marker lowering just below). PG assigns to the
9123 // i-th (1-based) element, NULL-padding when i exceeds the length.
9124 if matches!(self.peek(), Token::LBracket) {
9125 self.advance();
9126 let index = self.parse_expr(0)?;
9127 // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9128 // (and the open `arr[lo:]`), lowered to
9129 // `__array_assign_slice`. Only the single-subscript form
9130 // parsed before, so a slice assignment was a syntax error.
9131 let mut slice_hi: Option<Option<Expr>> = None;
9132 if matches!(self.peek(), Token::Colon) {
9133 self.advance();
9134 slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9135 None
9136 } else {
9137 Some(self.parse_expr(0)?)
9138 });
9139 }
9140 if !matches!(self.peek(), Token::RBracket) {
9141 return Err(self.err(format!(
9142 "expected `]` after array subscript in UPDATE SET, got {:?}",
9143 self.peek()
9144 )));
9145 }
9146 self.advance();
9147 if !matches!(self.peek(), Token::Eq) {
9148 return Err(self.err(format!(
9149 "expected `=` after array subscript in UPDATE SET, got {:?}",
9150 self.peek()
9151 )));
9152 }
9153 self.advance();
9154 let value = self.parse_expr(0)?;
9155 // PG merges several subscript writes to the same column into one
9156 // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9157 // assignment to `col` rather than each overwriting the original.
9158 let existing = assignments.iter().position(|(c, _)| c == &col);
9159 let base = match existing {
9160 Some(i) => assignments[i].1.clone(),
9161 None => Expr::Column(ColumnName {
9162 qualifier: None,
9163 name: col.clone(),
9164 }),
9165 };
9166 let assigned = match slice_hi {
9167 None => Expr::FunctionCall {
9168 name: "__array_assign".to_string(),
9169 args: alloc::vec![base, index, value],
9170 },
9171 Some(hi) => Expr::FunctionCall {
9172 name: "__array_assign_slice".to_string(),
9173 args: alloc::vec![
9174 base,
9175 index,
9176 hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9177 value,
9178 ],
9179 },
9180 };
9181 match existing {
9182 Some(i) => assignments[i].1 = assigned,
9183 None => assignments.push((col, assigned)),
9184 }
9185 if matches!(self.peek(), Token::Comma) {
9186 self.advance();
9187 continue;
9188 }
9189 break;
9190 }
9191 if !matches!(self.peek(), Token::Eq) {
9192 return Err(self.err(format!(
9193 "expected `=` after column name in UPDATE SET, got {:?}",
9194 self.peek()
9195 )));
9196 }
9197 self.advance();
9198 // `SET col = DEFAULT` — the column's declared default;
9199 // rides out as a marker call the update executor
9200 // resolves against the schema.
9201 let value = if matches!(self.peek(), Token::Default) {
9202 self.advance();
9203 Expr::FunctionCall {
9204 name: "__column_default".to_string(),
9205 args: Vec::new(),
9206 }
9207 } else {
9208 self.parse_expr(0)?
9209 };
9210 assignments.push((col, value));
9211 if matches!(self.peek(), Token::Comma) {
9212 self.advance();
9213 continue;
9214 }
9215 break;
9216 }
9217 // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9218 // update. Lowers onto the correlated-subquery machinery:
9219 // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9220 // and each assignment that references a FROM-list table
9221 // wraps into a correlated scalar subquery
9222 // (SELECT expr FROM src WHERE cond). Equivalent for the
9223 // unique-join shape (the overwhelmingly common one); a
9224 // multi-match, which PG resolves by arbitrary pick,
9225 // surfaces as a scalar-subquery cardinality error instead
9226 // of a silent arbitrary result.
9227 // v7.39 (round 420) — the MySQL multi-table form supplies the source
9228 // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9229 // the SAME lowering below. Both spellings together is not legal in
9230 // either dialect.
9231 let from_clause = if let Some(fc) = mysql_from {
9232 if matches!(self.peek(), Token::From) {
9233 return Err(self.err(alloc::string::String::from(
9234 "multi-table UPDATE already names its sources; drop the FROM clause",
9235 )));
9236 }
9237 Some(fc)
9238 } else if matches!(self.peek(), Token::From) {
9239 self.advance();
9240 Some(self.parse_from_clause()?)
9241 } else {
9242 None
9243 };
9244 let where_ = if matches!(self.peek(), Token::Where) {
9245 self.advance();
9246 Some(self.parse_expr(0)?)
9247 } else {
9248 None
9249 };
9250 // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9251 // and the TARGET-row filter are NOT the same predicate once a LEFT
9252 // join is involved:
9253 // * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9254 // one conjunction, and the whole thing filters target rows via
9255 // EXISTS.
9256 // * LEFT join: only the ON predicate belongs inside the source
9257 // subquery. The WHERE still filters TARGET rows (with source
9258 // columns read through the correlated subquery, which yields NULL
9259 // for an unmatched row — exactly LEFT-join semantics).
9260 // Round 420 folded ON into WHERE unconditionally and then dropped the
9261 // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9262 // WHERE a.id > 1` updated EVERY row.
9263 let sub_where = match (mysql_on.clone(), where_.clone()) {
9264 _ if mysql_outer => mysql_on.clone(),
9265 (Some(on), Some(w)) => Some(Expr::Binary {
9266 lhs: Box::new(on),
9267 op: crate::ast::BinOp::And,
9268 rhs: Box::new(w),
9269 }),
9270 (Some(on), None) => Some(on),
9271 (None, w) => w,
9272 };
9273 // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9274 // has no such clause on UPDATE, so this is accepted only under the
9275 // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9276 let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9277 let mut returning = self.parse_optional_returning()?;
9278 // v7.39 (round 533) — kept for the engine, which can resolve the
9279 // UNQUALIFIED leaves this lowering has to leave alone.
9280 let from_sources = from_clause.as_ref().map(|fc| {
9281 alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9282 from: fc.clone(),
9283 sub_where: sub_where.clone(),
9284 })
9285 });
9286 let (assignments, where_) = if let Some(fc) = from_clause {
9287 let names: Vec<String> = core::iter::once(&fc.primary)
9288 .chain(fc.joins.iter().map(|j| &j.table))
9289 .flat_map(|t| {
9290 t.alias
9291 .clone()
9292 .into_iter()
9293 .chain(core::iter::once(t.name.clone()))
9294 })
9295 .collect();
9296 let refs_list = |e: &Expr| -> bool {
9297 fn walk(e: &Expr, names: &[String]) -> bool {
9298 match e {
9299 Expr::Column(c) => c
9300 .qualifier
9301 .as_deref()
9302 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9303 Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9304 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9305 Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9306 Expr::Case {
9307 operand,
9308 branches,
9309 else_branch,
9310 } => {
9311 operand.as_deref().is_some_and(|o| walk(o, names))
9312 || branches
9313 .iter()
9314 .any(|(w, t)| walk(w, names) || walk(t, names))
9315 || else_branch.as_deref().is_some_and(|el| walk(el, names))
9316 }
9317 _ => false,
9318 }
9319 }
9320 walk(e, &names)
9321 };
9322 let sub_select = |items: Vec<SelectItem>| SelectStatement {
9323 locking: None,
9324 ctes: Vec::new(),
9325 distinct: false,
9326 distinct_on: Vec::new(),
9327 items,
9328 from: Some(fc.clone()),
9329 where_: sub_where.clone(),
9330 group_by: None,
9331 group_by_all: false,
9332 having: None,
9333 unions: Vec::new(),
9334 order_by: Vec::new(),
9335 limit: None,
9336 offset: None,
9337 limit_with_ties: false,
9338 window_check_exprs: Vec::new(),
9339 };
9340 // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9341 // assignment RHS with a correlated scalar subquery, instead of
9342 // wrapping the whole RHS. Wrapping the whole expr moved a target-
9343 // column reference (`SET v = v + u.bonus`, where `v` is the target
9344 // table's column) inside a subquery whose FROM only has the source
9345 // table, so the unqualified `v` resolved against the source and
9346 // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9347 // context — where they belong — fixes it; only the source columns
9348 // (`u.bonus`) become subqueries. A whole-expr fallback covers
9349 // compound variants the leaf-walk doesn't decompose.
9350 let make_subq = |inner: Expr| {
9351 Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9352 expr: inner,
9353 alias: None,
9354 }])))
9355 };
9356 let assignments = assignments
9357 .into_iter()
9358 .map(|(col, mut expr)| {
9359 wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9360 (col, expr)
9361 })
9362 .collect();
9363 let exists = Expr::Exists {
9364 subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9365 expr: Expr::Literal(Literal::Integer(1)),
9366 alias: None,
9367 }])),
9368 negated: false,
9369 };
9370 // v7.39 (round 241) — RETURNING may reference the FROM-list
9371 // tables too (`RETURNING emp.id, dept.name`); the same
9372 // leaf-to-correlated-subquery lowering the assignments get.
9373 // Without it the qualifier died at eval with "unknown table
9374 // qualifier". (RETURNING was parsed before this block — the
9375 // lowering is a pure AST transformation.)
9376 if let Some(items) = returning.as_mut() {
9377 for item in items.iter_mut() {
9378 if let SelectItem::Expr { expr, .. } = item {
9379 wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9380 }
9381 }
9382 }
9383 // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9384 // EVERY matching target row: it gets no EXISTS filter, but the
9385 // caller's WHERE still applies, with source columns read through
9386 // the correlated subquery (NULL when unmatched — LEFT-join
9387 // semantics). `sub_where` above already excluded the WHERE from
9388 // the source subquery for this case.
9389 if mysql_outer {
9390 let mut outer = where_;
9391 if let Some(w) = outer.as_mut() {
9392 wrap_from_leaves(w, &names, &make_subq, &refs_list);
9393 }
9394 (assignments, outer)
9395 } else {
9396 (assignments, Some(exists))
9397 }
9398 } else {
9399 (assignments, where_)
9400 };
9401 Ok(Statement::Update(crate::ast::UpdateStatement {
9402 ctes: Vec::new(),
9403 table,
9404 only,
9405 alias,
9406 assignments,
9407 from_sources,
9408 where_,
9409 order_limit: update_order_limit,
9410 returning,
9411 }))
9412 }
9413
9414 /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9415 /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9416 /// clause and its meaning are identical, so both call this rather than
9417 /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9418 /// legal. PG has no such clause on either statement, so it is read only
9419 /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9420 /// errors.
9421 ///
9422 /// `#[inline(never)]`: its locals would otherwise land on the statement-
9423 /// parsing recursion frame, which is what tipped the 512 KiB nesting
9424 /// stack in round 430.
9425 #[inline(never)]
9426 fn parse_mysql_dml_order_limit(
9427 &mut self,
9428 what: &str,
9429 ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9430 if !self.mysql_dialect {
9431 return Ok(None);
9432 }
9433 let order_by = self.parse_order_by_keys()?;
9434 let limit = if matches!(self.peek(), Token::Limit) {
9435 self.advance();
9436 let tok = self.advance();
9437 let Token::Integer(n) = tok else {
9438 return Err(self.err(alloc::format!(
9439 "expected integer after {what} LIMIT, got {tok:?}"
9440 )));
9441 };
9442 // MySQL rejects the `LIMIT offset, count` form here — only a
9443 // single row count is legal on a DML statement.
9444 if matches!(self.peek(), Token::Comma) {
9445 return Err(self.err(alloc::format!(
9446 "{what} LIMIT takes a row count, not an offset"
9447 )));
9448 }
9449 let n = u32::try_from(n)
9450 .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9451 Some(n)
9452 } else {
9453 None
9454 };
9455 if order_by.is_empty() && limit.is_none() {
9456 return Ok(None);
9457 }
9458 Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9459 order_by,
9460 limit,
9461 })))
9462 }
9463
9464 /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9465 /// the leading `DELETE` ident.
9466 fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9467 // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9468 // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9469 // USING a, b WHERE …` — the third MySQL spelling — needs no special
9470 // parse here; it reaches the existing USING path with the target
9471 // repeated in the list, which the source-list peel below handles.)
9472 // More than one name is a multi-TARGET delete, which SPG does not
9473 // model; it is refused rather than half-applied.
9474 let mysql_pre_target: Option<String> =
9475 if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9476 let first = self.expect_ident_like()?;
9477 if matches!(self.peek(), Token::Comma) {
9478 return Err(self.err(alloc::format!(
9479 "multi-table DELETE can only delete from one table; \
9480 `DELETE {first}, …` names several"
9481 )));
9482 }
9483 Some(first)
9484 } else {
9485 None
9486 };
9487 if !matches!(self.peek(), Token::From) {
9488 return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9489 }
9490 self.advance();
9491 // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9492 // lookahead as the UPDATE spelling.
9493 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9494 if s.eq_ignore_ascii_case("only"))
9495 && matches!(
9496 self.tokens.get(self.pos + 1),
9497 Some(Token::Ident(_) | Token::QuotedIdent(_))
9498 );
9499 if only {
9500 self.advance();
9501 }
9502 let table = self.expect_ident_like()?;
9503 // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9504 // spelling must not swallow the clause keywords that can follow
9505 // the target.
9506 let alias = if matches!(self.peek(), Token::As) {
9507 self.advance();
9508 Some(self.expect_ident_like()?)
9509 } else {
9510 match self.peek() {
9511 Token::Ident(s) | Token::QuotedIdent(s)
9512 if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9513 {
9514 let a = s.clone();
9515 self.advance();
9516 Some(a)
9517 }
9518 _ => None,
9519 }
9520 };
9521 // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9522 // through the SAME join grammar the FROM clause uses (see the
9523 // `advance()`-destroys-tokens note on `parse_from_joins`).
9524 let mut mysql_on: Option<Expr> = None;
9525 let mut mysql_outer = false;
9526 let mysql_using = if mysql_pre_target.is_some()
9527 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9528 {
9529 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9530 let mut joins = self.parse_from_joins(&target_qual)?;
9531 if joins.is_empty() {
9532 return Err(self.err(alloc::string::String::from(
9533 "multi-table DELETE needs at least one source table",
9534 )));
9535 }
9536 let head = joins.remove(0);
9537 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9538 mysql_on = head.on;
9539 Some(FromClause {
9540 primary: head.table,
9541 joins,
9542 })
9543 } else {
9544 None
9545 };
9546 // The pre-FROM target must be the table the FROM names (or its
9547 // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9548 // is not the scan target.
9549 if let Some(t) = &mysql_pre_target {
9550 let names_target = t.eq_ignore_ascii_case(&table)
9551 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9552 if !names_target {
9553 return Err(self.err(alloc::format!(
9554 "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9555 )));
9556 }
9557 }
9558 // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9559 // delete. Same lowering as UPDATE … FROM: the WHERE
9560 // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9561 // target row by the correlated machinery.
9562 let using_clause = if let Some(fc) = mysql_using {
9563 Some(fc)
9564 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9565 self.advance();
9566 let mut fc = self.parse_from_clause()?;
9567 // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9568 // repeats the TARGET as the first USING entry (PG's spelling
9569 // lists only the extra sources). Peel it so the source subquery
9570 // does not re-scan — and shadow — the target table.
9571 let primary_is_target =
9572 fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9573 if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9574 let head = fc.joins.remove(0);
9575 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9576 mysql_on = head.on;
9577 fc = FromClause {
9578 primary: head.table,
9579 joins: fc.joins,
9580 };
9581 }
9582 Some(fc)
9583 } else {
9584 None
9585 };
9586 let where_ = if matches!(self.peek(), Token::Where) {
9587 self.advance();
9588 Some(self.parse_expr(0)?)
9589 } else {
9590 None
9591 };
9592 // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9593 // read before RETURNING (MariaDB's own extension trails the LIMIT).
9594 let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9595 let mut returning = self.parse_optional_returning()?;
9596 let where_ = if let Some(fc) = using_clause {
9597 // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9598 // a USING-table reference in RETURNING becomes a correlated
9599 // scalar subquery over the USING list.
9600 let names: Vec<String> = core::iter::once(&fc.primary)
9601 .chain(fc.joins.iter().map(|j| &j.table))
9602 .flat_map(|t| {
9603 t.alias
9604 .clone()
9605 .into_iter()
9606 .chain(core::iter::once(t.name.clone()))
9607 })
9608 .collect();
9609 // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9610 // join filters the SOURCE subquery on the ON predicate alone and
9611 // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9612 // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9613 // rows); every other form folds ON and WHERE into one EXISTS.
9614 let sub_where = match (mysql_on.clone(), where_.clone()) {
9615 _ if mysql_outer => mysql_on.clone(),
9616 (Some(on), Some(w)) => Some(Expr::Binary {
9617 lhs: Box::new(on),
9618 op: crate::ast::BinOp::And,
9619 rhs: Box::new(w),
9620 }),
9621 (Some(on), None) => Some(on),
9622 (None, w) => w,
9623 };
9624 let exists_where = sub_where.clone();
9625 let sub_fc = fc.clone();
9626 let make_subq = move |leaf: Expr| -> Expr {
9627 Expr::ScalarSubquery(Box::new(SelectStatement {
9628 locking: None,
9629 ctes: Vec::new(),
9630 distinct: false,
9631 distinct_on: Vec::new(),
9632 items: alloc::vec![SelectItem::Expr {
9633 expr: leaf,
9634 alias: None,
9635 }],
9636 from: Some(sub_fc.clone()),
9637 where_: sub_where.clone(),
9638 group_by: None,
9639 group_by_all: false,
9640 having: None,
9641 unions: Vec::new(),
9642 order_by: Vec::new(),
9643 limit: None,
9644 offset: None,
9645 limit_with_ties: false,
9646 window_check_exprs: Vec::new(),
9647 }))
9648 };
9649 let refs = |e: &Expr| expr_refs_tables(e, &names);
9650 if let Some(items) = returning.as_mut() {
9651 for item in items.iter_mut() {
9652 if let SelectItem::Expr { expr, .. } = item {
9653 wrap_from_leaves(expr, &names, &make_subq, &refs);
9654 }
9655 }
9656 }
9657 // A LEFT join deletes the target rows the WHERE selects, reading
9658 // source columns through the correlated subquery (NULL when
9659 // unmatched); no EXISTS row filter.
9660 if mysql_outer {
9661 let mut outer = where_;
9662 if let Some(w) = outer.as_mut() {
9663 wrap_from_leaves(w, &names, &make_subq, &refs);
9664 }
9665 outer
9666 } else {
9667 Some(Expr::Exists {
9668 subquery: Box::new(SelectStatement {
9669 locking: None,
9670 ctes: Vec::new(),
9671 distinct: false,
9672 distinct_on: Vec::new(),
9673 items: alloc::vec![SelectItem::Expr {
9674 expr: Expr::Literal(Literal::Integer(1)),
9675 alias: None,
9676 }],
9677 from: Some(fc),
9678 where_: exists_where,
9679 group_by: None,
9680 group_by_all: false,
9681 having: None,
9682 unions: Vec::new(),
9683 order_by: Vec::new(),
9684 limit: None,
9685 offset: None,
9686 limit_with_ties: false,
9687 window_check_exprs: Vec::new(),
9688 }),
9689 negated: false,
9690 })
9691 }
9692 } else {
9693 where_
9694 };
9695 Ok(Statement::Delete(crate::ast::DeleteStatement {
9696 ctes: Vec::new(),
9697 table,
9698 only,
9699 alias,
9700 where_,
9701 order_limit: delete_order_limit,
9702 returning,
9703 }))
9704 }
9705
9706 /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
9707 /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
9708 /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
9709 /// keyword. v7.17 surface:
9710 /// * source: table reference (subquery source is a follow-up)
9711 /// * actions: UPDATE SET / DELETE / DO NOTHING (matched);
9712 /// INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
9713 /// * AND-conditioned WHEN clauses; clauses tried in declaration
9714 /// order
9715 fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
9716 // INTO
9717 let is_into_kw = matches!(self.peek(), Token::Into)
9718 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
9719 if !is_into_kw {
9720 return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
9721 }
9722 self.advance();
9723 let target = self.expect_ident_like()?;
9724 // Optional alias — bare ident before USING.
9725 let target_alias = match self.peek() {
9726 Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
9727 Some(self.expect_ident_like()?)
9728 }
9729 _ => None,
9730 };
9731 // USING
9732 let is_using_kw = matches!(
9733 self.peek(),
9734 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
9735 );
9736 if !is_using_kw {
9737 return Err(self.err(format!(
9738 "expected USING after MERGE INTO target, got {:?}",
9739 self.peek()
9740 )));
9741 }
9742 self.advance();
9743 // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
9744 // <table> [alias]`. PG requires an alias after a subquery source.
9745 let (source, source_select) = if matches!(self.peek(), Token::LParen) {
9746 self.advance(); // (
9747 // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
9748 // constant-SELECT lowering the derived-table parser uses
9749 // (PG deletes through this form; it was a parse error).
9750 let inner = if matches!(self.peek(), Token::Values) {
9751 self.advance(); // VALUES
9752 Statement::Select(self.parse_values_rows_body()?)
9753 } else {
9754 self.parse_select_stmt()?
9755 };
9756 match self.advance() {
9757 Token::RParen => {}
9758 other => {
9759 return Err(self.err(format!(
9760 "expected ')' after MERGE USING subquery, got {other:?}"
9761 )));
9762 }
9763 }
9764 let Statement::Select(sub) = inner else {
9765 return Err(self.err("MERGE USING subquery must be a SELECT".into()));
9766 };
9767 (String::new(), Some(Box::new(sub)))
9768 } else {
9769 (self.expect_ident_like()?, None)
9770 };
9771 let source_alias = match self.peek() {
9772 Token::Ident(s) | Token::QuotedIdent(s)
9773 if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
9774 {
9775 Some(self.expect_ident_like()?)
9776 }
9777 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
9778 self.advance(); // AS
9779 Some(self.expect_ident_like()?)
9780 }
9781 _ => None,
9782 };
9783 // v7.39 (round 768, F31-D5) — optional positional column-alias
9784 // list after the source alias (`s(id, v)`).
9785 let mut source_column_aliases: Vec<String> = Vec::new();
9786 if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
9787 self.advance();
9788 loop {
9789 source_column_aliases.push(self.expect_ident_like()?);
9790 match self.peek() {
9791 Token::Comma => {
9792 self.advance();
9793 }
9794 Token::RParen => {
9795 self.advance();
9796 break;
9797 }
9798 other => {
9799 return Err(self.err(format!(
9800 "expected ',' or ')' in MERGE source column list, got {other:?}"
9801 )));
9802 }
9803 }
9804 }
9805 }
9806 if source_select.is_some() && source_alias.is_none() {
9807 return Err(self.err("MERGE USING (subquery) requires an alias".into()));
9808 }
9809 // ON
9810 if !matches!(self.peek(), Token::On) {
9811 return Err(self.err(format!(
9812 "expected ON after MERGE … USING source, got {:?}",
9813 self.peek()
9814 )));
9815 }
9816 self.advance();
9817 let on = self.parse_expr(0)?;
9818 // One or more WHEN clauses.
9819 let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
9820 loop {
9821 let is_when_kw = matches!(
9822 self.peek(),
9823 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
9824 );
9825 if !is_when_kw {
9826 break;
9827 }
9828 self.advance(); // WHEN
9829 // [NOT] MATCHED
9830 let matched = if matches!(self.peek(), Token::Not) {
9831 self.advance();
9832 crate::ast::MergeMatched::NotMatched
9833 } else {
9834 crate::ast::MergeMatched::Matched
9835 };
9836 let is_matched_kw = matches!(
9837 self.peek(),
9838 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
9839 );
9840 if !is_matched_kw {
9841 return Err(self.err(format!(
9842 "expected MATCHED in WHEN clause, got {:?}",
9843 self.peek()
9844 )));
9845 }
9846 self.advance();
9847 // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
9848 // BY TARGET is the default (a synonym); BY SOURCE flips the clause
9849 // to fire for target rows no source row matches.
9850 let mut matched = matched;
9851 if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
9852 self.advance();
9853 match self.peek() {
9854 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
9855 self.advance();
9856 matched = crate::ast::MergeMatched::NotMatchedBySource;
9857 }
9858 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
9859 self.advance();
9860 }
9861 other => {
9862 return Err(self.err(format!(
9863 "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
9864 )));
9865 }
9866 }
9867 }
9868 // Optional AND <expr>
9869 let condition = if matches!(self.peek(), Token::And) {
9870 self.advance();
9871 Some(self.parse_expr(0)?)
9872 } else {
9873 None
9874 };
9875 // THEN
9876 let is_then_kw = matches!(
9877 self.peek(),
9878 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
9879 );
9880 if !is_then_kw {
9881 return Err(self.err(format!(
9882 "expected THEN in WHEN clause, got {:?}",
9883 self.peek()
9884 )));
9885 }
9886 self.advance();
9887 // Action: INSERT / UPDATE / DELETE / DO NOTHING
9888 let action = match self.peek().clone() {
9889 Token::Insert => {
9890 self.advance();
9891 // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
9892 // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
9893 // VALUES (…)` omits it and fills every column in declaration
9894 // order. PG accepts this; SPG used to require the list.
9895 let mut columns: Vec<String> = Vec::new();
9896 if matches!(self.peek(), Token::LParen) {
9897 self.advance();
9898 loop {
9899 columns.push(self.expect_ident_like()?);
9900 if matches!(self.peek(), Token::Comma) {
9901 self.advance();
9902 continue;
9903 }
9904 break;
9905 }
9906 if !matches!(self.peek(), Token::RParen) {
9907 return Err(self.err(format!(
9908 "expected ')' after INSERT column list, got {:?}",
9909 self.peek()
9910 )));
9911 }
9912 self.advance();
9913 }
9914 // VALUES (...)
9915 if !matches!(self.peek(), Token::Values) {
9916 return Err(self.err(format!(
9917 "expected VALUES in MERGE INSERT, got {:?}",
9918 self.peek()
9919 )));
9920 }
9921 self.advance();
9922 if !matches!(self.peek(), Token::LParen) {
9923 return Err(self.err(format!(
9924 "expected '(' after VALUES in MERGE INSERT, got {:?}",
9925 self.peek()
9926 )));
9927 }
9928 self.advance();
9929 let mut values: Vec<crate::ast::Expr> = Vec::new();
9930 loop {
9931 values.push(self.parse_expr(0)?);
9932 if matches!(self.peek(), Token::Comma) {
9933 self.advance();
9934 continue;
9935 }
9936 break;
9937 }
9938 if !matches!(self.peek(), Token::RParen) {
9939 return Err(self.err(format!(
9940 "expected ')' after MERGE INSERT values, got {:?}",
9941 self.peek()
9942 )));
9943 }
9944 self.advance();
9945 // Empty column list = positional into every column, so the
9946 // count is checked against the table arity at execution.
9947 if !columns.is_empty() && columns.len() != values.len() {
9948 return Err(self.err(format!(
9949 "MERGE INSERT column count ({}) ≠ value count ({})",
9950 columns.len(),
9951 values.len()
9952 )));
9953 }
9954 crate::ast::MergeAction::Insert { columns, values }
9955 }
9956 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
9957 self.advance();
9958 // SET
9959 let is_set_kw = matches!(
9960 self.peek(),
9961 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
9962 );
9963 if !is_set_kw {
9964 return Err(self.err(format!(
9965 "expected SET after UPDATE in MERGE, got {:?}",
9966 self.peek()
9967 )));
9968 }
9969 self.advance();
9970 let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
9971 loop {
9972 let col = self.expect_ident_like()?;
9973 if !matches!(self.peek(), Token::Eq) {
9974 return Err(self.err(format!(
9975 "expected '=' in MERGE UPDATE assignment, got {:?}",
9976 self.peek()
9977 )));
9978 }
9979 self.advance();
9980 let expr = self.parse_expr(0)?;
9981 assignments.push((col, expr));
9982 if matches!(self.peek(), Token::Comma) {
9983 self.advance();
9984 continue;
9985 }
9986 break;
9987 }
9988 crate::ast::MergeAction::Update { assignments }
9989 }
9990 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
9991 self.advance();
9992 crate::ast::MergeAction::Delete
9993 }
9994 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
9995 self.advance();
9996 let is_nothing_kw = matches!(
9997 self.peek(),
9998 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
9999 );
10000 if !is_nothing_kw {
10001 return Err(self.err(format!(
10002 "expected NOTHING after DO in MERGE clause, got {:?}",
10003 self.peek()
10004 )));
10005 }
10006 self.advance();
10007 crate::ast::MergeAction::DoNothing
10008 }
10009 other => {
10010 return Err(self.err(format!(
10011 "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
10012 )));
10013 }
10014 };
10015 // PG's grammar simply has no INSERT production under BY SOURCE
10016 // (a target row already exists there) — same syntax error.
10017 if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
10018 && matches!(action, crate::ast::MergeAction::Insert { .. })
10019 {
10020 return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
10021 }
10022 clauses.push(crate::ast::MergeWhenClause {
10023 matched,
10024 condition,
10025 action,
10026 });
10027 }
10028 if clauses.is_empty() {
10029 return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
10030 }
10031 // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
10032 // unconditional (no `AND`) WHEN of the same match kind: it could
10033 // never fire. Check per match kind in clause order.
10034 let mut seen_unconditional_matched = false;
10035 let mut seen_unconditional_not_matched = false;
10036 let mut seen_unconditional_by_source = false;
10037 for c in &clauses {
10038 let seen = match c.matched {
10039 crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10040 crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10041 crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10042 };
10043 if *seen {
10044 return Err(self.err(String::from(
10045 "unreachable WHEN clause specified after unconditional WHEN clause",
10046 )));
10047 }
10048 if c.condition.is_none() {
10049 *seen = true;
10050 }
10051 }
10052 // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10053 let returning = self.parse_optional_returning()?;
10054 Ok(Statement::Merge(crate::ast::MergeStatement {
10055 // Attached by `parse_with_cte_then_select` when the MERGE
10056 // heads a WITH clause (round 149).
10057 ctes: Vec::new(),
10058 target,
10059 target_alias,
10060 source,
10061 source_alias,
10062 source_select,
10063 source_column_aliases,
10064 on,
10065 clauses,
10066 returning,
10067 }))
10068 }
10069
10070 /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10071 /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10072 /// as SELECT, so `RETURNING *`, `RETURNING col`,
10073 /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10074 fn parse_optional_returning(
10075 &mut self,
10076 ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10077 let is_returning_kw = matches!(
10078 self.peek(),
10079 Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10080 );
10081 if !is_returning_kw {
10082 return Ok(None);
10083 }
10084 self.advance();
10085 let mut items = Vec::new();
10086 loop {
10087 items.push(self.parse_select_item()?);
10088 if matches!(self.peek(), Token::Comma) {
10089 self.advance();
10090 continue;
10091 }
10092 break;
10093 }
10094 Ok(Some(items))
10095 }
10096
10097 /// v6.0.4 — parse the tail of an ALTER statement after the
10098 /// leading `ALTER` keyword has been consumed. Only one form is
10099 /// supported in v6.0.4:
10100 ///
10101 /// ```text
10102 /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10103 /// ```
10104 fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10105 // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10106 // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10107 // exclusion) is accepted by stripping the `ONLY` keyword
10108 // before the table parse.
10109 // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10110 // and the long PG-dump tail are accepted as no-ops.
10111 match self.advance() {
10112 Token::Index => {}
10113 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10114 // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10115 // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10116 Token::Table => {
10117 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10118 self.advance();
10119 }
10120 return self.parse_alter_table_after_keyword();
10121 }
10122 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10123 return self.parse_alter_policy_after_keyword();
10124 }
10125 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10126 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10127 self.advance();
10128 }
10129 return self.parse_alter_table_after_keyword();
10130 }
10131 // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10132 // of the silent-noop tail.
10133 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10134 return self.parse_alter_sequence_after_keyword();
10135 }
10136 // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10137 // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10138 // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10139 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10140 // NB: the match arm consumed `TYPE` via self.advance(); the
10141 // cursor is now at the type name — do NOT advance again.
10142 let type_name = self.expect_ident_like()?;
10143 let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10144 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10145 if is_add_value {
10146 self.advance(); // ADD
10147 self.advance(); // VALUE
10148 // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10149 // IF/EXISTS as identifiers.
10150 let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10151 {
10152 let n1 = self.tokens.get(self.pos + 1);
10153 let n2 = self.tokens.get(self.pos + 2);
10154 if matches!(n1, Some(Token::Not))
10155 && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10156 {
10157 self.advance();
10158 self.advance();
10159 self.advance();
10160 true
10161 } else {
10162 false
10163 }
10164 } else {
10165 false
10166 };
10167 let label = self.expect_string_literal()?;
10168 let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10169 {
10170 let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10171 self.advance();
10172 let anchor = self.expect_string_literal()?;
10173 Some((is_before, anchor))
10174 } else {
10175 None
10176 };
10177 return Ok(Statement::AlterTypeAddValue {
10178 type_name,
10179 label,
10180 if_not_exists,
10181 position,
10182 });
10183 }
10184 // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10185 // Used to fall into the no-op tail below: accepted, silently
10186 // ignored. `RENAME TO <newtype>` keeps falling through.
10187 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10188 && matches!(
10189 self.tokens.get(self.pos + 1),
10190 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10191 )
10192 {
10193 self.advance(); // RENAME
10194 self.advance(); // VALUE
10195 let old = self.expect_string_literal()?;
10196 if matches!(self.peek(), Token::To) {
10197 self.advance();
10198 } else {
10199 self.expect_keyword_ident("to")?;
10200 }
10201 let new = self.expect_string_literal()?;
10202 return Ok(Statement::AlterTypeRenameValue {
10203 type_name,
10204 old,
10205 new,
10206 });
10207 }
10208 // Other ALTER TYPE forms — the ACTION stays a no-op
10209 // (pg_dump tail), but v7.39 (round 708) the NAME is
10210 // validated: `ALTER TYPE nosuch RENAME TO x` reported
10211 // success for a type that does not exist.
10212 self.consume_until_statement_boundary();
10213 return Ok(Statement::ValidateOnly {
10214 kind: crate::ast::ValidateOnlyKind::TypeName,
10215 names: alloc::vec![type_name],
10216 });
10217 }
10218 // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10219 // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10220 // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10221 // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10222 // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10223 // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10224 // pg_dump no-op list below: every form used to report success
10225 // and change nothing, which is worse than refusing outright
10226 // (a migration dropping a constraint kept rejecting data).
10227 // NOTE: the enclosing `match self.advance()` already consumed
10228 // the DOMAIN keyword, so the name is next.
10229 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10230 return self.parse_alter_domain_after_keyword();
10231 }
10232 // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10233 // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10234 // used to fall into the pg_dump no-op tail below, so a DBA
10235 // setting a per-role default was told it worked and nothing
10236 // happened. Intercepted here, BEFORE that tail.
10237 // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10238 // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10239 // interception below exists: swallowed with the no-op tail, an
10240 // unknown parameter name was ACCEPTED where PG18 answers
10241 // `unrecognized configuration parameter`. SPG applies nothing
10242 // either way — there is no postgresql.auto.conf — but it now
10243 // says so about a name it does not know.
10244 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10245 // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10246 // already consumed here. An extra advance eats the SET and
10247 // the parameter name is never seen — which is exactly the
10248 // bug a panic in this branch disproved: the branch WAS on
10249 // the path, the reading of it was wrong.
10250 let mut parameter = None;
10251 // SET <name> … | RESET <name> | RESET ALL
10252 if matches!(self.peek(), Token::Ident(k)
10253 if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10254 {
10255 self.advance();
10256 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10257 && !n.eq_ignore_ascii_case("all")
10258 {
10259 self.advance();
10260 // A dotted GUC (`plpgsql.check_asserts`) is two
10261 // tokens; keep the whole name.
10262 let mut full = n;
10263 while matches!(self.peek(), Token::Dot) {
10264 self.advance();
10265 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10266 full.push('.');
10267 full.push_str(&t);
10268 }
10269 }
10270 parameter = Some(full);
10271 }
10272 }
10273 self.consume_until_statement_boundary();
10274 return Ok(Statement::AlterSystem { parameter });
10275 }
10276 Token::Ident(s) | Token::QuotedIdent(s)
10277 if matches!(
10278 s.to_ascii_lowercase().as_str(),
10279 "role" | "user" | "database"
10280 ) && self.peeks_db_role_setting() =>
10281 {
10282 let is_database = s.eq_ignore_ascii_case("database");
10283 return self.parse_db_role_setting(is_database);
10284 }
10285 // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10286 // (the non-SET forms; SET/RESET took the branch above). The
10287 // attributes still no-op — recorded, and the ignored PASSWORD
10288 // is ledgered as its own follow-up — but the ROLE is validated:
10289 // any name was accepted for a role that does not exist.
10290 Token::Ident(s) | Token::QuotedIdent(s)
10291 if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10292 {
10293 // NB: the enclosing `match self.advance()` already consumed
10294 // ROLE/USER — the round-695 trap, hit again in this round's
10295 // first draft (the name was eaten and WITH parsed as the
10296 // role). The cursor is at the name.
10297 let name = self.expect_ident_or_string()?;
10298 // v7.39 (round 750) — scan the attribute tail for
10299 // PASSWORD. Everything else stays a recorded no-op, but
10300 // a dropped credential rotation is a SECURITY bug:
10301 // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10302 // changed nothing, so the old password kept working.
10303 // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10304 // NULL` clears the credential.
10305 let mut password: Option<Option<String>> = None;
10306 loop {
10307 match self.peek() {
10308 Token::Semicolon | Token::Eof => break,
10309 Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10310 self.advance();
10311 match self.advance() {
10312 Token::String(p) => password = Some(Some(p)),
10313 Token::Null => password = Some(None),
10314 Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10315 password = Some(None);
10316 }
10317 other => {
10318 return Err(self.err(alloc::format!(
10319 "expected password string or NULL after PASSWORD, got {other:?}"
10320 )));
10321 }
10322 }
10323 }
10324 _ => {
10325 self.advance();
10326 }
10327 }
10328 }
10329 if name.eq_ignore_ascii_case("all") {
10330 // `ALTER ROLE ALL …` names every role; nothing to check.
10331 return Ok(Statement::Empty);
10332 }
10333 if let Some(pw) = password {
10334 return Ok(Statement::AlterRolePassword { name, password: pw });
10335 }
10336 return Ok(Statement::ValidateOnly {
10337 kind: crate::ast::ValidateOnlyKind::RoleName,
10338 names: alloc::vec![name],
10339 });
10340 }
10341 // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10342 // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10343 // list far enough to validate the NAME; the actions still no-op.
10344 // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10345 // models none of them and their dumps are rare.)
10346 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10347 let name = self.expect_ident_or_string()?;
10348 self.consume_until_statement_boundary();
10349 return Ok(Statement::ValidateOnly {
10350 kind: crate::ast::ValidateOnlyKind::CollationName,
10351 names: alloc::vec![name],
10352 });
10353 }
10354 Token::Ident(s) | Token::QuotedIdent(s)
10355 if s.eq_ignore_ascii_case("text")
10356 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10357 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10358 {
10359 self.advance(); // SEARCH
10360 self.advance(); // CONFIGURATION
10361 let name = self.expect_ident_like()?;
10362 self.consume_until_statement_boundary();
10363 return Ok(Statement::ValidateOnly {
10364 kind: crate::ast::ValidateOnlyKind::TsConfigName,
10365 names: alloc::vec![name],
10366 });
10367 }
10368 Token::Ident(s) | Token::QuotedIdent(s)
10369 if s.eq_ignore_ascii_case("event")
10370 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10371 {
10372 self.advance(); // TRIGGER
10373 let name = self.expect_ident_like()?;
10374 self.consume_until_statement_boundary();
10375 return Ok(Statement::ValidateOnly {
10376 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10377 names: alloc::vec![name],
10378 });
10379 }
10380 Token::Ident(s) | Token::QuotedIdent(s)
10381 if s.eq_ignore_ascii_case("large")
10382 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10383 {
10384 self.advance(); // OBJECT
10385 let oid = match self.advance() {
10386 Token::Integer(n) => alloc::format!("{n}"),
10387 other => {
10388 return Err(
10389 self.err(alloc::format!("expected large object oid, got {other:?}"))
10390 );
10391 }
10392 };
10393 self.consume_until_statement_boundary();
10394 return Ok(Statement::ValidateOnly {
10395 kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10396 names: alloc::vec![oid],
10397 });
10398 }
10399 // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10400 // argument-list parse as DROP AGGREGATE (round 707); the
10401 // action no-ops, the existence check is real.
10402 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10403 // Same round-695 trap as above: AGGREGATE is already
10404 // consumed; the cursor is at the name.
10405 let name = self.expect_ident_like()?;
10406 let mut names = alloc::vec![name];
10407 if matches!(self.peek(), Token::LParen) {
10408 self.advance();
10409 loop {
10410 match self.peek().clone() {
10411 Token::RParen => {
10412 self.advance();
10413 break;
10414 }
10415 Token::Star => {
10416 self.advance();
10417 names.push(String::from("*"));
10418 }
10419 Token::Comma => {
10420 self.advance();
10421 }
10422 _ => {
10423 let mut t = self.expect_ident_like()?;
10424 while let Token::Ident(nx) = self.peek() {
10425 let nx = nx.clone();
10426 self.advance();
10427 t.push(' ');
10428 t.push_str(&nx);
10429 }
10430 names.push(t);
10431 }
10432 }
10433 }
10434 }
10435 self.consume_until_statement_boundary();
10436 return Ok(Statement::ValidateOnly {
10437 kind: crate::ast::ValidateOnlyKind::AggregateName,
10438 names,
10439 });
10440 }
10441 Token::Ident(s) | Token::QuotedIdent(s)
10442 if matches!(
10443 s.to_ascii_lowercase().as_str(),
10444 "view"
10445 | "function"
10446 | "database"
10447 | "schema"
10448 | "owner"
10449 | "default"
10450 | "extension"
10451 | "materialized"
10452 | "publication"
10453 | "subscription"
10454 // v7.37.17 (17.6 siblings) — additional ALTER
10455 // targets pg_dump / pg_dumpall / operator DB
10456 // migration scripts commonly emit. SPG has
10457 // no matching machinery for any of these; the
10458 // parser accepts + Empty-returns so pg_dump
10459 // tail statements don't stall.
10460 | "tablespace"
10461 | "language"
10462 | "operator"
10463 | "conversion"
10464 | "statistics"
10465 | "server"
10466 | "foreign"
10467 // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10468 // / TEMPLATE (CONFIGURATION intercepted above).
10469 | "text"
10470 ) =>
10471 {
10472 self.consume_until_statement_boundary();
10473 return Ok(Statement::Empty);
10474 }
10475 other => {
10476 return Err(self.err(format!(
10477 "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10478 after ALTER, got {other:?}"
10479 )));
10480 }
10481 }
10482 // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10483 // (mailrs migrate-042 ships these). The presence of an
10484 // IF EXISTS makes the subsequent name lookup tolerate
10485 // a missing index — engine returns CommandOk no-op.
10486 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10487 let next = self.tokens.get(self.pos + 1);
10488 if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10489 self.advance();
10490 self.advance();
10491 true
10492 } else {
10493 false
10494 }
10495 } else {
10496 false
10497 };
10498 let name = self.expect_ident_like()?;
10499 // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10500 // Detect BEFORE the REBUILD path so the existing REBUILD
10501 // arm stays untouched.
10502 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10503 self.advance();
10504 if matches!(self.peek(), Token::To) {
10505 self.advance();
10506 } else {
10507 self.expect_keyword_ident("to")?;
10508 }
10509 let new = self.expect_ident_like()?;
10510 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10511 name,
10512 target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10513 }));
10514 }
10515 // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10516 // A syntax error before; the index is validated, the params no-op.
10517 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10518 || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10519 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10520 {
10521 self.consume_until_statement_boundary();
10522 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10523 name,
10524 target: crate::ast::AlterIndexTarget::StorageParams,
10525 }));
10526 }
10527 // REBUILD
10528 self.expect_keyword_ident("rebuild")?;
10529 // Optional: WITH (encoding = <enc>)
10530 let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10531 self.advance();
10532 if !matches!(self.peek(), Token::LParen) {
10533 return Err(self.err(format!(
10534 "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10535 self.peek()
10536 )));
10537 }
10538 self.advance();
10539 self.expect_keyword_ident("encoding")?;
10540 if !matches!(self.peek(), Token::Eq) {
10541 return Err(self.err(format!(
10542 "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10543 self.peek()
10544 )));
10545 }
10546 self.advance();
10547 let enc_ident = match self.advance() {
10548 Token::Ident(s) | Token::QuotedIdent(s) => s,
10549 other => {
10550 return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10551 }
10552 };
10553 let enc = match enc_ident.to_ascii_lowercase().as_str() {
10554 "f32" => VecEncoding::F32,
10555 "sq8" => VecEncoding::Sq8,
10556 "half" => VecEncoding::F16,
10557 other => {
10558 return Err(self.err(format!(
10559 "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10560 )));
10561 }
10562 };
10563 if !matches!(self.peek(), Token::RParen) {
10564 return Err(self.err(format!(
10565 "expected ')' after encoding value, got {:?}",
10566 self.peek()
10567 )));
10568 }
10569 self.advance();
10570 Some(enc)
10571 } else {
10572 None
10573 };
10574 Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10575 name,
10576 target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10577 }))
10578 }
10579
10580 /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10581 /// only `SET` form currently supported; future v6.7.x can add
10582 /// more SET subjects without changing the dispatch shape.
10583 /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10584 /// subactions. Single-subaction shape stays a 1-element vec.
10585 fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10586 let table_name = self.expect_ident_like()?;
10587 let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10588 loop {
10589 let subaction = self.parse_alter_table_subaction()?;
10590 // ADD COLUMN with inline REFERENCES emits both an
10591 // AddColumn and an AddForeignKey subaction; the
10592 // helper returns 1 or 2 items.
10593 targets.extend(subaction);
10594 if matches!(self.peek(), Token::Comma) {
10595 self.advance();
10596 continue;
10597 }
10598 break;
10599 }
10600 Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10601 name: table_name,
10602 targets,
10603 }))
10604 }
10605
10606 /// Parse one ALTER TABLE subaction. Returns a Vec because
10607 /// inline `REFERENCES` on `ADD COLUMN` produces both an
10608 /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10609 fn parse_alter_table_subaction(
10610 &mut self,
10611 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10612 match self.peek() {
10613 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10614 self.advance();
10615 // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10616 // storage parameters: paren-prefixed; consume.
10617 if matches!(self.peek(), Token::LParen) {
10618 self.consume_until_statement_boundary();
10619 return Ok(Vec::new());
10620 }
10621 let setting = self.expect_ident_like()?;
10622 if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10623 if !matches!(self.peek(), Token::Eq) {
10624 return Err(self.err(alloc::format!(
10625 "expected '=' after hot_tier_bytes, got {:?}",
10626 self.peek()
10627 )));
10628 }
10629 self.advance();
10630 let n = self.expect_u64_literal()?;
10631 return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10632 }
10633 // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10634 // accept-and-no-op for ALTER TABLE SET <subject>
10635 // forms that pg_dump emits but SPG either treats
10636 // as N/A (single-tenant, single-owner, no shared
10637 // tablespaces) or accepts the dump-side declaration
10638 // without runtime effect:
10639 // SET SCHEMA <name> (18.11)
10640 // SET TABLESPACE <name> (18.8)
10641 // SET LOGGED / UNLOGGED (18.7 alt-form)
10642 // SET WITHOUT CLUSTER (18.13)
10643 // SET WITHOUT OIDS (PG legacy)
10644 // SET (option = value, …) (storage parameters)
10645 // SET REPLICA IDENTITY {…} (18.14)
10646 if setting.eq_ignore_ascii_case("schema")
10647 || setting.eq_ignore_ascii_case("tablespace")
10648 || setting.eq_ignore_ascii_case("logged")
10649 || setting.eq_ignore_ascii_case("unlogged")
10650 || setting.eq_ignore_ascii_case("without")
10651 {
10652 self.consume_until_statement_boundary();
10653 return Ok(Vec::new());
10654 }
10655 if setting.eq_ignore_ascii_case("replica") {
10656 // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10657 self.consume_until_statement_boundary();
10658 return Ok(Vec::new());
10659 }
10660 // SET (option=value, …) — storage parameters.
10661 if matches!(self.peek(), Token::LParen) {
10662 self.consume_until_statement_boundary();
10663 return Ok(Vec::new());
10664 }
10665 Err(self.err(alloc::format!(
10666 "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10667 hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10668 WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10669 )))
10670 }
10671 // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10672 // not ignored: round 645 gave SPG the inheritance the
10673 // v7.37.18 no-op said it did not have.
10674 Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10675 self.advance();
10676 let parent = self.expect_ident_like()?;
10677 self.consume_until_statement_boundary();
10678 Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10679 parent,
10680 detach: false
10681 }])
10682 }
10683 // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10684 // LEVEL SECURITY`, which has its own RLS arm below — without
10685 // the guard this swallowed NO FORCE as a no-op.
10686 Token::Ident(s)
10687 if s.eq_ignore_ascii_case("no")
10688 && !matches!(
10689 self.tokens.get(self.pos + 1),
10690 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10691 ) =>
10692 {
10693 self.advance();
10694 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
10695 if k.eq_ignore_ascii_case("inherit"))
10696 {
10697 self.advance();
10698 let parent = self.expect_ident_like()?;
10699 self.consume_until_statement_boundary();
10700 return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10701 parent,
10702 detach: true
10703 }]);
10704 }
10705 self.consume_until_statement_boundary();
10706 Ok(Vec::new())
10707 }
10708 // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
10709 // single-owner, so there is still nothing to record.
10710 //
10711 // v7.39 (round 652) — but the name now reaches the engine,
10712 // which refuses a role that does not exist as PG does. The
10713 // no-op was swallowing the whole statement, so a dump naming
10714 // a role this server never heard of restored clean and left
10715 // the table owned by whoever ran the restore.
10716 Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
10717 self.advance();
10718 if matches!(self.peek(), Token::To) {
10719 self.advance();
10720 }
10721 let role = self.expect_ident_like()?;
10722 Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
10723 role
10724 }])
10725 }
10726 // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
10727 // PG sets a hint; SPG doesn't have clustered storage, so the
10728 // hint itself stays a no-op.
10729 //
10730 // v7.39 (round 652) — the index name is checked now. PG
10731 // errors on one that does not exist, and swallowing that let
10732 // a typo'd CLUSTER ON pass silently.
10733 Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
10734 self.advance();
10735 // `ON` is a reserved token, not an ident.
10736 if !matches!(self.peek(), Token::On) {
10737 return Err(self.err(alloc::format!(
10738 "expected ON after CLUSTER, got {:?}",
10739 self.peek()
10740 )));
10741 }
10742 self.advance();
10743 let index = self.expect_ident_like()?;
10744 Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
10745 index: Some(index)
10746 }])
10747 }
10748 // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
10749 // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
10750 // what a logical decoder puts in the old-tuple image; SPG's
10751 // replication is SQL-text, so there is nothing to record.
10752 // Accept-and-no-op (it used to be a parse error).
10753 Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
10754 self.advance();
10755 // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
10756 // validates the index; DEFAULT / FULL / NOTHING stay no-op.
10757 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
10758 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
10759 {
10760 self.advance(); // IDENTITY
10761 self.advance(); // USING
10762 if matches!(self.peek(), Token::Index)
10763 || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
10764 {
10765 self.advance();
10766 }
10767 let index = self.expect_ident_like()?;
10768 self.consume_until_statement_boundary();
10769 return Ok(alloc::vec![
10770 crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
10771 ]);
10772 }
10773 self.consume_until_statement_boundary();
10774 Ok(Vec::new())
10775 }
10776 // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
10777 //
10778 // v7.39 (round 652) — it used to consume the statement and
10779 // return nothing, on the stated theory that SPG validated at
10780 // ADD CONSTRAINT time so there was never anything left to
10781 // validate. Measured against PG18, ADD CONSTRAINT did not
10782 // scan the existing rows at all — the comment described a
10783 // property SPG did not have, which is why nobody looked. Both
10784 // halves are real now: ADD scans unless told NOT VALID, and
10785 // this scans what NOT VALID skipped.
10786 Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
10787 self.advance();
10788 self.expect_keyword_ident("constraint")?;
10789 let name = self.expect_ident_like()?;
10790 Ok(alloc::vec![
10791 crate::ast::AlterTableTarget::ValidateConstraint { name }
10792 ])
10793 }
10794 // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
10795 // SET (option = value, …). PG uses it to clear per-table
10796 // storage params like fillfactor or autovacuum_*. SPG
10797 // engine-manages those parameters; accept-and-no-op.
10798 Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
10799 self.advance();
10800 self.consume_until_statement_boundary();
10801 Ok(Vec::new())
10802 }
10803 // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
10804 // type-of binding (PG 9.0+). SPG composite types
10805 // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
10806 // TABLE OF is rare and inverse of CREATE TABLE OF.
10807 // Accept-and-no-op until a customer dump round-trips it.
10808 Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
10809 self.advance();
10810 // v7.39 (round 710) — the type name is validated now.
10811 let type_name = self.expect_ident_like()?;
10812 self.consume_until_statement_boundary();
10813 Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
10814 type_name
10815 }])
10816 }
10817 // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
10818 // (reserved keyword) rather than Token::Ident("not"),
10819 // so it needs its own arm. Accept-and-no-op same as OF.
10820 Token::Not => {
10821 self.advance();
10822 self.consume_until_statement_boundary();
10823 Ok(Vec::new())
10824 }
10825 // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
10826 Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
10827 self.advance();
10828 self.expect_row_level_security()?;
10829 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10830 enabled: None,
10831 force: Some(true),
10832 }])
10833 }
10834 // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
10835 Token::Ident(s)
10836 if s.eq_ignore_ascii_case("no")
10837 && matches!(
10838 self.tokens.get(self.pos + 1),
10839 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10840 ) =>
10841 {
10842 self.advance(); // NO
10843 self.advance(); // FORCE
10844 self.expect_row_level_security()?;
10845 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10846 enabled: None,
10847 force: Some(false),
10848 }])
10849 }
10850 // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
10851 // (sets relrowsecurity). The guard requires the next token to be
10852 // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
10853 Token::Ident(s)
10854 if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
10855 && matches!(
10856 self.tokens.get(self.pos + 1),
10857 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
10858 ) =>
10859 {
10860 let enabled = s.eq_ignore_ascii_case("enable");
10861 self.advance(); // ENABLE/DISABLE
10862 self.expect_row_level_security()?;
10863 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10864 enabled: Some(enabled),
10865 force: None,
10866 }])
10867 }
10868 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
10869 self.advance();
10870 // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
10871 // {INDEX|KEY} [name] (cols)`, which every ORM migration
10872 // emits. The same grammar CREATE TABLE already accepts
10873 // inline (`KEY idx (a)`, prefix lengths and all), so it goes
10874 // through the SAME parser — an ALTER-only copy would be a
10875 // second place for the two to drift.
10876 if self.peek_mysql_inline_key_start() {
10877 return Ok(match self.parse_mysql_inline_key()? {
10878 Some(c) => {
10879 alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
10880 }
10881 // FULLTEXT / SPATIAL parse and are accepted as a
10882 // no-op here exactly as they are inline.
10883 None => Vec::new(),
10884 });
10885 }
10886 // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
10887 // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
10888 // PRIMARY KEY this way; mysqldump emits both.
10889 // Peek-only dispatch (no advance) — `advance()`
10890 // destructively replaces consumed tokens with Eof,
10891 // so saved-pos restore would land on Eofs.
10892 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
10893 {
10894 // The next-but-one ident is the constraint
10895 // name; the one after THAT is the kind.
10896 let kind_pos = self.pos + 2;
10897 let kind = self.tokens.get(kind_pos).cloned();
10898 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
10899 {
10900 let fk = self.parse_table_level_fk()?;
10901 return Ok(alloc::vec![
10902 crate::ast::AlterTableTarget::AddForeignKey(fk)
10903 ]);
10904 }
10905 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
10906 {
10907 self.advance(); // CONSTRAINT
10908 // v7.39 (read01 round 48) — keep the name; the engine
10909 // stores it now instead of dropping it on the floor.
10910 let con_name = self.expect_ident_like()?;
10911 self.advance(); // PRIMARY
10912 self.expect_keyword_ident("key")?;
10913 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10914 // v7.39 (round 711) — the ALTER form carries the
10915 // timing too (pg_dump writes it here).
10916 let (deferrable, initially_deferred) =
10917 self.consume_deferrable_clauses_timed()?;
10918 return Ok(alloc::vec![
10919 crate::ast::AlterTableTarget::AddTableConstraint(
10920 crate::ast::TableConstraint::PrimaryKey {
10921 name: Some(con_name),
10922 columns: cols,
10923 deferrable,
10924 initially_deferred,
10925 }
10926 )
10927 ]);
10928 }
10929 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
10930 {
10931 self.advance(); // CONSTRAINT
10932 // v7.39 (read01 round 48) — keep the name.
10933 let con_name = self.expect_ident_like()?;
10934 // v7.22 (mailrs round-13 gap 6) — delegate so
10935 // the optional `NULLS [NOT] DISTINCT` modifier
10936 // parses here too (pg_dump emits the ALTER
10937 // form; semantics enforced by the engine
10938 // since v7.13).
10939 let mut uc = self.parse_table_level_unique()?;
10940 if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
10941 *name = Some(con_name);
10942 }
10943 return Ok(alloc::vec![
10944 crate::ast::AlterTableTarget::AddTableConstraint(uc)
10945 ]);
10946 }
10947 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
10948 {
10949 self.advance(); // CONSTRAINT
10950 // v7.39 (read01 round 48) — keep the name.
10951 let con_name = self.expect_ident_like()?;
10952 self.advance(); // CHECK
10953 if !matches!(self.peek(), Token::LParen) {
10954 return Err(self.err(alloc::format!(
10955 "expected '(' after CHECK, got {:?}", self.peek()
10956 )));
10957 }
10958 self.advance();
10959 let expr = self.parse_expr(0)?;
10960 if matches!(self.peek(), Token::RParen) {
10961 self.advance();
10962 }
10963 let not_valid = self.parse_not_valid_suffix();
10964 return Ok(alloc::vec![
10965 crate::ast::AlterTableTarget::AddTableConstraint(
10966 crate::ast::TableConstraint::Check {
10967 name: Some(con_name),
10968 expr,
10969 not_valid,
10970 }
10971 )
10972 ]);
10973 }
10974 // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
10975 // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
10976 // exclusion constraints via this ALTER form.
10977 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
10978 {
10979 self.advance(); // CONSTRAINT
10980 let con_name = self.expect_ident_like()?;
10981 let mut ex = self.parse_table_level_exclude()?;
10982 if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
10983 *name = Some(con_name);
10984 }
10985 return Ok(alloc::vec![
10986 crate::ast::AlterTableTarget::AddTableConstraint(ex)
10987 ]);
10988 }
10989 // Unknown kind — fall through to FK path which
10990 // produces a descriptive parse error.
10991 }
10992 let is_fk = matches!(
10993 self.peek(),
10994 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
10995 || s.eq_ignore_ascii_case("foreign")
10996 );
10997 if is_fk {
10998 let fk = self.parse_table_level_fk()?;
10999 return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
11000 }
11001 // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
11002 // (no CONSTRAINT prefix) — same dispatch.
11003 match self.peek().clone() {
11004 Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
11005 self.advance();
11006 self.expect_keyword_ident("key")?;
11007 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11008 let (deferrable, initially_deferred) =
11009 self.consume_deferrable_clauses_timed()?;
11010 return Ok(alloc::vec![
11011 crate::ast::AlterTableTarget::AddTableConstraint(
11012 crate::ast::TableConstraint::PrimaryKey {
11013 name: None,
11014 columns: cols,
11015 deferrable,
11016 initially_deferred,
11017 }
11018 )
11019 ]);
11020 }
11021 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
11022 // v7.22 — delegate (NULLS [NOT] DISTINCT).
11023 let uc = self.parse_table_level_unique()?;
11024 return Ok(alloc::vec![
11025 crate::ast::AlterTableTarget::AddTableConstraint(uc)
11026 ]);
11027 }
11028 // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
11029 // prefix). The other three bare forms were here and
11030 // this one was not, so it fell through to the column
11031 // path and came back as "unexpected reserved keyword
11032 // 'check' at start of column definition".
11033 _ if self.peek_table_level_check_start() => {
11034 let chk = self.parse_table_level_check()?;
11035 let not_valid = self.parse_not_valid_suffix();
11036 let crate::ast::TableConstraint::Check { expr, .. } = chk else {
11037 unreachable!("parse_table_level_check returns Check")
11038 };
11039 return Ok(alloc::vec![
11040 crate::ast::AlterTableTarget::AddTableConstraint(
11041 crate::ast::TableConstraint::Check {
11042 name: None,
11043 expr,
11044 not_valid,
11045 }
11046 )
11047 ]);
11048 }
11049 // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11050 Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11051 let ex = self.parse_table_level_exclude()?;
11052 return Ok(alloc::vec![
11053 crate::ast::AlterTableTarget::AddTableConstraint(ex)
11054 ]);
11055 }
11056 _ => {}
11057 }
11058 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11059 self.advance();
11060 }
11061 let mut if_not_exists = false;
11062 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11063 self.advance();
11064 if !matches!(self.peek(), Token::Not) {
11065 return Err(self.err(alloc::format!(
11066 "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11067 self.peek()
11068 )));
11069 }
11070 self.advance();
11071 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11072 return Err(self.err(alloc::format!(
11073 "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11074 self.peek()
11075 )));
11076 }
11077 self.advance();
11078 if_not_exists = true;
11079 }
11080 // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11081 // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11082 // returns ColumnDef + an optional inline FK.
11083 let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11084 let col_name = column.name.clone();
11085 let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11086 column,
11087 if_not_exists,
11088 }];
11089 if let Some(mut fk) = col_level_fk {
11090 if fk.columns.is_empty() {
11091 fk.columns.push(col_name);
11092 }
11093 out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11094 }
11095 Ok(out)
11096 }
11097 Token::Drop => {
11098 self.advance();
11099 // v7.13.3 — dispatch on the next token. mailrs round-7
11100 // S8 closed DROP COLUMN; round-6 S7 closed
11101 // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11102 // RESTRICT modifiers.
11103 // DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11104 // DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11105 let subject = match self.peek() {
11106 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11107 self.advance();
11108 "constraint"
11109 }
11110 Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11111 self.advance();
11112 "column"
11113 }
11114 // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11115 // `INDEX` lexes as the reserved Token::Index, so it is
11116 // unambiguous. `KEY` is a plain ident, and PG allows a
11117 // column literally named "key", so only read it as the
11118 // keyword when a name follows it.
11119 Token::Index => {
11120 self.advance();
11121 "index"
11122 }
11123 Token::Ident(s)
11124 if s.eq_ignore_ascii_case("key")
11125 && matches!(
11126 self.tokens.get(self.pos + 1),
11127 Some(Token::Ident(_) | Token::QuotedIdent(_))
11128 ) =>
11129 {
11130 self.advance();
11131 "index"
11132 }
11133 // PG-canonical bare `DROP <col>` without COLUMN
11134 // keyword is also valid; treat any other ident
11135 // as the column name.
11136 Token::Ident(_) | Token::QuotedIdent(_) => "column",
11137 other => {
11138 return Err(self.err(alloc::format!(
11139 "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11140 )));
11141 }
11142 };
11143 let mut if_exists = false;
11144 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11145 let n1 = self.tokens.get(self.pos + 1);
11146 if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11147 self.advance();
11148 self.advance();
11149 if_exists = true;
11150 }
11151 }
11152 let name = self.expect_ident_like()?;
11153 let mut cascade = false;
11154 if matches!(
11155 self.peek(),
11156 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11157 || s.eq_ignore_ascii_case("restrict")
11158 ) {
11159 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11160 {
11161 cascade = true;
11162 }
11163 self.advance();
11164 }
11165 if subject == "index" {
11166 Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11167 name,
11168 if_exists,
11169 }])
11170 } else if subject == "constraint" {
11171 Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11172 name,
11173 if_exists,
11174 }])
11175 } else {
11176 Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11177 column: name,
11178 if_exists,
11179 cascade,
11180 }])
11181 }
11182 }
11183 Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11184 self.advance();
11185 // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11186 // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11187 // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11188 // immediately; accept-and-no-op.
11189 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11190 self.advance();
11191 self.consume_until_statement_boundary();
11192 return Ok(Vec::new());
11193 }
11194 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11195 self.advance();
11196 }
11197 let col_name = self.expect_ident_like()?;
11198 match self.peek() {
11199 Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11200 self.advance();
11201 }
11202 // v7.14.0 — pg_dump emits BIGSERIAL via
11203 // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11204 // nextval('seq')` (the sequence is created
11205 // separately). SPG's BIGSERIAL already uses
11206 // AUTO_INCREMENT; accept SET DEFAULT / DROP
11207 // DEFAULT / SET NOT NULL / DROP NOT NULL as
11208 // engine no-ops by consuming the tail.
11209 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11210 // v7.22 (round-13 T2) — `SET DEFAULT
11211 // nextval('…')` is how pg_dump spells a
11212 // SERIAL column (plain integer in CREATE
11213 // TABLE + this ALTER). It used to be
11214 // swallowed as a no-op, which silently
11215 // STRIPPED auto-increment from imported
11216 // schemas — the first post-import INSERT
11217 // without an explicit id then violated NOT
11218 // NULL. Lower it to the auto-increment
11219 // marker instead.
11220 let is_default_nextval =
11221 matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11222 && matches!(
11223 self.tokens.get(self.pos + 2),
11224 Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11225 );
11226 if is_default_nextval {
11227 let seq_name = self.scan_sequence_name_until_boundary();
11228 return Ok(alloc::vec![
11229 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11230 column: col_name,
11231 seq_name,
11232 }
11233 ]);
11234 }
11235 // v7.37.18 (18.1 + 18.2) — proper lowering.
11236 self.advance(); // consume "set"
11237 match self.peek().clone() {
11238 Token::Default => {
11239 self.advance();
11240 let default_expr = self.parse_expr(0)?;
11241 return Ok(alloc::vec![
11242 crate::ast::AlterTableTarget::AlterColumnSetDefault {
11243 column: col_name,
11244 default_expr,
11245 }
11246 ]);
11247 }
11248 Token::Not => {
11249 self.advance();
11250 if !matches!(self.peek(), Token::Null) {
11251 return Err(self.err(alloc::format!(
11252 "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11253 self.peek()
11254 )));
11255 }
11256 self.advance();
11257 return Ok(alloc::vec![
11258 crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11259 column: col_name,
11260 }
11261 ]);
11262 }
11263 // `SET EXPRESSION AS (expr)` (PG 17) — change a
11264 // stored generated column's expression and
11265 // recompute existing rows.
11266 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11267 self.advance(); // EXPRESSION
11268 if matches!(self.peek(), Token::As) {
11269 self.advance();
11270 }
11271 let expr = self.parse_expr(0)?;
11272 return Ok(alloc::vec![
11273 crate::ast::AlterTableTarget::AlterColumnSetExpression {
11274 column: col_name,
11275 expr,
11276 }
11277 ]);
11278 }
11279 other => {
11280 // Other SET subjects (STATISTICS,
11281 // STORAGE, COMPRESSION, …) stay no-ops —
11282 // storage hints with no SPG semantics.
11283 let _ = other;
11284 self.consume_until_statement_boundary();
11285 return Ok(Vec::new());
11286 }
11287 }
11288 }
11289 Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11290 self.advance(); // consume "drop"
11291 return self.parse_alter_column_drop_tail(col_name);
11292 }
11293 Token::Drop => {
11294 self.advance(); // consume Drop token
11295 return self.parse_alter_column_drop_tail(col_name);
11296 }
11297 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11298 // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11299 // GENERATED { ALWAYS | BY DEFAULT } AS
11300 // IDENTITY ( … )`: pg_dump's spelling for
11301 // identity columns. Same auto-increment
11302 // lowering as the nextval default; the
11303 // sequence options inside the parens are
11304 // no-ops under SPG's max+1 semantics.
11305 let is_generated = matches!(
11306 self.tokens.get(self.pos + 1),
11307 Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11308 );
11309 if !is_generated {
11310 return Err(self.err(alloc::format!(
11311 "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11312 self.tokens.get(self.pos + 1)
11313 )));
11314 }
11315 let seq_name = self.scan_sequence_name_until_boundary();
11316 return Ok(alloc::vec![
11317 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11318 column: col_name,
11319 seq_name,
11320 }
11321 ]);
11322 }
11323 // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11324 // column: floor the next allocated value at n (bare
11325 // RESTART = restart from the start value, 1).
11326 Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11327 self.advance();
11328 let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11329 {
11330 self.advance();
11331 let neg = if matches!(self.peek(), Token::Minus) {
11332 self.advance();
11333 true
11334 } else {
11335 false
11336 };
11337 match self.advance() {
11338 Token::Integer(v) => Some(if neg { -v } else { v }),
11339 other => {
11340 return Err(self.err(alloc::format!(
11341 "expected integer after RESTART WITH, got {other:?}"
11342 )));
11343 }
11344 }
11345 } else {
11346 None
11347 };
11348 return Ok(alloc::vec![
11349 crate::ast::AlterTableTarget::AlterColumnRestart {
11350 column: col_name,
11351 with,
11352 }
11353 ]);
11354 }
11355 other => {
11356 return Err(self.err(alloc::format!(
11357 "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11358 )));
11359 }
11360 }
11361 // v7.39 (round 713) — the type parser has consumed a
11362 // trailing `COLLATE <name>` since Phase 2.5, and
11363 // `parse_column_type_name` discarded it: `ALTER COLUMN t
11364 // TYPE text COLLATE "C"` parsed clean and changed
11365 // nothing. Keep the clause; the engine re-collates.
11366 let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _) =
11367 self.parse_type_with_implied_flags()?;
11368 let collation = if coll_explicit {
11369 coll_name.map(|n| (coll, n))
11370 } else {
11371 None
11372 };
11373 let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11374 {
11375 self.advance();
11376 Some(self.parse_expr(0)?)
11377 } else {
11378 None
11379 };
11380 Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11381 column: col_name,
11382 new_type,
11383 using,
11384 collation,
11385 }])
11386 }
11387 // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11388 // PG also supports `RENAME TO new_table` for table-name
11389 // rename; that surface is deferred (pg_dump never emits
11390 // it). If the first post-RENAME ident is `TO`, the user
11391 // is asking for table rename — error with a clear
11392 // message rather than misparsing `TO` as a column name.
11393 Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11394 self.advance();
11395 // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11396 // table-name rename (mailrs round-10 A.5 — used
11397 // by migrate-042's `RENAME TO email_contacts`).
11398 // `TO` lexes as Token::To.
11399 if matches!(self.peek(), Token::To)
11400 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11401 {
11402 self.advance();
11403 let new = self.expect_ident_like()?;
11404 return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11405 new,
11406 }]);
11407 }
11408 // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11409 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11410 self.advance();
11411 let old = self.expect_ident_like()?;
11412 if matches!(self.peek(), Token::To) {
11413 self.advance();
11414 } else {
11415 self.expect_keyword_ident("to")?;
11416 }
11417 let new = self.expect_ident_like()?;
11418 return Ok(alloc::vec![
11419 crate::ast::AlterTableTarget::RenameConstraint { old, new }
11420 ]);
11421 }
11422 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11423 self.advance();
11424 }
11425 let old = self.expect_ident_like()?;
11426 // `TO` is a reserved keyword token; accept both
11427 // Token::To and Token::Ident("to") for consistency.
11428 if matches!(self.peek(), Token::To) {
11429 self.advance();
11430 } else {
11431 self.expect_keyword_ident("to")?;
11432 }
11433 let new = self.expect_ident_like()?;
11434 Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11435 old,
11436 new,
11437 }])
11438 }
11439 // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11440 // { ALL | <name> }`. pg_dump --disable-triggers wraps
11441 // every data block with these. Real disable semantics —
11442 // not no-op — because reload correctness assumes the
11443 // triggers don't fire (rows already carry their
11444 // computed values from prod).
11445 Token::Ident(s)
11446 if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11447 {
11448 let enabled = s.eq_ignore_ascii_case("enable");
11449 self.advance();
11450 // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11451 // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11452 // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11453 // pg_dump output) — anything else falls through to
11454 // the catch-all error below.
11455 // v7.22 (round-13 T3) — mysqldump wraps every data
11456 // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11457 // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11458 // maintains indexes incrementally — engine no-op.
11459 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11460 self.advance();
11461 return Ok(Vec::new());
11462 }
11463 // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11464 // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11465 // to gate triggers on session_replication_role; SPG
11466 // has no replica role, so the prefix is consumed and
11467 // treated identically to the plain ENABLE/DISABLE
11468 // TRIGGER form.
11469 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11470 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11471 {
11472 self.advance();
11473 }
11474 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11475 return Err(self.err(alloc::format!(
11476 "expected TRIGGER after {}, got {:?}",
11477 if enabled { "ENABLE" } else { "DISABLE" },
11478 self.peek()
11479 )));
11480 }
11481 self.advance();
11482 // `ALL` lexes as Token::All (reserved); also
11483 // accept Token::Ident("all") for symmetry.
11484 // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11485 // TRIGGER selectors. USER (= all user triggers) is
11486 // semantically ALL here; REPLICA / ALWAYS gate on
11487 // session_replication_role which SPG doesn't track.
11488 // All map to TriggerSelector::All.
11489 let which = if matches!(self.peek(), Token::All)
11490 || matches!(self.peek(), Token::Ident(s)
11491 if s.eq_ignore_ascii_case("all")
11492 || s.eq_ignore_ascii_case("user")
11493 || s.eq_ignore_ascii_case("replica")
11494 || s.eq_ignore_ascii_case("always"))
11495 {
11496 self.advance();
11497 crate::ast::TriggerSelector::All
11498 } else {
11499 let name = self.expect_ident_like()?;
11500 crate::ast::TriggerSelector::Named(name)
11501 };
11502 Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11503 which,
11504 enabled,
11505 }])
11506 }
11507 // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11508 Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11509 self.advance();
11510 if !matches!(self.peek(), Token::Partition)
11511 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11512 if s.eq_ignore_ascii_case("partition"))
11513 {
11514 return Err(self.err(alloc::format!(
11515 "expected PARTITION after ATTACH, got {:?}",
11516 self.peek()
11517 )));
11518 }
11519 self.advance();
11520 let child = self.expect_ident_like()?;
11521 let bounds = self.parse_partition_bounds_tail()?;
11522 Ok(alloc::vec![
11523 crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11524 ])
11525 }
11526 // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11527 Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11528 self.advance();
11529 if !matches!(self.peek(), Token::Partition)
11530 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11531 if s.eq_ignore_ascii_case("partition"))
11532 {
11533 return Err(self.err(alloc::format!(
11534 "expected PARTITION after DETACH, got {:?}",
11535 self.peek()
11536 )));
11537 }
11538 self.advance();
11539 let child = self.expect_ident_like()?;
11540 let mut concurrently = false;
11541 let mut finalize = false;
11542 loop {
11543 match self.peek().clone() {
11544 Token::Ident(s) | Token::QuotedIdent(s)
11545 if s.eq_ignore_ascii_case("concurrently") =>
11546 {
11547 self.advance();
11548 concurrently = true;
11549 }
11550 Token::Ident(s) | Token::QuotedIdent(s)
11551 if s.eq_ignore_ascii_case("finalize") =>
11552 {
11553 self.advance();
11554 finalize = true;
11555 }
11556 _ => break,
11557 }
11558 }
11559 Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11560 child,
11561 concurrently,
11562 finalize,
11563 }])
11564 }
11565 other => Err(self.err(alloc::format!(
11566 "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11567 ))),
11568 }
11569 }
11570
11571 /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11572 /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11573 /// TABLE … ATTACH PARTITION. Shares the same grammar as
11574 /// `parse_partition_of_tail`'s bounds branch.
11575 /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11576 /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11577 /// lowering each to the respective AlterTableTarget. Any
11578 /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11579 /// no-op via consume_until_statement_boundary.
11580 fn parse_alter_column_drop_tail(
11581 &mut self,
11582 col_name: String,
11583 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11584 match self.peek().clone() {
11585 Token::Default => {
11586 self.advance();
11587 Ok(alloc::vec![
11588 crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11589 ])
11590 }
11591 Token::Not => {
11592 self.advance();
11593 if !matches!(self.peek(), Token::Null) {
11594 return Err(self.err(alloc::format!(
11595 "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11596 self.peek()
11597 )));
11598 }
11599 self.advance();
11600 Ok(alloc::vec![
11601 crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11602 ])
11603 }
11604 // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11605 // generated column into a plain column.
11606 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11607 self.advance();
11608 // v7.39 (round 187, U10) — IF EXISTS was consumed but
11609 // dropped, so the engine still errored on a plain
11610 // column; PG's semantics are NOTICE + skip.
11611 let mut if_exists = false;
11612 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11613 self.advance();
11614 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11615 self.advance();
11616 if_exists = true;
11617 }
11618 }
11619 Ok(alloc::vec![
11620 crate::ast::AlterTableTarget::AlterColumnDropExpression {
11621 column: col_name,
11622 if_exists,
11623 }
11624 ])
11625 }
11626 // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11627 // identity column into a plain column.
11628 Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11629 self.advance();
11630 let mut if_exists = false;
11631 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11632 self.advance();
11633 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11634 self.advance();
11635 if_exists = true;
11636 }
11637 }
11638 Ok(alloc::vec![
11639 crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11640 column: col_name,
11641 if_exists,
11642 }
11643 ])
11644 }
11645 _ => {
11646 self.consume_until_statement_boundary();
11647 Ok(Vec::new())
11648 }
11649 }
11650 }
11651
11652 /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11653 /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11654 /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11655 /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11656 fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11657 let mut opts = crate::ast::CopyOptions::default();
11658 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11659 return Ok(opts);
11660 }
11661 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11662 self.advance();
11663 }
11664 if matches!(self.peek(), Token::LParen) {
11665 self.advance();
11666 loop {
11667 self.parse_one_copy_option(&mut opts)?;
11668 match self.peek() {
11669 Token::Comma => {
11670 self.advance();
11671 }
11672 Token::RParen => {
11673 self.advance();
11674 break;
11675 }
11676 other => {
11677 return Err(self.err(alloc::format!(
11678 "expected ',' or ')' in COPY options, got {other:?}"
11679 )));
11680 }
11681 }
11682 }
11683 } else {
11684 while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11685 self.parse_one_copy_option(&mut opts)?;
11686 }
11687 }
11688 if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11689 return Err(self.err(alloc::format!(
11690 "unexpected token after COPY options: {:?}",
11691 self.peek()
11692 )));
11693 }
11694 Ok(opts)
11695 }
11696
11697 fn parse_one_copy_option(
11698 &mut self,
11699 opts: &mut crate::ast::CopyOptions,
11700 ) -> Result<(), ParseError> {
11701 use crate::ast::CopyFormat;
11702 // The option keyword. NULL lexes as its own token; the rest are
11703 // bare identifiers.
11704 let kw = match self.advance() {
11705 Token::Null => alloc::string::String::from("NULL"),
11706 Token::Ident(s) => s.to_uppercase(),
11707 other => {
11708 return Err(self.err(alloc::format!(
11709 "expected a COPY option keyword, got {other:?}"
11710 )));
11711 }
11712 };
11713 match kw.as_str() {
11714 "FORMAT" => {
11715 let fmt = self.expect_ident_like()?;
11716 match fmt.to_ascii_uppercase().as_str() {
11717 "CSV" => opts.format = CopyFormat::Csv,
11718 "TEXT" => opts.format = CopyFormat::Text,
11719 other => {
11720 return Err(self.err(alloc::format!(
11721 "COPY format \"{}\" not recognized",
11722 other.to_ascii_lowercase()
11723 )));
11724 }
11725 }
11726 }
11727 // Legacy bare format keywords.
11728 "CSV" => opts.format = CopyFormat::Csv,
11729 "TEXT" => opts.format = CopyFormat::Text,
11730 "HEADER" => {
11731 opts.header = match self.peek() {
11732 Token::True => {
11733 self.advance();
11734 true
11735 }
11736 Token::False => {
11737 self.advance();
11738 false
11739 }
11740 Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
11741 self.advance();
11742 true
11743 }
11744 Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
11745 self.advance();
11746 false
11747 }
11748 // Bare HEADER (no boolean) means HEADER true.
11749 _ => true,
11750 };
11751 }
11752 // r1066 (7.38 S5.1) — pgbench 14+ loads with
11753 // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
11754 // vacuum bookkeeping on a freshly created/truncated
11755 // table; SPG's per-statement visibility makes it a
11756 // faithful no-op, and rejecting it aborted `pgbench -i`
11757 // against the drop-in. Accept ON/OFF/bare, change nothing.
11758 "FREEZE" => match self.peek() {
11759 Token::True | Token::False => {
11760 self.advance();
11761 }
11762 Token::Ident(s)
11763 if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
11764 {
11765 self.advance();
11766 }
11767 _ => {}
11768 },
11769 "DELIMITER" | "QUOTE" | "ESCAPE" => {
11770 let s = match self.advance() {
11771 Token::String(s) => s,
11772 other => {
11773 return Err(self.err(alloc::format!(
11774 "COPY {kw} expects a single-character string, got {other:?}"
11775 )));
11776 }
11777 };
11778 // v7.39 (round 247) — PG's wording (0A000), keyword in
11779 // lowercase: "COPY delimiter must be a single one-byte
11780 // character".
11781 let one_byte_err = || {
11782 self.err(alloc::format!(
11783 "COPY {} must be a single one-byte character",
11784 kw.to_ascii_lowercase()
11785 ))
11786 };
11787 let mut chars = s.chars();
11788 let c = chars.next().ok_or_else(one_byte_err)?;
11789 if chars.next().is_some() || c.len_utf8() != 1 {
11790 return Err(one_byte_err());
11791 }
11792 match kw.as_str() {
11793 "DELIMITER" => opts.delimiter = Some(c),
11794 "QUOTE" => opts.quote = Some(c),
11795 _ => opts.escape = Some(c),
11796 }
11797 }
11798 // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
11799 "FORCE_QUOTE" => {
11800 if matches!(self.peek(), Token::Star) {
11801 self.advance();
11802 opts.force_quote = Some(Vec::new());
11803 } else {
11804 if !matches!(self.peek(), Token::LParen) {
11805 return Err(self.err(alloc::format!(
11806 "expected '(' or '*' after FORCE_QUOTE, got {:?}",
11807 self.peek()
11808 )));
11809 }
11810 self.advance();
11811 let mut cols = Vec::new();
11812 loop {
11813 cols.push(self.expect_ident_like()?);
11814 match self.peek() {
11815 Token::Comma => {
11816 self.advance();
11817 }
11818 Token::RParen => {
11819 self.advance();
11820 break;
11821 }
11822 other => {
11823 return Err(self.err(alloc::format!(
11824 "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
11825 )));
11826 }
11827 }
11828 }
11829 opts.force_quote = Some(cols);
11830 }
11831 }
11832 "NULL" => {
11833 opts.null_str = Some(match self.advance() {
11834 Token::String(s) => s,
11835 other => {
11836 return Err(self.err(alloc::format!(
11837 "COPY NULL expects a quoted string, got {other:?}"
11838 )));
11839 }
11840 });
11841 }
11842 // v7.39 (round 265) — the two CSV FROM-side column lists. Same
11843 // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
11844 // FORCE_NULL too.
11845 "FORCE_NOT_NULL" | "FORCE_NULL" => {
11846 let cols = self.parse_copy_column_list(&kw)?;
11847 if kw == "FORCE_NOT_NULL" {
11848 opts.force_not_null = Some(cols);
11849 } else {
11850 opts.force_null = Some(cols);
11851 }
11852 }
11853 other => {
11854 // PG's wording, lowercased option name.
11855 return Err(self.err(alloc::format!(
11856 "option \"{}\" not recognized",
11857 other.to_ascii_lowercase()
11858 )));
11859 }
11860 }
11861 Ok(())
11862 }
11863
11864 /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
11865 /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
11866 /// is the `*` spelling.
11867 fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
11868 if matches!(self.peek(), Token::Star) {
11869 self.advance();
11870 return Ok(Vec::new());
11871 }
11872 if !matches!(self.peek(), Token::LParen) {
11873 return Err(self.err(alloc::format!(
11874 "expected '(' or '*' after {kw}, got {:?}",
11875 self.peek()
11876 )));
11877 }
11878 self.advance();
11879 let mut cols = Vec::new();
11880 loop {
11881 cols.push(self.expect_ident_like()?);
11882 match self.peek() {
11883 Token::Comma => {
11884 self.advance();
11885 }
11886 Token::RParen => {
11887 self.advance();
11888 break;
11889 }
11890 other => {
11891 return Err(self.err(alloc::format!(
11892 "expected ',' or ')' in {kw} list, got {other:?}"
11893 )));
11894 }
11895 }
11896 }
11897 Ok(cols)
11898 }
11899
11900 fn parse_partition_bounds_tail(
11901 &mut self,
11902 ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
11903 use crate::ast::PartitionOfBoundsAst;
11904 match self.peek() {
11905 Token::Default => {
11906 self.advance();
11907 Ok(PartitionOfBoundsAst::Default)
11908 }
11909 Token::For => {
11910 self.advance();
11911 if !matches!(self.peek(), Token::Values) {
11912 return Err(
11913 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
11914 );
11915 }
11916 self.advance();
11917 let want_with = matches!(
11918 self.peek(),
11919 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
11920 );
11921 if want_with {
11922 self.advance();
11923 if !matches!(self.peek(), Token::LParen) {
11924 return Err(self.err(format!(
11925 "expected '(' after FOR VALUES WITH, got {:?}",
11926 self.peek()
11927 )));
11928 }
11929 self.advance();
11930 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
11931 loop {
11932 let key = self.expect_ident_like()?;
11933 let n = match self.peek().clone() {
11934 Token::Integer(v) if u32::try_from(v).is_ok() => {
11935 self.advance();
11936 v as u32
11937 }
11938 other => {
11939 return Err(self.err(format!(
11940 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
11941 )));
11942 }
11943 };
11944 match key.to_ascii_uppercase().as_str() {
11945 "MODULUS" => modulus = Some(n),
11946 "REMAINDER" => remainder = Some(n),
11947 other => {
11948 return Err(self.err(format!(
11949 "FOR VALUES WITH: unknown key {other:?}; \
11950 expected MODULUS or REMAINDER"
11951 )));
11952 }
11953 }
11954 match self.peek() {
11955 Token::Comma => {
11956 self.advance();
11957 }
11958 Token::RParen => {
11959 self.advance();
11960 break;
11961 }
11962 other => {
11963 return Err(self.err(format!(
11964 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
11965 )));
11966 }
11967 }
11968 }
11969 let modulus = modulus
11970 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
11971 let remainder = remainder.ok_or_else(|| {
11972 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
11973 })?;
11974 if modulus == 0 {
11975 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
11976 }
11977 if remainder >= modulus {
11978 return Err(self.err(format!(
11979 "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
11980 )));
11981 }
11982 return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
11983 }
11984 match self.peek() {
11985 Token::From => {
11986 self.advance();
11987 let lower = Box::new(self.parse_partition_bound_expr()?);
11988 if !matches!(self.peek(), Token::To) {
11989 return Err(self.err(format!(
11990 "expected TO after FROM (...), got {:?}",
11991 self.peek()
11992 )));
11993 }
11994 self.advance();
11995 let upper = Box::new(self.parse_partition_bound_expr()?);
11996 Ok(PartitionOfBoundsAst::Range { lower, upper })
11997 }
11998 Token::In => {
11999 self.advance();
12000 if !matches!(self.peek(), Token::LParen) {
12001 return Err(self.err(format!(
12002 "expected '(' after FOR VALUES IN, got {:?}",
12003 self.peek()
12004 )));
12005 }
12006 self.advance();
12007 let mut values = Vec::new();
12008 loop {
12009 values.push(self.parse_expr(0)?);
12010 match self.peek() {
12011 Token::Comma => {
12012 self.advance();
12013 }
12014 Token::RParen => {
12015 self.advance();
12016 break;
12017 }
12018 other => {
12019 return Err(self.err(format!(
12020 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
12021 )));
12022 }
12023 }
12024 }
12025 if values.is_empty() {
12026 return Err(
12027 self.err("FOR VALUES IN requires at least one literal".to_string())
12028 );
12029 }
12030 Ok(PartitionOfBoundsAst::List { values })
12031 }
12032 other => Err(self.err(format!(
12033 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
12034 ))),
12035 }
12036 }
12037 other => Err(self.err(format!(
12038 "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
12039 ))),
12040 }
12041 }
12042
12043 /// v7.16.2 — peek for `information_schema.<tbl>` /
12044 /// `pg_catalog.<tbl>` triples and, if matched, consume all
12045 /// three tokens + return a synthetic table name the engine's
12046 /// SELECT path recognises as a virtual view. Returns `None`
12047 /// when the head doesn't look like a meta-qualified name.
12048 /// Used by `parse_table_ref` to bypass the
12049 /// `expect_ident_like` schema-strip for these specific PG
12050 /// meta schemas (mailrs round-10 A.3).
12051 fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12052 // Extract the schema name. Must be a plain ident token.
12053 let schema = match self.tokens.get(self.pos) {
12054 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12055 _ => return None,
12056 };
12057 // Dot.
12058 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12059 return None;
12060 }
12061 // The table-side ident may lex as a reserved keyword
12062 // (e.g. `Token::Tables`). Tolerate the common ones via a
12063 // helper that reads the trailing token's underlying name.
12064 let tbl = match self.tokens.get(self.pos + 2)? {
12065 Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12066 Token::Tables => "tables".to_string(),
12067 // Other PG meta table names that may collide with
12068 // reserved keywords land here as needed.
12069 _ => return None,
12070 };
12071 // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12072 // names so the synthetic name doesn't double-prefix
12073 // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12074 let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12075 ("__spg_info_", tbl.to_ascii_lowercase())
12076 } else if schema.eq_ignore_ascii_case("pg_catalog") {
12077 // v7.39 (round 541) — only the catalogs SPG actually
12078 // synthesises are rewritten, which is what the BARE path
12079 // has always checked. Anything else keeps its own name and
12080 // takes the ordinary route: `pg_stat_activity` and friends
12081 // resolve through meta_view_result, and a name that is no
12082 // catalog at all gets PG's "relation does not exist"
12083 // instead of a message about a view SPG cannot materialise.
12084 let lowered = tbl.to_ascii_lowercase();
12085 if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12086 self.advance(); // schema
12087 self.advance(); // dot
12088 self.advance(); // tbl
12089 return Some((lowered.clone(), lowered));
12090 }
12091 let bare = lowered
12092 .strip_prefix("pg_")
12093 .map(alloc::string::String::from)
12094 .unwrap_or(lowered);
12095 ("__spg_pg_", bare)
12096 } else if schema.eq_ignore_ascii_case("mysql") {
12097 // v7.17.0 Phase 3.P0-65 — MySQL system schema
12098 // (`mysql.user`, `mysql.db`). Same synthetic-name
12099 // shape as pg_catalog.
12100 ("__spg_mysql_", tbl.to_ascii_lowercase())
12101 } else {
12102 return None;
12103 };
12104 self.advance(); // schema
12105 self.advance(); // dot
12106 self.advance(); // tbl
12107 Some((
12108 alloc::format!("{prefix}{normalised}"),
12109 tbl.to_ascii_lowercase(),
12110 ))
12111 }
12112
12113 /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12114 /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12115 /// implicit front of every search_path, so a bare reference to a
12116 /// known catalog table always means the catalog table. Only the
12117 /// names the engine actually synthesises are recognised — any
12118 /// other `pg_*` ident stays a user table (mailrs embed round-12).
12119 fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12120 // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12121 // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12122 // `pg_catalog` at the front of every search_path. (pg_stat_activity
12123 // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12124 // through the meta_view_result path instead, and already resolve
12125 // bare — they must NOT be listed here or the __spg_ rewrite would
12126 // mis-target them.)
12127 const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12128 let name = match self.tokens.get(self.pos) {
12129 Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12130 _ => return None,
12131 };
12132 // A following dot means this ident is a schema qualifier,
12133 // not a table name — let the qualified path handle it.
12134 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12135 return None;
12136 }
12137 if !PG_META_TABLES.contains(&name.as_str()) {
12138 return None;
12139 }
12140 self.advance();
12141 let bare = name.strip_prefix("pg_").unwrap_or(&name);
12142 Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12143 }
12144
12145 /// Consume a bare ident if its lowercase matches `kw`, else err.
12146 /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12147 /// Peeks only; the caller advances.
12148 fn peek_keyword_ident(&self, kw: &str) -> bool {
12149 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12150 }
12151
12152 fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12153 match self.advance() {
12154 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12155 other => Err(ParseError {
12156 message: format!("expected {kw:?}, got {other:?}"),
12157 token_pos: self.consumed_pos(),
12158 }),
12159 }
12160 }
12161
12162 /// Accept either a quoted identifier (`"foo"`) or a quoted string
12163 /// literal (`'foo'`) — same shape used by CREATE USER for the
12164 /// username slot.
12165 fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12166 match self.advance() {
12167 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12168 other => Err(ParseError {
12169 message: format!("expected identifier or string, got {other:?}"),
12170 token_pos: self.consumed_pos(),
12171 }),
12172 }
12173 }
12174
12175 fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12176 match self.advance() {
12177 Token::String(s) => Ok(s),
12178 other => Err(ParseError {
12179 message: format!("expected quoted string, got {other:?}"),
12180 token_pos: self.consumed_pos(),
12181 }),
12182 }
12183 }
12184
12185 fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12186 // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12187 // subqueries recurse through here without passing
12188 // parse_expr; share the same nesting budget.
12189 self.enter_nested()?;
12190 let r = self.parse_select_stmt_inner();
12191 self.nest_depth -= 1;
12192 r
12193 }
12194
12195 fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12196 // Caller dispatches on Token::Select; the inner helper handles
12197 // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12198 // get a fresh bare-select parse and may not have their own ORDER
12199 // BY / LIMIT.
12200 let mut head = self.parse_bare_select()?;
12201 self.parse_setop_chain_into(&mut head)?;
12202 self.parse_select_tail_into(&mut head)?;
12203 Ok(Statement::Select(head))
12204 }
12205
12206 /// v7.37.17 (17.6 siblings) — the three SQL set operations
12207 /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12208 /// token), and INTERSECT [ALL] (a bare ident — it was never
12209 /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12210 /// tighter than UNION / EXCEPT — the executor folds the chain
12211 /// left-to-right, which is already correct for LEADING
12212 /// intersects; an INTERSECT pair that FOLLOWS a union/except
12213 /// pair nests into that previous peer, so A UNION B INTERSECT C
12214 /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12215 /// groups.
12216 fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12217 // A parenthesized group arrives with its own (already
12218 // regrouped) unions on `head`; only the pairs THIS chain
12219 // appends participate in the precedence regroup below —
12220 // nesting an outer INTERSECT into a group-internal peer
12221 // would dissolve the explicit grouping.
12222 let boundary = head.unions.len();
12223 loop {
12224 let base = match self.peek() {
12225 Token::Union => UnionKind::Distinct,
12226 Token::Except => UnionKind::Except,
12227 Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12228 _ => break,
12229 };
12230 self.advance();
12231 let kind = if matches!(self.peek(), Token::All) {
12232 self.advance();
12233 match base {
12234 UnionKind::Distinct => UnionKind::All,
12235 UnionKind::Except => UnionKind::ExceptAll,
12236 _ => UnionKind::IntersectAll,
12237 }
12238 } else {
12239 base
12240 };
12241 let peer = self.parse_bare_select()?;
12242 head.unions.push((kind, peer));
12243 }
12244 let mut pairs = core::mem::take(&mut head.unions);
12245 let tail = pairs.split_off(boundary);
12246 let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12247 for (kind, peer) in tail {
12248 let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12249 // An intersect nests into the previous element of THIS
12250 // chain only; with no new previous element it stays at
12251 // the outer level (the left fold applies it to the
12252 // whole head, group included).
12253 match (
12254 is_intersect,
12255 regrouped.len() > boundary,
12256 regrouped.last_mut(),
12257 ) {
12258 (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12259 _ => regrouped.push((kind, peer)),
12260 }
12261 }
12262 head.unions = regrouped;
12263 Ok(())
12264 }
12265
12266 /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12267 /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12268 /// the top-level bare VALUES statement reuses it verbatim.
12269 /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12270 /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12271 /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12272 /// where the grouping-set universe is still in scope.
12273 fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12274 if !matches!(self.peek(), Token::Order) {
12275 return Ok(Vec::new());
12276 }
12277 self.advance();
12278 if !self.peek_is_by() {
12279 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12280 }
12281 self.advance();
12282 let mut keys = Vec::new();
12283 loop {
12284 // v7.39 (round 691) — save/restore, the discipline this parser
12285 // already uses around `pending_sample_preds`, so a subquery inside
12286 // a key neither inherits nor leaks the channel.
12287 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12288 let saved_coll = self.order_key_collation.take();
12289 let parsed = self.parse_expr(0);
12290 self.in_order_by_key = saved_flag;
12291 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12292 let expr = parsed?;
12293 let desc = if matches!(self.peek(), Token::Desc) {
12294 self.advance();
12295 true
12296 } else if matches!(self.peek(), Token::Asc) {
12297 self.advance();
12298 false
12299 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12300 // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12301 // one ordering per type, so the btree comparison operators map
12302 // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12303 // would need a custom operator class — honest error.
12304 self.advance();
12305 match self.advance() {
12306 Token::Lt | Token::LtEq => false,
12307 Token::Gt | Token::GtEq => true,
12308 other => {
12309 return Err(self.err(alloc::format!(
12310 "ORDER BY USING supports the btree comparison \
12311 operators (< <= > >=); got {other:?}"
12312 )));
12313 }
12314 }
12315 } else {
12316 false
12317 };
12318 // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12319 let nulls_first = self.parse_optional_nulls_placement()?;
12320 keys.push(OrderBy {
12321 expr,
12322 desc,
12323 nulls_first,
12324 collation,
12325 });
12326 if matches!(self.peek(), Token::Comma) {
12327 self.advance();
12328 } else {
12329 break;
12330 }
12331 }
12332 Ok(keys)
12333 }
12334
12335 fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12336 // v7.39 (round 135) — a grouping-set query may have already parsed +
12337 // rewritten its ORDER BY (to reference synthetic grouping columns); if
12338 // no ORDER BY token is present, keep that pre-set order_by rather than
12339 // clobbering it with an empty list.
12340 let parsed_keys = self.parse_order_by_keys()?;
12341 head.order_by = if parsed_keys.is_empty() {
12342 core::mem::take(&mut head.order_by)
12343 } else {
12344 parsed_keys
12345 };
12346 // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12347 // order. PG's grammar takes a limit clause and an offset clause
12348 // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12349 // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12350 // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12351 // spelling died on `expected end of input, got Limit`.
12352 //
12353 // Each may appear at most once, and LIMIT and FETCH FIRST are
12354 // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12355 // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12356 // A second one is left unconsumed here, which the caller reports
12357 // as trailing input rather than silently taking the last.
12358 let mut saw_limit = false;
12359 let mut saw_offset = false;
12360 loop {
12361 if !saw_limit && matches!(self.peek(), Token::Limit) {
12362 self.advance();
12363 // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12364 // PG synonyms for "no limit". Treat both as None
12365 // (no head.limit set) so the engine's existing
12366 // unlimited-result path takes over. Reject was the
12367 // pre-5.1 behaviour and broke pg_dump-flavoured
12368 // tooling that occasionally emits LIMIT NULL.
12369 if self.consume_limit_unbounded_sentinel() {
12370 head.limit = None;
12371 } else {
12372 let first = self.parse_limit_expr("LIMIT")?;
12373 // MySQL `LIMIT offset, count` — the first number is
12374 // the offset when a comma follows.
12375 if matches!(self.peek(), Token::Comma) {
12376 self.advance();
12377 let count = self.parse_limit_expr("LIMIT")?;
12378 head.offset = Some(first);
12379 saw_offset = true;
12380 head.limit = Some(count);
12381 } else {
12382 head.limit = Some(first);
12383 }
12384 }
12385 saw_limit = true;
12386 continue;
12387 }
12388 if !saw_offset && matches!(self.peek(), Token::Offset) {
12389 self.advance();
12390 // PG also accepts an optional `ROW` / `ROWS` trailer
12391 // after the offset value (`OFFSET 10 ROWS`). The
12392 // FETCH-FIRST branch below relies on the same.
12393 let off = self.parse_limit_expr("OFFSET")?;
12394 self.consume_optional_rows_keyword();
12395 head.offset = Some(off);
12396 saw_offset = true;
12397 continue;
12398 }
12399 // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12400 // the SQL-standard alias for LIMIT. PG accepts both
12401 // spellings interchangeably; pg_dump emits FETCH FIRST in
12402 // newer versions. We map it onto `head.limit` so the
12403 // engine path is unified.
12404 if !saw_limit
12405 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12406 if s.eq_ignore_ascii_case("fetch"))
12407 {
12408 self.advance(); // FETCH
12409 // `FIRST` or `NEXT` (both legal per SQL standard).
12410 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12411 if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12412 {
12413 self.advance();
12414 }
12415 // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12416 // implicit 1 — but we always consume one if present).
12417 let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12418 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12419 {
12420 // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12421 crate::ast::LimitExpr::Literal(1)
12422 } else {
12423 self.parse_limit_expr("FETCH FIRST")?
12424 };
12425 // Eat `ROW` / `ROWS` if not already consumed above.
12426 self.consume_optional_rows_keyword();
12427 // Optional `ONLY` (the spec form) — or the SQL:2008
12428 // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12429 // now honours WITH TIES by extending past the LIMIT
12430 // truncation point through every row that shares the
12431 // last-kept row's ORDER BY key.
12432 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12433 if s.eq_ignore_ascii_case("only"))
12434 {
12435 self.advance();
12436 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12437 if s.eq_ignore_ascii_case("with"))
12438 {
12439 self.advance(); // WITH
12440 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12441 if s.eq_ignore_ascii_case("ties"))
12442 {
12443 self.advance();
12444 head.limit_with_ties = true;
12445 }
12446 }
12447 head.limit = Some(count);
12448 saw_limit = true;
12449 continue;
12450 }
12451 break;
12452 }
12453 // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12454 // FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12455 // [ OF table_name [, …] ]
12456 // [ NOWAIT | SKIP LOCKED ]
12457 // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12458 // FOR SHARE OF t2`). SPG is a single-writer engine — every
12459 // SELECT already returns a consistent snapshot — so these
12460 // are accept-and-discard: the parser absorbs them so
12461 // mailrs / Rails / Django code paths that emit `SELECT
12462 // … FOR UPDATE` for advisory pessimistic locking load
12463 // without a parser error. The on-disk locking model is
12464 // unchanged; callers that rely on FOR UPDATE for read-
12465 // through-write ordering still get the right answer
12466 // because SPG serialises writes anyway.
12467 head.locking = self
12468 .consume_optional_for_lock_clauses()
12469 .map(alloc::boxed::Box::new);
12470 Ok(())
12471 }
12472
12473 /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12474 /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12475 /// LOCKED ]` trailers. Each clause is fully accepted and
12476 /// discarded — SPG's single-writer model already satisfies the
12477 /// callers' implicit ordering requirement. Stops at the first
12478 /// token that isn't `FOR`.
12479 fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12480 // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12481 // not discarded. PG keeps the strongest of several clauses; the
12482 // policy of the last one wins, which is what this loop records.
12483 let mut seen: Option<crate::ast::LockingClause> = None;
12484 while matches!(self.peek(), Token::For) {
12485 // v7.37.14 (A2.5-stub) — record that this query asked
12486 // for a row lock the parser is about to silently
12487 // discard. Operators surface the count via
12488 // `spg_sql::silent_for_update_count()` so they can
12489 // gauge how much of the workload depends on advisory
12490 // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12491 // before v7.37.15's per-row tuple locking lands.
12492 crate::record_silent_for_update_clause();
12493 self.advance(); // FOR
12494 // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12495 // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12496 let mut no_key = false;
12497 let mut key = false;
12498 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12499 if s.eq_ignore_ascii_case("no"))
12500 {
12501 self.advance(); // NO
12502 no_key = true;
12503 // The next ident should be KEY but be generous;
12504 // anything followed by UPDATE/SHARE is accepted.
12505 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12506 if s.eq_ignore_ascii_case("key"))
12507 {
12508 self.advance(); // KEY
12509 }
12510 }
12511 // `KEY` prefix (PG `FOR KEY SHARE`).
12512 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12513 if s.eq_ignore_ascii_case("key"))
12514 {
12515 self.advance(); // KEY
12516 key = true;
12517 }
12518 // Lock-strength keyword: UPDATE / SHARE. Required, but
12519 // we're lenient — an unexpected token here just bails
12520 // (we already consumed FOR; caller's downstream
12521 // dispatch will error if anything actually depends on
12522 // the trailing tokens).
12523 let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12524 if s.eq_ignore_ascii_case("update"));
12525 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12526 if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12527 {
12528 self.advance();
12529 use crate::ast::LockStrength as LS;
12530 let strength = match (is_update, no_key, key) {
12531 (true, true, _) => LS::NoKeyUpdate,
12532 (true, _, _) => LS::Update,
12533 (false, _, true) => LS::KeyShare,
12534 (false, _, _) => LS::Share,
12535 };
12536 seen = Some(crate::ast::LockingClause {
12537 strength,
12538 of_tables: alloc::vec::Vec::new(),
12539 policy: crate::ast::LockWait::Wait,
12540 });
12541 } else {
12542 // FOR by itself (or `FOR KEY` with nothing after) —
12543 // give up on the lock-clause path. We've already
12544 // advanced past FOR; further attempts to parse
12545 // here would clobber state.
12546 return seen;
12547 }
12548 // Optional `OF tbl[, tbl …]`. mailrs emits this when
12549 // joining and locking only a subset of tables.
12550 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12551 if s.eq_ignore_ascii_case("of"))
12552 {
12553 self.advance(); // OF
12554 #[allow(clippy::while_let_loop)]
12555 loop {
12556 match self.peek() {
12557 Token::Ident(_) | Token::QuotedIdent(_) => {
12558 // v7.39 (round 294) — the name is CAPTURED now: PG
12559 // validates it against the FROM clause, and an
12560 // uncaptured list silently means "lock everything".
12561 let mut nm = match self.advance() {
12562 Token::Ident(n) | Token::QuotedIdent(n) => n,
12563 _ => alloc::string::String::new(),
12564 };
12565 // Optional schema-qualified `schema.table`.
12566 if matches!(self.peek(), Token::Dot) {
12567 self.advance();
12568 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12569 {
12570 self.advance();
12571 nm = n;
12572 }
12573 }
12574 if let Some(c) = seen.as_mut() {
12575 c.of_tables.push(nm);
12576 }
12577 }
12578 _ => break,
12579 }
12580 if matches!(self.peek(), Token::Comma) {
12581 self.advance();
12582 } else {
12583 break;
12584 }
12585 }
12586 }
12587 // Optional `NOWAIT` | `SKIP LOCKED`.
12588 match self.peek().clone() {
12589 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12590 self.advance();
12591 if let Some(c) = seen.as_mut() {
12592 c.policy = crate::ast::LockWait::NoWait;
12593 }
12594 }
12595 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12596 self.advance(); // SKIP
12597 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12598 if s.eq_ignore_ascii_case("locked"))
12599 {
12600 self.advance(); // LOCKED
12601 if let Some(c) = seen.as_mut() {
12602 c.policy = crate::ast::LockWait::SkipLocked;
12603 }
12604 }
12605 }
12606 _ => {}
12607 }
12608 // Loop: PG allows multiple FOR clauses chained.
12609 }
12610 seen
12611 }
12612
12613 /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12614 /// Bind value gets resolved during prepared-statement Execute;
12615 /// the Pratt expression parser would over-accept here (e.g.
12616 /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12617 /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12618 /// sentinel tokens (PG synonyms for "no limit"). Returns true
12619 /// when one was consumed; caller skips the regular
12620 /// limit-value parse and leaves `head.limit` at None.
12621 fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12622 if matches!(self.peek(), Token::Null) {
12623 self.advance();
12624 return true;
12625 }
12626 if matches!(self.peek(), Token::All) {
12627 self.advance();
12628 return true;
12629 }
12630 false
12631 }
12632
12633 /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12634 /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12635 /// SQL-standard shape. No-op when missing.
12636 fn consume_optional_rows_keyword(&mut self) {
12637 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12638 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12639 {
12640 self.advance();
12641 }
12642 }
12643
12644 /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12645 ///
12646 /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12647 /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12648 /// constant, which is why that spelling keeps the token path below.
12649 ///
12650 /// Constants are folded here rather than carried into the tree: the
12651 /// 15+ execution paths that read the row count go through
12652 /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12653 /// means "no limit". A clause the engine could not resolve would
12654 /// therefore return the WHOLE table instead of failing. Folding at
12655 /// parse time keeps that impossible; a non-constant clause is still
12656 /// a clean error (recorded residual — closing it wants a resolution
12657 /// pre-pass on the simple-query path, where `substitute_placeholders`
12658 /// does not run).
12659 fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12660 // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12661 // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12662 // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12663 // ONLY` both work (its grammar takes a c_expr). Both measured
12664 // against PG 18.4 in round 305.
12665 if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12666 return self.parse_limit_constant(label);
12667 }
12668 // One pass, no rewind: `advance()` takes each token by
12669 // `mem::replace`, so a consumed token reads back as Eof and this
12670 // parser cannot backtrack. Everything — bare literal included —
12671 // is therefore folded from the parsed expression rather than
12672 // re-read from the token stream.
12673 let start = self.pos;
12674 let e = self.parse_expr(0)?;
12675 if let crate::ast::Expr::Placeholder(n) = e {
12676 return Ok(crate::ast::LimitExpr::Placeholder(n));
12677 }
12678 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12679 match fold_limit_constant(&e) {
12680 Some(Ok(v)) if v < 0 => Err(ParseError {
12681 message: alloc::format!("{neg_label} must not be negative"),
12682 token_pos: start,
12683 }),
12684 Some(Ok(v)) => u32::try_from(v)
12685 .map(crate::ast::LimitExpr::Literal)
12686 .map_err(|_| ParseError {
12687 message: alloc::format!("{label} value too large: {v}"),
12688 token_pos: start,
12689 }),
12690 Some(Err(message)) => Err(ParseError {
12691 message: message.replace("{L}", neg_label),
12692 token_pos: start,
12693 }),
12694 // v7.39 (round 305, V23) — not foldable at parse time
12695 // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
12696 // expression; the engine evaluates it once before dispatch.
12697 None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
12698 }
12699 }
12700
12701 fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12702 // v7.39 (round 239) — PG's row-count clause takes a bigint with its
12703 // coercion rules, not just an integer token: a NUMERIC rounds half
12704 // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
12705 // refused with PG's wording ("LIMIT must not be negative", 2201W /
12706 // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
12707 // content, failing as an input-syntax error on the value. General
12708 // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
12709 // they need an Expr-carrying LimitExpr variant.
12710 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12711 let err_at = |message: alloc::string::String, pos: usize| ParseError {
12712 message,
12713 token_pos: pos,
12714 };
12715 match self.advance() {
12716 Token::Integer(n) if n >= 0 => u32::try_from(n)
12717 .map(crate::ast::LimitExpr::Literal)
12718 .map_err(|_| ParseError {
12719 message: alloc::format!("{label} value too large: {n}"),
12720 token_pos: self.consumed_pos(),
12721 }),
12722 Token::Integer(_) => Err(err_at(
12723 alloc::format!("{neg_label} must not be negative"),
12724 self.pos.saturating_sub(1),
12725 )),
12726 Token::Numeric(t) => {
12727 let pos = self.pos.saturating_sub(1);
12728 let v: f64 = t.parse().map_err(|_| {
12729 err_at(
12730 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12731 pos,
12732 )
12733 })?;
12734 if v < 0.0 {
12735 return Err(err_at(
12736 alloc::format!("{neg_label} must not be negative"),
12737 pos,
12738 ));
12739 }
12740 // Round half away from zero — PG's numeric→bigint cast.
12741 // (no_std: no f64::round; v is non-negative, so truncating
12742 // v + 0.5 is the same thing.)
12743 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
12744 let rounded = (v + 0.5) as u64;
12745 u32::try_from(rounded)
12746 .map(crate::ast::LimitExpr::Literal)
12747 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
12748 }
12749 Token::Minus => {
12750 let pos = self.pos.saturating_sub(1);
12751 match self.peek() {
12752 Token::Integer(_) | Token::Numeric(_) => {
12753 self.advance();
12754 Err(err_at(
12755 alloc::format!("{neg_label} must not be negative"),
12756 pos,
12757 ))
12758 }
12759 other => Err(err_at(
12760 alloc::format!(
12761 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12762 ),
12763 pos,
12764 )),
12765 }
12766 }
12767 Token::String(t) => {
12768 let pos = self.pos.saturating_sub(1);
12769 match t.trim().parse::<i64>() {
12770 Ok(n) if n < 0 => Err(err_at(
12771 alloc::format!("{neg_label} must not be negative"),
12772 pos,
12773 )),
12774 Ok(n) => u32::try_from(n)
12775 .map(crate::ast::LimitExpr::Literal)
12776 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
12777 Err(_) => Err(err_at(
12778 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12779 pos,
12780 )),
12781 }
12782 }
12783 Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
12784 other => Err(ParseError {
12785 message: alloc::format!(
12786 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12787 ),
12788 token_pos: self.consumed_pos(),
12789 }),
12790 }
12791 }
12792
12793 /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
12794 /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
12795 /// `unions` empty and `order_by` / `limit` `None`; the top-level
12796 /// `parse_select_stmt` is responsible for filling those in.
12797 /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
12798 /// call in the expression tree to the per-set integer bitmask
12799 /// (PG semantics: one bit per argument, MSB first; 1 = the key
12800 /// is dropped in this grouping set). Runs during the ROLLUP /
12801 /// CUBE / GROUPING SETS expansion, where the set is known.
12802 /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
12803 /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
12804 fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
12805 if let Expr::FunctionCall { name, .. } = expr
12806 && name.eq_ignore_ascii_case("grouping")
12807 {
12808 if !out.iter().any(|e| e == expr) {
12809 out.push(expr.clone());
12810 }
12811 return;
12812 }
12813 match expr {
12814 Expr::Binary { lhs, rhs, .. } => {
12815 Self::collect_grouping_calls(lhs, out);
12816 Self::collect_grouping_calls(rhs, out);
12817 }
12818 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12819 Self::collect_grouping_calls(expr, out)
12820 }
12821 Expr::FunctionCall { args, .. } => {
12822 for a in args {
12823 Self::collect_grouping_calls(a, out);
12824 }
12825 }
12826 Expr::Case {
12827 operand,
12828 branches,
12829 else_branch,
12830 } => {
12831 if let Some(o) = operand {
12832 Self::collect_grouping_calls(o, out);
12833 }
12834 for (c, v) in branches {
12835 Self::collect_grouping_calls(c, out);
12836 Self::collect_grouping_calls(v, out);
12837 }
12838 if let Some(x) = else_branch {
12839 Self::collect_grouping_calls(x, out);
12840 }
12841 }
12842 _ => {}
12843 }
12844 }
12845
12846 /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
12847 /// `grp_exprs[k]` with a reference to the synthetic ordering column
12848 /// `__grp_ord_k` (injected per grouping-set branch).
12849 fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
12850 if let Expr::FunctionCall { name, .. } = expr
12851 && name.eq_ignore_ascii_case("grouping")
12852 {
12853 if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
12854 *expr = Expr::Column(crate::ast::ColumnName {
12855 qualifier: None,
12856 name: alloc::format!("__grp_ord_{k}"),
12857 });
12858 }
12859 return;
12860 }
12861 match expr {
12862 Expr::Binary { lhs, rhs, .. } => {
12863 Self::rewrite_grouping_to_col(lhs, grp_exprs);
12864 Self::rewrite_grouping_to_col(rhs, grp_exprs);
12865 }
12866 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12867 Self::rewrite_grouping_to_col(expr, grp_exprs)
12868 }
12869 Expr::FunctionCall { args, .. } => {
12870 for a in args {
12871 Self::rewrite_grouping_to_col(a, grp_exprs);
12872 }
12873 }
12874 Expr::Case {
12875 operand,
12876 branches,
12877 else_branch,
12878 } => {
12879 if let Some(o) = operand {
12880 Self::rewrite_grouping_to_col(o, grp_exprs);
12881 }
12882 for (c, v) in branches {
12883 Self::rewrite_grouping_to_col(c, grp_exprs);
12884 Self::rewrite_grouping_to_col(v, grp_exprs);
12885 }
12886 if let Some(x) = else_branch {
12887 Self::rewrite_grouping_to_col(x, grp_exprs);
12888 }
12889 }
12890 _ => {}
12891 }
12892 }
12893
12894 /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
12895 /// as the list of key sets it contributes. A bare expression is one
12896 /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
12897 /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
12898 /// the concatenation of its items' sets, where an item is itself an
12899 /// element, a parenthesized key list, or the empty set `()`. A
12900 /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
12901 /// move together.
12902 fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
12903 let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
12904 // ROLLUP ( … ) / CUBE ( … )
12905 if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
12906 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
12907 {
12908 let is_cube = is_kw(self.peek(), "cube");
12909 self.advance(); // ROLLUP / CUBE
12910 self.advance(); // (
12911 let mut units: Vec<Vec<Expr>> = Vec::new();
12912 loop {
12913 if matches!(self.peek(), Token::LParen) {
12914 // Composite unit: (a, b) rolls up as one.
12915 self.advance();
12916 let mut unit = Vec::new();
12917 if !matches!(self.peek(), Token::RParen) {
12918 loop {
12919 unit.push(self.parse_expr(0)?);
12920 match self.peek() {
12921 Token::Comma => {
12922 self.advance();
12923 }
12924 Token::RParen => break,
12925 other => {
12926 return Err(self.err(format!(
12927 "expected ',' or ')' in grouping unit, got {other:?}"
12928 )));
12929 }
12930 }
12931 }
12932 }
12933 self.advance(); // )
12934 units.push(unit);
12935 } else {
12936 units.push(alloc::vec![self.parse_expr(0)?]);
12937 }
12938 match self.peek() {
12939 Token::Comma => {
12940 self.advance();
12941 }
12942 Token::RParen => break,
12943 other => {
12944 return Err(self.err(format!(
12945 "expected ',' or ')' in grouping list, got {other:?}"
12946 )));
12947 }
12948 }
12949 }
12950 self.advance(); // )
12951 let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
12952 units
12953 .iter()
12954 .zip(unit_sel.iter())
12955 .filter(|(_, keep)| **keep)
12956 .flat_map(|(u, _)| u.iter().cloned())
12957 .collect()
12958 };
12959 let n = units.len();
12960 if is_cube {
12961 let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
12962 .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
12963 .collect();
12964 subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
12965 return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
12966 }
12967 return Ok((0..=n)
12968 .rev()
12969 .map(|keep| {
12970 let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
12971 flatten(&sel)
12972 })
12973 .collect());
12974 }
12975 // GROUPING SETS ( item [, item]* )
12976 if is_kw(self.peek(), "grouping")
12977 && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
12978 {
12979 self.advance(); // GROUPING
12980 self.advance(); // SETS
12981 if !matches!(self.peek(), Token::LParen) {
12982 return Err(self.err(format!(
12983 "expected '(' after GROUPING SETS, got {:?}",
12984 self.peek()
12985 )));
12986 }
12987 self.advance(); // outer (
12988 let mut sets: Vec<Vec<Expr>> = Vec::new();
12989 loop {
12990 if matches!(self.peek(), Token::LParen) {
12991 // A parenthesized key list (or the empty set).
12992 self.advance();
12993 let mut set = Vec::new();
12994 if !matches!(self.peek(), Token::RParen) {
12995 loop {
12996 set.push(self.parse_expr(0)?);
12997 match self.peek() {
12998 Token::Comma => {
12999 self.advance();
13000 }
13001 Token::RParen => break,
13002 other => {
13003 return Err(self.err(format!(
13004 "expected ',' or ')' in grouping set, got {other:?}"
13005 )));
13006 }
13007 }
13008 }
13009 }
13010 self.advance(); // )
13011 sets.push(set);
13012 } else {
13013 // A nested element: ROLLUP/CUBE/GROUPING SETS or a
13014 // bare expression.
13015 sets.extend(self.parse_grouping_element()?);
13016 }
13017 match self.peek() {
13018 Token::Comma => {
13019 self.advance();
13020 }
13021 Token::RParen => break,
13022 other => {
13023 return Err(self.err(format!(
13024 "expected ',' or ')' after a grouping set, got {other:?}"
13025 )));
13026 }
13027 }
13028 }
13029 self.advance(); // outer )
13030 return Ok(sets);
13031 }
13032 Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
13033 }
13034
13035 fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
13036 // v7.38 (read01) — a reference to a key that is dropped in this grouping
13037 // set evaluates to NULL, at any depth. Previously only a *top-level*
13038 // select item equal to a dropped key was nullified, so a key nested in
13039 // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13040 // column and failed to resolve against the set's synthetic schema.
13041 if dropped.iter().any(|d| d == expr) {
13042 *expr = Expr::Literal(Literal::Null);
13043 return;
13044 }
13045 if let Expr::FunctionCall { name, args } = expr
13046 && name.eq_ignore_ascii_case("grouping")
13047 {
13048 let mut mask: i64 = 0;
13049 for a in args.iter() {
13050 mask <<= 1;
13051 if dropped.iter().any(|d| d == a) {
13052 mask |= 1;
13053 }
13054 }
13055 // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13056 // literal: a bare integer in a select item is indistinguishable
13057 // from a positional reference once `ORDER BY 1` substitutes the
13058 // item back in, and the round-232 position check then read the
13059 // mask value as an out-of-range position. The cast changes
13060 // nothing semantically (grouping() is integer).
13061 *expr = Expr::Cast {
13062 expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13063 target: crate::ast::CastTarget::Int,
13064 };
13065 return;
13066 }
13067 // Generic recursion over the common expression shapes the
13068 // SELECT list uses; anything without child expressions is
13069 // left alone.
13070 match expr {
13071 Expr::FunctionCall { args, .. } => {
13072 for a in args {
13073 Self::substitute_grouping_calls(a, dropped);
13074 }
13075 }
13076 Expr::Binary { lhs, rhs, .. } => {
13077 Self::substitute_grouping_calls(lhs, dropped);
13078 Self::substitute_grouping_calls(rhs, dropped);
13079 }
13080 Expr::Unary { expr: inner, .. } => {
13081 Self::substitute_grouping_calls(inner, dropped);
13082 }
13083 Expr::Cast { expr: inner, .. } => {
13084 Self::substitute_grouping_calls(inner, dropped);
13085 }
13086 Expr::Case {
13087 operand,
13088 branches,
13089 else_branch,
13090 } => {
13091 if let Some(op) = operand {
13092 Self::substitute_grouping_calls(op, dropped);
13093 }
13094 for (w, t) in branches {
13095 Self::substitute_grouping_calls(w, dropped);
13096 Self::substitute_grouping_calls(t, dropped);
13097 }
13098 if let Some(e) = else_branch {
13099 Self::substitute_grouping_calls(e, dropped);
13100 }
13101 }
13102 // v7.38 (read01) — recurse into the remaining child-bearing shapes
13103 // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13104 // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13105 // …` is the canonical rollup-total label idiom).
13106 Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13107 Expr::Like { expr, pattern, .. } => {
13108 Self::substitute_grouping_calls(expr, dropped);
13109 Self::substitute_grouping_calls(pattern, dropped);
13110 }
13111 Expr::InList { expr, list, .. } => {
13112 Self::substitute_grouping_calls(expr, dropped);
13113 for item in list {
13114 Self::substitute_grouping_calls(item, dropped);
13115 }
13116 }
13117 Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13118 Expr::Array(items) => {
13119 for item in items {
13120 Self::substitute_grouping_calls(item, dropped);
13121 }
13122 }
13123 Expr::ArraySubscript { target, index } => {
13124 Self::substitute_grouping_calls(target, dropped);
13125 Self::substitute_grouping_calls(index, dropped);
13126 }
13127 Expr::ArraySlice { target, lo, hi } => {
13128 Self::substitute_grouping_calls(target, dropped);
13129 if let Some(lo) = lo {
13130 Self::substitute_grouping_calls(lo, dropped);
13131 }
13132 if let Some(hi) = hi {
13133 Self::substitute_grouping_calls(hi, dropped);
13134 }
13135 }
13136 Expr::AnyAll { expr, array, .. } => {
13137 Self::substitute_grouping_calls(expr, dropped);
13138 Self::substitute_grouping_calls(array, dropped);
13139 }
13140 _ => {}
13141 }
13142 }
13143
13144 fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13145 // v7.37.17 (17.6 siblings) — parenthesized set-operation
13146 // group: `( <select chain> )` usable anywhere a query block
13147 // is (head or peer of an outer chain). The group's own
13148 // unions ride the returned SelectStatement; the executor's
13149 // nested-peer recursion runs them.
13150 if matches!(self.peek(), Token::LParen)
13151 && matches!(
13152 self.tokens.get(self.pos + 1),
13153 Some(Token::Select | Token::LParen | Token::Values)
13154 )
13155 {
13156 self.advance(); // (
13157 self.enter_nested()?;
13158 // v7.37 D.20 — a group whose head is a VALUES list:
13159 // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13160 // otherwise recurse into a nested SELECT/group head.
13161 let mut head = (if matches!(self.peek(), Token::Values) {
13162 self.advance(); // VALUES
13163 self.parse_values_rows_body()
13164 } else {
13165 self.parse_bare_select()
13166 })
13167 .and_then(|mut h| {
13168 self.parse_setop_chain_into(&mut h)?;
13169 Ok(h)
13170 });
13171 self.nest_depth -= 1;
13172 let mut head = match &mut head {
13173 Ok(h) => core::mem::take(h),
13174 Err(_) => return head,
13175 };
13176 // v7.37.17 (17.6 siblings) — group-internal tail:
13177 // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13178 // group head, then wrap the group as a derived table
13179 // (SELECT * FROM (group)) so the outer chain / outer
13180 // tail can't clobber the group's own ordering or limit.
13181 let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13182 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13183 if s.eq_ignore_ascii_case("fetch"));
13184 if has_tail {
13185 self.parse_select_tail_into(&mut head)?;
13186 head = SelectStatement {
13187 locking: None,
13188 ctes: Vec::new(),
13189 distinct: false,
13190 distinct_on: Vec::new(),
13191 items: alloc::vec![SelectItem::Wildcard],
13192 from: Some(FromClause {
13193 primary: TableRef {
13194 name: "subquery".to_string(),
13195 alias: None,
13196 only: false,
13197 as_of_segment: None,
13198 unnest_expr: None,
13199 unnest_column_aliases: Vec::new(),
13200 with_ordinality: false,
13201 generate_series_args: None,
13202 lateral_subquery: Some(Box::new(head)),
13203 jsonb_each_text_arg: None,
13204 table_fn_call: None,
13205 rows_from: None,
13206 json_table: None,
13207 scalar_fn_item: false,
13208 },
13209 joins: Vec::new(),
13210 }),
13211 where_: None,
13212 group_by: None,
13213 group_by_all: false,
13214 having: None,
13215 unions: Vec::new(),
13216 order_by: Vec::new(),
13217 limit: None,
13218 offset: None,
13219 limit_with_ties: false,
13220 window_check_exprs: Vec::new(),
13221 };
13222 }
13223 if !matches!(self.peek(), Token::RParen) {
13224 return Err(self.err(format!(
13225 "expected ')' after parenthesized query group, got {:?}",
13226 self.peek()
13227 )));
13228 }
13229 self.advance();
13230 return Ok(head);
13231 }
13232 // `TABLE name` shorthand as a query block — valid anywhere
13233 // a SELECT head is (set-op peers included).
13234 if matches!(self.peek(), Token::Table)
13235 && matches!(
13236 self.tokens.get(self.pos + 1),
13237 Some(Token::Ident(_) | Token::QuotedIdent(_))
13238 )
13239 {
13240 return self.parse_table_shorthand();
13241 }
13242 if !matches!(self.peek(), Token::Select) {
13243 return Err(self.err(format!(
13244 "expected SELECT to start a query block, got {:?}",
13245 self.peek()
13246 )));
13247 }
13248 self.advance();
13249 let distinct = if matches!(self.peek(), Token::Distinct) {
13250 self.advance();
13251 true
13252 } else {
13253 false
13254 };
13255 // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13256 // keep the first row (per ORDER BY) of each group the
13257 // expressions define. Django's .distinct('field') shape.
13258 let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13259 self.advance(); // ON
13260 if !matches!(self.peek(), Token::LParen) {
13261 return Err(self.err(format!(
13262 "expected '(' after DISTINCT ON, got {:?}",
13263 self.peek()
13264 )));
13265 }
13266 self.advance();
13267 let mut exprs = Vec::new();
13268 loop {
13269 exprs.push(self.parse_expr(0)?);
13270 match self.peek() {
13271 Token::Comma => {
13272 self.advance();
13273 }
13274 Token::RParen => break,
13275 other => {
13276 return Err(self.err(format!(
13277 "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13278 )));
13279 }
13280 }
13281 }
13282 self.advance(); // )
13283 exprs
13284 } else {
13285 Vec::new()
13286 };
13287 let mut items = self.parse_select_list()?;
13288 // Scope the TABLESAMPLE lowering channel to this SELECT:
13289 // stash whatever an enclosing select accumulated, collect
13290 // our own FROM's predicates, restore after the combine.
13291 let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13292 let mut from = if matches!(self.peek(), Token::From) {
13293 self.advance();
13294 Some(self.parse_from_clause()?)
13295 } else {
13296 None
13297 };
13298 // v7.37 D.22 — a set-returning function in the projection with no FROM
13299 // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13300 // rows. Move the first SRF projection item to a FROM-position derived
13301 // table and replace it in the projection with a reference to its output
13302 // column; sibling scalar columns repeat per SRF row. PG names the output
13303 // column after the function (or its AS alias). Reuses the FROM-SRF
13304 // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13305 // works via the targetlist-SRF path.
13306 // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13307 // `SELECT * FROM f(args)` — the record's fields become the columns, which
13308 // is exactly what the function's own row shape already is. Anywhere else
13309 // (per outer row, or beside other items) it would need a real record-typed
13310 // projection, so it says so rather than answering something else.
13311 if let [
13312 SelectItem::Expr {
13313 expr: Expr::FunctionCall { name, args },
13314 ..
13315 },
13316 ] = items.as_slice()
13317 && name == "__record_expand"
13318 {
13319 let Some(Expr::FunctionCall {
13320 name: inner_name,
13321 args: inner_args,
13322 }) = args.first()
13323 else {
13324 return Err(self.err(
13325 "(<expr>).* expands a function's record — it needs a function call".into(),
13326 ));
13327 };
13328 if from.is_some() {
13329 return Err(self.err(
13330 "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13331 .into(),
13332 ));
13333 }
13334 let fn_ref = TableRef {
13335 name: inner_name.clone(),
13336 alias: None,
13337 only: false,
13338 as_of_segment: None,
13339 unnest_expr: None,
13340 unnest_column_aliases: Vec::new(),
13341 with_ordinality: false,
13342 generate_series_args: None,
13343 lateral_subquery: None,
13344 jsonb_each_text_arg: None,
13345 table_fn_call: Some(Box::new((
13346 inner_name.to_ascii_lowercase(),
13347 inner_args.clone(),
13348 ))),
13349 rows_from: None,
13350 json_table: None,
13351 scalar_fn_item: false,
13352 };
13353 items = alloc::vec![SelectItem::Wildcard];
13354 from = Some(FromClause {
13355 primary: fn_ref,
13356 joins: Vec::new(),
13357 });
13358 }
13359 // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13360 // FROM, keeps its marker: the ENGINE lowers it, because naming the
13361 // record's fields takes the catalog. It becomes a LATERAL of the same
13362 // function plus one item per declared column — the machinery rounds 65
13363 // and 69 already built.
13364 // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13365 // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13366 // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13367 // express, since the lifted one becomes a scan and the other would
13368 // expand per its rows (a cross product, not a zip). So when the
13369 // projection holds more than one top-level function call, the lift steps
13370 // aside and the engine's target-list expansion takes the whole list.
13371 let fn_call_items = items
13372 .iter()
13373 .filter(|it| {
13374 matches!(
13375 it,
13376 SelectItem::Expr {
13377 expr: Expr::FunctionCall { .. },
13378 ..
13379 }
13380 )
13381 })
13382 .count();
13383 if from.is_none() && fn_call_items <= 1 {
13384 let mut found: Option<(usize, TableRef, String)> = None;
13385 for (i, item) in items.iter().enumerate() {
13386 if let SelectItem::Expr {
13387 expr: Expr::FunctionCall { name, args },
13388 alias,
13389 } = item
13390 {
13391 let lname = name.to_ascii_lowercase();
13392 let colname = alias.clone().unwrap_or_else(|| lname.clone());
13393 let (unnest, gs) = match lname.as_str() {
13394 "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13395 "generate_series" if (2..=3).contains(&args.len()) => {
13396 (None, Some(args.clone()))
13397 }
13398 // v7.38 (read01) — generate_subscripts(arr, dim) in a
13399 // no-FROM projection yields the 1-based subscripts, i.e.
13400 // generate_series(1, array_length(arr, dim)); an invalid
13401 // dimension makes array_length NULL → 0 rows, as in PG.
13402 "generate_subscripts" if args.len() == 2 => (
13403 None,
13404 Some(alloc::vec![
13405 Expr::Literal(Literal::Integer(1)),
13406 Expr::FunctionCall {
13407 name: "array_length".to_string(),
13408 args: args.clone(),
13409 },
13410 ]),
13411 ),
13412 // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13413 // in a no-FROM projection unnest their *_to_array form.
13414 "string_to_table" | "regexp_split_to_table" => {
13415 let array_fn = if lname == "string_to_table" {
13416 "string_to_array"
13417 } else {
13418 "regexp_split_to_array"
13419 };
13420 (
13421 Some(Box::new(Expr::FunctionCall {
13422 name: array_fn.to_string(),
13423 args: args.clone(),
13424 })),
13425 None,
13426 )
13427 }
13428 // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13429 // a no-FROM projection expand per element. The scalar form
13430 // returns the elements as a TEXT array, so unnest over the
13431 // same call materialises one row each (same rewrite the
13432 // FROM-clause form uses).
13433 "jsonb_array_elements"
13434 | "json_array_elements"
13435 | "jsonb_array_elements_text"
13436 | "json_array_elements_text"
13437 if args.len() == 1 =>
13438 {
13439 (
13440 Some(Box::new(Expr::FunctionCall {
13441 name: lname.clone(),
13442 args: args.clone(),
13443 })),
13444 None,
13445 )
13446 }
13447 // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13448 // in a no-FROM projection expands per match (scalar form
13449 // returns the matches as a TEXT array → unnest).
13450 "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13451 Some(Box::new(Expr::FunctionCall {
13452 name: lname.clone(),
13453 args: args.clone(),
13454 })),
13455 None,
13456 ),
13457 _ => continue,
13458 };
13459 found = Some((
13460 i,
13461 TableRef {
13462 name: colname.clone(),
13463 alias: Some(colname.clone()),
13464 only: false,
13465 as_of_segment: None,
13466 unnest_expr: unnest,
13467 unnest_column_aliases: alloc::vec![colname.clone()],
13468 with_ordinality: false,
13469 generate_series_args: gs,
13470 lateral_subquery: None,
13471 jsonb_each_text_arg: None,
13472 table_fn_call: None,
13473 rows_from: None,
13474 json_table: None,
13475 scalar_fn_item: false,
13476 },
13477 colname,
13478 ));
13479 break;
13480 }
13481 }
13482 if let Some((idx, tref, colname)) = found {
13483 from = Some(FromClause {
13484 primary: tref,
13485 joins: Vec::new(),
13486 });
13487 items[idx] = SelectItem::Expr {
13488 expr: Expr::Column(ColumnName {
13489 qualifier: None,
13490 name: colname.clone(),
13491 }),
13492 alias: Some(colname),
13493 };
13494 }
13495 }
13496 let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13497 let where_ = if matches!(self.peek(), Token::Where) {
13498 self.advance();
13499 Some(self.parse_expr(0)?)
13500 } else {
13501 None
13502 };
13503 let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13504 Some(match acc {
13505 Some(w) => Expr::Binary {
13506 lhs: Box::new(pred),
13507 op: crate::ast::BinOp::And,
13508 rhs: Box::new(w),
13509 },
13510 None => pred,
13511 })
13512 });
13513 self.pending_sample_preds = enclosing_sample_preds;
13514 let mut group_by_all = false;
13515 // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13516 // share one expansion: `grouping_sets` lists the key subsets
13517 // (first = primary, assigned to stmt.group_by; the rest
13518 // become UNION ALL peers), `grouping_universe` is the full
13519 // key list used to compute each peer's dropped keys.
13520 let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13521 let mut grouping_universe: Vec<Expr> = Vec::new();
13522 // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13523 // A BOOL, not the key list: this frame is the statement parser's, and
13524 // round 430 measured that a `Vec` local here is enough on its own to
13525 // tip the 512 KiB nesting guard. The keys are recoverable from
13526 // `grouping_universe`, which a rollup fills with exactly them.
13527 let mut mysql_rollup = false;
13528 let group_by = if matches!(self.peek(), Token::Group) {
13529 self.advance();
13530 if !self.peek_is_by() {
13531 return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13532 }
13533 self.advance();
13534 // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13535 // every non-aggregate SELECT-list item later.
13536 if matches!(self.peek(), Token::All) {
13537 self.advance();
13538 group_by_all = true;
13539 None
13540 } else {
13541 // v7.39 (round 242) — PG's general grouping-element grammar:
13542 // GROUP BY [DISTINCT] element [, element]*, where an element
13543 // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13544 // SETS (…) — mixed freely. Each element yields a list of
13545 // key sets; the query's grouping sets are the CARTESIAN
13546 // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13547 // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13548 // content. ROLLUP/CUBE members may be composite
13549 // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13550 // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13551 // parser handled only a lone ROLLUP/CUBE/GS as the whole
13552 // clause.
13553 let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13554 self.advance();
13555 true
13556 } else {
13557 false
13558 };
13559 let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13560 loop {
13561 element_sets.push(self.parse_grouping_element()?);
13562 if matches!(self.peek(), Token::Comma) {
13563 self.advance();
13564 } else {
13565 break;
13566 }
13567 }
13568 let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13569 for el in &element_sets {
13570 let mut next: Vec<Vec<Expr>> = Vec::new();
13571 for base in &total {
13572 for set in el {
13573 let mut merged = base.clone();
13574 for k in set {
13575 if !merged.iter().any(|m| m == k) {
13576 merged.push(k.clone());
13577 }
13578 }
13579 next.push(merged);
13580 }
13581 }
13582 total = next;
13583 }
13584 // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13585 // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13586 // The keys and the aggregates come out identical; the ROW
13587 // ORDER does not, and that is the part a report depends on.
13588 // MySQL interleaves each group's subtotal right after its
13589 // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13590 // where the union-of-grouping-sets expansion emits every
13591 // leaf first and then every subtotal. MariaDB REFUSES an
13592 // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13593 // order itself — measured on MariaDB 11 and MySQL 9.7, which
13594 // agree on the order and disagree only on whether ORDER BY
13595 // is allowed (MySQL allows it; SPG allows it too, since
13596 // refusing would break the clients that can write it).
13597 if self.mysql_dialect
13598 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13599 && matches!(
13600 self.tokens.get(self.pos + 1),
13601 Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13602 )
13603 {
13604 self.advance(); // WITH
13605 self.advance(); // ROLLUP
13606 let keys = total.into_iter().next().unwrap_or_default();
13607 mysql_rollup = true;
13608 // n+1 prefixes, largest first — the same expansion
13609 // `ROLLUP (…)` produces.
13610 total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13611 }
13612 if distinct_sets {
13613 let mut seen: Vec<Vec<String>> = Vec::new();
13614 total.retain(|set| {
13615 let mut key: Vec<String> =
13616 set.iter().map(|e| alloc::format!("{e}")).collect();
13617 key.sort();
13618 if seen.contains(&key) {
13619 false
13620 } else {
13621 seen.push(key);
13622 true
13623 }
13624 });
13625 }
13626 if total.len() > 1 {
13627 let mut universe: Vec<Expr> = Vec::new();
13628 for set in &total {
13629 for k in set {
13630 if !universe.iter().any(|u| u == k) {
13631 universe.push(k.clone());
13632 }
13633 }
13634 }
13635 grouping_universe = universe;
13636 let primary = total[0].clone();
13637 grouping_sets = total;
13638 Some(primary)
13639 } else {
13640 // One set (a plain GROUP BY list, or a single-set
13641 // spelling like GROUPING SETS ((a, b))). An EMPTY
13642 // single set — GROUPING SETS (()) — stays
13643 // `Some(vec![])`: the grand-total group, which must
13644 // run the aggregate path.
13645 Some(total.into_iter().next().unwrap_or_default())
13646 }
13647 }
13648 } else {
13649 None
13650 };
13651 let having = if matches!(self.peek(), Token::Having) {
13652 self.advance();
13653 Some(self.parse_expr(0)?)
13654 } else {
13655 None
13656 };
13657 // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
13658 // OVER w parsed to a marker above; inline each definition
13659 // into the referencing WindowFunction nodes.
13660 let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
13661 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
13662 self.advance();
13663 loop {
13664 let wname = self.expect_ident_like()?;
13665 if !matches!(self.peek(), Token::As) {
13666 return Err(self.err(format!(
13667 "expected AS after WINDOW {wname}, got {:?}",
13668 self.peek()
13669 )));
13670 }
13671 self.advance();
13672 // v7.39 (round 229) — PG rejects a redefinition outright.
13673 if window_defs
13674 .iter()
13675 .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
13676 {
13677 return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
13678 }
13679 let def = self.parse_over_clause()?;
13680 // A definition may itself copy an earlier one
13681 // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
13682 // so resolve it against the defs already in scope. Same
13683 // copy rules as an `OVER (w1 …)` in the select list.
13684 let mut probe = Expr::WindowFunction {
13685 name: String::new(),
13686 args: Vec::new(),
13687 partition_by: def.0,
13688 order_by: def.1,
13689 frame: def.2,
13690 null_treatment: crate::ast::NullTreatment::Respect,
13691 filter: None,
13692 };
13693 Self::substitute_named_windows(&mut probe, &window_defs)
13694 .map_err(|m| self.err(m))?;
13695 let Expr::WindowFunction {
13696 partition_by,
13697 order_by,
13698 frame,
13699 ..
13700 } = probe
13701 else {
13702 unreachable!("probe is a WindowFunction")
13703 };
13704 window_defs.push((wname, (partition_by, order_by, frame)));
13705 if matches!(self.peek(), Token::Comma) {
13706 self.advance();
13707 continue;
13708 }
13709 break;
13710 }
13711 }
13712 // v7.39 (round 705) — which definitions did anything reference?
13713 // The ones nothing did used to be dropped here, unexamined, so
13714 // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
13715 // definition whether referenced or not. Their key expressions ride
13716 // out on the statement for the engine to resolve.
13717 let mut window_refs: Vec<String> = Vec::new();
13718 if !window_defs.is_empty() {
13719 for it in &items {
13720 if let SelectItem::Expr { expr, .. } = it {
13721 Self::collect_named_window_refs(expr, &mut window_refs);
13722 }
13723 }
13724 }
13725 let window_check_exprs: Vec<Expr> = window_defs
13726 .iter()
13727 .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
13728 .flat_map(|(_, (partition, order, _))| {
13729 partition
13730 .iter()
13731 .cloned()
13732 .chain(order.iter().map(|(e, _, _)| e.clone()))
13733 })
13734 .collect();
13735 if !window_defs.is_empty()
13736 || items
13737 .iter()
13738 .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
13739 {
13740 for it in &mut items {
13741 if let SelectItem::Expr { expr, .. } = it {
13742 Self::substitute_named_windows(expr, &window_defs)
13743 .map_err(|m| self.err(m))?;
13744 }
13745 }
13746 }
13747 // `GROUP BY 1` — positional keys substitute with the Nth
13748 // select item's expression (same contract ORDER BY has had
13749 // since v6.x). Out-of-range positions error.
13750 let group_by = match group_by {
13751 Some(mut keys) => {
13752 for k in &mut keys {
13753 if let Expr::Literal(Literal::Integer(n)) = k {
13754 let idx = *n;
13755 if idx < 1 || idx as usize > items.len() {
13756 return Err(self.err(alloc::format!(
13757 "GROUP BY position {idx} is not in select list"
13758 )));
13759 }
13760 match &items[(idx - 1) as usize] {
13761 SelectItem::Expr { expr, .. } => *k = expr.clone(),
13762 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
13763 return Err(self.err(alloc::format!(
13764 "GROUP BY position {idx} references a wildcard item"
13765 )));
13766 }
13767 }
13768 }
13769 }
13770 Some(keys)
13771 }
13772 None => None,
13773 };
13774 let mut stmt = SelectStatement {
13775 locking: None,
13776 ctes: Vec::new(),
13777 distinct,
13778 distinct_on,
13779 items,
13780 from,
13781 where_,
13782 group_by,
13783 group_by_all,
13784 having,
13785 unions: Vec::new(),
13786 order_by: Vec::new(),
13787 limit: None,
13788 offset: None,
13789 limit_with_ties: false,
13790 window_check_exprs,
13791 };
13792 // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
13793 // first set is the primary (already on stmt.group_by); each
13794 // further set becomes a UNION ALL peer with its dropped
13795 // keys (universe minus the set) replaced by NULL literals
13796 // in the peer's items and group_by. PG-legal: non-grouped
13797 // select items must be group keys or aggregates, so a
13798 // dropped key's occurrences in the projection are exactly
13799 // the ones to nullify.
13800 // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
13801 // over a plain GROUP BY (every argument must be a group key; the
13802 // mask is then 0) and rejects anything else with 42803. SPG's
13803 // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
13804 // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
13805 // function `grouping`".
13806 if grouping_sets.len() <= 1 {
13807 let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
13808 let mut calls: Vec<Expr> = Vec::new();
13809 for item in &stmt.items {
13810 if let SelectItem::Expr { expr, .. } = item {
13811 Self::collect_grouping_calls(expr, &mut calls);
13812 }
13813 }
13814 if let Some(h) = &stmt.having {
13815 Self::collect_grouping_calls(h, &mut calls);
13816 }
13817 for call in &calls {
13818 let Expr::FunctionCall { args, .. } = call else {
13819 continue;
13820 };
13821 for a in args {
13822 if !keys.iter().any(|k| k == a) {
13823 return Err(self.err(
13824 "arguments to GROUPING must be grouping expressions of the associated query level"
13825 .to_string(),
13826 ));
13827 }
13828 }
13829 }
13830 if !calls.is_empty() {
13831 for item in &mut stmt.items {
13832 if let SelectItem::Expr { expr, .. } = item {
13833 Self::substitute_grouping_calls(expr, &[]);
13834 }
13835 }
13836 if let Some(h) = &mut stmt.having {
13837 Self::substitute_grouping_calls(h, &[]);
13838 }
13839 }
13840 }
13841 if grouping_sets.len() > 1 {
13842 // The primary set's own dropped keys nullify in the
13843 // HEAD's projection too (GROUPING SETS's first set may
13844 // omit keys other sets use).
13845 let primary = grouping_sets[0].clone();
13846 let head_dropped: Vec<Expr> = grouping_universe
13847 .iter()
13848 .filter(|u| !primary.iter().any(|k| k == *u))
13849 .cloned()
13850 .collect();
13851 for set in grouping_sets.iter().skip(1) {
13852 let mut peer = stmt.clone();
13853 peer.unions = Vec::new();
13854 let dropped: Vec<&Expr> = grouping_universe
13855 .iter()
13856 .filter(|u| !set.iter().any(|k| k == *u))
13857 .collect();
13858 // Empty set = grand-total group: `Some(vec![])` forces
13859 // the aggregate path (one collapsed row) instead of a
13860 // per-row passthrough. See the primary-set note above.
13861 peer.group_by = Some(set.clone());
13862 let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
13863 for item in &mut peer.items {
13864 if let SelectItem::Expr { expr, alias } = item {
13865 if dropped.iter().any(|d| *d == expr) {
13866 // v7.39 — keep the dropped key's name on the
13867 // NULL literal so the UNION output column
13868 // (and any top-level ORDER BY on it) still
13869 // resolves.
13870 if alias.is_none()
13871 && let Expr::Column(c) = &expr
13872 {
13873 *alias = Some(c.name.clone());
13874 }
13875 *expr = Expr::Literal(Literal::Null);
13876 } else {
13877 Self::substitute_grouping_calls(expr, &dropped_owned);
13878 }
13879 }
13880 }
13881 if let Some(h) = &mut peer.having {
13882 Self::substitute_grouping_calls(h, &dropped_owned);
13883 }
13884 stmt.unions.push((UnionKind::All, peer));
13885 }
13886 for item in &mut stmt.items {
13887 if let SelectItem::Expr { expr, alias } = item {
13888 if head_dropped.iter().any(|d| d == expr) {
13889 if alias.is_none()
13890 && let Expr::Column(c) = &expr
13891 {
13892 *alias = Some(c.name.clone());
13893 }
13894 *expr = Expr::Literal(Literal::Null);
13895 } else {
13896 Self::substitute_grouping_calls(expr, &head_dropped);
13897 }
13898 }
13899 }
13900 if let Some(h) = &mut stmt.having {
13901 Self::substitute_grouping_calls(h, &head_dropped);
13902 }
13903 // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
13904 // (while `grouping_universe` / the per-branch sets are in scope). For
13905 // each grouping() call in it, inject a per-branch hidden column
13906 // `__grp_ord_K` carrying that branch's mask into the head + every
13907 // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
13908 // preserves this pre-set order_by; the engine strips `__grp_ord_*`
13909 // from the final output. A standalone grouping-set query has ORDER BY
13910 // (not an explicit set-op) next, so consuming it here is safe.
13911 // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
13912 // rollup carries the hierarchical order: sort by the grouping
13913 // keys with the rolled-up NULLs last, which is exactly the
13914 // interleaving both oracles emit. A client's own ORDER BY wins,
13915 // which is what MySQL does (MariaDB refuses to let one be
13916 // written at all).
13917 // The synthesised keys have to travel the SAME path a written
13918 // ORDER BY does: the block below is what turns a `grouping()`
13919 // call into the per-branch `__grp_ord_K` column the engine can
13920 // actually sort on. Bypassing it left a bare `grouping(text)`
13921 // for the evaluator to reject.
13922 let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
13923 self.parse_order_by_keys()?
13924 } else if mysql_rollup {
13925 Self::mysql_rollup_order(&grouping_universe)
13926 } else {
13927 Vec::new()
13928 };
13929 if !synthesised_or_parsed.is_empty() {
13930 let mut order_keys = synthesised_or_parsed;
13931 let mut grp_exprs: Vec<Expr> = Vec::new();
13932 for ob in &order_keys {
13933 Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
13934 }
13935 for (k, gexpr) in grp_exprs.iter().enumerate() {
13936 let colname = alloc::format!("__grp_ord_{k}");
13937 // Head branch (primary set) uses `head_dropped`.
13938 let mut he = gexpr.clone();
13939 Self::substitute_grouping_calls(&mut he, &head_dropped);
13940 stmt.items.push(SelectItem::Expr {
13941 expr: he,
13942 alias: Some(colname.clone()),
13943 });
13944 // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
13945 for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
13946 let set = &grouping_sets[i + 1];
13947 let dropped: Vec<Expr> = grouping_universe
13948 .iter()
13949 .filter(|u| !set.iter().any(|k| k == *u))
13950 .cloned()
13951 .collect();
13952 let mut pe = gexpr.clone();
13953 Self::substitute_grouping_calls(&mut pe, &dropped);
13954 peer.items.push(SelectItem::Expr {
13955 expr: pe,
13956 alias: Some(colname.clone()),
13957 });
13958 }
13959 }
13960 for ob in &mut order_keys {
13961 Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
13962 }
13963 stmt.order_by = order_keys;
13964 }
13965 }
13966 Ok(stmt)
13967 }
13968
13969 /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
13970 /// as ORDER BY keys.
13971 ///
13972 /// Per key: the rollup marker, then the key. Sorting on the key alone
13973 /// is not enough, and a table with a NULL in it says why — MariaDB puts
13974 /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
13975 /// the ROLLUP-introduced NULL last, and both print as NULL.
13976 /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
13977 /// real group including the data-NULL one, 1 only for the row the
13978 /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
13979 /// rolls up to NULL|2, a|1, b|3, NULL|6.
13980 ///
13981 /// `#[inline(never)]`: its locals must not join the statement parser's
13982 /// frame, which round 430 measured sitting against the nesting guard.
13983 #[inline(never)]
13984 fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
13985 let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
13986 for e in keys {
13987 out.push(OrderBy {
13988 expr: Expr::FunctionCall {
13989 name: "grouping".into(),
13990 args: alloc::vec![e.clone()],
13991 },
13992 desc: false,
13993 nulls_first: None,
13994 collation: None,
13995 });
13996 out.push(OrderBy {
13997 expr: e.clone(),
13998 desc: false,
13999 // MySQL orders NULL first on an ascending key.
14000 nulls_first: Some(true),
14001 collation: None,
14002 });
14003 }
14004 out
14005 }
14006
14007 /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
14008 /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
14009 #[inline(never)]
14010 fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
14011 use crate::ast::MaintainKind;
14012 self.skip_paren_option_list();
14013 let kind = match self.peek() {
14014 // `TABLE` and `INDEX` lex as keywords, not identifiers.
14015 Token::Table | Token::Index => {
14016 self.advance();
14017 MaintainKind::ReindexRelation
14018 }
14019 Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
14020 "index" | "table" => {
14021 self.advance();
14022 MaintainKind::ReindexRelation
14023 }
14024 "schema" => {
14025 self.advance();
14026 MaintainKind::ReindexSchema
14027 }
14028 "system" | "database" => {
14029 self.advance();
14030 MaintainKind::Whole
14031 }
14032 // PG requires the object type; anything else is the
14033 // caller's problem, not something to swallow.
14034 _ => MaintainKind::ReindexRelation,
14035 },
14036 _ => MaintainKind::Whole,
14037 };
14038 // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
14039 // allows the plain form, so the modifier is recorded rather than
14040 // skipped. It still has no effect on how the reindex runs.
14041 let mut concurrently = false;
14042 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14043 self.advance();
14044 concurrently = true;
14045 }
14046 let target = self.take_optional_maintain_name();
14047 self.consume_until_statement_boundary();
14048 Ok(Statement::Maintain {
14049 kind,
14050 concurrently,
14051 target,
14052 })
14053 }
14054
14055 /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14056 /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14057 #[inline(never)]
14058 fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14059 use crate::ast::MaintainKind;
14060 self.skip_paren_option_list();
14061 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14062 self.advance();
14063 }
14064 let target = self.take_optional_maintain_name();
14065 self.consume_until_statement_boundary();
14066 Ok(Statement::Maintain {
14067 kind: if target.is_some() {
14068 MaintainKind::ClusterRelation
14069 } else {
14070 MaintainKind::Whole
14071 },
14072 // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14073 // transaction block quite happily (measured).
14074 concurrently: false,
14075 target,
14076 })
14077 }
14078
14079 /// The next token as a relation / schema name, when there is one.
14080 fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14081 match self.peek() {
14082 Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14083 Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14084 _ => None,
14085 },
14086 _ => None,
14087 }
14088 }
14089
14090 /// A parenthesised option list, absorbed.
14091 fn skip_paren_option_list(&mut self) {
14092 if !matches!(self.peek(), Token::LParen) {
14093 return;
14094 }
14095 let mut depth = 0usize;
14096 loop {
14097 match self.advance() {
14098 Token::LParen => depth += 1,
14099 Token::RParen => {
14100 depth -= 1;
14101 if depth == 0 {
14102 return;
14103 }
14104 }
14105 Token::Eof => return,
14106 _ => {}
14107 }
14108 }
14109 }
14110
14111 /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14112 /// column list.
14113 ///
14114 /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14115 /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14116 /// / ALL. The three that describe physical storage have no meaning
14117 /// here, so they parse and change nothing rather than making a
14118 /// dump that mentions them fail to load.
14119 ///
14120 /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14121 /// parse chain the nesting sentinel is tuned against.
14122 #[inline(never)]
14123 fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14124 self.advance(); // LIKE
14125 let source = self.expect_ident_like()?;
14126 let mut options = crate::ast::LikeOptions::default();
14127 loop {
14128 let including = match self.peek() {
14129 Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14130 Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14131 _ => break,
14132 };
14133 self.advance();
14134 // `ALL` lexes as its own keyword, not an identifier.
14135 let opt = if matches!(self.peek(), Token::All) {
14136 self.advance();
14137 alloc::string::String::from("all")
14138 } else {
14139 self.expect_ident_like()?
14140 };
14141 let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14142 o.defaults = on;
14143 o.constraints = on;
14144 o.identity = on;
14145 o.generated = on;
14146 o.indexes = on;
14147 o.comments = on;
14148 };
14149 match opt.to_ascii_lowercase().as_str() {
14150 "all" => set(&mut options, including),
14151 "defaults" => options.defaults = including,
14152 "constraints" => options.constraints = including,
14153 "identity" => options.identity = including,
14154 "generated" => options.generated = including,
14155 "indexes" => options.indexes = including,
14156 "comments" => options.comments = including,
14157 // No storage model to copy into.
14158 "storage" | "statistics" | "compression" => {}
14159 other => {
14160 return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14161 }
14162 }
14163 }
14164 Ok(crate::ast::LikeSpec {
14165 source,
14166 at,
14167 options,
14168 })
14169 }
14170
14171 fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14172 // Caller already consumed CREATE; we're sitting on TABLE.
14173 debug_assert!(matches!(self.peek(), Token::Table));
14174 self.advance();
14175 let if_not_exists = self.consume_if_not_exists();
14176 let name = self.expect_ident_like()?;
14177 // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14178 // child shape has no column list; the child inherits its
14179 // columns from the parent at engine-DDL time. Detect it
14180 // before the `(` requirement below.
14181 if matches!(self.peek(), Token::Partition)
14182 && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14183 {
14184 self.advance(); // PARTITION
14185 self.advance(); // of
14186 let partition_of = self.parse_partition_of_tail()?;
14187 return Ok(Statement::CreateTable(CreateTableStatement {
14188 temporary: false,
14189 name,
14190 columns: Vec::new(),
14191 like_specs: Vec::new(),
14192 inherits: Vec::new(),
14193 if_not_exists,
14194 foreign_keys: Vec::new(),
14195 table_constraints: Vec::new(),
14196 partition_by: None,
14197 partition_of: Some(partition_of),
14198 }));
14199 }
14200 // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14201 // the materialized-view materialisation path (run the SELECT, infer the
14202 // column types, create + populate the table) but marks the node so the
14203 // executor creates a plain table without a mat-view registry entry.
14204 if matches!(self.peek(), Token::As) {
14205 self.advance();
14206 let body_stmt = self.parse_select_stmt()?;
14207 let Statement::Select(body) = body_stmt else {
14208 return Err(self.err(format!(
14209 "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14210 )));
14211 };
14212 let with_data = self.parse_optional_with_data(true)?;
14213 return Ok(Statement::CreateMaterializedView(
14214 crate::ast::CreateMaterializedViewStatement {
14215 temporary: false,
14216 name,
14217 if_not_exists,
14218 columns: Vec::new(),
14219 body,
14220 with_data,
14221 as_plain_table: true,
14222 },
14223 ));
14224 }
14225 if !matches!(self.peek(), Token::LParen) {
14226 return Err(self.err(format!(
14227 "expected '(' after table name, got {:?}",
14228 self.peek()
14229 )));
14230 }
14231 self.advance();
14232 let mut columns = Vec::new();
14233 let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14234 let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14235 let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14236 loop {
14237 // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14238 // column list. It is how a child that adds nothing of its own is
14239 // written, and this loop demanded at least one entry: `syntax
14240 // error at or near ")"`. The child takes the parent's columns,
14241 // which the INHERITS clause already arranges.
14242 if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14243 self.advance();
14244 break;
14245 }
14246 // v7.6.0 / v7.9.18 — distinguish table-level constraint
14247 // clauses from column definitions. Constraints start
14248 // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14249 // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14250 // a column.
14251 if self.peek_table_level_pk_start() {
14252 table_constraints.push(self.parse_table_level_primary_key()?);
14253 } else if matches!(self.peek(), Token::Like) {
14254 // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14255 // <opt> ]*`. The source table's shape lives in the catalog,
14256 // so this records the clause and the engine expands it.
14257 like_specs.push(self.parse_create_table_like(columns.len())?);
14258 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14259 // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14260 table_constraints.push(self.parse_table_level_exclude()?);
14261 } else if self.peek_table_level_unique_start() {
14262 table_constraints.push(self.parse_table_level_unique()?);
14263 } else if self.peek_table_level_check_start() {
14264 // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14265 table_constraints.push(self.parse_table_level_check()?);
14266 } else if self.peek_mysql_inline_key_start() {
14267 // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14268 // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14269 // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14270 // inside the column list. Skip name + paren list;
14271 // for UNIQUE KEY, register as a UC.
14272 if let Some(uc) = self.parse_mysql_inline_key()? {
14273 table_constraints.push(uc);
14274 }
14275 } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14276 // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14277 // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14278 // CHECK is named, and the named-CONSTRAINT arm used
14279 // to accept FOREIGN KEY only. The name is accepted
14280 // and discarded — same handling as every other SPG
14281 // constraint name.
14282 self.advance(); // CONSTRAINT
14283 // v7.39 (read01 round 48) — the name is kept now: the schema
14284 // stores it, so DROP / RENAME CONSTRAINT can find it.
14285 let con_name = self.expect_ident_like()?;
14286 let mut tc = match kind {
14287 NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14288 NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14289 NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14290 NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14291 };
14292 match &mut tc {
14293 crate::ast::TableConstraint::Check { name, .. }
14294 | crate::ast::TableConstraint::Unique { name, .. }
14295 | crate::ast::TableConstraint::PrimaryKey { name, .. }
14296 | crate::ast::TableConstraint::Exclude { name, .. } => {
14297 *name = Some(con_name);
14298 }
14299 _ => {}
14300 }
14301 table_constraints.push(tc);
14302 } else if self.peek_constraint_or_fk_start() {
14303 foreign_keys.push(self.parse_table_level_fk()?);
14304 } else {
14305 let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14306 // v7.13.0 — fold inline UNIQUE / CHECK column
14307 // constraints into table-level entries so the
14308 // engine path stays uniform.
14309 if col.is_unique {
14310 table_constraints.push(crate::ast::TableConstraint::Unique {
14311 name: None,
14312 columns: alloc::vec![col.name.clone()],
14313 nulls_not_distinct: col.unique_nulls_not_distinct,
14314 deferrable: col.constraint_deferrable,
14315 initially_deferred: col.constraint_initially_deferred,
14316 });
14317 }
14318 if let Some(check_expr) = col.check.clone() {
14319 table_constraints.push(crate::ast::TableConstraint::Check {
14320 name: None,
14321 expr: check_expr,
14322 not_valid: false,
14323 });
14324 }
14325 columns.push(col);
14326 if let Some(fk) = col_level_fk {
14327 foreign_keys.push(fk);
14328 }
14329 }
14330 match self.peek() {
14331 Token::Comma => {
14332 self.advance();
14333 }
14334 Token::RParen => {
14335 self.advance();
14336 break;
14337 }
14338 other => {
14339 return Err(
14340 self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14341 );
14342 }
14343 }
14344 }
14345 // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14346 // `CREATE TABLE k (LIKE t)` is a complete definition even though
14347 // nothing is written between the parentheses.
14348 // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14349 // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14350 // empty parentheses were a parse error in their own right — quite apart
14351 // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14352 // SPG does not have (filed separately).
14353 let _ = &like_specs;
14354 // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14355 // It sits between the column list and the MySQL table options,
14356 // and it was a syntax error until this round.
14357 let mut inherits: Vec<String> = Vec::new();
14358 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14359 if k.eq_ignore_ascii_case("inherits"))
14360 {
14361 self.advance();
14362 if !matches!(self.peek(), Token::LParen) {
14363 return Err(self.err(alloc::format!(
14364 "expected ( after INHERITS, got {:?}",
14365 self.peek()
14366 )));
14367 }
14368 self.advance();
14369 loop {
14370 inherits.push(self.expect_ident_like()?);
14371 if matches!(self.peek(), Token::Comma) {
14372 self.advance();
14373 continue;
14374 }
14375 break;
14376 }
14377 if !matches!(self.peek(), Token::RParen) {
14378 return Err(self.err(alloc::format!(
14379 "expected ) closing INHERITS, got {:?}",
14380 self.peek()
14381 )));
14382 }
14383 self.advance();
14384 }
14385 // v7.14.0 — consume MySQL/MariaDB table options after the
14386 // closing `)`. mysqldump emits things like
14387 // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14388 // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14389 // SPG accepts all forms as no-ops (each option is
14390 // `<ident> [=] <ident-or-string>` separated by whitespace).
14391 self.consume_mysql_table_options();
14392 // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14393 // SPG has no per-table reloptions, so accept and ignore them so a
14394 // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14395 self.consume_with_reloptions();
14396 // v7.37.6-B — declarative-partition-parent suffix
14397 // (`PARTITION BY RANGE (key_col)`) sits after the column
14398 // list + MySQL table-options. v7.37.6-B only accepts RANGE
14399 // and locks the key column at one ident; the engine then
14400 // verifies the column type is TIMESTAMPTZ.
14401 let partition_by = if matches!(self.peek(), Token::Partition) {
14402 self.advance(); // PARTITION
14403 if !self.peek_is_by() {
14404 return Err(self.err(format!(
14405 "expected BY after PARTITION, got {:?}",
14406 self.peek()
14407 )));
14408 }
14409 self.advance();
14410 Some(self.parse_partition_by_tail()?)
14411 } else {
14412 None
14413 };
14414 Ok(Statement::CreateTable(CreateTableStatement {
14415 temporary: false,
14416 name,
14417 columns,
14418 like_specs,
14419 inherits,
14420 if_not_exists,
14421 foreign_keys,
14422 table_constraints,
14423 partition_by,
14424 partition_of: None,
14425 }))
14426 }
14427
14428 /// v7.37.6-B — case-insensitive ident match helper for the
14429 /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14430 /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14431 /// didn't burn a global keyword slot for each (see the
14432 /// `Token::Partition` doc-comment in `lexer.rs`).
14433 fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14434 matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14435 }
14436
14437 /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14438 /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14439 fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14440 use crate::ast::{PartitionBySpec, PartitionKindAst};
14441 let kind = match self.peek() {
14442 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14443 self.advance();
14444 PartitionKindAst::Range
14445 }
14446 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14447 self.advance();
14448 PartitionKindAst::List
14449 }
14450 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14451 self.advance();
14452 PartitionKindAst::Hash
14453 }
14454 other => {
14455 return Err(self.err(format!(
14456 "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14457 )));
14458 }
14459 };
14460 if !matches!(self.peek(), Token::LParen) {
14461 return Err(self.err(format!(
14462 "expected '(' after PARTITION BY <strategy>, got {:?}",
14463 self.peek()
14464 )));
14465 }
14466 self.advance();
14467 let mut key_columns = Vec::new();
14468 loop {
14469 key_columns.push(self.expect_ident_like()?);
14470 match self.peek() {
14471 Token::Comma => {
14472 self.advance();
14473 }
14474 Token::RParen => {
14475 self.advance();
14476 break;
14477 }
14478 other => {
14479 return Err(self.err(format!(
14480 "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14481 )));
14482 }
14483 }
14484 }
14485 if key_columns.is_empty() {
14486 return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14487 }
14488 Ok(PartitionBySpec { kind, key_columns })
14489 }
14490
14491 /// v7.37.6-B — after `PARTITION OF`, expect
14492 /// <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14493 /// or
14494 /// <parent> DEFAULT
14495 fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14496 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14497 let parent_name = self.expect_ident_like()?;
14498 // v7.37.6-B rejects an explicit column list — the child
14499 // inherits from the parent. mailrs round-7 taught us that
14500 // CREATE TABLE-side schema reconciliation hides drift, so
14501 // we surface this as a parse error rather than silently
14502 // ignoring user columns.
14503 if matches!(self.peek(), Token::LParen) {
14504 return Err(self.err(
14505 "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14506 at v7.37.6-B; the child inherits its columns from the parent"
14507 .to_string(),
14508 ));
14509 }
14510 let bounds = match self.peek() {
14511 Token::Default => {
14512 self.advance();
14513 PartitionOfBoundsAst::Default
14514 }
14515 Token::For => {
14516 self.advance();
14517 if !matches!(self.peek(), Token::Values) {
14518 return Err(
14519 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14520 );
14521 }
14522 self.advance();
14523 // WITH is not a reserved Token in the lexer — it lexes
14524 // as Token::Ident("with"). Disambiguate manually.
14525 let want_with = matches!(
14526 self.peek(),
14527 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14528 );
14529 if want_with {
14530 self.advance();
14531 if !matches!(self.peek(), Token::LParen) {
14532 return Err(self.err(format!(
14533 "expected '(' after FOR VALUES WITH, got {:?}",
14534 self.peek()
14535 )));
14536 }
14537 self.advance();
14538 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14539 loop {
14540 let key = self.expect_ident_like()?;
14541 let n = match self.peek().clone() {
14542 Token::Integer(v) if u32::try_from(v).is_ok() => {
14543 self.advance();
14544 v as u32
14545 }
14546 other => {
14547 return Err(self.err(format!(
14548 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14549 )));
14550 }
14551 };
14552 match key.to_ascii_uppercase().as_str() {
14553 "MODULUS" => modulus = Some(n),
14554 "REMAINDER" => remainder = Some(n),
14555 other => {
14556 return Err(self.err(format!(
14557 "FOR VALUES WITH: unknown key {other:?}; \
14558 expected MODULUS or REMAINDER"
14559 )));
14560 }
14561 }
14562 match self.peek() {
14563 Token::Comma => {
14564 self.advance();
14565 }
14566 Token::RParen => {
14567 self.advance();
14568 break;
14569 }
14570 other => {
14571 return Err(self.err(format!(
14572 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14573 )));
14574 }
14575 }
14576 }
14577 let modulus = modulus
14578 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14579 let remainder = remainder.ok_or_else(|| {
14580 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14581 })?;
14582 if modulus == 0 {
14583 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14584 }
14585 if remainder >= modulus {
14586 return Err(self.err(format!(
14587 "FOR VALUES WITH: REMAINDER ({remainder}) \
14588 must be < MODULUS ({modulus})"
14589 )));
14590 }
14591 PartitionOfBoundsAst::Hash { modulus, remainder }
14592 } else {
14593 match self.peek() {
14594 Token::From => {
14595 self.advance();
14596 let lower = Box::new(self.parse_partition_bound_expr()?);
14597 if !matches!(self.peek(), Token::To) {
14598 return Err(self.err(format!(
14599 "expected TO after FROM (...), got {:?}",
14600 self.peek()
14601 )));
14602 }
14603 self.advance();
14604 let upper = Box::new(self.parse_partition_bound_expr()?);
14605 PartitionOfBoundsAst::Range { lower, upper }
14606 }
14607 // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14608 Token::In => {
14609 self.advance();
14610 if !matches!(self.peek(), Token::LParen) {
14611 return Err(self.err(format!(
14612 "expected '(' after FOR VALUES IN, got {:?}",
14613 self.peek()
14614 )));
14615 }
14616 self.advance();
14617 let mut values = Vec::new();
14618 loop {
14619 values.push(self.parse_expr(0)?);
14620 match self.peek() {
14621 Token::Comma => {
14622 self.advance();
14623 }
14624 Token::RParen => {
14625 self.advance();
14626 break;
14627 }
14628 other => {
14629 return Err(self.err(format!(
14630 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
14631 )));
14632 }
14633 }
14634 }
14635 if values.is_empty() {
14636 return Err(self.err(
14637 "FOR VALUES IN requires at least one literal".to_string(),
14638 ));
14639 }
14640 PartitionOfBoundsAst::List { values }
14641 }
14642 other => {
14643 return Err(self.err(format!(
14644 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
14645 )));
14646 }
14647 }
14648 }
14649 }
14650 other => {
14651 return Err(self.err(format!(
14652 "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
14653 )));
14654 }
14655 };
14656 Ok(PartitionOfSpec {
14657 parent_name,
14658 bounds,
14659 })
14660 }
14661
14662 /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
14663 /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
14664 /// markers (no-arg builtins) so the engine resolves them
14665 /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
14666 fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
14667 if !matches!(self.peek(), Token::LParen) {
14668 return Err(self.err(format!(
14669 "expected '(' before partition bound, got {:?}",
14670 self.peek()
14671 )));
14672 }
14673 self.advance();
14674 let expr = match self.peek() {
14675 Token::Ident(s) | Token::QuotedIdent(s)
14676 if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
14677 {
14678 let name = s.to_ascii_uppercase();
14679 self.advance();
14680 crate::ast::Expr::FunctionCall {
14681 name,
14682 args: Vec::new(),
14683 }
14684 }
14685 _ => self.parse_expr(0)?,
14686 };
14687 if !matches!(self.peek(), Token::RParen) {
14688 return Err(self.err(format!(
14689 "expected ')' after partition bound, got {:?}",
14690 self.peek()
14691 )));
14692 }
14693 self.advance();
14694 Ok(expr)
14695 }
14696
14697 /// v7.14.0 — true when the next tokens look like an inline
14698 /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
14699 /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
14700 /// — each followed by an optional name + `(...)`. Critical:
14701 /// a column NAMED `key` / `index` (PG accepts as ident) must
14702 /// NOT be mistaken for the KEY constraint shape. We disambig
14703 /// by requiring the keyword to be followed by either `(` or
14704 /// `<ident> (`.
14705 fn peek_mysql_inline_key_start(&self) -> bool {
14706 let cur = self.peek();
14707 // Shapes:
14708 // KEY (cols)
14709 // KEY name (cols)
14710 // INDEX (cols)
14711 // INDEX name (cols)
14712 // UNIQUE KEY [name] (cols)
14713 // UNIQUE INDEX [name] (cols)
14714 // FULLTEXT [KEY|INDEX] [name] (cols)
14715 // SPATIAL [KEY|INDEX] [name] (cols)
14716 let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
14717 // tokens at skip = the position AFTER the index-form
14718 // keywords (KEY/INDEX) have been consumed.
14719 match self.tokens.get(skip) {
14720 Some(Token::LParen) => true,
14721 Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
14722 matches!(self.tokens.get(skip + 1), Some(Token::LParen))
14723 }
14724 _ => false,
14725 }
14726 };
14727 // `INDEX` lexes as Token::Index (reserved), not as
14728 // Token::Ident("index"). Both shapes count as a KEY/INDEX
14729 // start; the peek helper below handles either.
14730 let is_key_or_index_tok = |t: &Token| -> bool {
14731 matches!(t, Token::Index)
14732 || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
14733 };
14734 match cur {
14735 Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
14736 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14737 after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
14738 }
14739 Token::Ident(s)
14740 if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
14741 {
14742 let nxt = self.tokens.get(self.pos + 1);
14743 let after_after = if nxt.is_some_and(is_key_or_index_tok) {
14744 self.pos + 2
14745 } else {
14746 self.pos + 1
14747 };
14748 after_keyword_followed_by_paren_or_ident_paren(after_after)
14749 }
14750 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
14751 let nxt = self.tokens.get(self.pos + 1);
14752 if !nxt.is_some_and(is_key_or_index_tok) {
14753 return false;
14754 }
14755 after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
14756 }
14757 _ => false,
14758 }
14759 }
14760
14761 /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
14762 /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
14763 /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
14764 /// returns Some(TableConstraint::Index) so the engine builds
14765 /// a real BTree index on the leading column (mysqldump
14766 /// `KEY idx_posts_author (author_id)` shape).
14767 /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
14768 /// (the storage layer has no matching AM).
14769 fn parse_mysql_inline_key(
14770 &mut self,
14771 ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
14772 // Detect UNIQUE prefix.
14773 let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
14774 {
14775 self.advance();
14776 true
14777 } else {
14778 false
14779 };
14780 // Consume FULLTEXT / SPATIAL prefix and record which one
14781 // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
14782 // dedicated TableConstraint variant so the engine can
14783 // build a tsvector-GIN; SPATIAL still has no matching
14784 // AM, so it falls back to accept-as-no-op.
14785 let mut is_fulltext = false;
14786 let mut is_spatial = false;
14787 if let Token::Ident(s) = self.peek().clone() {
14788 if s.eq_ignore_ascii_case("fulltext") {
14789 self.advance();
14790 is_fulltext = true;
14791 } else if s.eq_ignore_ascii_case("spatial") {
14792 self.advance();
14793 is_spatial = true;
14794 }
14795 }
14796 // KEY / INDEX keyword. `INDEX` lexes as Token::Index
14797 // (reserved); accept either token shape.
14798 match self.peek() {
14799 Token::Index => {
14800 self.advance();
14801 }
14802 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14803 self.advance();
14804 }
14805 other => {
14806 return Err(self.err(alloc::format!(
14807 "expected KEY/INDEX in inline index declaration, got {other:?}"
14808 )));
14809 }
14810 }
14811 // Optional index name (an ident before the `(`).
14812 // v7.15.0 — capture the name when present so the engine
14813 // builds the secondary index under the user's chosen
14814 // name (matches mysqldump's `KEY idx_x (col)` shape).
14815 let mut idx_name: Option<String> = None;
14816 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
14817 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
14818 {
14819 if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
14820 idx_name = Some(s);
14821 }
14822 }
14823 // Optional `USING BTREE` / `USING HASH` (MySQL).
14824 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
14825 self.advance();
14826 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14827 self.advance();
14828 }
14829 }
14830 // Required column list `(col [, col]*)`.
14831 if !matches!(self.peek(), Token::LParen) {
14832 return Err(self.err(alloc::format!(
14833 "expected '(' in inline KEY/INDEX, got {:?}",
14834 self.peek()
14835 )));
14836 }
14837 self.advance();
14838 let mut cols: Vec<String> = Vec::new();
14839 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
14840 self.advance();
14841 cols.push(s);
14842 // Skip optional `(length)` per-column prefix.
14843 if matches!(self.peek(), Token::LParen) {
14844 let mut depth = 1usize;
14845 self.advance();
14846 while depth > 0 {
14847 match self.peek() {
14848 Token::LParen => depth += 1,
14849 Token::RParen => depth -= 1,
14850 Token::Eof => break,
14851 _ => {}
14852 }
14853 self.advance();
14854 }
14855 }
14856 // Skip optional ASC / DESC.
14857 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
14858 || matches!(self.peek(), Token::Asc | Token::Desc)
14859 {
14860 self.advance();
14861 }
14862 if matches!(self.peek(), Token::Comma) {
14863 self.advance();
14864 continue;
14865 }
14866 break;
14867 }
14868 if matches!(self.peek(), Token::RParen) {
14869 self.advance();
14870 }
14871 // Trailing options on the inline index — comment / etc.
14872 // Skip until comma or `)`.
14873 while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
14874 self.advance();
14875 }
14876 if cols.is_empty() {
14877 return Ok(None);
14878 }
14879 if is_unique {
14880 // Carry the captured idx_name on UNIQUE too so future
14881 // engine work can name the underlying BTree
14882 // accordingly; today the unique-constraint installer
14883 // synthesises the name itself, but Display round-trip
14884 // benefits from preserving it.
14885 Ok(Some(crate::ast::TableConstraint::Unique {
14886 name: idx_name,
14887 columns: cols,
14888 nulls_not_distinct: false,
14889 // MySQL inline UNIQUE KEY has no deferral vocabulary.
14890 deferrable: false,
14891 initially_deferred: false,
14892 }))
14893 } else if is_fulltext {
14894 // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
14895 // routes through `TableConstraint::FulltextIndex`;
14896 // the engine builds a tsvector-GIN over each named
14897 // column so MATCH AGAINST gets a real inverted
14898 // index instead of a silently-dropped declaration.
14899 Ok(Some(crate::ast::TableConstraint::FulltextIndex {
14900 name: idx_name,
14901 columns: cols,
14902 }))
14903 } else if is_spatial {
14904 // SPG has no native SPATIAL AM. Accept-as-no-op
14905 // (declaration is parsed, but no index is built).
14906 Ok(None)
14907 } else {
14908 // v7.15.0 — plain KEY / INDEX builds a real BTree
14909 // secondary index.
14910 Ok(Some(crate::ast::TableConstraint::Index {
14911 name: idx_name,
14912 columns: cols,
14913 }))
14914 }
14915 }
14916
14917 /// v7.14.0 — consume MySQL/MariaDB table-options tail after
14918 /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
14919 /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
14920 /// (in any order, separated by whitespace).
14921 /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
14922 /// storage-parameter clause on CREATE TABLE. SPG has no per-table
14923 /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
14924 /// bare ident here, and only the parenthesised form is reloptions (so this
14925 /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
14926 fn consume_with_reloptions(&mut self) {
14927 let is_with = matches!(
14928 self.peek(),
14929 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14930 );
14931 if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
14932 return;
14933 }
14934 self.advance(); // WITH
14935 self.advance(); // (
14936 let mut depth = 1u32;
14937 while depth > 0 && !matches!(self.peek(), Token::Eof) {
14938 match self.peek() {
14939 Token::LParen => depth += 1,
14940 Token::RParen => depth -= 1,
14941 _ => {}
14942 }
14943 self.advance();
14944 }
14945 }
14946
14947 fn consume_mysql_table_options(&mut self) {
14948 loop {
14949 // Heuristic: a table option is an ident (or `DEFAULT`
14950 // reserved keyword) followed by `=` and an
14951 // ident / string / integer.
14952 let name_lc = match self.peek().clone() {
14953 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
14954 Token::Default => alloc::string::String::from("default"),
14955 _ => break,
14956 };
14957 let known = matches!(
14958 name_lc.as_str(),
14959 "engine"
14960 | "default"
14961 | "charset"
14962 | "collate"
14963 | "auto_increment"
14964 | "row_format"
14965 | "comment"
14966 | "pack_keys"
14967 | "stats_persistent"
14968 | "stats_auto_recalc"
14969 | "stats_sample_pages"
14970 | "key_block_size"
14971 | "tablespace"
14972 | "min_rows"
14973 | "max_rows"
14974 | "checksum"
14975 | "delay_key_write"
14976 | "insert_method"
14977 | "data"
14978 | "index"
14979 | "encryption"
14980 | "compression"
14981 );
14982 if !known {
14983 break;
14984 }
14985 self.advance(); // option name
14986 // `DEFAULT` optional prefix is followed by `CHARSET` /
14987 // `COLLATE`; consume the next ident too.
14988 if name_lc == "default" {
14989 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14990 self.advance();
14991 }
14992 }
14993 if matches!(self.peek(), Token::Eq) {
14994 self.advance();
14995 }
14996 match self.peek() {
14997 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_) | Token::Integer(_) => {
14998 self.advance();
14999 }
15000 _ => {}
15001 }
15002 }
15003 }
15004
15005 /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
15006 /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
15007 /// sure (otherwise a column literally named `primary` would
15008 /// be mistaken).
15009 fn peek_table_level_pk_start(&self) -> bool {
15010 let cur = self.peek();
15011 let nxt = self.tokens.get(self.pos + 1);
15012 let nxt2 = self.tokens.get(self.pos + 2);
15013 let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
15014 let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
15015 let is_lparen = matches!(nxt2, Some(Token::LParen));
15016 is_primary && is_key && is_lparen
15017 }
15018
15019 /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
15020 /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
15021 /// (mailrs round-5 G10).
15022 fn peek_table_level_unique_start(&self) -> bool {
15023 let cur = self.peek();
15024 let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
15025 if !is_unique {
15026 return false;
15027 }
15028 let n1 = self.tokens.get(self.pos + 1);
15029 // Plain `UNIQUE (…)`.
15030 if matches!(n1, Some(Token::LParen)) {
15031 return true;
15032 }
15033 // `UNIQUE NULLS [NOT] DISTINCT (…)`.
15034 let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
15035 if !is_nulls {
15036 return false;
15037 }
15038 let n2 = self.tokens.get(self.pos + 2);
15039 let n3 = self.tokens.get(self.pos + 3);
15040 let n4 = self.tokens.get(self.pos + 4);
15041 // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15042 if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15043 return true;
15044 }
15045 // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15046 if matches!(n2, Some(Token::Not))
15047 && matches!(n3, Some(Token::Distinct))
15048 && matches!(n4, Some(Token::LParen))
15049 {
15050 return true;
15051 }
15052 false
15053 }
15054
15055 fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15056 self.advance(); // PRIMARY
15057 self.advance(); // KEY
15058 let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15059 // v7.39 (round 711) — the trailer's values are CARRIED now; round
15060 // 621 consumed and dropped them (the storing half of F08).
15061 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15062 Ok(crate::ast::TableConstraint::PrimaryKey {
15063 name: None,
15064 columns,
15065 deferrable,
15066 initially_deferred,
15067 })
15068 }
15069
15070 fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15071 self.advance(); // UNIQUE
15072 // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15073 // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15074 // is `NULLS DISTINCT` per the SQL standard.
15075 let mut nulls_not_distinct = false;
15076 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15077 let n1 = self.tokens.get(self.pos + 1);
15078 let n2 = self.tokens.get(self.pos + 2);
15079 let is_not = matches!(n1, Some(Token::Not));
15080 let is_distinct = matches!(n2, Some(Token::Distinct));
15081 if is_not && is_distinct {
15082 self.advance(); // NULLS
15083 self.advance(); // NOT
15084 self.advance(); // DISTINCT
15085 nulls_not_distinct = true;
15086 } else if matches!(n1, Some(Token::Distinct)) {
15087 self.advance(); // NULLS
15088 self.advance(); // DISTINCT
15089 }
15090 }
15091 let columns = self.parse_paren_ident_list("UNIQUE")?;
15092 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15093 Ok(crate::ast::TableConstraint::Unique {
15094 name: None,
15095 columns,
15096 nulls_not_distinct,
15097 deferrable,
15098 initially_deferred,
15099 })
15100 }
15101
15102 /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15103 /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15104 /// expression.
15105 /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15106 /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15107 /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15108 /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15109 /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15110 /// commit: `NOT` starts no other suffix here, but reading both
15111 /// tokens before advancing keeps the caller's error message intact
15112 /// if someone writes `NOT NULL` by mistake.
15113 fn parse_not_valid_suffix(&mut self) -> bool {
15114 if !matches!(self.peek(), Token::Not) {
15115 return false;
15116 }
15117 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15118 {
15119 return false;
15120 }
15121 self.advance();
15122 self.advance();
15123 true
15124 }
15125
15126 fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15127 self.advance(); // EXCLUDE
15128 // Optional `USING <method>`.
15129 let mut method = None;
15130 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15131 self.advance();
15132 method = Some(match self.advance() {
15133 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15134 other => {
15135 return Err(self.err(alloc::format!(
15136 "expected index method after USING, got {other:?}"
15137 )));
15138 }
15139 });
15140 }
15141 if !matches!(self.peek(), Token::LParen) {
15142 return Err(self.err(alloc::format!(
15143 "expected '(' after EXCLUDE, got {:?}",
15144 self.peek()
15145 )));
15146 }
15147 self.advance();
15148 let mut elements: Vec<(String, String)> = Vec::new();
15149 loop {
15150 let col = match self.advance() {
15151 Token::Ident(s) | Token::QuotedIdent(s) => s,
15152 other => {
15153 return Err(self.err(alloc::format!(
15154 "expected column name in EXCLUDE, got {other:?}"
15155 )));
15156 }
15157 };
15158 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15159 return Err(self.err(alloc::format!(
15160 "expected WITH after EXCLUDE column, got {:?}",
15161 self.peek()
15162 )));
15163 }
15164 self.advance();
15165 let op = match self.advance() {
15166 Token::InetOverlap => String::from("&&"),
15167 Token::Intersects => String::from("?#"),
15168 Token::IsBelow => String::from("<^"),
15169 Token::IsAbove => String::from(">^"),
15170 Token::PatternLt => String::from("~<~"),
15171 Token::PatternLtEq => String::from("~<=~"),
15172 Token::PatternGt => String::from("~>~"),
15173 Token::PatternGtEq => String::from("~>=~"),
15174 Token::TsMatchOld => String::from("@@@"),
15175 Token::Eq => String::from("="),
15176 Token::JsonContains => String::from("@>"),
15177 Token::JsonContainedBy => String::from("<@"),
15178 Token::OverLeft => String::from("&<"),
15179 Token::OverRight => String::from("&>"),
15180 other => {
15181 return Err(self.err(alloc::format!(
15182 "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15183 )));
15184 }
15185 };
15186 elements.push((col, op));
15187 if matches!(self.peek(), Token::Comma) {
15188 self.advance();
15189 continue;
15190 }
15191 break;
15192 }
15193 if !matches!(self.peek(), Token::RParen) {
15194 return Err(self.err(alloc::format!(
15195 "expected ')' to close EXCLUDE, got {:?}",
15196 self.peek()
15197 )));
15198 }
15199 self.advance();
15200 Ok(crate::ast::TableConstraint::Exclude {
15201 name: None,
15202 method,
15203 elements,
15204 })
15205 }
15206
15207 fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15208 self.advance(); // CHECK
15209 if !matches!(self.peek(), Token::LParen) {
15210 return Err(self.err(alloc::format!(
15211 "expected '(' after CHECK, got {:?}",
15212 self.peek()
15213 )));
15214 }
15215 self.advance();
15216 let expr = self.parse_expr(0)?;
15217 if !matches!(self.peek(), Token::RParen) {
15218 return Err(self.err(alloc::format!(
15219 "expected ')' to close CHECK predicate, got {:?}",
15220 self.peek()
15221 )));
15222 }
15223 self.advance();
15224 // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15225 // are no existing rows for PG to skip, so it rejects the suffix.
15226 Ok(crate::ast::TableConstraint::Check {
15227 name: None,
15228 expr,
15229 not_valid: false,
15230 })
15231 }
15232
15233 /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15234 fn peek_table_level_check_start(&self) -> bool {
15235 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15236 }
15237
15238 /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15239 /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15240 /// on the dedicated FK path (`parse_table_level_fk` consumes its
15241 /// own CONSTRAINT prefix).
15242 fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15243 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15244 return None;
15245 }
15246 // tokens[pos+1] is the constraint name (any ident-like);
15247 // tokens[pos+2] is the kind keyword.
15248 match self.tokens.get(self.pos + 2) {
15249 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15250 Some(NamedTableConstraintKind::Check)
15251 }
15252 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15253 Some(NamedTableConstraintKind::Unique)
15254 }
15255 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15256 Some(NamedTableConstraintKind::PrimaryKey)
15257 }
15258 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15259 Some(NamedTableConstraintKind::Exclude)
15260 }
15261 _ => None,
15262 }
15263 }
15264
15265 fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15266 if !matches!(self.peek(), Token::LParen) {
15267 return Err(self.err(alloc::format!(
15268 "expected '(' after {ctx}, got {:?}",
15269 self.peek()
15270 )));
15271 }
15272 self.advance();
15273 let mut out = Vec::new();
15274 loop {
15275 out.push(self.expect_ident_like()?);
15276 match self.peek() {
15277 Token::Comma => {
15278 self.advance();
15279 }
15280 Token::RParen => {
15281 self.advance();
15282 break;
15283 }
15284 other => {
15285 return Err(self.err(alloc::format!(
15286 "expected ',' or ')' in {ctx} list, got {other:?}"
15287 )));
15288 }
15289 }
15290 }
15291 if out.is_empty() {
15292 return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15293 }
15294 Ok(out)
15295 }
15296
15297 /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15298 /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15299 /// table-level FK; a column def never starts with either keyword
15300 /// (column names are not in this reserved set).
15301 fn peek_constraint_or_fk_start(&self) -> bool {
15302 let is_constraint_kw = matches!(
15303 self.peek(),
15304 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15305 );
15306 let is_foreign_kw = matches!(
15307 self.peek(),
15308 Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15309 );
15310 is_constraint_kw || is_foreign_kw
15311 }
15312
15313 /// v7.6.0 — parse a table-level FK clause:
15314 /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15315 /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15316 fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15317 let mut name: Option<String> = None;
15318 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15319 self.advance();
15320 name = Some(self.expect_ident_like()?);
15321 }
15322 // `FOREIGN`
15323 match self.advance() {
15324 Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15325 other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15326 }
15327 // `KEY`
15328 match self.advance() {
15329 Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15330 other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15331 }
15332 // `(col, col, ...)`
15333 if !matches!(self.peek(), Token::LParen) {
15334 return Err(self.err(format!(
15335 "expected '(' after FOREIGN KEY, got {:?}",
15336 self.peek()
15337 )));
15338 }
15339 self.advance();
15340 let mut columns = Vec::new();
15341 loop {
15342 columns.push(self.expect_ident_like()?);
15343 match self.peek() {
15344 Token::Comma => {
15345 self.advance();
15346 }
15347 Token::RParen => {
15348 self.advance();
15349 break;
15350 }
15351 other => {
15352 return Err(self.err(format!(
15353 "expected ',' or ')' in FK column list, got {other:?}"
15354 )));
15355 }
15356 }
15357 }
15358 if columns.is_empty() {
15359 return Err(self.err("FOREIGN KEY requires at least one column".into()));
15360 }
15361 let (
15362 parent_table,
15363 parent_columns,
15364 on_delete,
15365 on_update,
15366 match_type,
15367 deferrable,
15368 initially_deferred,
15369 ) = self.parse_references_tail(columns.len())?;
15370 Ok(ForeignKeyConstraint {
15371 name,
15372 columns,
15373 parent_table,
15374 parent_columns,
15375 on_delete,
15376 on_update,
15377 match_type,
15378 deferrable,
15379 initially_deferred,
15380 })
15381 }
15382
15383 /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15384 /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15385 /// the local column count, used to default the parent column
15386 /// list when omitted (SQL spec: parent's PK is implied).
15387 fn parse_references_tail(
15388 &mut self,
15389 expected_arity: usize,
15390 ) -> Result<
15391 (
15392 String,
15393 Vec<String>,
15394 FkAction,
15395 FkAction,
15396 crate::ast::MatchType,
15397 // v7.39 (round 288) — deferrable, initially_deferred.
15398 bool,
15399 bool,
15400 ),
15401 ParseError,
15402 > {
15403 match self.advance() {
15404 Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15405 other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15406 }
15407 let parent_table = self.expect_ident_like()?;
15408 let mut parent_columns: Vec<String> = Vec::new();
15409 if matches!(self.peek(), Token::LParen) {
15410 self.advance();
15411 loop {
15412 parent_columns.push(self.expect_ident_like()?);
15413 match self.peek() {
15414 Token::Comma => {
15415 self.advance();
15416 }
15417 Token::RParen => {
15418 self.advance();
15419 break;
15420 }
15421 other => {
15422 return Err(self.err(format!(
15423 "expected ',' or ')' in REFERENCES column list, got {other:?}"
15424 )));
15425 }
15426 }
15427 }
15428 }
15429 if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15430 return Err(self.err(format!(
15431 "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15432 expected_arity,
15433 parent_columns.len()
15434 )));
15435 }
15436 // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15437 // it between the referenced column list and the ON / DEFERRABLE
15438 // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15439 // is skipped when any referencing column is NULL), so SIMPLE —
15440 // the default, and the only spelling pg_dump emits — is accepted
15441 // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15442 // mixed-NULL rule, which is not wired yet; reject them honestly
15443 // rather than silently applying SIMPLE (PG itself errors on
15444 // MATCH PARTIAL as "not yet implemented").
15445 let mut match_type = crate::ast::MatchType::Simple;
15446 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15447 self.advance();
15448 // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15449 // SIMPLE / PARTIAL arrive as bare identifiers.
15450 let kind = match self.advance() {
15451 Token::Full => "FULL".to_string(),
15452 Token::Ident(s) => s.to_uppercase(),
15453 other => {
15454 return Err(self.err(format!(
15455 "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15456 )));
15457 }
15458 };
15459 match kind.as_str() {
15460 "SIMPLE" => {} // Default — match_type stays Simple.
15461 // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15462 // when ALL referencing columns are NULL; a mixed-NULL key errors.
15463 "FULL" => match_type = crate::ast::MatchType::Full,
15464 "PARTIAL" => {
15465 return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15466 }
15467 _ => {
15468 return Err(self.err(format!(
15469 "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15470 )));
15471 }
15472 }
15473 }
15474 // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15475 // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15476 // <action>` / `ON UPDATE <action>` in either order. PG /
15477 // pg_dump emits the timing clause AFTER the ON clauses
15478 // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15479 // but the SQL spec allows either order. We loop over
15480 // every possible trailer and dispatch on the next token,
15481 // stopping when nothing matches. Phase 3.1 changes the
15482 // bare DEFERRABLE form from hard-error to accept-as-
15483 // immediate; SPG is single-writer with no deferred-
15484 // constraint window so the runtime semantics are always
15485 // immediate even when INITIALLY DEFERRED is requested.
15486 // PG's default referential action (no ON DELETE / ON UPDATE
15487 // clause) is NO ACTION, not RESTRICT — the two enforce
15488 // identically in SPG (single-writer, no deferred window; see the
15489 // shared match arm in constraints.rs) but information_schema.
15490 // referential_constraints must report NO ACTION to match PG.
15491 let mut on_delete = FkAction::NoAction;
15492 let mut on_update = FkAction::NoAction;
15493 let mut seen_on_delete = false;
15494 let mut seen_on_update = false;
15495 let mut deferrable = false;
15496 let mut initially_deferred = false;
15497 loop {
15498 // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15499 let before = self.pos;
15500 let (d, idef) = self.consume_deferrable_clauses_timed()?;
15501 if self.pos != before {
15502 deferrable = d;
15503 initially_deferred = idef;
15504 continue;
15505 }
15506 // ON DELETE / ON UPDATE.
15507 if !matches!(self.peek(), Token::On) {
15508 break;
15509 }
15510 self.advance();
15511 let which = self.advance();
15512 let action = self.parse_fk_action()?;
15513 match which {
15514 Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15515 if seen_on_delete {
15516 return Err(self.err("ON DELETE specified twice".into()));
15517 }
15518 seen_on_delete = true;
15519 on_delete = action;
15520 }
15521 Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15522 if seen_on_update {
15523 return Err(self.err("ON UPDATE specified twice".into()));
15524 }
15525 seen_on_update = true;
15526 on_update = action;
15527 }
15528 other => {
15529 return Err(
15530 self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15531 );
15532 }
15533 }
15534 }
15535 Ok((
15536 parent_table,
15537 parent_columns,
15538 on_delete,
15539 on_update,
15540 match_type,
15541 deferrable,
15542 initially_deferred,
15543 ))
15544 }
15545
15546 /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15547 /// NO ACTION`.
15548 fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15549 match self.advance() {
15550 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15551 Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15552 Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15553 Token::Null => Ok(FkAction::SetNull),
15554 Token::Default => Ok(FkAction::SetDefault),
15555 other => Err(self.err(format!(
15556 "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15557 ))),
15558 },
15559 Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15560 Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15561 other => Err(self.err(format!(
15562 "expected ACTION after NO in FK action, got {other:?}"
15563 ))),
15564 },
15565 other => Err(self.err(format!(
15566 "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15567 ))),
15568 }
15569 }
15570
15571 /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15572 /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15573 fn consume_if_not_exists(&mut self) -> bool {
15574 // `IF` arrives as a bare Ident (we don't reserve it because it
15575 // also appears mid-expression in PG, though we don't support
15576 // those forms yet).
15577 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15578 if !looks_like_if {
15579 return false;
15580 }
15581 // Peek one ahead before committing: only consume IF when it's
15582 // actually `IF NOT EXISTS`.
15583 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
15584 return false;
15585 }
15586 if !matches!(
15587 self.tokens.get(self.pos + 2),
15588 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15589 ) {
15590 return false;
15591 }
15592 self.advance(); // IF
15593 self.advance(); // NOT
15594 self.advance(); // EXISTS
15595 true
15596 }
15597
15598 /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
15599 /// Consumes IF EXISTS as a pair; returns false otherwise
15600 /// without consuming any tokens.
15601 /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
15602 /// ENABLE/DISABLE/FORCE/NO FORCE.
15603 fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
15604 for kw in ["row", "level", "security"] {
15605 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
15606 {
15607 return Err(self.err(alloc::format!(
15608 "expected {} in ROW LEVEL SECURITY, got {:?}",
15609 kw.to_ascii_uppercase(),
15610 self.peek()
15611 )));
15612 }
15613 self.advance();
15614 }
15615 Ok(())
15616 }
15617
15618 fn consume_if_exists(&mut self) -> bool {
15619 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15620 if !looks_like_if {
15621 return false;
15622 }
15623 if !matches!(
15624 self.tokens.get(self.pos + 1),
15625 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15626 ) {
15627 return false;
15628 }
15629 self.advance(); // IF
15630 self.advance(); // EXISTS
15631 true
15632 }
15633
15634 /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
15635 /// qualifiers after an index column ref. ASC / DESC are
15636 /// reserved tokens; NULLS / FIRST / LAST are bare idents.
15637 /// We accept and discard them since single-column BTree
15638 /// stores rows in natural key order today.
15639 /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
15640 /// ORDER BY key. Returns None when absent.
15641 fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
15642 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15643 return Ok(None);
15644 }
15645 self.advance();
15646 match self.advance() {
15647 Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
15648 Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
15649 other => Err(self.err(alloc::format!(
15650 "expected FIRST or LAST after NULLS, got {other:?}"
15651 ))),
15652 }
15653 }
15654
15655 /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
15656 /// rather than discarded.
15657 ///
15658 /// SPG's index does not scan in a direction — column ordering is
15659 /// intrinsic to the storage — but `pg_indexes.indexdef` is a
15660 /// reproduction of the DDL, and dropping the clause meant
15661 /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
15662 /// dump lost it, and a schema diff saw drift on every run.
15663 fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
15664 let mut order = crate::ast::IndexColumnOrder::default();
15665 loop {
15666 match self.peek() {
15667 Token::Asc => {
15668 self.advance();
15669 }
15670 Token::Desc => {
15671 order.descending = true;
15672 self.advance();
15673 }
15674 Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
15675 let look = self.tokens.get(self.pos + 1);
15676 if matches!(
15677 look,
15678 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
15679 || k.eq_ignore_ascii_case("last")
15680 ) {
15681 self.advance();
15682 order.nulls_first = Some(matches!(
15683 self.advance(),
15684 Token::Ident(k) if k.eq_ignore_ascii_case("first")
15685 ));
15686 } else {
15687 break;
15688 }
15689 }
15690 _ => break,
15691 }
15692 }
15693 order
15694 }
15695
15696 fn parse_create_index_stmt_after_create(
15697 &mut self,
15698 is_unique: bool,
15699 ) -> Result<Statement, ParseError> {
15700 // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
15701 debug_assert!(matches!(self.peek(), Token::Index));
15702 self.advance();
15703 // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
15704 // SPG's CREATE INDEX is synchronous end-to-end today (real
15705 // CONCURRENTLY variant with restartable scans queues with
15706 // v7.39 indexes epic), so the modifier has no runtime effect
15707 // — same accept-and-no-op shape as v7.37.16.5 DETACH
15708 // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
15709 // VIEW CONCURRENTLY.
15710 let mut concurrently = false;
15711 if matches!(
15712 self.peek(),
15713 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
15714 ) {
15715 self.advance();
15716 concurrently = true;
15717 }
15718 let if_not_exists = self.consume_if_not_exists();
15719 // v7.39 (read01 round 93) — the index name is optional (PG since
15720 // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
15721 // When the token after `[IF NOT EXISTS]` is already `ON`, no name
15722 // was given; leave it empty and the engine derives a PG-style
15723 // `<table>_<cols>_idx` name at CREATE time (with collision counter).
15724 let name = if matches!(self.peek(), Token::On) {
15725 String::new()
15726 } else {
15727 self.expect_ident_like()?
15728 };
15729 if !matches!(self.peek(), Token::On) {
15730 return Err(self.err(format!(
15731 "expected ON after CREATE INDEX <name>, got {:?}",
15732 self.peek()
15733 )));
15734 }
15735 self.advance();
15736 let table = self.expect_ident_like()?;
15737 // Optional `USING <method>` — only recognised method in v2.0 is
15738 // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
15739 // ident `using` (we don't promote it to a reserved keyword
15740 // because it isn't reserved anywhere else in our SQL surface).
15741 let mut method_name: Option<String> = None;
15742 let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15743 self.advance();
15744 let m = self.expect_ident_like()?;
15745 method_name = Some(m.to_ascii_lowercase());
15746 match m.to_ascii_lowercase().as_str() {
15747 "hnsw" => IndexMethod::Hnsw,
15748 "btree" => IndexMethod::BTree,
15749 "brin" => IndexMethod::Brin,
15750 // v7.12.3 — real GIN inverted index over `tsvector`.
15751 // v7.9.26b's `USING gin` → BTree silent fallback is
15752 // gone; the engine validates that the indexed column
15753 // is `tsvector` at CREATE INDEX time.
15754 "gin" => IndexMethod::Gin,
15755 // v7.9.26b — PG `pg_dump` emits `USING gist` /
15756 // `USING spgist` / `USING hash` for their built-in
15757 // AMs that SPG doesn't have a matching
15758 // implementation for; degrade to BTree on the
15759 // leading column so the schema loads + the index
15760 // catalogue stays consistent. Operator pays the
15761 // planner cost only for the queries that would have
15762 // used the specialised AM.
15763 "gist" | "spgist" | "hash" => IndexMethod::BTree,
15764 // v7.11.3 — pgvector ships both `ivfflat` and
15765 // `hnsw`. Customers shouldn't have to choose
15766 // their on-disk index method based on what SPG
15767 // implements; accept `ivfflat` as a synonym for
15768 // `hnsw` so PG schemas using either method drop
15769 // in. The vector distance op (`<->` / `<#>` /
15770 // `<=>`) at query time still picks the metric.
15771 "ivfflat" => IndexMethod::Hnsw,
15772 other => {
15773 return Err(self.err(alloc::format!(
15774 "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
15775 )));
15776 }
15777 }
15778 } else {
15779 IndexMethod::BTree
15780 };
15781 if !matches!(self.peek(), Token::LParen) {
15782 return Err(self.err(format!(
15783 "expected '(' before indexed column, got {:?}",
15784 self.peek()
15785 )));
15786 }
15787 self.advance();
15788 // v6.8.2 — accept either a bare column ident (legacy) or
15789 // an expression `fn(col, …)` for expression indexes.
15790 // Distinguish by peeking the token *after* the current
15791 // ident: `ident )` is the legacy column-only path;
15792 // anything else triggers the Pratt expression parser.
15793 // (`advance()` uses `mem::replace` to nil out the current
15794 // slot, so we can't save+rewind cleanly — peek-ahead via
15795 // direct index avoids the mutation.)
15796 let mut opclass: Option<String> = None;
15797 let mut key_collation: Option<String> = None;
15798 let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
15799 // Single column with `)` immediately after — fast path.
15800 // v7.9.29 — also: bare column followed by `,` (the
15801 // multi-column form `(a, b, c)`). Without this branch
15802 // the leading ident gets pulled into `parse_expr`
15803 // which then sets `expression = Some(Column(a))` and
15804 // breaks Display round-trip on the multi-column shape.
15805 Token::Ident(s) | Token::QuotedIdent(s)
15806 if matches!(
15807 self.tokens.get(self.pos + 1),
15808 Some(Token::RParen | Token::Comma)
15809 ) =>
15810 {
15811 self.advance();
15812 (s, None)
15813 }
15814 // v7.9.22 — single column followed by a pgvector
15815 // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
15816 // v7.15.0 — capture the opclass instead of discarding
15817 // it so the engine can dispatch (e.g. `gin_trgm_ops`
15818 // → real trigram-shingle GIN over a TEXT column).
15819 // Vector/HNSW opclasses still take their distance
15820 // metric from the query operator (`<->` / `<#>` /
15821 // `<=>`), so for those callers the opclass stays
15822 // informational.
15823 // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
15824 // opclass: `(embedding public.vector_cosine_ops)`. Strip
15825 // the schema and dispatch on the bare opclass, the same
15826 // treatment table/type names get.
15827 Token::Ident(s) | Token::QuotedIdent(s)
15828 if matches!(
15829 self.tokens.get(self.pos + 1),
15830 Some(Token::Ident(_) | Token::QuotedIdent(_))
15831 ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
15832 && matches!(
15833 self.tokens.get(self.pos + 3),
15834 Some(Token::Ident(op) | Token::QuotedIdent(op))
15835 if is_vector_opclass_name(op)
15836 ) =>
15837 {
15838 self.advance(); // column name
15839 self.advance(); // schema qualifier
15840 self.advance(); // dot
15841 let op_tok = self.advance();
15842 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15843 opclass = Some(op.to_ascii_lowercase());
15844 }
15845 (s, None)
15846 }
15847 // r1038 — an operator class is recognised by its POSITION, not
15848 // by a list of names. It used to be `is_vector_opclass_name`,
15849 // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
15850 // sentori's migration wrote — was a syntax error while
15851 // `USING gin (doc)` parsed. Anything sitting between a column
15852 // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
15853 // two bare identifiers in a row are not valid there otherwise.
15854 Token::Ident(s) | Token::QuotedIdent(s)
15855 if matches!(
15856 self.tokens.get(self.pos + 1),
15857 Some(Token::Ident(op) | Token::QuotedIdent(op))
15858 if is_vector_opclass_name(op) || Self::opclass_position_follows(
15859 self.tokens.get(self.pos + 2)
15860 )
15861 ) =>
15862 {
15863 self.advance(); // column name
15864 // Capture the opclass token, lower-cased for
15865 // case-insensitive engine dispatch.
15866 let op_tok = self.advance();
15867 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15868 opclass = Some(op.to_ascii_lowercase());
15869 }
15870 (s, None)
15871 }
15872 Token::Ident(_) | Token::QuotedIdent(_) => {
15873 // v7.39 (round 538) — an explicit COLLATE on the key,
15874 // read by LOOKAHEAD because `parse_expr` absorbs the
15875 // clause as a no-op (SPG orders text by bytes, which is
15876 // the C collation, so it changes nothing to honour). PG
15877 // still PRINTS it: an explicitly written `"C"` and the
15878 // collation a column inherits are different collation
15879 // OBJECTS even where they sort identically, which is why
15880 // `(a COLLATE "C")` shows on a C-collation database too.
15881 if matches!(
15882 self.tokens.get(self.pos + 1),
15883 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
15884 ) {
15885 key_collation = match self.tokens.get(self.pos + 2) {
15886 Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
15887 Some(n.clone())
15888 }
15889 _ => None,
15890 };
15891 }
15892 let key_expr = self.parse_expr(0)?;
15893 let primary = extract_first_column(&key_expr).ok_or_else(|| {
15894 self.err("expression index key must reference at least one column".into())
15895 })?;
15896 (primary, Some(key_expr))
15897 }
15898 // v7.37.43-T4 — parenthesised expression index key
15899 // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
15900 // PG's CREATE INDEX requires the expression to be in
15901 // its own parens to disambiguate function calls from
15902 // column lists, so this `LParen` is the inner open-paren
15903 // of an expression key. parse_expr handles the recursive
15904 // descent and consumes the matching `RParen`.
15905 Token::LParen => {
15906 let key_expr = self.parse_expr(0)?;
15907 let primary = extract_first_column(&key_expr).ok_or_else(|| {
15908 self.err("expression index key must reference at least one column".into())
15909 })?;
15910 (primary, Some(key_expr))
15911 }
15912 other => {
15913 return Err(self.err(format!(
15914 "expected column ident or expression, got {other:?}"
15915 )));
15916 }
15917 };
15918 // v7.9.14 — accept extra comma-separated columns inside
15919 // the index key parens (`CREATE INDEX … (a, b, c)`).
15920 // mailrs F2. Each extra column may carry an optional
15921 // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
15922 // — parsed and discarded; SPG doesn't honour direction
15923 // on a BTree index today (column ordering is intrinsic
15924 // to the storage). v7.10 will widen to genuine composite
15925 // index keys.
15926 let mut extra_columns: Vec<String> = Vec::new();
15927 // The leading column may also have ASC/DESC after it — and that
15928 // one is the column SPG indexes, so its clause is kept.
15929 let key_order = self.consume_optional_index_column_qualifiers();
15930 while matches!(self.peek(), Token::Comma) {
15931 self.advance();
15932 let extra = self.expect_ident_like()?;
15933 let _ = self.consume_optional_index_column_qualifiers();
15934 extra_columns.push(extra);
15935 }
15936 if !matches!(self.peek(), Token::RParen) {
15937 return Err(self.err(format!(
15938 "expected ')' after indexed column / expression, got {:?}",
15939 self.peek()
15940 )));
15941 }
15942 self.advance();
15943 // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
15944 // index-only-scan annotation. Bare ident (not a reserved
15945 // keyword) so we test by case-insensitive string match.
15946 let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
15947 {
15948 self.advance();
15949 if !matches!(self.peek(), Token::LParen) {
15950 return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
15951 }
15952 self.advance();
15953 let mut cols = Vec::new();
15954 loop {
15955 cols.push(self.expect_ident_like()?);
15956 match self.peek() {
15957 Token::Comma => {
15958 self.advance();
15959 }
15960 Token::RParen => {
15961 self.advance();
15962 break;
15963 }
15964 other => {
15965 return Err(self.err(format!(
15966 "expected ',' or ')' in INCLUDE list, got {other:?}"
15967 )));
15968 }
15969 }
15970 }
15971 cols
15972 } else {
15973 Vec::new()
15974 };
15975 // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
15976 // storage parameters. pgvector emits `WITH (lists = N)` for
15977 // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
15978 // SPG's HNSW picks its own parameters today (tunable via
15979 // env vars), so the WITH clause is informational and dropped.
15980 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15981 self.advance();
15982 if !matches!(self.peek(), Token::LParen) {
15983 return Err(self.err(format!(
15984 "expected '(' after WITH in CREATE INDEX, got {:?}",
15985 self.peek()
15986 )));
15987 }
15988 self.advance();
15989 loop {
15990 if matches!(self.peek(), Token::RParen) {
15991 self.advance();
15992 break;
15993 }
15994 // Drain `key = value` or bare `key` tokens.
15995 let _ = self.advance(); // key
15996 if matches!(self.peek(), Token::Eq) {
15997 self.advance();
15998 let _ = self.advance(); // value (int / string / ident)
15999 }
16000 match self.peek() {
16001 Token::Comma => {
16002 self.advance();
16003 }
16004 Token::RParen => {
16005 self.advance();
16006 break;
16007 }
16008 other => {
16009 return Err(self.err(format!(
16010 "expected ',' or ')' in WITH (…) clause, got {other:?}"
16011 )));
16012 }
16013 }
16014 }
16015 }
16016 // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
16017 // which sits between the key list and the WHERE clause.
16018 let mut nulls_not_distinct = false;
16019 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16020 let n1 = self.tokens.get(self.pos + 1);
16021 let n2 = self.tokens.get(self.pos + 2);
16022 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
16023 self.advance(); // NULLS
16024 self.advance(); // NOT
16025 self.advance(); // DISTINCT
16026 nulls_not_distinct = true;
16027 } else if matches!(n1, Some(Token::Distinct)) {
16028 self.advance(); // NULLS
16029 self.advance(); // DISTINCT
16030 }
16031 }
16032 // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
16033 let partial_predicate = if matches!(self.peek(), Token::Where) {
16034 self.advance();
16035 Some(self.parse_expr(0)?)
16036 } else {
16037 None
16038 };
16039 // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16040 // sense: uniqueness over an ANN structure has no clean
16041 // semantics. Reject early. (BRIN UNIQUE is similarly
16042 // meaningless — block both.)
16043 if is_unique && !matches!(method, IndexMethod::BTree) {
16044 return Err(self.err(alloc::format!(
16045 "UNIQUE is only supported on BTree indexes, got USING {:?}",
16046 method
16047 )));
16048 }
16049 Ok(Statement::CreateIndex(CreateIndexStatement {
16050 concurrently,
16051 name,
16052 key_order,
16053 key_collation,
16054 table,
16055 column,
16056 nulls_not_distinct,
16057 method,
16058 if_not_exists,
16059 included_columns,
16060 partial_predicate,
16061 extra_columns: extra_columns.clone(),
16062 expression,
16063 is_unique,
16064 opclass,
16065 method_name,
16066 }))
16067 }
16068
16069 /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16070 /// column-level `REFERENCES ...` clause. The trailing FK is
16071 /// normalised into table-level shape (single-element columns +
16072 /// parent_columns) so the engine sees one uniform constraint list.
16073 fn parse_column_def_with_fk(
16074 &mut self,
16075 ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16076 let col = self.parse_column_def()?;
16077 // v7.39 (round 308, V29) — an explicitly named inline FK:
16078 // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16079 // loop leaves this spelling intact precisely so the name can be
16080 // kept here; PG reports it in violation messages and matches it
16081 // in `SET CONSTRAINTS`.
16082 let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16083 {
16084 self.advance();
16085 Some(self.expect_ident_like()?)
16086 } else {
16087 None
16088 };
16089 // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16090 let inline_references = matches!(
16091 self.peek(),
16092 Token::Ident(s) if s.eq_ignore_ascii_case("references")
16093 );
16094 if !inline_references {
16095 return Ok((col, None));
16096 }
16097 let (
16098 parent_table,
16099 parent_columns,
16100 on_delete,
16101 on_update,
16102 match_type,
16103 deferrable,
16104 initially_deferred,
16105 ) = self.parse_references_tail(1)?;
16106 let fk = ForeignKeyConstraint {
16107 name: declared_name,
16108 columns: vec![col.name.clone()],
16109 parent_table,
16110 parent_columns,
16111 on_delete,
16112 on_update,
16113 match_type,
16114 deferrable,
16115 initially_deferred,
16116 };
16117 Ok((col, Some(fk)))
16118 }
16119
16120 /// v7.13.0 — parse a column type (consuming the type ident and
16121 /// any trailing parameters / `[]`), without surrounding column
16122 /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16123 /// Returns the resolved `ColumnTypeName` plus implied
16124 /// `(auto_increment, not_null)` flags from PG SERIAL family
16125 /// shorthands — callers that don't expect those (ALTER COLUMN
16126 /// TYPE) can discard them.
16127 fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16128 let (ty, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16129 Ok(ty)
16130 }
16131
16132 #[allow(clippy::type_complexity)]
16133 fn parse_type_with_implied_flags(
16134 &mut self,
16135 ) -> Result<
16136 (
16137 ColumnTypeName,
16138 bool,
16139 bool,
16140 Option<String>,
16141 Collation,
16142 // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16143 bool,
16144 // v7.39 (round 676) — the collation NAME as written, which the
16145 // `Collation` enum above cannot carry.
16146 Option<String>,
16147 bool,
16148 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16149 // list captured at type-parse time. None for all
16150 // non-ENUM types.
16151 Option<Vec<String>>,
16152 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16153 // list. Distinct from ENUM (subset semantics).
16154 Option<Vec<String>>,
16155 // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16156 // width, lost when the type collapses to SmallInt / Int.
16157 Option<MysqlIntWidth>,
16158 // v7.39 (round 424) — declared fractional-seconds precision of a
16159 // MySQL temporal column (bare spelling = 0). None under PG.
16160 Option<u8>,
16161 ),
16162 ParseError,
16163 > {
16164 let mut ty_ident = match self.advance() {
16165 Token::Ident(s) => s,
16166 // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16167 // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16168 // '<span>'` literal grammar. As a column type it lands
16169 // here directly; downstream resolution still uses the
16170 // canonical lowercase string.
16171 Token::Interval => "interval".to_string(),
16172 other => {
16173 return Err(ParseError {
16174 message: format!("expected column type, got {other:?}"),
16175 token_pos: self.consumed_pos(),
16176 });
16177 }
16178 };
16179 // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16180 // pg_dump qualifies extension types (`public.vector(1024)`).
16181 // SPG is single-namespace; drop the schema and resolve the
16182 // bare type — same treatment table names already get.
16183 while matches!(self.peek(), Token::Dot) {
16184 self.advance();
16185 ty_ident = self.expect_ident_like()?;
16186 }
16187 let mut implied_auto_increment = false;
16188 let mut implied_not_null = false;
16189 let mut user_type_ref: Option<String> = None;
16190 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16191 // value list, captured here and bubbled up through the
16192 // ColumnDef so the engine can attach it to the column
16193 // schema (and validate INSERT cells against it).
16194 let mut inline_enum_variants: Option<Vec<String>> = None;
16195 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16196 let mut inline_set_variants: Option<Vec<String>> = None;
16197 // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16198 // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16199 // collapses to SmallInt / Int. Only under the MySQL dialect.
16200 let mut mysql_int_width: Option<MysqlIntWidth> = None;
16201 // v7.39 (round 424) — the declared fractional-seconds precision of a
16202 // MySQL temporal column. Set by the temporal arms below; stays None
16203 // for PG (whose temporal columns keep full microseconds).
16204 let mut mysql_fsp: Option<u8> = None;
16205 let mut ty = match ty_ident.as_str() {
16206 // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16207 "smallserial" | "serial2" => {
16208 implied_auto_increment = true;
16209 implied_not_null = true;
16210 ColumnTypeName::SmallInt
16211 }
16212 "serial" | "serial4" => {
16213 implied_auto_increment = true;
16214 implied_not_null = true;
16215 ColumnTypeName::Int
16216 }
16217 "bigserial" | "serial8" => {
16218 implied_auto_increment = true;
16219 implied_not_null = true;
16220 ColumnTypeName::BigInt
16221 }
16222 // MySQL flavours we accept by aliasing to the closest SPG
16223 // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16224 // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16225 // 24-bit) → INT. UNSIGNED modifiers are consumed below
16226 // without semantic effect.
16227 // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16228 // PG's internal type names; pg_dump and hand-written PG schemas
16229 // use them interchangeably with smallint / int / bigint (the cast
16230 // path already accepted them, only the column grammar didn't).
16231 "smallint" | "int2" => {
16232 // v7.14.0 — MySQL display-width on integers
16233 // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16234 // parenthesised number is purely cosmetic — it
16235 // doesn't change storage. Accept + discard.
16236 self.consume_optional_paren_size();
16237 ColumnTypeName::SmallInt
16238 }
16239 // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16240 // canonical encoding for BOOLEAN. Every MySQL driver
16241 // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16242 // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16243 // 4.3 SPG classified TINYINT(1) as SmallInt, which
16244 // gave the customer i16-shaped values where the app
16245 // expected bool — a Tier-A silent type drift on
16246 // mysqldump restores. Now: `TINYINT(1)` → Bool;
16247 // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16248 // stay SmallInt (the legacy width-agnostic path).
16249 "tinyint" => {
16250 let width = self.peek_optional_paren_size_value();
16251 self.consume_optional_paren_size();
16252 if width == Some(1) {
16253 ColumnTypeName::Bool
16254 } else {
16255 // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16256 // lost width so the write path can enforce -128..127.
16257 if self.mysql_dialect {
16258 mysql_int_width = Some(MysqlIntWidth::Tiny);
16259 }
16260 ColumnTypeName::SmallInt
16261 }
16262 }
16263 "mediumint" => {
16264 self.consume_optional_paren_size();
16265 // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16266 if self.mysql_dialect {
16267 mysql_int_width = Some(MysqlIntWidth::Medium);
16268 }
16269 ColumnTypeName::Int
16270 }
16271 "int" | "integer" | "int4" => {
16272 self.consume_optional_paren_size();
16273 ColumnTypeName::Int
16274 }
16275 "bigint" | "int8" => {
16276 self.consume_optional_paren_size();
16277 ColumnTypeName::BigInt
16278 }
16279 // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16280 // (mailrs round-5 G6). Consume the optional `PRECISION`
16281 // tail when the type keyword was `double` / `DOUBLE`.
16282 //
16283 // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16284 // FLOAT". `FLOAT(p)` picks the width the way PG does:
16285 // p in 1..=24 is real, 25..=53 is double precision, and
16286 // anything else is an error.
16287 "float" | "double" | "real" => {
16288 if ty_ident.eq_ignore_ascii_case("double")
16289 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16290 {
16291 self.advance();
16292 }
16293 if ty_ident.eq_ignore_ascii_case("real") {
16294 // v7.39 (round 274) — the two dialects genuinely
16295 // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16296 // synonym for DOUBLE (8-byte). Round 269 made REAL
16297 // 32-bit globally and thereby narrowed the stored
16298 // precision of every MySQL REAL column.
16299 if self.mysql_dialect {
16300 ColumnTypeName::Float
16301 } else {
16302 ColumnTypeName::Real
16303 }
16304 } else if ty_ident.eq_ignore_ascii_case("float")
16305 && self.mysql_dialect
16306 && matches!(self.peek(), Token::LParen)
16307 && self.peek_paren_has_comma()
16308 {
16309 // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16310 // display form (`FLOAT(10,2)`), which PG has no
16311 // equivalent of. It was `syntax error at or near ","`,
16312 // so the whole CREATE failed. The digits are a display
16313 // hint only; SPG stores the full double.
16314 self.consume_optional_paren_size();
16315 ColumnTypeName::Float
16316 } else if ty_ident.eq_ignore_ascii_case("float")
16317 && matches!(self.peek(), Token::LParen)
16318 {
16319 // PG words the two bounds differently, and
16320 // parse_paren_size already rejects a zero.
16321 let p = self.parse_paren_size("FLOAT")?;
16322 if p > 53 {
16323 return Err(self.err(String::from(
16324 "precision for type float must be less than 54 bits",
16325 )));
16326 }
16327 if p <= 24 {
16328 ColumnTypeName::Real
16329 } else {
16330 ColumnTypeName::Float
16331 }
16332 } else {
16333 ColumnTypeName::Float
16334 }
16335 }
16336 // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16337 "float4" => ColumnTypeName::Real,
16338 "float8" => ColumnTypeName::Float,
16339 "text" => ColumnTypeName::Text,
16340 // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16341 // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16342 // real MySQL schema and NONE of them existed: the CREATE
16343 // failed outright with `type "blob" does not exist`, so the
16344 // table was never made. The sizes differ only in MySQL's
16345 // maximum length, which SPG does not cap, so they collapse
16346 // onto TEXT and BYTEA the way the unsized spellings do.
16347 "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16348 "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16349 // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16350 // enforce, consumed so the declaration parses.
16351 "varbinary" | "binary" => {
16352 self.consume_optional_paren_size();
16353 ColumnTypeName::Bytes
16354 }
16355 "name" => ColumnTypeName::Name,
16356 "xid" => ColumnTypeName::Xid,
16357 "oid" => ColumnTypeName::Oid,
16358 "xid8" => ColumnTypeName::Xid8,
16359 "bool" | "boolean" => ColumnTypeName::Bool,
16360 // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16361 // an unbounded `character varying`, which the arm below has always
16362 // read as text. Only the short spelling demanded a length, so
16363 // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16364 // there is — failed on `VARCHAR type requires (N)` while the long
16365 // spelling of the same thing was accepted. The same asymmetry
16366 // round 613 closed on the CAST side, here on the DDL side.
16367 "varchar" => {
16368 if matches!(self.peek(), Token::LParen) {
16369 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16370 } else {
16371 ColumnTypeName::Text
16372 }
16373 }
16374 // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16375 // `character` below (SQL standard).
16376 "char" => {
16377 if matches!(self.peek(), Token::LParen) {
16378 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16379 } else {
16380 ColumnTypeName::Char(1)
16381 }
16382 }
16383 // pg_dump's canonical spellings: `character varying(n)` = varchar,
16384 // `character(n)` = char, bare `character` = char(1). Unbounded
16385 // `character varying` maps to text.
16386 "character" => {
16387 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16388 self.advance();
16389 if matches!(self.peek(), Token::LParen) {
16390 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16391 } else {
16392 ColumnTypeName::Text
16393 }
16394 } else if matches!(self.peek(), Token::LParen) {
16395 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16396 } else {
16397 ColumnTypeName::Char(1)
16398 }
16399 }
16400 "vector" => {
16401 let dim = self.parse_paren_size("VECTOR")?;
16402 let encoding = self.parse_optional_vector_encoding()?;
16403 ColumnTypeName::Vector { dim, encoding }
16404 }
16405 // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16406 // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16407 // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16408 // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16409 // DECIMAL(10,2))` — how nearly every money column is written,
16410 // in either dialect — was a syntax error and the table was
16411 // never created. `FIXED` is MySQL's alias alone, so it is
16412 // taken only in that dialect.
16413 "numeric" | "decimal" | "dec" => {
16414 let (precision, scale) = self.parse_optional_numeric_params()?;
16415 ColumnTypeName::Numeric(precision, scale)
16416 }
16417 "fixed" if self.mysql_dialect => {
16418 let (precision, scale) = self.parse_optional_numeric_params()?;
16419 ColumnTypeName::Numeric(precision, scale)
16420 }
16421 "date" => ColumnTypeName::Date,
16422 // MySQL's `DATETIME` is the same domain as standard
16423 // `TIMESTAMP` — accept both spellings.
16424 "timestamp" | "datetime" => {
16425 // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16426 // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16427 // TIME ZONE` clause, so consume it first.
16428 // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16429 // (it truncates on write and pads on render), so capture it;
16430 // a bare spelling means precision 0 there. PG stores µs always
16431 // and keeps `None`.
16432 let n = self.take_optional_paren_size();
16433 if self.mysql_dialect {
16434 mysql_fsp = Some(n.unwrap_or(0).min(6));
16435 }
16436 // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16437 // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16438 // the full form. SPG canonicalises:
16439 // - WITH TIME ZONE → Timestamptz
16440 // - WITHOUT TIME ZONE → Timestamp
16441 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16442 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16443 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16444 {
16445 self.advance(); // WITH
16446 self.advance(); // TIME
16447 self.advance(); // ZONE
16448 ColumnTypeName::Timestamptz
16449 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16450 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16451 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16452 {
16453 self.advance(); // WITHOUT
16454 self.advance(); // TIME
16455 self.advance(); // ZONE
16456 ColumnTypeName::Timestamp
16457 } else {
16458 // A second `(precision)` cannot legally follow, but the
16459 // old grammar tolerated it; keep that tolerance.
16460 self.consume_optional_paren_size();
16461 ColumnTypeName::Timestamp
16462 }
16463 }
16464 // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16465 // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16466 // only PG-wire OID differs.
16467 "timestamptz" => {
16468 self.consume_optional_paren_size();
16469 ColumnTypeName::Timestamptz
16470 }
16471 // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16472 // validation. We accept the JSONB spelling too because
16473 // most PG clients default to it; SPG doesn't distinguish
16474 // the two (no path-operator perf advantage to model).
16475 "json" => ColumnTypeName::Json,
16476 "jsonb" => ColumnTypeName::Jsonb,
16477 // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16478 // surface here. Same storage shape; mapping happens at
16479 // the engine side via the ColumnTypeName → DataType
16480 // resolver. Literal forms are handled at coerce_value
16481 // time so the lexer stays untouched.
16482 "bytea" | "bytes" => ColumnTypeName::Bytes,
16483 // v7.17.0 Phase 7 — PG network address types
16484 // v7.17.0 had a Text-backed fallback here for
16485 // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16486 // each to a first-class type; the keywords are
16487 // bound below in the ζ-A block.
16488 // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16489 // The actual `to_tsvector` / `@@` / `ts_rank` surface
16490 // arrives in v7.12.1+; the type itself loads here so
16491 // mailrs's `scripts/init-schema.sql` runs unmodified.
16492 "tsvector" => ColumnTypeName::TsVector,
16493 "tsquery" => ColumnTypeName::TsQuery,
16494 // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
16495 // surface for Django / Rails / Hibernate's default
16496 // PK pattern.
16497 "uuid" => ColumnTypeName::Uuid,
16498 // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
16499 // Storage = three-field {months, days, micros}, catalog
16500 // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
16501 // line `INTERVAL` was parser-rejected at CREATE TABLE.
16502 "interval" => {
16503 // pg_dump emits field-qualified forms like `INTERVAL DAY TO
16504 // SECOND` and an optional `(p)` precision. SPG stores the full
16505 // {months,days,micros}; consume + ignore the qualifier/precision.
16506 while matches!(self.peek(), Token::To)
16507 || matches!(self.peek(), Token::Ident(s) if matches!(
16508 s.to_ascii_lowercase().as_str(),
16509 "year" | "month" | "day" | "hour" | "minute" | "second"
16510 ))
16511 {
16512 self.advance();
16513 }
16514 self.consume_optional_paren_size();
16515 ColumnTypeName::Interval
16516 }
16517 // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
16518 // i64 microseconds since 00:00:00. Wire OID 1083.
16519 // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
16520 "time" => {
16521 // v7.39 (round 424) — MySQL TIME carries a semantic
16522 // fractional-seconds precision, bare meaning 0.
16523 let n = self.take_optional_paren_size();
16524 if self.mysql_dialect {
16525 mysql_fsp = Some(n.unwrap_or(0).min(6));
16526 }
16527 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16528 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16529 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16530 {
16531 self.advance();
16532 self.advance();
16533 self.advance();
16534 ColumnTypeName::TimeTz
16535 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16536 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16537 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16538 {
16539 self.advance();
16540 self.advance();
16541 self.advance();
16542 ColumnTypeName::Time
16543 } else {
16544 ColumnTypeName::Time
16545 }
16546 }
16547 // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
16548 // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
16549 "year" => ColumnTypeName::Year,
16550 // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
16551 // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
16552 "timetz" => ColumnTypeName::TimeTz,
16553 // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
16554 // Wire OID 790.
16555 "money" => ColumnTypeName::Money,
16556 // v7.17.0 Phase 3.P0-38 — PG range types.
16557 "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
16558 "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
16559 "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
16560 "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
16561 "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
16562 "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
16563 // v7.37.5 δ — PG 14+ multirange keywords.
16564 "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
16565 "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
16566 "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
16567 "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
16568 "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
16569 "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
16570 // v7.37.5 ε — PG geometry scalar keywords.
16571 "point" => ColumnTypeName::Point,
16572 "lseg" => ColumnTypeName::Lseg,
16573 "path" => ColumnTypeName::Path,
16574 "box" => ColumnTypeName::PgBox,
16575 "polygon" => ColumnTypeName::Polygon,
16576 "line" => ColumnTypeName::Line,
16577 "circle" => ColumnTypeName::Circle,
16578 // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
16579 "inet" => ColumnTypeName::Inet,
16580 "cidr" => ColumnTypeName::Cidr,
16581 "macaddr" => ColumnTypeName::Macaddr,
16582 "macaddr8" => ColumnTypeName::Macaddr8,
16583 // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
16584 // width in the value, so the optional `(N)` typmod is accepted and
16585 // ignored (the column stores whatever width it's given).
16586 "bit" => {
16587 let varying = matches!(
16588 self.peek(),
16589 Token::Ident(k) if k.eq_ignore_ascii_case("varying")
16590 );
16591 if varying {
16592 self.advance();
16593 }
16594 // v7.39 (round 281) — the length used to be parsed and
16595 // dropped, so `bit(3)` accepted a five-bit string.
16596 let n = if matches!(self.peek(), Token::LParen) {
16597 self.parse_paren_size("BIT")?
16598 } else {
16599 0
16600 };
16601 if varying {
16602 ColumnTypeName::BitVarying(n)
16603 } else {
16604 ColumnTypeName::Bit(n)
16605 }
16606 }
16607 "varbit" => {
16608 let n = if matches!(self.peek(), Token::LParen) {
16609 self.parse_paren_size("VARBIT")?
16610 } else {
16611 0
16612 };
16613 ColumnTypeName::BitVarying(n)
16614 }
16615 "xml" => ColumnTypeName::Xml,
16616 // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
16617 "hstore" => ColumnTypeName::Hstore,
16618 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
16619 // `ENUM('a','b','c')`. Storage is TEXT; the value
16620 // list lands on `inline_enum_variants` for the
16621 // engine to validate INSERT cells against. Empty
16622 // value list is a parse error (matches MySQL).
16623 "enum" => {
16624 // Expect the opening `(`.
16625 if !matches!(self.peek(), Token::LParen) {
16626 return Err(self.err(alloc::format!(
16627 "expected '(' after ENUM, got {:?}",
16628 self.peek()
16629 )));
16630 }
16631 self.advance();
16632 let mut variants: Vec<String> = Vec::new();
16633 loop {
16634 match self.advance() {
16635 Token::String(s) => variants.push(s),
16636 other => {
16637 return Err(self.err(alloc::format!(
16638 "ENUM(...) expects string literal variants, got {other:?}"
16639 )));
16640 }
16641 }
16642 match self.peek() {
16643 Token::Comma => {
16644 self.advance();
16645 continue;
16646 }
16647 Token::RParen => {
16648 self.advance();
16649 break;
16650 }
16651 other => {
16652 return Err(self.err(alloc::format!(
16653 "expected ',' or ')' in ENUM(...), got {other:?}"
16654 )));
16655 }
16656 }
16657 }
16658 if variants.is_empty() {
16659 return Err(self.err("ENUM(...) must declare at least one variant".into()));
16660 }
16661 inline_enum_variants = Some(variants);
16662 // Storage is plain TEXT; the variant list lives on
16663 // the ColumnSchema side.
16664 ColumnTypeName::Text
16665 }
16666 // v7.17.0 Phase 3.P0-37 — MySQL inline SET
16667 // `SET('a','b','c')`. Same parse shape as ENUM;
16668 // semantics differ (subset rather than pick-one).
16669 "set" => {
16670 if !matches!(self.peek(), Token::LParen) {
16671 return Err(self.err(alloc::format!(
16672 "expected '(' after SET, got {:?}",
16673 self.peek()
16674 )));
16675 }
16676 self.advance();
16677 let mut variants: Vec<String> = Vec::new();
16678 loop {
16679 match self.advance() {
16680 Token::String(s) => variants.push(s),
16681 other => {
16682 return Err(self.err(alloc::format!(
16683 "SET(...) expects string literal variants, got {other:?}"
16684 )));
16685 }
16686 }
16687 match self.peek() {
16688 Token::Comma => {
16689 self.advance();
16690 continue;
16691 }
16692 Token::RParen => {
16693 self.advance();
16694 break;
16695 }
16696 other => {
16697 return Err(self.err(alloc::format!(
16698 "expected ',' or ')' in SET(...), got {other:?}"
16699 )));
16700 }
16701 }
16702 }
16703 if variants.is_empty() {
16704 return Err(self.err("SET(...) must declare at least one variant".into()));
16705 }
16706 inline_set_variants = Some(variants);
16707 ColumnTypeName::Text
16708 }
16709 _other => {
16710 // v7.17.0 Phase 1.4 — unknown ident → defer
16711 // resolution to the engine. Stored as Text in
16712 // ColumnTypeName + the original name carried as
16713 // `user_type_ref` so CREATE TABLE can look up
16714 // user-defined enum / domain types.
16715 user_type_ref = Some(ty_ident.clone());
16716 ColumnTypeName::Text
16717 }
16718 };
16719 // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
16720 // right after the type keyword. Pre-4.4 SPG consumed +
16721 // discarded the keyword, leaving a customer column
16722 // declared `id INT UNSIGNED NOT NULL` silently accepting
16723 // negative values — a Tier-A correctness drift where
16724 // application invariants (auto-increment-IDs never
16725 // negative) silently broke on cutover. Now: capture as
16726 // a column flag, persist on the schema, enforce at
16727 // INSERT / UPDATE time.
16728 let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
16729 {
16730 self.advance();
16731 true
16732 } else {
16733 false
16734 };
16735 // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
16736 // `<type> COLLATE <name>` post-fixes on text columns. SPG
16737 // stores text as UTF-8 always so CHARACTER SET is still a
16738 // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
16739 // name: it gets classified into a `Collation` variant the
16740 // engine consults at WHERE-eval time. PG `default` /
16741 // `pg_catalog.default` / `C` / `POSIX` collations all
16742 // resolve to `Binary` (the prior behaviour); `_ci` /
16743 // `case_insensitive` / `nocase` shift to CaseInsensitive.
16744 // The schema-qualifier form (`pg_catalog.default`) lexes
16745 // as `Ident '.' Ident` — peek for the `.` and consume both
16746 // halves so it's treated as one collation name. PG's
16747 // `IDENT.IDENT` collation form (which can appear here) is
16748 // resolved by Collation::from_collation_name on the bare
16749 // identifier after the dot.
16750 let mut collation = Collation::Binary;
16751 // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
16752 // clause was written. The engine needs this to tell an explicit
16753 // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
16754 // clause at all: both resolve to `Collation::Binary`, but under the
16755 // MySQL dialect the latter takes the folding default collation.
16756 let mut collation_explicit = false;
16757 let mut collation_name: Option<alloc::string::String> = None;
16758 loop {
16759 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
16760 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
16761 {
16762 self.advance(); // CHARACTER
16763 self.advance(); // SET
16764 if matches!(
16765 self.peek(),
16766 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
16767 ) {
16768 self.advance();
16769 }
16770 continue;
16771 }
16772 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
16773 self.advance(); // COLLATE
16774 // Accept Ident / QuotedIdent / String AND the
16775 // keyword-tokenised `Default` (PG `pg_catalog.default`
16776 // and bare `DEFAULT` collation names — `default` is a
16777 // reserved word so the lexer hands back Token::Default
16778 // not Token::Ident).
16779 let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
16780 match this.peek().clone() {
16781 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
16782 this.advance();
16783 Some(s)
16784 }
16785 Token::Default => {
16786 this.advance();
16787 Some(alloc::string::String::from("default"))
16788 }
16789 _ => None,
16790 }
16791 };
16792 let raw = if let Some(head) = read_collation_atom(self) {
16793 // Schema-qualified PG form: `pg_catalog.default`.
16794 if matches!(self.peek(), Token::Dot) {
16795 self.advance();
16796 let tail = read_collation_atom(self).unwrap_or_default();
16797 alloc::format!("{head}.{tail}")
16798 } else {
16799 head
16800 }
16801 } else {
16802 alloc::string::String::new()
16803 };
16804 if !raw.is_empty() {
16805 collation_explicit = true;
16806 // v7.39 (round 676) — keep the name too. The enum below
16807 // folds C / POSIX / en_US / default into one value, and
16808 // `pg_attribute.attcollation` has to tell them apart.
16809 // The schema qualifier goes: PG's `pg_catalog.default`
16810 // and a bare `default` name the same collation.
16811 // v7.39 (round 679) — strip a SCHEMA qualifier, not an
16812 // encoding suffix. Round 676 used `rsplit('.')` for
16813 // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
16814 // PG writes `pg_catalog.default` (qualifier) and
16815 // `en_US.utf8` (locale + encoding) with the same
16816 // separator. Only `pg_catalog.` is a qualifier, and it
16817 // is the only one PG's own dumps emit.
16818 let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
16819 let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
16820 collation_name = Some(alloc::string::String::from(bare));
16821 let parsed = Collation::from_collation_name(&raw);
16822 // Last COLLATE clause wins, but `Binary` from a
16823 // bare keyword like `default` should not
16824 // silently downgrade a stronger one set earlier
16825 // on the same column. v7.17 only ships one
16826 // non-Binary variant so a simple OR is enough.
16827 if parsed != Collation::Binary {
16828 collation = parsed;
16829 }
16830 }
16831 continue;
16832 }
16833 break;
16834 }
16835 // v7.10.10 — postfix `[]` widens the base type to its array
16836 // type. PG accepts `TYPE[]` after any base type and so does
16837 // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
16838 // all through; the old "only TEXT[]" note was stale).
16839 if matches!(self.peek(), Token::LBracket) {
16840 self.advance();
16841 if !matches!(self.peek(), Token::RBracket) {
16842 return Err(self.err(alloc::format!(
16843 "TEXT[] takes no dimension; got {:?}",
16844 self.peek()
16845 )));
16846 }
16847 self.advance();
16848 // v7.11.13 — widened to INT[] and BIGINT[] in addition
16849 // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
16850 // still error here.
16851 ty = match ty {
16852 ColumnTypeName::Text => ColumnTypeName::TextArray,
16853 ColumnTypeName::Int => ColumnTypeName::IntArray,
16854 ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
16855 // v7.37.5 β-P4 — INTERVAL[] via the same postfix
16856 // `[]` grammar. Wire OID 1187.
16857 ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
16858 // v7.37.5 γ — full PG array-of-scalar family.
16859 ColumnTypeName::Bool => ColumnTypeName::BoolArray,
16860 ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
16861 ColumnTypeName::Float => ColumnTypeName::FloatArray,
16862 // NUMERIC(p, s) loses its precision params at the
16863 // array level (matches PG: `NUMERIC[]` is untyped,
16864 // per-element precision flows through values).
16865 ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
16866 ColumnTypeName::Date => ColumnTypeName::DateArray,
16867 ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
16868 ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
16869 ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
16870 ColumnTypeName::Json => ColumnTypeName::JsonArray,
16871 ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
16872 ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
16873 // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
16874 // the array level (matches PG semantics where the
16875 // element precision is per-row, not column-wide).
16876 ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
16877 ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
16878 // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
16879 // follow-up.
16880 ColumnTypeName::Money => ColumnTypeName::MoneyArray,
16881 other => {
16882 return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
16883 }
16884 };
16885 // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
16886 // for INT/TEXT/BIGINT. Anything else is an error.
16887 if matches!(self.peek(), Token::LBracket) {
16888 self.advance();
16889 if !matches!(self.peek(), Token::RBracket) {
16890 return Err(self.err(alloc::format!(
16891 "TYPE[][] second dimension takes no size; got {:?}",
16892 self.peek()
16893 )));
16894 }
16895 self.advance();
16896 ty = match ty {
16897 ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
16898 ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
16899 ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
16900 // v7.39 (read01 round 75) — bool[][].
16901 ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
16902 other => {
16903 return Err(self.err(alloc::format!(
16904 "v7.17 2D arrays support INT[][] / BIGINT[][] / \
16905 TEXT[][] only; got {other:?}"
16906 )));
16907 }
16908 };
16909 }
16910 }
16911 Ok((
16912 ty,
16913 implied_auto_increment,
16914 implied_not_null,
16915 user_type_ref,
16916 collation,
16917 collation_explicit,
16918 collation_name,
16919 is_unsigned,
16920 inline_enum_variants,
16921 inline_set_variants,
16922 mysql_int_width,
16923 mysql_fsp,
16924 ))
16925 }
16926
16927 fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
16928 // v7.20 — PG reserves the table-constraint keywords, so a
16929 // BARE `UNIQUE` / `PRIMARY` / … in column position is a
16930 // malformed constraint clause (e.g. `UNIQUE a` missing its
16931 // parens), not a column named "unique". Since v7.17's
16932 // unknown-type leniency (`user_type_ref`) such a clause
16933 // would otherwise parse as a column with a user-defined
16934 // type — silently accepting invalid DDL. Quoted
16935 // identifiers ("unique" / `unique`) remain valid names.
16936 if let Token::Ident(s) = self.peek()
16937 && [
16938 "unique",
16939 "primary",
16940 "foreign",
16941 "constraint",
16942 "check",
16943 "references",
16944 "exclude",
16945 ]
16946 .iter()
16947 .any(|kw| s.eq_ignore_ascii_case(kw))
16948 {
16949 return Err(self.err(alloc::format!(
16950 "unexpected reserved keyword '{s}' at start of column definition \
16951 (malformed table constraint?)"
16952 )));
16953 }
16954 let name = self.expect_ident_like()?;
16955 let (
16956 ty,
16957 implied_auto_increment,
16958 implied_not_null,
16959 user_type_ref,
16960 collation,
16961 collation_explicit,
16962 collation_name,
16963 is_unsigned,
16964 inline_enum_variants,
16965 inline_set_variants,
16966 mysql_int_width,
16967 mysql_fsp,
16968 ) = self.parse_type_with_implied_flags()?;
16969 // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
16970 // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
16971 // each at most once.
16972 let mut default: Option<Expr> = None;
16973 let mut nullable = !implied_not_null;
16974 let mut nullability_seen = implied_not_null;
16975 let mut auto_increment = implied_auto_increment;
16976 let mut is_primary_key = false;
16977 let mut is_unique = false;
16978 let mut unique_nulls_not_distinct = false;
16979 let mut constraint_deferrable = false;
16980 let mut constraint_initially_deferred = false;
16981 let mut check: Option<Expr> = None;
16982 let mut on_update_runtime: Option<Expr> = None;
16983 let mut generated_stored_expr: Option<Box<Expr>> = None;
16984 let mut identity_always = false;
16985 loop {
16986 // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
16987 // not-null constraints by name and pg_dump emits them
16988 // inline: `id bigint CONSTRAINT contacts_id_not_null1
16989 // NOT NULL`. Accept and discard the name; whatever
16990 // constraint follows is parsed by the arms below.
16991 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16992 // v7.39 (round 308, V29) — a name on an inline
16993 // REFERENCES belongs to the FOREIGN KEY, and the caller
16994 // (`parse_column_def_with_fk`) is what builds it, so
16995 // leave the whole clause for it. Dropping the name here
16996 // is what made `CONSTRAINT fk_a REFERENCES …` come back
16997 // as the synthesised `c_pid_fkey` — which then could
16998 // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
16999 // `advance()` takes tokens by `mem::replace`, so there
17000 // is no rewinding once consumed.
17001 if matches!(
17002 self.tokens.get(self.pos + 2),
17003 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
17004 ) {
17005 break;
17006 }
17007 self.advance();
17008 let _name = self.expect_ident_like()?;
17009 continue;
17010 }
17011 // v7.39 (round 379) — MySQL's SHORT generated-column form
17012 // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
17013 // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
17014 // below), but hand-written schemas and app migrations use this.
17015 // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
17016 // SPG computes-and-stores either way, like the long form.
17017 if matches!(self.peek(), Token::As) {
17018 self.advance();
17019 if !matches!(self.peek(), Token::LParen) {
17020 return Err(self.err(alloc::format!(
17021 "expected '(' after AS in a generated column, got {:?}",
17022 self.peek()
17023 )));
17024 }
17025 self.advance();
17026 let expr = self.parse_expr(0)?;
17027 if !matches!(self.peek(), Token::RParen) {
17028 return Err(self.err(alloc::format!(
17029 "expected ')' after AS (<expr>), got {:?}",
17030 self.peek()
17031 )));
17032 }
17033 self.advance();
17034 if matches!(self.peek(), Token::Ident(s)
17035 if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
17036 {
17037 self.advance();
17038 }
17039 generated_stored_expr = Some(alloc::boxed::Box::new(expr));
17040 continue;
17041 }
17042 // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
17043 // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
17044 // the modern replacement for SERIAL in hand-written
17045 // schemas). Both flavours map onto the auto-increment
17046 // machinery — SPG's serial semantics ≈ BY DEFAULT;
17047 // ALWAYS's reject-explicit-values nuance is documented
17048 // leniency. Generated EXPRESSION columns
17049 // (`AS (expr) STORED`) are not supported: error loudly
17050 // instead of silently storing NULLs.
17051 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
17052 self.advance();
17053 let mut saw_generated_always = false;
17054 match self.peek().clone() {
17055 Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
17056 self.advance();
17057 saw_generated_always = true;
17058 }
17059 Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
17060 self.advance();
17061 if !matches!(self.peek(), Token::Default) {
17062 return Err(self.err(alloc::format!(
17063 "expected DEFAULT after GENERATED BY, got {:?}",
17064 self.peek()
17065 )));
17066 }
17067 self.advance();
17068 }
17069 other => {
17070 return Err(self.err(alloc::format!(
17071 "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
17072 )));
17073 }
17074 }
17075 if !matches!(self.peek(), Token::As) {
17076 return Err(self.err(alloc::format!(
17077 "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
17078 self.peek()
17079 )));
17080 }
17081 self.advance();
17082 // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
17083 // ( <expr> ) STORED` stored computed-column. The
17084 // expression is captured for the engine to recompute
17085 // on every INSERT / UPDATE. v7.37.7 accepts the
17086 // STORED keyword only; PG also has VIRTUAL, which
17087 // v7.37.7 carves out (sentori only uses STORED).
17088 if matches!(self.peek(), Token::LParen) {
17089 self.advance();
17090 let expr = self.parse_expr(0)?;
17091 if !matches!(self.peek(), Token::RParen) {
17092 return Err(self.err(alloc::format!(
17093 "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
17094 self.peek()
17095 )));
17096 }
17097 self.advance();
17098 let stored = match self.peek() {
17099 Token::Ident(s) | Token::QuotedIdent(s)
17100 if s.eq_ignore_ascii_case("stored") =>
17101 {
17102 self.advance();
17103 true
17104 }
17105 // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
17106 // generated columns. SPG computes them on write and
17107 // persists like STORED; the two are observably
17108 // identical for query results (the value, recompute
17109 // on base-column change, and NOT NULL enforcement all
17110 // match), so a PG 18 schema/dump using VIRTUAL loads
17111 // and behaves correctly. The compute-on-read storage
17112 // saving is an invisible internal difference.
17113 Token::Ident(s) | Token::QuotedIdent(s)
17114 if s.eq_ignore_ascii_case("virtual") =>
17115 {
17116 self.advance();
17117 false
17118 }
17119 other => {
17120 return Err(self.err(alloc::format!(
17121 "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
17122 got {other:?}"
17123 )));
17124 }
17125 };
17126 let _ = stored; // STORED / VIRTUAL both compute-and-store.
17127 generated_stored_expr = Some(Box::new(expr));
17128 continue;
17129 }
17130 self.expect_keyword_ident("identity")?;
17131 // Optional `(START WITH 1 INCREMENT BY 1 …)` —
17132 // consume the balanced parens and discard (SPG's
17133 // auto-increment is max+1-scan based).
17134 if matches!(self.peek(), Token::LParen) {
17135 let mut depth = 0usize;
17136 loop {
17137 match self.advance() {
17138 Token::LParen => depth += 1,
17139 Token::RParen => {
17140 depth -= 1;
17141 if depth == 0 {
17142 break;
17143 }
17144 }
17145 Token::Eof => {
17146 return Err(self.err(
17147 "unterminated sequence-options parens after IDENTITY".into(),
17148 ));
17149 }
17150 _ => {}
17151 }
17152 }
17153 }
17154 auto_increment = true;
17155 // v7.38 (read01) — remember the ALWAYS flavour so the engine
17156 // can reject explicit non-DEFAULT INSERT values (unless
17157 // OVERRIDING SYSTEM VALUE) the way PG does.
17158 identity_always = saw_generated_always;
17159 // PG identity columns are implicitly NOT NULL.
17160 nullable = false;
17161 continue;
17162 }
17163 // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
17164 // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
17165 // is accepted today. The "ON" token is an Ident
17166 // (not reserved) — peek before consuming.
17167 if matches!(self.peek(), Token::On)
17168 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17169 {
17170 self.advance(); // ON
17171 self.advance(); // update
17172 // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17173 let next = self.peek().clone();
17174 match next {
17175 Token::Ident(s) | Token::QuotedIdent(s)
17176 if s.eq_ignore_ascii_case("current_timestamp") =>
17177 {
17178 self.advance();
17179 // Optional `(N)` precision.
17180 if matches!(self.peek(), Token::LParen) {
17181 self.advance();
17182 if !matches!(self.peek(), Token::Integer(_)) {
17183 return Err(self.err(alloc::format!(
17184 "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17185 self.peek()
17186 )));
17187 }
17188 self.advance();
17189 if !matches!(self.peek(), Token::RParen) {
17190 return Err(self.err(alloc::format!(
17191 "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17192 self.peek()
17193 )));
17194 }
17195 self.advance();
17196 }
17197 on_update_runtime = Some(Expr::FunctionCall {
17198 name: "now".into(),
17199 args: Vec::new(),
17200 });
17201 continue;
17202 }
17203 other => {
17204 return Err(self.err(alloc::format!(
17205 "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17206 )));
17207 }
17208 }
17209 }
17210 if matches!(self.peek(), Token::Default) {
17211 if default.is_some() {
17212 return Err(self.err("DEFAULT specified twice".into()));
17213 }
17214 self.advance();
17215 default = Some(self.parse_expr(0)?);
17216 continue;
17217 }
17218 // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17219 // token with NOT NULL and sits EARLIER in the loop than the
17220 // deferrability arm, so without the lookahead it was reported as
17221 // "NOT NULL specified twice" (or "expected NULL after NOT").
17222 if matches!(self.peek(), Token::Not)
17223 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17224 {
17225 // NOT DEFERRABLE — explicit immediate; nothing to carry.
17226 self.consume_optional_deferrable_clauses()?;
17227 continue;
17228 }
17229 if matches!(self.peek(), Token::Not) {
17230 if nullability_seen {
17231 return Err(self.err("NOT NULL specified twice".into()));
17232 }
17233 self.advance();
17234 if !matches!(self.peek(), Token::Null) {
17235 return Err(self.err(format!(
17236 "expected NULL after NOT in column def, got {:?}",
17237 self.peek()
17238 )));
17239 }
17240 self.advance();
17241 nullable = false;
17242 nullability_seen = true;
17243 continue;
17244 }
17245 // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17246 // "this column is nullable" marker (the default in
17247 // standard SQL anyway). mysqldump emits it routinely
17248 // (`col TYPE NULL DEFAULT NULL` for nullable
17249 // timestamps etc). Accept + no-op.
17250 if matches!(self.peek(), Token::Null) {
17251 if nullability_seen && !nullable {
17252 // v7.39 (round 761, F31 tranche 2 #31) — PG's
17253 // sentence, PG18-measured (the table name is the
17254 // caller's; the column half is exact).
17255 return Err(self.err(alloc::format!(
17256 "conflicting NULL/NOT NULL declarations for column \"{name}\""
17257 )));
17258 }
17259 self.advance();
17260 nullable = true;
17261 nullability_seen = true;
17262 continue;
17263 }
17264 // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17265 // arrives as a bare Ident. Match either, case-insensitive.
17266 if let Token::Ident(s) = self.peek()
17267 && (s.eq_ignore_ascii_case("auto_increment")
17268 || s.eq_ignore_ascii_case("autoincrement"))
17269 {
17270 if auto_increment {
17271 return Err(self.err("AUTO_INCREMENT specified twice".into()));
17272 }
17273 self.advance();
17274 auto_increment = true;
17275 continue;
17276 }
17277 // v7.9.13 — inline `PRIMARY KEY` column constraint
17278 // (mailrs F1). Implies `NOT NULL`. The engine creates
17279 // a BTree index for the PK column at CREATE TABLE time
17280 // so FK parent-side index lookups resolve.
17281 // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17282 // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17283 // spelling was a parse error, so a pg_dump carrying one stopped
17284 // mid-restore. The clauses are consumed by the same helper the FK
17285 // path has used since round 288 and recorded nowhere: SPG enforces
17286 // the constraint IMMEDIATELY either way, which fails earlier than
17287 // PG inside a transaction that violates-then-repairs — a refusal,
17288 // not a wrong answer. True deferral is the open remainder of F08.
17289 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17290 || (matches!(self.peek(), Token::Not)
17291 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
17292 {
17293 // v7.39 (round 711) — CARRIED now (the storing half of
17294 // F08); round 621 only consumed.
17295 let (d, idef) = self.consume_deferrable_clauses_timed()?;
17296 constraint_deferrable |= d;
17297 constraint_initially_deferred |= idef;
17298 continue;
17299 }
17300 if let Token::Ident(s) = self.peek()
17301 && s.eq_ignore_ascii_case("primary")
17302 {
17303 if is_primary_key {
17304 return Err(self.err("PRIMARY KEY specified twice".into()));
17305 }
17306 // Peek-ahead for the required `KEY` token.
17307 let next = self.tokens.get(self.pos + 1);
17308 let next_is_key = matches!(
17309 next,
17310 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17311 );
17312 if !next_is_key {
17313 return Err(self.err(format!(
17314 "expected KEY after PRIMARY in column def, got {:?}",
17315 next
17316 )));
17317 }
17318 self.advance(); // PRIMARY
17319 self.advance(); // KEY
17320 is_primary_key = true;
17321 if nullability_seen && nullable {
17322 return Err(self.err(
17323 "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17324 ));
17325 }
17326 nullable = false;
17327 nullability_seen = true;
17328 continue;
17329 }
17330 // v7.13.0 — inline `UNIQUE` column constraint
17331 // (mailrs round-5 G2). Fold into a single-column
17332 // table-level UNIQUE at CREATE TABLE post-process time.
17333 if let Token::Ident(s) = self.peek()
17334 && s.eq_ignore_ascii_case("unique")
17335 {
17336 if is_unique {
17337 return Err(self.err("UNIQUE specified twice".into()));
17338 }
17339 self.advance();
17340 is_unique = true;
17341 // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17342 // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17343 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17344 let n1 = self.tokens.get(self.pos + 1);
17345 let n2 = self.tokens.get(self.pos + 2);
17346 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17347 self.advance(); // NULLS
17348 self.advance(); // NOT
17349 self.advance(); // DISTINCT
17350 unique_nulls_not_distinct = true;
17351 } else if matches!(n1, Some(Token::Distinct)) {
17352 self.advance(); // NULLS
17353 self.advance(); // DISTINCT
17354 }
17355 }
17356 continue;
17357 }
17358 // v7.13.0 — inline `CHECK (<expr>)` column constraint
17359 // (mailrs round-5 G3). PG semantics: column-level
17360 // CHECK is equivalent to a table-level CHECK. Multiple
17361 // inline CHECKs on the same column AND together.
17362 if let Token::Ident(s) = self.peek()
17363 && s.eq_ignore_ascii_case("check")
17364 {
17365 self.advance();
17366 if !matches!(self.peek(), Token::LParen) {
17367 return Err(self.err(alloc::format!(
17368 "expected '(' after CHECK in column def, got {:?}",
17369 self.peek()
17370 )));
17371 }
17372 self.advance();
17373 let pred = self.parse_expr(0)?;
17374 if !matches!(self.peek(), Token::RParen) {
17375 return Err(self.err(alloc::format!(
17376 "expected ')' to close CHECK predicate, got {:?}",
17377 self.peek()
17378 )));
17379 }
17380 self.advance();
17381 check = Some(match check.take() {
17382 Some(prev) => Expr::Binary {
17383 op: BinOp::And,
17384 lhs: Box::new(prev),
17385 rhs: Box::new(pred),
17386 },
17387 None => pred,
17388 });
17389 continue;
17390 }
17391 break;
17392 }
17393 Ok(ColumnDef {
17394 name,
17395 ty,
17396 nullable,
17397 default,
17398 auto_increment,
17399 is_primary_key,
17400 is_unique,
17401 unique_nulls_not_distinct,
17402 constraint_deferrable,
17403 constraint_initially_deferred,
17404 check,
17405 user_type_ref,
17406 on_update_runtime,
17407 collation,
17408 collation_explicit,
17409 collation_name,
17410 is_unsigned,
17411 inline_enum_variants,
17412 inline_set_variants,
17413 generated_stored_expr,
17414 identity_always,
17415 mysql_int_width,
17416 mysql_fsp,
17417 })
17418 }
17419
17420 /// `NUMERIC` may appear without parameters, with one (precision
17421 /// only, scale=0), or with both. Returns `(precision, scale)` with
17422 /// 0 = unspecified for the bare form.
17423 fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17424 if !matches!(self.peek(), Token::LParen) {
17425 // Bare `NUMERIC` — PG treats this as "unlimited precision";
17426 // we surface it as precision=0 to mean "unconstrained" so
17427 // the engine doesn't need a separate variant.
17428 return Ok((0, 0));
17429 }
17430 self.advance();
17431 // v7.39 (round 272) — PG's declared precision runs to 1000, and
17432 // it words the out-of-range case with the value it saw. SPG
17433 // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17434 // accepts failed to parse at all; values wider than i128 are
17435 // carried by the arbitrary-precision form.
17436 let precision = match self.advance() {
17437 Token::Integer(n) if (1..=1000).contains(&n) => {
17438 u16::try_from(n).expect("range-checked")
17439 }
17440 Token::Integer(n) => {
17441 return Err(ParseError {
17442 message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17443 token_pos: self.consumed_pos(),
17444 });
17445 }
17446 other => {
17447 return Err(ParseError {
17448 message: format!(
17449 "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17450 ),
17451 token_pos: self.consumed_pos(),
17452 });
17453 }
17454 };
17455 // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17456 // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17457 // then overflows). A negative scale rounds to tens / hundreds / …
17458 let scale = if matches!(self.peek(), Token::Comma) {
17459 self.advance();
17460 let neg = if matches!(self.peek(), Token::Minus) {
17461 self.advance();
17462 true
17463 } else {
17464 false
17465 };
17466 match self.advance() {
17467 Token::Integer(n) => {
17468 let signed = if neg { -n } else { n };
17469 if !(-1000..=1000).contains(&signed) {
17470 return Err(ParseError {
17471 message: format!(
17472 "NUMERIC scale {signed} must be between -1000 and 1000"
17473 ),
17474 token_pos: self.consumed_pos(),
17475 });
17476 }
17477 i16::try_from(signed).expect("range-checked")
17478 }
17479 other => {
17480 return Err(ParseError {
17481 message: format!("NUMERIC scale must be an integer, got {other:?}"),
17482 token_pos: self.consumed_pos(),
17483 });
17484 }
17485 }
17486 } else {
17487 0
17488 };
17489 if !matches!(self.peek(), Token::RParen) {
17490 return Err(self.err(format!(
17491 "expected ')' to close NUMERIC params, got {:?}",
17492 self.peek()
17493 )));
17494 }
17495 self.advance();
17496 Ok((precision, scale))
17497 }
17498
17499 /// Parse `(N)` where `N` is a positive integer literal — used by the
17500 /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
17501 /// for the error message.
17502 /// v6.0.1: parse the optional `USING <encoding>` clause that
17503 /// follows `VECTOR(N)` in a column definition. Missing clause
17504 /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
17505 /// ident → `ParseError` listing the encodings recognised today.
17506 fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
17507 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
17508 return Ok(VecEncoding::F32);
17509 }
17510 // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
17511 // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
17512 // consume the token when the very next token is a known
17513 // vector-encoding keyword (SQ8 / HALF). Otherwise leave
17514 // `USING` for the caller — it's the rewrite-expression form.
17515 let n1 = self.tokens.get(self.pos + 1);
17516 let next_is_encoding = matches!(
17517 n1,
17518 Some(Token::Ident(s))
17519 if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
17520 );
17521 if !next_is_encoding {
17522 return Ok(VecEncoding::F32);
17523 }
17524 self.advance();
17525 let enc_ident = match self.advance() {
17526 Token::Ident(s) => s,
17527 other => {
17528 return Err(self.err(format!(
17529 "expected vector encoding after USING, got {other:?}"
17530 )));
17531 }
17532 };
17533 match enc_ident.to_ascii_lowercase().as_str() {
17534 "sq8" => Ok(VecEncoding::Sq8),
17535 // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
17536 // binary16 per-element storage.
17537 "half" => Ok(VecEncoding::F16),
17538 other => Err(self.err(format!(
17539 "unknown vector encoding {other:?}; supported: SQ8, HALF"
17540 ))),
17541 }
17542 }
17543
17544 /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
17545 /// without consuming it. Returns `Some(N)` when the next
17546 /// tokens are `( <int> )`; None otherwise. Used by the
17547 /// TINYINT classifier to decide whether to map to Bool or
17548 /// SmallInt.
17549 fn peek_optional_paren_size_value(&self) -> Option<i64> {
17550 if !matches!(self.peek(), Token::LParen) {
17551 return None;
17552 }
17553 let next = self.tokens.get(self.pos + 1)?;
17554 let n = match next {
17555 Token::Integer(n) => *n,
17556 _ => return None,
17557 };
17558 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17559 return None;
17560 }
17561 Some(n)
17562 }
17563
17564 /// v7.14.0 — consume an optional MySQL display-width
17565 /// parenthesised number after an integer type, returning
17566 /// nothing. `TINYINT(1)` etc.
17567 /// v7.39 (round 360) — does the parenthesised group ahead contain a
17568 /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
17569 fn peek_paren_has_comma(&self) -> bool {
17570 let mut i = self.pos + 1;
17571 let mut depth = 1usize;
17572 while depth > 0 {
17573 match self.tokens.get(i) {
17574 Some(Token::LParen) => depth += 1,
17575 Some(Token::RParen) => depth -= 1,
17576 Some(Token::Comma) if depth == 1 => return true,
17577 None | Some(Token::Eof) => return false,
17578 _ => {}
17579 }
17580 i += 1;
17581 }
17582 false
17583 }
17584
17585 /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
17586 /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
17587 /// fractional-seconds precision that drives write truncation and render
17588 /// padding, where `consume_optional_paren_size` throws it away.
17589 /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
17590 fn take_optional_paren_size(&mut self) -> Option<u8> {
17591 let Some(Token::Integer(n)) = self
17592 .tokens
17593 .get(self.pos + 1)
17594 .filter(|_| matches!(self.peek(), Token::LParen))
17595 .cloned()
17596 else {
17597 self.consume_optional_paren_size();
17598 return None;
17599 };
17600 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17601 self.consume_optional_paren_size();
17602 return None;
17603 }
17604 self.consume_optional_paren_size();
17605 u8::try_from(n).ok()
17606 }
17607
17608 fn consume_optional_paren_size(&mut self) {
17609 if !matches!(self.peek(), Token::LParen) {
17610 return;
17611 }
17612 self.advance();
17613 // Skip until matching RParen (allow nested or any tokens).
17614 let mut depth = 1usize;
17615 while depth > 0 {
17616 match self.peek() {
17617 Token::LParen => depth += 1,
17618 Token::RParen => depth -= 1,
17619 Token::Eof => return,
17620 _ => {}
17621 }
17622 self.advance();
17623 }
17624 }
17625
17626 fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
17627 if !matches!(self.peek(), Token::LParen) {
17628 return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
17629 }
17630 self.advance();
17631 let n = match self.advance() {
17632 Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
17633 message: format!("{label} size too large: {n}"),
17634 token_pos: self.consumed_pos(),
17635 })?,
17636 other => {
17637 return Err(ParseError {
17638 message: format!("expected positive integer {label} size, got {other:?}"),
17639 token_pos: self.consumed_pos(),
17640 });
17641 }
17642 };
17643 if !matches!(self.peek(), Token::RParen) {
17644 return Err(self.err(format!(
17645 "expected ')' after {label} size, got {:?}",
17646 self.peek()
17647 )));
17648 }
17649 self.advance();
17650 Ok(n)
17651 }
17652
17653 /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
17654 /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
17655 /// key, like MySQL) whose action skips conflicting rows.
17656 /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
17657 /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
17658 /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
17659 /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
17660 /// common bulk-upsert spellings —
17661 /// INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
17662 /// REPLACE INTO t SELECT …
17663 /// — were a parse error / a duplicate-key failure respectively.
17664 ///
17665 /// Precedence: an explicitly written clause beats a statement-level flag.
17666 /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
17667 /// implicit `REPLACE` and `IGNORE` lowerings.
17668 fn parse_insert_conflict_clause(
17669 &mut self,
17670 replace: bool,
17671 ignore: bool,
17672 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17673 if let Some(c) = self.parse_optional_on_duplicate_key()? {
17674 return Ok(Some(c));
17675 }
17676 if let Some(c) = self.parse_optional_on_conflict()? {
17677 return Ok(Some(c));
17678 }
17679 if replace {
17680 // REPLACE INTO = delete-then-insert, which PG spells as
17681 // `ON CONFLICT DO UPDATE SET` over every column; the engine
17682 // reads an empty assignment list as "take the incoming row".
17683 return Ok(Some(crate::ast::OnConflictClause {
17684 target_columns: Vec::new(),
17685 index_where: None,
17686 constraint_name: None,
17687 mysql_lowered: true,
17688 action: crate::ast::OnConflictAction::Update {
17689 assignments: Vec::new(),
17690 where_: None,
17691 },
17692 }));
17693 }
17694 if ignore {
17695 return Ok(Some(Self::insert_ignore_clause()));
17696 }
17697 Ok(None)
17698 }
17699
17700 /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
17701 /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
17702 /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
17703 /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
17704 fn parse_optional_on_duplicate_key(
17705 &mut self,
17706 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17707 if !(matches!(self.peek(), Token::On)
17708 && matches!(self.tokens.get(self.pos + 1),
17709 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
17710 {
17711 return Ok(None);
17712 }
17713 self.advance(); // ON
17714 self.advance(); // DUPLICATE
17715 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
17716 return Err(self.err(format!(
17717 "expected KEY after ON DUPLICATE, got {:?}",
17718 self.peek()
17719 )));
17720 }
17721 self.advance();
17722 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
17723 return Err(self.err(format!(
17724 "expected UPDATE after ON DUPLICATE KEY, got {:?}",
17725 self.peek()
17726 )));
17727 }
17728 self.advance();
17729 let mut assignments: Vec<(String, Expr)> = Vec::new();
17730 loop {
17731 let col = self.expect_ident_like()?;
17732 if !matches!(self.peek(), Token::Eq) {
17733 return Err(self.err(format!(
17734 "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
17735 self.peek()
17736 )));
17737 }
17738 self.advance();
17739 let mut expr = self.parse_expr(0)?;
17740 Self::rewrite_mysql_values_refs(&mut expr);
17741 assignments.push((col, expr));
17742 if matches!(self.peek(), Token::Comma) {
17743 self.advance();
17744 continue;
17745 }
17746 break;
17747 }
17748 Ok(Some(crate::ast::OnConflictClause {
17749 target_columns: Vec::new(),
17750 index_where: None,
17751 constraint_name: None,
17752 mysql_lowered: true,
17753 action: crate::ast::OnConflictAction::Update {
17754 assignments,
17755 where_: None,
17756 },
17757 }))
17758 }
17759
17760 fn insert_ignore_clause() -> crate::ast::OnConflictClause {
17761 crate::ast::OnConflictClause {
17762 target_columns: Vec::new(),
17763 index_where: None,
17764 constraint_name: None,
17765 mysql_lowered: true,
17766 action: crate::ast::OnConflictAction::Nothing,
17767 }
17768 }
17769
17770 fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
17771 debug_assert!(
17772 matches!(self.peek(), Token::Insert)
17773 || (replace
17774 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
17775 );
17776 self.advance();
17777 // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
17778 // would raise a duplicate-key error instead of failing the statement,
17779 // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
17780 // plain ident to the lexer; only the MySQL dialect accepts it here.
17781 let ignore = self.mysql_dialect
17782 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
17783 if ignore {
17784 self.advance();
17785 }
17786 if !matches!(self.peek(), Token::Into) {
17787 return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
17788 }
17789 self.advance();
17790 let table = self.expect_ident_like()?;
17791 // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
17792 // grammar requires the AS keyword here (a bare identifier would be
17793 // ambiguous with a column list). The alias is what the ON CONFLICT
17794 // DO UPDATE expressions refer to the target row by.
17795 let alias = if matches!(self.peek(), Token::As) {
17796 self.advance();
17797 Some(self.expect_ident_like()?)
17798 } else {
17799 None
17800 };
17801 // v7.39 (round 428) — MySQL's SET-form INSERT:
17802 // INSERT INTO t SET a = 1, b = 'x'
17803 // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
17804 // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
17805 // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
17806 // measured). So it lowers to the column list + one VALUES row and
17807 // rejoins the ordinary path, which already handles every one of
17808 // those. PG has no such spelling, hence the dialect gate.
17809 if self.mysql_dialect
17810 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
17811 {
17812 self.advance(); // SET
17813 let mut names = Vec::new();
17814 let mut values = Vec::new();
17815 loop {
17816 names.push(self.expect_ident_like()?);
17817 if !matches!(self.peek(), Token::Eq) {
17818 return Err(self.err(alloc::format!(
17819 "expected '=' in INSERT … SET, got {:?}",
17820 self.peek()
17821 )));
17822 }
17823 self.advance();
17824 // `SET a = DEFAULT` rides the same `__column_default` marker
17825 // the VALUES-row and UPDATE-SET paths use; the INSERT
17826 // executor resolves it against the target column.
17827 if matches!(self.peek(), Token::Default) {
17828 self.advance();
17829 values.push(Expr::FunctionCall {
17830 name: "__column_default".to_string(),
17831 args: Vec::new(),
17832 });
17833 } else {
17834 values.push(self.parse_expr(0)?);
17835 }
17836 if matches!(self.peek(), Token::Comma) {
17837 self.advance();
17838 continue;
17839 }
17840 break;
17841 }
17842 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17843 let returning = self.parse_optional_returning()?;
17844 return Ok(Statement::Insert(InsertStatement {
17845 ctes: Vec::new(),
17846 table,
17847 alias,
17848 columns: Some(names),
17849 rows: alloc::vec![values],
17850 select_source: None,
17851 // MySQL's SET form has no `OVERRIDING …` clause (that is
17852 // PG's identity-column spelling).
17853 overriding: Overriding::None,
17854 mysql_ignore: ignore,
17855 on_conflict,
17856 returning,
17857 }));
17858 }
17859 // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
17860 // v7.39 (round 151) — a SELECT or WITH right after the paren is
17861 // a parenthesized query source instead (PG select_with_parens:
17862 // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
17863 // both keywords are reserved in PG, so no column list can start
17864 // with them.
17865 let columns = if matches!(self.peek(), Token::LParen) {
17866 self.advance();
17867 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17868 let select_stmt = if self.peek_is_with_kw() {
17869 self.advance();
17870 self.parse_nested_with_select()?
17871 } else {
17872 match self.parse_select_stmt()? {
17873 Statement::Select(s) => s,
17874 other => {
17875 return Err(self.err(alloc::format!(
17876 "expected SELECT in parenthesized INSERT source, got {other:?}"
17877 )));
17878 }
17879 }
17880 };
17881 if !matches!(self.peek(), Token::RParen) {
17882 return Err(self.err(format!(
17883 "expected ')' after parenthesized INSERT source, got {:?}",
17884 self.peek()
17885 )));
17886 }
17887 self.advance();
17888 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17889 let returning = self.parse_optional_returning()?;
17890 return Ok(Statement::Insert(InsertStatement {
17891 ctes: Vec::new(),
17892 table,
17893 alias: alias.clone(),
17894 columns: None,
17895 rows: Vec::new(),
17896 select_source: Some(Box::new(select_stmt)),
17897 on_conflict,
17898 returning,
17899 overriding: Overriding::None,
17900 mysql_ignore: ignore,
17901 }));
17902 }
17903 let mut names = Vec::new();
17904 loop {
17905 names.push(self.expect_ident_like()?);
17906 match self.peek() {
17907 Token::Comma => {
17908 self.advance();
17909 }
17910 Token::RParen => {
17911 self.advance();
17912 break;
17913 }
17914 other => {
17915 return Err(self.err(format!(
17916 "expected ',' or ')' in INSERT column list, got {other:?}"
17917 )));
17918 }
17919 }
17920 }
17921 Some(names)
17922 } else {
17923 None
17924 };
17925 // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
17926 // OVERRIDING SYSTEM VALUE for its identity columns. The clause
17927 // is captured on the statement so the engine can apply PG's
17928 // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
17929 let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
17930 {
17931 self.advance();
17932 let which = self.expect_ident_like()?;
17933 let ov = if which.eq_ignore_ascii_case("system") {
17934 Overriding::System
17935 } else if which.eq_ignore_ascii_case("user") {
17936 Overriding::User
17937 } else {
17938 return Err(self.err(format!(
17939 "expected SYSTEM or USER after OVERRIDING, got {which:?}"
17940 )));
17941 };
17942 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
17943 return Err(self.err(format!(
17944 "expected VALUE after OVERRIDING {}, got {:?}",
17945 which.to_ascii_uppercase(),
17946 self.peek()
17947 )));
17948 }
17949 self.advance();
17950 ov
17951 } else {
17952 Overriding::None
17953 };
17954 // `INSERT INTO t DEFAULT VALUES` — a single row made
17955 // entirely of column defaults. Lower to the permuted
17956 // column-list path with an empty list: every schema column
17957 // is unmapped, so the engine fills each from its default
17958 // (serials advance, plain defaults evaluate, the rest NULL).
17959 if matches!(self.peek(), Token::Default) {
17960 self.advance();
17961 if !matches!(self.peek(), Token::Values) {
17962 return Err(self.err(format!(
17963 "expected VALUES after DEFAULT in INSERT, got {:?}",
17964 self.peek()
17965 )));
17966 }
17967 self.advance();
17968 if columns.is_some() {
17969 return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
17970 }
17971 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17972 let returning = self.parse_optional_returning()?;
17973 return Ok(Statement::Insert(InsertStatement {
17974 ctes: Vec::new(),
17975 table,
17976 alias: alias.clone(),
17977 columns: Some(Vec::new()),
17978 rows: alloc::vec![Vec::new()],
17979 select_source: None,
17980 on_conflict,
17981 returning,
17982 overriding,
17983 mysql_ignore: ignore,
17984 }));
17985 }
17986 // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
17987 // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
17988 // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
17989 // SELECT …`) heads the SOURCE select, as in PG (the statement's
17990 // own WITH comes before INSERT).
17991 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17992 let select_stmt = if self.peek_is_with_kw() {
17993 self.advance();
17994 self.parse_nested_with_select()?
17995 } else {
17996 match self.parse_select_stmt()? {
17997 Statement::Select(s) => s,
17998 other => {
17999 return Err(self.err(alloc::format!(
18000 "expected SELECT after INSERT INTO ... target, got {other:?}"
18001 )));
18002 }
18003 }
18004 };
18005 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18006 let returning = self.parse_optional_returning()?;
18007 return Ok(Statement::Insert(InsertStatement {
18008 ctes: Vec::new(),
18009 table,
18010 alias: alias.clone(),
18011 columns,
18012 rows: Vec::new(),
18013 select_source: Some(Box::new(select_stmt)),
18014 on_conflict,
18015 returning,
18016 overriding,
18017 mysql_ignore: ignore,
18018 }));
18019 }
18020 if !matches!(self.peek(), Token::Values) {
18021 return Err(self.err(format!(
18022 "expected VALUES or SELECT after table name, got {:?}",
18023 self.peek()
18024 )));
18025 }
18026 self.advance();
18027 if !matches!(self.peek(), Token::LParen) {
18028 return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
18029 }
18030 let mut rows = Vec::new();
18031 loop {
18032 // Each iteration consumes one `(expr, expr, …)` tuple.
18033 if !matches!(self.peek(), Token::LParen) {
18034 return Err(self.err(format!(
18035 "expected '(' for next VALUES tuple, got {:?}",
18036 self.peek()
18037 )));
18038 }
18039 self.advance();
18040 let mut tuple = Vec::new();
18041 loop {
18042 // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
18043 // the column's declared default for that slot. Rides out as the
18044 // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
18045 // path uses; the INSERT executor resolves it per target column.
18046 if matches!(self.peek(), Token::Default) {
18047 self.advance();
18048 tuple.push(Expr::FunctionCall {
18049 name: "__column_default".to_string(),
18050 args: Vec::new(),
18051 });
18052 } else {
18053 tuple.push(self.parse_expr(0)?);
18054 }
18055 match self.peek() {
18056 Token::Comma => {
18057 self.advance();
18058 }
18059 Token::RParen => {
18060 self.advance();
18061 break;
18062 }
18063 other => {
18064 return Err(self.err(format!(
18065 "expected ',' or ')' in VALUES tuple, got {other:?}"
18066 )));
18067 }
18068 }
18069 }
18070 if tuple.is_empty() {
18071 return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
18072 }
18073 rows.push(tuple);
18074 // Continue with comma-separated tuples.
18075 if matches!(self.peek(), Token::Comma) {
18076 self.advance();
18077 } else {
18078 break;
18079 }
18080 }
18081 // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
18082 // to ON CONFLICT DO UPDATE with an empty conflict target
18083 // (the engine picks the table's first unique index, which
18084 // matches MySQL's any-unique-key behaviour for the common
18085 // single-key case). `VALUES(col)` in the assignments is
18086 // MySQL's spelling of EXCLUDED.col.
18087 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18088 let returning = self.parse_optional_returning()?;
18089 Ok(Statement::Insert(InsertStatement {
18090 ctes: Vec::new(),
18091 table,
18092 alias,
18093 columns,
18094 rows,
18095 select_source: None,
18096 on_conflict,
18097 returning,
18098 overriding,
18099 mysql_ignore: ignore,
18100 }))
18101 }
18102
18103 /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
18104 /// the incoming row's value — exactly PG's EXCLUDED.col.
18105 fn rewrite_mysql_values_refs(e: &mut Expr) {
18106 match e {
18107 Expr::FunctionCall { name, args }
18108 if name.eq_ignore_ascii_case("values")
18109 && args.len() == 1
18110 && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
18111 {
18112 let Expr::Column(c) = &args[0] else {
18113 unreachable!("guarded above");
18114 };
18115 *e = Expr::Column(crate::ast::ColumnName {
18116 qualifier: Some("EXCLUDED".to_string()),
18117 name: c.name.clone(),
18118 });
18119 }
18120 Expr::FunctionCall { args, .. } => {
18121 for a in args {
18122 Self::rewrite_mysql_values_refs(a);
18123 }
18124 }
18125 Expr::Binary { lhs, rhs, .. } => {
18126 Self::rewrite_mysql_values_refs(lhs);
18127 Self::rewrite_mysql_values_refs(rhs);
18128 }
18129 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
18130 Self::rewrite_mysql_values_refs(expr);
18131 }
18132 Expr::Case {
18133 operand,
18134 branches,
18135 else_branch,
18136 } => {
18137 if let Some(op) = operand {
18138 Self::rewrite_mysql_values_refs(op);
18139 }
18140 for (w, t) in branches {
18141 Self::rewrite_mysql_values_refs(w);
18142 Self::rewrite_mysql_values_refs(t);
18143 }
18144 if let Some(el) = else_branch {
18145 Self::rewrite_mysql_values_refs(el);
18146 }
18147 }
18148 _ => {}
18149 }
18150 }
18151
18152 /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
18153 /// clause sitting between the INSERT body and the trailing
18154 /// RETURNING. All keywords come in as bare idents; `ON` is
18155 /// a reserved Token though.
18156 fn parse_optional_on_conflict(
18157 &mut self,
18158 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18159 if !matches!(self.peek(), Token::On) {
18160 return Ok(None);
18161 }
18162 // Peek further: we want exactly "ON CONFLICT ...". If the
18163 // next ident isn't "conflict", let some other parser handle.
18164 let next_is_conflict = matches!(
18165 self.tokens.get(self.pos + 1),
18166 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18167 );
18168 if !next_is_conflict {
18169 return Ok(None);
18170 }
18171 self.advance(); // ON
18172 self.advance(); // CONFLICT
18173 // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18174 // the constraint instead of listing columns (the pg_dump
18175 // form); the engine resolves it.
18176 let mut constraint_name: Option<String> = None;
18177 if matches!(self.peek(), Token::On) {
18178 self.advance(); // ON
18179 match self.advance() {
18180 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18181 }
18182 other => {
18183 return Err(self.err(alloc::format!(
18184 "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18185 )));
18186 }
18187 }
18188 constraint_name = Some(self.expect_ident_like()?);
18189 }
18190 // Optional `(col [, col]*)` target list.
18191 let mut target_columns: Vec<String> = Vec::new();
18192 if matches!(self.peek(), Token::LParen) {
18193 self.advance();
18194 loop {
18195 target_columns.push(self.expect_ident_like()?);
18196 match self.peek() {
18197 Token::Comma => {
18198 self.advance();
18199 }
18200 Token::RParen => {
18201 self.advance();
18202 break;
18203 }
18204 other => {
18205 return Err(self.err(alloc::format!(
18206 "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18207 )));
18208 }
18209 }
18210 }
18211 }
18212 // v7.39 (round 240) — optional index predicate after the target
18213 // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18214 // PARTIAL unique index; SPG's arbiters are full indexes, which
18215 // satisfy any predicate, so it is parsed and carried but not
18216 // consulted (recorded residual: partial-unique-index arbiters).
18217 let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18218 self.advance();
18219 Some(self.parse_expr(0)?)
18220 } else {
18221 None
18222 };
18223 // Required `DO`.
18224 match self.advance() {
18225 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18226 other => {
18227 return Err(self.err(alloc::format!(
18228 "expected DO after ON CONFLICT [(…)], got {other:?}"
18229 )));
18230 }
18231 }
18232 // Action: NOTHING | UPDATE SET …
18233 let action = match self.advance() {
18234 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18235 crate::ast::OnConflictAction::Nothing
18236 }
18237 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18238 self.parse_on_conflict_update_action()?
18239 }
18240 other => {
18241 return Err(self.err(alloc::format!(
18242 "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18243 )));
18244 }
18245 };
18246 Ok(Some(crate::ast::OnConflictClause {
18247 target_columns,
18248 index_where,
18249 constraint_name,
18250 mysql_lowered: false,
18251 action,
18252 }))
18253 }
18254
18255 /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18256 /// `SET col = expr [, …] [WHERE cond]`. Caller already
18257 /// consumed `UPDATE`.
18258 fn parse_on_conflict_update_action(
18259 &mut self,
18260 ) -> Result<crate::ast::OnConflictAction, ParseError> {
18261 // `SET`
18262 match self.advance() {
18263 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18264 other => {
18265 return Err(self.err(alloc::format!(
18266 "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18267 )));
18268 }
18269 }
18270 let mut assignments: Vec<(String, Expr)> = Vec::new();
18271 loop {
18272 let col = self.expect_ident_like()?;
18273 if !matches!(self.peek(), Token::Eq) {
18274 return Err(self.err(alloc::format!(
18275 "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18276 self.peek()
18277 )));
18278 }
18279 self.advance();
18280 let value = self.parse_expr(0)?;
18281 assignments.push((col, value));
18282 if matches!(self.peek(), Token::Comma) {
18283 self.advance();
18284 continue;
18285 }
18286 break;
18287 }
18288 let where_ = if matches!(self.peek(), Token::Where) {
18289 self.advance();
18290 Some(self.parse_expr(0)?)
18291 } else {
18292 None
18293 };
18294 Ok(crate::ast::OnConflictAction::Update {
18295 assignments,
18296 where_,
18297 })
18298 }
18299
18300 fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18301 let mut items = Vec::new();
18302 // v7.39 (round 341, V66) — PG's target list may be EMPTY
18303 // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18304 // answers one zero-column row per row of t, and a bare `SELECT`
18305 // answers a single zero-column row. SPG required at least one
18306 // item, so both were syntax errors. Recognised by the token that
18307 // follows — nothing that can start an expression appears here.
18308 if self.select_list_is_empty_here() {
18309 return Ok(items);
18310 }
18311 loop {
18312 items.push(self.parse_select_item()?);
18313 if matches!(self.peek(), Token::Comma) {
18314 self.advance();
18315 } else {
18316 break;
18317 }
18318 }
18319 Ok(items)
18320 }
18321
18322 /// Is the target list empty at this point — i.e. does the next token
18323 /// end the SELECT's item list rather than start an item?
18324 fn select_list_is_empty_here(&self) -> bool {
18325 match self.peek() {
18326 Token::From
18327 | Token::Where
18328 | Token::Group
18329 | Token::Having
18330 | Token::Order
18331 | Token::Limit
18332 | Token::Offset
18333 | Token::Semicolon
18334 | Token::RParen
18335 | Token::Union
18336 | Token::Except
18337 | Token::Eof => true,
18338 // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18339 // with unreserved keywords, so they arrive as plain idents.
18340 Token::Ident(s) => {
18341 s.eq_ignore_ascii_case("fetch")
18342 || s.eq_ignore_ascii_case("window")
18343 || s.eq_ignore_ascii_case("intersect")
18344 }
18345 _ => false,
18346 }
18347 }
18348
18349 fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18350 if matches!(self.peek(), Token::Star) {
18351 self.advance();
18352 return Ok(SelectItem::Wildcard);
18353 }
18354 // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18355 // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18356 // choke on the `*` ("expected identifier, got Star"). The lookahead is
18357 // `<ident> . *` with nothing binding tighter.
18358 if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18359 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18360 && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18361 {
18362 self.advance(); // qualifier
18363 self.advance(); // .
18364 self.advance(); // *
18365 return Ok(SelectItem::QualifiedWildcard(q));
18366 }
18367 }
18368 let start_tok = self.pos;
18369 let expr = self.parse_expr(0)?;
18370 let end_tok = self.consumed_pos();
18371 // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18372 // multi-column function returns into columns. Marked here and lowered in
18373 // `parse_bare_select`, where the FROM clause is in hand.
18374 if matches!(self.peek(), Token::Dot)
18375 && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18376 {
18377 self.advance(); // .
18378 self.advance(); // *
18379 return Ok(SelectItem::Expr {
18380 expr: Expr::FunctionCall {
18381 name: "__record_expand".to_string(),
18382 args: alloc::vec![expr],
18383 },
18384 alias: None,
18385 });
18386 }
18387 let alias = match self.parse_optional_alias()? {
18388 Some(a) => Some(a),
18389 None => self.mysql_item_label(&expr, start_tok, end_tok),
18390 };
18391 Ok(SelectItem::Expr { expr, alias })
18392 }
18393
18394 /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18395 /// carries no `AS`, filled in here so every downstream path reports it
18396 /// without knowing the rule. `None` leaves the item un-aliased, which is
18397 /// what a PG session always gets.
18398 ///
18399 /// Measured against MariaDB 11, three rules and no more:
18400 ///
18401 /// | item | label | why |
18402 /// |------------------|------------|------------------------------|
18403 /// | `lbl.a` | `a` | a column reports its name |
18404 /// | `'it''s'` | `it's` | a string reports its VALUE |
18405 /// | `a + b` | `a + b` | anything else, source text |
18406 ///
18407 /// The third is why this lives in the parser at all: the label is the
18408 /// text the client WROTE, down to the spacing, so it cannot be printed
18409 /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18410 ///
18411 /// Comments survive, and that is right: through a `mariadb` CLI both
18412 /// servers answer `a + b` for `SELECT a /* c */ + b`, but that is the
18413 /// CLIENT stripping the comment before it sends. Asked over the raw
18414 /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18415 /// produces.
18416 fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18417 if !self.mysql_dialect {
18418 return None;
18419 }
18420 match expr {
18421 // A column already reports its own name downstream; naming it
18422 // again here would only re-state the qualifier the label drops.
18423 Expr::Column(_) => None,
18424 Expr::Literal(Literal::String(v)) => Some(v.clone()),
18425 _ => self.source_span(start_tok, end_tok).map(str::to_string),
18426 }
18427 }
18428
18429 /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18430 /// consumed VALUES keyword. Each row lowers to a constant SELECT
18431 /// with PG's default column1..columnN names; subsequent rows
18432 /// chain as UNION ALL peers. Shared by the FROM-position
18433 /// `( VALUES … )` arm and the top-level bare VALUES statement.
18434 fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
18435 let mut row_selects: Vec<SelectStatement> = Vec::new();
18436 loop {
18437 if !matches!(self.peek(), Token::LParen) {
18438 return Err(self.err(alloc::format!(
18439 "expected '(' to start a VALUES row, got {:?}",
18440 self.peek()
18441 )));
18442 }
18443 self.advance(); // (
18444 let mut items: Vec<SelectItem> = Vec::new();
18445 loop {
18446 let expr = self.parse_expr(0)?;
18447 items.push(SelectItem::Expr {
18448 expr,
18449 alias: Some(alloc::format!("column{}", items.len() + 1)),
18450 });
18451 match self.peek() {
18452 Token::Comma => {
18453 self.advance();
18454 }
18455 Token::RParen => break,
18456 other => {
18457 return Err(self.err(alloc::format!(
18458 "expected ',' or ')' in VALUES row, got {other:?}"
18459 )));
18460 }
18461 }
18462 }
18463 self.advance(); // )
18464 row_selects.push(SelectStatement {
18465 locking: None,
18466 ctes: Vec::new(),
18467 distinct: false,
18468 distinct_on: Vec::new(),
18469 items,
18470 from: None,
18471 where_: None,
18472 group_by: None,
18473 group_by_all: false,
18474 having: None,
18475 unions: Vec::new(),
18476 order_by: Vec::new(),
18477 limit: None,
18478 offset: None,
18479 limit_with_ties: false,
18480 window_check_exprs: Vec::new(),
18481 });
18482 if matches!(self.peek(), Token::Comma) {
18483 self.advance();
18484 continue;
18485 }
18486 break;
18487 }
18488 let mut head = row_selects.remove(0);
18489 head.unions = row_selects
18490 .into_iter()
18491 .map(|s| (UnionKind::All, s))
18492 .collect();
18493 Ok(head)
18494 }
18495
18496 fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
18497 // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
18498 // children. It was read as a table NAMED `only`, so the query
18499 // failed on `relation "only" does not exist`.
18500 //
18501 // v7.39 (round 644) — and it is no longer a no-op. Round 621
18502 // absorbed the keyword, reasoning that SPG's children are
18503 // separate relations a plain scan does not descend into, so ONLY
18504 // already described the scan. That stopped being true when a
18505 // partition parent started unioning its children: measured,
18506 // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
18507 // where PG answers 0. The flag is carried now.
18508 let mut only = false;
18509 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
18510 && matches!(
18511 self.tokens.get(self.pos + 1),
18512 Some(Token::Ident(_) | Token::QuotedIdent(_))
18513 )
18514 {
18515 only = true;
18516 self.advance();
18517 }
18518 // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
18519 // for these SRFs the keyword is noise at parse time: the
18520 // join executor already substitutes outer-column references
18521 // into unnest_expr / generate_series_args per outer row
18522 // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
18523 // licences the correlation even without the keyword. Absorb
18524 // it and fall through to the SRF arms below.
18525 // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
18526 // just the four builtin SRFs: a user set-returning function on a JOIN's
18527 // right side is the whole point of LATERAL. The keyword stays noise at
18528 // parse time — the join executor substitutes the outer row into the
18529 // call's arguments per outer row.
18530 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18531 && matches!(
18532 self.tokens.get(self.pos + 1),
18533 // The json_each family has its OWN `LATERAL …` arm below, which
18534 // needs to see the keyword — absorbing it here would send those
18535 // calls down the generic table-function channel instead.
18536 Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
18537 )
18538 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18539 {
18540 self.advance(); // LATERAL
18541 }
18542 // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
18543 // set-returning function whose argument may reference a
18544 // preceding FROM item. We rewrite this to
18545 // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
18546 // AS __srf__) AS <alias>` so the existing LATERAL subquery
18547 // executor handles per-outer-row evaluation and the
18548 // SRF-primary jsonb_each_text path handles the inner
18549 // materialisation. Sentori 0067 backfill is the dogfood
18550 // shape.
18551 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18552 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
18553 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18554 {
18555 self.advance(); // LATERAL
18556 let each_fn = match self.peek() {
18557 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18558 _ => unreachable!(),
18559 };
18560 self.advance(); // jsonb_each[_text] / json_each[_text]
18561 self.advance(); // (
18562 let arg = self.parse_expr(0)?;
18563 if !matches!(self.peek(), Token::RParen) {
18564 return Err(self.err(alloc::format!(
18565 "expected ')' after LATERAL {each_fn}() argument, got {:?}",
18566 self.peek()
18567 )));
18568 }
18569 self.advance();
18570 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18571 let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18572 // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
18573 // FROM jsonb_each_text(<arg>) AS __srf__
18574 // PG's `AS kv(key, value)` column-alias list maps
18575 // positions to names; default to (key, value) when
18576 // omitted (matching the SRF's natural column names).
18577 let srf_alias = "__srf__".to_string();
18578 let key_alias = column_aliases
18579 .first()
18580 .cloned()
18581 .unwrap_or_else(|| "key".to_string());
18582 let value_alias = column_aliases
18583 .get(1)
18584 .cloned()
18585 .unwrap_or_else(|| "value".to_string());
18586 let inner_select = crate::ast::SelectStatement {
18587 locking: None,
18588 ctes: Vec::new(),
18589 distinct: false,
18590 distinct_on: Vec::new(),
18591 items: alloc::vec![
18592 crate::ast::SelectItem::Expr {
18593 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18594 qualifier: Some(srf_alias.clone()),
18595 name: "key".to_string(),
18596 }),
18597 alias: Some(key_alias),
18598 },
18599 crate::ast::SelectItem::Expr {
18600 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18601 qualifier: Some(srf_alias.clone()),
18602 name: "value".to_string(),
18603 }),
18604 alias: Some(value_alias),
18605 },
18606 ],
18607 from: Some(crate::ast::FromClause {
18608 primary: TableRef {
18609 name: srf_alias.clone(),
18610 alias: Some(srf_alias.clone()),
18611 only: false,
18612 as_of_segment: None,
18613 unnest_expr: None,
18614 unnest_column_aliases: Vec::new(),
18615 with_ordinality: false,
18616 generate_series_args: None,
18617 lateral_subquery: None,
18618 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18619 table_fn_call: None,
18620 rows_from: None,
18621 json_table: None,
18622 scalar_fn_item: false,
18623 },
18624 joins: Vec::new(),
18625 }),
18626 where_: None,
18627 group_by: None,
18628 group_by_all: false,
18629 having: None,
18630 unions: Vec::new(),
18631 order_by: Vec::new(),
18632 limit: None,
18633 offset: None,
18634 limit_with_ties: false,
18635 window_check_exprs: Vec::new(),
18636 };
18637 return Ok(TableRef {
18638 name: alias.clone(),
18639 alias: Some(alias),
18640 only: false,
18641 as_of_segment: None,
18642 unnest_expr: None,
18643 unnest_column_aliases: Vec::new(),
18644 with_ordinality: false,
18645 generate_series_args: None,
18646 lateral_subquery: Some(Box::new(inner_select)),
18647 jsonb_each_text_arg: None,
18648 table_fn_call: None,
18649 rows_from: None,
18650 json_table: None,
18651 scalar_fn_item: false,
18652 });
18653 }
18654 // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
18655 // without an explicit `LATERAL` keyword is the same shape
18656 // PG accepts (SRF naturally licences lateral correlation).
18657 // We mirror the LATERAL rewrite when the argument syntactic-
18658 // ally references an outer column (Column { qualifier:
18659 // Some(_), … }). For simplicity we apply the rewrite
18660 // whenever the SRF directly follows JOIN/CROSS JOIN/comma
18661 // in the FROM-list — caller-side join parsing positions
18662 // this peek correctly.
18663 // (Implementation note: detection lives below; the LATERAL
18664 // branch above already covers the explicit form; the bare
18665 // form falls through to the plain SRF arm and the engine
18666 // treats it as a constant-arg SRF if no outer reference is
18667 // present.)
18668 // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
18669 // table. Detect at the head so it claims precedence over
18670 // every other table-ref shape (unnest / generate_series /
18671 // bare ident); the lateral subquery itself follows the
18672 // regular SELECT grammar.
18673 // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
18674 // t(cols)`. Each row lowers to a constant SELECT with PG's
18675 // default column1..columnN names; subsequent rows chain as
18676 // UNION ALL peers. The result rides the derived-table
18677 // lateral_subquery channel — zero executor work.
18678 if matches!(self.peek(), Token::LParen)
18679 && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
18680 {
18681 self.advance(); // (
18682 self.advance(); // VALUES
18683 let head = self.parse_values_rows_body()?;
18684 if !matches!(self.peek(), Token::RParen) {
18685 return Err(self.err(alloc::format!(
18686 "expected ')' after VALUES list, got {:?}",
18687 self.peek()
18688 )));
18689 }
18690 self.advance();
18691 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18692 let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
18693 return Ok(TableRef {
18694 name,
18695 alias: alias_ident,
18696 only: false,
18697 as_of_segment: None,
18698 unnest_expr: None,
18699 unnest_column_aliases: column_aliases,
18700 with_ordinality: false,
18701 generate_series_args: None,
18702 lateral_subquery: Some(Box::new(head)),
18703 jsonb_each_text_arg: None,
18704 table_fn_call: None,
18705 rows_from: None,
18706 json_table: None,
18707 scalar_fn_item: false,
18708 });
18709 }
18710 // v7.37.17 (17.6 siblings) — plain derived table:
18711 // `FROM ( SELECT … ) [AS] alias`. Rides the same
18712 // lateral_subquery channel the explicit LATERAL form uses —
18713 // an uncorrelated inner SELECT executes identically. The
18714 // inner parse carries UNION tails (they live on
18715 // SelectStatement.unions).
18716 // v7.37 D.20 — the derived-table inner may itself be a
18717 // parenthesized set-operation group (`FROM ((SELECT…) UNION
18718 // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
18719 // bare `(SELECT …)`. parse_one_statement already routes a leading
18720 // `(` set-op group (its LParen arm) and a leading WITH
18721 // (parse_with_cte_then_select), so widen the second-token gate to
18722 // Select | LParen | WITH.
18723 // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
18724 // PG's spelling of `SELECT * FROM t` and is accepted wherever a
18725 // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
18726 // has existed since the shorthand landed and `parse_bare_select`
18727 // already routes it ("valid anywhere a SELECT head is"); what was
18728 // missing is this second-token gate, and the CTE body's dispatch
18729 // below. Round 868 found both by putting the shorthand in a
18730 // subquery — the top-level forms had been the only ones tested.
18731 if matches!(self.peek(), Token::LParen)
18732 && (matches!(
18733 self.tokens.get(self.pos + 1),
18734 Some(Token::Select | Token::LParen | Token::Table)
18735 ) || matches!(self.tokens.get(self.pos + 1),
18736 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
18737 {
18738 self.advance(); // (
18739 let inner = match self.parse_one_statement()? {
18740 Statement::Select(s) => s,
18741 other => {
18742 return Err(self.err(alloc::format!(
18743 "expected SELECT inside derived table ( … ), got {other:?}"
18744 )));
18745 }
18746 };
18747 if !matches!(self.peek(), Token::RParen) {
18748 return Err(self.err(alloc::format!(
18749 "expected ')' after derived-table subquery, got {:?}",
18750 self.peek()
18751 )));
18752 }
18753 self.advance();
18754 // `AS t(a, b)` column-alias list rides the
18755 // unnest_column_aliases field (same positional-rename
18756 // contract the unnest SRFs use).
18757 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18758 let name = alias_ident
18759 .clone()
18760 .unwrap_or_else(|| "subquery".to_string());
18761 return Ok(TableRef {
18762 name,
18763 alias: alias_ident,
18764 only: false,
18765 as_of_segment: None,
18766 unnest_expr: None,
18767 unnest_column_aliases: column_aliases,
18768 with_ordinality: false,
18769 generate_series_args: None,
18770 lateral_subquery: Some(Box::new(inner)),
18771 jsonb_each_text_arg: None,
18772 table_fn_call: None,
18773 rows_from: None,
18774 json_table: None,
18775 scalar_fn_item: false,
18776 });
18777 }
18778 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18779 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18780 {
18781 self.advance(); // LATERAL
18782 self.advance(); // (
18783 // Parse the inner SELECT.
18784 let inner = match self.parse_one_statement()? {
18785 Statement::Select(s) => s,
18786 other => {
18787 return Err(self.err(alloc::format!(
18788 "expected SELECT inside LATERAL ( … ), got {other:?}"
18789 )));
18790 }
18791 };
18792 if !matches!(self.peek(), Token::RParen) {
18793 return Err(self.err(alloc::format!(
18794 "expected ')' after LATERAL subquery, got {:?}",
18795 self.peek()
18796 )));
18797 }
18798 self.advance();
18799 // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
18800 // `(VALUES …) t(g)` derived table round-trips through view-body
18801 // Display, which renders on the lateral_subquery channel).
18802 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18803 let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
18804 return Ok(TableRef {
18805 name,
18806 alias: alias_ident,
18807 only: false,
18808 as_of_segment: None,
18809 unnest_expr: None,
18810 unnest_column_aliases: column_aliases,
18811 with_ordinality: false,
18812 generate_series_args: None,
18813 lateral_subquery: Some(Box::new(inner)),
18814 jsonb_each_text_arg: None,
18815 table_fn_call: None,
18816 rows_from: None,
18817 json_table: None,
18818 scalar_fn_item: false,
18819 });
18820 }
18821 // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
18822 // function as a FROM item. Emits one row per (key, value)
18823 // pair in the JSONB object argument as TEXT columns. May
18824 // be wrapped in CROSS JOIN LATERAL when the argument
18825 // references a preceding FROM item (sentori migration
18826 // 0067 backfill shape: `CROSS JOIN LATERAL
18827 // jsonb_each_text(t.json_col) AS kv(key, value)`).
18828 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
18829 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18830 {
18831 let each_fn = match self.peek() {
18832 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18833 _ => unreachable!(),
18834 };
18835 self.advance(); // jsonb_each[_text] / json_each[_text]
18836 self.advance(); // (
18837 let arg = self.parse_expr(0)?;
18838 if !matches!(self.peek(), Token::RParen) {
18839 return Err(self.err(alloc::format!(
18840 "expected ')' after {each_fn}() argument, got {:?}",
18841 self.peek()
18842 )));
18843 }
18844 self.advance();
18845 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18846 let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18847 return Ok(TableRef {
18848 name,
18849 alias: alias_ident,
18850 only: false,
18851 as_of_segment: None,
18852 unnest_expr: None,
18853 // `AS t(k, v)` renames key/value positionally, same as the
18854 // LATERAL-position form already does.
18855 unnest_column_aliases: column_aliases,
18856 with_ordinality: false,
18857 generate_series_args: None,
18858 lateral_subquery: None,
18859 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18860 table_fn_call: None,
18861 rows_from: None,
18862 json_table: None,
18863 scalar_fn_item: false,
18864 });
18865 }
18866 // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
18867 // (+ json_ variants) — record-returning JSON functions with a
18868 // column-definition list. Desugar to a derived table that
18869 // projects each declared column from the JSON via `->>` + a cast,
18870 // over `jsonb_array_elements(J)` for the *set (per-element) form.
18871 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
18872 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18873 {
18874 return self.parse_json_to_record_from();
18875 }
18876 // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
18877 // row is a text[] of capture groups, so it cannot desugar to unnest
18878 // (that would flatten the array). Wrap it as a derived table
18879 // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
18880 // SRF path already emits one text[] row per match. PG names the column
18881 // `regexp_matches`; an `AS a(col)` alias overrides it.
18882 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18883 if s.eq_ignore_ascii_case("regexp_matches"))
18884 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18885 {
18886 self.advance(); // fn name
18887 self.advance(); // (
18888 let mut fn_args: Vec<Expr> = Vec::new();
18889 loop {
18890 fn_args.push(self.parse_expr(0)?);
18891 if matches!(self.peek(), Token::Comma) {
18892 self.advance();
18893 continue;
18894 }
18895 break;
18896 }
18897 if !matches!(self.peek(), Token::RParen) {
18898 return Err(self.err(alloc::format!(
18899 "expected ')' after regexp_matches() arguments, got {:?}",
18900 self.peek()
18901 )));
18902 }
18903 self.advance();
18904 // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
18905 // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
18906 // it, so it died on the `with` token while every other table function
18907 // accepted it.
18908 let with_ordinality = self.absorb_with_ordinality();
18909 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18910 let table_alias = alias_ident
18911 .clone()
18912 .unwrap_or_else(|| "regexp_matches".to_string());
18913 // PG names a single-column function's output column after the ALIAS
18914 // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
18915 // `m` reads as that column and not as a whole-row composite. Naming
18916 // it after the function regardless made `SELECT m[1] FROM … AS m`
18917 // subscript a record.
18918 let col_name = column_aliases
18919 .first()
18920 .cloned()
18921 .or_else(|| alias_ident.clone())
18922 .unwrap_or_else(|| "regexp_matches".to_string());
18923 let inner = crate::ast::SelectStatement {
18924 locking: None,
18925 ctes: Vec::new(),
18926 distinct: false,
18927 distinct_on: Vec::new(),
18928 items: alloc::vec![SelectItem::Expr {
18929 expr: Expr::FunctionCall {
18930 name: "regexp_matches".to_string(),
18931 args: fn_args,
18932 },
18933 alias: Some(col_name),
18934 }],
18935 from: None,
18936 where_: None,
18937 group_by: None,
18938 group_by_all: false,
18939 having: None,
18940 unions: Vec::new(),
18941 order_by: Vec::new(),
18942 limit: None,
18943 offset: None,
18944 limit_with_ties: false,
18945 window_check_exprs: Vec::new(),
18946 };
18947 return Ok(TableRef {
18948 name: table_alias.clone(),
18949 alias: Some(table_alias),
18950 only: false,
18951 as_of_segment: None,
18952 unnest_expr: None,
18953 unnest_column_aliases: column_aliases,
18954 with_ordinality,
18955 generate_series_args: None,
18956 lateral_subquery: Some(Box::new(inner)),
18957 jsonb_each_text_arg: None,
18958 table_fn_call: None,
18959 rows_from: None,
18960 json_table: None,
18961 // regexp_matches returns text[], a base type: `SELECT m FROM
18962 // regexp_matches(…) AS m` is the array, not a composite wrapping it.
18963 scalar_fn_item: true,
18964 });
18965 }
18966 // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
18967 // / json_ variants as a FROM item. Rewritten into
18968 // `unnest(<same fn>(<expr>))`: the scalar form returns the
18969 // elements as a TEXT array, and the existing unnest SRF path
18970 // materialises one row per element. PG's natural column name
18971 // is `value`; an `AS a(col)` column-alias list overrides it.
18972 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18973 if s.eq_ignore_ascii_case("jsonb_array_elements")
18974 || s.eq_ignore_ascii_case("json_array_elements")
18975 || s.eq_ignore_ascii_case("jsonb_array_elements_text")
18976 || s.eq_ignore_ascii_case("json_array_elements_text")
18977 || s.eq_ignore_ascii_case("jsonb_object_keys")
18978 || s.eq_ignore_ascii_case("json_object_keys")
18979 || s.eq_ignore_ascii_case("jsonb_path_query")
18980 || s.eq_ignore_ascii_case("json_path_query")
18981 || s.eq_ignore_ascii_case("generate_subscripts")
18982 || s.eq_ignore_ascii_case("string_to_table")
18983 || s.eq_ignore_ascii_case("regexp_split_to_table"))
18984 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18985 {
18986 let fn_name = match self.peek() {
18987 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18988 _ => unreachable!(),
18989 };
18990 self.advance(); // fn name
18991 self.advance(); // (
18992 let mut fn_args: Vec<Expr> = Vec::new();
18993 loop {
18994 fn_args.push(self.parse_expr(0)?);
18995 if matches!(self.peek(), Token::Comma) {
18996 self.advance();
18997 continue;
18998 }
18999 break;
19000 }
19001 if !matches!(self.peek(), Token::RParen) {
19002 return Err(self.err(alloc::format!(
19003 "expected ')' after {fn_name}() arguments, got {:?}",
19004 self.peek()
19005 )));
19006 }
19007 self.advance();
19008 let with_ordinality = self.absorb_with_ordinality();
19009 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19010 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
19011 // PG's natural column name: the array-elements SRFs
19012 // declare an OUT parameter `value`; jsonb_object_keys
19013 // and generate_subscripts have none, so the column is
19014 // named after the function. A bare table alias on a
19015 // single-column SRF renames the column too (PG: `FROM
19016 // generate_subscripts(a, 1) AS s` projects column s) —
19017 // except for the OUT-parameter SRFs, whose column stays
19018 // `value` under a bare alias.
19019 let natural_col = if fn_name.ends_with("_array_elements")
19020 || fn_name.ends_with("_array_elements_text")
19021 {
19022 "value".to_string()
19023 } else {
19024 alias_ident.clone().unwrap_or_else(|| fn_name.clone())
19025 };
19026 let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
19027 // Keep any further entries — the second names the
19028 // ordinality column under WITH ORDINALITY.
19029 srf_cols.extend(column_aliases.into_iter().skip(1));
19030 // The *_to_table SRFs are row-streams over the existing
19031 // *_to_array scalars — map the call target; the display
19032 // name (alias / column defaults) keeps the SRF spelling.
19033 let call_name = match fn_name.as_str() {
19034 "string_to_table" => "string_to_array".to_string(),
19035 "regexp_split_to_table" => "regexp_split_to_array".to_string(),
19036 _ => fn_name,
19037 };
19038 // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
19039 // preceding FROM item (bare or qualified column) is correlated;
19040 // route it through the per-outer-row lateral channel.
19041 let expr = crate::ast::Expr::FunctionCall {
19042 name: call_name,
19043 args: fn_args,
19044 };
19045 let correlated = Self::expr_has_any_column(&expr);
19046 let tref = TableRef {
19047 name,
19048 alias: alias_ident,
19049 only: false,
19050 as_of_segment: None,
19051 unnest_expr: Some(Box::new(expr)),
19052 unnest_column_aliases: srf_cols,
19053 with_ordinality,
19054 generate_series_args: None,
19055 lateral_subquery: None,
19056 jsonb_each_text_arg: None,
19057 table_fn_call: None,
19058 rows_from: None,
19059 json_table: None,
19060 // Each of these returns a BASE type (jsonb / text / int), so the item's
19061 // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
19062 // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
19063 scalar_fn_item: !with_ordinality,
19064 };
19065 return Ok(if correlated {
19066 Self::wrap_correlated_srf(tref)
19067 } else {
19068 tref
19069 });
19070 }
19071 // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
19072 // explicit parallel-zip syntax. Each entry lowers to its
19073 // array-returning scalar form (unnest(x) → x itself; the
19074 // FROM-SRF rewrite family → their scalar array calls) and
19075 // the list rides the multi-arg unnest zip channel:
19076 // NULL-padded to the longest, WITH ORDINALITY appends the
19077 // counter. generate_series has no scalar array form and
19078 // errors honestly.
19079 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
19080 && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
19081 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19082 {
19083 self.advance(); // ROWS
19084 self.advance(); // FROM
19085 self.advance(); // (
19086 let mut entries: Vec<Expr> = Vec::new();
19087 // v7.39 (read01 round 74) — the generic channel, filled in parallel.
19088 // Used only when some entry has no array form.
19089 let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
19090 loop {
19091 let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
19092 if !matches!(self.peek(), Token::LParen) {
19093 return Err(self.err(alloc::format!(
19094 "expected '(' after {fn_name} in ROWS FROM, got {:?}",
19095 self.peek()
19096 )));
19097 }
19098 self.advance();
19099 let mut fn_args: Vec<Expr> = Vec::new();
19100 if !matches!(self.peek(), Token::RParen) {
19101 loop {
19102 fn_args.push(self.parse_expr(0)?);
19103 if matches!(self.peek(), Token::Comma) {
19104 self.advance();
19105 continue;
19106 }
19107 break;
19108 }
19109 }
19110 if !matches!(self.peek(), Token::RParen) {
19111 return Err(self.err(alloc::format!(
19112 "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
19113 self.peek()
19114 )));
19115 }
19116 self.advance();
19117 let entry = match fn_name.as_str() {
19118 "unnest" => {
19119 if fn_args.len() != 1 {
19120 return Err(
19121 self.err("unnest inside ROWS FROM takes exactly one array".into())
19122 );
19123 }
19124 fn_args.pop().expect("len checked")
19125 }
19126 "jsonb_array_elements"
19127 | "json_array_elements"
19128 | "jsonb_array_elements_text"
19129 | "json_array_elements_text"
19130 | "jsonb_object_keys"
19131 | "json_object_keys"
19132 | "generate_subscripts" => crate::ast::Expr::FunctionCall {
19133 name: fn_name,
19134 args: fn_args,
19135 },
19136 "string_to_table" => crate::ast::Expr::FunctionCall {
19137 name: "string_to_array".to_string(),
19138 args: fn_args,
19139 },
19140 "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
19141 name: "regexp_split_to_array".to_string(),
19142 args: fn_args,
19143 },
19144 // v7.39 (read01 round 74) — an SRF with no array form
19145 // (`generate_series`, a user `RETURNS SETOF` function) has no
19146 // scalar expression to zip, so the WHOLE list switches to the
19147 // rows_from channel, which runs each function and zips the
19148 // rows themselves. The all-array case keeps the old lowering:
19149 // it is well-trodden and this must not disturb it.
19150 _ => {
19151 generic.push((fn_name, fn_args));
19152 if matches!(self.peek(), Token::Comma) {
19153 self.advance();
19154 continue;
19155 }
19156 break;
19157 }
19158 };
19159 generic.push((
19160 // The array-able entries carry their lowered expr along, so a
19161 // MIXED list still works: the engine sees the scalar array
19162 // form and unnests it.
19163 "__array".to_string(),
19164 alloc::vec![entry.clone()],
19165 ));
19166 entries.push(entry);
19167 if matches!(self.peek(), Token::Comma) {
19168 self.advance();
19169 continue;
19170 }
19171 break;
19172 }
19173 if !matches!(self.peek(), Token::RParen) {
19174 return Err(self.err(alloc::format!(
19175 "expected ')' to close ROWS FROM, got {:?}",
19176 self.peek()
19177 )));
19178 }
19179 self.advance();
19180 let with_ordinality = self.absorb_with_ordinality();
19181 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19182 let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19183 // v7.39 (read01 round 74) — some entry had no array form, so the whole
19184 // list rides the generic channel.
19185 if generic.iter().any(|(n, _)| n != "__array") {
19186 let correlated = generic
19187 .iter()
19188 .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19189 let tref = TableRef {
19190 name,
19191 alias: alias_ident,
19192 only: false,
19193 as_of_segment: None,
19194 unnest_expr: None,
19195 unnest_column_aliases,
19196 with_ordinality,
19197 generate_series_args: None,
19198 lateral_subquery: None,
19199 jsonb_each_text_arg: None,
19200 table_fn_call: None,
19201 rows_from: Some(generic),
19202 json_table: None,
19203 scalar_fn_item: false,
19204 };
19205 return Ok(if correlated {
19206 Self::wrap_correlated_srf(tref)
19207 } else {
19208 tref
19209 });
19210 }
19211 let correlated = entries.iter().any(Self::expr_has_any_column);
19212 let expr = if entries.len() == 1 {
19213 entries.pop().expect("len checked")
19214 } else {
19215 crate::ast::Expr::FunctionCall {
19216 name: "__unnest_zip".to_string(),
19217 args: entries,
19218 }
19219 };
19220 let tref = TableRef {
19221 name,
19222 alias: alias_ident,
19223 only: false,
19224 as_of_segment: None,
19225 unnest_expr: Some(Box::new(expr)),
19226 unnest_column_aliases,
19227 with_ordinality,
19228 generate_series_args: None,
19229 lateral_subquery: None,
19230 jsonb_each_text_arg: None,
19231 table_fn_call: None,
19232 rows_from: None,
19233 json_table: None,
19234 scalar_fn_item: false,
19235 };
19236 return Ok(if correlated {
19237 Self::wrap_correlated_srf(tref)
19238 } else {
19239 tref
19240 });
19241 }
19242 // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19243 // source. Detect at the head before the bare-ident fallback;
19244 // unnest is not a reserved token.
19245 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19246 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19247 {
19248 self.advance(); // unnest
19249 self.advance(); // (
19250 let mut srf_args = alloc::vec![self.parse_expr(0)?];
19251 while matches!(self.peek(), Token::Comma) {
19252 self.advance();
19253 srf_args.push(self.parse_expr(0)?);
19254 }
19255 if !matches!(self.peek(), Token::RParen) {
19256 return Err(self.err(alloc::format!(
19257 "expected ')' after unnest() argument, got {:?}",
19258 self.peek()
19259 )));
19260 }
19261 self.advance();
19262 // Multi-arg unnest(a, b, …) zips the arrays in
19263 // parallel, NULL-padding to the longest (PG's ROWS
19264 // FROM shorthand). Lower onto the unnest channel as an
19265 // internal marker call the executors unpack.
19266 let expr = if srf_args.len() == 1 {
19267 srf_args.pop().expect("len checked")
19268 } else {
19269 crate::ast::Expr::FunctionCall {
19270 name: "__unnest_zip".to_string(),
19271 args: srf_args,
19272 }
19273 };
19274 let with_ordinality = self.absorb_with_ordinality();
19275 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19276 let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19277 let correlated = Self::expr_has_any_column(&expr);
19278 let tref = TableRef {
19279 name,
19280 alias: alias_ident,
19281 only: false,
19282 as_of_segment: None,
19283 unnest_expr: Some(Box::new(expr)),
19284 unnest_column_aliases,
19285 with_ordinality,
19286 generate_series_args: None,
19287 lateral_subquery: None,
19288 jsonb_each_text_arg: None,
19289 table_fn_call: None,
19290 rows_from: None,
19291 json_table: None,
19292 scalar_fn_item: false,
19293 };
19294 return Ok(if correlated {
19295 Self::wrap_correlated_srf(tref)
19296 } else {
19297 tref
19298 });
19299 }
19300 // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19301 // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19302 // generic table-fn arg parser can't read), so it is intercepted
19303 // here BEFORE the generic dispatch. The doc expr may reference
19304 // outer columns (implicit LATERAL) — same correlated-wrap rule.
19305 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19306 if s.eq_ignore_ascii_case("json_table"))
19307 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19308 {
19309 let tref = self.parse_json_table_ref()?;
19310 let correlated = tref
19311 .json_table
19312 .as_deref()
19313 .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19314 return Ok(if correlated {
19315 Self::wrap_correlated_srf(tref)
19316 } else {
19317 tref
19318 });
19319 }
19320 // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19321 // functions dispatched by name (`pg_partition_tree('t')`,
19322 // `pg_partition_ancestors('t')`). Same head-detection shape as
19323 // unnest; the engine executor owns the row shape per function.
19324 // v7.39 (read01 round 65) — and a USER function in FROM position
19325 // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19326 // (generate_series / unnest / the json_each family) keep it — their arms
19327 // sit further down, so they are excluded here by name rather than by
19328 // ordering. Anything else that is an ident followed by `(` is a table
19329 // function; the engine executor decides whether it is a builtin, a
19330 // set-returning user function, or an error.
19331 // 7.38.1 S5.1 — pg_dump spells its table functions
19332 // schema-qualified (`pg_catalog.pg_options_to_table(...)`);
19333 // strip the pg_catalog prefix here so the same head-detection
19334 // fires. Only pg_catalog: a user schema's `s.f(x)` keeps its
19335 // meaning.
19336 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
19337 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
19338 && matches!(
19339 self.tokens.get(self.pos + 2),
19340 Some(Token::Ident(_) | Token::QuotedIdent(_))
19341 )
19342 && matches!(self.tokens.get(self.pos + 3), Some(Token::LParen))
19343 {
19344 self.advance(); // pg_catalog
19345 self.advance(); // .
19346 }
19347 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19348 if !s.eq_ignore_ascii_case("generate_series")
19349 && !s.eq_ignore_ascii_case("unnest")
19350 && !is_json_each_name(s))
19351 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19352 {
19353 // Body out-of-line — this parse sits on the FROM/subquery
19354 // recursion chain (debug frame-cliff discipline).
19355 // v7.39 (read01 round 69) — a call whose arguments reference an outer
19356 // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19357 // outer row, so it rides the lateral channel. Same rule the unnest
19358 // arm uses.
19359 let tref = self.parse_table_fn_ref()?;
19360 let correlated = tref
19361 .table_fn_call
19362 .as_deref()
19363 .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19364 return Ok(if correlated {
19365 Self::wrap_correlated_srf(tref)
19366 } else {
19367 tref
19368 });
19369 }
19370 // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19371 // [, step])` set-returning source. Same shape as unnest:
19372 // detect at the head, parse the comma-separated arg list,
19373 // dispatch downstream through the engine's set-returning
19374 // path. Supports integer triplets (mailrs's `WITH row_no AS
19375 // (SELECT * FROM generate_series(1, N))` pattern) and
19376 // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19377 // date-range iteration pattern, which pre-3.10 had no
19378 // direct equivalent in SPG).
19379 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19380 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19381 {
19382 self.advance(); // generate_series
19383 self.advance(); // (
19384 let mut args: Vec<Expr> = Vec::new();
19385 loop {
19386 args.push(self.parse_expr(0)?);
19387 if matches!(self.peek(), Token::Comma) {
19388 self.advance();
19389 continue;
19390 }
19391 break;
19392 }
19393 if !matches!(self.peek(), Token::RParen) {
19394 return Err(self.err(alloc::format!(
19395 "expected ')' after generate_series() arguments, got {:?}",
19396 self.peek()
19397 )));
19398 }
19399 self.advance();
19400 if args.len() < 2 || args.len() > 3 {
19401 return Err(self.err(alloc::format!(
19402 "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19403 args.len()
19404 )));
19405 }
19406 let with_ordinality = self.absorb_with_ordinality();
19407 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19408 let name = alias_ident
19409 .clone()
19410 .unwrap_or_else(|| "generate_series".to_string());
19411 let correlated = args.iter().any(Self::expr_has_any_column);
19412 let tref = TableRef {
19413 name,
19414 alias: alias_ident,
19415 only: false,
19416 as_of_segment: None,
19417 unnest_expr: None,
19418 unnest_column_aliases: column_aliases,
19419 with_ordinality,
19420 generate_series_args: Some(args),
19421 lateral_subquery: None,
19422 jsonb_each_text_arg: None,
19423 table_fn_call: None,
19424 rows_from: None,
19425 json_table: None,
19426 scalar_fn_item: false,
19427 };
19428 return Ok(if correlated {
19429 Self::wrap_correlated_srf(tref)
19430 } else {
19431 tref
19432 });
19433 }
19434 // v7.16.2 — preserve information_schema / pg_catalog
19435 // qualifiers (mailrs round-10 A.3). The generic
19436 // `expect_ident_like` strip silently drops the schema;
19437 // we want the engine to recognise these PG meta tables
19438 // and synthesise rows from the live catalog. Produce a
19439 // synthetic name (`__spg_info_columns` etc.) so the
19440 // engine's SELECT-side router can dispatch without
19441 // clashing with any user-defined `columns` table.
19442 let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
19443 (synth, Some(orig))
19444 } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
19445 (synth, Some(orig))
19446 } else {
19447 (self.expect_ident_like()?, None)
19448 };
19449 // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
19450 // time-travel clause. Parse BEFORE the alias so the
19451 // alias can still ride at the tail (`tbl AS OF SEGMENT
19452 // '5' alias`). `AS` is a reserved keyword token, while
19453 // `OF` and `SEGMENT` are bare idents.
19454 let as_of_segment = if matches!(self.peek(), Token::As)
19455 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
19456 {
19457 self.advance(); // AS
19458 self.advance(); // OF
19459 let kw = match self.peek().clone() {
19460 Token::Ident(s) | Token::QuotedIdent(s) => s,
19461 other => {
19462 return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
19463 }
19464 };
19465 if !kw.eq_ignore_ascii_case("segment") {
19466 return Err(self.err(format!(
19467 "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
19468 )));
19469 }
19470 self.advance();
19471 // Segment id literal — accept either a string or
19472 // integer for operator ergonomics.
19473 let id = match self.advance() {
19474 Token::String(s) => s
19475 .parse::<u32>()
19476 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19477 Token::Integer(n) => u32::try_from(n)
19478 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19479 other => {
19480 return Err(self.err(format!(
19481 "expected segment id literal after AS OF SEGMENT, got {other:?}"
19482 )));
19483 }
19484 };
19485 Some(id)
19486 } else {
19487 None
19488 };
19489 // TABLESAMPLE is not a reserved token — keep the bare-ident
19490 // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
19491 let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
19492 {
19493 None
19494 } else {
19495 self.parse_optional_alias()?
19496 };
19497 // r1052 — a catalog name rewritten to its synthetic form keeps
19498 // the WRITTEN name as the relation's alias, so `pg_cast.oid`
19499 // still binds after `pg_cast` became `__spg_pg_cast`. PG
19500 // semantics: the visible name of `pg_catalog.pg_cast` IS
19501 // `pg_cast`. Without this, every table-name-qualified column
19502 // on a synthesised catalog answered "missing FROM-clause
19503 // entry" — which is the wall pg_dump hit on its first
19504 // pg_proc/pg_cast query.
19505 let alias = match (&alias, &meta_original) {
19506 (None, Some(orig)) if *orig != name => Some(orig.clone()),
19507 _ => alias,
19508 };
19509 // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
19510 // (PG grammar). BERNOULLI lowers to a per-row
19511 // `random() < p/100` conjunct on the enclosing SELECT's
19512 // WHERE — exact row-level Bernoulli semantics. SYSTEM
19513 // shares the lowering: SPG has no page structure to
19514 // sample, and the row-level form returns the same expected
19515 // fraction. REPEATABLE(seed) promises a deterministic
19516 // sample SPG cannot honour yet — honest error rather than
19517 // a silently ignored seed.
19518 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
19519 self.advance();
19520 let method = self.expect_ident_like()?;
19521 if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
19522 return Err(self.err(alloc::format!(
19523 "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
19524 )));
19525 }
19526 if !matches!(self.peek(), Token::LParen) {
19527 return Err(self.err(alloc::format!(
19528 "expected '(' after TABLESAMPLE {}, got {:?}",
19529 method.to_ascii_uppercase(),
19530 self.peek()
19531 )));
19532 }
19533 self.advance();
19534 let percent = self.parse_expr(0)?;
19535 if !matches!(self.peek(), Token::RParen) {
19536 return Err(self.err(alloc::format!(
19537 "expected ')' after TABLESAMPLE percentage, got {:?}",
19538 self.peek()
19539 )));
19540 }
19541 self.advance();
19542 // REPEATABLE(seed) → a deterministic per-row draw seeded by
19543 // `seed`, so the sample is stable across repeats and rescans.
19544 // Non-REPEATABLE keeps the non-deterministic `random()` draw.
19545 let mut sample_seed: Option<Expr> = None;
19546 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
19547 self.advance();
19548 if !matches!(self.peek(), Token::LParen) {
19549 return Err(self.err(alloc::format!(
19550 "expected '(' after REPEATABLE, got {:?}",
19551 self.peek()
19552 )));
19553 }
19554 self.advance();
19555 let seed = self.parse_expr(0)?;
19556 if !matches!(self.peek(), Token::RParen) {
19557 return Err(self.err(alloc::format!(
19558 "expected ')' after REPEATABLE seed, got {:?}",
19559 self.peek()
19560 )));
19561 }
19562 self.advance();
19563 sample_seed = Some(seed);
19564 }
19565 let draw = match sample_seed {
19566 Some(seed) => Expr::FunctionCall {
19567 name: "__tsm_fract".to_string(),
19568 args: alloc::vec![seed],
19569 },
19570 None => Expr::FunctionCall {
19571 name: "random".to_string(),
19572 args: Vec::new(),
19573 },
19574 };
19575 self.pending_sample_preds.push(Expr::Binary {
19576 lhs: Box::new(draw),
19577 op: crate::ast::BinOp::Lt,
19578 rhs: Box::new(Expr::Binary {
19579 lhs: Box::new(percent),
19580 op: crate::ast::BinOp::Div,
19581 rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
19582 }),
19583 });
19584 }
19585 Ok(TableRef {
19586 name,
19587 alias,
19588 only,
19589 as_of_segment,
19590 unnest_expr: None,
19591 unnest_column_aliases: Vec::new(),
19592 with_ordinality: false,
19593 generate_series_args: None,
19594 lateral_subquery: None,
19595 jsonb_each_text_arg: None,
19596 table_fn_call: None,
19597 rows_from: None,
19598 json_table: None,
19599 scalar_fn_item: false,
19600 })
19601 }
19602
19603 /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
19604 /// but also accepts `AS alias(col [, col, …])` — the
19605 /// PG-standard table-function column-list form. The column
19606 /// list is only honoured when paired with `UNNEST(...)` in
19607 /// the parent; other call sites currently discard it.
19608 /// True when the expression tree contains a qualified column
19609 /// reference (`t.col`) — the syntactic marker that an SRF
19610 /// argument correlates with a preceding FROM item.
19611 fn expr_has_qualified_column(e: &Expr) -> bool {
19612 match e {
19613 Expr::Column(c) => c.qualifier.is_some(),
19614 Expr::Binary { lhs, rhs, .. } => {
19615 Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
19616 }
19617 Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
19618 Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
19619 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
19620 Expr::Case {
19621 operand,
19622 branches,
19623 else_branch,
19624 } => {
19625 operand
19626 .as_deref()
19627 .is_some_and(Self::expr_has_qualified_column)
19628 || branches.iter().any(|(w, t)| {
19629 Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
19630 })
19631 || else_branch
19632 .as_deref()
19633 .is_some_and(Self::expr_has_qualified_column)
19634 }
19635 _ => false,
19636 }
19637 }
19638
19639 /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
19640 /// counts a bare (unqualified) column. A set-returning function has no
19641 /// input columns of its own, so ANY column in its arguments is an outer
19642 /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
19643 fn expr_has_any_column(e: &Expr) -> bool {
19644 match e {
19645 Expr::Column(_) => true,
19646 Expr::Binary { lhs, rhs, .. } => {
19647 Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
19648 }
19649 Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
19650 Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
19651 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
19652 // v7.39 (round 759, F31-B8b) — a column INSIDE an array
19653 // constructor or subscript fell to the `_ => false` arm, so
19654 // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
19655 // channel and the eager peer eval answered `column "x" does
19656 // not exist` (the substitution walker already recurses both
19657 // shapes; only this detector was blind to them).
19658 Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
19659 Expr::ArraySubscript { target, index } => {
19660 Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
19661 }
19662 Expr::Case {
19663 operand,
19664 branches,
19665 else_branch,
19666 } => {
19667 operand.as_deref().is_some_and(Self::expr_has_any_column)
19668 || branches
19669 .iter()
19670 .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
19671 || else_branch
19672 .as_deref()
19673 .is_some_and(Self::expr_has_any_column)
19674 }
19675 _ => false,
19676 }
19677 }
19678
19679 /// Wrap a correlated SRF table ref (`unnest(t.col)` /
19680 /// `generate_series(1, t.n)`) into the lateral_subquery
19681 /// channel: `SELECT * FROM <srf>` executes per outer row with
19682 /// outer references substituted (v7.37.43-T4.5 machinery).
19683 /// Uncorrelated SRFs stay on their plain channels.
19684 fn wrap_correlated_srf(srf: TableRef) -> TableRef {
19685 let name = srf.name.clone();
19686 let alias = srf.alias.clone();
19687 let inner = crate::ast::SelectStatement {
19688 locking: None,
19689 ctes: Vec::new(),
19690 distinct: false,
19691 distinct_on: Vec::new(),
19692 items: alloc::vec![crate::ast::SelectItem::Wildcard],
19693 from: Some(crate::ast::FromClause {
19694 primary: srf,
19695 joins: Vec::new(),
19696 }),
19697 where_: None,
19698 group_by: None,
19699 group_by_all: false,
19700 having: None,
19701 unions: Vec::new(),
19702 order_by: Vec::new(),
19703 limit: None,
19704 offset: None,
19705 limit_with_ties: false,
19706 window_check_exprs: Vec::new(),
19707 };
19708 TableRef {
19709 name,
19710 alias,
19711 only: false,
19712 as_of_segment: None,
19713 unnest_expr: None,
19714 unnest_column_aliases: Vec::new(),
19715 with_ordinality: false,
19716 generate_series_args: None,
19717 lateral_subquery: Some(Box::new(inner)),
19718 jsonb_each_text_arg: None,
19719 table_fn_call: None,
19720 rows_from: None,
19721 json_table: None,
19722 scalar_fn_item: false,
19723 }
19724 }
19725
19726 /// True when the expression tree contains an unresolved
19727 /// `OVER w` marker (see parse_over_clause).
19728 fn expr_has_named_window(e: &Expr) -> bool {
19729 match e {
19730 Expr::WindowFunction { partition_by, .. } => matches!(
19731 partition_by.as_slice(),
19732 [Expr::Column(c)] if matches!(
19733 c.qualifier.as_deref(),
19734 Some("__named_window__") | Some("__named_window_ref__")
19735 )
19736 ),
19737 Expr::Binary { lhs, rhs, .. } => {
19738 Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
19739 }
19740 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
19741 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
19742 Expr::Case {
19743 operand,
19744 branches,
19745 else_branch,
19746 } => {
19747 operand.as_deref().is_some_and(Self::expr_has_named_window)
19748 || branches.iter().any(|(w, t)| {
19749 Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
19750 })
19751 || else_branch
19752 .as_deref()
19753 .is_some_and(Self::expr_has_named_window)
19754 }
19755 _ => false,
19756 }
19757 }
19758
19759 /// v7.39 (round 705) — the NAMES the expression references through the
19760 /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
19761 /// definitions nothing referenced. Traversal mirrors
19762 /// `expr_has_named_window` above.
19763 fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
19764 match e {
19765 Expr::WindowFunction { partition_by, .. } => {
19766 if let [Expr::Column(c)] = partition_by.as_slice()
19767 && matches!(
19768 c.qualifier.as_deref(),
19769 Some("__named_window__") | Some("__named_window_ref__")
19770 )
19771 {
19772 into.push(c.name.clone());
19773 }
19774 }
19775 Expr::Binary { lhs, rhs, .. } => {
19776 Self::collect_named_window_refs(lhs, into);
19777 Self::collect_named_window_refs(rhs, into);
19778 }
19779 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19780 Self::collect_named_window_refs(expr, into);
19781 }
19782 Expr::FunctionCall { args, .. } => {
19783 for a in args {
19784 Self::collect_named_window_refs(a, into);
19785 }
19786 }
19787 Expr::Case {
19788 operand,
19789 branches,
19790 else_branch,
19791 } => {
19792 if let Some(o) = operand.as_deref() {
19793 Self::collect_named_window_refs(o, into);
19794 }
19795 for (w, t) in branches {
19796 Self::collect_named_window_refs(w, into);
19797 Self::collect_named_window_refs(t, into);
19798 }
19799 if let Some(eb) = else_branch.as_deref() {
19800 Self::collect_named_window_refs(eb, into);
19801 }
19802 }
19803 _ => {}
19804 }
19805 }
19806
19807 /// Inline named-window definitions into the `OVER w` markers.
19808 /// An unknown name errors (PG: window "w" does not exist).
19809 #[allow(clippy::type_complexity)]
19810 fn substitute_named_windows(
19811 e: &mut Expr,
19812 defs: &[(
19813 String,
19814 (
19815 Vec<Expr>,
19816 Vec<(Expr, bool, Option<bool>)>,
19817 Option<WindowFrame>,
19818 ),
19819 )],
19820 ) -> Result<(), String> {
19821 match e {
19822 Expr::WindowFunction {
19823 partition_by,
19824 order_by,
19825 frame,
19826 ..
19827 } => {
19828 // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
19829 // from the bare `OVER w1` (a plain reference).
19830 let named = match partition_by.as_slice() {
19831 [Expr::Column(c)] => match c.qualifier.as_deref() {
19832 Some("__named_window__") => Some((c.name.clone(), false)),
19833 Some("__named_window_ref__") => Some((c.name.clone(), true)),
19834 _ => None,
19835 },
19836 _ => None,
19837 };
19838 if let Some((wname, is_copy)) = named {
19839 let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
19840 else {
19841 return Err(alloc::format!("window {wname:?} does not exist"));
19842 };
19843 if !is_copy {
19844 *partition_by = def.0.clone();
19845 *order_by = def.1.clone();
19846 *frame = def.2.clone();
19847 return Ok(());
19848 }
19849 // v7.39 (round 229) — PG's copy rules, probed against
19850 // 18.4: a copy inherits the partitioning, may supply an
19851 // ordering only when the base has none, and may not copy
19852 // a base that already carries a frame (its own frame
19853 // would be ambiguous with the inherited one).
19854 if !def.1.is_empty() && !order_by.is_empty() {
19855 return Err(alloc::format!(
19856 "cannot override ORDER BY clause of window \"{wname}\""
19857 ));
19858 }
19859 if def.2.is_some() {
19860 return Err(alloc::format!(
19861 "cannot copy window \"{wname}\" because it has a frame clause"
19862 ));
19863 }
19864 *partition_by = def.0.clone();
19865 if order_by.is_empty() {
19866 *order_by = def.1.clone();
19867 }
19868 }
19869 Ok(())
19870 }
19871 Expr::Binary { lhs, rhs, .. } => {
19872 Self::substitute_named_windows(lhs, defs)?;
19873 Self::substitute_named_windows(rhs, defs)
19874 }
19875 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19876 Self::substitute_named_windows(expr, defs)
19877 }
19878 Expr::FunctionCall { args, .. } => {
19879 for a in args {
19880 Self::substitute_named_windows(a, defs)?;
19881 }
19882 Ok(())
19883 }
19884 Expr::Case {
19885 operand,
19886 branches,
19887 else_branch,
19888 } => {
19889 if let Some(op) = operand {
19890 Self::substitute_named_windows(op, defs)?;
19891 }
19892 for (w, t) in branches {
19893 Self::substitute_named_windows(w, defs)?;
19894 Self::substitute_named_windows(t, defs)?;
19895 }
19896 if let Some(el) = else_branch {
19897 Self::substitute_named_windows(el, defs)?;
19898 }
19899 Ok(())
19900 }
19901 _ => Ok(()),
19902 }
19903 }
19904
19905 /// SQL-standard `TABLE name` shorthand — builds the equivalent
19906 /// `SELECT * FROM name` head. Callers own set-op chain / tail
19907 /// composition.
19908 fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
19909 debug_assert!(matches!(self.peek(), Token::Table));
19910 self.advance(); // TABLE
19911 let tname = self.expect_ident_like()?;
19912 Ok(SelectStatement {
19913 locking: None,
19914 ctes: Vec::new(),
19915 distinct: false,
19916 distinct_on: Vec::new(),
19917 items: alloc::vec![SelectItem::Wildcard],
19918 from: Some(FromClause {
19919 primary: TableRef {
19920 name: tname,
19921 alias: None,
19922 only: false,
19923 as_of_segment: None,
19924 unnest_expr: None,
19925 unnest_column_aliases: Vec::new(),
19926 with_ordinality: false,
19927 generate_series_args: None,
19928 lateral_subquery: None,
19929 jsonb_each_text_arg: None,
19930 table_fn_call: None,
19931 rows_from: None,
19932 json_table: None,
19933 scalar_fn_item: false,
19934 },
19935 joins: Vec::new(),
19936 }),
19937 where_: None,
19938 group_by: None,
19939 group_by_all: false,
19940 having: None,
19941 unions: Vec::new(),
19942 order_by: Vec::new(),
19943 limit: None,
19944 offset: None,
19945 limit_with_ties: false,
19946 window_check_exprs: Vec::new(),
19947 })
19948 }
19949
19950 /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
19951 /// variants) → a derived table that reads each declared column out of
19952 /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
19953 /// `jsonb_array_elements(J)` (one row per element, column `value`);
19954 /// the scalar *record form projects a single row straight off `J`.
19955 /// Rides the existing lateral-subquery channel, so no new executor or
19956 /// AST is needed.
19957 fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
19958 use crate::ast::{
19959 BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
19960 };
19961 let fn_name = match self.peek() {
19962 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19963 _ => unreachable!("caller guarded is_json_to_record_name"),
19964 };
19965 self.advance(); // fn name
19966 self.advance(); // (
19967 let mut arg = self.parse_expr(0)?;
19968 // populate_record(base, json): the base only carries the record
19969 // type here — the JSON argument is the second expression.
19970 let mut base: Option<Expr> = None;
19971 if matches!(self.peek(), Token::Comma) {
19972 self.advance();
19973 base = Some(arg);
19974 arg = self.parse_expr(0)?;
19975 }
19976 if !matches!(self.peek(), Token::RParen) {
19977 return Err(self.err(alloc::format!(
19978 "expected ')' after {fn_name}() argument, got {:?}",
19979 self.peek()
19980 )));
19981 }
19982 self.advance(); // )
19983 let is_set = fn_name.ends_with("recordset");
19984 // `[AS] alias ( col type [, …] )` column-definition list.
19985 if matches!(self.peek(), Token::As) {
19986 self.advance();
19987 }
19988 let alias_opt = match self.peek() {
19989 Token::Ident(s) | Token::QuotedIdent(s) => {
19990 let a = s.clone();
19991 self.advance();
19992 Some(a)
19993 }
19994 _ => None,
19995 };
19996 // v7.39 (read01 round 76) — the populate family's canonical PG
19997 // spelling carries no column list at all: the row shape comes from
19998 // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
19999 // j)`). The parser has no catalog, so hand the two arguments to the
20000 // engine's table-function channel, which does. Only `*_to_record*`
20001 // (whose base is bare `record`) genuinely requires the list.
20002 if !matches!(self.peek(), Token::LParen) {
20003 if let Some(base_expr) = base {
20004 let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
20005 return Ok(TableRef {
20006 name: alias.clone(),
20007 alias: Some(alias),
20008 only: false,
20009 as_of_segment: None,
20010 unnest_expr: None,
20011 unnest_column_aliases: Vec::new(),
20012 with_ordinality: false,
20013 generate_series_args: None,
20014 lateral_subquery: None,
20015 jsonb_each_text_arg: None,
20016 table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
20017 rows_from: None,
20018 json_table: None,
20019 scalar_fn_item: false,
20020 });
20021 }
20022 return Err(self.err(alloc::format!(
20023 "expected '(' to start the {fn_name} column-definition list, got {:?}",
20024 self.peek()
20025 )));
20026 }
20027 let Some(alias) = alias_opt else {
20028 return Err(self.err(alloc::format!(
20029 "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
20030 )));
20031 };
20032 self.advance(); // (
20033 let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
20034 loop {
20035 let col = self.expect_ident_like()?;
20036 let ty = self.parse_cast_target()?;
20037 coldefs.push((col, ty));
20038 if matches!(self.peek(), Token::Comma) {
20039 self.advance();
20040 continue;
20041 }
20042 if matches!(self.peek(), Token::RParen) {
20043 self.advance();
20044 break;
20045 }
20046 return Err(self.err(alloc::format!(
20047 "expected ',' or ')' in {fn_name} column list, got {:?}",
20048 self.peek()
20049 )));
20050 }
20051 if coldefs.is_empty() {
20052 return Err(self.err(alloc::format!(
20053 "{fn_name} column-definition list must declare at least one column"
20054 )));
20055 }
20056 // Per column: (base ->> 'col')::type AS col. The base is the
20057 // per-element `value` column for the *set form, or the argument
20058 // itself for the scalar record form.
20059 let items: Vec<SelectItem> = coldefs
20060 .into_iter()
20061 .map(|(col, ty)| {
20062 let base = if is_set {
20063 Expr::Column(ColumnName {
20064 qualifier: None,
20065 name: "value".to_string(),
20066 })
20067 } else {
20068 arg.clone()
20069 };
20070 SelectItem::Expr {
20071 expr: Expr::Cast {
20072 expr: Box::new(Expr::Binary {
20073 lhs: Box::new(base),
20074 op: BinOp::JsonGetText,
20075 rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
20076 }),
20077 target: ty,
20078 },
20079 alias: Some(col),
20080 }
20081 })
20082 .collect();
20083 let from = if is_set {
20084 let elem_fn = if fn_name.starts_with("jsonb") {
20085 "jsonb_array_elements"
20086 } else {
20087 "json_array_elements"
20088 };
20089 Some(FromClause {
20090 primary: TableRef {
20091 name: "value".to_string(),
20092 alias: None,
20093 only: false,
20094 as_of_segment: None,
20095 unnest_expr: Some(Box::new(Expr::FunctionCall {
20096 name: elem_fn.to_string(),
20097 args: alloc::vec![arg],
20098 })),
20099 unnest_column_aliases: alloc::vec!["value".to_string()],
20100 with_ordinality: false,
20101 generate_series_args: None,
20102 lateral_subquery: None,
20103 jsonb_each_text_arg: None,
20104 table_fn_call: None,
20105 rows_from: None,
20106 json_table: None,
20107 scalar_fn_item: false,
20108 },
20109 joins: Vec::new(),
20110 })
20111 } else {
20112 None
20113 };
20114 let inner = SelectStatement {
20115 locking: None,
20116 ctes: Vec::new(),
20117 distinct: false,
20118 distinct_on: Vec::new(),
20119 items,
20120 from,
20121 where_: None,
20122 group_by: None,
20123 group_by_all: false,
20124 having: None,
20125 unions: Vec::new(),
20126 order_by: Vec::new(),
20127 limit: None,
20128 offset: None,
20129 limit_with_ties: false,
20130 window_check_exprs: Vec::new(),
20131 };
20132 Ok(TableRef {
20133 name: alias.clone(),
20134 alias: Some(alias),
20135 only: false,
20136 as_of_segment: None,
20137 unnest_expr: None,
20138 unnest_column_aliases: Vec::new(),
20139 with_ordinality: false,
20140 generate_series_args: None,
20141 lateral_subquery: Some(Box::new(inner)),
20142 jsonb_each_text_arg: None,
20143 table_fn_call: None,
20144 rows_from: None,
20145 json_table: None,
20146 scalar_fn_item: false,
20147 })
20148 }
20149
20150 /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
20151 /// Returns true when the clause was present. `WITH` alone (a
20152 /// CTE can never start here) is not enough — the ORDINALITY
20153 /// ident must follow, so a stray WITH still errors downstream.
20154 fn absorb_with_ordinality(&mut self) -> bool {
20155 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
20156 && matches!(self.tokens.get(self.pos + 1),
20157 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
20158 {
20159 self.advance();
20160 self.advance();
20161 true
20162 } else {
20163 false
20164 }
20165 }
20166
20167 /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
20168 /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
20169 /// Out-of-line: the caller sits on the FROM recursion chain.
20170 #[inline(never)]
20171 fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
20172 let fn_name = match self.advance() {
20173 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20174 _ => unreachable!("caller peeked an ident"),
20175 };
20176 self.advance(); // (
20177 let mut args: Vec<Expr> = Vec::new();
20178 if !matches!(self.peek(), Token::RParen) {
20179 loop {
20180 args.push(self.parse_expr(0)?);
20181 if matches!(self.peek(), Token::Comma) {
20182 self.advance();
20183 continue;
20184 }
20185 break;
20186 }
20187 }
20188 if !matches!(self.peek(), Token::RParen) {
20189 return Err(self.err(alloc::format!(
20190 "expected ')' after {fn_name}() arguments, got {:?}",
20191 self.peek()
20192 )));
20193 }
20194 self.advance();
20195 // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20196 // counter column rides after the function's own, and the alias list
20197 // names it.
20198 let with_ordinality = self.absorb_with_ordinality();
20199 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20200 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20201 Ok(TableRef {
20202 name,
20203 alias: alias_ident,
20204 only: false,
20205 as_of_segment: None,
20206 unnest_expr: None,
20207 unnest_column_aliases,
20208 with_ordinality,
20209 generate_series_args: None,
20210 lateral_subquery: None,
20211 jsonb_each_text_arg: None,
20212 table_fn_call: Some(Box::new((fn_name, args))),
20213 rows_from: None,
20214 json_table: None,
20215 scalar_fn_item: false,
20216 })
20217 }
20218
20219 /// v7.39 (round 205, JSON_TABLE) — parse
20220 /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20221 /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20222 /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20223 #[inline(never)]
20224 fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20225 self.advance(); // json_table
20226 self.advance(); // (
20227 let doc = Box::new(self.parse_expr(0)?);
20228 self.expect_comma_json_table()?;
20229 let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20230 // Optional `PASSING <expr> AS <name> [, …]`.
20231 let mut passing: Vec<(String, Expr)> = Vec::new();
20232 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20233 self.advance();
20234 loop {
20235 let e = self.parse_expr(0)?;
20236 if !matches!(self.peek(), Token::As) {
20237 return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20238 }
20239 self.advance();
20240 let vname = match self.advance() {
20241 Token::Ident(s) | Token::QuotedIdent(s) => s,
20242 other => {
20243 return Err(self.err(alloc::format!(
20244 "expected PASSING variable name, got {other:?}"
20245 )));
20246 }
20247 };
20248 passing.push((vname, e));
20249 if matches!(self.peek(), Token::Comma) {
20250 self.advance();
20251 continue;
20252 }
20253 break;
20254 }
20255 }
20256 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20257 return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20258 }
20259 self.advance();
20260 let columns = self.parse_json_table_columns()?;
20261 if !matches!(self.peek(), Token::RParen) {
20262 return Err(self.err(alloc::format!(
20263 "expected ')' to close JSON_TABLE, got {:?}",
20264 self.peek()
20265 )));
20266 }
20267 self.advance();
20268 let alias_ident = self.parse_optional_alias()?;
20269 let name = alias_ident
20270 .clone()
20271 .unwrap_or_else(|| String::from("json_table"));
20272 Ok(TableRef {
20273 name,
20274 alias: alias_ident,
20275 only: false,
20276 as_of_segment: None,
20277 unnest_expr: None,
20278 unnest_column_aliases: Vec::new(),
20279 with_ordinality: false,
20280 generate_series_args: None,
20281 lateral_subquery: None,
20282 jsonb_each_text_arg: None,
20283 table_fn_call: None,
20284 rows_from: None,
20285 json_table: Some(Box::new(crate::ast::JsonTable {
20286 doc,
20287 row_path,
20288 columns,
20289 passing,
20290 })),
20291 scalar_fn_item: false,
20292 })
20293 }
20294
20295 fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20296 if !matches!(self.peek(), Token::Comma) {
20297 return Err(self.err(alloc::format!(
20298 "expected ',' after JSON_TABLE document, got {:?}",
20299 self.peek()
20300 )));
20301 }
20302 self.advance();
20303 Ok(())
20304 }
20305
20306 fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20307 match self.advance() {
20308 Token::String(s) => Ok(s),
20309 other => Err(self.err(alloc::format!(
20310 "expected {what} string literal, got {other:?}"
20311 ))),
20312 }
20313 }
20314
20315 /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20316 #[inline(never)]
20317 fn parse_json_table_columns(
20318 &mut self,
20319 ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20320 if !matches!(self.peek(), Token::LParen) {
20321 return Err(self.err("expected '(' after COLUMNS".into()));
20322 }
20323 self.advance();
20324 let mut cols = Vec::new();
20325 loop {
20326 cols.push(self.parse_json_table_one_column()?);
20327 if matches!(self.peek(), Token::Comma) {
20328 self.advance();
20329 continue;
20330 }
20331 break;
20332 }
20333 if !matches!(self.peek(), Token::RParen) {
20334 return Err(self.err(alloc::format!(
20335 "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20336 self.peek()
20337 )));
20338 }
20339 self.advance();
20340 Ok(cols)
20341 }
20342
20343 fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20344 use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20345 // NESTED [PATH] '<p>' COLUMNS (...)
20346 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20347 self.advance();
20348 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20349 self.advance();
20350 }
20351 let path = self.parse_json_string_literal("NESTED PATH")?;
20352 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20353 return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20354 }
20355 self.advance();
20356 let columns = self.parse_json_table_columns()?;
20357 return Ok(JsonTableColumn::Nested { path, columns });
20358 }
20359 // <name> ...
20360 let name = match self.advance() {
20361 Token::Ident(s) | Token::QuotedIdent(s) => s,
20362 other => {
20363 return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20364 }
20365 };
20366 // <name> FOR ORDINALITY
20367 if matches!(self.peek(), Token::For) {
20368 self.advance();
20369 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20370 return Err(self.err("expected ORDINALITY after FOR".into()));
20371 }
20372 self.advance();
20373 return Ok(JsonTableColumn::Ordinality { name });
20374 }
20375 // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20376 let ty = self.parse_column_type_name()?;
20377 let mut format_json = false;
20378 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20379 self.advance();
20380 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20381 return Err(self.err("expected JSON after FORMAT".into()));
20382 }
20383 self.advance();
20384 format_json = true;
20385 }
20386 let mut exists = false;
20387 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20388 self.advance();
20389 exists = true;
20390 }
20391 let mut path = alloc::format!("$.{name}");
20392 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20393 self.advance();
20394 path = self.parse_json_string_literal("column PATH")?;
20395 }
20396 if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20397 // `FORMAT JSON` after PATH (alternate placement).
20398 self.advance();
20399 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20400 self.advance();
20401 }
20402 format_json = true;
20403 }
20404 let mut wrapper = false;
20405 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20406 self.advance();
20407 // optional CONDITIONAL/UNCONDITIONAL
20408 if matches!(self.peek(), Token::Ident(s)
20409 if s.eq_ignore_ascii_case("unconditional")
20410 || s.eq_ignore_ascii_case("conditional"))
20411 {
20412 self.advance();
20413 }
20414 if !matches!(self.peek(), Token::Ident(s)
20415 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20416 {
20417 return Err(self.err("expected WRAPPER after WITH".into()));
20418 }
20419 self.advance();
20420 // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20421 if matches!(self.peek(), Token::Ident(s)
20422 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20423 {
20424 self.advance();
20425 }
20426 wrapper = true;
20427 }
20428 // ON EMPTY / ON ERROR clauses (two, in any order).
20429 let mut on_empty = JsonTableOnBehavior::Null;
20430 let mut on_error = JsonTableOnBehavior::Null;
20431 for _ in 0..2 {
20432 let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20433 {
20434 self.advance();
20435 Some(JsonTableOnBehavior::Error)
20436 } else if matches!(self.peek(), Token::Null) {
20437 self.advance();
20438 Some(JsonTableOnBehavior::Null)
20439 } else if matches!(self.peek(), Token::Default) {
20440 self.advance();
20441 Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
20442 } else {
20443 None
20444 };
20445 let Some(behavior) = behavior else { break };
20446 // `ON {EMPTY|ERROR}`
20447 if !matches!(self.peek(), Token::On) {
20448 return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
20449 }
20450 self.advance();
20451 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
20452 self.advance();
20453 on_empty = behavior;
20454 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
20455 self.advance();
20456 on_error = behavior;
20457 } else {
20458 return Err(self.err("expected EMPTY or ERROR after ON".into()));
20459 }
20460 }
20461 Ok(JsonTableColumn::Regular {
20462 name,
20463 ty,
20464 path,
20465 exists,
20466 format_json,
20467 wrapper,
20468 on_empty,
20469 on_error,
20470 })
20471 }
20472
20473 fn parse_optional_alias_with_columns(
20474 &mut self,
20475 ) -> Result<(Option<String>, Vec<String>), ParseError> {
20476 let alias = self.parse_optional_alias()?;
20477 if alias.is_none() {
20478 return Ok((None, Vec::new()));
20479 }
20480 let mut cols: Vec<String> = Vec::new();
20481 if matches!(self.peek(), Token::LParen) {
20482 self.advance();
20483 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
20484 self.advance();
20485 cols.push(s);
20486 if matches!(self.peek(), Token::Comma) {
20487 self.advance();
20488 continue;
20489 }
20490 break;
20491 }
20492 if matches!(self.peek(), Token::RParen) {
20493 self.advance();
20494 }
20495 }
20496 Ok((alias, cols))
20497 }
20498
20499 /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
20500 /// whose keyword token was already consumed and whose `(` is the
20501 /// current token. Factored out of `parse_atom` (and marked
20502 /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
20503 /// recursive `parse_atom` frame — inlining them there enlarges the
20504 /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
20505 /// against, risking an overflow before the budget triggers.
20506 #[inline(never)]
20507 fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
20508 self.advance(); // (
20509 let mut args = Vec::new();
20510 if !matches!(self.peek(), Token::RParen) {
20511 loop {
20512 args.push(self.parse_expr(0)?);
20513 match self.peek() {
20514 Token::Comma => {
20515 self.advance();
20516 }
20517 Token::RParen => break,
20518 other => {
20519 return Err(self.err(alloc::format!(
20520 "expected ',' or ')' in {name}() args, got {other:?}"
20521 )));
20522 }
20523 }
20524 }
20525 }
20526 self.advance(); // )
20527 Ok(Expr::FunctionCall {
20528 name: name.into(),
20529 args,
20530 })
20531 }
20532
20533 /// FROM-clause: a primary table reference plus zero-or-more joined
20534 /// peers expressed via either `, <table>` (cross-product, no ON) or
20535 /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
20536 /// v1.10 keeps the join list flat (left-associative nested-loop
20537 /// semantics).
20538 fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
20539 let primary = self.parse_table_ref()?;
20540 let primary_qual = primary
20541 .alias
20542 .clone()
20543 .unwrap_or_else(|| primary.name.clone());
20544 let joins = self.parse_from_joins(&primary_qual)?;
20545 Ok(FromClause { primary, joins })
20546 }
20547
20548 /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
20549 /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
20550 /// SAME grammar after its target table has already been consumed.
20551 /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
20552 /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
20553 /// be parsed forward, once.)
20554 /// `left_primary_qual` is the qualifier (alias, else name) of whatever
20555 /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
20556 /// target in the MySQL multi-table form. It only feeds the `USING (…)`
20557 /// desugaring, which needs a name for the left side of each equality.
20558 fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
20559 let mut joins = Vec::new();
20560 loop {
20561 // `, <table>` — cross-product with no ON.
20562 if matches!(self.peek(), Token::Comma) {
20563 self.advance();
20564 let table = self.parse_table_ref()?;
20565 joins.push(FromJoin {
20566 kind: JoinKind::Cross,
20567 table,
20568 on: None,
20569 using_cols: None,
20570 natural: false,
20571 });
20572 continue;
20573 }
20574 // v7.37.16 — optional leading `NATURAL` before the join
20575 // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
20576 // not a lexer keyword (it arrives as a bare Ident), so match
20577 // it case-insensitively here. When present, no ON/USING
20578 // clause is allowed — the common columns are resolved at
20579 // execution time.
20580 let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
20581 if natural {
20582 self.advance();
20583 }
20584 // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
20585 // CROSS JOIN, and bare JOIN (defaults to INNER).
20586 let kind =
20587 match self.peek() {
20588 Token::Inner => {
20589 self.advance();
20590 if !matches!(self.peek(), Token::Join) {
20591 return Err(self
20592 .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
20593 }
20594 self.advance();
20595 JoinKind::Inner
20596 }
20597 Token::Left => {
20598 self.advance();
20599 if matches!(self.peek(), Token::Outer) {
20600 self.advance();
20601 }
20602 if !matches!(self.peek(), Token::Join) {
20603 return Err(self.err(format!(
20604 "expected JOIN after LEFT [OUTER], got {:?}",
20605 self.peek()
20606 )));
20607 }
20608 self.advance();
20609 JoinKind::Left
20610 }
20611 Token::Cross => {
20612 self.advance();
20613 if !matches!(self.peek(), Token::Join) {
20614 return Err(self
20615 .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
20616 }
20617 self.advance();
20618 JoinKind::Cross
20619 }
20620 // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
20621 Token::Right => {
20622 self.advance();
20623 if matches!(self.peek(), Token::Outer) {
20624 self.advance();
20625 }
20626 if !matches!(self.peek(), Token::Join) {
20627 return Err(self.err(format!(
20628 "expected JOIN after RIGHT [OUTER], got {:?}",
20629 self.peek()
20630 )));
20631 }
20632 self.advance();
20633 JoinKind::Right
20634 }
20635 // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
20636 Token::Full => {
20637 self.advance();
20638 if matches!(self.peek(), Token::Outer) {
20639 self.advance();
20640 }
20641 if !matches!(self.peek(), Token::Join) {
20642 return Err(self.err(format!(
20643 "expected JOIN after FULL [OUTER], got {:?}",
20644 self.peek()
20645 )));
20646 }
20647 self.advance();
20648 JoinKind::FullOuter
20649 }
20650 Token::Join => {
20651 self.advance();
20652 JoinKind::Inner
20653 }
20654 _ => break,
20655 };
20656 let table = self.parse_table_ref()?;
20657 // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
20658 // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
20659 // where prev_table is the most-recent left-side table
20660 // (the previous join's table if any, else the FROM primary).
20661 // PG semantics around column merging are richer (USING'd
20662 // cols become deduplicated single output columns); for
20663 // sugar purposes the predicate-only form covers the
20664 // baseline corpus shape and chained `… JOIN x USING (k)
20665 // JOIN y USING (k)` calls.
20666 // v7.37.16 — NATURAL joins carry no ON/USING clause; the
20667 // common columns resolve at execution time.
20668 if natural {
20669 joins.push(FromJoin {
20670 kind,
20671 table,
20672 on: None,
20673 using_cols: None,
20674 natural: true,
20675 });
20676 continue;
20677 }
20678 let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
20679 // v7.37.16 — capture the USING column list (in addition to
20680 // the ON desugar below) so the executor can perform PG's
20681 // column-merge on the output side.
20682 let mut using_cols: Option<Vec<String>> = None;
20683 let on = if matches!(self.peek(), Token::On) {
20684 self.advance();
20685 Some(self.parse_expr(0)?)
20686 } else if using_match {
20687 self.advance();
20688 if !matches!(self.peek(), Token::LParen) {
20689 return Err(
20690 self.err(format!("expected '(' after USING, got {:?}", self.peek()))
20691 );
20692 }
20693 self.advance();
20694 let mut cols: Vec<String> = Vec::new();
20695 loop {
20696 match self.peek().clone() {
20697 Token::Ident(s) | Token::QuotedIdent(s) => {
20698 self.advance();
20699 cols.push(s);
20700 }
20701 other => {
20702 return Err(self.err(format!(
20703 "expected column name inside USING (…), got {other:?}"
20704 )));
20705 }
20706 }
20707 match self.peek() {
20708 Token::Comma => {
20709 self.advance();
20710 continue;
20711 }
20712 Token::RParen => {
20713 self.advance();
20714 break;
20715 }
20716 other => {
20717 return Err(self.err(format!(
20718 "expected ',' or ')' inside USING (…), got {other:?}"
20719 )));
20720 }
20721 }
20722 }
20723 if cols.is_empty() {
20724 return Err(self.err("USING (…) requires at least one column".to_string()));
20725 }
20726 using_cols = Some(cols.clone());
20727 // Pick the left-side alias: prev join's table if any,
20728 // else FROM primary. Use alias when present, else
20729 // table name (PG-equivalent qualifier).
20730 let left_qual: String = joins
20731 .last()
20732 .map(|j| {
20733 j.table
20734 .alias
20735 .clone()
20736 .unwrap_or_else(|| j.table.name.clone())
20737 })
20738 .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
20739 let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
20740 let mut iter = cols.into_iter().map(|c| Expr::Binary {
20741 lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20742 qualifier: Some(left_qual.clone()),
20743 name: c.clone(),
20744 })),
20745 op: crate::ast::BinOp::Eq,
20746 rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20747 qualifier: Some(right_qual.clone()),
20748 name: c,
20749 })),
20750 });
20751 let first = iter.next().expect("at least one col");
20752 Some(iter.fold(first, |acc, pred| Expr::Binary {
20753 lhs: alloc::boxed::Box::new(acc),
20754 op: crate::ast::BinOp::And,
20755 rhs: alloc::boxed::Box::new(pred),
20756 }))
20757 } else if kind == JoinKind::Cross {
20758 None
20759 } else {
20760 return Err(self.err(format!(
20761 "expected ON or USING after {:?} JOIN, got {:?}",
20762 kind,
20763 self.peek()
20764 )));
20765 };
20766 joins.push(FromJoin {
20767 kind,
20768 table,
20769 on,
20770 using_cols,
20771 natural: false,
20772 });
20773 }
20774 Ok(joins)
20775 }
20776
20777 /// Optional alias after an expression or table:
20778 /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
20779 /// accepted (PG-style implicit alias). Returns `None` if the next token
20780 /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
20781 fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
20782 if matches!(self.peek(), Token::As) {
20783 self.advance();
20784 // v7.39 (round 340, V56) — after AS the next token MUST be an
20785 // identifier. This used to return None and "let the caller
20786 // surface the error on the next expectation", but when AS is
20787 // the LAST token there is no next expectation: `SELECT 1 AS`
20788 // parsed clean and silently dropped the alias. PG rejects it.
20789 if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
20790 return self.expect_ident_like().map(Some);
20791 }
20792 return Err(self.err(alloc::format!(
20793 "expected an alias after AS, got {:?}",
20794 self.peek()
20795 )));
20796 }
20797 // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
20798 // grammar reserves a long list of follow-keywords from the
20799 // alias slot. SPG's bareword approximation: skip a small
20800 // set of idents that would otherwise be swallowed as the
20801 // table alias and break trailing clauses like CREATE
20802 // MATERIALIZED VIEW … WITH [NO] DATA or future ON
20803 // CONFLICT WHERE shapes.
20804 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
20805 if is_alias_stopword(s) {
20806 return Ok(None);
20807 }
20808 return Ok(self.expect_ident_like().ok());
20809 }
20810 Ok(None)
20811 }
20812
20813 /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
20814 fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
20815 // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
20816 // error beats a stack overflow (an overflow aborts the
20817 // embedding host process).
20818 self.enter_nested()?;
20819 let r = self.parse_expr_inner(min_prec);
20820 self.nest_depth -= 1;
20821 r
20822 }
20823
20824 /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
20825 /// When the upcoming tokens form one, return the underlying
20826 /// operator token and the position just past the closing paren
20827 /// so the binary loop can dispatch on the plain operator.
20828 fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
20829 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
20830 return None;
20831 }
20832 if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
20833 return None;
20834 }
20835 let mut i = self.pos + 2;
20836 // Optional schema qualifier (pg_catalog.<op> etc.).
20837 if matches!(self.tokens.get(i), Some(Token::Ident(_)))
20838 && matches!(self.tokens.get(i + 1), Some(Token::Dot))
20839 {
20840 i += 2;
20841 }
20842 let op_tok = self.tokens.get(i)?.clone();
20843 if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
20844 return None;
20845 }
20846 Some((i + 2, op_tok))
20847 }
20848
20849 /// PG operator symbols that lower onto function calls in
20850 /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
20851 /// family → regexp_like, comparison rung), `^@` (starts_with,
20852 /// comparison rung), `^` (power, tighter than `*`), `#`
20853 /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
20854 /// subset of the OR bits so the subtraction never borrows).
20855 fn try_symbol_operator(
20856 &mut self,
20857 lhs: &Expr,
20858 min_prec: u8,
20859 ) -> Result<Option<Expr>, ParseError> {
20860 enum Sym {
20861 Regex { ci: bool, negated: bool },
20862 Like { ci: bool, negated: bool },
20863 StartsWith,
20864 Power,
20865 Xor,
20866 RangeAdjacent,
20867 }
20868 // v7.39 (IS-precedence knife) — the low-precedence postfix
20869 // predicates ride this existing leaf call (zero new frame slots
20870 // on the nesting chain).
20871 if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
20872 return Ok(Some(e));
20873 }
20874 let (sym, prec): (Sym, u8) = match self.peek() {
20875 Token::Tilde => (
20876 Sym::Regex {
20877 ci: false,
20878 negated: false,
20879 },
20880 5,
20881 ),
20882 Token::TildeStar => (
20883 Sym::Regex {
20884 ci: true,
20885 negated: false,
20886 },
20887 5,
20888 ),
20889 Token::NotTilde => (
20890 Sym::Regex {
20891 ci: false,
20892 negated: true,
20893 },
20894 5,
20895 ),
20896 Token::NotTildeStar => (
20897 Sym::Regex {
20898 ci: true,
20899 negated: true,
20900 },
20901 5,
20902 ),
20903 // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
20904 Token::DoubleTilde => (
20905 Sym::Like {
20906 ci: false,
20907 negated: false,
20908 },
20909 5,
20910 ),
20911 Token::DoubleTildeStar => (
20912 Sym::Like {
20913 ci: true,
20914 negated: false,
20915 },
20916 5,
20917 ),
20918 Token::NotDoubleTilde => (
20919 Sym::Like {
20920 ci: false,
20921 negated: true,
20922 },
20923 5,
20924 ),
20925 Token::NotDoubleTildeStar => (
20926 Sym::Like {
20927 ci: true,
20928 negated: true,
20929 },
20930 5,
20931 ),
20932 Token::CaretAt => (Sym::StartsWith, 5),
20933 // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
20934 // tighter than `* / & |`, which the prec-9 rung preserves —
20935 // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
20936 Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
20937 Token::Caret => (Sym::Power, 9),
20938 // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
20939 // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
20940 Token::Hash => (Sym::Xor, 6),
20941 Token::Adjacent => (Sym::RangeAdjacent, 5),
20942 _ => return Ok(None),
20943 };
20944 if prec < min_prec {
20945 return Ok(None);
20946 }
20947 self.advance();
20948 let rhs = self.parse_expr(prec + 1)?;
20949 let out = match sym {
20950 Sym::Regex { ci, negated } => {
20951 let mut args = alloc::vec![lhs.clone(), rhs];
20952 if ci {
20953 args.push(Expr::Literal(Literal::String(String::from("i"))));
20954 }
20955 maybe_not(
20956 Expr::FunctionCall {
20957 name: String::from("regexp_like"),
20958 args,
20959 },
20960 negated,
20961 )
20962 }
20963 Sym::Like { ci, negated } => Expr::Like {
20964 expr: alloc::boxed::Box::new(lhs.clone()),
20965 pattern: alloc::boxed::Box::new(rhs),
20966 negated,
20967 case_insensitive: ci,
20968 },
20969 Sym::StartsWith => Expr::FunctionCall {
20970 name: String::from("starts_with"),
20971 args: alloc::vec![lhs.clone(), rhs],
20972 },
20973 Sym::Power => Expr::FunctionCall {
20974 name: String::from("power"),
20975 args: alloc::vec![lhs.clone(), rhs],
20976 },
20977 // `#` bitwise XOR — a real operator now (was desugared to
20978 // `(a|b)-(a&b)`, algebraically identical for integers but
20979 // undefined for bit strings; the direct op handles both).
20980 Sym::Xor => Expr::Binary {
20981 lhs: Box::new(lhs.clone()),
20982 op: BinOp::BitXor,
20983 rhs: Box::new(rhs),
20984 },
20985 // range `-|-` "is adjacent to" — lowered to a catalog function.
20986 Sym::RangeAdjacent => Expr::FunctionCall {
20987 name: String::from("range_adjacent"),
20988 args: alloc::vec![lhs.clone(), rhs],
20989 },
20990 };
20991 Ok(Some(out))
20992 }
20993
20994 /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
20995 /// predicates, moved out of the tight postfix-cast loop: PG binds
20996 /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
20997 /// looser than EVERY binary operator (only NOT/AND/OR are looser),
20998 /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
20999 /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
21000 /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
21001 /// when nothing at this position belongs to the family. Out-of-line
21002 /// (`inline(never)`): the caller sits on the per-nesting-level frame
21003 /// chain that MAX_NEST_DEPTH is tuned against.
21004 #[inline(never)]
21005 fn parse_postfix_predicate(
21006 &mut self,
21007 lhs: &Expr,
21008 min_prec: u8,
21009 ) -> Result<Option<Expr>, ParseError> {
21010 // Reached through try_symbol_operator (an existing leaf call of
21011 // the binary loop) so NO new stack slots land on the per-nesting
21012 // frame chain; the lhs clones only when a predicate actually
21013 // consumes it.
21014 match self.peek() {
21015 // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
21016 // comparison family rung 5 (each +1 from the pre-XOR ladder).
21017 Token::Is if min_prec <= 4 => {}
21018 Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
21019 Token::Not
21020 if min_prec <= 5
21021 && matches!(
21022 self.tokens.get(self.pos + 1),
21023 Some(Token::Between | Token::In | Token::Like)
21024 ) => {}
21025 Token::Not | Token::Ident(_)
21026 if min_prec <= 5
21027 && (matches!(self.peek(), Token::Ident(s)
21028 if s.eq_ignore_ascii_case("ilike")
21029 || (self.mysql_dialect
21030 && (s.eq_ignore_ascii_case("regexp")
21031 || s.eq_ignore_ascii_case("rlike")))
21032 || (s.eq_ignore_ascii_case("similar")
21033 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
21034 || (matches!(self.peek(), Token::Not)
21035 && matches!(self.tokens.get(self.pos + 1),
21036 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21037 || (self.mysql_dialect
21038 && (s.eq_ignore_ascii_case("regexp")
21039 || s.eq_ignore_ascii_case("rlike")))
21040 || s.eq_ignore_ascii_case("similar")))) => {}
21041 _ => return Ok(None),
21042 }
21043 let mut expr = lhs.clone();
21044 // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
21045 // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
21046 if min_prec <= 4 {
21047 if matches!(self.peek(), Token::Is) {
21048 self.advance();
21049 let negated = if matches!(self.peek(), Token::Not) {
21050 self.advance();
21051 true
21052 } else {
21053 false
21054 };
21055 // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
21056 // mailrs pg_dump.
21057 if matches!(self.peek(), Token::Distinct) {
21058 self.advance();
21059 if !matches!(self.peek(), Token::From) {
21060 return Err(self.err(format!(
21061 "expected FROM after IS{} DISTINCT, got {:?}",
21062 if negated { " NOT" } else { "" },
21063 self.peek()
21064 )));
21065 }
21066 self.advance();
21067 // Right-hand side: parse at the same precedence
21068 // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
21069 // groups as `x IS DISTINCT FROM (a + b)`.
21070 let rhs = self.parse_expr(5)?;
21071 let op = if negated {
21072 BinOp::IsNotDistinctFrom
21073 } else {
21074 BinOp::IsDistinctFrom
21075 };
21076 expr = Expr::Binary {
21077 op,
21078 lhs: Box::new(expr),
21079 rhs: Box::new(rhs),
21080 };
21081 {
21082 return Ok(Some(expr));
21083 }
21084 }
21085 // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
21086 // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
21087 // Lowers onto pg_is_json(x, kind); NOT wraps the
21088 // call in a logical negation.
21089 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21090 if s.eq_ignore_ascii_case("json"))
21091 {
21092 self.advance(); // JSON
21093 let kind = match self.peek() {
21094 Token::Ident(s) | Token::QuotedIdent(s)
21095 if matches!(
21096 s.to_ascii_lowercase().as_str(),
21097 "value" | "object" | "array" | "scalar"
21098 ) =>
21099 {
21100 let k = s.to_ascii_lowercase();
21101 self.advance();
21102 k
21103 }
21104 _ => "value".to_string(),
21105 };
21106 let call = Expr::FunctionCall {
21107 name: "pg_is_json".to_string(),
21108 args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
21109 };
21110 expr = if negated {
21111 Expr::Unary {
21112 op: UnOp::Not,
21113 expr: Box::new(call),
21114 }
21115 } else {
21116 call
21117 };
21118 {
21119 return Ok(Some(expr));
21120 }
21121 }
21122 // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
21123 // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
21124 // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
21125 {
21126 let form_kw = match self.peek() {
21127 Token::Ident(s) | Token::QuotedIdent(s)
21128 if matches!(
21129 s.to_ascii_uppercase().as_str(),
21130 "NFC" | "NFD" | "NFKC" | "NFKD"
21131 ) && matches!(
21132 self.tokens.get(self.pos + 1),
21133 Some(Token::Ident(n) | Token::QuotedIdent(n))
21134 if n.eq_ignore_ascii_case("normalized")
21135 ) =>
21136 {
21137 Some(s.to_ascii_uppercase())
21138 }
21139 _ => None,
21140 };
21141 let bare_normalized = form_kw.is_none()
21142 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21143 if s.eq_ignore_ascii_case("normalized"));
21144 if form_kw.is_some() || bare_normalized {
21145 if form_kw.is_some() {
21146 self.advance(); // form keyword
21147 }
21148 self.advance(); // NORMALIZED
21149 let mut args = alloc::vec![expr];
21150 if let Some(f) = form_kw {
21151 args.push(Expr::Literal(Literal::String(f)));
21152 }
21153 let call = Expr::FunctionCall {
21154 name: "is_normalized".to_string(),
21155 args,
21156 };
21157 expr = if negated {
21158 Expr::Unary {
21159 op: UnOp::Not,
21160 expr: Box::new(call),
21161 }
21162 } else {
21163 call
21164 };
21165 {
21166 return Ok(Some(expr));
21167 }
21168 }
21169 }
21170 // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
21171 // three-valued boolean tests. IS TRUE/FALSE never
21172 // return NULL, so they lower to CASE forms whose
21173 // ELSE catches the NULL branch; IS UNKNOWN on a
21174 // boolean is exactly IS NULL.
21175 if matches!(self.peek(), Token::True | Token::False)
21176 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
21177 {
21178 let tok = self.advance();
21179 let test = match tok {
21180 Token::True => Some(true),
21181 Token::False => Some(false),
21182 _ => None, // UNKNOWN
21183 };
21184 // v7.39 (round 328, V45) — kept as what the user
21185 // wrote. These used to be lowered here into `CASE` /
21186 // `IS NULL`; the semantics were right but the AST no
21187 // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
21188 // was echoed back as
21189 // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
21190 expr = Expr::BoolTest {
21191 expr: Box::new(expr),
21192 value: test,
21193 negated,
21194 };
21195 {
21196 return Ok(Some(expr));
21197 }
21198 }
21199 if !matches!(self.peek(), Token::Null) {
21200 return Err(self.err(format!(
21201 "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21202 if negated { " NOT" } else { "" },
21203 self.peek()
21204 )));
21205 }
21206 self.advance();
21207 expr = Expr::IsNull {
21208 expr: Box::new(expr),
21209 negated,
21210 };
21211 {
21212 return Ok(Some(expr));
21213 }
21214 }
21215 }
21216 // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21217 if min_prec <= 5 {
21218 // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21219 // Look one token ahead so a stray `NOT` not followed by any of
21220 // these flows through to the early return below untouched.
21221 let negated = if matches!(self.peek(), Token::Not) {
21222 let next = self.tokens.get(self.pos + 1);
21223 matches!(next, Some(Token::Between | Token::In | Token::Like))
21224 || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21225 || (self.mysql_dialect
21226 && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21227 || s.eq_ignore_ascii_case("similar"))
21228 } else {
21229 false
21230 };
21231 if negated {
21232 self.advance();
21233 }
21234 if matches!(self.peek(), Token::Between) {
21235 expr = self.parse_between_tail(expr, negated)?;
21236 {
21237 return Ok(Some(expr));
21238 }
21239 }
21240 if matches!(self.peek(), Token::In) {
21241 if self.suppress_in_tail && !negated {
21242 // POSITION(sub IN str) — IN belongs to the
21243 // enclosing function syntax; stop here.
21244 {
21245 return Ok(None);
21246 }
21247 }
21248 expr = self.parse_in_tail(expr, negated)?;
21249 {
21250 return Ok(Some(expr));
21251 }
21252 }
21253 // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21254 // lowers onto the internal __similar_to(expr, pat[, esc]) call
21255 // (the SQL→regex transform runs inside, in the backtracking-
21256 // friendly shape SPG's matcher needs).
21257 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21258 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21259 {
21260 self.advance(); // SIMILAR
21261 self.advance(); // TO
21262 let pattern = self.parse_expr(6)?;
21263 let mut args = alloc::vec![expr, pattern];
21264 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21265 self.advance();
21266 args.push(self.parse_expr(6)?);
21267 }
21268 let call = Expr::FunctionCall {
21269 name: "__similar_to".to_string(),
21270 args,
21271 };
21272 expr = maybe_not(call, negated);
21273 {
21274 return Ok(Some(expr));
21275 }
21276 }
21277 if matches!(self.peek(), Token::Like) {
21278 self.advance();
21279 // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21280 if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21281 expr = q;
21282 {
21283 return Ok(Some(expr));
21284 }
21285 }
21286 // Pattern at the same precedence as other comparison RHSes —
21287 // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21288 let mut pattern = self.parse_expr(6)?;
21289 // `ESCAPE 'c'` — rewrite a literal pattern to the
21290 // default backslash escape at parse time. Custom
21291 // escapes on non-literal patterns would need
21292 // matcher support; error honestly.
21293 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21294 self.advance();
21295 let esc = self.parse_expr(6)?;
21296 pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21297 }
21298 expr = Expr::Like {
21299 expr: Box::new(expr),
21300 pattern: Box::new(pattern),
21301 negated,
21302 case_insensitive: false,
21303 };
21304 {
21305 return Ok(Some(expr));
21306 }
21307 }
21308 // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21309 // keyword reaches us as a plain identifier.
21310 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21311 self.advance();
21312 if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21313 expr = q;
21314 {
21315 return Ok(Some(expr));
21316 }
21317 }
21318 let pattern = self.parse_expr(6)?;
21319 expr = Expr::Like {
21320 expr: Box::new(expr),
21321 pattern: Box::new(pattern),
21322 negated,
21323 case_insensitive: true,
21324 };
21325 {
21326 return Ok(Some(expr));
21327 }
21328 }
21329 // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21330 // operator (RLIKE is the alias). It is a keyword, not `~`, and
21331 // matches case-insensitively under the default collation, so it
21332 // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21333 // `~*` operator uses, wrapped in NOT when negated.
21334 if self.mysql_dialect
21335 && matches!(self.peek(), Token::Ident(s)
21336 if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21337 {
21338 self.advance();
21339 let pattern = self.parse_expr(6)?;
21340 let call = Expr::FunctionCall {
21341 name: String::from("regexp_like"),
21342 args: alloc::vec![
21343 expr,
21344 pattern,
21345 Expr::Literal(Literal::String(String::from("i"))),
21346 ],
21347 };
21348 return Ok(Some(maybe_not(call, negated)));
21349 }
21350 }
21351 let _ = expr;
21352 Ok(None)
21353 }
21354
21355 fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21356 let mut lhs = self.parse_unary()?;
21357 let mut chain_len = 0usize;
21358 loop {
21359 // OPERATOR([schema.]op) reduces to its underlying
21360 // operator token before the normal dispatch.
21361 let explicit = self.peek_explicit_operator();
21362 let dispatch = match &explicit {
21363 Some((_, tok)) => self.binop_here(tok),
21364 None => self.binop_here(self.peek()),
21365 };
21366 let Some((op, prec)) = dispatch else {
21367 // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21368 // of the symbol family. `binop_here` answers None for them
21369 // because they lower onto function calls rather than a
21370 // BinOp, and the fallback below reads `self.peek()` — the
21371 // word OPERATOR, not the operator. `pg_dump` writes every
21372 // catalog predicate this way, so its first query failed
21373 // and no dump ran:
21374 //
21375 // AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21376 //
21377 // Collapsing the wrapper to the operator it names puts the
21378 // token where the fallback already looks.
21379 if let Some((next, op_tok)) = explicit {
21380 self.tokens.splice(self.pos..next, [op_tok]);
21381 }
21382 if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21383 lhs = e;
21384 chain_len += 1;
21385 if chain_len > MAX_BINARY_CHAIN {
21386 return Err(self.err(alloc::format!(
21387 "more than {MAX_BINARY_CHAIN} chained binary operators"
21388 )));
21389 }
21390 continue;
21391 }
21392 break;
21393 };
21394 if prec < min_prec {
21395 break;
21396 }
21397 // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21398 // iteratively but evaluates and drops recursively;
21399 // depth beyond the budget overflows worker stacks.
21400 chain_len += 1;
21401 if chain_len > MAX_BINARY_CHAIN {
21402 return Err(self.err(alloc::format!(
21403 "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21404 )));
21405 }
21406 match explicit {
21407 Some((end_pos, _)) => self.pos = end_pos,
21408 None => {
21409 self.advance();
21410 }
21411 }
21412 // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21413 // ANY is a bare ident; ALL is a reserved Token. Both
21414 // require an immediate `(` to disambiguate from
21415 // identifier columns named `any` / `all`.
21416 let any_kind = match self.peek() {
21417 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21418 Some(false)
21419 }
21420 Token::Ident(s) | Token::QuotedIdent(s)
21421 if (s.eq_ignore_ascii_case("any")
21422 || s.eq_ignore_ascii_case("some")
21423 || s.eq_ignore_ascii_case("all"))
21424 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21425 {
21426 Some(!s.eq_ignore_ascii_case("all"))
21427 }
21428 _ => None,
21429 };
21430 if let Some(is_any) = any_kind {
21431 lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21432 continue;
21433 }
21434 let rhs = self.parse_expr(prec + 1)?;
21435 lhs = Expr::Binary {
21436 lhs: Box::new(lhs),
21437 op,
21438 rhs: Box::new(rhs),
21439 };
21440 }
21441 Ok(lhs)
21442 }
21443
21444 /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
21445 /// and the array form.
21446 ///
21447 /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
21448 /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
21449 /// this block's `Expr` temporaries and four `format!` sites slots in
21450 /// that frame on every level of `((((1))))`, which never reaches it.
21451 #[inline(never)]
21452 fn parse_any_all_rhs(
21453 &mut self,
21454 lhs: Expr,
21455 op: BinOp,
21456 is_any: bool,
21457 ) -> Result<Expr, ParseError> {
21458 self.advance(); // ident
21459 self.advance(); // (
21460 // `x op ANY (SELECT …)` — the quantified-subquery
21461 // form. `= ANY` is exactly IN; the other operators
21462 // lower onto EXISTS over the subquery as a derived
21463 // table, comparing against its single projection
21464 // aliased __v (x's columns resolve correlated).
21465 // ALL is the negated-EXISTS complement; a NULL
21466 // element makes PG return NULL where this lowering
21467 // returns true — the NOT NULL column case (the
21468 // practical one) is exact.
21469 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
21470 // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
21471 // legal PG too (round-151 sibling). Out-of-line
21472 // (#[inline(never)] helper) — this sits on
21473 // parse_expr's recursive frame and the two-armed
21474 // SELECT temporary blew the nesting-budget stack.
21475 let mut sub = self.parse_any_all_select_body()?;
21476 if !matches!(self.peek(), Token::RParen) {
21477 return Err(self.err(alloc::format!(
21478 "expected ')' after ANY/ALL subquery, got {:?}",
21479 self.peek()
21480 )));
21481 }
21482 self.advance();
21483 if sub.items.len() != 1 {
21484 return Err(self.err(alloc::format!(
21485 "ANY/ALL subquery must return one column, got {}",
21486 sub.items.len()
21487 )));
21488 }
21489 if is_any && matches!(op, BinOp::Eq) {
21490 return Ok(Expr::InSubquery {
21491 expr: Box::new(lhs),
21492 subquery: Box::new(sub),
21493 negated: false,
21494 });
21495 }
21496 // The engine's subquery resolvers materialise
21497 // the single-column result into an ARRAY the
21498 // existing AnyAll three-valued eval consumes.
21499 return Ok(Expr::AnyAll {
21500 expr: Box::new(lhs),
21501 op,
21502 array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
21503 is_any,
21504 });
21505 }
21506 let arr = self.parse_expr(0)?;
21507 if !matches!(self.peek(), Token::RParen) {
21508 return Err(self.err(alloc::format!(
21509 "expected ')' after ANY/ALL argument, got {:?}",
21510 self.peek()
21511 )));
21512 }
21513 self.advance();
21514 Ok(Expr::AnyAll {
21515 expr: Box::new(lhs),
21516 op,
21517 array: Box::new(arr),
21518 is_any,
21519 })
21520 }
21521
21522 /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
21523 /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
21524 #[inline(never)]
21525 fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
21526 self.advance();
21527 let e = self.parse_expr(9)?;
21528 Ok(build_center_call(e))
21529 }
21530
21531 /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
21532 /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
21533 /// unary minus.
21534 ///
21535 /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
21536 /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
21537 /// the Expr-sized local stays out of that frame.
21538 #[inline(never)]
21539 fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21540 self.advance();
21541 let e = self.parse_expr(9)?;
21542 Ok(Expr::FunctionCall {
21543 name: alloc::string::String::from(name),
21544 args: alloc::vec![e],
21545 })
21546 }
21547
21548 /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
21549 /// (horizontal). Out-of-line from `parse_unary` (frame budget).
21550 #[inline(never)]
21551 fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
21552 self.advance();
21553 let e = self.parse_expr(9)?;
21554 Ok(Expr::FunctionCall {
21555 name: alloc::string::String::from(if vertical {
21556 "isvertical"
21557 } else {
21558 "ishorizontal"
21559 }),
21560 args: alloc::vec![e],
21561 })
21562 }
21563
21564 /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
21565 /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
21566 /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
21567 #[inline(never)]
21568 fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
21569 self.advance();
21570 let e = self.parse_expr(9)?;
21571 Ok(Expr::Cast {
21572 expr: Box::new(e),
21573 target: CastTarget::Named("binary".to_string()),
21574 })
21575 }
21576
21577 /// The prefix operators that share one shape: take the token, parse
21578 /// an operand at `prec`, wrap it.
21579 ///
21580 /// `#[inline(never)]`, and one function instead of five arms, for the
21581 /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
21582 /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
21583 /// debug build gives EVERY arm's locals a slot in the frame, whichever
21584 /// arm runs. `((((1))))` reaches none of these arms and was carrying
21585 /// five `Expr`-sized locals per level for them anyway.
21586 #[inline(never)]
21587 fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
21588 self.advance();
21589 let e = self.parse_expr(prec)?;
21590 Ok(Expr::Unary {
21591 op,
21592 expr: Box::new(e),
21593 })
21594 }
21595
21596 /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
21597 /// and separate from it because of the literal folding below and the
21598 /// `format!` temporaries that folding needs.
21599 #[inline(never)]
21600 fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
21601 self.advance();
21602 // v7.39 (round 549) — fold the sign into an integer literal that
21603 // only fits once it is negative.
21604 //
21605 // `9223372036854775808` is one past i64::MAX, so the lexer hands
21606 // it over as a NUMERIC and `-` on a numeric stays numeric. PG
21607 // folds the sign first, so `-9223372036854775808` is a bigint
21608 // there — and `-9223372036854775808 - 1` raises "bigint out of
21609 // range" where SPG quietly answered -9223372036854775809, a value
21610 // no bigint can hold. The arithmetic itself was already checked;
21611 // only the literal's type was wrong.
21612 if let Token::Numeric(lit) = self.peek()
21613 && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
21614 {
21615 self.advance();
21616 return Ok(Expr::Literal(Literal::Integer(folded)));
21617 }
21618 // Unary minus binds tighter than `*`/`/` (now at prec 7 after
21619 // `<->` slotted into 5 and arithmetic shifted up).
21620 let e = self.parse_expr(9)?;
21621 Ok(Expr::Unary {
21622 op: UnOp::Neg,
21623 expr: Box::new(e),
21624 })
21625 }
21626
21627 /// tsquery `!!` prefix negation, lowered to the catalog function.
21628 /// Binds like unary minus. Out-of-line for the frame reason on
21629 /// `parse_unary_op`.
21630 #[inline(never)]
21631 fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
21632 self.advance();
21633 let e = self.parse_expr(9)?;
21634 Ok(Expr::FunctionCall {
21635 name: String::from("tsquery_not"),
21636 args: alloc::vec![e],
21637 })
21638 }
21639
21640 fn parse_unary(&mut self) -> Result<Expr, ParseError> {
21641 match self.peek() {
21642 // NOT binds tighter than AND / XOR / OR but looser than
21643 // comparisons — its operand takes everything ≥ the comparison
21644 // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
21645 // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
21646 // was rung 3, behaviour-identical when 3 was unused; AND now
21647 // occupies 3, so this must be 4 to keep NOT tighter than AND.)
21648 Token::Not => self.parse_unary_op(UnOp::Not, 4),
21649 // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
21650 // The body is out-of-line: `parse_unary` is one of the three
21651 // frames the parser's MAX_NEST_DEPTH is tuned against, and an
21652 // inline arm here overflowed the native stack in
21653 // `nesting_budget_errors_cleanly` — the guard test caught it,
21654 // exactly as the eval-side cliff did in rounds 346 and 351.
21655 Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
21656 self.parse_binary_prefix()
21657 }
21658 // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
21659 // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
21660 // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
21661 Token::Bang => self.parse_unary_op(UnOp::Not, 9),
21662 Token::Minus => self.parse_prefix_minus(),
21663 // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
21664 // worked only because the lexer reads it as one signed literal;
21665 // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
21666 // PG18 and MariaDB take all of them. Binds like unary minus.
21667 Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
21668 // Bitwise NOT binds like unary minus.
21669 Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
21670 // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
21671 // "center of" operator; desugars to center(x). The whole arm
21672 // is out-of-line: parse_unary sits on the per-nesting-level
21673 // frame chain that MAX_NEST_DEPTH is tuned against, so no
21674 // Expr-sized local may live in this frame.
21675 Token::TsMatch => self.parse_prefix_center(),
21676 // v7.39 (round 508) — the prefix operators that are named
21677 // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
21678 // is length. Out-of-line for the same nesting-frame reason as
21679 // parse_prefix_center — parse_unary sits on the recursive cycle
21680 // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
21681 // live in this frame.
21682 Token::At => self.parse_prefix_call("abs"),
21683 Token::Hash => self.parse_prefix_call("npoints"),
21684 Token::AtMinusAt => self.parse_prefix_call("length"),
21685 // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
21686 // "is horizontal" (lseg / line); desugars to the existing
21687 // isvertical()/ishorizontal() functions. Out-of-line for the
21688 // same nesting-frame reason as parse_prefix_center.
21689 Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
21690 Token::GeomHoriz => self.parse_prefix_geom_axis(false),
21691 Token::DoubleBang => self.parse_prefix_tsquery_not(),
21692 _ => self.parse_atom(),
21693 }
21694 }
21695
21696 /// Parse a parenthesised scalar subquery body after the caller has consumed
21697 /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
21698 /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
21699 /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
21700 /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
21701 /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
21702 /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
21703 /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
21704 /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
21705 /// which sits on the recursive nesting-budget cycle (a few extra bytes there
21706 /// tips the deep-nesting test into a stack overflow).
21707 #[inline(never)]
21708 fn array_subquery_ahead(&self) -> bool {
21709 if !matches!(self.peek(), Token::LParen) {
21710 return false;
21711 }
21712 matches!(
21713 self.tokens.get(self.pos + 1),
21714 Some(Token::Select | Token::Values)
21715 ) || matches!(
21716 self.tokens.get(self.pos + 1),
21717 Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
21718 )
21719 }
21720
21721 /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
21722 /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
21723 /// locals stay off parse_atom's recursive frame (round 105).
21724 #[inline(never)]
21725 fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
21726 self.advance(); // consume `[`
21727 let mut items: Vec<Expr> = Vec::new();
21728 if !matches!(self.peek(), Token::RBracket) {
21729 loop {
21730 // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
21731 // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
21732 if matches!(self.peek(), Token::LBracket) {
21733 items.push(self.parse_array_bracket_body()?);
21734 } else {
21735 items.push(self.parse_expr(0)?);
21736 }
21737 match self.peek() {
21738 Token::Comma => {
21739 self.advance();
21740 }
21741 Token::RBracket => break,
21742 other => {
21743 return Err(self.err(alloc::format!(
21744 "expected ',' or ']' in ARRAY literal, got {other:?}"
21745 )));
21746 }
21747 }
21748 }
21749 }
21750 self.advance(); // consume `]`
21751 Ok(Expr::Array(items))
21752 }
21753
21754 /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
21755 /// is already consumed; the current token is `(`. Desugars to a scalar
21756 /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
21757 /// the subquery's single-column rows in order — reusing the existing
21758 /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
21759 /// keeps the large `Statement` local off parse_atom's recursive frame.
21760 #[inline(never)]
21761 fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
21762 self.advance(); // consume `(`
21763 let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
21764 if w.eq_ignore_ascii_case("with"));
21765 let sub = if is_with {
21766 self.advance(); // WITH
21767 self.parse_with_cte_then_select()?
21768 } else {
21769 self.parse_select_stmt()?
21770 };
21771 if !matches!(self.peek(), Token::RParen) {
21772 return Err(self.err(alloc::format!(
21773 "expected ')' to close ARRAY(subquery), got {:?}",
21774 self.peek()
21775 )));
21776 }
21777 self.advance(); // consume `)`
21778 // Reuse the parser to build the array_agg wrapper from the subquery's
21779 // canonical text — avoids hand-constructing the derived-table AST.
21780 let wrapper = alloc::format!(
21781 "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
21782 );
21783 let stmt = parse_statement(&wrapper)
21784 .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
21785 let Statement::Select(sel) = stmt else {
21786 return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
21787 };
21788 Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
21789 }
21790
21791 #[inline(never)]
21792 fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
21793 let inner = if is_with {
21794 self.advance(); // WITH
21795 self.parse_with_cte_then_select()?
21796 } else {
21797 self.parse_select_stmt()?
21798 };
21799 match self.advance() {
21800 Token::RParen => {
21801 let Statement::Select(s) = inner else {
21802 return Err(ParseError {
21803 message: "scalar subquery body must be a SELECT".into(),
21804 token_pos: self.consumed_pos(),
21805 });
21806 };
21807 Ok(Expr::ScalarSubquery(Box::new(s)))
21808 }
21809 other => Err(ParseError {
21810 message: format!("expected ')' after scalar subquery, got {other:?}"),
21811 token_pos: self.consumed_pos(),
21812 }),
21813 }
21814 }
21815
21816 /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
21817 /// literals. The lexer splits them into an ident + string; recombine
21818 /// here. Out-of-line and returning `Option` so `parse_atom` — the
21819 /// recursive frame the 768 KiB stack budget is tuned against — pays no
21820 /// frame for the `body` / `bits` strings and their char loops (the
21821 /// round-367 frame cliff, M20).
21822 #[inline(never)]
21823 fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
21824 let is_hex = match self.peek() {
21825 Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
21826 Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
21827 _ => return None,
21828 };
21829 if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
21830 return None;
21831 }
21832 self.advance();
21833 let Token::String(body) = self.advance() else {
21834 unreachable!("guarded above");
21835 };
21836 // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
21837 // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
21838 // (hex pairs, even count required — MariaDB errors on an odd
21839 // count); `b'1010'` packs its bits big-endian, left-padded to a
21840 // byte. Lower both onto the bytea cast.
21841 if self.mysql_dialect {
21842 if is_hex {
21843 if body.len() % 2 == 1 {
21844 return Some(Err(self.err(alloc::format!(
21845 "invalid hex string literal X'{body}': odd digit count"
21846 ))));
21847 }
21848 for c in body.chars() {
21849 if !c.is_ascii_hexdigit() {
21850 return Some(Err(
21851 self.err(alloc::format!("invalid hexadecimal digit {c:?} in X'…'"))
21852 ));
21853 }
21854 }
21855 return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
21856 }
21857 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21858 return Some(Err(
21859 self.err(alloc::format!("invalid binary digit {bad:?} in b'…'"))
21860 ));
21861 }
21862 return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
21863 }
21864 let bits = if is_hex {
21865 let mut out = String::with_capacity(body.len() * 4);
21866 for c in body.chars() {
21867 let Some(d) = c.to_digit(16) else {
21868 return Some(Err(self.err(alloc::format!(
21869 "invalid hexadecimal digit {c:?} in X'…' bit string"
21870 ))));
21871 };
21872 out.push_str(&alloc::format!("{d:04b}"));
21873 }
21874 out
21875 } else {
21876 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21877 return Some(Err(self.err(alloc::format!(
21878 "invalid binary digit {bad:?} in B'…' bit string"
21879 ))));
21880 }
21881 body
21882 };
21883 // Route through the postfix-cast loop so a chained cast like
21884 // `B'1010'::int` attaches onto the implicit `::bit` cast instead
21885 // of erroring at the `::`.
21886 // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
21887 // literal keeps its exact length, while an explicit `::bit` cast is
21888 // bit(1) with pad/truncate semantics (PG).
21889 Some(self.finish_postfix_casts(Expr::Cast {
21890 expr: Box::new(Expr::Literal(Literal::String(bits))),
21891 target: CastTarget::Named("__bit_literal".to_string()),
21892 }))
21893 }
21894
21895 fn parse_atom(&mut self) -> Result<Expr, ParseError> {
21896 if let Some(res) = self.try_parse_bit_string_literal() {
21897 return res;
21898 }
21899 let tok_pos = self.pos;
21900 match self.advance() {
21901 Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
21902 Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
21903 // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
21904 // carrying the source mantissa + scale so no precision is lost. A
21905 // literal too wide for i128 falls back to double precision.
21906 // Out-of-line (#[inline(never)]) — this arm sits on the
21907 // parse_expr recursion chain; its expansion locals must not
21908 // widen the recursive frame (debug frame-cliff discipline).
21909 Token::Numeric(s) => match numeric_token_to_literal(s) {
21910 Ok(lit) => Ok(Expr::Literal(lit)),
21911 Err(msg) => Err(self.err(msg)),
21912 },
21913 Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
21914 // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
21915 // (the lexer only emits this token in the MySQL dialect). Lower
21916 // onto the existing bytea cast; out-of-line to keep this arm off
21917 // the parse recursion frame.
21918 Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
21919 Token::True => Ok(Expr::Literal(Literal::Bool(true))),
21920 Token::False => Ok(Expr::Literal(Literal::Bool(false))),
21921 Token::Null => Ok(Expr::Literal(Literal::Null)),
21922 // v6.1.1 — `$N` placeholder. The actual Value lookup
21923 // happens in the engine eval path against the prepared-
21924 // statement bind buffer.
21925 Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
21926 Token::LParen => {
21927 // v4.10: `(SELECT ...)` in expression position is a
21928 // scalar subquery; otherwise it's a parenthesised
21929 // expression. Peek for SELECT keyword to dispatch.
21930 // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
21931 // lexes as Ident("with") (not a reserved token). The subquery body
21932 // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
21933 // so its large `Statement` local stays out of parse_atom's stack
21934 // frame — parse_atom is on the recursive `((…))` cycle and the
21935 // nesting budget is tuned to its frame size).
21936 let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21937 if s.eq_ignore_ascii_case("with"));
21938 if matches!(self.peek(), Token::Select) || is_with {
21939 self.parse_paren_scalar_subquery(is_with)
21940 } else {
21941 let e = self.parse_expr(0)?;
21942 // `(a, b, …)` — a row constructor. Valid only
21943 // in front of a comparison operator or [NOT]
21944 // IN; both expand at parse time (lexicographic
21945 // comparison / OR'd row equalities).
21946 if matches!(self.peek(), Token::Comma) {
21947 let mut row = alloc::vec![e];
21948 while matches!(self.peek(), Token::Comma) {
21949 self.advance();
21950 row.push(self.parse_expr(0)?);
21951 }
21952 if !matches!(self.peek(), Token::RParen) {
21953 return Err(self.err(alloc::format!(
21954 "expected ')' after row constructor, got {:?}",
21955 self.peek()
21956 )));
21957 }
21958 self.advance();
21959 // A bare `(a, b, …)` row constructor can carry postfix
21960 // (`::text`, `.field`) just like `ROW(a, b, …)`; the
21961 // early return here skips parse_atom's tail postfix
21962 // pass, so fold casts in explicitly. For the
21963 // comparison / predicate forms nothing postfix follows,
21964 // so this is a no-op.
21965 return self
21966 .parse_row_comparison_tail(row)
21967 .and_then(|e| self.finish_postfix_casts(e));
21968 }
21969 match self.advance() {
21970 Token::RParen => Ok(e),
21971 other => Err(ParseError {
21972 message: format!("expected ')', got {other:?}"),
21973 token_pos: self.consumed_pos(),
21974 }),
21975 }
21976 }
21977 }
21978 Token::LBracket => self.parse_vector_literal_body(),
21979 Token::Extract => self.parse_extract_atom(),
21980 Token::Interval => self.parse_interval_atom(),
21981 // `LEFT` / `RIGHT` are reserved-keyword tokens because the
21982 // grammar dedicates arms for `LEFT [OUTER] JOIN` /
21983 // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
21984 // expression position calling the PG `left(string, n)` /
21985 // `right(string, n)` function; rebuild the AST as a regular
21986 // function call so the engine's apply_function dispatch picks
21987 // it up. Delegated to a #[inline(never)] helper so its locals
21988 // don't bloat this recursive `parse_atom` frame (the nesting
21989 // budget in `enter_nested` is tuned to parse_atom's size).
21990 Token::Left if matches!(self.peek(), Token::LParen) => {
21991 self.parse_lr_string_function_call("left")
21992 }
21993 Token::Right if matches!(self.peek(), Token::LParen) => {
21994 self.parse_lr_string_function_call("right")
21995 }
21996 // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
21997 // token; we match on the bare ident. NOT is a token
21998 // (consumed in the comparison rung), but `EXISTS (...)`
21999 // at the top of an expression starts here.
22000 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
22001 self.parse_exists_atom(false)
22002 }
22003 // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
22004 // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
22005 // CASE is a bare ident; we dispatch on lowercase match.
22006 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
22007 self.parse_case_atom()
22008 }
22009 // v7.37.17 (17.6 siblings) — PG typed datetime literals:
22010 // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
22011 // '…'`. Lower onto the ::cast node so the existing
22012 // runtime text→date/timestamp paths do the parsing. The
22013 // string must follow immediately, else the ident stays a
22014 // plain column reference.
22015 Token::Ident(s)
22016 if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
22017 && matches!(self.peek(), Token::String(_)) =>
22018 {
22019 let target =
22020 typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
22021 let Token::String(lit) = self.advance() else {
22022 unreachable!("peek guaranteed a string token");
22023 };
22024 Ok(Expr::Cast {
22025 expr: Box::new(Expr::Literal(Literal::String(lit))),
22026 target,
22027 })
22028 }
22029 // v7.39 (round 221) — the SQL-standard long spellings:
22030 // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
22031 // TIME ZONE '…'`. Consume the modifier and lower to the same
22032 // typed-literal cast (`timetz` / `timestamptz` for WITH).
22033 Token::Ident(s)
22034 if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
22035 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
22036 || w.eq_ignore_ascii_case("without"))
22037 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
22038 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
22039 && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
22040 {
22041 let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
22042 self.advance(); // WITH / WITHOUT
22043 self.advance(); // TIME
22044 self.advance(); // ZONE
22045 let Token::String(lit) = self.advance() else {
22046 unreachable!("guard checked a string token");
22047 };
22048 let base = s.to_ascii_lowercase();
22049 let target = match (base.as_str(), with_tz) {
22050 ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
22051 ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
22052 (_, true) => CastTarget::Timestamptz,
22053 (_, false) => CastTarget::Timestamp,
22054 };
22055 Ok(Expr::Cast {
22056 expr: Box::new(Expr::Literal(Literal::String(lit))),
22057 target,
22058 })
22059 }
22060 // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
22061 // gathers the subquery's single-column rows (in its row order)
22062 // into an array. Desugared to `array_agg` over the subquery as a
22063 // derived table; out-of-line to keep parse_atom's frame small (it
22064 // sits on the recursive nesting-budget cycle).
22065 Token::Ident(s) | Token::QuotedIdent(s)
22066 if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
22067 {
22068 self.parse_array_subquery()
22069 }
22070 // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
22071 // is not a reserved token; we match by case-insensitive
22072 // ident. The opening `[` must follow immediately. v7.39 (read01
22073 // round 105) — the body moved out-of-line so its `Vec`/loop locals
22074 // leave parse_atom's frame (which sits on the nesting-budget cycle).
22075 Token::Ident(s) | Token::QuotedIdent(s)
22076 if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
22077 {
22078 self.parse_array_literal_body()
22079 }
22080 // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
22081 // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
22082 // We special-case before the generic ident dispatch so
22083 // the AGAINST clause never reaches the function-call
22084 // loop (which would mis-read `(cols) AGAINST` as a
22085 // call with no trailing modifier). The shape is
22086 // rewritten to a Boolean OR over per-column
22087 // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
22088 // term)` so the existing FTS evaluator handles
22089 // semantics — the fulltext-GIN built at CREATE TABLE
22090 // time is currently a "real index that survives dump
22091 // round-trip"; the planner hook that actually uses
22092 // it for posting-list intersection lands in a later
22093 // sub-phase (Phase 2.2b) without touching this surface.
22094 Token::Ident(s) | Token::QuotedIdent(s)
22095 if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
22096 {
22097 self.parse_match_against_atom()
22098 }
22099 Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
22100 // v7.37.43-T4 — PG-unreserved keywords are legal column /
22101 // alias names in expression context too. `release` appears
22102 // in sentori `0003_partition_events.sql` as both a column
22103 // reference (SELECT … release …) and an INSERT column list
22104 // entry. Mirrors `expect_ident_like`'s expansion of the
22105 // identifier set.
22106 other if unreserved_keyword_text(&other).is_some() => {
22107 let s = unreserved_keyword_text(&other).unwrap();
22108 self.finish_ident_atom(s)
22109 }
22110 // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
22111 // only inside `SET` before, so `SELECT @@autocommit` — which
22112 // every MySQL connector asks at handshake — was a parse error.
22113 // MariaDB accepts the bare, `@@session.` and `@@global.`
22114 // spellings alike and answers from the session's own value.
22115 Token::SessionVar(v) => {
22116 // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
22117 // has nothing to do with a `@@` engine setting: its own
22118 // per-session namespace, and an unset one reads NULL instead
22119 // of raising. Stripping every `@` (as this did) made `@x` and
22120 // `@@x` the same node, so `SELECT @x` answered "Unknown
22121 // system variable".
22122 Ok(variable_ref_atom(&v))
22123 }
22124 other => Err(ParseError {
22125 message: format!("unexpected token {other:?} in expression"),
22126 token_pos: tok_pos,
22127 }),
22128 }
22129 // After parsing the atom, fold any postfix `::vector` casts.
22130 .and_then(|atom| self.finish_postfix_casts(atom))
22131 }
22132
22133 /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
22134 /// Both bind tighter than any binary op.
22135 /// Shared cast-target parser for postfix `::TYPE` and the
22136 /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
22137 /// If the next tokens are `( N )`, consume them and return the canonical
22138 /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
22139 /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
22140 fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
22141 if !matches!(self.peek(), Token::LParen) {
22142 return None;
22143 }
22144 self.advance(); // (
22145 let n = match self.advance() {
22146 Token::Integer(n) => n,
22147 _ => return Some(base.to_string()), // malformed → drop precision
22148 };
22149 if matches!(self.peek(), Token::RParen) {
22150 self.advance();
22151 }
22152 Some(alloc::format!("{base}({n})"))
22153 }
22154
22155 fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
22156 // r1052 — `::pg_catalog.regproc` and friends: pg_dump
22157 // schema-qualifies every cast target, and `pg_catalog.X` names
22158 // exactly the builtin type X. Consume the qualifier and let
22159 // the ordinary target parse decide.
22160 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
22161 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
22162 {
22163 self.advance();
22164 self.advance();
22165 }
22166 let target = match self.advance() {
22167 Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
22168 "int" | "integer" | "int4" => {
22169 if matches!(self.peek(), Token::LBracket)
22170 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22171 {
22172 self.advance();
22173 self.advance();
22174 CastTarget::IntArray
22175 } else {
22176 CastTarget::Int
22177 }
22178 }
22179 "bigint" | "int8" => {
22180 if matches!(self.peek(), Token::LBracket)
22181 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22182 {
22183 self.advance();
22184 self.advance();
22185 CastTarget::BigIntArray
22186 } else {
22187 CastTarget::BigInt
22188 }
22189 }
22190 "float" | "double" => CastTarget::Float,
22191 "text" => {
22192 // v7.10.11 — `::TEXT[]` widens to TextArray.
22193 if matches!(self.peek(), Token::LBracket)
22194 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22195 {
22196 self.advance();
22197 self.advance();
22198 CastTarget::TextArray
22199 } else {
22200 CastTarget::Text
22201 }
22202 }
22203 "bool" | "boolean" => CastTarget::Bool,
22204 "vector" => CastTarget::Vector,
22205 "date" => CastTarget::Date,
22206 // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22207 // seconds precision through the Named path (the engine rounds
22208 // the sub-second field); bare `::timestamp` keeps the fast arm.
22209 "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22210 Some(named) => CastTarget::Named(named),
22211 None => CastTarget::Timestamp,
22212 },
22213 "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22214 Some(named) => CastTarget::Named(named),
22215 None => CastTarget::Timestamptz,
22216 },
22217 "interval" => CastTarget::Interval,
22218 "json" => CastTarget::Json,
22219 "jsonb" => CastTarget::Jsonb,
22220 // v7.39 (round 694) — these have dedicated CastTarget
22221 // variants, so they never reached the postfix `[]` handling
22222 // further down and `::regtype[]` was a SYNTAX error at the
22223 // `]`. PG has an array type for every scalar; take the
22224 // suffix here and hand the canonical `<ty>_array` name to
22225 // the engine, the same shape every other array cast uses.
22226 "regtype" if self.peek_postfix_array_brackets() => {
22227 self.advance();
22228 self.advance();
22229 CastTarget::Named(alloc::string::String::from("regtype_array"))
22230 }
22231 "regclass" if self.peek_postfix_array_brackets() => {
22232 self.advance();
22233 self.advance();
22234 CastTarget::Named(alloc::string::String::from("regclass_array"))
22235 }
22236 "regtype" => CastTarget::RegType,
22237 "regclass" => CastTarget::RegClass,
22238 // v7.12.0 — `::tsvector` / `::tsquery`.
22239 // Engine decodes the LHS text via the PG
22240 // external form parser.
22241 // v7.39 (round 352, M8) — MySQL's own cast targets.
22242 // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22243 // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22244 // such type, so they are taken only in that dialect and
22245 // fall through to the "type does not exist" arm otherwise.
22246 "signed" | "unsigned" if self.mysql_dialect => {
22247 if matches!(self.peek(), Token::Ident(k)
22248 if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22249 {
22250 self.advance();
22251 }
22252 CastTarget::Named(s.to_ascii_lowercase())
22253 }
22254 // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22255 // in MySQL: MariaDB answers '123' where the SQL-standard
22256 // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22257 // Truncating a number to its first digit is a wrong answer
22258 // with no error, so the MySQL session gets MySQL's reading.
22259 "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22260 CastTarget::Text
22261 }
22262 "tsvector" => CastTarget::TsVector,
22263 "tsquery" => CastTarget::TsQuery,
22264 // v7.17.0 — `::uuid`. Engine decodes the LHS
22265 // text via `spg_storage::parse_uuid_str`.
22266 "uuid" => CastTarget::Uuid,
22267 // v7.18 — `::bytea`. Engine decodes the LHS
22268 // text via the PG hex form (`'\xdeadbeef'`)
22269 // or escape form (`'\\x05\\x00'`). Closes
22270 // mailrs D-pre #3 reverse-acceptance gap.
22271 "bytea" => CastTarget::Bytea,
22272 // v7.37.5 ship triage — generic typed-cast escape.
22273 // Anything the long-tail PG type ident table knows
22274 // about(network/bit/geometry/multirange/etc.)flows
22275 // through `CastTarget::Named(canonical)`; the engine
22276 // resolves via `column_type_to_data_type` and dispatches
22277 // through the typed `coerce_value` path. Truly
22278 // unrecognised idents still hit the error arm below
22279 // because the engine rejects them.
22280 other => {
22281 // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22282 // `::varchar(255)`, etc. Capture into the canonical
22283 // `name(p,s)` form so `type_name_to_data_type` can
22284 // reconstruct the `DataType::Numeric { precision,
22285 // scale }` (and similar param-carrying types).
22286 let mut name = other.to_string();
22287 // v7.39 (round 281) — `::bit varying(3)` is two
22288 // words; fold the tail in so the typmod reaches the
22289 // type resolver instead of tripping the parser.
22290 if name.eq_ignore_ascii_case("bit")
22291 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22292 {
22293 self.advance();
22294 name = alloc::string::String::from("varbit");
22295 }
22296 // v7.39 (round 613) — `::character varying` is the same
22297 // two-word shape and had no fold, so the `varying` was
22298 // left behind and the cast became a bare `character`,
22299 // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22300 // `a` where PG answers `ab`. Silently, and for a spelling
22301 // pg_dump writes.
22302 if name.eq_ignore_ascii_case("character")
22303 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22304 {
22305 self.advance();
22306 name = alloc::string::String::from("varchar");
22307 }
22308 if matches!(self.peek(), Token::LParen) {
22309 let mut buf = alloc::string::String::from("(");
22310 let mut depth = 0usize;
22311 loop {
22312 match self.advance() {
22313 Token::LParen => {
22314 depth += 1;
22315 if depth > 1 {
22316 buf.push('(');
22317 }
22318 }
22319 Token::RParen => {
22320 depth -= 1;
22321 if depth == 0 {
22322 buf.push(')');
22323 break;
22324 }
22325 buf.push(')');
22326 }
22327 Token::Comma => buf.push(','),
22328 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22329 // v7.39 (round 273) — a minus used to fall
22330 // into the catch-all below and vanish, so
22331 // `::numeric(10,-2)` reached the engine as
22332 // the text `numeric(10,2)` and silently
22333 // rounded to two DECIMALS instead of to
22334 // hundreds. A dropped token is not a
22335 // no-op when it carries a sign.
22336 Token::Minus => buf.push('-'),
22337 Token::Eof => break,
22338 _ => {}
22339 }
22340 }
22341 name.push_str(&buf);
22342 }
22343 // Optional postfix `[]` widens to the array form —
22344 // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22345 // The engine's `type_name_to_data_type` recognises
22346 // the canonical `<ty>_array` form.
22347 if matches!(self.peek(), Token::LBracket)
22348 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22349 {
22350 self.advance();
22351 self.advance();
22352 name.push_str("_array");
22353 }
22354 CastTarget::Named(name)
22355 }
22356 },
22357 Token::Interval => CastTarget::Interval,
22358 // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22359 // "char" (oid 18, SPG Char1 — distinct from bare `char`
22360 // = char(1)); other quoted names resolve like idents.
22361 Token::QuotedIdent(q) => {
22362 if q.eq_ignore_ascii_case("char") {
22363 CastTarget::Named("char1".into())
22364 } else {
22365 CastTarget::Named(q.to_ascii_lowercase())
22366 }
22367 }
22368 other => {
22369 return Err(ParseError {
22370 message: format!("expected type ident after `::`, got {other:?}"),
22371 token_pos: self.consumed_pos(),
22372 });
22373 }
22374 };
22375 // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22376 // target to its array sibling. Closed-enum arms (Bool /
22377 // SmallInt / Numeric / Float / Date / …) didn't carry the
22378 // explicit widening that Text / Int / BigInt did, so
22379 // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22380 // error. The widening here mirrors the per-arm Text /
22381 // Int / BigInt logic above + folds the new ζ-A first-class
22382 // types through `CastTarget::Named("<ty>_array")`.
22383 if matches!(self.peek(), Token::LBracket)
22384 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22385 {
22386 let widened = match &target {
22387 CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22388 CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22389 // v7.39 (round 326, V43) — the two temporal types stay
22390 // distinct. Both used to widen to `timestamptz_array`, so
22391 // `::timestamp[]` named the wrong target in its own error
22392 // message and lost the zone-less identity on the way.
22393 CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22394 CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22395 CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22396 CastTarget::Json | CastTarget::Jsonb => {
22397 Some(CastTarget::Named("jsonb_array".to_string()))
22398 }
22399 CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22400 CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22401 CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22402 CastTarget::Named(name) => {
22403 let mut a = name.clone();
22404 a.push_str("_array");
22405 Some(CastTarget::Named(a))
22406 }
22407 // Int / BigInt / Text / Vector / TsVector / TsQuery /
22408 // RegType / RegClass / TextArray / IntArray /
22409 // BigIntArray already finalised — leave as is.
22410 _ => None,
22411 };
22412 if let Some(w) = widened {
22413 self.advance();
22414 self.advance();
22415 return Ok(w);
22416 }
22417 }
22418 Ok(target)
22419 }
22420
22421 fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
22422 loop {
22423 // v7.38 (read01, T9) — composite field access `(expr).field`.
22424 // A bare `a.b` is consumed as a qualified column inside the ident
22425 // atom, so a Dot only survives to this postfix position when the
22426 // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
22427 // `.*` whole-row expansion is not handled here (projection-level).
22428 if matches!(self.peek(), Token::Dot)
22429 && matches!(
22430 self.tokens.get(self.pos + 1),
22431 Some(Token::Ident(_) | Token::QuotedIdent(_))
22432 )
22433 {
22434 self.advance(); // .
22435 let field = match self.advance() {
22436 Token::Ident(s) | Token::QuotedIdent(s) => s,
22437 other => {
22438 return Err(
22439 self.err(format!("expected a field name after '.', got {other:?}"))
22440 );
22441 }
22442 };
22443 expr = Expr::FieldAccess {
22444 base: Box::new(expr),
22445 field,
22446 };
22447 continue;
22448 }
22449 if matches!(self.peek(), Token::DoubleColon) {
22450 self.advance();
22451 // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
22452 // target set to include INTERVAL (reserved Token),
22453 // TIMESTAMPTZ, and PG catalog regtype / regclass.
22454 // mailrs follow-up H3a + H3b.
22455 let target = self.parse_cast_target()?;
22456 expr = Expr::Cast {
22457 expr: Box::new(expr),
22458 target,
22459 };
22460 continue;
22461 }
22462 // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
22463 // returns NULL for out-of-range. Multiple subscripts
22464 // chain: `a[i][j]` parses left-to-right.
22465 if matches!(self.peek(), Token::LBracket) {
22466 self.advance();
22467 // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
22468 // bare index stays a subscript.
22469 let lo = if matches!(self.peek(), Token::Colon) {
22470 None
22471 } else {
22472 Some(self.parse_expr(0)?)
22473 };
22474 if matches!(self.peek(), Token::Colon) {
22475 self.advance();
22476 let hi = if matches!(self.peek(), Token::RBracket) {
22477 None
22478 } else {
22479 Some(Box::new(self.parse_expr(0)?))
22480 };
22481 if !matches!(self.peek(), Token::RBracket) {
22482 return Err(self.err(alloc::format!(
22483 "expected ']' after array slice, got {:?}",
22484 self.peek()
22485 )));
22486 }
22487 self.advance();
22488 expr = Expr::ArraySlice {
22489 target: Box::new(expr),
22490 lo: lo.map(Box::new),
22491 hi,
22492 };
22493 continue;
22494 }
22495 let index = lo.expect("non-colon branch parsed an index");
22496 if !matches!(self.peek(), Token::RBracket) {
22497 return Err(self.err(alloc::format!(
22498 "expected ']' after array index, got {:?}",
22499 self.peek()
22500 )));
22501 }
22502 self.advance();
22503 expr = Expr::ArraySubscript {
22504 target: Box::new(expr),
22505 index: Box::new(index),
22506 };
22507 continue;
22508 }
22509 // `expr AT TIME ZONE zone` — lowers to PG's own function
22510 // form timezone(zone, expr); the scalar implements the
22511 // offset shift (named zones error there — no tzdata).
22512 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
22513 && matches!(self.tokens.get(self.pos + 1),
22514 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
22515 && matches!(self.tokens.get(self.pos + 2),
22516 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
22517 {
22518 self.advance(); // AT
22519 self.advance(); // TIME
22520 self.advance(); // ZONE
22521 // Zone at comparison precedence so AND/OR stay out.
22522 let zone = self.parse_expr(6)?;
22523 expr = Expr::FunctionCall {
22524 name: "timezone".to_string(),
22525 args: alloc::vec![zone, expr],
22526 };
22527 continue;
22528 }
22529 // `expr COLLATE "name"` — SPG's single text ordering IS
22530 // byte order, i.e. the C collation. The byte-order
22531 // spellings absorb as no-ops; a locale collation would
22532 // silently sort differently from PG, so it errors
22533 // honestly instead.
22534 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
22535 self.advance();
22536 let mut cname = match self.advance() {
22537 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22538 other => {
22539 return Err(self.err(alloc::format!(
22540 "expected collation name after COLLATE, got {other:?}"
22541 )));
22542 }
22543 };
22544 // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
22545 // is how `pg_dump` writes the default one:
22546 // `… COLLATE pg_catalog.default`. Reading a single token
22547 // left the SCHEMA as the name, so the clause was refused
22548 // as an unsupported locale collation and no dump ran.
22549 if matches!(self.peek(), Token::Dot) {
22550 self.advance();
22551 cname = match self.advance() {
22552 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22553 // `default` lexes as a KEYWORD, and it is the name
22554 // pg_dump writes — the same trap round 535 hit with
22555 // TABLE / INDEX / FULL.
22556 Token::Default => alloc::string::String::from("default"),
22557 other => {
22558 return Err(self.err(alloc::format!(
22559 "expected collation name after COLLATE, got {other:?}"
22560 )));
22561 }
22562 };
22563 }
22564 let lc = cname.to_ascii_lowercase();
22565 // v7.39 (round 371, M4 P4b) — a per-expression MySQL
22566 // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
22567 // family / `binary`) forces byte-wise, which is exactly
22568 // what `BINARY expr` does — lower onto that so every fold
22569 // site (comparison, LIKE, ORDER BY) suppresses via
22570 // `is_binary_coerced`. A `_ci` family override folds, and
22571 // under the MySQL dialect the default already folds, so it
22572 // absorbs as a no-op; likewise the C / byte-order spellings.
22573 if self.mysql_dialect && (lc.ends_with("_bin") || lc == "binary") {
22574 expr = Expr::Cast {
22575 expr: alloc::boxed::Box::new(expr),
22576 target: CastTarget::Named("binary".to_string()),
22577 };
22578 continue;
22579 }
22580 let mysql_ci = self.mysql_dialect
22581 && (lc.ends_with("_ci")
22582 || matches!(lc.as_str(), "case_insensitive" | "nocase"));
22583 // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
22584 // goes to the lowering channel, the byte-order spellings
22585 // included. Round 691 recorded only the names the old
22586 // allow-list rejected, which left `ORDER BY a COLLATE "C"`
22587 // absorbed as a no-op — and once a column could declare a
22588 // collation, absorbing the clause meant the COLUMN's
22589 // collation won where the query had asked for bytes.
22590 if self.in_order_by_key && !mysql_ci {
22591 self.order_key_collation = Some(cname);
22592 continue;
22593 }
22594 if !matches!(
22595 lc.as_str(),
22596 "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
22597 ) && !mysql_ci
22598 {
22599 return Err(self.err(alloc::format!(
22600 "COLLATE {cname:?}: SPG orders text by bytes (the C \
22601 collation); locale collations are not supported yet — \
22602 use COLLATE \"C\" or drop the clause"
22603 )));
22604 }
22605 continue;
22606 }
22607 return Ok(expr);
22608 }
22609 }
22610
22611 /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
22612 /// the first token that is not one. Schema qualifiers collapse to the
22613 /// last part, which is what every other name path here does (SPG is
22614 /// single-schema).
22615 fn take_comma_separated_names(&mut self) -> Vec<String> {
22616 let mut out = Vec::new();
22617 while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
22618 self.advance();
22619 let mut last = n;
22620 while matches!(self.peek(), Token::Dot) {
22621 self.advance();
22622 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
22623 last = t;
22624 }
22625 }
22626 out.push(last);
22627 if matches!(self.peek(), Token::Comma) {
22628 self.advance();
22629 } else {
22630 break;
22631 }
22632 }
22633 out
22634 }
22635
22636 /// v7.39 (round 694) — is the next token pair a postfix `[]`?
22637 ///
22638 /// The general cast-target path tests this inline; the types with their
22639 /// own `CastTarget` variant need it as a guard on their match arm,
22640 /// which is what this exists for.
22641 fn peek_postfix_array_brackets(&self) -> bool {
22642 matches!(self.peek(), Token::LBracket)
22643 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22644 }
22645
22646 /// Parse the operator tail after a `(a, b, …)` row constructor
22647 /// and expand at parse time. `=` is the conjunction of element
22648 /// equalities; `<>` its negation; the order operators expand
22649 /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
22650 /// equalities. Anything else (a bare row value, a subquery
22651 /// RHS) errors honestly — SPG has no composite runtime value.
22652 fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
22653 fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
22654 let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
22655 lhs: Box::new(l.clone()),
22656 op: BinOp::Eq,
22657 rhs: Box::new(r.clone()),
22658 });
22659 let first = it.next().expect("row has at least two elements");
22660 it.fold(first, |acc, e| Expr::Binary {
22661 lhs: Box::new(acc),
22662 op: BinOp::And,
22663 rhs: Box::new(e),
22664 })
22665 }
22666 // Lexicographic (a,b) OP (c,d):
22667 // a STRICT c OR (a = c AND (b OP d)) — recursing right.
22668 fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
22669 if lhs.len() == 1 {
22670 return Expr::Binary {
22671 lhs: Box::new(lhs[0].clone()),
22672 op: last,
22673 rhs: Box::new(rhs[0].clone()),
22674 };
22675 }
22676 let head_strict = Expr::Binary {
22677 lhs: Box::new(lhs[0].clone()),
22678 op: strict,
22679 rhs: Box::new(rhs[0].clone()),
22680 };
22681 let head_eq = Expr::Binary {
22682 lhs: Box::new(lhs[0].clone()),
22683 op: BinOp::Eq,
22684 rhs: Box::new(rhs[0].clone()),
22685 };
22686 Expr::Binary {
22687 lhs: Box::new(head_strict),
22688 op: BinOp::Or,
22689 rhs: Box::new(Expr::Binary {
22690 lhs: Box::new(head_eq),
22691 op: BinOp::And,
22692 rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
22693 }),
22694 }
22695 }
22696 let negated_in = if matches!(self.peek(), Token::Not)
22697 && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
22698 {
22699 self.advance();
22700 true
22701 } else {
22702 false
22703 };
22704 if matches!(self.peek(), Token::In) {
22705 self.advance();
22706 if !matches!(self.peek(), Token::LParen) {
22707 return Err(self.err(alloc::format!(
22708 "expected '(' after row IN, got {:?}",
22709 self.peek()
22710 )));
22711 }
22712 self.advance();
22713 // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
22714 // not a list of literal rows. Row-vs-list decomposes to
22715 // OR-of-AND above, but the subquery's rows are only known at
22716 // runtime, so keep it as a RowInSubquery node.
22717 if matches!(self.peek(), Token::Select) {
22718 let inner = self.parse_select_stmt()?;
22719 if !matches!(self.peek(), Token::RParen) {
22720 return Err(self.err(alloc::format!(
22721 "expected ')' after row IN-subquery, got {:?}",
22722 self.peek()
22723 )));
22724 }
22725 self.advance();
22726 let Statement::Select(s) = inner else {
22727 unreachable!("parse_select_stmt always returns Statement::Select")
22728 };
22729 return Ok(Expr::RowInSubquery {
22730 row,
22731 subquery: Box::new(s),
22732 negated: negated_in,
22733 });
22734 }
22735 let mut alternatives: Vec<Expr> = Vec::new();
22736 loop {
22737 // Optional ROW keyword before the paren row.
22738 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22739 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22740 {
22741 self.advance();
22742 }
22743 if !matches!(self.peek(), Token::LParen) {
22744 return Err(self.err(alloc::format!(
22745 "expected '(' to open a row inside IN, got {:?}",
22746 self.peek()
22747 )));
22748 }
22749 self.advance();
22750 let mut rhs = alloc::vec![self.parse_expr(0)?];
22751 while matches!(self.peek(), Token::Comma) {
22752 self.advance();
22753 rhs.push(self.parse_expr(0)?);
22754 }
22755 if !matches!(self.peek(), Token::RParen) {
22756 return Err(self.err(alloc::format!(
22757 "expected ')' after row inside IN, got {:?}",
22758 self.peek()
22759 )));
22760 }
22761 self.advance();
22762 if rhs.len() != row.len() {
22763 return Err(self.err(alloc::format!(
22764 "row IN arity mismatch: left has {}, right has {}",
22765 row.len(),
22766 rhs.len()
22767 )));
22768 }
22769 alternatives.push(row_eq(&row, &rhs));
22770 if matches!(self.peek(), Token::Comma) {
22771 self.advance();
22772 continue;
22773 }
22774 break;
22775 }
22776 if !matches!(self.peek(), Token::RParen) {
22777 return Err(self.err(alloc::format!(
22778 "expected ')' to close row IN list, got {:?}",
22779 self.peek()
22780 )));
22781 }
22782 self.advance();
22783 let mut it = alternatives.into_iter();
22784 let first = it.next().expect("IN list has at least one row");
22785 let combined = it.fold(first, |acc, e| Expr::Binary {
22786 lhs: Box::new(acc),
22787 op: BinOp::Or,
22788 rhs: Box::new(e),
22789 });
22790 return Ok(maybe_not(combined, negated_in));
22791 }
22792 // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
22793 // two periods share at least one time point. Each pair is
22794 // normalised with least/greatest (PG accepts the endpoints
22795 // in either order), then lowered to the standard
22796 // `start1 < end2 AND start2 < end1` form.
22797 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
22798 if row.len() != 2 {
22799 return Err(self.err(alloc::format!(
22800 "OVERLAPS needs (start, end) pairs; left side has {} elements",
22801 row.len()
22802 )));
22803 }
22804 self.advance();
22805 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22806 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22807 {
22808 self.advance();
22809 }
22810 if !matches!(self.peek(), Token::LParen) {
22811 return Err(self.err(alloc::format!(
22812 "expected '(' after OVERLAPS, got {:?}",
22813 self.peek()
22814 )));
22815 }
22816 self.advance();
22817 let r0 = self.parse_expr(0)?;
22818 if !matches!(self.peek(), Token::Comma) {
22819 return Err(self.err(alloc::format!(
22820 "OVERLAPS needs (start, end) on the right, got {:?}",
22821 self.peek()
22822 )));
22823 }
22824 self.advance();
22825 let r1 = self.parse_expr(0)?;
22826 if !matches!(self.peek(), Token::RParen) {
22827 return Err(self.err(alloc::format!(
22828 "expected ')' after OVERLAPS pair, got {:?}",
22829 self.peek()
22830 )));
22831 }
22832 self.advance();
22833 let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
22834 name: String::from(name),
22835 args: alloc::vec![a.clone(), b.clone()],
22836 };
22837 let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
22838 lhs: Box::new(lhs),
22839 op: BinOp::Lt,
22840 rhs: Box::new(rhs),
22841 };
22842 return Ok(Expr::Binary {
22843 lhs: Box::new(lt(
22844 pair_fn("least", &row[0], &row[1]),
22845 pair_fn("greatest", &r0, &r1),
22846 )),
22847 op: BinOp::And,
22848 rhs: Box::new(lt(
22849 pair_fn("least", &r0, &r1),
22850 pair_fn("greatest", &row[0], &row[1]),
22851 )),
22852 });
22853 }
22854 // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
22855 // PG, `IS NULL` is true only when EVERY field is NULL, and
22856 // `IS NOT NULL` is true only when every field is non-NULL — the
22857 // latter is NOT the negation of the former (a mixed row is
22858 // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
22859 // which reproduces exactly that all-fields semantics.
22860 if matches!(self.peek(), Token::Is) {
22861 self.advance();
22862 let negated = if matches!(self.peek(), Token::Not) {
22863 self.advance();
22864 true
22865 } else {
22866 false
22867 };
22868 if !matches!(self.peek(), Token::Null) {
22869 return Err(self.err(alloc::format!(
22870 "expected NULL after row IS [NOT], got {:?}",
22871 self.peek()
22872 )));
22873 }
22874 self.advance();
22875 let mut it = row.iter().map(|e| Expr::IsNull {
22876 expr: Box::new(e.clone()),
22877 negated,
22878 });
22879 let first = it.next().expect("row has at least two elements");
22880 return Ok(it.fold(first, |acc, e| Expr::Binary {
22881 lhs: Box::new(acc),
22882 op: BinOp::And,
22883 rhs: Box::new(e),
22884 }));
22885 }
22886 let op = match self.peek() {
22887 Token::Eq => BinOp::Eq,
22888 Token::NotEq => BinOp::NotEq,
22889 Token::Lt => BinOp::Lt,
22890 Token::LtEq => BinOp::LtEq,
22891 Token::Gt => BinOp::Gt,
22892 Token::GtEq => BinOp::GtEq,
22893 // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
22894 // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
22895 // constructor value, identical to the `ROW(a, b, …)` keyword form:
22896 // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
22897 // (`::text`, `.field`) applies at the caller just as it does for the
22898 // ROW(...) node. All the comparison / predicate forms returned above.
22899 _ => {
22900 return Ok(Expr::FunctionCall {
22901 name: String::from("row"),
22902 args: row,
22903 });
22904 }
22905 };
22906 self.advance();
22907 // Optional ROW keyword before the paren row.
22908 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22909 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22910 {
22911 self.advance();
22912 }
22913 if !matches!(self.peek(), Token::LParen) {
22914 return Err(self.err(alloc::format!(
22915 "expected '(' to open the right-hand row, got {:?}",
22916 self.peek()
22917 )));
22918 }
22919 self.advance();
22920 // `(a, b) <op> (SELECT x, y)` — compare against a single-row
22921 // subquery. Kept as a node (the subquery's row is a runtime value);
22922 // the literal-RHS form below still decomposes at parse time.
22923 if matches!(self.peek(), Token::Select) {
22924 let inner = self.parse_select_stmt()?;
22925 if !matches!(self.peek(), Token::RParen) {
22926 return Err(self.err(alloc::format!(
22927 "expected ')' after row comparison subquery, got {:?}",
22928 self.peek()
22929 )));
22930 }
22931 self.advance();
22932 let Statement::Select(s) = inner else {
22933 unreachable!("parse_select_stmt always returns Statement::Select")
22934 };
22935 return Ok(Expr::RowCmpSubquery {
22936 row,
22937 op,
22938 subquery: Box::new(s),
22939 });
22940 }
22941 let mut rhs = alloc::vec![self.parse_expr(0)?];
22942 while matches!(self.peek(), Token::Comma) {
22943 self.advance();
22944 rhs.push(self.parse_expr(0)?);
22945 }
22946 if !matches!(self.peek(), Token::RParen) {
22947 return Err(self.err(alloc::format!(
22948 "expected ')' after right-hand row, got {:?}",
22949 self.peek()
22950 )));
22951 }
22952 self.advance();
22953 if rhs.len() != row.len() {
22954 // v7.39 (round 239) — PG's wording (42601).
22955 return Err(self.err("unequal number of entries in row expressions".to_string()));
22956 }
22957 Ok(match op {
22958 BinOp::Eq => row_eq(&row, &rhs),
22959 BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
22960 BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
22961 BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
22962 BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
22963 BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
22964 _ => unreachable!("op restricted above"),
22965 })
22966 }
22967
22968 /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
22969 /// escape character becomes the matcher's default backslash:
22970 /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
22971 /// → the char itself, and any pre-existing backslash escapes
22972 /// itself so it stays literal. Both operands must be string
22973 /// literals — a runtime pattern would need matcher support.
22974 fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
22975 let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
22976 (&pattern, &esc)
22977 else {
22978 return Err(
22979 "LIKE ... ESCAPE requires string-literal pattern and escape \
22980 (runtime escape characters are not supported yet)"
22981 .into(),
22982 );
22983 };
22984 // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
22985 // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
22986 // multi-character escape is an error.
22987 let esc_ch: Option<char> = {
22988 let mut ch_iter = e.chars();
22989 match (ch_iter.next(), ch_iter.next()) {
22990 (Some(c), None) => Some(c),
22991 (None, _) => None,
22992 (Some(_), Some(_)) => {
22993 return Err(alloc::format!(
22994 "ESCAPE must be a single character, got {e:?}"
22995 ));
22996 }
22997 }
22998 };
22999 let mut out = String::with_capacity(p.len() + 4);
23000 let mut chars = p.chars();
23001 while let Some(c) = chars.next() {
23002 if Some(c) == esc_ch {
23003 match chars.next() {
23004 // Escaped wildcard / escaped escape → keep the
23005 // next char literal via backslash.
23006 Some(next) => {
23007 out.push('\\');
23008 out.push(next);
23009 }
23010 None => {
23011 return Err("LIKE pattern ends with the escape character".into());
23012 }
23013 }
23014 } else if c == '\\' && esc_ch != Some('\\') {
23015 // A raw backslash is literal under a custom (or absent) escape
23016 // — escape it for the backslash-based matcher.
23017 out.push('\\');
23018 out.push('\\');
23019 } else {
23020 out.push(c);
23021 }
23022 }
23023 Ok(Expr::Literal(Literal::String(out)))
23024 }
23025
23026 /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
23027 /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
23028 /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
23029 /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
23030 /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
23031 /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
23032 /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
23033 /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
23034 /// array expression errors honestly rather than silently mismatching.
23035 fn try_like_any_all(
23036 &mut self,
23037 base: &Expr,
23038 negated: bool,
23039 case_insensitive: bool,
23040 ) -> Result<Option<Expr>, ParseError> {
23041 let is_any = match self.peek() {
23042 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
23043 Token::Ident(s)
23044 if s.eq_ignore_ascii_case("any")
23045 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
23046 {
23047 true
23048 }
23049 _ => return Ok(None),
23050 };
23051 self.advance(); // ANY / ALL
23052 self.advance(); // '('
23053 let arr = self.parse_expr(0)?;
23054 if !matches!(self.peek(), Token::RParen) {
23055 return Err(self.err(format!(
23056 "expected ')' after LIKE {} argument, got {:?}",
23057 if is_any { "ANY" } else { "ALL" },
23058 self.peek()
23059 )));
23060 }
23061 self.advance(); // ')'
23062 let Expr::Array(items) = arr else {
23063 return Err(self.err(
23064 "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
23065 ));
23066 };
23067 let mut clauses = items.into_iter().map(|p| Expr::Like {
23068 expr: Box::new(base.clone()),
23069 pattern: Box::new(p),
23070 negated,
23071 case_insensitive,
23072 });
23073 let Some(first) = clauses.next() else {
23074 // ANY(empty) = FALSE, ALL(empty) = TRUE.
23075 return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
23076 };
23077 let op = if is_any { BinOp::Or } else { BinOp::And };
23078 let combined = clauses.fold(first, |acc, c| Expr::Binary {
23079 lhs: Box::new(acc),
23080 op,
23081 rhs: Box::new(c),
23082 });
23083 Ok(Some(combined))
23084 }
23085
23086 /// `x BETWEEN low AND high` → `(x >= low) AND (x <= high)`, wrapped in
23087 /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
23088 /// `AND` is not swallowed.
23089 fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23090 self.advance(); // BETWEEN
23091 // SYMMETRIC — the bounds may arrive in either order; both
23092 // orientations OR together. ASYMMETRIC is the default and
23093 // absorbs as noise.
23094 let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
23095 {
23096 self.advance();
23097 true
23098 } else {
23099 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
23100 self.advance();
23101 }
23102 false
23103 };
23104 let low = self.parse_expr(6)?;
23105 if !matches!(self.peek(), Token::And) {
23106 return Err(self.err(format!(
23107 "expected AND after BETWEEN low bound, got {:?}",
23108 self.peek()
23109 )));
23110 }
23111 self.advance();
23112 let high = self.parse_expr(6)?;
23113 let target = Box::new(expr);
23114 let range = |lo: Expr, hi: Expr| Expr::Binary {
23115 lhs: Box::new(Expr::Binary {
23116 lhs: target.clone(),
23117 op: BinOp::GtEq,
23118 rhs: Box::new(lo),
23119 }),
23120 op: BinOp::And,
23121 rhs: Box::new(Expr::Binary {
23122 lhs: target.clone(),
23123 op: BinOp::LtEq,
23124 rhs: Box::new(hi),
23125 }),
23126 };
23127 let combined = if symmetric {
23128 Expr::Binary {
23129 lhs: Box::new(range(low.clone(), high.clone())),
23130 op: BinOp::Or,
23131 rhs: Box::new(range(high, low)),
23132 }
23133 } else {
23134 range(low, high)
23135 };
23136 Ok(maybe_not(combined, negated))
23137 }
23138
23139 /// `x IN (a, b, c)` → chained OR of equalities. Empty list collapses
23140 /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
23141 /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
23142 /// Caller already consumed the leading `WITH` ident.
23143 /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
23144 /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
23145 /// self-reference that appears more than once in a single term.
23146 fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
23147 use crate::ast::{CteBody, SelectStatement};
23148 if !cte.recursive {
23149 return Ok(());
23150 }
23151 let CteBody::Select(body) = &cte.body else {
23152 return Ok(());
23153 };
23154 // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
23155 // check the anchor and every peer term.
23156 let has_order = |s: &SelectStatement| !s.order_by.is_empty();
23157 let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
23158 if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
23159 return Err(self.err(String::from(
23160 "ORDER BY in a recursive query is not implemented",
23161 )));
23162 }
23163 if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
23164 return Err(self.err(String::from(
23165 "LIMIT in a recursive query is not implemented",
23166 )));
23167 }
23168 let self_refs = |s: &SelectStatement| -> usize {
23169 let Some(from) = &s.from else {
23170 return 0;
23171 };
23172 let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
23173 for j in &from.joins {
23174 if j.table.name.eq_ignore_ascii_case(&cte.name) {
23175 n += 1;
23176 }
23177 }
23178 n
23179 };
23180 if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
23181 return Err(self.err(alloc::format!(
23182 "recursive reference to query \"{}\" must not appear more than once",
23183 cte.name
23184 )));
23185 }
23186 // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
23187 // apply only when the body actually references itself (a non-self-
23188 // referencing CTE under WITH RECURSIVE may use any set-op shape).
23189 let anchor_refs = self_refs(body);
23190 let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
23191 if anchor_refs > 0 || union_refs {
23192 // Shape: the top level must be UNION [ALL] arms only. A self-ref
23193 // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
23194 // "does not have the form" error — SPG used to compute a value.
23195 if body.unions.is_empty()
23196 || body.unions.iter().any(|(k, _)| {
23197 !matches!(
23198 k,
23199 crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
23200 )
23201 })
23202 {
23203 return Err(self.err(alloc::format!(
23204 "recursive query \"{}\" does not have the form non-recursive-term \
23205 UNION [ALL] recursive-term",
23206 cte.name
23207 )));
23208 }
23209 if anchor_refs > 0 {
23210 return Err(self.err(alloc::format!(
23211 "recursive reference to query \"{}\" must not appear within its non-recursive term",
23212 cte.name
23213 )));
23214 }
23215 }
23216 let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23217 for (_, u) in &body.unions {
23218 if self_refs(u) == 0 {
23219 continue;
23220 }
23221 // The self-reference must not sit on the nullable side of an outer
23222 // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23223 if let Some(from) = &u.from {
23224 for (i, j) in from.joins.iter().enumerate() {
23225 let left_has_self = is_self(&from.primary)
23226 || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23227 let violated = match j.kind {
23228 crate::ast::JoinKind::Left => is_self(&j.table),
23229 crate::ast::JoinKind::Right => left_has_self,
23230 crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23231 _ => false,
23232 };
23233 if violated {
23234 return Err(self.err(alloc::format!(
23235 "recursive reference to query \"{}\" must not appear within an outer join",
23236 cte.name
23237 )));
23238 }
23239 }
23240 }
23241 // No aggregates at the top level of the recursive term (SPG used
23242 // to run them and surface a misleading downstream error).
23243 let mut items_and_having: Vec<&Expr> = Vec::new();
23244 for it in &u.items {
23245 if let crate::ast::SelectItem::Expr { expr, .. } = it {
23246 items_and_having.push(expr);
23247 }
23248 }
23249 if let Some(h) = &u.having {
23250 items_and_having.push(h);
23251 }
23252 for e in items_and_having {
23253 if expr_has_toplevel_aggregate(e) {
23254 return Err(self.err(String::from(
23255 "aggregate functions are not allowed in a recursive query's recursive term",
23256 )));
23257 }
23258 }
23259 }
23260 // A self-reference inside a sublink expression (EXISTS / IN / scalar
23261 // subquery) anywhere in the body is rejected; a plain FROM derived
23262 // table is legal in PG and untouched here.
23263 let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23264 all_terms.extend(body.unions.iter().map(|(_, u)| u));
23265 for term in all_terms {
23266 if select_has_self_ref_in_sublink(term, &cte.name) {
23267 return Err(self.err(alloc::format!(
23268 "recursive reference to query \"{}\" must not appear within a subquery",
23269 cte.name
23270 )));
23271 }
23272 }
23273 Ok(())
23274 }
23275
23276 /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23277 /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23278 /// right after parse so the engine sees a plain recursive CTE with the
23279 /// tracking columns already projected. DEPTH FIRST and CYCLE are
23280 /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23281 /// text-rendered rows can't provide, and errors honestly.
23282 fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23283 use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23284 if cte.search.is_none() && cte.cycle.is_none() {
23285 return Ok(());
23286 }
23287 let cte_name = cte.name.clone();
23288 let col_names = cte.column_overrides.clone();
23289 if col_names.is_empty() {
23290 return Err(
23291 self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23292 );
23293 }
23294 let search = cte.search.take();
23295 let cycle = cte.cycle.take();
23296 let mut extra_cols: Vec<String> = Vec::new();
23297 let col_ref = |name: &str| {
23298 Expr::Column(ColumnName {
23299 qualifier: Some(cte_name.clone()),
23300 name: name.to_string(),
23301 })
23302 };
23303 // Position of a SEARCH/CYCLE column within the CTE's column list.
23304 let pos_of = |name: &str| -> Result<usize, ParseError> {
23305 col_names
23306 .iter()
23307 .position(|c| c.eq_ignore_ascii_case(name))
23308 .ok_or_else(|| {
23309 self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23310 })
23311 };
23312 let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23313 let mut args = Vec::with_capacity(positions.len());
23314 for &p in positions {
23315 match items.get(p) {
23316 Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23317 _ => {
23318 return Err(self.err(
23319 "SEARCH/CYCLE column maps to a non-expression select item".into(),
23320 ));
23321 }
23322 }
23323 }
23324 Ok(Expr::FunctionCall {
23325 name: "row".into(),
23326 args,
23327 })
23328 };
23329 let CteBody::Select(body) = &mut cte.body else {
23330 return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23331 };
23332 if body.unions.is_empty() {
23333 return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23334 }
23335 let rec = body.unions.len() - 1; // recursive term = last UNION peer
23336
23337 if let Some(srch) = search {
23338 // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23339 // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23340 // no typed `record[]`, but element-wise array ORDER BY is correct
23341 // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23342 // exactly onto a typed array: DEPTH is the root→node path
23343 // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23344 // orders numerically (multi-digit keys included), matching PG.
23345 //
23346 // A multi-column BY would need a record[] to keep the per-node key
23347 // tuple orderable, which SPG can't express — error honestly there
23348 // rather than mis-order.
23349 if srch.by_columns.len() != 1 {
23350 return Err(self.err(
23351 "SEARCH … BY with multiple columns needs typed record[] ordering \
23352 SPG doesn't have yet; a single BY column is supported"
23353 .into(),
23354 ));
23355 }
23356 let key_pos = pos_of(&srch.by_columns[0])?;
23357 let base_key = match body.items.get(key_pos) {
23358 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23359 _ => {
23360 return Err(
23361 self.err("SEARCH BY column maps to a non-expression select item".into())
23362 );
23363 }
23364 };
23365 let rec_key = match body.unions[rec].1.items.get(key_pos) {
23366 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23367 _ => {
23368 return Err(
23369 self.err("SEARCH BY column maps to a non-expression select item".into())
23370 );
23371 }
23372 };
23373 if srch.depth_first {
23374 // base: ARRAY[key]; rec: array_append(cte.set, key).
23375 body.items.push(SelectItem::Expr {
23376 expr: Expr::Array(alloc::vec![base_key]),
23377 alias: Some(srch.set_column.clone()),
23378 });
23379 body.unions[rec].1.items.push(SelectItem::Expr {
23380 expr: Expr::FunctionCall {
23381 name: "array_append".into(),
23382 args: alloc::vec![col_ref(&srch.set_column), rec_key],
23383 },
23384 alias: Some(srch.set_column.clone()),
23385 });
23386 } else {
23387 // BREADTH: [depth, key]; depth starts at 0 and increments. The
23388 // leading depth element dominates the element-wise comparison,
23389 // so shallower rows sort first, then by key — PG's (depth, key).
23390 body.items.push(SelectItem::Expr {
23391 expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
23392 alias: Some(srch.set_column.clone()),
23393 });
23394 // rec depth = cte.set[1] + 1.
23395 let parent_depth = Expr::ArraySubscript {
23396 target: Box::new(col_ref(&srch.set_column)),
23397 index: Box::new(Expr::Literal(Literal::Integer(1))),
23398 };
23399 body.unions[rec].1.items.push(SelectItem::Expr {
23400 expr: Expr::Array(alloc::vec![
23401 Expr::Binary {
23402 lhs: Box::new(parent_depth),
23403 op: BinOp::Add,
23404 rhs: Box::new(Expr::Literal(Literal::Integer(1))),
23405 },
23406 rec_key,
23407 ]),
23408 alias: Some(srch.set_column.clone()),
23409 });
23410 }
23411 extra_cols.push(srch.set_column);
23412 }
23413
23414 if let Some(cyc) = cycle {
23415 let positions: Vec<usize> = cyc
23416 .columns
23417 .iter()
23418 .map(|c| pos_of(c))
23419 .collect::<Result<_, _>>()?;
23420 // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
23421 // cast it to text for the cycle path: membership only needs equality,
23422 // and the record text form gives SPG a TextArray path (SPG has no
23423 // typed record[] array). Cycle detection is unaffected.
23424 let base_row = Expr::Cast {
23425 expr: Box::new(row_of(&body.items, &positions)?),
23426 target: CastTarget::Text,
23427 };
23428 let rec_row = Expr::Cast {
23429 expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
23430 target: CastTarget::Text,
23431 };
23432 let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
23433 let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
23434 // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
23435 body.items.push(SelectItem::Expr {
23436 expr: Expr::Literal(dflt.clone()),
23437 alias: Some(cyc.mark_column.clone()),
23438 });
23439 body.items.push(SelectItem::Expr {
23440 expr: Expr::Array(alloc::vec![base_row]),
23441 alias: Some(cyc.path_column.clone()),
23442 });
23443 // rec mark: ROW(cols) already in the path → cycle.
23444 let hit = Expr::AnyAll {
23445 expr: Box::new(rec_row.clone()),
23446 op: BinOp::Eq,
23447 array: Box::new(col_ref(&cyc.path_column)),
23448 is_any: true,
23449 };
23450 let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
23451 Expr::Case {
23452 operand: None,
23453 branches: alloc::vec![(hit, Expr::Literal(mark))],
23454 else_branch: Some(Box::new(Expr::Literal(dflt))),
23455 }
23456 } else {
23457 hit
23458 };
23459 body.unions[rec].1.items.push(SelectItem::Expr {
23460 expr: mark_expr,
23461 alias: Some(cyc.mark_column.clone()),
23462 });
23463 // rec path: array_append(cte.path, ROW(cols)).
23464 body.unions[rec].1.items.push(SelectItem::Expr {
23465 expr: Expr::FunctionCall {
23466 name: "array_append".into(),
23467 args: alloc::vec![col_ref(&cyc.path_column), rec_row],
23468 },
23469 alias: Some(cyc.path_column.clone()),
23470 });
23471 // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
23472 let stop = Expr::Unary {
23473 op: UnOp::Not,
23474 expr: Box::new(col_ref(&cyc.mark_column)),
23475 };
23476 let w = &mut body.unions[rec].1.where_;
23477 *w = Some(match w.take() {
23478 Some(prev) => Expr::Binary {
23479 lhs: Box::new(prev),
23480 op: BinOp::And,
23481 rhs: Box::new(stop),
23482 },
23483 None => stop,
23484 });
23485 extra_cols.push(cyc.mark_column);
23486 extra_cols.push(cyc.path_column);
23487 }
23488 cte.column_overrides.extend(extra_cols);
23489 Ok(())
23490 }
23491
23492 /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
23493 /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
23494 fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
23495 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
23496 return Ok(None);
23497 }
23498 self.advance(); // SEARCH
23499 let depth_first = match self.peek() {
23500 Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
23501 Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
23502 other => {
23503 return Err(self.err(format!(
23504 "expected DEPTH or BREADTH after SEARCH, got {other:?}"
23505 )));
23506 }
23507 };
23508 self.advance();
23509 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
23510 return Err(self.err(format!(
23511 "expected FIRST after SEARCH mode, got {:?}",
23512 self.peek()
23513 )));
23514 }
23515 self.advance();
23516 if !self.peek_is_by() {
23517 return Err(self.err(format!(
23518 "expected BY after SEARCH … FIRST, got {:?}",
23519 self.peek()
23520 )));
23521 }
23522 self.advance();
23523 let mut by_columns = alloc::vec![self.expect_ident_like()?];
23524 while matches!(self.peek(), Token::Comma) {
23525 self.advance();
23526 by_columns.push(self.expect_ident_like()?);
23527 }
23528 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23529 return Err(self.err(format!(
23530 "expected SET in SEARCH clause, got {:?}",
23531 self.peek()
23532 )));
23533 }
23534 self.advance();
23535 let set_column = self.expect_ident_like()?;
23536 Ok(Some(crate::ast::SearchClause {
23537 depth_first,
23538 by_columns,
23539 set_column,
23540 }))
23541 }
23542
23543 /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
23544 /// USING pathcol`. Returns None when the next token isn't CYCLE.
23545 fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
23546 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
23547 return Ok(None);
23548 }
23549 self.advance(); // CYCLE
23550 let mut columns = alloc::vec![self.expect_ident_like()?];
23551 while matches!(self.peek(), Token::Comma) {
23552 self.advance();
23553 columns.push(self.expect_ident_like()?);
23554 }
23555 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23556 return Err(self.err(format!(
23557 "expected SET in CYCLE clause, got {:?}",
23558 self.peek()
23559 )));
23560 }
23561 self.advance();
23562 let mark_column = self.expect_ident_like()?;
23563 let mut mark_value = None;
23564 let mut default_value = None;
23565 if matches!(self.peek(), Token::To) {
23566 self.advance();
23567 mark_value = Some(self.parse_cycle_literal()?);
23568 if !matches!(self.peek(), Token::Default) {
23569 return Err(self.err(format!(
23570 "expected DEFAULT after CYCLE … TO, got {:?}",
23571 self.peek()
23572 )));
23573 }
23574 self.advance();
23575 default_value = Some(self.parse_cycle_literal()?);
23576 }
23577 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
23578 return Err(self.err(format!(
23579 "expected USING in CYCLE clause, got {:?}",
23580 self.peek()
23581 )));
23582 }
23583 self.advance();
23584 let path_column = self.expect_ident_like()?;
23585 Ok(Some(crate::ast::CycleClause {
23586 columns,
23587 mark_column,
23588 mark_value,
23589 default_value,
23590 path_column,
23591 }))
23592 }
23593
23594 /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
23595 /// literal (string / bool / number) in PG.
23596 fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
23597 match self.parse_expr(0)? {
23598 Expr::Literal(l) => Ok(l),
23599 other => Err(self.err(format!(
23600 "CYCLE mark/default value must be a literal, got {other:?}"
23601 ))),
23602 }
23603 }
23604
23605 fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
23606 // v4.22: WITH RECURSIVE — optional keyword right after WITH.
23607 // Comes through as an identifier; consume it if present and
23608 // mark every CTE in the clause as recursive (PG semantics —
23609 // the flag is per-WITH, not per-CTE).
23610 let mut recursive = false;
23611 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
23612 && s.eq_ignore_ascii_case("recursive")
23613 {
23614 self.advance();
23615 recursive = true;
23616 }
23617 let mut ctes = Vec::new();
23618 loop {
23619 let name = self.expect_ident_like()?;
23620 // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
23621 // PG uses these to rename the body's output columns; we
23622 // do the same below by overriding `columns[i].name`.
23623 let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
23624 self.advance();
23625 let mut names = Vec::new();
23626 loop {
23627 names.push(self.expect_ident_like()?);
23628 if matches!(self.peek(), Token::Comma) {
23629 self.advance();
23630 continue;
23631 }
23632 break;
23633 }
23634 if !matches!(self.peek(), Token::RParen) {
23635 return Err(self.err(format!(
23636 "expected ')' to close CTE column list, got {:?}",
23637 self.peek()
23638 )));
23639 }
23640 self.advance();
23641 names
23642 } else {
23643 Vec::new()
23644 };
23645 // AS is a reserved Token::As (used by SELECT-item / FROM
23646 // aliasing) — handle it specially rather than as a bare
23647 // ident.
23648 if !matches!(self.peek(), Token::As) {
23649 return Err(self.err(format!(
23650 "expected AS after CTE name {name:?}, got {:?}",
23651 self.peek()
23652 )));
23653 }
23654 self.advance();
23655 // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
23656 // MATERIALIZED` optimizer hints. SPG materialises every
23657 // CTE, so both spellings are accepted and absorbed.
23658 if matches!(self.peek(), Token::Not) {
23659 self.advance(); // NOT
23660 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23661 if s.eq_ignore_ascii_case("materialized"))
23662 {
23663 self.advance();
23664 } else {
23665 return Err(self.err(format!(
23666 "expected MATERIALIZED after AS NOT, got {:?}",
23667 self.peek()
23668 )));
23669 }
23670 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23671 if s.eq_ignore_ascii_case("materialized"))
23672 {
23673 self.advance();
23674 }
23675 if !matches!(self.peek(), Token::LParen) {
23676 return Err(self.err(format!(
23677 "expected '(' after AS in WITH clause, got {:?}",
23678 self.peek()
23679 )));
23680 }
23681 self.advance();
23682 // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
23683 // RETURNING) as the CTE body in addition to SELECT.
23684 // PG writable CTE semantics. UPDATE / DELETE come in as
23685 // bare Idents (lexer keeps SELECT / INSERT as reserved
23686 // tokens but treats the rest of DML as case-insensitive
23687 // idents).
23688 let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23689 let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23690 let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23691 let body = match self.peek() {
23692 Token::Select => {
23693 let inner = self.parse_select_stmt()?;
23694 let Statement::Select(s) = inner else {
23695 unreachable!("parse_select_stmt returns Select");
23696 };
23697 crate::ast::CteBody::Select(s)
23698 }
23699 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23700 // `SELECT * FROM t` this way and accepts it wherever a
23701 // SELECT goes, so the CTE body dispatch needs its own
23702 // arm: this match is keyed on the FIRST token, and
23703 // `Token::Table` fell through to a tail that then
23704 // rejected what it got. `parse_table_shorthand` has
23705 // returned a desugared SelectStatement since the
23706 // shorthand landed — only the routing was missing.
23707 // Round 868 found this by putting the shorthand in a
23708 // subquery; every earlier check used a top-level form.
23709 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23710 // `SELECT * FROM t` this way and accepts it wherever a
23711 // SELECT goes, so the CTE body dispatch needs its own
23712 // arm: this match is keyed on the FIRST token, and
23713 // `Token::Table` fell through to a tail that rejected
23714 // what it got. `parse_table_shorthand` has returned a
23715 // desugared SelectStatement since the shorthand landed —
23716 // only the routing was missing, here and in the derived
23717 // table's second-token gate. Round 868 found both by
23718 // putting the shorthand in a subquery; every earlier
23719 // check had used a top-level form.
23720 Token::Table
23721 if matches!(
23722 self.tokens.get(self.pos + 1),
23723 Some(Token::Ident(_) | Token::QuotedIdent(_))
23724 ) =>
23725 {
23726 let mut head = self.parse_table_shorthand()?;
23727 self.parse_setop_chain_into(&mut head)?;
23728 self.parse_select_tail_into(&mut head)?;
23729 crate::ast::CteBody::Select(head)
23730 }
23731 // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
23732 // WITH t(a) AS (VALUES (1), (2)) … lowers through
23733 // the shared rows helper onto a Select body.
23734 Token::Values => {
23735 self.advance(); // VALUES
23736 let mut head = self.parse_values_rows_body()?;
23737 // A VALUES seed can head a set-operation chain —
23738 // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
23739 // SELECT n+1 FROM t …). Attach any trailing
23740 // UNION / INTERSECT / EXCEPT peers so the
23741 // recursive-CTE body parses like the SELECT seed.
23742 self.parse_setop_chain_into(&mut head)?;
23743 crate::ast::CteBody::Select(head)
23744 }
23745 Token::Insert => {
23746 let inner = self.parse_one_statement()?;
23747 let Statement::Insert(s) = inner else {
23748 unreachable!("Token::Insert routes to Insert");
23749 };
23750 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23751 }
23752 _ if is_update_kw => {
23753 let inner = self.parse_one_statement()?;
23754 let Statement::Update(s) = inner else {
23755 return Err(
23756 self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
23757 );
23758 };
23759 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23760 }
23761 _ if is_delete_kw => {
23762 let inner = self.parse_one_statement()?;
23763 let Statement::Delete(s) = inner else {
23764 return Err(
23765 self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
23766 );
23767 };
23768 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23769 }
23770 // v7.39 (round 149) — PG 17 allows MERGE as a
23771 // data-modifying CTE body.
23772 _ if is_merge_kw => {
23773 let inner = self.parse_one_statement()?;
23774 let Statement::Merge(s) = inner else {
23775 return Err(
23776 self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
23777 );
23778 };
23779 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23780 }
23781 // v7.39 (round 151) — a CTE body may itself be
23782 // WITH-headed (PG grammar: PreparableStmt carries its
23783 // own with_clause). The nested statement keeps its own
23784 // ctes; the modifying-CTE-at-top-level rule is enforced
23785 // at execution.
23786 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
23787 self.advance(); // WITH
23788 match self.parse_with_cte_then_select()? {
23789 Statement::Select(s) => crate::ast::CteBody::Select(s),
23790 Statement::Insert(s) => {
23791 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23792 }
23793 Statement::Update(s) => {
23794 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23795 }
23796 Statement::Delete(s) => {
23797 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23798 }
23799 Statement::Merge(s) => {
23800 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23801 }
23802
23803 other => {
23804 return Err(self.err(format!(
23805 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23806 )));
23807 }
23808 }
23809 }
23810 other => {
23811 return Err(self.err(format!(
23812 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23813 )));
23814 }
23815 };
23816 if !matches!(self.peek(), Token::RParen) {
23817 return Err(self.err(format!(
23818 "expected ')' after CTE body, got {:?}",
23819 self.peek()
23820 )));
23821 }
23822 self.advance();
23823 // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
23824 // CTE, desugared into extra body columns by the engine.
23825 let search = self.parse_cte_search_clause()?;
23826 let cycle = self.parse_cte_cycle_clause()?;
23827 let mut cte = crate::ast::Cte {
23828 name,
23829 body,
23830 recursive,
23831 column_overrides,
23832 search,
23833 cycle,
23834 };
23835 self.validate_recursive_cte(&cte)?;
23836 self.desugar_cte_search_cycle(&mut cte)?;
23837 ctes.push(cte);
23838 if matches!(self.peek(), Token::Comma) {
23839 self.advance();
23840 continue;
23841 }
23842 break;
23843 }
23844 // v7.37.43-T4.4 — the outer body may be SELECT (classical),
23845 // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
23846 // the parsed CTEs to whichever statement the body produces.
23847 let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23848 let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23849 let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23850 match self.peek() {
23851 Token::Select => {
23852 let body_stmt = self.parse_select_stmt()?;
23853 let Statement::Select(mut body) = body_stmt else {
23854 unreachable!()
23855 };
23856 body.ctes = ctes;
23857 Ok(Statement::Select(body))
23858 }
23859 Token::Insert => {
23860 let body_stmt = self.parse_one_statement()?;
23861 let Statement::Insert(mut body) = body_stmt else {
23862 unreachable!()
23863 };
23864 body.ctes = ctes;
23865 Ok(Statement::Insert(body))
23866 }
23867 _ if outer_is_update => {
23868 let body_stmt = self.parse_one_statement()?;
23869 let Statement::Update(mut body) = body_stmt else {
23870 return Err(self.err(format!("expected UPDATE after WITH clause")));
23871 };
23872 body.ctes = ctes;
23873 Ok(Statement::Update(body))
23874 }
23875 _ if outer_is_delete => {
23876 let body_stmt = self.parse_one_statement()?;
23877 let Statement::Delete(mut body) = body_stmt else {
23878 return Err(self.err(format!("expected DELETE after WITH clause")));
23879 };
23880 body.ctes = ctes;
23881 Ok(Statement::Delete(body))
23882 }
23883 // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
23884 // WITH RECURSIVE is rejected with PG's exact message
23885 // (parse analysis, transformWithClause).
23886 _ if outer_is_merge => {
23887 if recursive {
23888 return Err(self.err(String::from(
23889 "WITH RECURSIVE is not supported for MERGE statement",
23890 )));
23891 }
23892 let body_stmt = self.parse_one_statement()?;
23893 let Statement::Merge(mut body) = body_stmt else {
23894 return Err(self.err(format!("expected MERGE after WITH clause")));
23895 };
23896 body.ctes = ctes;
23897 Ok(Statement::Merge(body))
23898 }
23899 other => Err(self.err(format!(
23900 "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
23901 ))),
23902 }
23903 }
23904
23905 /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
23906 /// already consumed the leading `EXISTS` ident via
23907 /// `self.advance()`.
23908 /// v7.13.0 — parse the rest of a `CASE … END` expression after
23909 /// the leading `CASE` ident has been consumed (mailrs round-5
23910 /// G9). Supports both the searched form
23911 /// (`CASE WHEN cond THEN val …`) and the simple form
23912 /// (`CASE operand WHEN val THEN val …`).
23913 fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
23914 // Disambiguate searched vs simple form: if the next token
23915 // is `WHEN`, we're in the searched form. Otherwise the
23916 // intervening expression is the operand.
23917 let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
23918 None
23919 } else {
23920 Some(Box::new(self.parse_expr(0)?))
23921 };
23922 let mut branches: Vec<(Expr, Expr)> = Vec::new();
23923 loop {
23924 match self.peek() {
23925 Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
23926 self.advance();
23927 let cond = self.parse_expr(0)?;
23928 match self.peek() {
23929 Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
23930 self.advance();
23931 }
23932 other => {
23933 return Err(self.err(alloc::format!(
23934 "expected THEN after CASE WHEN <expr>, got {other:?}"
23935 )));
23936 }
23937 }
23938 let value = self.parse_expr(0)?;
23939 branches.push((cond, value));
23940 }
23941 _ => break,
23942 }
23943 }
23944 if branches.is_empty() {
23945 return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
23946 }
23947 let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
23948 {
23949 self.advance();
23950 Some(Box::new(self.parse_expr(0)?))
23951 } else {
23952 None
23953 };
23954 match self.peek() {
23955 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
23956 self.advance();
23957 }
23958 other => {
23959 return Err(self.err(alloc::format!(
23960 "expected END to close CASE expression, got {other:?}"
23961 )));
23962 }
23963 }
23964 Ok(Expr::Case {
23965 operand,
23966 branches,
23967 else_branch,
23968 })
23969 }
23970
23971 /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
23972 /// query-source position (EXISTS / IN / INSERT source / CTE body /
23973 /// view body). Caller consumed the WITH keyword. Only a SELECT
23974 /// outer is grammatical here; the data-modifying-CTE-at-top-level
23975 /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
23976 /// maps correctly.
23977 fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
23978 let inner = self.parse_with_cte_then_select()?;
23979 match inner {
23980 Statement::Select(s) => Ok(s),
23981 other => Err(self.err(format!(
23982 "expected SELECT after WITH in a subquery, got {other:?}"
23983 ))),
23984 }
23985 }
23986
23987 /// True when the next token is the (unquoted) WITH keyword. WITH is
23988 /// reserved in PG, so a bare `with` can never be a column reference
23989 /// in these positions; a quoted `"with"` stays an identifier.
23990 fn peek_is_with_kw(&self) -> bool {
23991 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
23992 }
23993
23994 /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
23995 /// `#[inline(never)]` keeps the large SelectStatement temporaries
23996 /// off parse_expr's recursive frame (the nesting-budget stack
23997 /// cliff — see the round-153 gate regression).
23998 #[inline(never)]
23999 fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24000 if self.peek_is_with_kw() {
24001 self.advance();
24002 self.parse_nested_with_select()
24003 } else {
24004 match self.parse_select_stmt()? {
24005 Statement::Select(s) => Ok(s),
24006 other => Err(self.err(alloc::format!(
24007 "expected SELECT inside ANY/ALL, got {other:?}"
24008 ))),
24009 }
24010 }
24011 }
24012
24013 fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
24014 if !matches!(self.peek(), Token::LParen) {
24015 return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
24016 }
24017 self.advance();
24018 // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
24019 let s = if self.peek_is_with_kw() {
24020 self.advance();
24021 self.parse_nested_with_select()?
24022 } else {
24023 let inner = self.parse_select_stmt()?;
24024 let Statement::Select(s) = inner else {
24025 unreachable!("parse_select_stmt returns Select")
24026 };
24027 s
24028 };
24029 if !matches!(self.peek(), Token::RParen) {
24030 return Err(self.err(format!(
24031 "expected ')' after EXISTS-subquery, got {:?}",
24032 self.peek()
24033 )));
24034 }
24035 self.advance();
24036 Ok(Expr::Exists {
24037 subquery: Box::new(s),
24038 negated,
24039 })
24040 }
24041
24042 fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
24043 self.advance(); // IN
24044 if !matches!(self.peek(), Token::LParen) {
24045 return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
24046 }
24047 self.advance();
24048 // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
24049 // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
24050 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
24051 let s = if self.peek_is_with_kw() {
24052 self.advance();
24053 self.parse_nested_with_select()?
24054 } else {
24055 let inner = self.parse_select_stmt()?;
24056 let Statement::Select(s) = inner else {
24057 unreachable!("parse_select_stmt always returns Statement::Select")
24058 };
24059 s
24060 };
24061 if !matches!(self.peek(), Token::RParen) {
24062 return Err(self.err(format!(
24063 "expected ')' after IN-subquery, got {:?}",
24064 self.peek()
24065 )));
24066 }
24067 self.advance();
24068 return Ok(Expr::InSubquery {
24069 expr: Box::new(expr),
24070 subquery: Box::new(s),
24071 negated,
24072 });
24073 }
24074 let mut elements = Vec::new();
24075 if !matches!(self.peek(), Token::RParen) {
24076 loop {
24077 elements.push(self.parse_expr(0)?);
24078 match self.peek() {
24079 Token::Comma => {
24080 self.advance();
24081 }
24082 Token::RParen => break,
24083 other => {
24084 return Err(
24085 self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
24086 );
24087 }
24088 }
24089 }
24090 }
24091 self.advance(); // ')'
24092 // v7.30.2 (mailrs round-25) — flat InList node instead of a
24093 // left-deep OR-Eq chain: chain depth scaled with the element
24094 // count and overflowed the stack (eval + drop are recursive).
24095 if elements.is_empty() {
24096 return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
24097 }
24098 Ok(Expr::InList {
24099 expr: Box::new(expr),
24100 list: elements,
24101 negated,
24102 })
24103 }
24104
24105 /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
24106 /// already consumed by the caller. Elements must be numeric literals
24107 /// (with optional unary `-`); any compound expression is rejected at
24108 /// parse time so the runtime never needs to evaluate inside a vector.
24109 /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
24110 /// has already consumed the `EXTRACT` token before calling us —
24111 /// we pick up at the opening `(`.
24112 /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
24113 /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
24114 /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
24115 /// per-column OR-fold of
24116 /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
24117 /// term)` so the existing FTS evaluator handles semantics.
24118 ///
24119 /// The mode modifier is accepted-and-ignored at v7.17 — all
24120 /// modes map to the same `plainto_tsquery` rewrite. Boolean-
24121 /// mode operators (`+foo -bar`) would need their own parser
24122 /// (Phase 2.2c); customers who hit them today already get a
24123 /// correct lexeme-match against the bare term, only without
24124 /// the +/- precedence the customer asked for.
24125 fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
24126 // Already at `MATCH`-consumed position; the dispatcher
24127 // confirmed the next token is `(`.
24128 if !matches!(self.peek(), Token::LParen) {
24129 return Err(self.err(alloc::format!(
24130 "expected '(' after MATCH, got {:?}",
24131 self.peek()
24132 )));
24133 }
24134 self.advance();
24135 let mut cols: Vec<Expr> = Vec::new();
24136 loop {
24137 cols.push(self.parse_expr(0)?);
24138 match self.peek() {
24139 Token::Comma => {
24140 self.advance();
24141 }
24142 Token::RParen => break,
24143 other => {
24144 return Err(self.err(alloc::format!(
24145 "expected ',' or ')' in MATCH column list, got {other:?}"
24146 )));
24147 }
24148 }
24149 }
24150 self.advance(); // ')'
24151 // Expect AGAINST.
24152 match self.peek() {
24153 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
24154 self.advance();
24155 }
24156 other => {
24157 return Err(self.err(alloc::format!(
24158 "expected AGAINST after MATCH column list, got {other:?}"
24159 )));
24160 }
24161 }
24162 if !matches!(self.peek(), Token::LParen) {
24163 return Err(self.err(alloc::format!(
24164 "expected '(' after AGAINST, got {:?}",
24165 self.peek()
24166 )));
24167 }
24168 self.advance();
24169 // Read AGAINST's argument as a single primary token —
24170 // string literal, placeholder, or column-ref ident. We
24171 // can't call `parse_expr` / `parse_unary` here because
24172 // the postfix chain inside `parse_atom` would greedily
24173 // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
24174 // and fail at "expected '(' after IN". Customers always
24175 // write a literal or bound parameter in AGAINST, so this
24176 // restriction is non-blocking; the error path explains
24177 // the limit if a more complex expression shows up.
24178 let term = match self.advance() {
24179 Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
24180 Token::Placeholder(n) => Expr::Placeholder(n),
24181 Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
24182 qualifier: None,
24183 name: s,
24184 }),
24185 other => {
24186 return Err(self.err(alloc::format!(
24187 "MATCH ... AGAINST(<term>) expects a string literal, \
24188 bound parameter, or column ref, got {other:?}"
24189 )));
24190 }
24191 };
24192 // Optional mode tail — accept-and-ignore at v7.17:
24193 // IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
24194 // IN BOOLEAN MODE
24195 // WITH QUERY EXPANSION
24196 loop {
24197 match self.peek() {
24198 // IN lexes as a reserved Token::In, not an ident,
24199 // so it gets its own arm.
24200 Token::In => {
24201 self.advance();
24202 }
24203 Token::Ident(s) | Token::QuotedIdent(s)
24204 if s.eq_ignore_ascii_case("natural")
24205 || s.eq_ignore_ascii_case("language")
24206 || s.eq_ignore_ascii_case("boolean")
24207 || s.eq_ignore_ascii_case("mode")
24208 || s.eq_ignore_ascii_case("with")
24209 || s.eq_ignore_ascii_case("query")
24210 || s.eq_ignore_ascii_case("expansion") =>
24211 {
24212 self.advance();
24213 }
24214 _ => break,
24215 }
24216 }
24217 if !matches!(self.peek(), Token::RParen) {
24218 return Err(self.err(alloc::format!(
24219 "expected ')' to close AGAINST, got {:?}",
24220 self.peek()
24221 )));
24222 }
24223 self.advance();
24224 // Build per-column `to_tsvector('simple', col) @@
24225 // plainto_tsquery('simple', term)` and OR-fold.
24226 let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24227 let plainto = Expr::FunctionCall {
24228 name: String::from("plainto_tsquery"),
24229 args: alloc::vec![simple_lit(), term.clone()],
24230 };
24231 let mut folded: Option<Expr> = None;
24232 for col in cols {
24233 let to_tsv = Expr::FunctionCall {
24234 name: String::from("to_tsvector"),
24235 args: alloc::vec![simple_lit(), col],
24236 };
24237 let leaf = Expr::Binary {
24238 lhs: Box::new(to_tsv),
24239 op: crate::ast::BinOp::TsMatch,
24240 rhs: Box::new(plainto.clone()),
24241 };
24242 folded = Some(match folded {
24243 None => leaf,
24244 Some(prev) => Expr::Binary {
24245 lhs: Box::new(prev),
24246 op: crate::ast::BinOp::Or,
24247 rhs: Box::new(leaf),
24248 },
24249 });
24250 }
24251 match folded {
24252 Some(e) => Ok(e),
24253 None => Err(self.err(String::from(
24254 "MATCH(...) AGAINST(...) requires at least one column",
24255 ))),
24256 }
24257 }
24258
24259 fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24260 if !matches!(self.peek(), Token::LParen) {
24261 return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24262 }
24263 self.advance();
24264 let field_name = self.expect_ident_like()?;
24265 let field = match field_name.to_ascii_lowercase().as_str() {
24266 // PG accepts the plural spellings (years/months/…/millenniums) as
24267 // aliases for the singular fields — its datetime unit table has both.
24268 // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24269 "year" | "years" => ExtractField::Year,
24270 "month" | "months" => ExtractField::Month,
24271 "day" | "days" => ExtractField::Day,
24272 "hour" | "hours" => ExtractField::Hour,
24273 "minute" | "minutes" => ExtractField::Minute,
24274 "second" | "seconds" => ExtractField::Second,
24275 "microsecond" | "microseconds" => ExtractField::Microsecond,
24276 "epoch" => ExtractField::Epoch,
24277 "dow" => ExtractField::Dow,
24278 "isodow" => ExtractField::Isodow,
24279 "doy" => ExtractField::Doy,
24280 "week" | "weeks" => ExtractField::Week,
24281 "isoyear" => ExtractField::Isoyear,
24282 "quarter" => ExtractField::Quarter,
24283 "decade" | "decades" => ExtractField::Decade,
24284 "century" | "centuries" => ExtractField::Century,
24285 "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24286 "julian" => ExtractField::Julian,
24287 "millisecond" | "milliseconds" => ExtractField::Millisecond,
24288 "timezone" => ExtractField::Timezone,
24289 "timezone_hour" => ExtractField::TimezoneHour,
24290 "timezone_minute" => ExtractField::TimezoneMinute,
24291 // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24292 // reports an unknown one with the source type (22023); carry the
24293 // raw name so eval can word it.
24294 other => ExtractField::Other(alloc::string::String::from(other)),
24295 };
24296 if !matches!(self.peek(), Token::From) {
24297 return Err(self.err(format!(
24298 "expected FROM after EXTRACT field, got {:?}",
24299 self.peek()
24300 )));
24301 }
24302 self.advance();
24303 let source = self.parse_expr(0)?;
24304 if !matches!(self.peek(), Token::RParen) {
24305 return Err(self.err(format!(
24306 "expected ')' to close EXTRACT, got {:?}",
24307 self.peek()
24308 )));
24309 }
24310 self.advance();
24311 Ok(Expr::Extract {
24312 field,
24313 source: Box::new(source),
24314 })
24315 }
24316
24317 /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24318 /// is already consumed; we expect a single string literal next and
24319 /// resolve it into `Literal::Interval` at parse time so the engine
24320 /// never has to re-tokenise inside the string.
24321 /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24322 /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24323 /// is the SQL-standard form and is left to the path below.
24324 fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24325 // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24326 let (offset, sign) = match self.peek() {
24327 Token::Minus => (1, "-"),
24328 _ => (0, ""),
24329 };
24330 let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24331 return None;
24332 };
24333 self.tokens
24334 .get(self.pos + offset + 1)
24335 .filter(|t| mysql_interval_unit(t).is_some())?;
24336 Some((alloc::format!("{sign}{n}"), offset + 1))
24337 }
24338
24339 /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24340 /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24341 /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24342 ///
24343 /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24344 /// this by parsing the group and then restoring `self.pos` — which could
24345 /// never have worked, because `advance()` DESTROYS the token it returns
24346 /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24347 /// inert only because both branches errored back then.
24348 fn interval_paren_is_quantity(&self) -> bool {
24349 let mut depth = 0usize;
24350 let mut saw_top_level_comma = false;
24351 let mut i = self.pos;
24352 while let Some(tok) = self.tokens.get(i) {
24353 match tok {
24354 Token::LParen => depth += 1,
24355 Token::RParen => {
24356 depth = depth.saturating_sub(1);
24357 if depth == 0 {
24358 return !saw_top_level_comma
24359 && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
24360 .is_some();
24361 }
24362 }
24363 // A comma directly inside the outermost parens means the
24364 // argument list of the INTERVAL() function.
24365 Token::Comma if depth == 1 => saw_top_level_comma = true,
24366 Token::Eof => return false,
24367 _ => {}
24368 }
24369 i += 1;
24370 }
24371 false
24372 }
24373
24374 fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
24375 // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
24376 // (the index of the last Ni ≤ N), distinct from the interval literal.
24377 // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
24378 // is decided by a non-destructive lookahead (round 422) before either
24379 // branch consumes anything. MySQL only.
24380 if self.mysql_dialect
24381 && matches!(self.peek(), Token::LParen)
24382 && !self.interval_paren_is_quantity()
24383 {
24384 self.advance(); // (
24385 let mut args = Vec::new();
24386 if !matches!(self.peek(), Token::RParen) {
24387 loop {
24388 args.push(self.parse_expr(0)?);
24389 if matches!(self.peek(), Token::Comma) {
24390 self.advance();
24391 continue;
24392 }
24393 break;
24394 }
24395 }
24396 if !matches!(self.peek(), Token::RParen) {
24397 return Err(self.err(alloc::format!(
24398 "expected ')' after INTERVAL() arguments, got {:?}",
24399 self.peek()
24400 )));
24401 }
24402 self.advance(); // )
24403 return Ok(Expr::FunctionCall {
24404 name: alloc::string::String::from("interval"),
24405 args,
24406 });
24407 }
24408 // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
24409 // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
24410 // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
24411 // writes every date arithmetic there is, and it did not parse at
24412 // all. PG rejects the unquoted form outright (`syntax error at or
24413 // near "1"`, measured), so it is taken only in the MySQL dialect —
24414 // PG's own `INTERVAL '1' DAY` is untouched below.
24415 if self.mysql_dialect
24416 && let Some((text, consume)) = self.peek_unquoted_interval_count()
24417 {
24418 for _ in 0..consume {
24419 self.advance(); // the optional `-` and the number
24420 }
24421 let Some(unit) = mysql_interval_unit(self.peek()) else {
24422 return Err(self.err(alloc::format!(
24423 "expected an interval unit after INTERVAL {text}, got {:?}",
24424 self.peek()
24425 )));
24426 };
24427 self.advance(); // the unit
24428 let (months, days, micros) = scale_mysql_interval(&text, unit)
24429 .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
24430 return Ok(Expr::Literal(Literal::Interval {
24431 months,
24432 days,
24433 micros,
24434 // The canonical rendering, so Display round-trips into a
24435 // form both dialects read back.
24436 text: alloc::format!("{text} {unit}"),
24437 }));
24438 }
24439 // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
24440 // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
24441 // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
24442 // Those cannot fold into a compile-time `Literal::Interval`, so they
24443 // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
24444 // builtin, which builds the value at run time (and yields NULL for a
24445 // NULL quantity, as MariaDB does). The literal path above still folds
24446 // the constant case — it is cheaper and round-trips through Display.
24447 //
24448 // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
24449 // MySQL's quoted spelling) keep the qualifier path below.
24450 if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
24451 let qty = self.parse_expr(0)?;
24452 let Some(unit) = mysql_interval_unit(self.peek()) else {
24453 return Err(self.err(alloc::format!(
24454 "expected an interval unit after INTERVAL <expr>, got {:?}",
24455 self.peek()
24456 )));
24457 };
24458 self.advance(); // the unit
24459 return Ok(make_interval_call(qty, unit));
24460 }
24461 let tok = self.advance();
24462 let Token::String(text) = tok else {
24463 return Err(self.err(format!(
24464 "expected string literal after INTERVAL, got {tok:?}"
24465 )));
24466 };
24467 // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
24468 // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
24469 // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
24470 // bare number means and the leading/trailing precision.
24471 let field1 = interval_field_of(self.peek());
24472 let qualifier = if let Some(f1) = field1 {
24473 self.advance();
24474 let f2 = if matches!(self.peek(), Token::To) {
24475 self.advance();
24476 let Some(f) = interval_field_of(self.peek()) else {
24477 return Err(self.err(format!(
24478 "expected an interval field after TO, got {:?}",
24479 self.peek()
24480 )));
24481 };
24482 self.advance();
24483 Some(f)
24484 } else {
24485 None
24486 };
24487 Some((f1, f2))
24488 } else {
24489 None
24490 };
24491 let (months, days, micros) = match qualifier {
24492 Some(q) => interpret_qualified_interval(&text, q),
24493 None => parse_interval_text(&text),
24494 }
24495 .ok_or_else(|| ParseError {
24496 message: format!(
24497 "cannot parse INTERVAL {text:?}; \
24498 expected `<n> <unit> [<n> <unit> ...]` with units \
24499 microsecond[s], millisecond[s], second[s], minute[s], \
24500 hour[s], day[s], week[s], month[s], year[s]"
24501 ),
24502 token_pos: self.consumed_pos(),
24503 })?;
24504 Ok(Expr::Literal(Literal::Interval {
24505 months,
24506 days,
24507 micros,
24508 text,
24509 }))
24510 }
24511
24512 /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
24513 /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
24514 /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
24515 /// than a pgvector literal.
24516 fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
24517 self.advance(); // consume `[`
24518 let mut items: Vec<Expr> = Vec::new();
24519 if !matches!(self.peek(), Token::RBracket) {
24520 loop {
24521 if matches!(self.peek(), Token::LBracket) {
24522 items.push(self.parse_array_bracket_body()?);
24523 } else {
24524 items.push(self.parse_expr(0)?);
24525 }
24526 match self.peek() {
24527 Token::Comma => {
24528 self.advance();
24529 }
24530 Token::RBracket => break,
24531 other => {
24532 return Err(self.err(alloc::format!(
24533 "expected ',' or ']' in array literal, got {other:?}"
24534 )));
24535 }
24536 }
24537 }
24538 }
24539 self.advance(); // consume `]`
24540 Ok(Expr::Array(items))
24541 }
24542
24543 fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
24544 let mut elems = Vec::new();
24545 if matches!(self.peek(), Token::RBracket) {
24546 self.advance();
24547 return Ok(Expr::Literal(Literal::Vector(elems)));
24548 }
24549 loop {
24550 let e = self.parse_expr(0)?;
24551 let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
24552 message: format!("vector element must be a numeric literal, got {e:?}"),
24553 token_pos: self.pos,
24554 })?;
24555 elems.push(x);
24556 match self.peek() {
24557 Token::Comma => {
24558 self.advance();
24559 }
24560 Token::RBracket => {
24561 self.advance();
24562 break;
24563 }
24564 other => {
24565 return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
24566 }
24567 }
24568 }
24569 Ok(Expr::Literal(Literal::Vector(elems)))
24570 }
24571
24572 /// Atom that started with an identifier: could be `t.col`, `col`, or
24573 /// `func(arg, ...)`. Detect each shape by looking at the next token.
24574 /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
24575 /// [, ...])`. Caller has already consumed `OVER`. Either clause
24576 /// is optional; an empty `()` is also legal (PG semantics).
24577 /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
24578 /// modifier between `name(args)` and `OVER (...)`. Default is
24579 /// `Respect`. Unrecognised idents leave the stream unchanged.
24580 fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
24581 let Token::Ident(s) = self.peek().clone() else {
24582 return NullTreatment::Respect;
24583 };
24584 let is_ignore = s.eq_ignore_ascii_case("ignore");
24585 let is_respect = s.eq_ignore_ascii_case("respect");
24586 if !is_ignore && !is_respect {
24587 return NullTreatment::Respect;
24588 }
24589 // Lookahead for NULLS — only consume both tokens together.
24590 // pos+1 must hold a "nulls" ident.
24591 if self.pos + 1 < self.tokens.len()
24592 && let Token::Ident(s2) = &self.tokens[self.pos + 1]
24593 && s2.eq_ignore_ascii_case("nulls")
24594 {
24595 self.advance();
24596 self.advance();
24597 return if is_ignore {
24598 NullTreatment::Ignore
24599 } else {
24600 NullTreatment::Respect
24601 };
24602 }
24603 NullTreatment::Respect
24604 }
24605
24606 /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
24607 /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
24608 /// (same shape as the `OVER` tail). Consumes the whole clause and
24609 /// returns the predicate; returns `None` when no `FILTER` follows.
24610 fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
24611 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24612 return Ok(None);
24613 };
24614 if !s.eq_ignore_ascii_case("filter") {
24615 return Ok(None);
24616 }
24617 self.advance(); // FILTER
24618 if !matches!(self.peek(), Token::LParen) {
24619 return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
24620 }
24621 self.advance(); // (
24622 if !matches!(self.peek(), Token::Where) {
24623 return Err(self.err(format!(
24624 "expected WHERE inside FILTER (...), got {:?}",
24625 self.peek()
24626 )));
24627 }
24628 self.advance(); // WHERE
24629 let cond = self.parse_expr(0)?;
24630 if !matches!(self.peek(), Token::RParen) {
24631 return Err(self.err(format!(
24632 "expected ')' to close FILTER (WHERE ...), got {:?}",
24633 self.peek()
24634 )));
24635 }
24636 self.advance(); // )
24637 Ok(Some(Box::new(cond)))
24638 }
24639
24640 /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
24641 /// the separator as the aggregate's second argument, which is the
24642 /// shape `string_agg` already takes. Returns whether one was there.
24643 fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
24644 if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
24645 return Ok(false);
24646 }
24647 self.advance();
24648 let Token::String(sep) = self.peek().clone() else {
24649 return Err(self.err(alloc::format!(
24650 "expected a string literal after SEPARATOR, got {:?}",
24651 self.peek()
24652 )));
24653 };
24654 self.advance();
24655 args.push(Expr::Literal(Literal::String(sep)));
24656 Ok(true)
24657 }
24658
24659 /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
24660 /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
24661 /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
24662 /// keys, or an empty vec when no `WITHIN GROUP` follows.
24663 fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
24664 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24665 return Ok(Vec::new());
24666 };
24667 if !s.eq_ignore_ascii_case("within") {
24668 return Ok(Vec::new());
24669 }
24670 self.advance(); // WITHIN
24671 if !matches!(self.peek(), Token::Group) {
24672 return Err(self.err(format!(
24673 "expected GROUP after WITHIN, got {:?}",
24674 self.peek()
24675 )));
24676 }
24677 self.advance(); // GROUP
24678 if !matches!(self.peek(), Token::LParen) {
24679 return Err(self.err(format!(
24680 "expected '(' after WITHIN GROUP, got {:?}",
24681 self.peek()
24682 )));
24683 }
24684 self.advance(); // (
24685 if !matches!(self.peek(), Token::Order) {
24686 return Err(self.err(format!(
24687 "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
24688 self.peek()
24689 )));
24690 }
24691 self.advance(); // ORDER
24692 if !self.peek_is_by() {
24693 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24694 }
24695 self.advance(); // BY
24696 let mut keys: Vec<OrderBy> = Vec::new();
24697 loop {
24698 // v7.39 (round 691) — save/restore, the discipline this parser
24699 // already uses around `pending_sample_preds`, so a subquery inside
24700 // a key neither inherits nor leaks the channel.
24701 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
24702 let saved_coll = self.order_key_collation.take();
24703 let parsed = self.parse_expr(0);
24704 self.in_order_by_key = saved_flag;
24705 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
24706 let expr = parsed?;
24707 let desc = if matches!(self.peek(), Token::Desc) {
24708 self.advance();
24709 true
24710 } else if matches!(self.peek(), Token::Asc) {
24711 self.advance();
24712 false
24713 } else {
24714 false
24715 };
24716 let nulls_first = self.parse_optional_nulls_placement()?;
24717 keys.push(OrderBy {
24718 expr,
24719 desc,
24720 nulls_first,
24721 collation,
24722 });
24723 if matches!(self.peek(), Token::Comma) {
24724 self.advance();
24725 } else {
24726 break;
24727 }
24728 }
24729 if !matches!(self.peek(), Token::RParen) {
24730 return Err(self.err(format!(
24731 "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
24732 self.peek()
24733 )));
24734 }
24735 self.advance(); // )
24736 Ok(keys)
24737 }
24738
24739 /// No frame clause is supported.
24740 #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
24741 fn parse_over_clause(
24742 &mut self,
24743 ) -> Result<
24744 (
24745 Vec<Expr>,
24746 Vec<(Expr, bool, Option<bool>)>,
24747 Option<WindowFrame>,
24748 ),
24749 ParseError,
24750 > {
24751 // `OVER w` — a named-window reference. The WINDOW clause
24752 // parses after the select list, so the name rides out as a
24753 // marker in partition_by; parse_bare_select substitutes the
24754 // definition once the clause is known.
24755 if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
24756 let name = w.clone();
24757 self.advance();
24758 return Ok((
24759 alloc::vec![Expr::Column(crate::ast::ColumnName {
24760 qualifier: Some("__named_window__".to_string()),
24761 name,
24762 })],
24763 Vec::new(),
24764 None,
24765 ));
24766 }
24767 if !matches!(self.peek(), Token::LParen) {
24768 return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
24769 }
24770 self.advance();
24771 let mut partition_by = Vec::new();
24772 let mut order_by = Vec::new();
24773 // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
24774 // window, refined in place. PG's rules (probed against 18.4) differ
24775 // from the bare `OVER w1` form, so the reference rides out under its
24776 // own marker and `substitute_named_windows` applies them. The base
24777 // name is any leading identifier that isn't a window-spec keyword.
24778 let base_window = match self.peek() {
24779 Token::Ident(s) | Token::QuotedIdent(s)
24780 if !s.eq_ignore_ascii_case("partition")
24781 && !s.eq_ignore_ascii_case("rows")
24782 && !s.eq_ignore_ascii_case("range")
24783 && !s.eq_ignore_ascii_case("groups") =>
24784 {
24785 let n = s.clone();
24786 self.advance();
24787 Some(n)
24788 }
24789 _ => None,
24790 };
24791 // PARTITION BY ?
24792 // v7.37.6-B promoted PARTITION to a reserved keyword
24793 // (Token::Partition); pre-7.37.6-B catalogs lexed it as
24794 // `Token::Ident("partition")`. Accept both so older sources
24795 // and the new lexer surface land on the same path.
24796 let is_partition_kw = match self.peek() {
24797 Token::Partition => true,
24798 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
24799 _ => false,
24800 };
24801 if is_partition_kw {
24802 self.advance();
24803 if !self.peek_is_by() {
24804 return Err(self.err(format!(
24805 "expected BY after PARTITION, got {:?}",
24806 self.peek()
24807 )));
24808 }
24809 self.advance();
24810 loop {
24811 partition_by.push(self.parse_expr(0)?);
24812 if matches!(self.peek(), Token::Comma) {
24813 self.advance();
24814 continue;
24815 }
24816 break;
24817 }
24818 }
24819 // ORDER BY ?
24820 if matches!(self.peek(), Token::Order) {
24821 self.advance();
24822 if !self.peek_is_by() {
24823 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24824 }
24825 self.advance();
24826 loop {
24827 let e = self.parse_expr(0)?;
24828 let desc = if matches!(self.peek(), Token::Desc) {
24829 self.advance();
24830 true
24831 } else if matches!(self.peek(), Token::Asc) {
24832 self.advance();
24833 false
24834 } else {
24835 false
24836 };
24837 // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
24838 let nulls_first = self.parse_optional_nulls_placement()?;
24839 order_by.push((e, desc, nulls_first));
24840 if matches!(self.peek(), Token::Comma) {
24841 self.advance();
24842 continue;
24843 }
24844 break;
24845 }
24846 }
24847 // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
24848 // Both keywords come through the lexer as identifiers; match
24849 // case-insensitively.
24850 let mut frame: Option<WindowFrame> = None;
24851 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
24852 let kind = if s.eq_ignore_ascii_case("rows") {
24853 Some(FrameKind::Rows)
24854 } else if s.eq_ignore_ascii_case("range") {
24855 Some(FrameKind::Range)
24856 } else if s.eq_ignore_ascii_case("groups") {
24857 // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
24858 Some(FrameKind::Groups)
24859 } else {
24860 None
24861 };
24862 if let Some(kind) = kind {
24863 self.advance();
24864 frame = Some(self.parse_frame_tail(kind)?);
24865 }
24866 }
24867 if !matches!(self.peek(), Token::RParen) {
24868 return Err(self.err(format!(
24869 "expected ')' to close OVER clause, got {:?}",
24870 self.peek()
24871 )));
24872 }
24873 self.advance();
24874 if let Some(base) = base_window {
24875 // A copy may refine but never override the base's partitioning
24876 // (PG rejects it outright, before looking the name up).
24877 if !partition_by.is_empty() {
24878 return Err(self.err(alloc::format!(
24879 "cannot override PARTITION BY clause of window \"{base}\""
24880 )));
24881 }
24882 partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
24883 qualifier: Some("__named_window_ref__".to_string()),
24884 name: base,
24885 })];
24886 }
24887 Ok((partition_by, order_by, frame))
24888 }
24889
24890 /// v4.20: parse the tail of an explicit frame, given the `ROWS`
24891 /// or `RANGE` keyword was just consumed. Accepts both
24892 /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
24893 /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
24894 /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
24895 fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
24896 let (start, end) = if matches!(self.peek(), Token::Between) {
24897 self.advance();
24898 let start = self.parse_frame_bound()?;
24899 if !matches!(self.peek(), Token::And) {
24900 return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
24901 }
24902 self.advance();
24903 let end = self.parse_frame_bound()?;
24904 (start, Some(end))
24905 } else {
24906 (self.parse_frame_bound()?, None)
24907 };
24908 let exclude = self.parse_frame_exclusion()?;
24909 Ok(WindowFrame {
24910 kind,
24911 start,
24912 end,
24913 exclude,
24914 })
24915 }
24916
24917 /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
24918 /// after a frame spec. NO OTHERS is the default no-op.
24919 fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
24920 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
24921 return Ok(FrameExclusion::NoOthers);
24922 }
24923 self.advance(); // EXCLUDE
24924 match self.peek() {
24925 Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
24926 self.advance();
24927 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
24928 return Err(self.err(format!(
24929 "expected ROW after EXCLUDE CURRENT, got {:?}",
24930 self.peek()
24931 )));
24932 }
24933 self.advance();
24934 Ok(FrameExclusion::CurrentRow)
24935 }
24936 // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
24937 // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
24938 // Without this arm it fell to the catch-all, whose message
24939 // self-contradictingly listed GROUP as expected.
24940 Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
24941 self.advance();
24942 Ok(FrameExclusion::Group)
24943 }
24944 Token::Group => {
24945 self.advance();
24946 Ok(FrameExclusion::Group)
24947 }
24948 Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
24949 self.advance();
24950 Ok(FrameExclusion::Ties)
24951 }
24952 Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
24953 self.advance();
24954 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
24955 return Err(self.err(format!(
24956 "expected OTHERS after EXCLUDE NO, got {:?}",
24957 self.peek()
24958 )));
24959 }
24960 self.advance();
24961 Ok(FrameExclusion::NoOthers)
24962 }
24963 other => Err(self.err(format!(
24964 "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
24965 ))),
24966 }
24967 }
24968
24969 /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
24970 /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
24971 /// `UNBOUNDED FOLLOWING`.
24972 fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
24973 // Interval-typed offset for a value-based RANGE frame over a
24974 // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
24975 // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
24976 // PRECEDING`.
24977 if let Some((months, days, micros)) = self.try_take_interval_offset()? {
24978 let dir = self.expect_ident_like()?;
24979 return if dir.eq_ignore_ascii_case("preceding") {
24980 Ok(FrameBound::IntervalPreceding {
24981 months,
24982 days,
24983 micros,
24984 })
24985 } else if dir.eq_ignore_ascii_case("following") {
24986 Ok(FrameBound::IntervalFollowing {
24987 months,
24988 days,
24989 micros,
24990 })
24991 } else {
24992 Err(self.err(format!(
24993 "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
24994 )))
24995 };
24996 }
24997 // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
24998 if let Token::Integer(n) = *self.peek() {
24999 self.advance();
25000 let n: u64 = u64::try_from(n).map_err(|_| {
25001 self.err(format!(
25002 "invalid frame offset {n} — expected non-negative integer"
25003 ))
25004 })?;
25005 let dir = self.expect_ident_like()?;
25006 return if dir.eq_ignore_ascii_case("preceding") {
25007 Ok(FrameBound::OffsetPreceding(n))
25008 } else if dir.eq_ignore_ascii_case("following") {
25009 Ok(FrameBound::OffsetFollowing(n))
25010 } else {
25011 Err(self.err(format!(
25012 "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
25013 )))
25014 };
25015 }
25016 let first = self.expect_ident_like()?;
25017 if first.eq_ignore_ascii_case("unbounded") {
25018 let dir = self.expect_ident_like()?;
25019 return if dir.eq_ignore_ascii_case("preceding") {
25020 Ok(FrameBound::UnboundedPreceding)
25021 } else if dir.eq_ignore_ascii_case("following") {
25022 Ok(FrameBound::UnboundedFollowing)
25023 } else {
25024 Err(self.err(format!(
25025 "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
25026 )))
25027 };
25028 }
25029 if first.eq_ignore_ascii_case("current") {
25030 let row = self.expect_ident_like()?;
25031 if !row.eq_ignore_ascii_case("row") {
25032 return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
25033 }
25034 return Ok(FrameBound::CurrentRow);
25035 }
25036 Err(self.err(format!(
25037 "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
25038 )))
25039 }
25040
25041 /// Detect and consume a leading interval offset in a frame bound —
25042 /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
25043 /// `(months, days, micros)`. Leaves the cursor on the trailing
25044 /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
25045 /// when the next tokens are not an interval offset.
25046 fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
25047 // Shape A — `INTERVAL '1 day'`.
25048 if matches!(self.peek(), Token::Interval) {
25049 self.advance(); // INTERVAL
25050 let atom = self.parse_interval_atom()?;
25051 if let Expr::Literal(Literal::Interval {
25052 months,
25053 days,
25054 micros,
25055 ..
25056 }) = atom
25057 {
25058 return Ok(Some((months, days, micros)));
25059 }
25060 return Err(self.err("expected an interval literal in frame offset".to_string()));
25061 }
25062 // Shape B — `'1 day'::interval`. Look ahead for the exact
25063 // string / `::` / interval-target triple before committing.
25064 if let Token::String(text) = self.peek() {
25065 let target_is_interval = match self.tokens.get(self.pos + 2) {
25066 Some(Token::Interval) => true,
25067 Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
25068 _ => false,
25069 };
25070 let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
25071 && target_is_interval;
25072 if is_cast {
25073 let text = text.clone();
25074 self.advance(); // string
25075 self.advance(); // ::
25076 self.advance(); // interval
25077 let parts = parse_interval_text(&text).ok_or_else(|| {
25078 self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
25079 })?;
25080 return Ok(Some(parts));
25081 }
25082 }
25083 Ok(None)
25084 }
25085
25086 fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
25087 if matches!(self.peek(), Token::Dot) {
25088 self.advance();
25089 let name = self.expect_ident_like()?;
25090 // v7.14.0 — schema-qualified function call
25091 // `<schema>.<fn>(args)`. PG dumps emit
25092 // `pg_catalog.set_config(...)` in the preamble. SPG
25093 // is single-namespace: drop the schema prefix and
25094 // route the dispatch on the bare function name.
25095 if matches!(self.peek(), Token::LParen) {
25096 return self.finish_ident_atom(name);
25097 }
25098 return Ok(Expr::Column(ColumnName {
25099 qualifier: Some(first),
25100 name,
25101 }));
25102 }
25103 if matches!(self.peek(), Token::LParen) {
25104 self.advance();
25105 // `COUNT(*)` — special-cased here because `*` isn't a normal
25106 // expression token. Lower-case match on `first` since the lexer
25107 // folds identifiers.
25108 if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
25109 self.advance();
25110 if !matches!(self.peek(), Token::RParen) {
25111 return Err(self.err(format!(
25112 "expected ')' after COUNT(*), got {:?}",
25113 self.peek()
25114 )));
25115 }
25116 self.advance();
25117 // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
25118 let filter = self.parse_filter_clause()?;
25119 // v4.12: COUNT(*) OVER (...) — same window tail.
25120 let null_treatment = self.parse_null_treatment_modifier();
25121 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25122 && s.eq_ignore_ascii_case("over")
25123 {
25124 self.advance();
25125 let (partition_by, order_by, frame) = self.parse_over_clause()?;
25126 return Ok(Expr::WindowFunction {
25127 name: "count_star".into(),
25128 args: Vec::new(),
25129 partition_by,
25130 order_by,
25131 frame,
25132 null_treatment,
25133 filter,
25134 });
25135 }
25136 if let Some(filter) = filter {
25137 return Ok(Expr::AggregateOrdered {
25138 call: Box::new(Expr::FunctionCall {
25139 name: "count_star".into(),
25140 args: Vec::new(),
25141 }),
25142 order_by: Vec::new(),
25143 distinct: false,
25144 filter: Some(filter),
25145 });
25146 }
25147 return Ok(Expr::FunctionCall {
25148 name: "count_star".into(),
25149 args: Vec::new(),
25150 });
25151 }
25152 // Function call. PG-style: zero-or-more comma-separated args.
25153 let mut args = Vec::new();
25154 // v7.38 (read01, T14) — named-argument notation `argname => value`.
25155 // Names are collected in lock-step with `args` and resolved to
25156 // positional order after the loop (the AST stays positional).
25157 let mut arg_names: Vec<Option<String>> = Vec::new();
25158 let mut agg_order_by: Vec<OrderBy> = Vec::new();
25159 // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
25160 // seen, so the value arguments before it can be folded.
25161 let mut saw_separator = false;
25162 // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
25163 // v7.32 (round-29) — accept the dual `ALL` quantifier too
25164 // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
25165 let agg_distinct = if matches!(self.peek(), Token::Distinct) {
25166 self.advance();
25167 true
25168 } else if matches!(self.peek(), Token::All) {
25169 self.advance();
25170 false
25171 } else {
25172 false
25173 };
25174 // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
25175 // TIMESTAMPDIFF take a bare unit keyword as the first
25176 // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
25177 // bare type keyword (DATE / TIME / DATETIME); lower them
25178 // onto string literals so the evaluator sees plain text.
25179 if ((first.eq_ignore_ascii_case("timestampadd")
25180 || first.eq_ignore_ascii_case("timestampdiff"))
25181 && matches!(self.peek(), Token::Ident(u) if matches!(
25182 u.to_ascii_lowercase().as_str(),
25183 "microsecond" | "second" | "minute" | "hour" | "day"
25184 | "week" | "month" | "quarter" | "year"
25185 )))
25186 || (first.eq_ignore_ascii_case("get_format")
25187 && matches!(self.peek(), Token::Ident(u) if matches!(
25188 u.to_ascii_lowercase().as_str(),
25189 "date" | "time" | "datetime" | "timestamp"
25190 )))
25191 {
25192 if let Token::Ident(u) = self.peek() {
25193 args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
25194 }
25195 self.advance();
25196 if matches!(self.peek(), Token::Comma) {
25197 self.advance();
25198 }
25199 }
25200 // `ROW(a, b, …)` keyword constructor. Followed by a
25201 // comparison operator or [NOT] IN it joins the paren
25202 // row-constructor machinery (fieldwise parse-time
25203 // expansion); bare, it stays a `row` call the evaluator
25204 // renders as PG record text.
25205 if first.eq_ignore_ascii_case("row") {
25206 let mut row_items = Vec::new();
25207 if !matches!(self.peek(), Token::RParen) {
25208 loop {
25209 row_items.push(self.parse_expr(0)?);
25210 match self.peek() {
25211 Token::Comma => {
25212 self.advance();
25213 }
25214 Token::RParen => break,
25215 other => {
25216 return Err(self.err(format!(
25217 "expected ',' or ')' in ROW(...), got {other:?}"
25218 )));
25219 }
25220 }
25221 }
25222 }
25223 self.advance(); // ')'
25224 let comparison_follows = matches!(
25225 self.peek(),
25226 Token::Eq
25227 | Token::NotEq
25228 | Token::Lt
25229 | Token::LtEq
25230 | Token::Gt
25231 | Token::GtEq
25232 | Token::In
25233 ) || (matches!(self.peek(), Token::Not)
25234 && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25235 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25236 if comparison_follows && !row_items.is_empty() {
25237 return self.parse_row_comparison_tail(row_items);
25238 }
25239 return Ok(Expr::FunctionCall {
25240 name: String::from("row"),
25241 args: row_items,
25242 });
25243 }
25244 // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25245 // the parse-mode keyword introduces the source text. SPG
25246 // carries XML as text, so both modes lower to __xmlparse(expr)
25247 // which validates well-formedness and returns Value::Xml.
25248 if first.eq_ignore_ascii_case("xmlparse")
25249 && matches!(self.peek(), Token::Ident(kw)
25250 if kw.eq_ignore_ascii_case("document")
25251 || kw.eq_ignore_ascii_case("content"))
25252 {
25253 let mode = match self.advance() {
25254 Token::Ident(kw) => kw.to_ascii_lowercase(),
25255 _ => unreachable!("peeked an ident"),
25256 };
25257 let src = self.parse_expr(0)?;
25258 if !matches!(self.peek(), Token::RParen) {
25259 return Err(self.err(format!(
25260 "expected ')' to close XMLPARSE, got {:?}",
25261 self.peek()
25262 )));
25263 }
25264 self.advance();
25265 return Ok(Expr::FunctionCall {
25266 name: String::from("__xmlparse"),
25267 args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25268 });
25269 }
25270 // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25271 // keyword introduces the element name (a bare or quoted
25272 // identifier), then optional content expressions. Lower to a
25273 // plain `xmlelement(name_text, content …)` call.
25274 if first.eq_ignore_ascii_case("xmlelement")
25275 && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25276 {
25277 self.advance(); // consume NAME
25278 let elem_name = match self.peek().clone() {
25279 Token::Ident(n) | Token::QuotedIdent(n) => {
25280 self.advance();
25281 n
25282 }
25283 other => {
25284 return Err(self.err(format!(
25285 "expected element name after XMLELEMENT NAME, got {other:?}"
25286 )));
25287 }
25288 };
25289 let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25290 while matches!(self.peek(), Token::Comma) {
25291 self.advance();
25292 args.push(self.parse_expr(0)?);
25293 }
25294 if !matches!(self.peek(), Token::RParen) {
25295 return Err(self.err(format!(
25296 "expected ')' to close XMLELEMENT, got {:?}",
25297 self.peek()
25298 )));
25299 }
25300 self.advance();
25301 return Ok(Expr::FunctionCall {
25302 name: String::from("xmlelement"),
25303 args,
25304 });
25305 }
25306 // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
25307 // becomes a `<name>value</name>` element; a bare column infers its
25308 // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
25309 if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
25310 let mut args: Vec<Expr> = Vec::new();
25311 loop {
25312 let val = self.parse_expr(0)?;
25313 let name = if matches!(self.peek(), Token::As) {
25314 self.advance();
25315 match self.peek().clone() {
25316 Token::Ident(n) | Token::QuotedIdent(n) => {
25317 self.advance();
25318 n
25319 }
25320 other => {
25321 return Err(self.err(format!(
25322 "expected name after AS in XMLFOREST, got {other:?}"
25323 )));
25324 }
25325 }
25326 } else if let Expr::Column(c) = &val {
25327 c.name.clone()
25328 } else {
25329 return Err(
25330 self.err("XMLFOREST element without a column name needs AS".into())
25331 );
25332 };
25333 args.push(Expr::Literal(Literal::String(name)));
25334 args.push(val);
25335 if matches!(self.peek(), Token::Comma) {
25336 self.advance();
25337 } else {
25338 break;
25339 }
25340 }
25341 if !matches!(self.peek(), Token::RParen) {
25342 return Err(self.err(format!(
25343 "expected ')' to close XMLFOREST, got {:?}",
25344 self.peek()
25345 )));
25346 }
25347 self.advance();
25348 return Ok(Expr::FunctionCall {
25349 name: String::from("xmlforest"),
25350 args,
25351 });
25352 }
25353 // SQL-standard `POSITION(sub IN str)` — lowers onto
25354 // strpos(str, sub). IN is the argument separator here,
25355 // so the needle parses with the IN-tail suppressed.
25356 if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
25357 let saved = self.suppress_in_tail;
25358 self.suppress_in_tail = true;
25359 let needle = self.parse_expr(0);
25360 self.suppress_in_tail = saved;
25361 let needle = needle?;
25362 if matches!(self.peek(), Token::In) {
25363 self.advance();
25364 let haystack = self.parse_expr(0)?;
25365 if !matches!(self.peek(), Token::RParen) {
25366 return Err(self.err(format!(
25367 "expected ')' to close POSITION, got {:?}",
25368 self.peek()
25369 )));
25370 }
25371 self.advance();
25372 return Ok(Expr::FunctionCall {
25373 name: String::from("strpos"),
25374 args: alloc::vec![haystack, needle],
25375 });
25376 }
25377 // position(sub, str) comma form (incl. bytea) —
25378 // hand the parsed first arg to the generic list.
25379 args.push(needle);
25380 if matches!(self.peek(), Token::Comma) {
25381 self.advance();
25382 }
25383 }
25384 // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
25385 // FROM str)` — lowers onto btrim / ltrim / rtrim. The
25386 // plain comma forms TRIM(str) / TRIM(str, chars) keep
25387 // riding the generic argument list below.
25388 if first.eq_ignore_ascii_case("trim") {
25389 let mode = match self.peek() {
25390 Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
25391 self.advance();
25392 Some("btrim")
25393 }
25394 Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
25395 self.advance();
25396 Some("ltrim")
25397 }
25398 Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
25399 self.advance();
25400 Some("rtrim")
25401 }
25402 _ => None,
25403 };
25404 if mode.is_some() || matches!(self.peek(), Token::From) {
25405 // TRIM([mode] FROM str) — no strip-chars.
25406 let chars = if matches!(self.peek(), Token::From) {
25407 None
25408 } else {
25409 Some(self.parse_expr(0)?)
25410 };
25411 if !matches!(self.peek(), Token::From) {
25412 return Err(self.err(format!(
25413 "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
25414 self.peek()
25415 )));
25416 }
25417 self.advance();
25418 let target = self.parse_expr(0)?;
25419 if !matches!(self.peek(), Token::RParen) {
25420 return Err(
25421 self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
25422 );
25423 }
25424 self.advance();
25425 let mut trim_args = alloc::vec![target];
25426 if let Some(c) = chars {
25427 trim_args.push(c);
25428 }
25429 return Ok(Expr::FunctionCall {
25430 name: String::from(mode.unwrap_or("btrim")),
25431 args: trim_args,
25432 });
25433 }
25434 }
25435 if !matches!(self.peek(), Token::RParen) {
25436 loop {
25437 // v7.38 (read01, T14) — `argname => value` names this arg.
25438 // v7.39 (read01 round 77) — `argname := value` is the same
25439 // thing, and it is the spelling PG's own docs lead with. It
25440 // was simply never lexed here, so every `f(x := 1)` died in
25441 // the parser regardless of what `f` was.
25442 let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
25443 (
25444 Token::Ident(n) | Token::QuotedIdent(n),
25445 Some(Token::FatArrow | Token::ColonEq),
25446 ) => {
25447 let name = n.clone();
25448 self.advance(); // name
25449 self.advance(); // => / :=
25450 Some(name)
25451 }
25452 _ => None,
25453 };
25454 // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
25455 // array's elements into a variadic call's trailing args
25456 // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
25457 // reserved, so it arrives as a bare ident before the arg.
25458 let is_variadic = this_name.is_none()
25459 && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
25460 if is_variadic {
25461 self.advance();
25462 }
25463 let arg = self.parse_expr(0)?;
25464 args.push(match &this_name {
25465 // The callee's parameter names decide the slot, and a
25466 // user function's live in the catalog. Carry the name
25467 // to eval rather than guessing here.
25468 Some(n) => Expr::NamedArg {
25469 name: n.clone(),
25470 expr: Box::new(arg),
25471 },
25472 None if is_variadic => Expr::Variadic(Box::new(arg)),
25473 None => arg,
25474 });
25475 arg_names.push(this_name);
25476 // v7.25 (round-17) — standard `CAST(expr AS type)`.
25477 // The `::` cast already worked; this lowers the
25478 // function form onto the same Expr::Cast node.
25479 if first.eq_ignore_ascii_case("cast")
25480 && args.len() == 1
25481 && matches!(self.peek(), Token::As)
25482 {
25483 self.advance();
25484 let target = self.parse_cast_target()?;
25485 if !matches!(self.peek(), Token::RParen) {
25486 return Err(self.err(format!(
25487 "expected ')' to close CAST, got {:?}",
25488 self.peek()
25489 )));
25490 }
25491 self.advance();
25492 return Ok(Expr::Cast {
25493 expr: Box::new(args.pop().expect("one arg")),
25494 target,
25495 });
25496 }
25497 // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
25498 // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
25499 // keywords; SPG's lexer makes them plain idents (so they'd be
25500 // read as column refs). Lower the keyword to the string form
25501 // the evaluator already accepts.
25502 if first.eq_ignore_ascii_case("normalize")
25503 && args.len() == 1
25504 && matches!(self.peek(), Token::Comma)
25505 {
25506 let form = match self.tokens.get(self.pos + 1) {
25507 Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
25508 let up = f.to_ascii_uppercase();
25509 matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
25510 }
25511 _ => None,
25512 };
25513 if let Some(up) = form {
25514 self.advance(); // comma
25515 self.advance(); // form keyword
25516 args.push(Expr::Literal(Literal::String(up)));
25517 }
25518 }
25519 // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
25520 // form. Desugars to the comma-list shape evaluator already
25521 // handles. Triggered after the first arg when the function
25522 // name is substring / substr and the next token is FROM
25523 // (a reserved keyword in PG; SPG also reserves it).
25524 // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
25525 // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
25526 // internal __substring_similar(str, pat, esc) call.
25527 if (first.eq_ignore_ascii_case("substring")
25528 || first.eq_ignore_ascii_case("substr"))
25529 && args.len() == 1
25530 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
25531 {
25532 self.advance(); // SIMILAR
25533 let pattern = self.parse_expr(0)?;
25534 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
25535 {
25536 return Err(self.err(format!(
25537 "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
25538 self.peek()
25539 )));
25540 }
25541 self.advance(); // ESCAPE
25542 let esc = self.parse_expr(0)?;
25543 if !matches!(self.peek(), Token::RParen) {
25544 return Err(self.err(format!(
25545 "expected ')' to close substring(... SIMILAR ...), got {:?}",
25546 self.peek()
25547 )));
25548 }
25549 self.advance();
25550 args.push(pattern);
25551 args.push(esc);
25552 return Ok(Expr::FunctionCall {
25553 name: "__substring_similar".to_string(),
25554 args,
25555 });
25556 }
25557 if (first.eq_ignore_ascii_case("substring")
25558 || first.eq_ignore_ascii_case("substr"))
25559 && args.len() == 1
25560 && matches!(self.peek(), Token::From | Token::For)
25561 {
25562 // `substring(str FROM pos [FOR len])`, or the FOR-only
25563 // `substring(str FOR len)` which PG treats as FROM 1.
25564 if matches!(self.peek(), Token::From) {
25565 self.advance();
25566 let start = self.parse_expr(0)?;
25567 args.push(start);
25568 } else {
25569 args.push(Expr::Literal(Literal::Integer(1)));
25570 }
25571 if matches!(self.peek(), Token::For) {
25572 self.advance();
25573 let length = self.parse_expr(0)?;
25574 args.push(length);
25575 }
25576 if !matches!(self.peek(), Token::RParen) {
25577 return Err(self.err(format!(
25578 "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
25579 self.peek()
25580 )));
25581 }
25582 self.advance();
25583 return Ok(Expr::FunctionCall {
25584 name: first.to_ascii_lowercase(),
25585 args,
25586 });
25587 }
25588 // PG `overlay(str PLACING repl FROM n [FOR len])`
25589 // syntactic form. Desugars to the `overlay(str,
25590 // repl, n[, len])` comma-list shape the evaluator
25591 // already implements. `PLACING` is not a reserved
25592 // token in SPG, so it arrives as a bare Ident.
25593 if first.eq_ignore_ascii_case("overlay")
25594 && args.len() == 1
25595 && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
25596 {
25597 self.advance(); // consume PLACING
25598 args.push(self.parse_expr(0)?); // replacement
25599 if !matches!(self.peek(), Token::From) {
25600 return Err(self.err(format!(
25601 "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
25602 self.peek()
25603 )));
25604 }
25605 self.advance();
25606 args.push(self.parse_expr(0)?); // start position
25607 if matches!(self.peek(), Token::For) {
25608 self.advance();
25609 args.push(self.parse_expr(0)?); // length
25610 }
25611 if !matches!(self.peek(), Token::RParen) {
25612 return Err(self.err(format!(
25613 "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
25614 self.peek()
25615 )));
25616 }
25617 self.advance();
25618 return Ok(Expr::FunctionCall {
25619 name: String::from("overlay"),
25620 args,
25621 });
25622 }
25623 // `TRIM(chars FROM str)` — the keyword-less
25624 // spelling lands here after the chars parse
25625 // (the keyword forms return earlier).
25626 if first.eq_ignore_ascii_case("trim")
25627 && args.len() == 1
25628 && matches!(self.peek(), Token::From)
25629 {
25630 self.advance();
25631 let target = self.parse_expr(0)?;
25632 if !matches!(self.peek(), Token::RParen) {
25633 return Err(self.err(format!(
25634 "expected ')' to close TRIM(chars FROM str), got {:?}",
25635 self.peek()
25636 )));
25637 }
25638 self.advance();
25639 let chars = args.pop().expect("one arg");
25640 return Ok(Expr::FunctionCall {
25641 name: String::from("btrim"),
25642 args: alloc::vec![target, chars],
25643 });
25644 }
25645 // v7.24 (round-16 A) — aggregate-internal
25646 // ordering: `array_agg(x ORDER BY y DESC NULLS
25647 // LAST)`. Keys close the argument list.
25648 if matches!(self.peek(), Token::Order) {
25649 self.advance();
25650 if !self.peek_is_by() {
25651 return Err(self.err(format!(
25652 "expected BY after ORDER in aggregate args, got {:?}",
25653 self.peek()
25654 )));
25655 }
25656 self.advance();
25657 loop {
25658 // v7.39 (round 691) — save/restore, the discipline this parser
25659 // already uses around `pending_sample_preds`, so a subquery inside
25660 // a key neither inherits nor leaks the channel.
25661 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25662 let saved_coll = self.order_key_collation.take();
25663 let parsed = self.parse_expr(0);
25664 self.in_order_by_key = saved_flag;
25665 let collation =
25666 core::mem::replace(&mut self.order_key_collation, saved_coll);
25667 let expr = parsed?;
25668 let desc = if matches!(self.peek(), Token::Desc) {
25669 self.advance();
25670 true
25671 } else if matches!(self.peek(), Token::Asc) {
25672 self.advance();
25673 false
25674 } else {
25675 false
25676 };
25677 let nulls_first = self.parse_optional_nulls_placement()?;
25678 agg_order_by.push(OrderBy {
25679 expr,
25680 desc,
25681 nulls_first,
25682 collation,
25683 });
25684 if matches!(self.peek(), Token::Comma) {
25685 self.advance();
25686 } else {
25687 break;
25688 }
25689 }
25690 // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
25691 // follow the ORDER BY inside GROUP_CONCAT.
25692 if self.consume_group_concat_separator(&mut args)? {
25693 saw_separator = true;
25694 }
25695 if !matches!(self.peek(), Token::RParen) {
25696 return Err(self.err(format!(
25697 "expected ')' after aggregate ORDER BY, got {:?}",
25698 self.peek()
25699 )));
25700 }
25701 break;
25702 }
25703 // v7.39 (round 354, M12) — …or directly after the
25704 // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
25705 // own spelling of what PG passes as string_agg's second
25706 // argument; it was a parse error, so every MySQL query
25707 // that names its own separator failed outright.
25708 if self.consume_group_concat_separator(&mut args)? {
25709 saw_separator = true;
25710 break;
25711 }
25712 match self.peek() {
25713 Token::Comma => {
25714 self.advance();
25715 }
25716 Token::RParen => break,
25717 other => {
25718 return Err(self.err(format!(
25719 "expected ',' or ')' in function args, got {other:?}"
25720 )));
25721 }
25722 }
25723 }
25724 }
25725 // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
25726 // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
25727 // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
25728 // meaning a separator — that is what the explicit SEPARATOR
25729 // tail is for. Fold them into one `concat(...)` so the
25730 // aggregate keeps its single value argument.
25731 if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
25732 let values = args.len() - usize::from(saw_separator);
25733 if values > 1 {
25734 let sep_arg = if saw_separator { args.pop() } else { None };
25735 let folded = Expr::FunctionCall {
25736 name: "concat".to_string(),
25737 args: core::mem::take(&mut args),
25738 };
25739 args.push(folded);
25740 if let Some(sep) = sep_arg {
25741 args.push(sep);
25742 }
25743 }
25744 }
25745 self.advance(); // consume ')'
25746 // v7.39 (read01 round 77) — named arguments are NOT reordered here
25747 // any more. The parser has no catalog, so it could only ever resolve
25748 // the handful of `make_*` builtins whose parameter names were baked
25749 // into a table right here — every user function got
25750 // "does not support named arguments", though the catalog has been
25751 // storing its parameter names all along. Reordering happens in eval,
25752 // in one place, for builtins and user functions alike.
25753 // v7.32 (round-29) — ordered-set aggregate tail
25754 // `name(direct_args) WITHIN GROUP (ORDER BY …)`
25755 // (percentile_cont / percentile_disc / mode). The sort spec
25756 // lands in the same `order_by` slot a decorated aggregate
25757 // uses; the executor dispatches on the function name. WITHIN
25758 // GROUP and an intra-argument ORDER BY are mutually
25759 // exclusive (PG rejects both).
25760 let within_group_order = self.parse_within_group_clause()?;
25761 if !within_group_order.is_empty() && !agg_order_by.is_empty() {
25762 return Err(self.err(
25763 "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
25764 .into(),
25765 ));
25766 }
25767 let within_group_seen = !within_group_order.is_empty();
25768 let agg_order_by = if within_group_order.is_empty() {
25769 agg_order_by
25770 } else {
25771 within_group_order
25772 };
25773 // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
25774 let filter = self.parse_filter_clause()?;
25775 // v4.12: window-function tail — `name(args) OVER (...)`.
25776 // Promotes the just-parsed FunctionCall into a
25777 // WindowFunction node carrying partition + order.
25778 // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
25779 // / `RESPECT NULLS OVER (...)` between the closing paren
25780 // and `OVER`.
25781 let null_treatment = self.parse_null_treatment_modifier();
25782 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25783 && s.eq_ignore_ascii_case("over")
25784 {
25785 self.advance();
25786 // v7.39 (round 230) — PG implements neither modifier for a
25787 // windowed call and says so (0A000). Both used to be parsed
25788 // and then silently dropped here, so `count(DISTINCT v)
25789 // OVER (…)` quietly answered the non-distinct count.
25790 if agg_distinct {
25791 return Err(
25792 self.err("DISTINCT is not implemented for window functions".to_string())
25793 );
25794 }
25795 if !agg_order_by.is_empty() {
25796 // PG separates the two shapes that land here: a
25797 // WITHIN GROUP call is an ordered-set aggregate and gets
25798 // its own message naming the aggregate; a plain
25799 // `agg(x ORDER BY y)` gets the generic one.
25800 let msg = if within_group_seen {
25801 alloc::format!("OVER is not supported for ordered-set aggregate {first}")
25802 } else {
25803 "aggregate ORDER BY is not implemented for window functions".to_string()
25804 };
25805 return Err(self.err(msg));
25806 }
25807 let (partition_by, order_by, frame) = self.parse_over_clause()?;
25808 return Ok(Expr::WindowFunction {
25809 name: first,
25810 args,
25811 partition_by,
25812 order_by,
25813 frame,
25814 null_treatment,
25815 filter,
25816 });
25817 }
25818 if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
25819 return Ok(Expr::AggregateOrdered {
25820 call: Box::new(Expr::FunctionCall { name: first, args }),
25821 order_by: agg_order_by,
25822 distinct: agg_distinct,
25823 filter,
25824 });
25825 }
25826 // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
25827 // over TIMESTAMPTZ and has no timestamp overload, so a
25828 // timestamp argument is coerced on the way in and the answer
25829 // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
25830 // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
25831 // zone`. SPG answered `timestamp without time zone`, dropping
25832 // the offset from every rendering.
25833 //
25834 // Writing the coercion PG performs makes the existing
25835 // argument-driven typing (the one `date_trunc` uses) reach the
25836 // right answer, rather than teaching the type layer a second
25837 // rule. MySQL's DATE_ADD is a different function that returns
25838 // DATE or DATETIME, so this is PG-dialect only.
25839 //
25840 // Out-of-line because this sits on the RECURSIVE descent
25841 // frame: an inline block with locals here costs every nesting
25842 // level, and the suite's deep-nesting sentinel overflowed the
25843 // 512 KiB parser stack the moment one was added (round 430's
25844 // lesson, in the same shape).
25845 if !self.mysql_dialect {
25846 lift_date_add_arg_to_timestamptz(&first, &mut args);
25847 }
25848 return Ok(Expr::FunctionCall { name: first, args });
25849 }
25850 // v7.9.20 — SQL-standard parenless keyword expressions
25851 // (PG treats these as functions called without parens).
25852 // Resolve to a synthetic FunctionCall so the engine's
25853 // eval path reuses the existing function-call routing.
25854 // mailrs G3.
25855 let lc = first.to_ascii_lowercase();
25856 if matches!(
25857 lc.as_str(),
25858 "current_date"
25859 | "current_time"
25860 | "current_timestamp"
25861 | "localtimestamp"
25862 | "localtime"
25863 // v7.37.17 (17.6 siblings) — session-identity SQL-
25864 // standard parenless keywords. current_user /
25865 // session_user / user were already caught by the
25866 // pgwire canned-response shortcut but bare-select
25867 // in the embedded engine went through Expr::Column
25868 // and errored. Adding them here so the parser
25869 // resolves to a synthetic FunctionCall that reuses
25870 // the existing eval/functions.rs dispatch.
25871 | "current_user"
25872 | "session_user"
25873 | "current_role"
25874 | "current_catalog"
25875 | "current_schema"
25876 | "current_database"
25877 // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
25878 | "system_user"
25879 ) {
25880 return Ok(Expr::FunctionCall {
25881 name: lc,
25882 args: Vec::new(),
25883 });
25884 }
25885 Ok(Expr::Column(ColumnName {
25886 qualifier: None,
25887 name: first,
25888 }))
25889 }
25890}
25891
25892/// v7.39 (round 522) — write the coercion PG's `date_add` /
25893/// `date_subtract` signature performs.
25894///
25895/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
25896/// timestamp argument is cast on the way in and the answer is
25897/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
25898/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
25899/// `timestamp without time zone`, dropping the offset from every
25900/// rendering of the result.
25901///
25902/// Writing the cast the signature implies lets the existing
25903/// argument-driven typing (the one `date_trunc` uses) reach the right
25904/// answer instead of teaching the type layer a second rule. MySQL's
25905/// DATE_ADD is a different function returning DATE or DATETIME, so the
25906/// caller applies this in PG dialect only.
25907///
25908/// A free function, and not a block at the call site, because the caller
25909/// is on the recursive-descent frame chain.
25910#[inline(never)]
25911fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
25912 if args.len() != 2
25913 || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
25914 {
25915 return;
25916 }
25917 let base = args.remove(0);
25918 args.insert(
25919 0,
25920 Expr::Cast {
25921 expr: Box::new(base),
25922 target: CastTarget::Timestamptz,
25923 },
25924 );
25925}
25926
25927/// v6.8.2 — walk an expression tree and return the first column
25928/// reference's bare name. Used by `parse_create_index_stmt_after_create`
25929/// to derive `CreateIndexStatement.column` from an expression
25930/// key (so downstream planner code resolving a primary column
25931/// position keeps working with expression indexes). Returns
25932/// `None` when the expression has no column ref at all — caller
25933/// surfaces that as a parse error.
25934fn extract_first_column(expr: &Expr) -> Option<String> {
25935 match expr {
25936 Expr::Column(cn) => Some(cn.name.clone()),
25937 Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
25938 Expr::Binary { lhs, rhs, .. } => {
25939 extract_first_column(lhs).or_else(|| extract_first_column(rhs))
25940 }
25941 Expr::Unary { expr: e, .. } => extract_first_column(e),
25942 // v7.39 (read01 round 93) — a cast wraps its operand: a common
25943 // expression-index key is `lower(col::text)`, where the column
25944 // sits under the `::text` cast inside the function arg. Without
25945 // descending here the key was rejected as "references no column".
25946 Expr::Cast { expr: e, .. } => extract_first_column(e),
25947 _ => None,
25948 }
25949}
25950
25951fn maybe_not(expr: Expr, negated: bool) -> Expr {
25952 if negated {
25953 Expr::Unary {
25954 op: UnOp::Not,
25955 expr: Box::new(expr),
25956 }
25957 } else {
25958 expr
25959 }
25960}
25961
25962/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
25963/// things in the two dialects, and SPG read all three PG's way:
25964///
25965/// | token | PG (and SPG) | MySQL, measured |
25966/// |---|---|---|
25967/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
25968/// | `&&` | inet / array overlap | **AND** |
25969/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
25970///
25971/// `1 || 0` answering the string '10' on a MySQL session is a wrong
25972/// answer with no error, which is why they are routed here rather than
25973/// left to the shared table.
25974impl Parser {
25975 fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
25976 if self.mysql_dialect {
25977 // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
25978 // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
25979 // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
25980 if let Token::Ident(w) = tok
25981 && w.eq_ignore_ascii_case("div")
25982 {
25983 return Some((BinOp::IntDiv, 8));
25984 }
25985 // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
25986 // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
25987 // the lexer; the `MOD(x, y)` function form is unaffected (MOD
25988 // there sits in operand position, not infix).
25989 if let Token::Ident(w) = tok
25990 && w.eq_ignore_ascii_case("mod")
25991 {
25992 return Some((BinOp::Mod, 8));
25993 }
25994 // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
25995 // plain ident to the lexer. Its precedence sits between OR (1)
25996 // and AND (3) — hence rung 2, the slot freed by moving AND up.
25997 if let Token::Ident(w) = tok
25998 && w.eq_ignore_ascii_case("xor")
25999 {
26000 return Some((BinOp::LogicalXor, 2));
26001 }
26002 match tok {
26003 Token::Concat => return Some((BinOp::Or, 1)),
26004 // MySQL's `&&` is logical AND, sharing AND's rung (3).
26005 Token::InetOverlap => return Some((BinOp::And, 3)),
26006 // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
26007 Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
26008 _ => {}
26009 }
26010 }
26011 binop_from(tok)
26012 }
26013}
26014
26015// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
26016// (which sits strictly between OR and AND), every level from AND upward was
26017// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
26018// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
26019// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
26020// the *relative* order of every PG operator is unchanged by the shift.
26021fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
26022 let pair = match tok {
26023 Token::Or => (BinOp::Or, 1),
26024 Token::And => (BinOp::And, 3),
26025 Token::Eq => (BinOp::Eq, 5),
26026 Token::NotEq => (BinOp::NotEq, 5),
26027 Token::Lt => (BinOp::Lt, 5),
26028 Token::LtEq => (BinOp::LtEq, 5),
26029 Token::Gt => (BinOp::Gt, 5),
26030 Token::GtEq => (BinOp::GtEq, 5),
26031 // pgvector distance ops all sit on the same rung — tighter than
26032 // comparisons (5) so `col <-> v < threshold` parses correctly.
26033 Token::L2Distance => (BinOp::L2Distance, 6),
26034 // v7.39 (read01 geo_ops.c) — geometric predicates ride the
26035 // comparison rung.
26036 Token::GeomParallel => (BinOp::GeomParallel, 5),
26037 // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
26038 // comparison rung.
26039 Token::OverLeft => (BinOp::OverLeft, 5),
26040 Token::OverRight => (BinOp::OverRight, 5),
26041 Token::GeomPerp => (BinOp::GeomPerp, 5),
26042 Token::GeomSameAs => (BinOp::GeomSameAs, 5),
26043 Token::ClosestPoint => (BinOp::ClosestPoint, 6),
26044 Token::GeomHoriz => (BinOp::GeomHoriz, 5),
26045 Token::InnerProduct => (BinOp::InnerProduct, 6),
26046 Token::CosineDistance => (BinOp::CosineDistance, 6),
26047 Token::Plus => (BinOp::Add, 7),
26048 Token::Minus => (BinOp::Sub, 7),
26049 // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
26050 // binds every "other" operator (`||`, `|`, `&`, `#`, the
26051 // pgvector distances above) BETWEEN additive (7) and the
26052 // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
26053 // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
26054 // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
26055 // ("matches PG conceptually" — the round-753 audit measured it
26056 // false; the old rung errored on `'a' || 1 + 1` with
26057 // `text + integer`). Same-level chains left-fold, as PG does.
26058 Token::Concat => (BinOp::Concat, 6),
26059 Token::Pipe => (BinOp::BitOr, 6),
26060 Token::Amp => (BinOp::BitAnd, 6),
26061 Token::Star => (BinOp::Mul, 8),
26062 Token::Slash => (BinOp::Div, 8),
26063 Token::Percent => (BinOp::Mod, 8),
26064 // v4.14: JSON path ops bind tighter than comparisons (5)
26065 // and additive (7) so `doc->'k' = 'v'` parses correctly.
26066 // Same rung as the multiplicative ops.
26067 Token::JsonGet => (BinOp::JsonGet, 8),
26068 Token::JsonGetText => (BinOp::JsonGetText, 8),
26069 Token::JsonGetPath => (BinOp::JsonGetPath, 8),
26070 Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
26071 Token::JsonContains => (BinOp::JsonContains, 8),
26072 Token::JsonPathExists => (BinOp::JsonPathExists, 8),
26073 Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
26074 Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
26075 Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
26076 Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
26077 Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
26078 // v7.12.2 — `@@` binds at the comparison rung (looser than
26079 // arithmetic, tighter than AND / OR). PG places `@@` at
26080 // the same precedence as `=` / `<`, so we follow.
26081 Token::TsMatch => (BinOp::TsMatch, 5),
26082 // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
26083 // PG places these at the comparison rung (same level as `=`),
26084 // so we follow.
26085 Token::InetContainedBy => (BinOp::InetContainedBy, 5),
26086 Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
26087 Token::InetContains => (BinOp::InetContains, 5),
26088 Token::InetContainsEq => (BinOp::InetContainsEq, 5),
26089 Token::InetOverlap => (BinOp::InetOverlap, 5),
26090 // v7.39 (round 508) — the geometric and pattern-order predicates
26091 // ride the comparison rung, as every other predicate does.
26092 Token::Intersects => (BinOp::Intersects, 5),
26093 Token::IsBelow => (BinOp::IsBelow, 5),
26094 Token::IsAbove => (BinOp::IsAbove, 5),
26095 Token::PatternLt => (BinOp::PatternLt, 5),
26096 Token::PatternLtEq => (BinOp::PatternLtEq, 5),
26097 Token::PatternGt => (BinOp::PatternGt, 5),
26098 Token::PatternGtEq => (BinOp::PatternGtEq, 5),
26099 // `@@@` is the old spelling of `@@` and means exactly it.
26100 Token::TsMatchOld => (BinOp::TsMatch, 5),
26101 _ => return None,
26102 };
26103 Some(pair)
26104}
26105
26106#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26107// `as f32` here is intentional: vector elements widen / narrow into f32 on
26108// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
26109// past ~15 decimal digits — both are acceptable for a fixed-precision
26110// pgvector column.
26111/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
26112/// implicit table alias and break trailing clauses. WITH lands
26113/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
26114/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
26115/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
26116/// / VALUES / FOR / LATERAL — all of which would otherwise be
26117/// silently swallowed by `parse_optional_alias`.
26118fn is_alias_stopword(s: &str) -> bool {
26119 matches!(
26120 s.to_ascii_lowercase().as_str(),
26121 "with"
26122 | "on"
26123 | "where"
26124 | "having"
26125 | "group"
26126 | "order"
26127 | "limit"
26128 | "offset"
26129 | "union"
26130 | "except"
26131 | "intersect"
26132 | "returning"
26133 | "set"
26134 | "values"
26135 | "for"
26136 | "window"
26137 | "tablesample"
26138 | "lateral"
26139 | "left"
26140 | "right"
26141 | "inner"
26142 | "outer"
26143 | "full"
26144 | "cross"
26145 | "join"
26146 | "natural"
26147 | "using"
26148 | "fetch"
26149 )
26150}
26151
26152fn extract_numeric_literal(e: &Expr) -> Option<f32> {
26153 match e {
26154 Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
26155 Expr::Literal(Literal::Float(x)) => Some(*x as f32),
26156 // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
26157 // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
26158 // so scale the divisor by hand instead of `f32::powi`.)
26159 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26160 let mut div = 1.0f32;
26161 for _ in 0..*scale {
26162 div *= 10.0;
26163 }
26164 Some(*unscaled as f32 / div)
26165 }
26166 Expr::Unary {
26167 op: UnOp::Neg,
26168 expr,
26169 } => extract_numeric_literal(expr).map(|x| -x),
26170 _ => None,
26171 }
26172}
26173
26174/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
26175/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
26176/// negative. Returns `None` if any pair fails to parse or no pair is found.
26177///
26178/// Recognised units (case-insensitive, optional trailing `s`):
26179/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
26180/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
26181/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
26182/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
26183/// (PG-canonical: DST and month-boundary semantics depend on this).
26184/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
26185/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
26186/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
26187#[allow(clippy::cast_possible_truncation)]
26188fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
26189 let mut months: i64 = 0;
26190 let mut days: i64 = 0;
26191 let mut micros: i64 = 0;
26192 let mut in_time = false;
26193 let mut num = alloc::string::String::new();
26194 for ch in rest.chars() {
26195 if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
26196 num.push(ch);
26197 continue;
26198 }
26199 if ch == 'T' || ch == 't' {
26200 if !num.is_empty() {
26201 return None;
26202 }
26203 in_time = true;
26204 continue;
26205 }
26206 let n: f64 = num.parse().ok()?;
26207 num.clear();
26208 match (ch, in_time) {
26209 ('Y' | 'y', false) => months += (n * 12.0) as i64,
26210 ('M', false) => months += n as i64,
26211 ('W' | 'w', false) => days += (n * 7.0) as i64,
26212 ('D' | 'd', false) => days += n as i64,
26213 ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26214 ('M', true) => micros += (n * 60_000_000.0) as i64,
26215 ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26216 _ => return None,
26217 }
26218 }
26219 if !num.is_empty() {
26220 return None;
26221 }
26222 Some((
26223 i32::try_from(months).ok()?,
26224 i32::try_from(days).ok()?,
26225 micros,
26226 ))
26227}
26228
26229/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
26230/// leading `-` negates the whole value). Rejects date-like strings.
26231fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
26232 let (neg, body) = match s.strip_prefix('-') {
26233 Some(b) => (true, b),
26234 None => (false, s),
26235 };
26236 let (y, m) = body.split_once('-')?;
26237 let years: i32 = y.parse().ok()?;
26238 let mons: i32 = m.parse().ok()?;
26239 if years < 0 || mons < 0 {
26240 return None;
26241 }
26242 let total = years.checked_mul(12)?.checked_add(mons)?;
26243 Some((if neg { -total } else { total }, 0, 0))
26244}
26245
26246/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
26247/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
26248fn parse_interval_clock(tok: &str) -> Option<i64> {
26249 let (neg, body) = match tok.strip_prefix('-') {
26250 Some(r) => (true, r),
26251 None => (false, tok.strip_prefix('+').unwrap_or(tok)),
26252 };
26253 let mut it = body.split(':');
26254 let h: i64 = it.next()?.parse().ok()?;
26255 let m: i64 = it.next()?.parse().ok()?;
26256 let s_tok = it.next().unwrap_or("0");
26257 if it.next().is_some() {
26258 return None;
26259 }
26260 let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
26261 let sec: i64 = sec.parse().ok()?;
26262 let mut f = alloc::string::String::from(frac);
26263 while f.len() < 6 {
26264 f.push('0');
26265 }
26266 f.truncate(6);
26267 let fus: i64 = f.parse().ok()?;
26268 sec.checked_mul(1_000_000)?.checked_add(fus)?
26269 } else {
26270 s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
26271 };
26272 let total = h
26273 .checked_mul(3_600_000_000)?
26274 .checked_add(m.checked_mul(60_000_000)?)?
26275 .checked_add(sec_us)?;
26276 Some(if neg { -total } else { total })
26277}
26278
26279/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
26280/// every spelling PG accepts (measured against live PG18.4, not guessed):
26281/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
26282/// Before this, the unit table matched long names only, with an ad-hoc
26283/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
26284/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
26285/// INTERVAL", and it had also grown arms for the debris that stripping leaves
26286/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
26287/// fractional) both read from this one table now.
26288fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
26289 let u = raw.to_ascii_lowercase();
26290 Some(match u.as_str() {
26291 "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
26292 "microsecond"
26293 }
26294 "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
26295 "millisecond"
26296 }
26297 "second" | "seconds" | "sec" | "secs" | "s" => "second",
26298 "minute" | "minutes" | "min" | "mins" | "m" => "minute",
26299 "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
26300 "day" | "days" | "d" => "day",
26301 "week" | "weeks" | "w" => "week",
26302 "month" | "months" | "mon" | "mons" => "month",
26303 "year" | "years" | "yr" | "yrs" | "y" => "year",
26304 "decade" | "decades" | "dec" | "decs" => "decade",
26305 "century" | "centuries" | "cent" | "c" => "century",
26306 "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
26307 _ => return None,
26308 })
26309}
26310
26311/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
26312/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
26313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26314pub(crate) enum IntervalField {
26315 Year,
26316 Month,
26317 Day,
26318 Hour,
26319 Minute,
26320 Second,
26321}
26322
26323/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
26324/// spellings aren't standard for the qualifier position, so only the singular
26325/// forms are accepted.
26326/// v7.39 (round 350, M7) — MySQL's interval units, measured against
26327/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
26328/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
26329/// take a `'1 2'` style literal — are not read here; they stay a parse
26330/// error rather than being silently misread.)
26331/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
26332///
26333/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
26334/// to do with a `@@` engine setting, and an unset one reads NULL rather
26335/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
26336/// were the same node and `SELECT @x` answered "Unknown system variable".)
26337/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
26338/// not see a session override — measured, after `SET autocommit=0`,
26339/// `@@global.autocommit` is still 1.
26340///
26341/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
26342/// the parser's nesting budget is tuned against, and building these
26343/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
26344/// wall `parse_left_right_atom` and friends were factored out for).
26345#[inline(never)]
26346fn variable_ref_atom(raw: &str) -> Expr {
26347 let user_var = !raw.starts_with("@@");
26348 let bare = raw.trim_start_matches('@').to_ascii_lowercase();
26349 Expr::FunctionCall {
26350 name: String::from(if user_var {
26351 "__spg_user_var"
26352 } else {
26353 "__spg_session_var"
26354 }),
26355 args: alloc::vec![Expr::Literal(Literal::String(bare))],
26356 }
26357}
26358
26359fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
26360 let Token::Ident(s) = tok else { return None };
26361 Some(match () {
26362 () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
26363 () if s.eq_ignore_ascii_case("second") => "second",
26364 () if s.eq_ignore_ascii_case("minute") => "minute",
26365 () if s.eq_ignore_ascii_case("hour") => "hour",
26366 () if s.eq_ignore_ascii_case("day") => "day",
26367 () if s.eq_ignore_ascii_case("week") => "week",
26368 () if s.eq_ignore_ascii_case("month") => "month",
26369 () if s.eq_ignore_ascii_case("quarter") => "quarter",
26370 () if s.eq_ignore_ascii_case("year") => "year",
26371 () => return None,
26372 })
26373}
26374
26375/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
26376/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
26377/// which constructs the value at run time. Only the slot the unit names
26378/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
26379/// slot the builtin has (months and fractional seconds respectively).
26380fn make_interval_call(qty: Expr, unit: &str) -> Expr {
26381 let zero = || Expr::Literal(Literal::Integer(0));
26382 let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
26383 lhs: alloc::boxed::Box::new(qty.clone()),
26384 op,
26385 rhs: alloc::boxed::Box::new(by),
26386 };
26387 // (years, months, weeks, days, hours, mins, secs)
26388 let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
26389 match unit {
26390 "year" => args[0] = qty,
26391 "quarter" => {
26392 args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
26393 }
26394 "month" => args[1] = qty,
26395 "week" => args[2] = qty,
26396 "day" => args[3] = qty,
26397 "hour" => args[4] = qty,
26398 "minute" => args[5] = qty,
26399 "second" => args[6] = qty,
26400 // The builtin's seconds slot takes a fraction, so microseconds ride
26401 // it scaled down; the divisor is a NUMERIC literal so the division
26402 // stays exact rather than going through a float.
26403 "microsecond" => {
26404 args[6] = scaled(
26405 crate::ast::BinOp::Div,
26406 Expr::Literal(Literal::Numeric {
26407 unscaled: 1_000_000,
26408 scale: 0,
26409 }),
26410 );
26411 }
26412 _ => args[3] = qty,
26413 }
26414 Expr::FunctionCall {
26415 name: alloc::string::String::from("make_interval"),
26416 args,
26417 }
26418}
26419
26420/// `(count, unit)` → `(months, days, micros)`.
26421fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
26422 let n: i64 = count.trim().parse().ok()?;
26423 Some(match unit {
26424 "microsecond" => (0, 0, n),
26425 "second" => (0, 0, n.checked_mul(1_000_000)?),
26426 "minute" => (0, 0, n.checked_mul(60_000_000)?),
26427 "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
26428 "day" => (0, i32::try_from(n).ok()?, 0),
26429 "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
26430 "month" => (i32::try_from(n).ok()?, 0, 0),
26431 "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
26432 "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
26433 _ => return None,
26434 })
26435}
26436
26437fn interval_field_of(tok: &Token) -> Option<IntervalField> {
26438 let Token::Ident(s) = tok else { return None };
26439 Some(match () {
26440 () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
26441 () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
26442 () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
26443 () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
26444 () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
26445 () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
26446 () => return None,
26447 })
26448}
26449
26450/// v7.39 (read01 round 102) — interpret an interval literal under a field
26451/// qualifier. Returns `(months, days, micros)`.
26452///
26453/// * A single field applied to a bare number sets which unit the number means,
26454/// truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
26455/// SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
26456/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
26457/// * Every other range, and any literal a single field can't read as a plain
26458/// number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
26459/// interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
26460/// like PG, and the qualifier there only bounds precision.
26461fn interpret_qualified_interval(
26462 text: &str,
26463 (f1, f2): (IntervalField, Option<IntervalField>),
26464) -> Option<(i32, i32, i64)> {
26465 if let Some(f2) = f2 {
26466 if f1 == IntervalField::Year && f2 == IntervalField::Month {
26467 if let Some(m) = parse_year_month_literal(text) {
26468 return Some((m, 0, 0));
26469 }
26470 }
26471 return parse_interval_text(text);
26472 }
26473 // Single field: reinterpret a bare number; otherwise the default parse.
26474 let trimmed = text.trim();
26475 if let Ok(val) = trimmed.parse::<f64>() {
26476 // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
26477 #[allow(clippy::cast_possible_truncation)]
26478 let whole = val as i64;
26479 #[allow(clippy::cast_possible_truncation)]
26480 let secs_micros = {
26481 let m = val * 1_000_000.0;
26482 if m >= 0.0 {
26483 (m + 0.5) as i64
26484 } else {
26485 (m - 0.5) as i64
26486 }
26487 };
26488 return Some(match f1 {
26489 IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
26490 IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
26491 IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
26492 IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
26493 IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
26494 IntervalField::Second => (0, 0, secs_micros),
26495 });
26496 }
26497 parse_interval_text(text)
26498}
26499
26500/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
26501fn parse_year_month_literal(text: &str) -> Option<i32> {
26502 let t = text.trim();
26503 let (neg, body) = match t.strip_prefix('-') {
26504 Some(r) => (true, r),
26505 None => (false, t.strip_prefix('+').unwrap_or(t)),
26506 };
26507 let mut it = body.split('-');
26508 let years: i32 = it.next()?.trim().parse().ok()?;
26509 let months: i32 = match it.next() {
26510 Some(m) => m.trim().parse().ok()?,
26511 None => 0,
26512 };
26513 if it.next().is_some() {
26514 return None;
26515 }
26516 let total = years.checked_mul(12)?.checked_add(months)?;
26517 Some(if neg { -total } else { total })
26518}
26519
26520pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
26521 // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
26522 // `@` is decorative; a trailing `ago` negates the whole interval.
26523 let mut trimmed = s.trim();
26524 trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
26525 let mut negate = false;
26526 if let Some(rest) = trimmed
26527 .strip_suffix("ago")
26528 .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
26529 {
26530 negate = true;
26531 trimmed = rest.trim();
26532 }
26533 let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
26534 let (mo, d, us) = v?;
26535 if negate {
26536 Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
26537 } else {
26538 Some((mo, d, us))
26539 }
26540 };
26541 let s = trimmed;
26542 // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
26543 // are single tokens, not the `<n> <unit>` pair form handled below.
26544 if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
26545 return finish(parse_iso8601_interval(rest));
26546 }
26547 if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
26548 if let Some(iv) = parse_year_month_interval(trimmed) {
26549 return finish(Some(iv));
26550 }
26551 }
26552 // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
26553 // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
26554 // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
26555 if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
26556 if let Ok(n) = trimmed.parse::<i64>() {
26557 return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
26558 }
26559 if let Ok(f) = trimmed.parse::<f64>() {
26560 if f.is_finite() {
26561 #[allow(clippy::cast_possible_truncation)]
26562 return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
26563 }
26564 }
26565 }
26566 // v7.39 (round 243) — PG accepts the number and unit run together
26567 // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
26568 // the `<n> <unit>` pair loop below sees them as two.
26569 let raw_parts: Vec<&str> = s.split_whitespace().collect();
26570 let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
26571 for p in raw_parts {
26572 let boundary = p
26573 .char_indices()
26574 .find(|(i, c)| {
26575 *i > 0
26576 && c.is_ascii_alphabetic()
26577 && p[..*i]
26578 .chars()
26579 .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
26580 && p[..*i].chars().any(|d| d.is_ascii_digit())
26581 })
26582 .map(|(i, _)| i);
26583 match boundary {
26584 Some(i) => {
26585 parts.push(&p[..i]);
26586 parts.push(&p[i..]);
26587 }
26588 None => parts.push(p),
26589 }
26590 }
26591 // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
26592 // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
26593 // remains is the `<n> <unit>` pair form handled below.
26594 let mut clock_us: i64 = 0;
26595 let mut had_clock = false;
26596 if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
26597 clock_us = parse_interval_clock(parts[pos])?;
26598 parts.remove(pos);
26599 had_clock = true;
26600 }
26601 // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
26602 // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
26603 let mut lone_days: i32 = 0;
26604 if had_clock && parts.len() == 1 {
26605 if let Ok(n) = parts[0].parse::<i64>() {
26606 lone_days = i32::try_from(n).ok()?;
26607 parts.clear();
26608 }
26609 }
26610 if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
26611 return None;
26612 }
26613 let mut months: i32 = 0;
26614 let mut days: i32 = lone_days;
26615 let mut micros: i64 = clock_us;
26616 let mut i = 0;
26617 while i < parts.len() {
26618 let unit_stripped = canonical_interval_unit(parts[i + 1])?;
26619 if let Ok(n) = parts[i].parse::<i64>() {
26620 match unit_stripped {
26621 "microsecond" => micros = micros.checked_add(n)?,
26622 "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
26623 "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
26624 "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
26625 "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
26626 "day" => {
26627 let n32 = i32::try_from(n).ok()?;
26628 days = days.checked_add(n32)?;
26629 }
26630 "week" => {
26631 let n32 = i32::try_from(n).ok()?;
26632 days = days.checked_add(n32.checked_mul(7)?)?;
26633 }
26634 "month" => {
26635 let n32 = i32::try_from(n).ok()?;
26636 months = months.checked_add(n32)?;
26637 }
26638 "year" => {
26639 let n32 = i32::try_from(n).ok()?;
26640 months = months.checked_add(n32.checked_mul(12)?)?;
26641 }
26642 // v7.39 (read01 timestamp.c) — the larger calendar units.
26643 "decade" => {
26644 let n32 = i32::try_from(n).ok()?;
26645 months = months.checked_add(n32.checked_mul(120)?)?;
26646 }
26647 "century" => {
26648 let n32 = i32::try_from(n).ok()?;
26649 months = months.checked_add(n32.checked_mul(1200)?)?;
26650 }
26651 "millennium" => {
26652 let n32 = i32::try_from(n).ok()?;
26653 months = months.checked_add(n32.checked_mul(12000)?)?;
26654 }
26655 _ => return None,
26656 }
26657 } else if let Ok(f) = parts[i].parse::<f64>() {
26658 // Fractional units cascade down to the next-finer field the way
26659 // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
26660 // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
26661 // no_std: f64 has no trunc/fract/round methods, so do them with
26662 // casts (toward-zero) + explicit round-half-away-from-zero.
26663 #[allow(clippy::cast_possible_truncation)]
26664 fn round_i64(x: f64) -> i64 {
26665 if x >= 0.0 {
26666 (x + 0.5) as i64
26667 } else {
26668 (x - 0.5) as i64
26669 }
26670 }
26671 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26672 fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
26673 const DAY_US: f64 = 86_400_000_000.0;
26674 let whole = d as i64; // truncates toward zero
26675 let frac = d - whole as f64;
26676 *days = days.checked_add(i32::try_from(whole).ok()?)?;
26677 *micros = micros.checked_add(round_i64(frac * DAY_US))?;
26678 Some(())
26679 }
26680 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26681 match unit_stripped {
26682 "microsecond" => micros = micros.checked_add(round_i64(f))?,
26683 "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
26684 "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
26685 "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
26686 "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
26687 "day" => add_days_frac(&mut days, &mut micros, f)?,
26688 "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
26689 "month" => {
26690 let whole = f as i64;
26691 months = months.checked_add(i32::try_from(whole).ok()?)?;
26692 add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
26693 }
26694 "year" => {
26695 let m = f * 12.0;
26696 let whole = m as i64;
26697 months = months.checked_add(i32::try_from(whole).ok()?)?;
26698 add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
26699 }
26700 _ => return None,
26701 }
26702 } else {
26703 return None;
26704 }
26705 i += 2;
26706 }
26707 finish(Some((months, days, micros)))
26708}
26709
26710/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
26711/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
26712/// `interval` is intentionally absent (handled by its own parser arm).
26713/// Returns `None` for names that aren't sensible as a bare typed literal, so
26714/// the caller falls back to treating the ident as a column reference.
26715fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
26716 Some(match ident {
26717 "date" => CastTarget::Date,
26718 "timestamp" | "datetime" => CastTarget::Timestamp,
26719 "timestamptz" => CastTarget::Timestamptz,
26720 "bool" | "boolean" => CastTarget::Bool,
26721 "int" | "integer" | "int4" => CastTarget::Int,
26722 "bigint" | "int8" => CastTarget::BigInt,
26723 "float8" | "double precision" => CastTarget::Float,
26724 "uuid" => CastTarget::Uuid,
26725 "bytea" => CastTarget::Bytea,
26726 "json" => CastTarget::Json,
26727 "jsonb" => CastTarget::Jsonb,
26728 // Types without a dedicated CastTarget variant flow through the
26729 // generic Named path (engine resolves via column_type_to_data_type).
26730 "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
26731 | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
26732 | "money" | "bit" | "varbit"
26733 // Geometric types accept the `TYPE 'literal'` prefix spelling too.
26734 | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
26735 // Range / multirange types likewise.
26736 | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
26737 | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
26738 | "datemultirange" | "tsmultirange" | "tstzmultirange"
26739 // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
26740 | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
26741 CastTarget::Named(alloc::string::String::from(ident))
26742 }
26743 _ => return None,
26744 })
26745}
26746
26747/// v7.12.4 — map a bare type-name identifier (the form that
26748/// appears in a function arg list or RETURNS clause) to a
26749/// [`ColumnTypeName`]. Returns `None` for unknown / extension
26750/// types so the caller can preserve them as
26751/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
26752///
26753/// Subset of the full column-type grammar — we deliberately
26754/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
26755/// here because function-arg types in v7.12.4 are mostly the
26756/// bare form (`text`, `int`, `bytea`, …).
26757/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
26758/// than being `name TYPE`?
26759///
26760/// The multi-word spellings SQL allows for a bare argument type, each
26761/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
26762///
26763/// NOTE this list also exists in `spg-storage`, which computes the
26764/// signature key from the rendered argument text and has to reach the
26765/// same verdict. The two crates are siblings — neither depends on the
26766/// other — and each already carries its own table of type spellings
26767/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
26768/// there), so this follows the structure rather than inventing new
26769/// duplication. Recorded as V49.
26770pub fn is_multiword_type_phrase(phrase: &str) -> bool {
26771 let t = phrase.trim().to_ascii_lowercase();
26772 let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
26773 matches!(
26774 base,
26775 "double precision"
26776 | "character varying"
26777 | "bit varying"
26778 | "timestamp with time zone"
26779 | "timestamp without time zone"
26780 | "time with time zone"
26781 | "time without time zone"
26782 | "national character"
26783 | "national character varying"
26784 )
26785}
26786
26787fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
26788 Some(match ident.to_ascii_lowercase().as_str() {
26789 "smallint" | "tinyint" => ColumnTypeName::SmallInt,
26790 "int" | "integer" | "mediumint" => ColumnTypeName::Int,
26791 "bigint" => ColumnTypeName::BigInt,
26792 "float" | "double" => ColumnTypeName::Float,
26793 // v7.39 (round 269) — real is 32-bit.
26794 "real" | "float4" => ColumnTypeName::Real,
26795 "text" => ColumnTypeName::Text,
26796 "bool" | "boolean" => ColumnTypeName::Bool,
26797 "date" => ColumnTypeName::Date,
26798 "timestamp" | "datetime" => ColumnTypeName::Timestamp,
26799 "timestamptz" => ColumnTypeName::Timestamptz,
26800 "json" => ColumnTypeName::Json,
26801 "jsonb" => ColumnTypeName::Jsonb,
26802 "bytea" | "bytes" => ColumnTypeName::Bytes,
26803 "tsvector" => ColumnTypeName::TsVector,
26804 "tsquery" => ColumnTypeName::TsQuery,
26805 "uuid" => ColumnTypeName::Uuid,
26806 "interval" => ColumnTypeName::Interval,
26807 "time" => ColumnTypeName::Time,
26808 "year" => ColumnTypeName::Year,
26809 "timetz" => ColumnTypeName::TimeTz,
26810 "money" => ColumnTypeName::Money,
26811 _ => return None,
26812 })
26813}
26814
26815/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
26816/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
26817///
26818/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
26819/// / embedded SQL land in v7.12.5+):
26820///
26821/// ```text
26822/// body := [ws] block [ws]
26823/// block := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
26824/// stmt := assign | return
26825/// assign := assign_target := expr
26826/// assign_target := ( NEW | OLD ) . ident | ident
26827/// return := RETURN ( NEW | OLD | NULL | expr )
26828/// ```
26829///
26830/// `expr` is parsed by recursing into the regular `Parser` — so a
26831/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
26832/// NEW.subject || ' ' || NEW.sender)` body shape works without
26833/// the body parser knowing what `to_tsvector` is.
26834///
26835/// Errors here cause the caller to fall back to
26836/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
26837/// successful, but the executor will refuse to invoke the
26838/// function with an "unparseable body" error.
26839/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
26840/// from the crate root as `spg_sql::parse_function_body`.
26841pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26842 parse_plpgsql_body(body)
26843}
26844
26845fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26846 // Use the regular lexer on the body text. The trailing
26847 // `END;` may or may not have a semicolon; the lexer treats
26848 // both forms identically.
26849 let tokens = lexer::tokenize(body).map_err(|e| ParseError {
26850 message: alloc::format!("plpgsql body lex error: {e}"),
26851 token_pos: 0,
26852 })?;
26853 let mut parser = Parser::new(tokens);
26854 parser.parse_plpgsql_block()
26855}
26856
26857/// v7.39 (GUC) — the textual body of a SET value, for list joining.
26858fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
26859 match v {
26860 crate::ast::SetValue::String(s)
26861 | crate::ast::SetValue::Ident(s)
26862 | crate::ast::SetValue::Number(s) => s.clone(),
26863 crate::ast::SetValue::Default => "DEFAULT".into(),
26864 }
26865}
26866
26867/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
26868/// contains an aggregate call at ITS OWN query level (recursion stops at
26869/// sublink boundaries — a sublink's aggregates belong to the sublink).
26870/// Backs the "aggregate functions are not allowed in a recursive query's
26871/// recursive term" well-formedness check.
26872fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
26873 const AGG_NAMES: &[&str] = &[
26874 "count",
26875 "sum",
26876 "min",
26877 "max",
26878 "avg",
26879 "string_agg",
26880 "array_agg",
26881 "bool_and",
26882 "bool_or",
26883 "every",
26884 "any_value",
26885 "json_agg",
26886 "jsonb_agg",
26887 "json_object_agg",
26888 "jsonb_object_agg",
26889 "bit_and",
26890 "bit_or",
26891 "bit_xor",
26892 "var_pop",
26893 "var_samp",
26894 "variance",
26895 "stddev",
26896 "stddev_pop",
26897 "stddev_samp",
26898 "range_agg",
26899 "range_intersect_agg",
26900 "percentile_cont",
26901 "percentile_disc",
26902 "mode",
26903 "corr",
26904 "covar_pop",
26905 "covar_samp",
26906 ];
26907 match e {
26908 Expr::AggregateOrdered { .. } => true,
26909 Expr::FunctionCall { name, args } => {
26910 AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
26911 || args.iter().any(expr_has_toplevel_aggregate)
26912 }
26913 Expr::NamedArg { expr, .. }
26914 | Expr::Variadic(expr)
26915 | Expr::Unary { expr, .. }
26916 | Expr::Cast { expr, .. }
26917 | Expr::IsNull { expr, .. }
26918 | Expr::FieldAccess { base: expr, .. }
26919 | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
26920 Expr::Binary { lhs, rhs, .. } => {
26921 expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
26922 }
26923 Expr::Like { expr, pattern, .. } => {
26924 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
26925 }
26926 Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
26927 Expr::InList { expr, list, .. } => {
26928 expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
26929 }
26930 Expr::ArraySubscript { target, index } => {
26931 expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
26932 }
26933 Expr::ArraySlice { target, lo, hi } => {
26934 expr_has_toplevel_aggregate(target)
26935 || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
26936 || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
26937 }
26938 Expr::AnyAll { expr, array, .. } => {
26939 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
26940 }
26941 Expr::Case {
26942 operand,
26943 branches,
26944 else_branch,
26945 } => {
26946 operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
26947 || branches
26948 .iter()
26949 .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
26950 || else_branch
26951 .as_deref()
26952 .is_some_and(expr_has_toplevel_aggregate)
26953 }
26954 // The outer-level operands of a sublink can aggregate; the sublink's
26955 // own body cannot leak its aggregates up here.
26956 Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
26957 Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
26958 row.iter().any(expr_has_toplevel_aggregate)
26959 }
26960 _ => false,
26961 }
26962}
26963
26964/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
26965/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
26966/// named table anywhere in its subtree. A plain FROM derived table is NOT a
26967/// sublink and is legal in a recursive term, so it is not walked here.
26968fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
26969 let mut exprs: Vec<&Expr> = Vec::new();
26970 for it in &s.items {
26971 if let crate::ast::SelectItem::Expr { expr, .. } = it {
26972 exprs.push(expr);
26973 }
26974 }
26975 if let Some(w) = &s.where_ {
26976 exprs.push(w);
26977 }
26978 if let Some(h) = &s.having {
26979 exprs.push(h);
26980 }
26981 if let Some(g) = &s.group_by {
26982 exprs.extend(g.iter());
26983 }
26984 if let Some(from) = &s.from {
26985 for j in &from.joins {
26986 if let Some(on) = &j.on {
26987 exprs.push(on);
26988 }
26989 }
26990 }
26991 exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
26992}
26993
26994/// Does this expression contain a sublink whose subquery mentions `name`?
26995fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
26996 match e {
26997 Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
26998 Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
26999 Expr::InSubquery { expr, subquery, .. } => {
27000 expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
27001 }
27002 Expr::RowInSubquery { row, subquery, .. } => {
27003 row.iter().any(|x| expr_sublink_mentions(x, name))
27004 || select_mentions_table(subquery, name)
27005 }
27006 Expr::RowCmpSubquery { row, subquery, .. } => {
27007 row.iter().any(|x| expr_sublink_mentions(x, name))
27008 || select_mentions_table(subquery, name)
27009 }
27010 Expr::NamedArg { expr, .. }
27011 | Expr::Variadic(expr)
27012 | Expr::Unary { expr, .. }
27013 | Expr::Cast { expr, .. }
27014 | Expr::IsNull { expr, .. }
27015 | Expr::FieldAccess { base: expr, .. }
27016 | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
27017 Expr::Binary { lhs, rhs, .. } => {
27018 expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
27019 }
27020 Expr::Like { expr, pattern, .. } => {
27021 expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
27022 }
27023 Expr::FunctionCall { args, .. } | Expr::Array(args) => {
27024 args.iter().any(|x| expr_sublink_mentions(x, name))
27025 }
27026 Expr::InList { expr, list, .. } => {
27027 expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
27028 }
27029 Expr::ArraySubscript { target, index } => {
27030 expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
27031 }
27032 Expr::ArraySlice { target, lo, hi } => {
27033 expr_sublink_mentions(target, name)
27034 || lo
27035 .as_deref()
27036 .is_some_and(|x| expr_sublink_mentions(x, name))
27037 || hi
27038 .as_deref()
27039 .is_some_and(|x| expr_sublink_mentions(x, name))
27040 }
27041 Expr::AnyAll { expr, array, .. } => {
27042 expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
27043 }
27044 Expr::Case {
27045 operand,
27046 branches,
27047 else_branch,
27048 } => {
27049 operand
27050 .as_deref()
27051 .is_some_and(|x| expr_sublink_mentions(x, name))
27052 || branches
27053 .iter()
27054 .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
27055 || else_branch
27056 .as_deref()
27057 .is_some_and(|x| expr_sublink_mentions(x, name))
27058 }
27059 _ => false,
27060 }
27061}
27062
27063/// Does this SELECT (in full — FROM tables, derived tables, its own
27064/// sublinks, and union arms) mention the named table?
27065fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
27066 if let Some(from) = &s.from {
27067 if from.primary.name.eq_ignore_ascii_case(name) {
27068 return true;
27069 }
27070 if let Some(sub) = &from.primary.lateral_subquery
27071 && select_mentions_table(sub, name)
27072 {
27073 return true;
27074 }
27075 for j in &from.joins {
27076 if j.table.name.eq_ignore_ascii_case(name) {
27077 return true;
27078 }
27079 if let Some(sub) = &j.table.lateral_subquery
27080 && select_mentions_table(sub, name)
27081 {
27082 return true;
27083 }
27084 }
27085 }
27086 if select_has_self_ref_in_sublink(s, name) {
27087 return true;
27088 }
27089 s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
27090}
27091
27092/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
27093/// row count, the way PG evaluates one before applying it.
27094///
27095/// `None` = not a constant (a column, a subquery, a function call).
27096/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
27097/// message stands in for LIMIT / OFFSET, which the caller substitutes.
27098/// All wordings were read off live PG 18.4.
27099fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
27100 use crate::ast::{BinOp, Expr, Literal, UnOp};
27101 match e {
27102 Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
27103 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27104 Some(Ok(round_scaled_half_away(*unscaled, *scale)))
27105 }
27106 // PG coerces a string by its CONTENT, and fails on the value.
27107 Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
27108 |_| {
27109 Err(alloc::format!(
27110 "invalid input syntax for type bigint: \"{t}\""
27111 ))
27112 },
27113 |n| Ok(i128::from(n)),
27114 )),
27115 Expr::Literal(Literal::Bool(_)) => Some(Err(
27116 "argument of {L} must be type bigint, not type boolean".into(),
27117 )),
27118 Expr::Unary {
27119 op: UnOp::Neg,
27120 expr,
27121 } => match fold_limit_constant(expr)? {
27122 Ok(v) => Some(Ok(-v)),
27123 e @ Err(_) => Some(e),
27124 },
27125 Expr::Binary { lhs, op, rhs } => {
27126 let a = match fold_limit_constant(lhs)? {
27127 Ok(v) => v,
27128 e @ Err(_) => return Some(e),
27129 };
27130 let b = match fold_limit_constant(rhs)? {
27131 Ok(v) => v,
27132 e @ Err(_) => return Some(e),
27133 };
27134 let out = match op {
27135 BinOp::Add => a.checked_add(b),
27136 BinOp::Sub => a.checked_sub(b),
27137 BinOp::Mul => a.checked_mul(b),
27138 BinOp::Div if b != 0 => a.checked_div(b),
27139 BinOp::Div => return Some(Err("division by zero".into())),
27140 BinOp::Mod if b != 0 => a.checked_rem(b),
27141 BinOp::Mod => return Some(Err("division by zero".into())),
27142 _ => return None,
27143 };
27144 // PG evaluates the arithmetic in the operand's own type, so an
27145 // int-by-int product that leaves int range fails there — before
27146 // the row count is ever looked at.
27147 match out {
27148 Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
27149 Some(Err("integer out of range".into()))
27150 }
27151 Some(v) => Some(Ok(v)),
27152 None => Some(Err("integer out of range".into())),
27153 }
27154 }
27155 _ => None,
27156 }
27157}
27158
27159/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
27160/// cast, which is what makes `LIMIT 2.5` keep three rows.
27161fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
27162 if scale == 0 {
27163 return unscaled;
27164 }
27165 let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
27166 return 0;
27167 };
27168 let neg = unscaled < 0;
27169 let mag = unscaled.unsigned_abs() as i128;
27170 let rounded = (mag + div / 2) / div;
27171 if neg { -rounded } else { rounded }
27172}
27173
27174#[cfg(test)]
27175mod tests {
27176 use super::*;
27177 use alloc::string::ToString;
27178
27179 fn parse(s: &str) -> Statement {
27180 parse_statement(s).expect("parse ok")
27181 }
27182
27183 // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
27184 // `tables`, `partition`, etc. are unreserved keywords per PG's
27185 // `pg_get_keywords()` and MUST be usable as column / table /
27186 // alias names. Pre-T4 every drop-in user whose schema had one
27187 // of these as a column name (sentori events.release, mailrs
27188 // messages.index in some forks) blew the parser up at CREATE
27189 // TABLE time with "expected identifier, got Release". The
27190 // generalisation lives in `unreserved_keyword_text` + the
27191 // `expect_ident_like` and `parse_atom` arms that consult it.
27192 #[test]
27193 fn release_usable_as_column_name_in_create_table() {
27194 let stmt =
27195 parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
27196 if let Statement::CreateTable(t) = stmt {
27197 let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
27198 assert_eq!(names, alloc::vec!["id", "release", "payload"]);
27199 } else {
27200 panic!("expected CreateTable");
27201 }
27202 }
27203
27204 #[test]
27205 fn release_usable_as_column_ref_in_select_projection() {
27206 // The sentori `0003_partition_events.sql` INSERT-SELECT
27207 // walk references `release` in both column lists; the
27208 // projection-side use exercises `parse_atom`'s relaxed
27209 // identifier set.
27210 parse("SELECT id, release, payload FROM events WHERE id = 1");
27211 }
27212
27213 #[test]
27214 fn release_usable_as_column_ref_in_insert_column_list() {
27215 // INSERT INTO t (id, release, payload) VALUES (…)
27216 parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
27217 }
27218
27219 #[test]
27220 fn alter_column_drop_not_null_uses_keyword_drop_token() {
27221 // Sentori `0013_audit_tombstone.sql` issues
27222 // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
27223 // emits Token::Drop (not Ident("drop")); the parser must
27224 // accept both in the ALTER COLUMN sub-dispatch.
27225 parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
27226 }
27227
27228 #[test]
27229 fn create_index_accepts_parenthesised_expression_key() {
27230 // sentori `0040_events_bundle_idx.sql` shape — JSONB
27231 // expression index. Pre-T4 the parser bailed at the
27232 // inner `(` with "expected column ident or expression,
27233 // got LParen". The Token::LParen arm in CREATE INDEX
27234 // routes through the expression parser instead.
27235 parse(
27236 "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
27237 ON events ((payload->'bundle'->>'id'))",
27238 );
27239 }
27240
27241 // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
27242 // surface as parse errors, never stack overflows (embed hosts
27243 // abort on overflow).
27244 /// The nesting budget is a COUNT; what it has to fit inside is a
27245 /// number of BYTES, and only one of those two is stable across
27246 /// compiler versions. Round 847 measured 30,336 bytes per level
27247 /// after a toolchain move, which puts 64 levels at 1.94 MB and
27248 /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
27249 /// aborted instead of erroring, which is precisely the outcome it
27250 /// exists to rule out.
27251 ///
27252 /// So the budget is metered rather than assumed. The ceiling leaves
27253 /// the depth SPG advertises fitting in a default 2 MiB thread with
27254 /// room to spare, in the debug build, where frames are widest.
27255 #[test]
27256 fn nesting_frame_cost_stays_under_ceiling() {
27257 // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
27258 // thread keeps a margin for whatever called the parser.
27259 const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
27260
27261 frame_meter::reset();
27262 let depth = frame_meter::SAMPLE_HI + 8;
27263 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27264 parse(&sql);
27265
27266 let per_level = frame_meter::bytes_per_level();
27267 {
27268 extern crate std;
27269 std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
27270 }
27271 assert!(
27272 per_level <= CEILING,
27273 "{per_level} bytes per nesting level exceeds {CEILING}; \
27274 {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
27275 in parse_expr_inner / parse_unary rather than lowering the \
27276 depth or widening the stack.",
27277 per_level * MAX_NEST_DEPTH
27278 );
27279 }
27280
27281 #[test]
27282 fn nesting_budget_errors_cleanly() {
27283 let depth = MAX_NEST_DEPTH + 50;
27284 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27285 let err = parse_statement(&sql).expect_err("must reject");
27286 assert!(err.message.contains("nests deeper"), "{err:?}");
27287 // Within budget still parses.
27288 let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
27289 parse(&sql);
27290 }
27291
27292 #[test]
27293 fn binary_chain_budget_errors_cleanly() {
27294 let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
27295 let err = parse_statement(&sql).expect_err("must reject");
27296 assert!(err.message.contains("chained binary"), "{err:?}");
27297 // Within budget still parses (chain depth ≤ budget is safe
27298 // for recursive eval/drop on 2 MiB stacks).
27299 let sql = format!("SELECT 1{}", " + 1".repeat(200));
27300 parse(&sql);
27301 }
27302
27303 #[test]
27304 fn in_list_unaffected_by_chain_budget() {
27305 // Flat InList: 20k elements parse fine and stay flat.
27306 let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
27307 let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
27308 let Statement::Select(s) = parse(&sql) else {
27309 panic!("expected select")
27310 };
27311 let Some(Expr::InList { list, negated, .. }) = s.where_ else {
27312 panic!("expected flat InList, got {:?}", s.where_)
27313 };
27314 assert_eq!(list.len(), 20_000);
27315 assert!(!negated);
27316 }
27317
27318 fn lit_int(n: i64) -> Expr {
27319 Expr::Literal(Literal::Integer(n))
27320 }
27321
27322 fn col(name: &str) -> Expr {
27323 Expr::Column(ColumnName {
27324 qualifier: None,
27325 name: name.into(),
27326 })
27327 }
27328
27329 #[test]
27330 fn select_single_integer() {
27331 let s = parse("SELECT 1");
27332 let Statement::Select(s) = s else {
27333 panic!("expected SELECT")
27334 };
27335 assert_eq!(s.items.len(), 1);
27336 assert!(s.from.is_none());
27337 assert!(s.where_.is_none());
27338 }
27339
27340 #[test]
27341 fn select_multiple_literal_kinds() {
27342 let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
27343 let Statement::Select(s) = s else {
27344 panic!("expected SELECT")
27345 };
27346 assert_eq!(s.items.len(), 5);
27347 }
27348
27349 #[test]
27350 fn select_wildcard_from_table() {
27351 let s = parse("SELECT * FROM users");
27352 let Statement::Select(s) = s else {
27353 panic!("expected SELECT")
27354 };
27355 assert!(matches!(s.items[..], [SelectItem::Wildcard]));
27356 assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
27357 }
27358
27359 #[test]
27360 fn select_with_table_alias() {
27361 let s = parse("SELECT * FROM users AS u");
27362 let Statement::Select(s) = s else {
27363 panic!("expected SELECT")
27364 };
27365 let t = &s.from.as_ref().unwrap().primary;
27366 assert_eq!(t.name, "users");
27367 assert_eq!(t.alias.as_deref(), Some("u"));
27368 }
27369
27370 #[test]
27371 fn select_with_where_eq() {
27372 let s = parse("SELECT a FROM t WHERE a = 1");
27373 let Statement::Select(s) = s else {
27374 panic!("expected SELECT")
27375 };
27376 let w = s.where_.unwrap();
27377 assert_eq!(
27378 w,
27379 Expr::Binary {
27380 lhs: Box::new(col("a")),
27381 op: BinOp::Eq,
27382 rhs: Box::new(lit_int(1)),
27383 }
27384 );
27385 }
27386
27387 #[test]
27388 fn arithmetic_precedence() {
27389 let s = parse("SELECT 1 + 2 * 3");
27390 let Statement::Select(s) = s else {
27391 panic!("expected SELECT")
27392 };
27393 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27394 panic!("wildcard?")
27395 };
27396 assert_eq!(
27397 expr,
27398 &Expr::Binary {
27399 lhs: Box::new(lit_int(1)),
27400 op: BinOp::Add,
27401 rhs: Box::new(Expr::Binary {
27402 lhs: Box::new(lit_int(2)),
27403 op: BinOp::Mul,
27404 rhs: Box::new(lit_int(3)),
27405 }),
27406 }
27407 );
27408 }
27409
27410 #[test]
27411 fn parentheses_override_precedence() {
27412 let s = parse("SELECT (1 + 2) * 3");
27413 let Statement::Select(s) = s else {
27414 panic!("expected SELECT")
27415 };
27416 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27417 panic!()
27418 };
27419 assert_eq!(
27420 expr,
27421 &Expr::Binary {
27422 lhs: Box::new(Expr::Binary {
27423 lhs: Box::new(lit_int(1)),
27424 op: BinOp::Add,
27425 rhs: Box::new(lit_int(2)),
27426 }),
27427 op: BinOp::Mul,
27428 rhs: Box::new(lit_int(3)),
27429 }
27430 );
27431 }
27432
27433 #[test]
27434 fn not_binds_below_comparison() {
27435 // `NOT a = 1` should parse as `NOT (a = 1)`.
27436 let s = parse("SELECT NOT a = 1 FROM t");
27437 let Statement::Select(s) = s else {
27438 panic!("expected SELECT")
27439 };
27440 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27441 panic!()
27442 };
27443 assert_eq!(
27444 expr,
27445 &Expr::Unary {
27446 op: UnOp::Not,
27447 expr: Box::new(Expr::Binary {
27448 lhs: Box::new(col("a")),
27449 op: BinOp::Eq,
27450 rhs: Box::new(lit_int(1)),
27451 }),
27452 }
27453 );
27454 }
27455
27456 #[test]
27457 fn unary_minus_binds_above_multiplication() {
27458 // `-a * 2` should be `(-a) * 2`.
27459 let s = parse("SELECT -a * 2 FROM t");
27460 let Statement::Select(s) = s else {
27461 panic!("expected SELECT")
27462 };
27463 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27464 panic!()
27465 };
27466 assert_eq!(
27467 expr,
27468 &Expr::Binary {
27469 lhs: Box::new(Expr::Unary {
27470 op: UnOp::Neg,
27471 expr: Box::new(col("a")),
27472 }),
27473 op: BinOp::Mul,
27474 rhs: Box::new(lit_int(2)),
27475 }
27476 );
27477 }
27478
27479 #[test]
27480 fn qualified_column() {
27481 let s = parse("SELECT t.col FROM t");
27482 let Statement::Select(s) = s else {
27483 panic!("expected SELECT")
27484 };
27485 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27486 panic!()
27487 };
27488 assert_eq!(
27489 expr,
27490 &Expr::Column(ColumnName {
27491 qualifier: Some("t".into()),
27492 name: "col".into()
27493 })
27494 );
27495 }
27496
27497 #[test]
27498 fn select_item_alias_with_as() {
27499 let s = parse("SELECT a AS y FROM t");
27500 let Statement::Select(s) = s else {
27501 panic!("expected SELECT")
27502 };
27503 let SelectItem::Expr { alias, .. } = &s.items[0] else {
27504 panic!()
27505 };
27506 assert_eq!(alias.as_deref(), Some("y"));
27507 }
27508
27509 #[test]
27510 fn trailing_semicolon_accepted() {
27511 let s = parse("SELECT 1;");
27512 let Statement::Select(s) = s else {
27513 panic!("expected SELECT")
27514 };
27515 assert_eq!(s.items.len(), 1);
27516 }
27517
27518 #[test]
27519 fn boolean_chain_with_and_or_not() {
27520 // (NOT a) OR (b AND (NOT c))
27521 let s = parse("SELECT NOT a OR b AND NOT c FROM t");
27522 let Statement::Select(s) = s else {
27523 panic!("expected SELECT")
27524 };
27525 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27526 panic!()
27527 };
27528 let expected = Expr::Binary {
27529 lhs: Box::new(Expr::Unary {
27530 op: UnOp::Not,
27531 expr: Box::new(col("a")),
27532 }),
27533 op: BinOp::Or,
27534 rhs: Box::new(Expr::Binary {
27535 lhs: Box::new(col("b")),
27536 op: BinOp::And,
27537 rhs: Box::new(Expr::Unary {
27538 op: UnOp::Not,
27539 expr: Box::new(col("c")),
27540 }),
27541 }),
27542 };
27543 assert_eq!(expr, &expected);
27544 }
27545
27546 #[test]
27547 fn empty_input_errors() {
27548 // v7.14.0 — pg_dump preambles emit several comment-only
27549 // / blank-line statements that collapse to Statement::
27550 // Empty rather than a parse error. The old "SELECT in
27551 // message" assertion is stale; verify the new contract:
27552 // empty / whitespace / comment-only input parses to
27553 // Statement::Empty.
27554 assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
27555 assert!(matches!(
27556 parse_statement(" \n\t ").unwrap(),
27557 Statement::Empty
27558 ));
27559 // Sanity: malformed-but-non-empty still errors.
27560 assert!(parse_statement("SELECT FROM WHERE").is_err());
27561 }
27562
27563 #[test]
27564 fn unmatched_paren_errors() {
27565 assert!(parse_statement("SELECT (1 + 2").is_err());
27566 }
27567
27568 #[test]
27569 fn display_round_trip_simple_select() {
27570 let original = parse("SELECT a + 1 FROM t WHERE a > 0");
27571 let text = original.to_string();
27572 let again = parse_statement(&text).expect("re-parse");
27573 assert_eq!(original, again);
27574 }
27575
27576 // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
27577
27578 #[test]
27579 fn create_table_single_column() {
27580 let s = parse("CREATE TABLE foo (a INT)");
27581 let Statement::CreateTable(c) = s else {
27582 panic!("expected CreateTable")
27583 };
27584 assert_eq!(c.name, "foo");
27585 assert_eq!(c.columns.len(), 1);
27586 assert_eq!(c.columns[0].name, "a");
27587 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27588 assert!(c.columns[0].nullable);
27589 }
27590
27591 #[test]
27592 fn create_table_multi_column_with_not_null_mix() {
27593 let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
27594 let Statement::CreateTable(c) = s else {
27595 panic!()
27596 };
27597 assert_eq!(c.columns.len(), 4);
27598 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27599 assert!(!c.columns[0].nullable);
27600 assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
27601 assert!(c.columns[1].nullable);
27602 assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
27603 assert!(!c.columns[2].nullable);
27604 assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
27605 }
27606
27607 #[test]
27608 fn create_table_bigint_supported() {
27609 let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
27610 let Statement::CreateTable(c) = s else {
27611 panic!()
27612 };
27613 assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
27614 }
27615
27616 #[test]
27617 fn create_table_vector_default_is_f32() {
27618 let s = parse("CREATE TABLE t (v VECTOR(128))");
27619 let Statement::CreateTable(c) = s else {
27620 panic!()
27621 };
27622 assert_eq!(
27623 c.columns[0].ty,
27624 ColumnTypeName::Vector {
27625 dim: 128,
27626 encoding: VecEncoding::F32,
27627 },
27628 );
27629 }
27630
27631 #[test]
27632 fn create_table_vector_using_sq8() {
27633 // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
27634 // Case-insensitive on both `USING` and the encoding name.
27635 for sql in [
27636 "CREATE TABLE t (v VECTOR(128) USING SQ8)",
27637 "CREATE TABLE t (v VECTOR(128) using sq8)",
27638 ] {
27639 let s = parse(sql);
27640 let Statement::CreateTable(c) = s else {
27641 panic!()
27642 };
27643 assert_eq!(
27644 c.columns[0].ty,
27645 ColumnTypeName::Vector {
27646 dim: 128,
27647 encoding: VecEncoding::Sq8,
27648 },
27649 "{sql}",
27650 );
27651 }
27652 }
27653
27654 #[test]
27655 fn create_table_vector_using_unknown_errors() {
27656 // v7.16.1 — the inline `USING <encoding>` shape on
27657 // CREATE TABLE column defs was withdrawn before
27658 // v7.14.0 in favour of `CREATE INDEX … USING hnsw
27659 // (col vector_<metric>_ops)`; the parser now rejects
27660 // USING at column-list position with a clearer
27661 // "expected ',' or ')'" message. Test asserts the
27662 // current rejection, not the old "unknown vector
27663 // encoding" string.
27664 let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
27665 assert!(
27666 err.message.contains("USING")
27667 || err.message.contains("using")
27668 || err.message.contains("')'")
27669 || err.message.contains("','"),
27670 "expected USING/column-list rejection, got: {}",
27671 err.message
27672 );
27673 }
27674
27675 #[test]
27676 fn vector_using_sq8_display_roundtrips() {
27677 // The Display impl must produce text that re-parses to the
27678 // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
27679 let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
27680 let Statement::CreateTable(c) = s else {
27681 panic!()
27682 };
27683 assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
27684 }
27685
27686 #[test]
27687 fn parser_recognises_placeholders() {
27688 use crate::ast::{Expr, SelectItem, Statement};
27689 // $N in expression position parses as Expr::Placeholder(N).
27690 let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
27691 let Statement::Select(sel) = s else { panic!() };
27692 assert!(matches!(
27693 sel.items[0],
27694 SelectItem::Expr {
27695 expr: Expr::Placeholder(1),
27696 alias: None
27697 }
27698 ));
27699 // $2 + 1
27700 let SelectItem::Expr {
27701 expr: Expr::Binary { lhs, rhs, .. },
27702 ..
27703 } = &sel.items[1]
27704 else {
27705 panic!()
27706 };
27707 assert!(matches!(**lhs, Expr::Placeholder(2)));
27708 assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
27709 // WHERE x = $3
27710 let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
27711 panic!()
27712 };
27713 assert!(matches!(**rhs, Expr::Placeholder(3)));
27714 }
27715
27716 #[test]
27717 fn parser_rejects_dollar_zero() {
27718 // $0 is not valid in PG; the lexer rejects it.
27719 assert!(parse_statement("SELECT $0").is_err());
27720 }
27721
27722 #[test]
27723 fn placeholder_display_roundtrips() {
27724 // The Display impl must produce text that re-lexes to the
27725 // same Placeholder token.
27726 let s = parse("SELECT $42 FROM t");
27727 let printed = s.to_string();
27728 assert!(printed.contains("$42"));
27729 let again = parse(&printed);
27730 assert_eq!(s, again);
27731 }
27732
27733 #[test]
27734 fn alter_index_rebuild_bare() {
27735 use crate::ast::{AlterIndexTarget, Statement};
27736 let s = parse("ALTER INDEX my_idx REBUILD");
27737 let Statement::AlterIndex(a) = s else {
27738 panic!("expected AlterIndex, got {s:?}")
27739 };
27740 assert_eq!(a.name, "my_idx");
27741 assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
27742 }
27743
27744 #[test]
27745 fn alter_index_rebuild_with_encoding() {
27746 use crate::ast::{AlterIndexTarget, Statement};
27747 for (sql, want) in [
27748 (
27749 "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
27750 VecEncoding::F32,
27751 ),
27752 (
27753 "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
27754 VecEncoding::Sq8,
27755 ),
27756 (
27757 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27758 VecEncoding::F16,
27759 ),
27760 ] {
27761 let s = parse(sql);
27762 let Statement::AlterIndex(a) = s else {
27763 panic!("{sql}: expected AlterIndex")
27764 };
27765 assert_eq!(a.name, "my_idx");
27766 assert_eq!(
27767 a.target,
27768 AlterIndexTarget::Rebuild {
27769 encoding: Some(want)
27770 },
27771 "{sql}"
27772 );
27773 }
27774 }
27775
27776 #[test]
27777 fn alter_index_rebuild_unknown_encoding_errors() {
27778 let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
27779 assert!(
27780 err.message.contains("unknown vector encoding"),
27781 "got: {}",
27782 err.message
27783 );
27784 }
27785
27786 #[test]
27787 fn alter_index_rebuild_display_roundtrips() {
27788 for (input, want) in [
27789 ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
27790 (
27791 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27792 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27793 ),
27794 (
27795 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27796 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27797 ),
27798 ] {
27799 let s = parse(input);
27800 assert_eq!(s.to_string(), want);
27801 }
27802 }
27803
27804 #[test]
27805 fn create_table_unknown_type_defers_to_engine() {
27806 // v4.9 picked XML as a parse-time "unsupported column
27807 // type" probe. v7.17.0 Phase 1.4 changed the contract:
27808 // an unknown type ident parses as Text + `user_type_ref`
27809 // so CREATE TABLE can resolve user-defined enum / domain
27810 // types — rejection of truly-unknown types moved to the
27811 // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
27812 // to a first-class built-in, so this probe switched to a
27813 // synthetic name nothing in the lexer will ever recognise.
27814 let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
27815 let Statement::CreateTable(t) = stmt else {
27816 panic!("expected CreateTable");
27817 };
27818 assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
27819 }
27820
27821 #[test]
27822 fn create_table_missing_table_keyword_errors() {
27823 assert!(parse_statement("CREATE x (a INT)").is_err());
27824 }
27825
27826 // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
27827 // `PARTITION OF parent <bounds>` child parse + Display round-trip.
27828
27829 #[test]
27830 fn parse_create_table_partition_by_range() {
27831 use crate::ast::{PartitionBySpec, PartitionKindAst};
27832 let stmt = parse_statement(
27833 "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
27834 payload JSONB) PARTITION BY RANGE (ts)",
27835 )
27836 .unwrap();
27837 let Statement::CreateTable(t) = stmt else {
27838 panic!("expected CreateTable");
27839 };
27840 assert!(t.partition_of.is_none(), "parent has no partition_of");
27841 assert_eq!(t.columns.len(), 3);
27842 let by = t.partition_by.as_ref().expect("expected PARTITION BY");
27843 assert_eq!(
27844 by,
27845 &PartitionBySpec {
27846 kind: PartitionKindAst::Range,
27847 key_columns: alloc::vec!["ts".to_string()],
27848 }
27849 );
27850 // Display round-trip preserves the suffix. `quote_ident`
27851 // only adds double quotes when the ident needs escaping, so
27852 // a plain `ts` survives bare here.
27853 assert!(
27854 t.to_string().contains("PARTITION BY RANGE (ts)"),
27855 "Display lost PARTITION BY suffix: {t}"
27856 );
27857 }
27858
27859 #[test]
27860 fn parse_create_table_partition_of_range() {
27861 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
27862 let stmt = parse_statement(
27863 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
27864 FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
27865 )
27866 .unwrap();
27867 let Statement::CreateTable(t) = stmt else {
27868 panic!("expected CreateTable");
27869 };
27870 assert!(t.columns.is_empty(), "child inherits columns from parent");
27871 assert!(t.partition_by.is_none());
27872 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27873 assert_eq!(of.parent_name, "events_partitioned");
27874 let PartitionOfSpec { bounds, .. } = of.clone();
27875 match bounds {
27876 PartitionOfBoundsAst::Range { lower, upper } => {
27877 assert!(lower.to_string().contains("2026-06-01"));
27878 assert!(upper.to_string().contains("2026-07-01"));
27879 }
27880 other => panic!("expected Range, got {other:?}"),
27881 }
27882 // Display round-trip emits the FOR VALUES tail. `quote_ident`
27883 // skips quotes when not required, so the parent name appears
27884 // bare here.
27885 let s = t.to_string();
27886 assert!(
27887 s.contains("PARTITION OF events_partitioned"),
27888 "Display lost PARTITION OF: {s}"
27889 );
27890 assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
27891 assert!(s.contains(") TO ("), "Display lost TO: {s}");
27892 }
27893
27894 #[test]
27895 fn parse_create_table_partition_of_default() {
27896 use crate::ast::PartitionOfBoundsAst;
27897 let stmt =
27898 parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
27899 .unwrap();
27900 let Statement::CreateTable(t) = stmt else {
27901 panic!("expected CreateTable");
27902 };
27903 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27904 assert_eq!(of.parent_name, "events_partitioned");
27905 assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
27906 assert!(
27907 t.to_string()
27908 .contains("PARTITION OF events_partitioned DEFAULT"),
27909 "Display lost DEFAULT: {t}"
27910 );
27911 }
27912
27913 #[test]
27914 fn parse_create_table_partition_by_list() {
27915 // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
27916 // child with `FOR VALUES IN (lit, lit, …)`.
27917 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27918 let parent =
27919 parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
27920 .unwrap();
27921 let Statement::CreateTable(t) = parent else {
27922 panic!("expected CreateTable");
27923 };
27924 let Some(PartitionBySpec {
27925 kind,
27926 ref key_columns,
27927 }) = t.partition_by
27928 else {
27929 panic!("expected PARTITION BY");
27930 };
27931 assert_eq!(kind, PartitionKindAst::List);
27932 assert_eq!(*key_columns, vec!["region".to_string()]);
27933 assert!(t.to_string().contains("PARTITION BY LIST (region)"));
27934
27935 let child = parse_statement(
27936 "CREATE TABLE events_apac PARTITION OF events_listed \
27937 FOR VALUES IN ('jp', 'kr', 'tw')",
27938 )
27939 .unwrap();
27940 let Statement::CreateTable(c) = child else {
27941 panic!("expected CreateTable");
27942 };
27943 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27944 let PartitionOfBoundsAst::List { values } = &of.bounds else {
27945 panic!("expected List bounds, got {:?}", of.bounds);
27946 };
27947 assert_eq!(values.len(), 3);
27948 let disp = c.to_string();
27949 assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
27950 }
27951
27952 #[test]
27953 fn parse_create_table_partition_by_hash() {
27954 // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
27955 // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
27956 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27957 let parent =
27958 parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
27959 let Statement::CreateTable(t) = parent else {
27960 panic!("expected CreateTable");
27961 };
27962 let Some(PartitionBySpec {
27963 kind,
27964 ref key_columns,
27965 }) = t.partition_by
27966 else {
27967 panic!("expected PARTITION BY");
27968 };
27969 assert_eq!(kind, PartitionKindAst::Hash);
27970 assert_eq!(*key_columns, vec!["id".to_string()]);
27971 assert!(t.to_string().contains("PARTITION BY HASH (id)"));
27972
27973 let child = parse_statement(
27974 "CREATE TABLE orders_h_0 PARTITION OF orders_h \
27975 FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
27976 )
27977 .unwrap();
27978 let Statement::CreateTable(c) = child else {
27979 panic!("expected CreateTable");
27980 };
27981 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27982 let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
27983 panic!("expected Hash bounds");
27984 };
27985 assert_eq!(modulus, 4);
27986 assert_eq!(remainder, 0);
27987 let disp = c.to_string();
27988 assert!(
27989 disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
27990 "Display lost HASH bounds: {disp}"
27991 );
27992
27993 // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
27994 let bad = parse_statement(
27995 "CREATE TABLE orders_h_bad PARTITION OF orders_h \
27996 FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
27997 );
27998 let msg = format!("{}", bad.unwrap_err());
27999 assert!(
28000 msg.contains("REMAINDER") && msg.contains("MODULUS"),
28001 "expected REMAINDER/MODULUS validation error: {msg}"
28002 );
28003 }
28004
28005 #[test]
28006 fn parse_create_table_partition_of_rejects_columns() {
28007 // v7.37.6-B contract: PARTITION OF children inherit columns
28008 // from the parent; an explicit list MUST surface as a parse
28009 // error rather than getting silently ignored.
28010 let err = parse_statement(
28011 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
28012 FOR VALUES FROM ('a') TO ('b')",
28013 );
28014 assert!(err.is_err(), "expected parse error for explicit columns");
28015 let msg = format!("{}", err.unwrap_err());
28016 assert!(
28017 msg.contains("PARTITION OF") && msg.contains("column"),
28018 "error should mention PARTITION OF + columns: {msg}"
28019 );
28020 }
28021
28022 #[test]
28023 fn insert_single_value() {
28024 let s = parse("INSERT INTO foo VALUES (42)");
28025 let Statement::Insert(i) = s else {
28026 panic!("expected Insert")
28027 };
28028 assert_eq!(i.table, "foo");
28029 assert_eq!(i.rows.len(), 1);
28030 assert_eq!(i.rows[0].len(), 1);
28031 assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
28032 }
28033
28034 #[test]
28035 fn insert_multi_value_with_mixed_literals() {
28036 let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
28037 let Statement::Insert(i) = s else { panic!() };
28038 assert_eq!(i.rows.len(), 1);
28039 assert_eq!(i.rows[0].len(), 5);
28040 }
28041
28042 #[test]
28043 fn insert_missing_into_errors() {
28044 assert!(parse_statement("INSERT foo VALUES (1)").is_err());
28045 }
28046
28047 #[test]
28048 fn create_table_round_trip() {
28049 let original =
28050 parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
28051 let text = original.to_string();
28052 let again = parse_statement(&text).expect("re-parse");
28053 assert_eq!(original, again);
28054 }
28055
28056 #[test]
28057 fn insert_round_trip_with_negation_and_string() {
28058 let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
28059 let text = original.to_string();
28060 let again = parse_statement(&text).expect("re-parse");
28061 assert_eq!(original, again);
28062 }
28063
28064 #[test]
28065 fn unknown_keyword_at_statement_start_errors() {
28066 // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
28067 // the top-level dispatch still has no branch to take.
28068 let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
28069 assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
28070 }
28071
28072 // --- v0.8 CREATE INDEX --------------------------------------------------
28073
28074 #[test]
28075 fn create_index_basic() {
28076 let s = parse("CREATE INDEX idx_id ON users (id)");
28077 let Statement::CreateIndex(c) = s else {
28078 panic!("expected CreateIndex")
28079 };
28080 assert_eq!(c.name, "idx_id");
28081 assert_eq!(c.table, "users");
28082 assert_eq!(c.column, "id");
28083 }
28084
28085 #[test]
28086 fn create_index_missing_on_errors() {
28087 assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
28088 }
28089
28090 #[test]
28091 fn create_index_missing_paren_errors() {
28092 assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
28093 }
28094
28095 #[test]
28096 fn create_index_round_trip() {
28097 let original = parse("CREATE INDEX by_name ON users (name)");
28098 let again = parse_statement(&original.to_string()).unwrap();
28099 assert_eq!(original, again);
28100 }
28101
28102 // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
28103
28104 #[test]
28105 fn create_unique_index_basic() {
28106 let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
28107 let Statement::CreateIndex(c) = s else {
28108 panic!("expected CreateIndex");
28109 };
28110 assert!(c.is_unique);
28111 assert_eq!(c.column, "a");
28112 assert!(c.partial_predicate.is_none());
28113 }
28114
28115 #[test]
28116 fn create_unique_index_partial() {
28117 // mailrs's email_templates "one default per user" shape.
28118 let s = parse(
28119 "CREATE UNIQUE INDEX idx_email_templates_user_default \
28120 ON email_templates (user_address) WHERE is_default = true",
28121 );
28122 let Statement::CreateIndex(c) = s else {
28123 panic!("expected CreateIndex");
28124 };
28125 assert!(c.is_unique);
28126 assert_eq!(c.table, "email_templates");
28127 assert_eq!(c.column, "user_address");
28128 assert!(c.partial_predicate.is_some());
28129 }
28130
28131 #[test]
28132 fn create_unique_index_composite_with_predicate() {
28133 // mailrs's calendar_events instance: composite columns.
28134 let s = parse(
28135 "CREATE UNIQUE INDEX uq_calendar_events_instance \
28136 ON calendar_events (calendar_id, uid, recurrence_id) \
28137 WHERE recurrence_id IS NOT NULL",
28138 );
28139 let Statement::CreateIndex(c) = s else {
28140 panic!("expected CreateIndex");
28141 };
28142 assert!(c.is_unique);
28143 assert_eq!(c.column, "calendar_id");
28144 assert_eq!(
28145 c.extra_columns,
28146 vec!["uid".to_string(), "recurrence_id".to_string()]
28147 );
28148 assert!(c.partial_predicate.is_some());
28149 }
28150
28151 #[test]
28152 fn create_unique_index_using_btree_ok() {
28153 let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
28154 assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
28155 }
28156
28157 #[test]
28158 fn create_unique_index_using_hnsw_rejected() {
28159 let err =
28160 parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
28161 assert!(err.message.contains("UNIQUE"), "{}", err.message);
28162 }
28163
28164 #[test]
28165 fn create_unique_index_round_trip() {
28166 let original = parse(
28167 "CREATE UNIQUE INDEX uq_calendar_events_master \
28168 ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
28169 );
28170 let again = parse_statement(&original.to_string()).unwrap();
28171 assert_eq!(original, again);
28172 }
28173
28174 #[test]
28175 fn create_unique_without_index_errors() {
28176 let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
28177 // v7.39 (round 340, V56) — PG 18.4, verbatim.
28178 assert_eq!(err.message, "syntax error at or near \"TABLE\"");
28179 }
28180
28181 // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
28182
28183 #[test]
28184 fn create_table_bytea_column() {
28185 let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
28186 let Statement::CreateTable(c) = s else {
28187 panic!("expected CreateTable");
28188 };
28189 assert_eq!(c.columns.len(), 2);
28190 assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
28191 assert!(!c.columns[1].nullable);
28192 }
28193
28194 #[test]
28195 fn create_table_bytes_alias_column() {
28196 let s = parse("CREATE TABLE t (blob BYTES)");
28197 let Statement::CreateTable(c) = s else {
28198 panic!("expected CreateTable");
28199 };
28200 assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
28201 }
28202
28203 #[test]
28204 fn bytea_round_trip_display() {
28205 let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
28206 let again = parse_statement(&original.to_string()).unwrap();
28207 assert_eq!(original, again);
28208 }
28209
28210 // --- v0.9 transactions -------------------------------------------------
28211
28212 #[test]
28213 fn begin_commit_rollback_parse_as_unit_variants() {
28214 assert_eq!(parse("BEGIN"), Statement::Begin(None));
28215 assert_eq!(parse("COMMIT"), Statement::Commit);
28216 // r1066 — PG synonyms pgbench's tpcb script relies on.
28217 assert_eq!(parse("END"), Statement::Commit);
28218 assert_eq!(parse("END TRANSACTION"), Statement::Commit);
28219 assert_eq!(parse("COMMIT WORK"), Statement::Commit);
28220 assert_eq!(parse("ROLLBACK"), Statement::Rollback);
28221 // Trailing semicolons accepted too.
28222 assert_eq!(parse("BEGIN;"), Statement::Begin(None));
28223 // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
28224 // statement (with or without the WORK/TRANSACTION noise word).
28225 assert_eq!(
28226 parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
28227 Statement::Begin(Some(IsolationLevel::RepeatableRead))
28228 );
28229 assert_eq!(
28230 parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
28231 Statement::Begin(Some(IsolationLevel::Serializable))
28232 );
28233 // A non-isolation mode keeps the session default (None).
28234 assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
28235 }
28236
28237 // --- v1.2: pgvector distance ops + ::vector cast --------------------
28238
28239 #[test]
28240 fn inner_product_binop_parses() {
28241 let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
28242 let Statement::Select(s) = s else { panic!() };
28243 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28244 panic!()
28245 };
28246 assert!(matches!(
28247 expr,
28248 Expr::Binary {
28249 op: BinOp::InnerProduct,
28250 ..
28251 }
28252 ));
28253 }
28254
28255 #[test]
28256 fn cosine_distance_binop_parses() {
28257 let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
28258 let Statement::Select(s) = s else { panic!() };
28259 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28260 panic!()
28261 };
28262 assert!(matches!(
28263 expr,
28264 Expr::Binary {
28265 op: BinOp::CosineDistance,
28266 ..
28267 }
28268 ));
28269 }
28270
28271 #[test]
28272 fn vector_cast_postfix_wraps_string_literal() {
28273 let s = parse("SELECT '[1,2,3]'::vector FROM t");
28274 let Statement::Select(s) = s else { panic!() };
28275 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28276 panic!()
28277 };
28278 assert!(matches!(
28279 expr,
28280 Expr::Cast {
28281 target: CastTarget::Vector,
28282 ..
28283 }
28284 ));
28285 }
28286
28287 #[test]
28288 fn unsupported_cast_target_errors() {
28289 // v7.37.5 ship triage promoted the parser to accept every
28290 // ident as a `CastTarget::Named(canonical)`; the engine
28291 // surfaces the "unsupported cast target" error at eval
28292 // time when `type_name_to_data_type` can't resolve it.
28293 // Parser-side error now requires a NON-ident after `::`
28294 // (e.g. a punctuation token).
28295 let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
28296 assert_eq!(err.message, "syntax error at or near \",\"");
28297 }
28298
28299 #[test]
28300 fn tx_statements_round_trip() {
28301 for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
28302 let original = parse(q);
28303 let again = parse_statement(&original.to_string()).unwrap();
28304 assert_eq!(original, again);
28305 }
28306 }
28307
28308 #[test]
28309 fn interval_text_parsing_units() {
28310 // v7.37.5 β — three-field shape `(months, days, micros)` so
28311 // `'1 day'` and `'24 hours'` no longer collide (PG parity).
28312 // Single unit.
28313 assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
28314 assert_eq!(
28315 parse_interval_text("24 hours"),
28316 Some((0, 0, 86_400_000_000))
28317 );
28318 assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
28319 assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
28320 assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
28321 assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
28322 // Compound spans accumulate per-dimension.
28323 assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
28324 assert_eq!(
28325 parse_interval_text("1 day 2 hours"),
28326 Some((0, 1, 7_200_000_000))
28327 );
28328 // Negative numbers carry through per-dimension.
28329 assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
28330 // Bad shapes return None.
28331 assert_eq!(parse_interval_text(""), None);
28332 assert_eq!(parse_interval_text("garbage"), None);
28333 assert_eq!(parse_interval_text("1 fortnight"), None);
28334 // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
28335 // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
28336 assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
28337 assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
28338 assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
28339 }
28340
28341 #[test]
28342 fn interval_literal_roundtrips_via_display() {
28343 let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
28344 let s = parsed.to_string();
28345 // Display preserves the original text verbatim.
28346 assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
28347 // And re-parsing yields a structurally equal statement.
28348 let again = parse_statement(&s).unwrap();
28349 assert_eq!(parsed, again);
28350 }
28351
28352 // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
28353
28354 #[test]
28355 fn parser_recognises_create_publication_bare() {
28356 let s = parse("CREATE PUBLICATION pub_a");
28357 let Statement::CreatePublication(p) = s else {
28358 panic!("expected CreatePublication, got {s:?}")
28359 };
28360 assert_eq!(p.name, "pub_a");
28361 assert_eq!(p.scope, PublicationScope::AllTables);
28362 }
28363
28364 #[test]
28365 fn parser_recognises_create_publication_for_all_tables() {
28366 let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
28367 let Statement::CreatePublication(p) = s else {
28368 panic!("expected CreatePublication, got {s:?}")
28369 };
28370 assert_eq!(p.name, "pub_a");
28371 assert_eq!(p.scope, PublicationScope::AllTables);
28372 }
28373
28374 #[test]
28375 fn parser_recognises_drop_publication() {
28376 let s = parse("DROP PUBLICATION pub_a");
28377 let Statement::DropPublication { name, .. } = s else {
28378 panic!("expected DropPublication, got {s:?}")
28379 };
28380 assert_eq!(name, "pub_a");
28381 }
28382
28383 #[test]
28384 fn parser_recognises_for_table_list() {
28385 let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
28386 let Statement::CreatePublication(p) = s else {
28387 panic!("expected CreatePublication, got {s:?}")
28388 };
28389 assert_eq!(p.name, "pub_a");
28390 let PublicationScope::ForTables(ts) = p.scope else {
28391 panic!("expected ForTables scope")
28392 };
28393 assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
28394 }
28395
28396 #[test]
28397 fn parser_rejects_bare_for_tables_and_takes_in_schema() {
28398 // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
28399 // is rejected (`invalid publication object list`; the old
28400 // test pinned an unverifiable "PG 19 accepts both" claim);
28401 // TABLES pairs with IN SCHEMA.
28402 let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
28403 .expect_err("bare FOR TABLES must reject");
28404 assert!(
28405 alloc::format!("{err}").contains("invalid publication object list"),
28406 "got: {err}"
28407 );
28408 let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
28409 let Statement::CreatePublication(p) = s else {
28410 panic!("expected CreatePublication, got {s:?}")
28411 };
28412 let PublicationScope::TablesInSchema(schema) = p.scope else {
28413 panic!("expected TablesInSchema")
28414 };
28415 assert_eq!(schema, "public");
28416 }
28417
28418 #[test]
28419 fn parser_recognises_for_all_tables_except_list() {
28420 let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
28421 let Statement::CreatePublication(p) = s else {
28422 panic!()
28423 };
28424 let PublicationScope::AllTablesExcept(ts) = p.scope else {
28425 panic!("expected AllTablesExcept")
28426 };
28427 assert_eq!(ts, alloc::vec!["t1", "t2"]);
28428 }
28429
28430 #[test]
28431 fn parser_rejects_for_table_with_empty_list() {
28432 // `FOR TABLE` with nothing after is a parse error.
28433 let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
28434 .expect_err("must error on empty list");
28435 // No specific message asserted — the call falls through to
28436 // expect_ident_like which yields "expected identifier, got …".
28437 assert!(!err.message.is_empty());
28438 }
28439
28440 #[test]
28441 fn parser_recognises_show_publications() {
28442 // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
28443 // bare ident in this position, NOT a reserved keyword.
28444 let s = parse("SHOW PUBLICATIONS");
28445 assert!(matches!(s, Statement::ShowPublications));
28446 }
28447
28448 // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
28449
28450 #[test]
28451 fn parser_recognises_create_subscription_single_publication() {
28452 let s = parse(
28453 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
28454 );
28455 let Statement::CreateSubscription(c) = s else {
28456 panic!("expected CreateSubscription, got {s:?}")
28457 };
28458 assert_eq!(c.name, "sub_a");
28459 assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
28460 assert_eq!(c.publications, alloc::vec!["pub_a"]);
28461 }
28462
28463 #[test]
28464 fn parser_recognises_create_subscription_multi_publication() {
28465 let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
28466 let Statement::CreateSubscription(c) = s else {
28467 panic!()
28468 };
28469 assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
28470 }
28471
28472 #[test]
28473 fn parser_rejects_create_subscription_missing_connection() {
28474 let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
28475 .expect_err("must error on missing CONNECTION");
28476 assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
28477 }
28478
28479 #[test]
28480 fn parser_rejects_create_subscription_missing_publication() {
28481 let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
28482 .expect_err("must error on missing PUBLICATION");
28483 assert_eq!(err.message, "syntax error at end of input");
28484 }
28485
28486 #[test]
28487 fn parser_recognises_drop_subscription() {
28488 let s = parse("DROP SUBSCRIPTION sub_a");
28489 let Statement::DropSubscription { name, .. } = s else {
28490 panic!("expected DropSubscription, got {s:?}")
28491 };
28492 assert_eq!(name, "sub_a");
28493 }
28494
28495 #[test]
28496 fn parser_recognises_show_subscriptions() {
28497 let s = parse("SHOW SUBSCRIPTIONS");
28498 assert!(matches!(s, Statement::ShowSubscriptions));
28499 }
28500
28501 #[test]
28502 fn parser_recognises_wait_for_wal_position_no_timeout() {
28503 let s = parse("WAIT FOR WAL POSITION 12345");
28504 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28505 panic!("expected WaitForWalPosition, got {s:?}")
28506 };
28507 assert_eq!(pos, 12345);
28508 assert!(timeout_ms.is_none());
28509 }
28510
28511 #[test]
28512 fn parser_recognises_wait_for_wal_position_with_timeout() {
28513 let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
28514 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28515 panic!()
28516 };
28517 assert_eq!(pos, 67890);
28518 assert_eq!(timeout_ms, Some(5000));
28519 }
28520
28521 #[test]
28522 fn parser_rejects_wait_with_negative_position() {
28523 // The lexer treats `-` as a token; `expect_u64_literal`
28524 // only sees the Integer that follows, so the negative
28525 // arrives as a unary-minus expression at higher levels.
28526 // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
28527 // parse error one way or another.
28528 let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
28529 assert!(!err.message.is_empty());
28530 }
28531
28532 #[test]
28533 fn parser_recognises_bare_analyze() {
28534 let s = parse("ANALYZE");
28535 assert!(matches!(s, Statement::Analyze(None)));
28536 }
28537
28538 #[test]
28539 fn parser_recognises_analyze_with_table() {
28540 let s = parse("ANALYZE users");
28541 let Statement::Analyze(Some(name)) = s else {
28542 panic!("expected Analyze, got {s:?}")
28543 };
28544 assert_eq!(name, "users");
28545 }
28546
28547 #[test]
28548 fn parser_recognises_analyze_with_quoted_table() {
28549 let s = parse("ANALYZE \"Mixed Case\"");
28550 let Statement::Analyze(Some(name)) = s else {
28551 panic!()
28552 };
28553 assert_eq!(name, "Mixed Case");
28554 }
28555
28556 #[test]
28557 fn parser_rejects_analyze_with_garbage_token() {
28558 let err = parse_statement("ANALYZE 42").expect_err("must error");
28559 assert!(!err.message.is_empty());
28560 }
28561
28562 #[test]
28563 fn analyze_display_roundtrips() {
28564 for sql in ["ANALYZE", "ANALYZE users"] {
28565 let s = parse(sql);
28566 let printed = s.to_string();
28567 let again = parse_statement(&printed)
28568 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28569 assert_eq!(s, again);
28570 }
28571 }
28572
28573 #[test]
28574 fn wait_for_display_roundtrips() {
28575 for sql in [
28576 "WAIT FOR WAL POSITION 12345",
28577 "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
28578 ] {
28579 let s = parse(sql);
28580 let printed = s.to_string();
28581 let again = parse_statement(&printed)
28582 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28583 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28584 }
28585 }
28586
28587 #[test]
28588 fn subscription_ddl_display_roundtrips() {
28589 for sql in [
28590 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
28591 "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
28592 "DROP SUBSCRIPTION sub_a",
28593 "SHOW SUBSCRIPTIONS",
28594 ] {
28595 let s = parse(sql);
28596 let printed = s.to_string();
28597 let again = parse_statement(&printed)
28598 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28599 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28600 }
28601 }
28602
28603 #[test]
28604 fn parser_drop_dispatches_user_vs_publication() {
28605 // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
28606 // tokenises DROP. Both targets must still parse.
28607 let s = parse("DROP USER 'alice'");
28608 let Statement::DropUser { name, .. } = s else {
28609 panic!("expected DropUser, got {s:?}")
28610 };
28611 assert_eq!(name, "alice");
28612 // And DROP PUBLICATION lands the new variant.
28613 let s = parse("DROP PUBLICATION p1");
28614 assert!(matches!(s, Statement::DropPublication { .. }));
28615 }
28616
28617 #[test]
28618 fn publication_ddl_display_roundtrips() {
28619 // Every CREATE PUBLICATION variant must Display → parse →
28620 // same AST. v6.1.3 covers all three scope shapes.
28621 for sql in [
28622 "CREATE PUBLICATION pub_a",
28623 "CREATE PUBLICATION pub_a FOR ALL TABLES",
28624 "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
28625 "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
28626 "DROP PUBLICATION pub_a",
28627 "SHOW PUBLICATIONS",
28628 ] {
28629 let s = parse(sql);
28630 let printed = s.to_string();
28631 let again = parse_statement(&printed)
28632 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28633 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28634 }
28635 }
28636
28637 // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
28638
28639 #[test]
28640 fn create_function_returns_trigger_plpgsql_minimal() {
28641 let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
28642 let s = parse(sql);
28643 let Statement::CreateFunction(f) = s else {
28644 panic!("expected CreateFunction");
28645 };
28646 assert_eq!(f.name, "noop");
28647 assert!(!f.or_replace);
28648 assert!(f.args.is_empty());
28649 assert!(matches!(f.returns, FunctionReturn::Trigger));
28650 assert_eq!(f.language, "plpgsql");
28651 let FunctionBody::PlPgSql(block) = f.body else {
28652 panic!("expected PlPgSql body");
28653 };
28654 assert_eq!(block.statements.len(), 1);
28655 assert!(matches!(
28656 block.statements[0],
28657 PlPgSqlStmt::Return(ReturnTarget::New)
28658 ));
28659 }
28660
28661 #[test]
28662 fn create_function_or_replace_with_assignment() {
28663 // mailrs-shape trigger function: NEW.col := to_tsvector(...);
28664 // RETURN NEW.
28665 let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
28666BEGIN
28667 NEW.search_vector := to_tsvector('english', NEW.subject);
28668 RETURN NEW;
28669END;
28670$$";
28671 let s = parse(sql);
28672 let Statement::CreateFunction(f) = s else {
28673 panic!("expected CreateFunction");
28674 };
28675 assert!(f.or_replace);
28676 let FunctionBody::PlPgSql(block) = &f.body else {
28677 panic!("expected PlPgSql body");
28678 };
28679 assert_eq!(block.statements.len(), 2);
28680 // First statement: NEW.search_vector := to_tsvector(...)
28681 let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
28682 panic!("expected Assign as first stmt");
28683 };
28684 match target {
28685 AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
28686 other => panic!("expected NEW.col, got {other:?}"),
28687 }
28688 // Second statement: RETURN NEW
28689 assert!(matches!(
28690 block.statements[1],
28691 PlPgSqlStmt::Return(ReturnTarget::New)
28692 ));
28693 }
28694
28695 #[test]
28696 fn create_trigger_after_insert_or_update() {
28697 let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
28698 let s = parse(sql);
28699 let Statement::CreateTrigger(t) = s else {
28700 panic!("expected CreateTrigger");
28701 };
28702 assert_eq!(t.name, "tg");
28703 assert_eq!(t.table, "messages");
28704 assert_eq!(t.timing, TriggerTiming::After);
28705 assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
28706 assert_eq!(t.for_each, TriggerForEach::Row);
28707 assert_eq!(t.function, "update_sv");
28708 }
28709
28710 #[test]
28711 fn create_trigger_before_delete_execute_procedure_alias() {
28712 // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
28713 let sql =
28714 "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
28715 let s = parse(sql);
28716 let Statement::CreateTrigger(t) = s else {
28717 panic!("expected CreateTrigger");
28718 };
28719 assert_eq!(t.timing, TriggerTiming::Before);
28720 assert_eq!(t.events, vec![TriggerEvent::Delete]);
28721 }
28722
28723 #[test]
28724 fn drop_trigger_if_exists_round_trips() {
28725 // No parser support for DROP TRIGGER yet — added in v7.12.5
28726 // alongside the broader DROP …{IF EXISTS} cleanup. The
28727 // AST + Display impls are in place so we round-trip via
28728 // construction:
28729 let s = Statement::DropTrigger {
28730 name: "tg".into(),
28731 table: "messages".into(),
28732 if_exists: true,
28733 };
28734 assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
28735 }
28736
28737 #[test]
28738 fn trigger_ddl_display_roundtrips_through_parser() {
28739 // CREATE TRIGGER + its referenced CREATE FUNCTION must
28740 // Display → parse → same AST (modulo PL/pgSQL body
28741 // formatting which is parser-canonicalised).
28742 for sql in [
28743 "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
28744 "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
28745 ] {
28746 let s = parse(sql);
28747 let printed = s.to_string();
28748 let again = parse_statement(&printed)
28749 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28750 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28751 }
28752 }
28753}