Read a line of SQL one character at a time and cut it into typed tokens — keywords, identifiers, numbers, strings and symbols — in the order they were typed, or reject it with one error that says where it went wrong.
Up to now a line was either a meta command or something the program could not recognise. From here on, the rest is SQL, and the first thing to do with SQL is not to understand it but to cut it up.
A line arrives as characters. select name from users where id=1; is thirty-four of them, and nothing in them says where one word stops and the next begins: sometimes a space does, sometimes an = does, and inside quotes a space does not. The parser in the next stage wants to ask questions like "is the next thing the keyword FROM?" and "is this a name?". If it had to answer them by looking at characters, every rule of the grammar would also have to know about spaces, capitals, quotes and digits. The lexer answers all of those questions once, at the door, and hands on a list of tokens: small records of a type and the text it was made from.
There is a second reason, and it shapes the error messages. The lexer is the last part of the program that sees the line as characters, so it is the one that can say where something went wrong. A parser can say "unexpected token"; only the lexer can say "column 17".
The scanner is a small state machine. It starts between tokens; the first character of the next token picks a state; the state decides which characters still belong; the first one that does not ends the token and sends the scanner back to the start, without consuming that character.
Three things in the picture carry the whole stage. A token ends when the next character does not belong to it, not when a space appears, which is why id=1 is three tokens. The character that ended a word or a number is looked at again from Between, which is why nothing is lost between two tokens. And a string is the one state where whitespace, capitals and symbols stop meaning anything: 'Select * From' is one token.
Here is update users set name = '' where id=1; going through it:
| Characters | State that read them | Token |
|---|---|---|
update | Word | [KEYWORD_UPDATE - update] |
users | Word | [IDENTIFIER - users] |
set | Word | [KEYWORD_SET - set] |
name | Word | [IDENTIFIER - name] |
= | Between | [SYMBOL - =] |
'' | String | [STRING - ] |
where | Word | [KEYWORD_WHERE - where] |
id | Word, ended by = | [IDENTIFIER - id] |
= | Between | [SYMBOL - =] |
1 | Number, ended by ; | [NUMBER - 1] |
; | Between | [SYMBOL - ;] |
A word is classified only once it has been read to its end. selectivity begins with the letters of select, and a scanner that checks for keywords as it goes will emit a keyword and then an identifier ivity. Read the word, then compare the whole of it — ignoring case — with the nine keywords of this part. If it is one of them it is that keyword; if not, it is an identifier.
The lexer does not decide whether an identifier makes sense. users is a table, id is a column, and is neither in this part — all three are identifiers here. Whether they belong where they stand is the parser's question, and whether the table exists is asked even later.
The scanner stands at a position in the line and repeats one decision until the line runs out. It never goes back, and it never looks at meaning: users from * select is tokenized as happily as a real query.
_ starts a word, a digit starts a number, a single quote starts a string, one of the eight contract symbols is a symbol by itself.The start is decided by one character; the end is decided by the kind. Each rule is 'keep going while the next character still belongs'.
_. A symbol, a quote, whitespace or the end of the line ends it — no space is needed, so select*from is three tokens.Only once a word has been read to its end do you ask what it is.
KEYWORD_ followed by the keyword in capitals.selectivity or fromage, is an identifier, because the comparison is with the whole word.The tokenize debug command prints the list, or the error, and nothing in between.
tokenize included, starting at 1.These are the exact lines the tests check. The full definition is on the output contract page, sections 3 and 4.
tokenize <sql> prints one line per token, [<TYPE> - <text>], in the order the tokens appear in the line:
| Type | What it matches | Example |
|---|---|---|
KEYWORD_<WORD> | one of select insert into values from where update set delete, any case | [KEYWORD_SELECT - SELECT] |
IDENTIFIER | a letter or _, then letters, digits and _ | [IDENTIFIER - user_1] |
NUMBER | one or more digits, nothing else | [NUMBER - 42] |
STRING | '...' on one line, no escape sequences; the text is what is between the quotes | [STRING - Dan Mar] |
SYMBOL | exactly one of , ( ) ; = * < > | [SYMBOL - <] |
The text is always the token as typed. A keyword keeps its case: the type is in capitals, the text is not rewritten. A string's text leaves the quotes out, so the empty string prints as [STRING - ].
Some edges, because the tests visit them:
1.5 or -1 as a number, that is fine; if it reports 00202 at the . or the -, that is fine too. Decide what 1.2.3 means before you accept dots at all.1abc is [NUMBER - 1] then [IDENTIFIER - abc]. A number ends at the first character that is not a digit.'it''s' is the two strings it and s.tokenize with nothing after it prints nothing at all.Two conditions are errors, each with its own code. An error is one line and a lexer error ends with at col N, the 1-based column counted from the start of the line — tokenize included:
| Condition | Output | The column points at |
|---|---|---|
| a string with no closing quote | [ERROR:00201] <message> at col N | the opening quote |
| a character no token can start with | [ERROR:00202] <message> at col N | the character itself |
The message between the code and at col is yours to word. A failed tokenize prints the error line and no tokens, not even those it read before the error, and the session carries on.
Welcome to droid!>tokenize select * from users where id = 1;[KEYWORD_SELECT - select][SYMBOL - *][KEYWORD_FROM - from][IDENTIFIER - users][KEYWORD_WHERE - where][IDENTIFIER - id][SYMBOL - =][NUMBER - 1][SYMBOL - ;]>tokenize UPDATE users SET name = 'Dan Mar' WHERE id=1;[KEYWORD_UPDATE - UPDATE][IDENTIFIER - users][KEYWORD_SET - SET][IDENTIFIER - name][SYMBOL - =][STRING - Dan Mar][KEYWORD_WHERE - WHERE][IDENTIFIER - id][SYMBOL - =][NUMBER - 1][SYMBOL - ;]>tokenize select selectivity from users;[KEYWORD_SELECT - select][IDENTIFIER - selectivity][KEYWORD_FROM - from][IDENTIFIER - users][SYMBOL - ;]>tokenize insert into users values (1, 'danimar);[ERROR:00201] Unterminated string literal at col 39>tokenize select # from users;[ERROR:00202] Unexpected character: # at col 17>.exitexiting.. good bye!
Things to notice. UPDATE and SET kept their capitals, and id=1 came apart without a space. selectivity stayed one identifier. The unterminated string is reported at column 39, where its quote opened, and the first three tokens of that line were never printed. And two errors in a row did not end the session.
selectivity comes out as [KEYWORD_SELECT - select] and [IDENTIFIER - ivity]. Read the whole word, then compare.UPDATE into update. Compare a copy, print the original.id=1, the = ends the identifier. If the scanner steps past it while finishing id, the = never becomes a token. The character that ends a word or a number is the start of whatever comes next.select*from one unknown word. Spaces are one way a token ends, not the only one.'' must print [STRING - ], with the space after the dash.tokenize included, and the first character is column 1. For an unterminated string it is the column of the opening quote, not the end of the line.The page above is the same for every language. These are the only language-specific hints.
isalpha, isdigit and isspace take an int that must be representable as unsigned char; cast a plain char before passing it. strncasecmp compares without case, but compare lengths first, or select will match the start of selectivity.std::string_view lets a token point into the line without copying; the line has to outlive the list. The same unsigned char cast applies to std::isalpha.char_indices() or bytes rather than indexing a &str by position. eq_ignore_ascii_case compares a word with a keyword without allocating a lowercased copy.std.ascii.isAlphabetic, std.ascii.isDigit and std.ascii.eqlIgnoreCase cover the whole character check. Slices into the line need no allocation, as long as the line lives as long as the tokens.