Skip to main content

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}