squonk_ast/precedence/mod.rs
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4//! One binding-power table for parsing and rendering SQL expressions and set
5//! operations.
6
7use crate::ast::{
8 BinaryOperator, EqualsSpelling, IsDistinctFromSpelling, IsNotDistinctFromSpelling, SetOperator,
9 UnaryOperator,
10};
11
12/// Operator associativity used by parser validation and renderer grouping.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum Assoc {
15 /// Left-associative (`a - b - c` groups as `(a - b) - c`).
16 Left,
17 /// Right-associative (`a = b = c` groups as `a = (b = c)`).
18 Right,
19 /// Non-associative — chaining the operator is a parse error.
20 NonAssoc,
21}
22
23/// Pratt and render-time binding powers for an infix operator.
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub struct BindingPower {
26 /// Left-hand operand.
27 pub left: u8,
28 /// Right-hand operand.
29 pub right: u8,
30 /// The operator's associativity; see [`Assoc`].
31 pub assoc: Assoc,
32}
33
34/// Dialect-owned Pratt and render-time binding powers.
35///
36/// The table is field-based rather than array-indexed so adding a
37/// [`BinaryOperator`] or [`UnaryOperator`] forces this module to name its binding
38/// power. Dialects can replace the whole table on [`FeatureSet`] or build a
39/// small delta with [`with_binary`](Self::with_binary).
40///
41/// [`FeatureSet`]: crate::dialect::FeatureSet
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub struct BindingPowerTable {
44 /// Binding power of logical `OR`.
45 pub or: BindingPower,
46 /// MySQL `XOR` logical exclusive-or: binds tighter than `OR` and looser than
47 /// `AND` (MySQL's `OR < XOR < AND` precedence). No standard operator occupies
48 /// this rank, so it gets its own field rather than sharing `or`/`and`.
49 pub xor: BindingPower,
50 /// Binding power of logical `AND`.
51 pub and: BindingPower,
52 /// Binding power of the comparison operators (`=`/`<`/`>`/…).
53 pub comparison: BindingPower,
54 /// The range/pattern/membership predicate tier — `[NOT] BETWEEN`, `[NOT] IN`,
55 /// `[NOT] LIKE`/`ILIKE`/`SIMILAR TO`. `None` tracks [`comparison`](Self::comparison)
56 /// (the historical placement every dialect parsed these at, so a dialect that
57 /// re-associates its comparisons carries these with it); `Some` re-ranks them to a
58 /// dedicated tier, read through [`range_predicate`](Self::range_predicate).
59 ///
60 /// PostgreSQL/DuckDB place this family one tier ABOVE the comparison operators
61 /// (`<`/`>`/`=`) and below the `%left Op` "any other operator" rank — gram.y's
62 /// `%nonassoc BETWEEN IN_P LIKE ILIKE SIMILAR` row — so `a = b BETWEEN c AND d` groups
63 /// `a = (b BETWEEN c AND d)` (engine-measured on pg_query 6.1 and DuckDB
64 /// `json_serialize_sql`); the PostgreSQL/Lenient presets set
65 /// `Some(`[`RANGE_PREDICATE_ABOVE_COMPARISON`]`)`. MySQL/SQLite rank BETWEEN at or
66 /// below comparison (their manuals), so they leave this `None` and track `comparison`
67 /// automatically, staying byte-identical. The `IS [NOT] NULL`/`IS DISTINCT`/truth-value
68 /// predicates are a SEPARATE (in PostgreSQL/DuckDB, looser) tier — see
69 /// [`is_predicate_override`](Self::is_predicate_override).
70 pub range_predicate_override: Option<BindingPower>,
71 /// The `IS`-family predicate tier — the postfix `IS [NOT] NULL` / `ISNULL` / `NOTNULL` /
72 /// `NOT NULL` null tests, the `IS [NOT] {TRUE|FALSE|UNKNOWN}` truth-value tests, `IS [NOT]
73 /// NORMALIZED`, and the infix `IS [NOT] DISTINCT FROM` (keyword form). `None` tracks
74 /// [`comparison`](Self::comparison) (MySQL/SQLite rank `IS` at the comparison/equality tier,
75 /// so they stay byte-identical); `Some` re-ranks the whole family to a dedicated tier read
76 /// through [`predicate`](Self::predicate).
77 ///
78 /// PostgreSQL and DuckDB place this family one tier BELOW the comparison operators
79 /// (`<`/`>`/`=`) and above `NOT` — PostgreSQL gram.y's `%nonassoc IS ISNULL NOTNULL` row,
80 /// which sits under `%nonassoc '<' '>' '='` — so `a <> b IS NULL` groups `(a <> b) IS NULL`
81 /// and `a IS DISTINCT FROM b = c` groups `a IS DISTINCT FROM (b = c)` (engine-measured on
82 /// PostgreSQL 16 `pg_get_viewdef` and DuckDB 1.5.4 `json_serialize_sql`); the
83 /// PostgreSQL/DuckDB/Lenient presets set `Some(`[`IS_PREDICATE_BELOW_COMPARISON`]`)`.
84 /// Non-associative, matching both engines (`a IS DISTINCT FROM b IS DISTINCT FROM c` is a
85 /// parse error). SQLite's bare general `IS`/`IS NOT` and MySQL's `<=>` are comparison-tier
86 /// null-safe (in)equality, distinguished by spelling, and are unaffected by this override.
87 pub is_predicate_override: Option<BindingPower>,
88 /// The `==` spelling of equality ([`BinaryOperator::Eq`] with
89 /// [`EqualsSpelling::Double`]). Carries the
90 /// [`comparison`](Self::comparison) value in every dialect but DuckDB, where `==`
91 /// is not the `%nonassoc '='` comparison but a generic `%left Op` operator: it
92 /// binds *tighter than* the comparisons and looser than additive, left-associative
93 /// (`1 == 2 == 3` is `((1 = 2) = 3)`, `1 < 2 == 3` is `(1 < (2 = 3))`, `1 + 1 == 2`
94 /// is `((1 + 1) = 2)` — measured on 1.5.4). A distinct field so this DuckDB
95 /// re-ranking never disturbs the `=`/`<`/`>` comparisons it shares
96 /// [`BinaryOperator::Eq`] with; [`with_binary`](Self::with_binary) keeps the two in
97 /// sync for the comparison-family callers (SQLite/MySQL move both together) and lets
98 /// DuckDB move `==` alone.
99 pub double_equals: BindingPower,
100 /// Binding power of the additive operators (`+`/`-`).
101 pub additive: BindingPower,
102 /// Binding power of the multiplicative operators (`*`/`/`/`%`).
103 pub multiplicative: BindingPower,
104 /// PostgreSQL exponentiation (`^`, [`BinaryOperator::Exponent`]). Its OWN precedence
105 /// row in gram.y (`%left '^'`): tighter than [`multiplicative`](Self::multiplicative)
106 /// `* / %` and looser than the unary sign, left-associative (`2 ^ 3 ^ 2` is
107 /// `(2 ^ 3) ^ 2`, `2 ^ 3 * 2` is `(2 ^ 3) * 2` — engine-measured on pg_query). Distinct
108 /// from [`bitwise_xor`](Self::bitwise_xor): MySQL's `^` (bitwise XOR) binds tighter than
109 /// `*`, PostgreSQL's `^` (power) binds tighter than `*` but is a different operator at a
110 /// different (higher) rank, so the two never share a field. Only reachable under
111 /// [`CaretOperator::Exponent`](crate::dialect::CaretOperator).
112 pub exponent: BindingPower,
113 /// Binding power of the `||` string-concatenation operator.
114 pub string_concat: BindingPower,
115 /// PostgreSQL's "any other operator" rank — the precedence gram.y gives every
116 /// native and user-defined symbolic operator outside the arithmetic/comparison
117 /// core (`%left Op OPERATOR`): looser than additive `+`/`-`, tighter than the
118 /// comparison/`BETWEEN` family. The `@>`/`<@` containment and `->>` JSON
119 /// operators bind here, left-associative. It carries the same value as
120 /// [`string_concat`](Self::string_concat) (`||` is itself an "any other operator"
121 /// in PostgreSQL) but is a distinct field so moving one does not move the other.
122 pub any_operator: BindingPower,
123 /// The `->` token's rank ([`BinaryOperator::JsonGet`], which is also the DuckDB
124 /// lambda arrow — one token, one rank). In PostgreSQL/SQLite it carries the
125 /// [`any_operator`](Self::any_operator) value (`->` is an ordinary `Op` there),
126 /// but DuckDB lexes `->` as its own `LAMBDA_ARROW` grammar token ranked *below
127 /// every* expression operator — `x -> x % 2 = 0` and even `x -> x OR y` put the
128 /// whole right side in the lambda body, and `NOT x -> y` takes `NOT x` as the
129 /// left operand (measured on 1.5.4 via `json_serialize_sql`) — while its `->>`
130 /// stays at the `Op` rank. A distinct field so a dialect can move `->` without
131 /// moving `->>`/`@>`/`<@`, the same split rationale as
132 /// [`string_concat`](Self::string_concat) vs `any_operator`.
133 pub json_get: BindingPower,
134 /// Bitwise OR (`|`). In PostgreSQL/SQLite/DuckDB the four binary bitwise operators
135 /// share one rank between additive and comparison (engine-measured: `1 | 2 & 2` is
136 /// `(1 | 2) & 2`), so the fields below carry the same standard value but stay distinct
137 /// — MySQL ranks `|` < `&` < `<<`/`>>` at three separate levels (its documented
138 /// grammar), so a dialect moves each independently, exactly as
139 /// [`string_concat`](Self::string_concat) and [`any_operator`](Self::any_operator)
140 /// split.
141 pub bitwise_or: BindingPower,
142 /// Bitwise AND (`&`). Standard-equal to [`bitwise_or`](Self::bitwise_or); tighter than
143 /// it in MySQL.
144 pub bitwise_and: BindingPower,
145 /// Bitwise shift (`<<` / `>>`, one shared rank — the two shifts never diverge in any
146 /// dialect). Looser than additive everywhere; tighter than `&` and looser than
147 /// additive in MySQL.
148 pub bitwise_shift: BindingPower,
149 /// Bitwise exclusive-or (PostgreSQL `#`, MySQL `^`). In the standard table it carries
150 /// PostgreSQL's "any other operator" rank (looser than additive, like `#`); MySQL
151 /// re-ranks it *tighter than* multiplicative (`^` binds above `*`), so it is its own
152 /// field.
153 pub bitwise_xor: BindingPower,
154 /// Binding power of the prefix `NOT` operator.
155 pub prefix_not: u8,
156 /// Binding power of the prefix `+`/`-` sign.
157 pub prefix_sign: u8,
158 /// Prefix bitwise complement (`~`). Binds like the unary sign in SQLite/MySQL, but in
159 /// PostgreSQL/DuckDB it sits between the arithmetic operators and the binary bitwise
160 /// family: one above [`bitwise_or`](Self::bitwise_or)'s left rank, so the bitwise
161 /// binaries do not fold into its operand (`~ 1 & 3` is `(~ 1) & 3`) while the tighter
162 /// arithmetic does (`~ 1 + 1` is `~ (1 + 1)`) — both engine-measured — and the render
163 /// still parenthesizes `~ (a & b)` (the strict-inequality break the equal-rank case
164 /// would lose).
165 pub prefix_bitwise_not: u8,
166 /// `expr AT TIME ZONE zone` (PostgreSQL): binds tighter than the arithmetic
167 /// operators and looser than `COLLATE` and the unary sign.
168 pub at_time_zone: BindingPower,
169 /// `expr COLLATE collation` (PostgreSQL): just tighter than `AT TIME ZONE`,
170 /// just looser than the unary sign.
171 pub collate: BindingPower,
172 /// `base[index]` / `base[lo:hi]` array subscript (PostgreSQL): binds tighter
173 /// than the unary sign and looser than the `::` typecast.
174 pub subscript: BindingPower,
175 /// `expr::type` typecast (PostgreSQL): one of the tightest-binding operators,
176 /// just looser than composite field selection.
177 pub typecast: BindingPower,
178 /// `(expr).field` composite field selection (PostgreSQL): the tightest of the
179 /// postfix operators.
180 pub field_selection: BindingPower,
181}
182
183/// Dialect-owned set-operation binding powers.
184///
185/// Set operations combine [`SetExpr`](crate::ast::SetExpr) query bodies rather
186/// than [`Expr`](crate::ast::Expr) nodes, so they use a small parallel table
187/// keyed by [`SetOperator`]. The same binding-power discipline still applies:
188/// parser precedence climbing and render-time parenthesization both read this
189/// table, so `INTERSECT` cannot drift from `UNION`/`EXCEPT` in one direction only.
190#[derive(Clone, Copy, Debug, PartialEq, Eq)]
191pub struct SetOperationBindingPowerTable {
192 /// Binding power of `UNION`/`EXCEPT`.
193 pub union_except: BindingPower,
194 /// Binding power of `INTERSECT` (tighter than `UNION`/`EXCEPT`).
195 pub intersect: BindingPower,
196}
197
198impl SetOperationBindingPowerTable {
199 /// Standard SQL/PostgreSQL set-operation binding powers.
200 pub const STANDARD: Self = Self {
201 union_except: BindingPower {
202 left: 10,
203 right: 11,
204 assoc: Assoc::Left,
205 },
206 intersect: BindingPower {
207 left: 20,
208 right: 21,
209 assoc: Assoc::Left,
210 },
211 };
212
213 /// Return the binding power for `op`.
214 pub const fn set_operation(&self, op: &SetOperator) -> BindingPower {
215 match op {
216 SetOperator::Union | SetOperator::Except => self.union_except,
217 SetOperator::Intersect => self.intersect,
218 }
219 }
220
221 /// Return a copy of this table with one set-operator class replaced.
222 pub const fn with_set_operator(mut self, op: &SetOperator, bp: BindingPower) -> Self {
223 match op {
224 SetOperator::Union | SetOperator::Except => self.union_except = bp,
225 SetOperator::Intersect => self.intersect = bp,
226 }
227 self
228 }
229
230 /// Return whether a child set operation needs parentheses under `parent`.
231 pub const fn needs_parens(
232 &self,
233 parent: &SetOperator,
234 child: &SetOperator,
235 side: Side,
236 ) -> bool {
237 needs_parens_between(self.set_operation(parent), self.set_operation(child), side)
238 }
239}
240
241impl BindingPowerTable {
242 /// Standard M1 binding powers shared by the ANSI and PostgreSQL presets.
243 pub const STANDARD: Self = Self {
244 or: BindingPower {
245 left: 10,
246 right: 11,
247 assoc: Assoc::Left,
248 },
249 // Between `OR` (10) and `AND` (20): MySQL ranks `XOR` strictly between them.
250 // Left-associative, like the other boolean operators (`a XOR b XOR c` groups
251 // left).
252 xor: BindingPower {
253 left: 15,
254 right: 16,
255 assoc: Assoc::Left,
256 },
257 and: BindingPower {
258 left: 20,
259 right: 21,
260 assoc: Assoc::Left,
261 },
262 comparison: BindingPower {
263 left: 40,
264 right: 41,
265 assoc: Assoc::NonAssoc,
266 },
267 // Range/pattern/membership predicates track comparison by default; the
268 // PostgreSQL/Lenient presets re-rank them one tier above it (see the field docs).
269 range_predicate_override: None,
270 // The `IS`-family predicates track comparison by default (MySQL/SQLite rank them at
271 // the comparison/equality tier); the PostgreSQL/DuckDB/Lenient presets re-rank them
272 // one tier below comparison (see the field docs).
273 is_predicate_override: None,
274 // `==` tracks `=` by default (same rank, same non-associativity); DuckDB's
275 // preset re-ranks it alone to the generic `%left Op` level (see the field docs).
276 double_equals: BindingPower {
277 left: 40,
278 right: 41,
279 assoc: Assoc::NonAssoc,
280 },
281 additive: BindingPower {
282 left: 50,
283 right: 51,
284 assoc: Assoc::Left,
285 },
286 multiplicative: BindingPower {
287 left: 60,
288 right: 61,
289 assoc: Assoc::Left,
290 },
291 // PostgreSQL `^` exponentiation sits above multiplicative (`60`) and below the
292 // unary sign (`80`), left-associative — its own gram.y precedence row. Only the
293 // presets whose `caret_operator` is `Exponent` (PostgreSQL/DuckDB) reach it;
294 // elsewhere `^` is bitwise XOR (MySQL) or nothing, at a different rank.
295 exponent: BindingPower {
296 left: 65,
297 right: 66,
298 assoc: Assoc::Left,
299 },
300 // `||` binds looser than additive and tighter than comparison, matching
301 // PostgreSQL's "any other operator" rank: `a || b + c` is `a || (b + c)`.
302 string_concat: BindingPower {
303 left: 45,
304 right: 46,
305 assoc: Assoc::Left,
306 },
307 // PostgreSQL's "any other operator" rank sits at the same level as `||`
308 // (both are `%left Op OPERATOR` in gram.y): looser than additive (50),
309 // tighter than comparison (40), left-associative.
310 any_operator: BindingPower {
311 left: 45,
312 right: 46,
313 assoc: Assoc::Left,
314 },
315 // `->` is an ordinary "any other operator" in the standard table; DuckDB's
316 // preset moves this field below `or` (its lambda-arrow rank).
317 json_get: BindingPower {
318 left: 45,
319 right: 46,
320 assoc: Assoc::Left,
321 },
322 // The binary bitwise operators sit at PostgreSQL's "any other operator" rank in
323 // the standard table (looser than additive `50`, tighter than comparison `40`),
324 // engine-measured for PostgreSQL/SQLite/DuckDB. MySQL's preset re-ranks them at
325 // three distinct levels. All left-associative.
326 bitwise_or: BindingPower {
327 left: 45,
328 right: 46,
329 assoc: Assoc::Left,
330 },
331 bitwise_and: BindingPower {
332 left: 45,
333 right: 46,
334 assoc: Assoc::Left,
335 },
336 bitwise_shift: BindingPower {
337 left: 45,
338 right: 46,
339 assoc: Assoc::Left,
340 },
341 bitwise_xor: BindingPower {
342 left: 45,
343 right: 46,
344 assoc: Assoc::Left,
345 },
346 prefix_not: 30,
347 prefix_sign: 80,
348 // PostgreSQL/DuckDB rank prefix `~` one above the bitwise binaries' left rank
349 // (`45`): the bitwise binaries do not capture into its operand (`~ 1 & 3` groups
350 // `(~ 1) & 3`) but additive `50` does (`~ 1 + 1` groups `~ (1 + 1)`). SQLite/MySQL
351 // override this to the tight `prefix_sign` rank. The value differs from `bitwise_*`
352 // deliberately so the renderer's strict-inequality parenthesization still wraps
353 // `~ (a & b)`.
354 prefix_bitwise_not: 46,
355 // The PostgreSQL postfix operators all bind tighter than the binary
356 // arithmetic operators; their relative order follows gram.y (lowest to
357 // highest): `AT TIME ZONE` < `COLLATE` < unary sign < `[]` < `::` < `.`.
358 at_time_zone: BindingPower {
359 left: 70,
360 right: 71,
361 assoc: Assoc::Left,
362 },
363 collate: BindingPower {
364 left: 74,
365 right: 75,
366 assoc: Assoc::Left,
367 },
368 subscript: BindingPower {
369 left: 84,
370 right: 85,
371 assoc: Assoc::Left,
372 },
373 typecast: BindingPower {
374 left: 88,
375 right: 89,
376 assoc: Assoc::Left,
377 },
378 field_selection: BindingPower {
379 left: 92,
380 right: 93,
381 assoc: Assoc::Left,
382 },
383 };
384
385 /// Return the binary binding power for `op`.
386 pub const fn binary(&self, op: &BinaryOperator) -> BindingPower {
387 match op {
388 BinaryOperator::Or => self.or,
389 BinaryOperator::Xor => self.xor,
390 BinaryOperator::And => self.and,
391 // `RLIKE`/`REGEXP` match at comparison precedence (like `LIKE`); folding
392 // onto `self.comparison` keeps them a single source of truth with the
393 // comparisons and the predicates, so a dialect that moves comparison
394 // precedence moves regex match with it.
395 // The keyword `IS [NOT] DISTINCT FROM` is the infix member of the `IS`-family
396 // predicate tier (PostgreSQL `%prec IS`): it reads [`predicate`](Self::predicate),
397 // so a preset that re-ranks the family below comparison carries the distinct test
398 // with the postfix `IS NULL`/truth-value predicates as one source of truth (and
399 // shares their non-associative chain rejection and render parenthesization).
400 BinaryOperator::IsDistinctFrom(IsDistinctFromSpelling::Keyword)
401 | BinaryOperator::IsNotDistinctFrom(IsNotDistinctFromSpelling::Keyword) => {
402 self.predicate()
403 }
404 // SQLite's bare general `IS`/`IS NOT` and MySQL's `<=>` are comparison-tier
405 // null-safe (in)equality (SQLite groups `IS` with `=`; MySQL's `<=>` is a
406 // comparison operator), so they stay on `comparison` even when a dialect moves the
407 // keyword `IS DISTINCT FROM` family below it.
408 BinaryOperator::IsDistinctFrom(IsDistinctFromSpelling::Is)
409 | BinaryOperator::IsNotDistinctFrom(
410 IsNotDistinctFromSpelling::Is | IsNotDistinctFromSpelling::NullSafeEq,
411 ) => self.comparison,
412 // `==` reads its own field (DuckDB re-ranks it to the generic `%left Op`
413 // level); every other equality/comparison spelling stays on `comparison`.
414 BinaryOperator::Eq(EqualsSpelling::Double) => self.double_equals,
415 BinaryOperator::Eq(_)
416 | BinaryOperator::NotEq(_)
417 | BinaryOperator::Lt
418 | BinaryOperator::LtEq
419 | BinaryOperator::Gt
420 | BinaryOperator::GtEq
421 | BinaryOperator::Regexp(_)
422 | BinaryOperator::Glob
423 | BinaryOperator::Match => self.comparison,
424 BinaryOperator::Plus | BinaryOperator::Minus => self.additive,
425 // `DIV`/`//` (integer division) and `MOD` join `*`/`/`/`%` at multiplicative.
426 BinaryOperator::Multiply
427 | BinaryOperator::Divide
428 | BinaryOperator::Modulo(_)
429 | BinaryOperator::IntegerDivide(_) => self.multiplicative,
430 BinaryOperator::Exponent => self.exponent,
431 BinaryOperator::StringConcat => self.string_concat,
432 // The PostgreSQL `@>`/`<@` containment and `->>` JSON operators bind at
433 // the "any other operator" rank (ADR-0008), folded onto one field — a
434 // dialect moving that rank moves them together. `->` reads its own field
435 // because DuckDB re-ranks it alone (see `json_get`).
436 BinaryOperator::Contains
437 | BinaryOperator::ContainedBy
438 | BinaryOperator::StartsWith
439 | BinaryOperator::JsonGetText
440 // The PostgreSQL `jsonb` existence/path/search operators (`?`/`?|`/`?&`/`@?`/
441 // `@@`/`#>`/`#>>`/`#-`) are all ordinary `%left Op` operators (engine-measured:
442 // tighter than comparison, looser than additive, left-associative), so they ride
443 // the same "any other operator" rank as `@>`/`<@`/`->>`.
444 | BinaryOperator::JsonExists
445 | BinaryOperator::JsonExistsAny
446 | BinaryOperator::JsonExistsAll
447 | BinaryOperator::JsonPathExists
448 | BinaryOperator::JsonPathMatch
449 | BinaryOperator::JsonExtractPath
450 | BinaryOperator::JsonExtractPathText
451 | BinaryOperator::JsonDeletePath
452 // The `&&` overlap operator is an ordinary `%left Op` operator in
453 // PostgreSQL/DuckDB (engine-measured on DuckDB 1.5.4: tighter than comparison
454 // and `AND`, left-associative), so it rides the same "any other operator" rank.
455 | BinaryOperator::Overlap => self.any_operator,
456 BinaryOperator::JsonGet => self.json_get,
457 // The binary bitwise operators each read their own field: coincident in the
458 // standard table, but MySQL ranks `|` < `&` < `<<`/`>>` at distinct levels.
459 BinaryOperator::BitwiseOr => self.bitwise_or,
460 BinaryOperator::BitwiseAnd => self.bitwise_and,
461 BinaryOperator::BitwiseShiftLeft | BinaryOperator::BitwiseShiftRight => {
462 self.bitwise_shift
463 }
464 // Both XOR spellings (`#`/`^`) fold onto one rank; the spelling tag never
465 // affects precedence (it is which dialect, and each has one XOR rank).
466 BinaryOperator::BitwiseXor(_) => self.bitwise_xor,
467 // `OVERLAPS` carries a fixed cross-dialect rank (its PostgreSQL `%nonassoc
468 // OVERLAPS` row, just above comparison and below `Op`), not a table field —
469 // see `OVERLAPS_PREDICATE`.
470 BinaryOperator::Overlaps => OVERLAPS_PREDICATE,
471 }
472 }
473
474 /// Return the prefix binding power for `op`.
475 pub const fn prefix(&self, op: &UnaryOperator) -> u8 {
476 match op {
477 UnaryOperator::Not => self.prefix_not,
478 // `PRIOR` (Oracle/Snowflake `CONNECT BY`) binds like the unary sign in every
479 // dialect that has it, so it shares `prefix_sign` (tighter than comparison):
480 // `PRIOR a = b` groups as `(PRIOR a) = b`. No separate table field — the rank
481 // never diverges from the sign operators, so widening the per-preset table
482 // would add an axis no measured boundary moves.
483 UnaryOperator::Minus | UnaryOperator::Plus | UnaryOperator::Prior => self.prefix_sign,
484 UnaryOperator::BitwiseNot => self.prefix_bitwise_not,
485 }
486 }
487
488 /// Return the binding power of the `IS`-family predicates (`IS [NOT] NULL`, `ISNULL`,
489 /// `NOTNULL`, `NOT NULL`, `IS [NOT] {TRUE|FALSE|UNKNOWN}`, `IS [NOT] NORMALIZED`, and the
490 /// keyword `IS [NOT] DISTINCT FROM`).
491 ///
492 /// Defaults to [`comparison`](Self::comparison) — returned when
493 /// [`is_predicate_override`](Self::is_predicate_override) is `None`, so MySQL/SQLite (which
494 /// rank `IS` at the comparison/equality tier) carry these predicates with their comparisons
495 /// — while the PostgreSQL/DuckDB/Lenient presets override it to their dedicated tier one
496 /// rank below comparison. The parser climbs the family at this rank and forbids chaining
497 /// them with comparisons, and render-time parenthesization reads the *same* level, so the
498 /// two can never drift (ADR-0008).
499 pub const fn predicate(&self) -> BindingPower {
500 match self.is_predicate_override {
501 Some(bp) => bp,
502 None => self.comparison,
503 }
504 }
505
506 /// Return the binding power of the range/pattern/membership predicates (`[NOT]
507 /// BETWEEN`, `[NOT] IN`, `[NOT] LIKE`/`ILIKE`/`SIMILAR TO`).
508 ///
509 /// Defaults to [`comparison`](Self::comparison) — returned when
510 /// [`range_predicate_override`](Self::range_predicate_override) is `None`, so a dialect
511 /// that re-associates its comparisons carries these predicates with it — while the
512 /// PostgreSQL/Lenient presets override it to their dedicated tighter tier. The parser
513 /// climbs these predicates at this rank and the renderer parenthesizes them by it, one
514 /// source of truth (ADR-0008).
515 pub const fn range_predicate(&self) -> BindingPower {
516 match self.range_predicate_override {
517 Some(bp) => bp,
518 None => self.comparison,
519 }
520 }
521
522 /// Return a copy of this table with one binary operator class replaced.
523 pub const fn with_binary(mut self, op: &BinaryOperator, bp: BindingPower) -> Self {
524 match op {
525 BinaryOperator::Or => self.or = bp,
526 BinaryOperator::Xor => self.xor = bp,
527 BinaryOperator::And => self.and = bp,
528 // `==` alone moves only its own field (DuckDB re-ranks it apart from the
529 // comparisons); the rest of the comparison family moves `comparison` and
530 // carries `==` with it, so a dialect that re-associates its comparisons
531 // (SQLite/MySQL set them `Left` via `with_binary(&Eq(Single), …)`) keeps `==`
532 // tracking `=` without a second call.
533 BinaryOperator::Eq(EqualsSpelling::Double) => self.double_equals = bp,
534 BinaryOperator::Eq(_)
535 | BinaryOperator::NotEq(_)
536 | BinaryOperator::Lt
537 | BinaryOperator::LtEq
538 | BinaryOperator::Gt
539 | BinaryOperator::GtEq
540 | BinaryOperator::IsDistinctFrom(_)
541 | BinaryOperator::IsNotDistinctFrom(_)
542 | BinaryOperator::Regexp(_)
543 | BinaryOperator::Glob
544 | BinaryOperator::Match => {
545 self.comparison = bp;
546 self.double_equals = bp;
547 }
548 BinaryOperator::Plus | BinaryOperator::Minus => self.additive = bp,
549 BinaryOperator::Multiply
550 | BinaryOperator::Divide
551 | BinaryOperator::Modulo(_)
552 | BinaryOperator::IntegerDivide(_) => {
553 self.multiplicative = bp;
554 }
555 BinaryOperator::Exponent => self.exponent = bp,
556 BinaryOperator::StringConcat => self.string_concat = bp,
557 BinaryOperator::Contains
558 | BinaryOperator::ContainedBy
559 | BinaryOperator::StartsWith
560 | BinaryOperator::JsonGetText
561 | BinaryOperator::JsonExists
562 | BinaryOperator::JsonExistsAny
563 | BinaryOperator::JsonExistsAll
564 | BinaryOperator::JsonPathExists
565 | BinaryOperator::JsonPathMatch
566 | BinaryOperator::JsonExtractPath
567 | BinaryOperator::JsonExtractPathText
568 | BinaryOperator::JsonDeletePath
569 | BinaryOperator::Overlap => self.any_operator = bp,
570 BinaryOperator::JsonGet => self.json_get = bp,
571 BinaryOperator::BitwiseOr => self.bitwise_or = bp,
572 BinaryOperator::BitwiseAnd => self.bitwise_and = bp,
573 BinaryOperator::BitwiseShiftLeft | BinaryOperator::BitwiseShiftRight => {
574 self.bitwise_shift = bp;
575 }
576 BinaryOperator::BitwiseXor(_) => self.bitwise_xor = bp,
577 // `OVERLAPS` has a fixed cross-dialect rank (`OVERLAPS_PREDICATE`), so there is
578 // no table field to re-rank; a `with_binary` call for it is a no-op.
579 BinaryOperator::Overlaps => {}
580 }
581 self
582 }
583
584 /// Return whether a child binary expression needs parentheses under `parent`.
585 pub const fn needs_parens(
586 &self,
587 parent: &BinaryOperator,
588 child: &BinaryOperator,
589 side: Side,
590 ) -> bool {
591 needs_parens_between(self.binary(parent), self.binary(child), side)
592 }
593}
594
595/// Standard binding-power table used by the builtin dialect presets.
596pub const STANDARD_BINDING_POWERS: BindingPowerTable = BindingPowerTable::STANDARD;
597
598/// The binding power of DuckDB's unparenthesized `<expr> IN <c_expr>` list-membership
599/// operator ([`Expr::InExpr`](crate::ast::Expr::InExpr)).
600///
601/// A fixed rank — not a [`BindingPowerTable`] field — because the operator exists only
602/// under DuckDB (and the permissive Lenient union), where the comparison / string-concat
603/// ranks it sits between are the standard `40` / `45`. DuckDB's own `IN_P` precedence is
604/// just above the comparison operators and just below `Op`/`||`: `z = w IN y` groups
605/// `z = (w IN y)` (tighter than `=`), while `a * b IN y` groups `(a * b) IN y` and
606/// `a || b IN y` groups `(a || b) IN y` (looser than `*` and `||` on the left operand;
607/// all measured on 1.5.4 via `json_serialize_sql`). Left-associative — `z IN y IN w` is
608/// `(z IN y) IN w`. The `c_expr` right operand (subscript in, `::`/`COLLATE` out) is a
609/// grammatical restriction enforced by the parser, independent of this rank.
610pub const UNPARENTHESIZED_IN_LIST: BindingPower = BindingPower {
611 left: 42,
612 right: 43,
613 assoc: Assoc::Left,
614};
615
616/// The binding power of the SQL-standard `OVERLAPS` period predicate
617/// ([`BinaryOperator::Overlaps`]).
618///
619/// A fixed rank — not a [`BindingPowerTable`] field — because `OVERLAPS` exists only under
620/// the PostgreSQL/Lenient presets and its precedence never varies by dialect: PostgreSQL's
621/// `%nonassoc OVERLAPS` gram.y row sits just above the comparison/`BETWEEN` family (`40`)
622/// and just below the `%left Op` "any other operator" rank (`45`), so `x OVERLAPS y = TRUE`
623/// groups `(x OVERLAPS y) = TRUE`. Non-associative: the boolean result is not a `row`, so a
624/// second `OVERLAPS` has no valid row operand and the parser rejects the chain by operand
625/// shape rather than this flag — the [`Assoc::NonAssoc`] tag drives the renderer's
626/// parenthesization, matching the grammar's non-chaining.
627pub const OVERLAPS_PREDICATE: BindingPower = BindingPower {
628 left: 42,
629 right: 43,
630 assoc: Assoc::NonAssoc,
631};
632
633/// The binding power of the range/pattern/membership predicate tier under the
634/// PostgreSQL/Lenient presets (`[NOT] BETWEEN`, `[NOT] IN`, `[NOT] LIKE`/`ILIKE`/`SIMILAR
635/// TO`; see [`BindingPowerTable::range_predicate_override`]).
636///
637/// PostgreSQL's `%nonassoc BETWEEN IN_P LIKE ILIKE SIMILAR` tier: one rank above the
638/// comparison operators (`40`) and below the `%left Op` "any other operator" rank (`45`),
639/// so `a = b BETWEEN c AND d` groups `a = (b BETWEEN c AND d)`. Shares
640/// [`OVERLAPS_PREDICATE`]'s numeric rank (gram.y places `OVERLAPS` one line tighter, but
641/// the two never meet as adjacent operands — `OVERLAPS` requires two-element rows — so the
642/// distinction is unobservable). Non-associative: `a BETWEEN b AND c BETWEEN d AND e` is a
643/// parse error, matching PostgreSQL and DuckDB.
644pub const RANGE_PREDICATE_ABOVE_COMPARISON: BindingPower = BindingPower {
645 left: 42,
646 right: 43,
647 assoc: Assoc::NonAssoc,
648};
649
650/// The binding power of the `IS`-family predicate tier under the PostgreSQL/DuckDB/Lenient
651/// presets (the postfix `IS [NOT] NULL`/`ISNULL`/`NOTNULL`/`NOT NULL` null tests, the `IS [NOT]
652/// {TRUE|FALSE|UNKNOWN}` truth-value tests, `IS [NOT] NORMALIZED`, and the keyword `IS [NOT]
653/// DISTINCT FROM`; see [`BindingPowerTable::is_predicate_override`]).
654///
655/// PostgreSQL's `%nonassoc IS ISNULL NOTNULL` tier: one rank BELOW the comparison operators
656/// (`40`) and above `%right NOT` (`30`), so `a <> b IS NULL` groups `(a <> b) IS NULL` and
657/// `a IS DISTINCT FROM b = c` groups `a IS DISTINCT FROM (b = c)` (engine-measured on PostgreSQL
658/// 16 and DuckDB 1.5.4). Non-associative: `a IS DISTINCT FROM b IS DISTINCT FROM c` is a parse
659/// error, matching both engines; the postfix `a <> b IS NULL` still parses because its left
660/// operand is a comparison (rank `40`), not another `IS`-family predicate.
661pub const IS_PREDICATE_BELOW_COMPARISON: BindingPower = BindingPower {
662 left: 35,
663 right: 36,
664 assoc: Assoc::NonAssoc,
665};
666
667/// Standard set-operation binding-power table used by the builtin dialect presets.
668pub const STANDARD_SET_OPERATION_BINDING_POWERS: SetOperationBindingPowerTable =
669 SetOperationBindingPowerTable::STANDARD;
670
671/// Child side within a binary parent expression.
672#[derive(Clone, Copy, Debug, PartialEq, Eq)]
673pub enum Side {
674 /// The left child of a binary parent expression.
675 Left,
676 /// The right child of a binary parent expression.
677 Right,
678}
679
680/// Whether a `child` node needs parentheses under a `parent` node on the given
681/// side, given their binding powers.
682///
683/// This is the render-time left/right binding-power comparison (Calcite
684/// `leftPrec`/`rightPrec`), factored out so binary-operator,
685/// comparison-predicate, and set-operation parenthesization all derive parens from
686/// one rule and cannot diverge. A child binding looser than the side the parent
687/// reaches across needs parens; an equal-precedence child needs them on the side
688/// the parent's associativity does not already imply (always, when non-associative).
689pub const fn needs_parens_between(parent: BindingPower, child: BindingPower, side: Side) -> bool {
690 let same_precedence = parent.left == child.left && parent.right == child.right;
691
692 match side {
693 Side::Left => {
694 child.right < parent.left
695 || (same_precedence
696 && match parent.assoc {
697 Assoc::Left => false,
698 Assoc::Right | Assoc::NonAssoc => true,
699 })
700 }
701 Side::Right => {
702 child.left < parent.right
703 || (same_precedence
704 && match parent.assoc {
705 Assoc::Right => false,
706 Assoc::Left | Assoc::NonAssoc => true,
707 })
708 }
709 }
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715 use crate::ast::{EqualsSpelling, NotEqSpelling};
716
717 #[test]
718 fn pratt_binding_powers_group_standard_sql_operators() {
719 let plus = STANDARD_BINDING_POWERS.binary(&BinaryOperator::Plus);
720 let multiply = STANDARD_BINDING_POWERS.binary(&BinaryOperator::Multiply);
721 let equals = STANDARD_BINDING_POWERS.binary(&BinaryOperator::Eq(EqualsSpelling::Single));
722 let and = STANDARD_BINDING_POWERS.binary(&BinaryOperator::And);
723 let concat = STANDARD_BINDING_POWERS.binary(&BinaryOperator::StringConcat);
724
725 assert!(plus.right < multiply.left);
726 assert!(plus.left < multiply.right);
727 assert!(and.right < equals.left);
728 // PostgreSQL: `||` is looser than additive (`a || b + c` == `a || (b + c)`)
729 // and tighter than comparison (`a = b || c` == `a = (b || c)`).
730 assert!(concat.right < plus.left);
731 assert!(equals.right < concat.left);
732 }
733
734 #[test]
735 fn render_parentheses_are_derived_from_binding_powers() {
736 assert!(STANDARD_BINDING_POWERS.needs_parens(
737 &BinaryOperator::Multiply,
738 &BinaryOperator::Plus,
739 Side::Left
740 ));
741 assert!(!STANDARD_BINDING_POWERS.needs_parens(
742 &BinaryOperator::Plus,
743 &BinaryOperator::Multiply,
744 Side::Right
745 ));
746 }
747
748 #[test]
749 fn predicate_level_tracks_comparison_precedence() {
750 // Predicates (`IS NULL` / `BETWEEN` / `IN`) parse at comparison precedence
751 // (`binding_power(Eq)`), so the render-time predicate level is a derived
752 // accessor onto it, not a second field — they can never drift (ADR-0008).
753 assert_eq!(
754 STANDARD_BINDING_POWERS.predicate(),
755 STANDARD_BINDING_POWERS.binary(&BinaryOperator::Eq(EqualsSpelling::Single)),
756 );
757
758 // Moving comparison precedence as dialect data moves the predicate level
759 // with it, keeping render parens aligned with how the parser climbed them.
760 const TIGHT_COMPARISON: BindingPowerTable = STANDARD_BINDING_POWERS.with_binary(
761 &BinaryOperator::Eq(EqualsSpelling::Single),
762 BindingPower {
763 left: 70,
764 right: 71,
765 assoc: Assoc::NonAssoc,
766 },
767 );
768 assert_eq!(
769 TIGHT_COMPARISON.predicate(),
770 TIGHT_COMPARISON.binary(&BinaryOperator::Eq(EqualsSpelling::Single)),
771 );
772 assert_eq!(TIGHT_COMPARISON.predicate().left, 70);
773 }
774
775 #[test]
776 fn range_predicate_defaults_to_comparison_and_tracks_its_associativity() {
777 // With no override, the range/pattern/membership tier IS the comparison level, so a
778 // dialect that re-associates its comparisons (MySQL's `Left` chain) carries the range
779 // predicates with it — no independent drift.
780 assert_eq!(STANDARD_BINDING_POWERS.range_predicate_override, None);
781 assert_eq!(
782 STANDARD_BINDING_POWERS.range_predicate(),
783 STANDARD_BINDING_POWERS.comparison,
784 );
785 const LEFT_COMPARISON: BindingPowerTable = STANDARD_BINDING_POWERS.with_binary(
786 &BinaryOperator::Eq(EqualsSpelling::Single),
787 BindingPower {
788 left: 40,
789 right: 41,
790 assoc: Assoc::Left,
791 },
792 );
793 assert_eq!(LEFT_COMPARISON.range_predicate().assoc, Assoc::Left);
794 }
795
796 #[test]
797 fn range_predicate_override_ranks_above_comparison_below_any_operator() {
798 // The PostgreSQL/Lenient override: range predicates bind tighter than the comparison
799 // operators (so `a = b BETWEEN c AND d` groups `a = (b BETWEEN c AND d)`) and looser
800 // than the "any other operator" rank, non-associative.
801 const PG_RANGE: BindingPowerTable = BindingPowerTable {
802 range_predicate_override: Some(RANGE_PREDICATE_ABOVE_COMPARISON),
803 ..STANDARD_BINDING_POWERS
804 };
805 let range = PG_RANGE.range_predicate();
806 let comparison = PG_RANGE.binary(&BinaryOperator::Eq(EqualsSpelling::Single));
807 assert!(comparison.left < range.left, "tighter than comparison");
808 assert!(
809 range.left < PG_RANGE.any_operator.left,
810 "looser than any-operator"
811 );
812 assert_eq!(range.assoc, Assoc::NonAssoc);
813 // The override leaves the IS-family predicate level (`predicate()`) on comparison.
814 assert_eq!(PG_RANGE.predicate(), comparison);
815 }
816
817 #[test]
818 fn predicate_operands_parenthesize_like_comparisons() {
819 // A compound operand of a predicate is parenthesized exactly when it would
820 // be under a comparison: looser-binding operands and equal-precedence
821 // (non-associative) comparisons need parens; tighter ones do not (ADR-0008).
822 let predicate = STANDARD_BINDING_POWERS.predicate();
823 let additive = STANDARD_BINDING_POWERS.binary(&BinaryOperator::Plus);
824 let or = STANDARD_BINDING_POWERS.binary(&BinaryOperator::Or);
825 let comparison =
826 STANDARD_BINDING_POWERS.binary(&BinaryOperator::Eq(EqualsSpelling::Single));
827
828 // `a + b IS NULL`: `+` binds tighter, so the operand stays bare.
829 assert!(!needs_parens_between(predicate, additive, Side::Left));
830 // `(a OR b) IS NULL`: `OR` binds looser, so it must be parenthesized.
831 assert!(needs_parens_between(predicate, or, Side::Left));
832 // `(a = b) IS NULL`: same precedence, non-associative -> parenthesized.
833 assert!(needs_parens_between(predicate, comparison, Side::Left));
834 // A `BETWEEN` bound parses on the right at comparison precedence, so the
835 // same operands group the same way there.
836 assert!(!needs_parens_between(predicate, additive, Side::Right));
837 assert!(needs_parens_between(predicate, comparison, Side::Right));
838 }
839
840 #[test]
841 fn associativity_controls_equal_precedence_parentheses() {
842 assert!(!STANDARD_BINDING_POWERS.needs_parens(
843 &BinaryOperator::Minus,
844 &BinaryOperator::Plus,
845 Side::Left
846 ));
847 assert!(STANDARD_BINDING_POWERS.needs_parens(
848 &BinaryOperator::Minus,
849 &BinaryOperator::Plus,
850 Side::Right
851 ));
852 assert!(STANDARD_BINDING_POWERS.needs_parens(
853 &BinaryOperator::Eq(EqualsSpelling::Single),
854 &BinaryOperator::Lt,
855 Side::Left
856 ));
857 assert!(STANDARD_BINDING_POWERS.needs_parens(
858 &BinaryOperator::Eq(EqualsSpelling::Single),
859 &BinaryOperator::Lt,
860 Side::Right
861 ));
862 }
863
864 #[test]
865 fn postgres_postfix_operators_rank_above_arithmetic_in_gram_y_order() {
866 // gram.y precedence, lowest to highest: multiplicative < AT TIME ZONE <
867 // COLLATE < unary sign < subscript < typecast < field selection (ADR-0008).
868 let table = STANDARD_BINDING_POWERS;
869 let multiplicative = table.multiplicative.left;
870 assert!(multiplicative < table.at_time_zone.left);
871 assert!(table.at_time_zone.left < table.collate.left);
872 assert!(table.collate.left < table.prefix_sign);
873 assert!(table.prefix_sign < table.subscript.left);
874 assert!(table.subscript.left < table.typecast.left);
875 assert!(table.typecast.left < table.field_selection.left);
876
877 // Every postfix operator is left-associative (`a::int::text` groups left).
878 for bp in [
879 table.at_time_zone,
880 table.collate,
881 table.subscript,
882 table.typecast,
883 table.field_selection,
884 ] {
885 assert_eq!(bp.assoc, Assoc::Left);
886 }
887 }
888
889 #[test]
890 fn precedence_values_match_m1_ordering() {
891 assert_eq!(STANDARD_BINDING_POWERS.binary(&BinaryOperator::Or).left, 10);
892 assert_eq!(
893 STANDARD_BINDING_POWERS.binary(&BinaryOperator::And).left,
894 20
895 );
896 assert_eq!(STANDARD_BINDING_POWERS.prefix(&UnaryOperator::Not), 30);
897 assert_eq!(
898 STANDARD_BINDING_POWERS
899 .binary(&BinaryOperator::Eq(EqualsSpelling::Single))
900 .left,
901 40
902 );
903 assert_eq!(
904 STANDARD_BINDING_POWERS.binary(&BinaryOperator::Plus).left,
905 50
906 );
907 assert_eq!(
908 STANDARD_BINDING_POWERS
909 .binary(&BinaryOperator::Multiply)
910 .left,
911 60
912 );
913 assert_eq!(
914 STANDARD_BINDING_POWERS
915 .binary(&BinaryOperator::StringConcat)
916 .left,
917 45
918 );
919 assert_eq!(STANDARD_BINDING_POWERS.prefix(&UnaryOperator::Plus), 80);
920 assert_eq!(STANDARD_BINDING_POWERS.prefix(&UnaryOperator::Minus), 80);
921 }
922
923 #[test]
924 fn mysql_keyword_operators_rank_at_their_documented_levels() {
925 use crate::ast::{IntegerDivideSpelling, ModuloSpelling, RegexpSpelling};
926
927 // `XOR` sits strictly between `OR` and `AND` (MySQL `OR < XOR < AND`) and is
928 // left-associative like the other boolean operators.
929 let or = STANDARD_BINDING_POWERS.binary(&BinaryOperator::Or);
930 let xor = STANDARD_BINDING_POWERS.binary(&BinaryOperator::Xor);
931 let and = STANDARD_BINDING_POWERS.binary(&BinaryOperator::And);
932 assert!(or.left < xor.left);
933 assert!(xor.left < and.left);
934 assert_eq!(xor.assoc, Assoc::Left);
935
936 // `DIV`/`//` and the `MOD` keyword share the multiplicative level with `*`/`/`/`%`.
937 let multiplicative = STANDARD_BINDING_POWERS.binary(&BinaryOperator::Multiply);
938 assert_eq!(
939 STANDARD_BINDING_POWERS
940 .binary(&BinaryOperator::IntegerDivide(IntegerDivideSpelling::Div)),
941 multiplicative
942 );
943 assert_eq!(
944 STANDARD_BINDING_POWERS.binary(&BinaryOperator::IntegerDivide(
945 IntegerDivideSpelling::SlashSlash
946 )),
947 multiplicative
948 );
949 assert_eq!(
950 STANDARD_BINDING_POWERS.binary(&BinaryOperator::Modulo(ModuloSpelling::Mod)),
951 multiplicative,
952 );
953 assert_eq!(
954 STANDARD_BINDING_POWERS.binary(&BinaryOperator::Modulo(ModuloSpelling::Percent)),
955 multiplicative,
956 );
957
958 // `RLIKE`/`REGEXP` match at comparison precedence regardless of spelling.
959 let comparison =
960 STANDARD_BINDING_POWERS.binary(&BinaryOperator::Eq(EqualsSpelling::Single));
961 assert_eq!(
962 STANDARD_BINDING_POWERS.binary(&BinaryOperator::Regexp(RegexpSpelling::Rlike)),
963 comparison,
964 );
965 assert_eq!(
966 STANDARD_BINDING_POWERS.binary(&BinaryOperator::Regexp(RegexpSpelling::Regexp)),
967 comparison,
968 );
969 }
970
971 #[test]
972 fn postgres_at_family_operators_bind_at_the_any_operator_rank() {
973 let table = STANDARD_BINDING_POWERS;
974 let additive = STANDARD_BINDING_POWERS.binary(&BinaryOperator::Plus);
975 let comparison =
976 STANDARD_BINDING_POWERS.binary(&BinaryOperator::Eq(EqualsSpelling::Single));
977
978 // `@>`/`<@`/`->`/`->>` all fold onto the "any other operator" rank: the same
979 // value as `||`, left-associative, tighter than comparison and looser than
980 // additive (PostgreSQL `%left Op OPERATOR`, between the two).
981 for op in [
982 BinaryOperator::Contains,
983 BinaryOperator::ContainedBy,
984 BinaryOperator::JsonGet,
985 BinaryOperator::JsonGetText,
986 ] {
987 let bp = STANDARD_BINDING_POWERS.binary(&op);
988 assert_eq!(bp, table.any_operator);
989 assert_eq!(
990 bp,
991 STANDARD_BINDING_POWERS.binary(&BinaryOperator::StringConcat)
992 );
993 assert_eq!(bp.assoc, Assoc::Left);
994 assert!(comparison.left < bp.left, "tighter than comparison");
995 assert!(bp.left < additive.left, "looser than additive");
996 }
997 }
998
999 #[test]
1000 fn any_operator_field_is_independent_of_string_concat() {
1001 // They carry the same default value, but moving `||` must not move the `@`-family
1002 // (they are distinct fields in the table).
1003 const HIGH_CONCAT: BindingPowerTable = STANDARD_BINDING_POWERS.with_binary(
1004 &BinaryOperator::StringConcat,
1005 BindingPower {
1006 left: 70,
1007 right: 71,
1008 assoc: Assoc::Left,
1009 },
1010 );
1011 assert_eq!(HIGH_CONCAT.binary(&BinaryOperator::StringConcat).left, 70);
1012 assert_eq!(
1013 HIGH_CONCAT.binary(&BinaryOperator::Contains),
1014 STANDARD_BINDING_POWERS.any_operator,
1015 );
1016 }
1017
1018 #[test]
1019 fn json_get_field_is_independent_of_the_any_operator_rank() {
1020 // `->` carries the `any_operator` value in the standard table, but moving it
1021 // (the DuckDB lambda-arrow re-rank) must not move `->>`/`@>`/`<@` — the same
1022 // distinct-field contract `string_concat` vs `any_operator` upholds.
1023 const LOOSE_ARROW: BindingPowerTable = STANDARD_BINDING_POWERS.with_binary(
1024 &BinaryOperator::JsonGet,
1025 BindingPower {
1026 left: 4,
1027 right: 5,
1028 assoc: Assoc::Left,
1029 },
1030 );
1031 assert_eq!(
1032 STANDARD_BINDING_POWERS.binary(&BinaryOperator::JsonGet),
1033 STANDARD_BINDING_POWERS.any_operator,
1034 );
1035 assert_eq!(LOOSE_ARROW.binary(&BinaryOperator::JsonGet).left, 4);
1036 for untouched in [
1037 BinaryOperator::JsonGetText,
1038 BinaryOperator::Contains,
1039 BinaryOperator::ContainedBy,
1040 ] {
1041 assert_eq!(
1042 LOOSE_ARROW.binary(&untouched),
1043 STANDARD_BINDING_POWERS.any_operator,
1044 );
1045 }
1046 }
1047
1048 #[test]
1049 fn standard_bitwise_operators_rank_between_additive_and_comparison() {
1050 use crate::ast::BitwiseXorSpelling;
1051
1052 let table = STANDARD_BINDING_POWERS;
1053 let additive = table.binary(&BinaryOperator::Plus);
1054 let comparison = table.binary(&BinaryOperator::Eq(EqualsSpelling::Single));
1055
1056 // PostgreSQL/SQLite/DuckDB place the four binary bitwise operators at one rank
1057 // (engine-measured), looser than additive and tighter than comparison, all
1058 // left-associative — the "any other operator" rank `#` also occupies.
1059 for op in [
1060 BinaryOperator::BitwiseOr,
1061 BinaryOperator::BitwiseAnd,
1062 BinaryOperator::BitwiseShiftLeft,
1063 BinaryOperator::BitwiseShiftRight,
1064 BinaryOperator::BitwiseXor(BitwiseXorSpelling::Hash),
1065 ] {
1066 let bp = table.binary(&op);
1067 assert_eq!(bp.assoc, Assoc::Left, "{op:?} is left-associative");
1068 assert!(comparison.left < bp.left, "{op:?} tighter than comparison");
1069 assert!(bp.left < additive.left, "{op:?} looser than additive");
1070 }
1071 // The two shift spellings share one rank; the two XOR spellings share one rank.
1072 assert_eq!(
1073 table.binary(&BinaryOperator::BitwiseShiftLeft),
1074 table.binary(&BinaryOperator::BitwiseShiftRight),
1075 );
1076 assert_eq!(
1077 table.binary(&BinaryOperator::BitwiseXor(BitwiseXorSpelling::Hash)),
1078 table.binary(&BinaryOperator::BitwiseXor(BitwiseXorSpelling::Caret)),
1079 );
1080 }
1081
1082 #[test]
1083 fn standard_prefix_bitwise_not_sits_between_arithmetic_and_the_bitwise_binaries() {
1084 // PostgreSQL/DuckDB (engine-measured): `~` binds looser than additive so `~ 1 + 1`
1085 // groups `~ (1 + 1)`, yet tighter-or-equal to the bitwise binaries so `~ 1 & 3`
1086 // groups `(~ 1) & 3`. The Pratt rule is `child.left > prefix_rbp` captures, so the
1087 // prefix rank must be `>= bitwise_or.left` and `< additive.left`.
1088 let table = STANDARD_BINDING_POWERS;
1089 let rbp = table.prefix(&UnaryOperator::BitwiseNot);
1090 assert!(
1091 table.binary(&BinaryOperator::BitwiseOr).left <= rbp,
1092 "`&`/`|` do not fold into `~`'s operand"
1093 );
1094 assert!(
1095 rbp < table.binary(&BinaryOperator::Plus).left,
1096 "additive folds into `~`'s operand"
1097 );
1098 // Strictly above the bitwise-binary left rank so the renderer parenthesizes
1099 // `~ (a & b)` — an equal value would drop the required parentheses.
1100 assert!(rbp > table.binary(&BinaryOperator::BitwiseAnd).left);
1101 }
1102
1103 #[test]
1104 fn bitwise_fields_move_independently() {
1105 use crate::ast::BitwiseXorSpelling;
1106
1107 // MySQL's divergence needs `|` < `&` < `<<`/`>>` at distinct ranks, so moving one
1108 // must not move the others — the ADR-0008 distinct-field contract.
1109 const TIGHT_AND: BindingPowerTable = STANDARD_BINDING_POWERS.with_binary(
1110 &BinaryOperator::BitwiseAnd,
1111 BindingPower {
1112 left: 48,
1113 right: 49,
1114 assoc: Assoc::Left,
1115 },
1116 );
1117 assert_eq!(TIGHT_AND.binary(&BinaryOperator::BitwiseAnd).left, 48);
1118 assert_eq!(
1119 TIGHT_AND.binary(&BinaryOperator::BitwiseOr),
1120 STANDARD_BINDING_POWERS.binary(&BinaryOperator::BitwiseOr),
1121 );
1122 assert_eq!(
1123 TIGHT_AND.binary(&BinaryOperator::BitwiseShiftLeft),
1124 STANDARD_BINDING_POWERS.binary(&BinaryOperator::BitwiseShiftLeft),
1125 );
1126 assert_eq!(
1127 TIGHT_AND.binary(&BinaryOperator::BitwiseXor(BitwiseXorSpelling::Caret)),
1128 STANDARD_BINDING_POWERS.binary(&BinaryOperator::BitwiseXor(BitwiseXorSpelling::Caret)),
1129 );
1130 }
1131
1132 #[test]
1133 fn comparisons_are_non_associative_for_m1() {
1134 assert_eq!(
1135 STANDARD_BINDING_POWERS
1136 .binary(&BinaryOperator::Eq(EqualsSpelling::Single))
1137 .assoc,
1138 Assoc::NonAssoc
1139 );
1140 assert_eq!(
1141 STANDARD_BINDING_POWERS
1142 .binary(&BinaryOperator::NotEq(NotEqSpelling::AngleBracket))
1143 .assoc,
1144 Assoc::NonAssoc
1145 );
1146 assert_eq!(
1147 STANDARD_BINDING_POWERS.binary(&BinaryOperator::Lt).assoc,
1148 Assoc::NonAssoc
1149 );
1150 assert_eq!(
1151 STANDARD_BINDING_POWERS.binary(&BinaryOperator::LtEq).assoc,
1152 Assoc::NonAssoc
1153 );
1154 assert_eq!(
1155 STANDARD_BINDING_POWERS.binary(&BinaryOperator::Gt).assoc,
1156 Assoc::NonAssoc
1157 );
1158 assert_eq!(
1159 STANDARD_BINDING_POWERS.binary(&BinaryOperator::GtEq).assoc,
1160 Assoc::NonAssoc
1161 );
1162 }
1163
1164 #[test]
1165 fn binding_power_table_supports_const_deltas() {
1166 const SQLITE_LIKE: BindingPowerTable = STANDARD_BINDING_POWERS.with_binary(
1167 &BinaryOperator::StringConcat,
1168 BindingPower {
1169 left: 70,
1170 right: 71,
1171 assoc: Assoc::Left,
1172 },
1173 );
1174
1175 assert_eq!(SQLITE_LIKE.binary(&BinaryOperator::StringConcat).left, 70);
1176 assert_eq!(
1177 SQLITE_LIKE.binary(&BinaryOperator::Plus),
1178 STANDARD_BINDING_POWERS.binary(&BinaryOperator::Plus),
1179 );
1180
1181 const LEFT_ASSOC_COMPARISON: BindingPowerTable = STANDARD_BINDING_POWERS.with_binary(
1182 &BinaryOperator::Lt,
1183 BindingPower {
1184 left: 40,
1185 right: 41,
1186 assoc: Assoc::Left,
1187 },
1188 );
1189
1190 assert_eq!(
1191 LEFT_ASSOC_COMPARISON
1192 .binary(&BinaryOperator::Eq(EqualsSpelling::Single))
1193 .assoc,
1194 Assoc::Left,
1195 );
1196 assert!(!LEFT_ASSOC_COMPARISON.needs_parens(
1197 &BinaryOperator::Lt,
1198 &BinaryOperator::Eq(EqualsSpelling::Single),
1199 Side::Left,
1200 ));
1201 assert!(LEFT_ASSOC_COMPARISON.needs_parens(
1202 &BinaryOperator::Lt,
1203 &BinaryOperator::Eq(EqualsSpelling::Single),
1204 Side::Right,
1205 ));
1206 }
1207
1208 #[test]
1209 fn set_operation_binding_powers_rank_intersect_above_union_except() {
1210 let union = STANDARD_SET_OPERATION_BINDING_POWERS.set_operation(&SetOperator::Union);
1211 let except = STANDARD_SET_OPERATION_BINDING_POWERS.set_operation(&SetOperator::Except);
1212 let intersect =
1213 STANDARD_SET_OPERATION_BINDING_POWERS.set_operation(&SetOperator::Intersect);
1214
1215 assert_eq!(union, except);
1216 assert!(union.right < intersect.left);
1217 assert!(intersect.left > except.right);
1218 }
1219
1220 #[test]
1221 fn set_operation_parentheses_are_derived_from_binding_powers() {
1222 assert!(!STANDARD_SET_OPERATION_BINDING_POWERS.needs_parens(
1223 &SetOperator::Union,
1224 &SetOperator::Intersect,
1225 Side::Right,
1226 ));
1227 assert!(STANDARD_SET_OPERATION_BINDING_POWERS.needs_parens(
1228 &SetOperator::Intersect,
1229 &SetOperator::Union,
1230 Side::Left,
1231 ));
1232 assert!(STANDARD_SET_OPERATION_BINDING_POWERS.needs_parens(
1233 &SetOperator::Union,
1234 &SetOperator::Except,
1235 Side::Right,
1236 ));
1237 assert!(!STANDARD_SET_OPERATION_BINDING_POWERS.needs_parens(
1238 &SetOperator::Except,
1239 &SetOperator::Union,
1240 Side::Left,
1241 ));
1242 }
1243
1244 #[test]
1245 fn set_operation_binding_power_table_supports_const_deltas() {
1246 const UNION_IS_HIGHER: SetOperationBindingPowerTable =
1247 STANDARD_SET_OPERATION_BINDING_POWERS.with_set_operator(
1248 &SetOperator::Union,
1249 BindingPower {
1250 left: 30,
1251 right: 31,
1252 assoc: Assoc::Left,
1253 },
1254 );
1255
1256 assert_eq!(UNION_IS_HIGHER.set_operation(&SetOperator::Except).left, 30,);
1257 assert_eq!(
1258 UNION_IS_HIGHER.set_operation(&SetOperator::Intersect).left,
1259 STANDARD_SET_OPERATION_BINDING_POWERS
1260 .set_operation(&SetOperator::Intersect)
1261 .left,
1262 );
1263 }
1264}