Parse IN and BETWEEN, with and without NOT, and rewrite each into the comparisons, AND and OR it stands for, so that printing, checking, three-valued logic and key ranges all work on it unchanged.
where age >= 30 and age <= 45 and where dept = 'eng' or dept = 'hr' or dept = 'ops' already work. People write them all the time, and SQL has a shorter way to say each: age between 30 and 45 and dept in ('eng', 'hr', 'ops').
Neither one needs anything new below the parser. A BETWEEN is two comparisons and an AND; an IN is a chain of equalities and ORs. So this stage teaches a technique as much as a feature: rewriting. The parser reads the short form and builds the tree of the long one. Everything after it (the checks, the evaluator, the NULL rules of the previous stage and the key bounds of the planner) sees an ordinary expression and works unchanged.
The operand appears once in what was typed and once per comparison in the tree, so it is copied. a + 1 between 2 and 3 becomes ((a + 1) >= 2) AND ((a + 1) <= 3). ast prints the rewritten tree, by the printing rules of the expressions stage, so you can see exactly what your parser built.
Where the predicate sits in the grammar matters. It stands at the level of the comparisons, one per level, as IS NULL does: arithmetic binds tighter, so a + 5 between 20 and 35 asks about a + 5, and NOT is looser, so not a in (10, 20) negates the whole IN. The AND inside a BETWEEN belongs to it: a between 10 and 30 and c = 1 is the BETWEEN, then AND c = 1. That is why the low and high bounds are parsed at the arithmetic level and not as full expressions, which would swallow the second AND.
Both ends are included, because the comparisons are >= and <=. And between 50 and 20 keeps nothing: no value is both at least 50 and at most 20. Some engines swap reversed ends; SQL does not, and neither does droid.
NULLs follow the previous stage's tables. A NULL operand makes every comparison unknown, so neither c between 1 and 2 nor c not between 1 and 2 keeps that row. The surprising case is a NULL in an IN list:
c | c in (1, NULL) | c not in (1, NULL) |
|---|---|---|
| 1 | true OR unknown = true | NOT true = false |
| 2 | false OR unknown = unknown | NOT unknown = unknown |
| NULL | unknown | unknown |
c not in (1, NULL) keeps no row at all. It is one of the best-known traps in SQL: a NOT IN over a list that happens to hold a NULL silently returns nothing. With a rewrite you do not have to remember it; it falls out of the OR and NOT tables you already built.
The key range is found by the planner you already have. A BETWEEN on the table's first column becomes two conjuncts, id >= 2 and id <= 3, and each is a key bound, so the plan is a range IndexScan. An IN is an OR, and an OR is never a key bound, so it stays a Filter over a SeqScan:
IN and BETWEEN sit at the level of the comparisons, where IS NULL already is. After the left operand, look at the next word.
Build the tree the long form would have built, and hand it on. Nothing after the parser needs to know the shorthand existed.
These are the exact lines the tests check. The full definition is on the output contract page, sections 40 to 42.
| Input | Output |
|---|---|
tokenize a not between 1 and 2 | [IDENTIFIER - a], [KEYWORD_NOT - not], [KEYWORD_BETWEEN - between], [NUMBER - 1], [KEYWORD_AND - and], [NUMBER - 2] |
tokenize id in (1, 2) | [IDENTIFIER - id], [KEYWORD_IN - in], [SYMBOL - (], [NUMBER - 1], [SYMBOL - ,], [NUMBER - 2], [SYMBOL - )] |
ast select * from t where a between 1 and 5; | Where: (a >= 1) AND (a <= 5) |
ast select * from t where a not between 1 and 5; | Where: NOT ((a >= 1) AND (a <= 5)) |
ast select * from t where a in (1, 2, 3); | Where: ((a = 1) OR (a = 2)) OR (a = 3) |
ast select * from t where a in (7); | Where: a = 7 |
ast select * from t where a not in (1, 2); | Where: NOT ((a = 1) OR (a = 2)) |
ast select * from t where a + 1 between 2 and 3; | Where: ((a + 1) >= 2) AND ((a + 1) <= 3) |
explain select * from t where id between 3 and 5; | [PLAN] └── IndexScan (table=t, range: id >= 3 AND id <= 5) |
explain select * from t where id in (2, 4); | [PLAN] └── Filter ((id = 2) OR (id = 4)) over [PLAN] └── SeqScan (table=t) |
in (), in 1, in (1,), between 1, between 1 and, a not = 1, a in (1) = c | [ERROR:00302] <message> |
| an unknown column; mixed types; a number past 4294967295 | [ERROR:00402], [ERROR:00403], [ERROR:00404] <message>, as for the comparisons |
in and between are keywords. There is no new error code: every mistake is a syntax error or the error the rewritten comparisons would give.
$droid --db people.dbWelcome to droid!>create table p (id int, name varchar(20), dept varchar(10), age int);Create table statement executed successfully>insert into p (id, name, dept, age) values (1, 'ada', 'eng', 36);Insert statement executed successfully (1 rows)>insert into p (id, name, dept, age) values (2, 'bob', 'ops', 41);Insert statement executed successfully (1 rows)>insert into p (id, name, age) values (3, 'cy', 29);Insert statement executed successfully (1 rows)>insert into p (id, name, dept, age) values (4, 'dan', 'hr', 52);Insert statement executed successfully (1 rows)>select name from p where age between 30 and 45;nameadabob(2 rows)[EXEC] rows_examined=4>select name from p where dept in ('eng', 'hr');nameadadan(2 rows)[EXEC] rows_examined=4>select name from p where dept not in ('eng', NULL);name(0 rows)[EXEC] rows_examined=4>ast select * from p where dept not in ('eng', 'hr');Statement: SELECTTable: pColumns: [*]Where: NOT ((dept = 'eng') OR (dept = 'hr'))>explain select name from p where id between 2 and 3;[PLAN] select name from p where id between 2 and 3[PLAN] └── IndexScan (table=p, range: id >= 2 AND id <= 3)>.exitexiting.. good bye!
Cy's department is NULL, so the IN leaves that row out, and so would any NOT IN. The NOT IN with a NULL in its list keeps nobody, not even bob, whose department is in neither. The messages are yours to word; the tests check only the codes.
2 and c = 1 as one operand, and the type check fails. Parse the bounds at the arithmetic level.> and < instead of >= and <= drop the rows on the boundary.between 50 and 20 must keep nothing.!= against each item and ignoring unknowns makes c not in (1, NULL) keep every row but 1. Build the OR and the NOT and the tables do the rest.a not in (...) and not a in (...) mean the same; a not = 1 is a syntax error, not NOT (a = 1).The page above is the same for every language. These are the only language-specific hints.
std::unique_ptr tree cannot share a child; a clone() method on the node type makes the copies explicit.Clone on the expression enum, Box children included, and call .clone() on the operand for each comparison.