Skip to main content

inillucent_sql/
precedence.rs

1//! The operator precedence table, as data.
2//!
3//! Invariant: precedence lives in one table that tests read, not in the shape
4//! of a hand-written descent. A table can be checked against the published
5//! order in a loop; a nest of functions can only be checked by reading it.
6//!
7//! The order is SQLite's own, weakest binding first:
8//!
9//! ```text
10//! OR
11//! AND
12//! NOT (unary, prefix)
13//! = == <> != > >= < <= IS IS NOT IN LIKE GLOB MATCH REGEXP BETWEEN ISNULL NOTNULL
14//! & | << >>
15//! + -
16//! * / %
17//! ||  -> ->>
18//! COLLATE (postfix)
19//! ~ + - (unary, prefix)
20//! ```
21//!
22//! SQLite gives every comparison and quasi-comparison the same precedence,
23//! which is why `a = b IS NULL` parses as `(a = b) IS NULL` rather than as
24//! `a = (b IS NULL)`. Splitting them into separate levels is the most common
25//! way to get this wrong.
26
27use crate::lexer::Punctuator;
28
29/// A binding power: the precedence a parser compares against.
30#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
31pub struct Power(pub u8);
32
33/// Below every operator; where a fresh expression starts.
34pub const LOWEST: Power = Power(0);
35/// `OR`.
36pub const OR: Power = Power(1);
37/// `AND`.
38pub const AND: Power = Power(2);
39/// Prefix `NOT`.
40pub const NOT: Power = Power(3);
41/// Every comparison, and the quasi-comparisons that share its level.
42pub const COMPARISON: Power = Power(4);
43/// `&`, `|`, `<<`, `>>`.
44pub const BITWISE: Power = Power(5);
45/// `<->`, `<=>`, `<#>`, `<+>`, `<~>`, `<%>`.
46///
47/// **The same level as the bitwise operators, which is where PostgreSQL puts
48/// them.** pgvector's distances are ordinary user-defined operators there, and
49/// PostgreSQL gives "any other operator" a slot that binds tighter than a
50/// comparison and looser than `+`. That is the slot that makes
51/// `WHERE v <=> q < 0.5` and `ORDER BY v <=> q` parse the way anybody writing
52/// them means, and it is the only property of the level that matters: nothing
53/// mixes a distance with a shift.
54pub const DISTANCE: Power = Power(5);
55/// `+` and `-`.
56pub const ADDITIVE: Power = Power(6);
57/// `*`, `/`, `%`.
58pub const MULTIPLICATIVE: Power = Power(7);
59/// `||`, `->`, `->>`.
60pub const CONCAT: Power = Power(8);
61/// Postfix `COLLATE`.
62pub const COLLATE: Power = Power(9);
63/// Prefix `~`, `+`, `-`.
64pub const UNARY: Power = Power(10);
65
66/// Returns the binding power of an infix punctuator, when it has one.
67pub fn infix_power(punctuator: Punctuator) -> Option<Power> {
68    let power = match punctuator {
69        Punctuator::Equal
70        | Punctuator::NotEqual
71        | Punctuator::Less
72        | Punctuator::LessEqual
73        | Punctuator::Greater
74        | Punctuator::GreaterEqual => COMPARISON,
75        Punctuator::BitAnd | Punctuator::BitOr | Punctuator::ShiftLeft | Punctuator::ShiftRight => {
76            BITWISE
77        }
78        Punctuator::L2Distance
79        | Punctuator::CosineDistance
80        | Punctuator::NegativeInnerProduct
81        | Punctuator::L1Distance
82        | Punctuator::HammingDistance
83        | Punctuator::JaccardDistance => DISTANCE,
84        Punctuator::Plus | Punctuator::Minus => ADDITIVE,
85        Punctuator::Star | Punctuator::Slash | Punctuator::Percent => MULTIPLICATIVE,
86        Punctuator::Concat | Punctuator::Arrow | Punctuator::DoubleArrow => CONCAT,
87        _ => return None,
88    };
89    Some(power)
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    /// The published order, weakest first. A change to the table that does not
97    /// change this list has changed how SQL parses.
98    #[test]
99    fn the_levels_are_in_the_published_order() {
100        let levels = [
101            LOWEST,
102            OR,
103            AND,
104            NOT,
105            COMPARISON,
106            BITWISE,
107            ADDITIVE,
108            MULTIPLICATIVE,
109            CONCAT,
110            COLLATE,
111            UNARY,
112        ];
113        for pair in levels.windows(2) {
114            let (weaker, stronger) = (pair.first().copied(), pair.get(1).copied());
115            assert!(weaker < stronger, "{weaker:?} !< {stronger:?}");
116        }
117    }
118
119    /// Every comparison shares one level. This is the rule that decides how
120    /// `a = b IS NULL` parses, and separating them is the usual mistake.
121    #[test]
122    fn every_comparison_shares_one_level() {
123        for punctuator in [
124            Punctuator::Equal,
125            Punctuator::NotEqual,
126            Punctuator::Less,
127            Punctuator::LessEqual,
128            Punctuator::Greater,
129            Punctuator::GreaterEqual,
130        ] {
131            assert_eq!(infix_power(punctuator), Some(COMPARISON), "{punctuator:?}");
132        }
133    }
134
135    /// Concatenation binds tighter than arithmetic, which is not the rule most
136    /// languages use and is the rule SQLite uses.
137    #[test]
138    fn concatenation_binds_tighter_than_arithmetic() {
139        assert!(infix_power(Punctuator::Concat) > infix_power(Punctuator::Star));
140        assert!(infix_power(Punctuator::Star) > infix_power(Punctuator::Plus));
141        assert!(infix_power(Punctuator::Plus) > infix_power(Punctuator::BitOr));
142    }
143
144    /// Punctuation that is not an operator has no power at all, so the Pratt
145    /// loop stops on it rather than treating it as a weak operator.
146    #[test]
147    fn non_operators_have_no_power() {
148        for punctuator in [
149            Punctuator::LeftParen,
150            Punctuator::RightParen,
151            Punctuator::Comma,
152            Punctuator::Semicolon,
153            Punctuator::Dot,
154            Punctuator::BitNot,
155        ] {
156            assert_eq!(infix_power(punctuator), None, "{punctuator:?}");
157        }
158    }
159}