myTinySQL v0.2.0-2-g5000098
A tiny SQL interpreter over fixed-width text files
Loading...
Searching...
No Matches
Parser.cpp
Go to the documentation of this file.
1
20
21#include <format>
22#include <string_view>
23#include <utility>
24
25#include "Statement.hpp"
26#include "Util.hpp"
27
28namespace tinysql {
29
30namespace {
31
33enum class TokenKind {
34 Word,
35 String,
36 Symbol,
37 End,
38};
39
41struct Token {
43 std::string text;
44};
45
47constexpr std::string_view kDelimiters = " \t\r\n(),*;@=<>!'";
48
52bool isWordChar(char c)
53{
54 const auto byte = static_cast<unsigned char>(c);
55 return (byte >= 'A' && byte <= 'Z') || (byte >= 'a' && byte <= 'z') || (byte >= '0' && byte <= '9')
56 || byte >= 0x80;
57}
58
67bool isApostrophe(std::string_view text, std::size_t i)
68{
69 return i > 0 && i + 1 < text.size() && text[i] == '\''
70 && isWordChar(text[i - 1]) && isWordChar(text[i + 1]);
71}
72
77std::vector<Token> tokenize(std::string_view sql)
78{
79 std::vector<Token> tokens;
80 std::size_t i = 0;
81 while (i < sql.size()) {
82 const char c = sql[i];
83 if (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
84 ++i;
85 } else if (c == '\'') {
86 // 'quoted string', with '' standing for one quote; an apostrophe (d'Asti) does not end it
87 std::string text;
88 const bool afterWord = i > 0 && isWordChar(sql[i - 1]); // e.g. Foscari' in 'Ca' Foscari'
89 ++i;
90 while (true) {
91 if (i >= sql.size()) {
92 throw SqlError(afterWord ? "unterminated quoted string (an apostrophe at the end of a word, "
93 "as in Ca' Foscari, is written twice inside quotes: 'Ca'' Foscari')"
94 : "unterminated quoted string");
95 }
96 if (sql[i] == '\'') {
97 if (i + 1 < sql.size() && sql[i + 1] == '\'') {
98 text += '\'';
99 i += 2;
100 continue;
101 }
102 if (isApostrophe(sql, i)) {
103 text += sql[i++];
104 continue;
105 }
106 ++i;
107 break;
108 }
109 text += sql[i++];
110 }
111 tokens.push_back({TokenKind::String, std::move(text)});
112 } else if (std::string_view("(),*;@").find(c) != std::string_view::npos) {
113 tokens.push_back({TokenKind::Symbol, std::string(1, c)});
114 ++i;
115 } else if (std::string_view("=<>!").find(c) != std::string_view::npos) {
116 std::string_view two = sql.substr(i, 2);
117 if (two == "<=" || two == ">=" || two == "!=" || two == "<>") {
118 tokens.push_back({TokenKind::Symbol, std::string(two)});
119 i += 2;
120 } else if (c == '!') {
121 throw SqlError("unexpected '!'");
122 } else {
123 tokens.push_back({TokenKind::Symbol, std::string(1, c)});
124 ++i;
125 }
126 } else {
127 // A word runs to the next delimiter; an apostrophe inside it (L'Aquila) is part of the word.
128 std::size_t stop = i;
129 while (stop < sql.size()
130 && (kDelimiters.find(sql[stop]) == std::string_view::npos || isApostrophe(sql, stop))) {
131 ++stop;
132 }
133 tokens.push_back({TokenKind::Word, std::string(sql.substr(i, stop - i))});
134 i = stop;
135 }
136 }
137 tokens.push_back({TokenKind::End, ""});
138 return tokens;
139}
140
144std::string describe(const Token& token)
145{
146 switch (token.kind) {
147 case TokenKind::End:
148 return "end of statement";
149 case TokenKind::String:
150 return std::format("'{}'", token.text);
151 default:
152 return std::format("\"{}\"", token.text);
153 }
154}
155
159bool isComparison(std::string_view op)
160{
161 return op == "=" || op == "!=" || op == "<>" || op == "<" || op == "<=" || op == ">" || op == ">=";
162}
163
165class Parser {
166public:
169 explicit Parser(std::vector<Token> tokens) : tokens_(std::move(tokens)) {}
170
175 {
176 const std::string keyword = toUpper(peek().text);
177 if (peek().kind != TokenKind::Word) {
178 fail("a command");
179 }
180 if (keyword == "SELECT") return parseSelect();
181 if (keyword == "INSERT") return parseInsert();
182 if (keyword == "UPDATE") return parseUpdate();
183 if (keyword == "DELETE") return parseDelete();
184 if (keyword == "CREATE") return parseCreateTable();
185 if (keyword == "COLUMN") return parseColumn();
186 if (keyword == "DESCRIBE") return parseDescribe();
187 throw SqlError(std::format("unknown command \"{}\"", peek().text));
188 }
189
190private:
192 std::vector<Token> tokens_;
194 std::size_t pos_ = 0;
195
198 const Token& peek() const { return tokens_[pos_]; }
201 Token next() { return peek().kind == TokenKind::End ? peek() : tokens_[pos_++]; }
202
206 [[noreturn]] void fail(std::string_view expected) const
207 {
208 throw SqlError(std::format("expected {} but found {}", expected, describe(peek())));
209 }
210
214 bool acceptKeyword(std::string_view keyword)
215 {
216 if (peek().kind == TokenKind::Word && equalsIgnoreCase(peek().text, keyword)) {
217 ++pos_;
218 return true;
219 }
220 return false;
221 }
222
226 void expectKeyword(std::string_view keyword)
227 {
228 if (!acceptKeyword(keyword)) {
229 fail(keyword);
230 }
231 }
232
236 bool acceptSymbol(std::string_view symbol)
237 {
238 if (peek().kind == TokenKind::Symbol && peek().text == symbol) {
239 ++pos_;
240 return true;
241 }
242 return false;
243 }
244
248 void expectSymbol(std::string_view symbol)
249 {
250 if (!acceptSymbol(symbol)) {
251 fail(std::format("'{}'", symbol));
252 }
253 }
254
259 std::string expectWord(std::string_view what)
260 {
261 if (peek().kind != TokenKind::Word) {
262 fail(what);
263 }
264 return next().text;
265 }
266
270 std::string expectValue()
271 {
272 if (peek().kind != TokenKind::Word && peek().kind != TokenKind::String) {
273 fail("a value");
274 }
275 return next().text;
276 }
277
281 {
282 while (acceptSymbol(";") || acceptSymbol("@")) {
283 }
284 if (peek().kind != TokenKind::End) {
285 fail("end of statement");
286 }
287 }
288
293 {
294 Condition condition;
295 condition.column = expectWord("a column name");
296 if (acceptKeyword("LIKE")) {
297 condition.op = "LIKE";
298 } else {
299 if (peek().kind != TokenKind::Symbol || !isComparison(peek().text)) {
300 fail("a comparison (= != < <= > >= or LIKE)");
301 }
302 condition.op = next().text;
303 }
304 condition.value = expectValue();
305 if (peek().kind == TokenKind::Word) {
306 if (equalsIgnoreCase(peek().text, "AND") || equalsIgnoreCase(peek().text, "OR")) {
307 throw SqlError(std::format("WHERE takes one condition: {} is not supported", toUpper(peek().text)));
308 }
309 std::string words = condition.value;
310 while (peek().kind == TokenKind::Word) {
311 words += ' ' + next().text;
312 }
313 throw SqlError(std::format("put a value with spaces in quotes, e.g. {} = '{}'", condition.column, words));
314 }
315 return condition;
316 }
317
322 {
323 expectKeyword("SELECT");
324 SelectStmt stmt;
325 do {
326 if (acceptSymbol("*")) {
327 stmt.items.push_back({SelectItem::Kind::Star, "*"});
328 } else if (peek().kind == TokenKind::String) {
329 stmt.items.push_back({SelectItem::Kind::Literal, next().text});
330 } else {
331 stmt.items.push_back({SelectItem::Kind::Column, expectWord("a column name")});
332 }
333 } while (acceptSymbol(","));
334
335 expectKeyword("FROM");
336 stmt.table = expectWord("a table name");
337
338 if (acceptKeyword("WHERE")) {
339 stmt.where = parseCondition();
340 }
341
342 expectEnd();
343 return stmt;
344 }
345
350 {
351 expectKeyword("INSERT");
352 expectKeyword("INTO");
353 InsertStmt stmt;
354 stmt.table = expectWord("a table name");
355 expectKeyword("VALUES");
356 expectSymbol("(");
357 do {
358 // A value is a 'quoted string', or one or more bare words joined by single spaces.
359 if (peek().kind == TokenKind::String) {
360 stmt.values.push_back(next().text);
361 continue;
362 }
363 std::string value;
364 while (peek().kind == TokenKind::Word) {
365 if (!value.empty()) {
366 value += ' ';
367 }
368 value += next().text;
369 }
370 stmt.values.push_back(std::move(value));
371 } while (acceptSymbol(","));
372 expectSymbol(")");
373 expectEnd();
374 return stmt;
375 }
376
381 {
382 expectKeyword("UPDATE");
383 UpdateStmt stmt;
384 stmt.table = expectWord("a table name");
385 expectKeyword("SET");
386 do {
387 Assignment assignment;
388 assignment.column = expectWord("a column name");
389 expectSymbol("=");
390 assignment.value = expectValue();
391 if (peek().kind == TokenKind::Word && !equalsIgnoreCase(peek().text, "WHERE")) {
392 throw SqlError(std::format("put a value with spaces in quotes, e.g. {} = '{} {}'",
393 assignment.column, assignment.value, peek().text));
394 }
395 stmt.assignments.push_back(std::move(assignment));
396 } while (acceptSymbol(","));
397 if (!acceptKeyword("WHERE")) {
398 fail("WHERE (UPDATE always needs a condition, e.g. WHERE ID = 3)");
399 }
400 stmt.where = parseCondition();
401 expectEnd();
402 return stmt;
403 }
404
409 {
410 expectKeyword("DELETE");
411 expectKeyword("FROM");
412 DeleteStmt stmt;
413 stmt.table = expectWord("a table name");
414 if (!acceptKeyword("WHERE")) {
415 fail("WHERE (DELETE always needs a condition, e.g. WHERE ID = 3)");
416 }
417 stmt.where = parseCondition();
418 expectEnd();
419 return stmt;
420 }
421
426 {
427 expectKeyword("CREATE");
428 expectKeyword("TABLE");
429 CreateTableStmt stmt;
430 stmt.table = expectWord("a table name");
431 expectSymbol("(");
432 do {
433 ColumnDefinition column;
434 column.name = expectWord("a column name");
435 const std::string type = toUpper(expectWord("a type (CHAR or NUMBER)"));
436 if (type == "CHAR") {
437 column.type = "S1";
438 } else if (type == "NUMBER") {
439 column.type = "N1";
440 } else {
441 throw SqlError(std::format("unknown type \"{}\" for column {}: use CHAR(<width>) or NUMBER(<width>)",
442 type, column.name));
443 }
444 expectSymbol("(");
445 const std::string width = expectWord("a width");
446 auto value = parseInteger(width);
447 if (!value || *value < 1) {
448 throw SqlError(std::format("invalid width \"{}\" for column {}: expected a positive number",
449 width, column.name));
450 }
451 column.width = static_cast<std::size_t>(*value);
452 expectSymbol(")");
453 stmt.columns.push_back(std::move(column));
454 } while (acceptSymbol(","));
455 expectSymbol(")");
456 expectEnd();
457 return stmt;
458 }
459
464 {
465 expectKeyword("COLUMN");
466 ColumnFormatStmt stmt;
467 stmt.column = expectWord("a column name");
468 expectKeyword("FORMAT");
469 const std::string format = expectWord("a format such as A16");
470 auto width = parseInteger(std::string_view(format).substr(1));
471 if (format.size() < 2 || (format[0] != 'A' && format[0] != 'a') || !width || *width < 1) {
472 throw SqlError(std::format("invalid format \"{}\", expected A<width> such as A16", format));
473 }
474 stmt.width = static_cast<std::size_t>(*width);
475 expectEnd();
476 return stmt;
477 }
478
483 {
484 expectKeyword("DESCRIBE");
485 DescribeStmt stmt;
486 if (peek().kind == TokenKind::Word) {
487 stmt.table = next().text;
488 }
489 expectEnd();
490 return stmt;
491 }
492};
493
497bool isComment(std::string_view sql)
498{
499 sql = trim(sql);
500 if (sql.empty() || sql.starts_with("--")) {
501 return true;
502 }
503 const std::string_view first = sql.substr(0, sql.find_first_of(" \t"));
504 return equalsIgnoreCase(first, "REM");
505}
506
507} // namespace
508
509Statement parseStatement(std::string_view sql)
510{
511 if (isComment(sql)) {
512 return CommentStmt{};
513 }
514 return Parser(tokenize(sql)).parse();
515}
516
517std::vector<std::string> splitStatements(std::string_view script)
518{
519 std::vector<std::string> statements;
520 std::size_t lineStart = 0;
521 while (lineStart <= script.size()) {
522 std::size_t lineEnd = script.find('\n', lineStart);
523 if (lineEnd == std::string_view::npos) {
524 lineEnd = script.size();
525 }
526 const std::string_view line = script.substr(lineStart, lineEnd - lineStart);
527 lineStart = lineEnd + 1;
528
529 if (isComment(line)) {
530 continue;
531 }
532 std::string current;
533 bool inQuotes = false;
534 for (std::size_t i = 0; i < line.size(); ++i) {
535 const char c = line[i];
536 if (c == '\'' && !isApostrophe(line, i)) {
537 inQuotes = !inQuotes;
538 }
539 if (c == ';' && !inQuotes) {
540 if (!trim(current).empty()) {
541 statements.emplace_back(trim(current));
542 }
543 current.clear();
544 } else {
545 current += c;
546 }
547 }
548 if (!trim(current).empty()) {
549 statements.emplace_back(trim(current));
550 }
551 }
552 return statements;
553}
554
555} // namespace tinysql
The parsed form of every statement, and the functions that build it from text.
std::variant< SelectStmt, InsertStmt, UpdateStmt, DeleteStmt, CreateTableStmt, ColumnFormatStmt, DescribeStmt, CommentStmt > Statement
Any parsed statement: exactly one of the statement types above.
Definition Statement.hpp:98
std::vector< std::string > splitStatements(std::string_view script)
Splits a script into statements.
Definition Parser.cpp:517
Statement parseStatement(std::string_view sql)
Parses one statement.
Definition Parser.cpp:509
The SqlError exception and small string helpers used everywhere.
std::optional< long long > parseInteger(std::string_view text)
Parses a whole string as a base-10 integer.
Definition Util.cpp:82
std::string toUpper(std::string_view text)
Converts ASCII letters to upper case.
Definition Util.cpp:42
bool equalsIgnoreCase(std::string_view a, std::string_view b)
Compares two strings, ignoring the case of ASCII letters.
Definition Util.cpp:49
std::string_view trim(std::string_view text)
Removes leading and trailing spaces, tabs and line ends.
Definition Util.cpp:34
A problem with one statement: bad syntax, unknown table, value too long...
Definition Util.hpp:16
A recursive-descent parser: one method per statement type, reading the tokens left to right.
Definition Parser.cpp:165
void expectSymbol(std::string_view symbol)
Consumes a symbol that must come next.
Definition Parser.cpp:248
Condition parseCondition()
Parses <column> <op> <value>, after the WHERE keyword; <op> is a comparison or LIKE.
Definition Parser.cpp:292
ColumnFormatStmt parseColumn()
Parses COLUMN <col> FORMAT A<width>.
Definition Parser.cpp:463
std::string expectWord(std::string_view what)
Consumes a word (a name) that must come next.
Definition Parser.cpp:259
bool acceptKeyword(std::string_view keyword)
Consumes the next token if it is a given keyword.
Definition Parser.cpp:214
UpdateStmt parseUpdate()
Parses UPDATE <table> SET <col> = <value>, ... WHERE ....
Definition Parser.cpp:380
SelectStmt parseSelect()
Parses SELECT <items> FROM <table> [WHERE ...].
Definition Parser.cpp:321
Statement parse()
Parses the whole statement, choosing the method from its first word.
Definition Parser.cpp:174
void expectEnd()
Checks that the statement ends here, after any trailing ; or @.
Definition Parser.cpp:280
Token next()
Consumes the next token (the End token is never consumed).
Definition Parser.cpp:201
Parser(std::vector< Token > tokens)
Prepares to parse a statement.
Definition Parser.cpp:169
bool acceptSymbol(std::string_view symbol)
Consumes the next token if it is a given symbol.
Definition Parser.cpp:236
const Token & peek() const
Looks at the next token without consuming it.
Definition Parser.cpp:198
InsertStmt parseInsert()
Parses INSERT INTO <table> VALUES (...). A value is a quoted string, or one or more bare words joined...
Definition Parser.cpp:349
DescribeStmt parseDescribe()
Parses DESCRIBE [<table>].
Definition Parser.cpp:482
std::vector< Token > tokens_
The tokens, ending with TokenKind::End.
Definition Parser.cpp:192
DeleteStmt parseDelete()
Parses DELETE FROM <table> WHERE ....
Definition Parser.cpp:408
void fail(std::string_view expected) const
Reports a syntax error at the current token.
Definition Parser.cpp:206
void expectKeyword(std::string_view keyword)
Consumes a keyword that must come next.
Definition Parser.cpp:226
std::string expectValue()
Consumes a value that must come next: a word or a quoted string.
Definition Parser.cpp:270
CreateTableStmt parseCreateTable()
Parses CREATE TABLE <table> (<col> <type>(<width>), ...), where <type> is CHAR or NUMBER.
Definition Parser.cpp:425
bool isComment(std::string_view sql)
Tells whether a line is a comment or empty.
Definition Parser.cpp:497
bool isWordChar(char c)
Tells whether a character can stand next to an apostrophe inside a word.
Definition Parser.cpp:52
bool isApostrophe(std::string_view text, std::size_t i)
Tells whether the ' at a position is an apostrophe inside a word, as in d'Asti, L'Aquila or dell'800.
Definition Parser.cpp:67
std::string describe(const Token &token)
Describes a token for an error message.
Definition Parser.cpp:144
constexpr std::string_view kDelimiters
The characters that end a word (a ' only when it is not an apostrophe, see isApostrophe()).
Definition Parser.cpp:47
std::vector< Token > tokenize(std::string_view sql)
Splits a statement into tokens.
Definition Parser.cpp:77
bool isComparison(std::string_view op)
Tells whether a symbol is a comparison operator.
Definition Parser.cpp:159
TokenKind
The kinds of token the tokenizer produces.
Definition Parser.cpp:33
@ Symbol
Punctuation or an operator: ( ) , * ; @ = != <> < <= > >=.
@ Word
A keyword, name or bare value, e.g. SELECT, EMPLOY, Torino, 42.
@ String
A 'quoted string', without its quotes.
One <column> = <value> of an UPDATE's SET list.
Definition Statement.hpp:54
std::string column
The column to change.
Definition Statement.hpp:55
std::string value
Its new value.
Definition Statement.hpp:56
One column of a CREATE TABLE: <name> CHAR(<width>) or <name> NUMBER(<width>).
Definition Statement.hpp:73
std::string name
The column name.
Definition Statement.hpp:74
std::string type
S1 for CHAR, N1 for NUMBER: the codes SYSTEM_TABLE records.
Definition Statement.hpp:75
std::size_t width
Number of characters the column occupies in every row.
Definition Statement.hpp:76
COLUMN <name> FORMAT A<width>: the console display width of a column.
Definition Statement.hpp:67
std::string column
The column name.
Definition Statement.hpp:68
std::size_t width
Display width in characters.
Definition Statement.hpp:69
REM ..., -- ..., or an empty line: nothing to run.
Definition Statement.hpp:91
A WHERE <column> <op> <value> test.
Definition Statement.hpp:28
std::string value
The value to compare with (a number when both sides are integers), or the LIKE pattern.
Definition Statement.hpp:31
std::string column
The column to test.
Definition Statement.hpp:29
std::string op
One of = != <> < <= > >=, or LIKE.
Definition Statement.hpp:30
CREATE TABLE <table> (<column> <type>(<width>)[, ...]): adds a table to SYSTEM_TABLE.
Definition Statement.hpp:80
std::string table
The new table's name.
Definition Statement.hpp:81
std::vector< ColumnDefinition > columns
Its columns, in file order.
Definition Statement.hpp:82
DELETE FROM <table> WHERE <condition>. The WHERE is required, so a table is never emptied by accident...
Definition Statement.hpp:48
Condition where
Which rows to remove.
Definition Statement.hpp:50
std::string table
The table to remove rows from.
Definition Statement.hpp:49
DESCRIBE lists the tables; DESCRIBE <table> lists that table's columns.
Definition Statement.hpp:86
std::optional< std::string > table
The table to describe, or empty to list all tables.
Definition Statement.hpp:87
INSERT INTO <table> VALUES (<values>).
Definition Statement.hpp:42
std::vector< std::string > values
The values, without the ID when it is assigned automatically.
Definition Statement.hpp:44
std::string table
The table to add a row to.
Definition Statement.hpp:43
SELECT <items> FROM <table> [WHERE <condition>].
Definition Statement.hpp:35
std::vector< SelectItem > items
What to show, in order.
Definition Statement.hpp:36
std::string table
The table to read.
Definition Statement.hpp:37
std::optional< Condition > where
The optional WHERE test.
Definition Statement.hpp:38
UPDATE <table> SET <col> = <value>[, ...] WHERE <condition>. The WHERE is required,...
Definition Statement.hpp:60
std::string table
The table to change.
Definition Statement.hpp:61
std::vector< Assignment > assignments
The columns to set and their new values.
Definition Statement.hpp:62
Condition where
Which rows to change.
Definition Statement.hpp:63
TokenKind kind
What kind of token this is.
Definition Parser.cpp:42