The Project Operator and Aliases
Compute and name the columns a query returns, as an operator of its own
Parse a select list of expressions with optional AS aliases, give each output column its header, evaluate the items for every row the WHERE keeps, and show the work as a Project node at the top of the plan.
- Projection: choosing and computing the output columns is an operator like the scan and the filter
- An item is any expression, so the evaluator of the expressions stage computes it
- Headers: an alias when there is one, otherwise the expression as ast prints it
- An alias names the output, not the table: the WHERE runs before it exists
- Project sits on top: it sees only the rows the Filter let through
Why this stage exists
select name, id from users; has worked since The Core Engine, but only because SELECT picked columns out of a row by name. A query's output is more than a choice of columns: select price * qty as total, select -balance, select age > 30. Each output column is computed, and each needs a name to print in the header. The next stages need it too: aggregates such as count(*) live in the select list, and ORDER BY and GROUP BY talk about what it computes.
So the select list becomes a list of expressions, and computing it becomes an operator of its own: Project. It sits at the top of the plan, above the scan and the filter, and turns each row they hand up into the row the user sees.
Where Project sits
The order of the operators is the order of the work. The scan reads rows, the Filter keeps the ones the WHERE allows, and Project computes the output from what is left:
>explain select name, id * 2 as dbl from t where a = 6;[PLAN] select name, id * 2 as dbl from t where a = 6[PLAN] └── Project (cols=[name, id * 2 AS dbl])[PLAN] └── Filter (a = 6)[PLAN] └── SeqScan (table=t)
Two things follow from that order. First, an item is only ever evaluated for a row the WHERE kept, so select a / b from t where b != 0; never divides by zero. Second, an alias does not exist yet when the WHERE runs. select id * 2 as dbl from t where dbl > 2; asks the Filter for a column the table does not have, and is 00402. If the alias is also the name of a real column, the WHERE means the real column. Real databases agree on this: the select list is computed last, even though it is written first.
select * keeps its old plan, with no Project node: every column, unchanged, in table order, is what the scan already hands up.
Naming an output column
Every column of the result needs a header. There are three cases:
| Item | Header |
|---|---|
id * 2 as dbl | dbl, the alias as typed |
name | name, the column as typed, as before |
a + b * 2 | a + (b * 2), the item printed as ast prints it |
Using the printed form for an unnamed item means every header is defined by a rule you have already built, with no special case. The parentheses show the tree, so the header is also a small reminder of how the item was parsed.
Values of any type
An item can be any expression, so it can have any type the evaluator knows. An INT prints its digits, now possibly with a minus sign, since -a and a - 10 can go below zero. A VARCHAR prints as stored and a NULL as NULL. A comparison is a truth value, and prints true or false, or NULL when it is unknown: select c > 1 from t; on a row whose c is NULL prints NULL.
A literal is an item too. select 1, 'x', id from t; prints 1 and x on every row, under the headers 1 and 'x'.
The algorithm in words
Parse the select list
The select list was a list of names. Now each entry is a full expression, so the parser reads it with the expression parser and then looks for an alias.
- 1.If the next token is *, the select list is * and the next token must be FROM.
- 2.Otherwise read an expression, starting at the loosest level, OR.
- 3.If AS follows, the next token must be a name: it is the item's alias.
- 4.A comma means another item follows; FROM ends the list. Anything else is 00302.
- 5.Invariant: the number of items, their order and their aliases are exactly as typed.
Run the Project operator
Project is a Volcano operator like the others: asked for its next row, it asks its child, computes, and hands up a new row.
- 1.Before any row: check the select list and the WHERE in the contract's order (names, then types, then number ranges), and work out each header: the alias, or the item printed as ast prints it.
- 2.Ask the child for its next row: the Filter, or the scan when there is no WHERE.
- 3.When the child has no more rows, neither does Project.
- 4.Otherwise evaluate each item on that row, from the left, and hand up the list of results as the output row.
- 5.An error while evaluating stops the statement, and nothing is printed but the error.
- 6.Invariant: Project never reads a row the WHERE dropped, and examines nothing itself: rows_examined is the scan's.
Output contract for this stage
These are the exact lines the tests check. The full definition is on the output contract page, sections 40 to 43.
| Input | Output |
|---|---|
select id, id * 2 as dbl from t; | id | dbl, then 1 | 2 and so on |
select id * 2, a + b * 2, -a from t; | the header id * 2 | a + (b * 2) | -a |
select id, a = 6 as six, c > 1 from t; | 1 | true | NULL when c is NULL |
tokenize id as n | [IDENTIFIER - id], [KEYWORD_AS - as], [IDENTIFIER - n] |
ast select id * 2 as dbl, name from t; | Columns: [id * 2 AS dbl, name] |
explain select id from t where id = 2; | [PLAN] └── Project (cols=[id]) above [PLAN] └── IndexScan (table=t, key=2) |
| an alias used in the WHERE, a column the table does not have | [ERROR:00402] <message> |
select *, id from t;, select id as from t; | [ERROR:00302] <message> |
.mode csv, then select 'a,b' from t where id = 1; | "'a,b'", then "a,b" |
The checks run before any row is read: every column of the select list then of the WHERE (00402), then the operators of the select list then of the WHERE (00403), then the numbers of both (00404). rows_examined counts what the scan reads, exactly as before; Project adds nothing to it. In CSV mode the header line is the same headers separated by ,, each quoted like any field: select 'a,b' from t; has the header "'a,b'".
What you should see when done
$droid --db shop.dbWelcome to droid!>create table items (id int, price int, qty int, name varchar(20));Create table statement executed successfully>insert into items (id, price, qty, name) values (1, 250, 4, 'lamp');Insert statement executed successfully (1 rows)>insert into items (id, price, qty, name) values (2, 40, 0, 'cable');Insert statement executed successfully (1 rows)>insert into items (id, price, name) values (3, 900, 'desk');Insert statement executed successfully (1 rows)>select name, price * qty as total, qty > 0 from items;name | total | qty > 0lamp | 1000 | truecable | 0 | falsedesk | NULL | NULL(3 rows)[EXEC] rows_examined=3>select price / qty from items;[ERROR:01501] Division by zero>select name, price / qty as each from items where qty > 0;name | eachlamp | 62(1 rows)[EXEC] rows_examined=3>select name from items where total > 100;[ERROR:00402] Column total does not exist>explain select name, price - 50 from items where id >= 2;[PLAN] select name, price - 50 from items where id >= 2[PLAN] └── Project (cols=[name, price - 50])[PLAN] └── IndexScan (table=items, range: id >= 2)>.exitexiting.. good bye!
The desk's quantity was left out, so it is NULL, and so is everything computed from it. Dividing by the cable's zero stops the second query, while the third never reaches that row, because the WHERE drops it before Project computes anything. The messages are yours to word; the tests check only the codes.
Common bugs in this stage
- Projecting before filtering. Computing the items for every row, then filtering, divides by zero on rows the WHERE would have dropped.
- Headers as typed. Echoing the item's text gives
a+b*2ora + b * 2; the header is the printed tree,a + (b * 2). - Aliases visible to the WHERE. Resolving WHERE columns against the select list makes
where dbl > 2work; it must be00402. - A Project node for
*.select *keeps the plan it had. - Truth values printed as 1 and 0. A comparison prints
trueorfalse, and unknown printsNULL. - Negative numbers printed unsigned. An
INTcolumn never goes below 0, but an item can. Print the signed value. - Checking the WHERE first. An unknown column in the select list must be reported before one in the WHERE.
Language notes
The page above is the same for every language. These are the only language-specific hints.
- C and C++: keep the items as an array of expression pointers with an optional alias each; an output row is an array of the value structs from the evaluator. Print an
int64_twithPRId64from<inttypes.h>. - Rust:
Vec<(Expr, Option<String>)>for the items; implementingDisplayfor your expression type gives youast, the plan line and the header from one function. - Zig: a slice of items, each an expression and an optional alias (
?[]const u8); aformatfunction on the expression type servesast,explainand the header alike.
Checklist
One line per thing the tests check. The tags name the test cases.
- An item can be an expression, named by ASexpr-column
- An unnamed expression's header is the expression, printed as ast prints itunnamed-expr-header
- Items come in the order written, and a column may come twicecol-subset-order
- A literal is an item with the same value on every rowliteral-items
- An item over a NULL is NULLnull-arith
- A comparison as an item prints true, false, or NULL for unknowntruth-items
- The WHERE may use columns the select list does notwhere-on-unprojected-col
- An alias names an output column; the WHERE cannot read italias-not-in-where
- Names, then types, then number ranges, the select list before the WHEREcheck-order
- An item that divides by zero fails the statement; a row the WHERE drops is never projectedruntime-errors
- * stands alone, and AS needs a namesyntax-errors
- CSV mode prints aliases and expressions the same waycsv-headers
- A select list that is not * is a Project node at the top of the planexplain-project
- as is a keyword; ast prints each item with its aliastokenize-ast-select-list