Read the token list from the lexer against a small grammar, and either produce a statement — its kind, its table, its columns, its values and its WHERE clause — or reject the line with one error that says what was expected and where.
The lexer turned a line into words. It did not care what order they came in: users from * select; was five perfectly good tokens. Now the order matters. The parser answers two questions about the token list: is this a statement? and, if it is, what does it say?
The second answer is the point. Everything after this stage — executing an INSERT, choosing a plan, walking the B+Tree — wants to know "which table, which columns, which values, which condition", and none of it wants to know that the values were in parentheses or that the keyword was typed in capitals. The parser produces that answer once, as a small structure called the abstract syntax tree: abstract because the punctuation is gone, a tree because a statement has parts and some parts have parts of their own.
The first answer is what makes the second safe. If the parser only produces a statement when the whole line fits the grammar, every later stage can trust what it is given, and none of them needs its own checks for a missing parenthesis.
This is every statement the part accepts. Words in capitals are keywords, t and c are names, op is a comparison, v is a value.
INSERT INTO t (c, ...) VALUES (v, ...);
SELECT * | c, ... FROM t [WHERE c op v];
UPDATE t SET c = v, ... [WHERE c op v];
DELETE FROM t [WHERE c op v];
op is =, < or >
v is a NUMBER or a STRING
A few things are worth reading twice. INSERT always names its columns. , ... means one or more, never zero. The square brackets mean the WHERE clause is optional, and it is the same clause in all three statements that have it. Its comparison is always one of three symbols; the parser only records which one — comparing values is the job of whoever runs the statement. Note that SET always uses =: it assigns, it does not compare. And every statement ends with ;.
Each statement is a path through its rule. Here is SELECT, with the choices the parser makes by looking one token ahead:
Every arrow is one token the parser expects. A token that matches no arrow out of the current state is a syntax error at that token: select from users; has no arrow for from out of SelectList, so the error points at from: column 12 in ast select from users;, the ast prefix counted in.
Two arrows say not taken. The token that ends a list, or that shows there is no WHERE, is looked at but left in place for the next step to take. That is what one token of lookahead means.
The WHERE part of this picture is shared. UPDATE and DELETE reach Clause by their own path and leave it by the same one, which is why the clause is best written once.
The parser walks the token list once, left to right, with one position that only moves forward. At each step it either accepts the token it expects and moves on, or stops with an error.
* or a list of names, WHERE or the end — look at the next token to choose. Never take a token you have not decided to keep.;. After it, the line must be over.Column lists, value lists and SET assignments are all the same shape: one item, then as many times as you like a comma and another item.
SELECT, UPDATE and DELETE all end the same way: an optional WHERE clause, then ;. Write the clause once and have all three use it.
=, < or > — then a value: a number or a string.The grammar is all this stage requires. One more check needs nothing but the statement itself.
) that closes the value list.Four steps, each reusing what the one before it built. Get one working end to end, with its ast output, before starting the next.
* or a list.c = v instead of a single name.These are the exact lines the tests check. The full definition is on the output contract page, sections 3 and 5.
ast <sql> prints up to five lines, always in this order:
| Line | When | Holds |
|---|---|---|
Statement: INSERT | always | INSERT, SELECT, UPDATE or DELETE |
Table: users | always | the table name as typed |
Columns: [id, name] | INSERT, SELECT, UPDATE | INSERT: the column list. SELECT: the select list, [*] for *. UPDATE: the assigned columns |
Values: [1, 'dan'] | INSERT, UPDATE | the values in the same positions as their columns |
Where: id < 5 | only with a WHERE clause | column, operator (=, < or >) and value |
A line is absent when the statement has no such part. Lists are separated by a comma and a space, inside square brackets. Strings are printed in single quotes and numbers as their digits, in Values: and in Where:. Names are printed as typed, and nothing is reordered.
Three codes belong to the parser. Like the lexer's, each error is one line ending with at col N, where N is the first character of the token the parser could not accept, counted from the start of the line with ast included; if the line ran out, it is one past the last character.
) that closes the value listThe message is yours to word. A failed parse prints the error line and no AST. A line the lexer rejects prints the lexer's error, with its code, and the parser does not run.
Welcome to droid!>ast insert into users (email, id) values ('bob@example.com', 7);Statement: INSERTTable: usersColumns: [email, id]Values: ['bob@example.com', 7]>ast select name, email from users where name = 'alice';Statement: SELECTTable: usersColumns: [name, email]Where: name = 'alice'>ast update users set name = 'a', email = 'b' where id = 3;Statement: UPDATETable: usersColumns: [name, email]Values: ['a', 'b']Where: id = 3>ast delete from users;Statement: DELETETable: users>ast launch from users;[ERROR:00301] Unrecognized statement: launch at col 5>ast select from users;[ERROR:00302] Expected column list at col 12>ast insert into users (id, name, email) values (1, 'danimar');[ERROR:00303] Column count and value count must match at col 61>ast select # from users;[ERROR:00202] Unexpected character: # at col 12>.exitexiting.. good bye!
Things to notice. The INSERT kept email before id, because that is how it was typed. The DELETE printed two lines and nothing else — no empty Where:. The error for select from users; points at from, the token that was there instead of a column. And the last line never reached the parser: the lexer stopped it at #.
select * from users and prints an AST without ever checking for ;. A statement ends at ;, and nothing may follow it.Where: with nothing after it for a statement with no clause. The line is absent, not empty.;, or the first mismatched value, or not at all. It is 00303, at the ) that closes the value list.1 and '1' must print differently: the number bare, the string in quotes. Keep whether each value was a NUMBER or a STRING token.ast in.The page above is the same for every language. These are the only language-specific hints.
realloc or give them a fixed maximum and report an error beyond it, never write past the end. Decide who frees the token list and the statement, and free both on the error path too.std::variant or a small struct with a kind field holds a value that is either a number or a string. std::vector for the lists.enum for the statement kinds and another for values makes the "no Where line" rule an Option. Result carries the error, code and column, out of every rule, and ? passes it up.union(enum) for values and statements, and an error union for the rules. An ArrayList for each list; pass the allocator in and free on every path.