Skip to main content

inillucent_sql/
keyword.rs

1//! The keyword table of the pinned SQLite release, and the fallback rule.
2//!
3//! Invariant: this table is transcribed from the keyword list the pinned
4//! release documents, not generated from its parser source, and every entry
5//! records whether SQLite lets that word be used as a bare identifier. A word
6//! that is a keyword here but an identifier there is a parse divergence with no
7//! other symptom, so the fallback flag is data rather than a special case
8//! buried in the parser.
9//!
10//! SQLite's real rule is per grammar position, and it is written in its grammar
11//! as **two** declarations rather than one. A `%fallback` declaration lets a
12//! large set of keywords stand in for an identifier wherever an identifier is
13//! expected; that set is reproduced here as [`Keyword::may_fall_back`]. Beside
14//! it, a `%token_class` declaration names a second set that the *name*
15//! production accepts and two narrower positions do not:
16//!
17//! ```text
18//! %token_class idj  ID|INDEXED|JOIN_KW.
19//! %token_class ids  ID|STRING.
20//! nm(A)       ::= idj(A).      // every name: a column, a table, an index
21//! nm(A)       ::= STRING(A).
22//! as(X)       ::= AS nm(Y).    // an alias written with AS is a name
23//! as(X)       ::= ids(X).      // one written without AS is not
24//! typename(A) ::= ids(A).      // and a declared type is not either
25//! typename(A) ::= typename ids.
26//! ```
27//!
28//! `JOIN_KW` is `CROSS FULL INNER LEFT NATURAL OUTER RIGHT`, and neither those
29//! seven nor `INDEXED` is in the fallback set - which is why transcribing
30//! `%fallback` alone was not enough. `CREATE TABLE pairs (left TEXT)` is a
31//! schema SQLite writes and accepts, and it was refused here as a syntax error;
32//! the *reason* it was refused is that a single flat flag
33//! cannot express a rule the grammar states twice. So the second set is data
34//! too, as [`Keyword::JOIN_KEYWORDS`] and [`Keyword::may_be_name`], and the
35//! parser asks whichever question the position calls for.
36//!
37//! The two questions are not interchangeable and widening one into the other is
38//! a real regression rather than a harmless loosening. SQLite accepts `SELECT a
39//! AS left FROM t` and refuses `SELECT a left FROM t`; it refuses `SELECT *
40//! FROM t left` while accepting `SELECT * FROM t LEFT JOIN u ON ...`, and the
41//! bare-alias position is exactly what keeps a join keyword from swallowing its
42//! own join. And it accepts `CREATE TABLE t (left TEXT)` while refusing `CREATE
43//! TABLE t (a left)`, because the column's *name* and the type beside it take
44//! different classes - so asking the wide question in the type position trades
45//! one divergence from the pinned release for another.
46
47/// A SQL keyword recognised by the pinned release.
48#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
49pub struct Keyword(u8);
50
51/// Declares the keyword table: a constant per word, the lookup, and the name.
52macro_rules! keywords {
53    ($($index:expr => $constant:ident, $text:literal, $fallback:literal;)*) => {
54        impl Keyword {
55            $(
56                #[doc = concat!("The `", stringify!($constant), "` keyword.")]
57                pub const $constant: Keyword = Keyword($index);
58            )*
59
60            /// Returns the canonical upper-case spelling of the keyword.
61            pub fn text(self) -> &'static [u8] {
62                match self.0 {
63                    $($index => $text,)*
64                    _ => b"",
65                }
66            }
67
68            /// Returns whether the keyword may stand in for an identifier.
69            ///
70            /// SQLite declares a fallback set so that historical schemas using
71            /// words like `KEY` or `MATCH` as column names keep working. A word
72            /// outside the set is a hard keyword in every position.
73            pub fn may_fall_back(self) -> bool {
74                match self.0 {
75                    $($index => $fallback,)*
76                    _ => false,
77                }
78            }
79        }
80
81        /// Every keyword, in table order.
82        pub const KEYWORDS: &[Keyword] = &[$(Keyword::$constant),*];
83
84        /// Returns the keyword an ASCII-case-insensitive word names.
85        pub fn lookup(word: &[u8]) -> Option<Keyword> {
86            if word.len() > MAX_KEYWORD_LEN {
87                return None;
88            }
89            let mut upper = [0u8; MAX_KEYWORD_LEN];
90            let slot = upper.get_mut(..word.len())?;
91            for (target, byte) in slot.iter_mut().zip(word.iter()) {
92                *target = byte.to_ascii_uppercase();
93            }
94            let upper = upper.get(..word.len())?;
95            match upper {
96                $($text => Some(Keyword::$constant),)*
97                _ => None,
98            }
99        }
100    };
101}
102
103/// The longest keyword in the table, which bounds the case-folding buffer.
104pub const MAX_KEYWORD_LEN: usize = 17;
105
106keywords! {
107    0 => ABORT, b"ABORT", true;
108    1 => ACTION, b"ACTION", true;
109    2 => ADD, b"ADD", false;
110    3 => AFTER, b"AFTER", true;
111    4 => ALL, b"ALL", false;
112    5 => ALTER, b"ALTER", false;
113    6 => ALWAYS, b"ALWAYS", true;
114    7 => ANALYZE, b"ANALYZE", true;
115    8 => AND, b"AND", false;
116    9 => AS, b"AS", false;
117    10 => ASC, b"ASC", true;
118    11 => ATTACH, b"ATTACH", true;
119    12 => AUTOINCREMENT, b"AUTOINCREMENT", false;
120    13 => BEFORE, b"BEFORE", true;
121    14 => BEGIN, b"BEGIN", true;
122    15 => BETWEEN, b"BETWEEN", false;
123    16 => BY, b"BY", true;
124    17 => CASCADE, b"CASCADE", true;
125    18 => CASE, b"CASE", false;
126    19 => CAST, b"CAST", true;
127    20 => CHECK, b"CHECK", false;
128    21 => COLLATE, b"COLLATE", false;
129    22 => COLUMN, b"COLUMN", true;
130    23 => COMMIT, b"COMMIT", false;
131    24 => CONFLICT, b"CONFLICT", true;
132    25 => CONSTRAINT, b"CONSTRAINT", false;
133    26 => CREATE, b"CREATE", false;
134    27 => CROSS, b"CROSS", false;
135    28 => CURRENT, b"CURRENT", true;
136    29 => CURRENT_DATE, b"CURRENT_DATE", false;
137    30 => CURRENT_TIME, b"CURRENT_TIME", false;
138    31 => CURRENT_TIMESTAMP, b"CURRENT_TIMESTAMP", false;
139    32 => DATABASE, b"DATABASE", true;
140    33 => DEFAULT, b"DEFAULT", false;
141    34 => DEFERRABLE, b"DEFERRABLE", false;
142    35 => DEFERRED, b"DEFERRED", true;
143    36 => DELETE, b"DELETE", false;
144    37 => DESC, b"DESC", true;
145    38 => DETACH, b"DETACH", true;
146    39 => DISTINCT, b"DISTINCT", false;
147    40 => DO, b"DO", true;
148    41 => DROP, b"DROP", false;
149    42 => EACH, b"EACH", true;
150    43 => ELSE, b"ELSE", false;
151    44 => END, b"END", true;
152    45 => ESCAPE, b"ESCAPE", false;
153    46 => EXCEPT, b"EXCEPT", false;
154    47 => EXCLUDE, b"EXCLUDE", true;
155    48 => EXCLUSIVE, b"EXCLUSIVE", true;
156    49 => EXISTS, b"EXISTS", false;
157    50 => EXPLAIN, b"EXPLAIN", true;
158    51 => FAIL, b"FAIL", true;
159    52 => FILTER, b"FILTER", true;
160    53 => FIRST, b"FIRST", true;
161    54 => FOLLOWING, b"FOLLOWING", true;
162    55 => FOR, b"FOR", true;
163    56 => FOREIGN, b"FOREIGN", false;
164    57 => FROM, b"FROM", false;
165    58 => FULL, b"FULL", false;
166    59 => GENERATED, b"GENERATED", true;
167    60 => GLOB, b"GLOB", false;
168    61 => GROUP, b"GROUP", false;
169    62 => GROUPS, b"GROUPS", true;
170    63 => HAVING, b"HAVING", false;
171    64 => IF, b"IF", true;
172    65 => IGNORE, b"IGNORE", true;
173    66 => IMMEDIATE, b"IMMEDIATE", true;
174    67 => IN, b"IN", false;
175    68 => INDEX, b"INDEX", false;
176    69 => INDEXED, b"INDEXED", false;
177    70 => INITIALLY, b"INITIALLY", true;
178    71 => INNER, b"INNER", false;
179    72 => INSERT, b"INSERT", false;
180    73 => INSTEAD, b"INSTEAD", true;
181    74 => INTERSECT, b"INTERSECT", false;
182    75 => INTO, b"INTO", false;
183    76 => IS, b"IS", false;
184    77 => ISNULL, b"ISNULL", false;
185    78 => JOIN, b"JOIN", false;
186    79 => KEY, b"KEY", true;
187    80 => LAST, b"LAST", true;
188    81 => LEFT, b"LEFT", false;
189    82 => LIKE, b"LIKE", false;
190    83 => LIMIT, b"LIMIT", false;
191    84 => MATCH, b"MATCH", true;
192    85 => MATERIALIZED, b"MATERIALIZED", true;
193    86 => NATURAL, b"NATURAL", false;
194    87 => NO, b"NO", true;
195    88 => NOT, b"NOT", false;
196    89 => NOTHING, b"NOTHING", false;
197    90 => NOTNULL, b"NOTNULL", false;
198    91 => NULL, b"NULL", false;
199    92 => NULLS, b"NULLS", true;
200    93 => OF, b"OF", true;
201    94 => OFFSET, b"OFFSET", true;
202    95 => ON, b"ON", false;
203    96 => OR, b"OR", false;
204    97 => ORDER, b"ORDER", false;
205    98 => OTHERS, b"OTHERS", true;
206    99 => OUTER, b"OUTER", false;
207    100 => OVER, b"OVER", true;
208    101 => PARTITION, b"PARTITION", true;
209    102 => PLAN, b"PLAN", true;
210    103 => PRAGMA, b"PRAGMA", true;
211    104 => PRECEDING, b"PRECEDING", true;
212    105 => PRIMARY, b"PRIMARY", false;
213    106 => QUERY, b"QUERY", true;
214    107 => RAISE, b"RAISE", true;
215    108 => RANGE, b"RANGE", true;
216    109 => RECURSIVE, b"RECURSIVE", true;
217    110 => REFERENCES, b"REFERENCES", false;
218    111 => REGEXP, b"REGEXP", false;
219    112 => REINDEX, b"REINDEX", true;
220    113 => RELEASE, b"RELEASE", true;
221    114 => RENAME, b"RENAME", true;
222    115 => REPLACE, b"REPLACE", true;
223    116 => RESTRICT, b"RESTRICT", true;
224    117 => RETURNING, b"RETURNING", false;
225    118 => RIGHT, b"RIGHT", false;
226    119 => ROLLBACK, b"ROLLBACK", true;
227    120 => ROW, b"ROW", true;
228    121 => ROWS, b"ROWS", true;
229    122 => SAVEPOINT, b"SAVEPOINT", true;
230    123 => SELECT, b"SELECT", false;
231    124 => SET, b"SET", false;
232    125 => TABLE, b"TABLE", false;
233    126 => TEMP, b"TEMP", true;
234    127 => TEMPORARY, b"TEMPORARY", true;
235    128 => THEN, b"THEN", false;
236    129 => TIES, b"TIES", true;
237    130 => TO, b"TO", false;
238    131 => TRANSACTION, b"TRANSACTION", false;
239    132 => TRIGGER, b"TRIGGER", true;
240    133 => UNBOUNDED, b"UNBOUNDED", true;
241    134 => UNION, b"UNION", false;
242    135 => UNIQUE, b"UNIQUE", false;
243    136 => UPDATE, b"UPDATE", false;
244    137 => USING, b"USING", false;
245    138 => VACUUM, b"VACUUM", true;
246    139 => VALUES, b"VALUES", false;
247    140 => VIEW, b"VIEW", true;
248    141 => VIRTUAL, b"VIRTUAL", true;
249    142 => WHEN, b"WHEN", false;
250    143 => WHERE, b"WHERE", false;
251    144 => WINDOW, b"WINDOW", true;
252    145 => WITH, b"WITH", true;
253    146 => WITHOUT, b"WITHOUT", true;
254}
255
256impl Keyword {
257    /// Returns the canonical spelling as text, for diagnostics.
258    pub fn as_str(self) -> &'static str {
259        core::str::from_utf8(self.text()).unwrap_or("")
260    }
261
262    /// SQLite's `JOIN_KW` token class: the words that introduce a join.
263    ///
264    /// They are hard keywords in the sense that matters to `may_fall_back` -
265    /// none appears in the `%fallback` declaration - and they are still legal
266    /// names, because the name production accepts the token class directly.
267    /// Both facts are true at once and the grammar states them separately.
268    pub const JOIN_KEYWORDS: &'static [Keyword] = &[
269        Keyword::CROSS,
270        Keyword::FULL,
271        Keyword::INNER,
272        Keyword::LEFT,
273        Keyword::NATURAL,
274        Keyword::OUTER,
275        Keyword::RIGHT,
276    ];
277
278    /// Returns whether the keyword introduces a join.
279    pub fn is_join_keyword(self) -> bool {
280        Keyword::JOIN_KEYWORDS.contains(&self)
281    }
282
283    /// Returns whether the keyword may be written where a **name** is expected.
284    ///
285    /// This is SQLite's `nm ::= idj | STRING` with `idj ::= ID|INDEXED|JOIN_KW`,
286    /// so it is the fallback set plus the seven join keywords plus `INDEXED`.
287    /// It is the right question for a column declaration, a table or index
288    /// name, a qualified reference, a window name, and an alias written with
289    /// `AS`.
290    ///
291    /// It is the **wrong** question for the two positions that take `ids` - a
292    /// bare alias and a declared type name - which take only
293    /// [`Keyword::may_fall_back`]; see this module's header for why the two
294    /// cannot be merged.
295    pub fn may_be_name(self) -> bool {
296        self.may_fall_back() || self.is_join_keyword() || self == Keyword::INDEXED
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    /// The pinned release documents 147 keywords; a table that has drifted from
305    /// that count is either missing a word or has invented one.
306    #[test]
307    fn the_table_holds_every_documented_keyword() {
308        assert_eq!(KEYWORDS.len(), 147);
309    }
310
311    /// Lookup is ASCII-case-insensitive, which is the whole reason it exists.
312    #[test]
313    fn lookup_ignores_case() {
314        assert_eq!(lookup(b"select"), Some(Keyword::SELECT));
315        assert_eq!(lookup(b"SeLeCt"), Some(Keyword::SELECT));
316        assert_eq!(lookup(b"SELECT"), Some(Keyword::SELECT));
317        assert_eq!(lookup(b"selectx"), None);
318    }
319
320    /// A word longer than the longest keyword cannot be one, and must not
321    /// overrun the fold buffer while proving it.
322    #[test]
323    fn an_overlong_word_is_not_a_keyword() {
324        assert_eq!(lookup(&[b'A'; 512]), None);
325    }
326
327    /// Every constant must round-trip through its own text.
328    #[test]
329    fn every_keyword_round_trips() {
330        for keyword in KEYWORDS {
331            let text = keyword.text();
332            assert_eq!(lookup(text), Some(*keyword), "{}", keyword.as_str());
333        }
334    }
335
336    /// The fallback set is the compatibility rule that lets old schemas keep
337    /// working. `KEY` must be usable as a name and `SELECT` must not.
338    #[test]
339    fn the_fallback_set_matches_the_grammar() {
340        assert!(Keyword::KEY.may_fall_back());
341        assert!(Keyword::MATCH.may_fall_back());
342        assert!(Keyword::ROWS.may_fall_back());
343        assert!(!Keyword::SELECT.may_fall_back());
344        assert!(!Keyword::FROM.may_fall_back());
345        assert!(!Keyword::WHERE.may_fall_back());
346    }
347
348    /// A join keyword is outside the fallback set and is still a name, which is
349    /// the whole reason the two questions are separate.
350    #[test]
351    fn a_join_keyword_is_a_name_without_being_a_fallback() {
352        for keyword in Keyword::JOIN_KEYWORDS {
353            assert!(!keyword.may_fall_back(), "{}", keyword.as_str());
354            assert!(keyword.may_be_name(), "{}", keyword.as_str());
355        }
356        assert!(!Keyword::INDEXED.may_fall_back());
357        assert!(Keyword::INDEXED.may_be_name());
358    }
359
360    /// The name set is the fallback set plus exactly eight words. Pinned as a
361    /// count so that widening either set is a visible edit rather than a
362    /// silent one - `may_fall_back` is the transcription of `%fallback` and
363    /// must not drift to mean "may be a name".
364    #[test]
365    fn the_name_set_is_the_fallback_set_plus_the_token_class() {
366        let extra: Vec<&str> = KEYWORDS
367            .iter()
368            .filter(|keyword| keyword.may_be_name() && !keyword.may_fall_back())
369            .map(|keyword| keyword.as_str())
370            .collect();
371        assert_eq!(
372            extra,
373            vec!["CROSS", "FULL", "INDEXED", "INNER", "LEFT", "NATURAL", "OUTER", "RIGHT"]
374        );
375        for keyword in KEYWORDS {
376            if keyword.may_fall_back() {
377                assert!(keyword.may_be_name(), "{}", keyword.as_str());
378            }
379        }
380    }
381
382    /// A hard keyword is a name in neither position.
383    #[test]
384    fn a_hard_keyword_is_never_a_name() {
385        assert!(!Keyword::SELECT.may_be_name());
386        assert!(!Keyword::FROM.may_be_name());
387        assert!(!Keyword::JOIN.may_be_name());
388        assert!(!Keyword::ON.may_be_name());
389    }
390}