myTinySQL v0.2.0-2-g5000098
A tiny SQL interpreter over fixed-width text files
Loading...
Searching...
No Matches
Session.cpp
Go to the documentation of this file.
1
3
4#include "Session.hpp"
5
6#include <algorithm>
7#include <compare>
8#include <format>
9#include <fstream>
10#include <limits>
11
12#include "Catalog.hpp"
13#include "TableFile.hpp"
14#include "Util.hpp"
15
16namespace tinysql {
17
18namespace {
19
26bool matches(std::string_view fieldValue, const Condition& condition)
27{
28 if (condition.op == "LIKE") {
29 return matchesLike(fieldValue, condition.value);
30 }
31
32 std::strong_ordering order = std::strong_ordering::equal;
33 auto left = parseInteger(fieldValue);
34 auto right = parseInteger(condition.value);
35 if (left && right) {
36 order = *left <=> *right;
37 } else {
38 order = trim(fieldValue) <=> trim(condition.value);
39 }
40
41 const std::string& op = condition.op;
42 if (op == "=") return order == 0;
43 if (op == "!=" || op == "<>") return order != 0;
44 if (op == "<") return order < 0;
45 if (op == "<=") return order <= 0;
46 if (op == ">") return order > 0;
47 return order >= 0; // ">="
48}
49
54ResultSet fitted(std::vector<std::string> headers, std::vector<std::vector<std::string>> rows)
55{
56 ResultSet result;
57 result.widths.resize(headers.size());
58 for (std::size_t i = 0; i < headers.size(); ++i) {
59 result.widths[i] = headers[i].size();
60 for (const auto& row : rows) {
61 result.widths[i] = std::max(result.widths[i], row[i].size());
62 }
63 }
64 result.literal.assign(headers.size(), false);
65 result.storageWidths.assign(headers.size(), 0);
66 result.headers = std::move(headers);
67 result.rows = std::move(rows);
68 return result;
69}
70
75std::string plural(std::size_t n, std::string_view word)
76{
77 return std::format("{} {}{}", n, word, n == 1 ? "" : "s");
78}
79
81constexpr std::size_t kMaxNameLength = 20;
82
84constexpr std::size_t kMaxRecordLength = 99999;
85
91void checkName(std::string_view name, std::string_view what)
92{
93 auto isLetter = [](char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); };
94 auto isAllowed = [&](char c) { return isLetter(c) || (c >= '0' && c <= '9') || c == '_' || c == '-'; };
95 if (name.empty() || name.size() > kMaxNameLength || !isLetter(name.front())
96 || !std::ranges::all_of(name, isAllowed)) {
97 throw SqlError(std::format("invalid {} name '{}': use up to {} letters, digits, _ and -, starting with a letter",
98 what, name, kMaxNameLength));
99 }
100}
101
102} // namespace
103
104Session::Session(std::filesystem::path dataDir) : dataDir_(std::move(dataDir)) {}
105
107{
108 StatementResult result;
109 try {
110 const Statement statement = parseStatement(sql);
111 result = std::visit([this](const auto& stmt) { return run(stmt); }, statement);
112 } catch (const SqlError& e) {
113 result.failed = true;
114 result.message = e.what();
115 } catch (const std::exception& e) {
116 result.failed = true;
117 result.message = std::format("internal error: {}", e.what());
118 }
119 result.sql = std::string(trim(sql));
120 return result;
121}
122
123std::vector<StatementResult> Session::executeScript(std::string_view script)
124{
125 std::vector<StatementResult> results;
126 for (const std::string& sql : splitStatements(script)) {
127 results.push_back(execute(sql));
128 }
129 return results;
130}
131
133{
134 const Catalog catalog = Catalog::load(dataDir_);
135 const TableSchema& schema = catalog.table(stmt.table);
136
137 // What each output column shows: a table column (by index) or a literal text.
138 struct Output {
139 std::string header;
140 std::optional<std::size_t> source;
141 std::size_t width;
142 };
143 std::vector<Output> outputs;
144 auto addColumn = [&](std::size_t index) {
145 const Column& column = schema.columns[index];
146 auto custom = displayWidths_.find(toUpper(column.name));
147 // COLUMN ... FORMAT wins; otherwise the column is wide enough for its values and its name.
148 const std::size_t width = custom != displayWidths_.end() ? custom->second
149 : std::max(column.width(), column.name.size());
150 outputs.push_back({column.name, index, width});
151 };
152 for (const SelectItem& item : stmt.items) {
153 switch (item.kind) {
155 for (std::size_t i = 0; i < schema.columns.size(); ++i) {
156 addColumn(i);
157 }
158 break;
160 auto index = schema.indexOf(item.text);
161 if (!index) {
162 throw SqlError(std::format("unknown column '{}' in table {}", item.text, schema.name));
163 }
164 addColumn(*index);
165 break;
166 }
168 outputs.push_back({item.text, std::nullopt, item.text.size()});
169 break;
170 }
171 }
172
173 ResultSet result;
174
175 std::optional<std::size_t> whereIndex;
176 if (stmt.where) {
177 whereIndex = schema.indexOf(stmt.where->column);
178 if (!whereIndex) {
179 throw SqlError(std::format("unknown column '{}' in table {}", stmt.where->column, schema.name));
180 }
181 }
182
183 for (const Output& output : outputs) {
184 result.headers.push_back(output.header);
185 result.widths.push_back(output.width);
186 result.literal.push_back(!output.source);
187 result.storageWidths.push_back(output.source ? schema.columns[*output.source].width() : 0);
188 }
189
190 // Rows can be edited and deleted by ID when the table has an ID column, the result shows it,
191 // and the table is not the catalogue itself.
192 if (schema.hasAutoId() && !equalsIgnoreCase(schema.name, kCatalogTable)) {
193 auto id = std::ranges::find_if(outputs, [](const Output& o) { return o.source == std::size_t{0}; });
194 if (id != outputs.end()) {
195 result.idColumn = static_cast<std::size_t>(id - outputs.begin());
196 }
197 }
198
199 for (const Row& row : TableFile(schema, catalog.dataFile(schema)).readRows()) {
200 if (whereIndex && !matches(row[*whereIndex], *stmt.where)) {
201 continue;
202 }
203 std::vector<std::string> values;
204 values.reserve(outputs.size());
205 for (const Output& output : outputs) {
206 values.push_back(output.source ? row[*output.source] : output.header);
207 }
208 result.rows.push_back(std::move(values));
209 }
210
211 StatementResult statementResult;
212 statementResult.tableName = schema.name;
213 statementResult.message = plural(result.rows.size(), "row");
214 statementResult.table = std::move(result);
215 return statementResult;
216}
217
219{
220 const Catalog catalog = Catalog::load(dataDir_);
221 const TableSchema& schema = catalog.table(stmt.table);
222 const TableFile file(schema, catalog.dataFile(schema));
223
224 Row values = stmt.values;
225 const std::size_t columns = schema.columns.size();
226 if (schema.hasAutoId() && values.size() + 1 == columns) {
227 values.insert(values.begin(), std::to_string(file.nextId()));
228 } else if (values.size() != columns) {
229 throw SqlError(std::format("table {} has {} columns{}: got {} values", schema.name, columns,
230 schema.hasAutoId() ? std::format(" (give {} and ID is assigned)", columns - 1) : "",
231 values.size()));
232 } else if (schema.hasAutoId()) {
233 const auto rows = std::filesystem::exists(catalog.dataFile(schema)) ? file.readRows() : std::vector<Row>{};
234 if (std::ranges::any_of(rows, [&](const Row& row) { return row.front() == values.front(); })) {
235 throw SqlError(std::format("table {} already has a row with ID {}", schema.name, values.front()));
236 }
237 }
238 file.append(values);
239
240 StatementResult result;
241 result.tableName = schema.name;
242 result.message = std::format("1 row inserted into {}", schema.name);
243 if (schema.hasAutoId()) {
244 result.message += std::format(" with ID {}", values.front());
245 }
246 return result;
247}
248
250{
251 const Catalog catalog = Catalog::load(dataDir_);
252 const TableSchema& schema = catalog.table(stmt.table);
253 if (equalsIgnoreCase(schema.name, kCatalogTable)) {
254 throw SqlError("SYSTEM_TABLE cannot be changed with UPDATE: edit the file instead");
255 }
256
257 std::vector<std::pair<std::size_t, std::string>> newValues;
258 for (const Assignment& assignment : stmt.assignments) {
259 const auto index = schema.indexOf(assignment.column);
260 if (!index) {
261 throw SqlError(std::format("unknown column '{}' in table {}", assignment.column, schema.name));
262 }
263 if (*index == 0 && schema.hasAutoId()) {
264 throw SqlError("the ID column cannot be changed: rows are found by their ID");
265 }
266 newValues.emplace_back(*index, assignment.value);
267 }
268 const auto whereIndex = schema.indexOf(stmt.where.column);
269 if (!whereIndex) {
270 throw SqlError(std::format("unknown column '{}' in table {}", stmt.where.column, schema.name));
271 }
272
273 const std::size_t updated = TableFile(schema, catalog.dataFile(schema)).updateRows(
274 [&](const Row& row) { return matches(row[*whereIndex], stmt.where); }, newValues);
275
276 StatementResult result;
277 result.tableName = schema.name;
278 result.message = std::format("{} updated in {}", plural(updated, "row"), schema.name);
279 return result;
280}
281
283{
284 const Catalog catalog = Catalog::load(dataDir_);
285 const TableSchema& schema = catalog.table(stmt.table);
286 if (equalsIgnoreCase(schema.name, kCatalogTable)) {
287 throw SqlError("SYSTEM_TABLE cannot be changed with DELETE: edit the file instead");
288 }
289 const auto whereIndex = schema.indexOf(stmt.where.column);
290 if (!whereIndex) {
291 throw SqlError(std::format("unknown column '{}' in table {}", stmt.where.column, schema.name));
292 }
293
294 const std::size_t removed = TableFile(schema, catalog.dataFile(schema)).removeRows([&](const Row& row) {
295 return matches(row[*whereIndex], stmt.where);
296 });
297
298 StatementResult result;
299 result.tableName = schema.name;
300 result.message = std::format("{} deleted from {}", plural(removed, "row"), schema.name);
301 return result;
302}
303
305{
306 const Catalog catalog = Catalog::load(dataDir_);
307
308 // The new table's definition; names are kept in upper case, like the existing tables.
309 checkName(stmt.table, "table");
310 TableSchema schema{.name = toUpper(stmt.table), .columns = {}, .recordLength = 0};
311 auto sameName = [&](const TableSchema& t) { return equalsIgnoreCase(t.name, schema.name); };
312 if (std::ranges::any_of(catalog.tables(), sameName)) {
313 throw SqlError(std::format("table {} already exists", schema.name));
314 }
315 for (const ColumnDefinition& definition : stmt.columns) {
316 checkName(definition.name, "column");
317 const std::string name = toUpper(definition.name);
318 if (schema.indexOf(name)) {
319 throw SqlError(std::format("column {} appears twice", name));
320 }
321 if (name == "ID" && !schema.columns.empty()) {
322 throw SqlError("the ID column must be the first one: rows are numbered by it");
323 }
324 // The sum stops at the largest std::size_t instead of wrapping around to a small number,
325 // which would pass the length check below.
326 constexpr std::size_t kLargest = std::numeric_limits<std::size_t>::max();
327 const std::size_t end =
328 definition.width > kLargest - schema.recordLength ? kLargest : schema.recordLength + definition.width;
329 schema.columns.push_back(Column{
330 .name = name,
331 .type = definition.type,
332 .from = schema.recordLength,
333 .to = end - 1,
334 });
335 schema.recordLength = end;
336 }
337 if (schema.recordLength > kMaxRecordLength) {
338 throw SqlError(std::format("a row of table {} would be {} characters long: the limit is {}",
339 schema.name, schema.recordLength, kMaxRecordLength));
340 }
341
342 // One SYSTEM_TABLE line per column, numbered after the last line.
343 const TableSchema& catalogSchema = catalog.table(kCatalogTable);
344 const TableFile catalogFile(catalogSchema, catalog.dataFile(catalogSchema));
345 long long id = catalogFile.nextId();
346 std::vector<Row> lines;
347 for (std::size_t i = 0; i < schema.columns.size(); ++i) {
348 const Column& column = schema.columns[i];
349 lines.push_back({std::to_string(id++), std::to_string(i + 1), schema.name, column.name, column.type,
350 std::to_string(column.from), std::to_string(column.to), std::to_string(column.width()),
351 std::to_string(schema.recordLength)});
352 }
353
354 // First the empty data file, then the catalogue: if the catalogue cannot be written,
355 // the data file is removed again and nothing has changed.
356 const auto dataFile = catalog.dataFile(schema);
357 if (std::filesystem::exists(dataFile)) {
358 throw SqlError(std::format("data file {} already exists: remove it, or choose another name",
359 dataFile.filename().string()));
360 }
361 if (!std::ofstream(dataFile)) {
362 throw SqlError(std::format("cannot create {}", dataFile.string()));
363 }
364 try {
365 catalogFile.appendRows(lines);
366 } catch (...) {
367 std::filesystem::remove(dataFile);
368 throw;
369 }
370
371 StatementResult result;
372 result.tableName = schema.name;
373 result.message = std::format("table {} created with {} (row length {})", schema.name,
374 plural(schema.columns.size(), "column"), schema.recordLength);
375 return result;
376}
377
379{
380 displayWidths_[toUpper(stmt.column)] = stmt.width;
381 StatementResult result;
382 result.silent = true;
383 return result;
384}
385
387{
388 const Catalog catalog = Catalog::load(dataDir_);
389 std::vector<std::vector<std::string>> rows;
390 StatementResult result;
391
392 if (!stmt.table) {
393 for (const TableSchema& table : catalog.tables()) {
394 rows.push_back({table.name, std::to_string(table.columns.size()), std::to_string(table.recordLength)});
395 }
396 result.message = plural(rows.size(), "table");
397 result.table = fitted({"TABLE", "COLUMNS", "ROW_LENGTH"}, std::move(rows));
398 return result;
399 }
400
401 const TableSchema& schema = catalog.table(*stmt.table);
402 for (const Column& column : schema.columns) {
403 rows.push_back({column.name, column.type, std::to_string(column.from), std::to_string(column.to),
404 std::to_string(column.width())});
405 }
406 result.tableName = schema.name;
407 result.message = plural(rows.size(), "column");
408 result.table = fitted({"COLUMN", "TYPE", "FROM", "TO", "WIDTH"}, std::move(rows));
409 return result;
410}
411
413{
414 StatementResult result;
415 result.silent = true;
416 return result;
417}
418
419} // namespace tinysql
The table definitions read from SYSTEM_TABLE: tables, their columns and positions.
constexpr std::string_view kCatalogTable
Name of the catalogue table: it describes every table, itself included, and is its own data file.
Definition Catalog.hpp:16
Running statements, and the results they produce.
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
Reading, appending, editing and removing the rows of one table's data file.
std::vector< std::string > Row
One row of a table: one value per column, trailing spaces removed.
Definition TableFile.hpp:19
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 matchesLike(std::string_view text, std::string_view pattern)
Matches text against an SQL LIKE pattern, ignoring the case of ASCII letters.
Definition Util.cpp:54
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
All table definitions, read from the SYSTEM_TABLE file (one 80-character line per column).
Definition Catalog.hpp:47
std::filesystem::path dataFile(const TableSchema &table) const
The file holding a table's rows.
Definition Catalog.cpp:125
const std::vector< TableSchema > & tables() const
All tables, in the order they appear in SYSTEM_TABLE.
Definition Catalog.hpp:63
const TableSchema & table(std::string_view name) const
Finds a table by name.
Definition Catalog.cpp:116
static Catalog load(const std::filesystem::path &dataDir)
Reads SYSTEM_TABLE from a data directory.
Definition Catalog.cpp:74
std::filesystem::path dataDir_
Where the tables are.
Definition Session.hpp:107
StatementResult run(const SelectStmt &stmt)
Runs a SELECT.
Definition Session.cpp:132
std::map< std::string, std::size_t > displayWidths_
Upper-case column name to display width.
Definition Session.hpp:108
std::vector< StatementResult > executeScript(std::string_view script)
Runs every statement of a script, in order.
Definition Session.cpp:123
StatementResult execute(std::string_view sql)
Parses and runs one statement.
Definition Session.cpp:106
Session(std::filesystem::path dataDir)
Starts a session.
Definition Session.cpp:104
A problem with one statement: bad syntax, unknown table, value too long...
Definition Util.hpp:16
Reads and changes the rows of one table's data file.
Definition TableFile.hpp:26
std::size_t removeRows(const std::function< bool(const Row &)> &shouldRemove) const
Removes the rows that match a test.
Definition TableFile.cpp:77
long long nextId() const
The ID to give the next new row.
std::size_t updateRows(const std::function< bool(const Row &)> &shouldUpdate, const std::vector< std::pair< std::size_t, std::string > > &newValues) const
Changes some columns in the rows that match a test.
Definition TableFile.cpp:82
void append(const Row &values) const
Adds one row at the end of the file.
std::vector< Row > readRows() const
Reads every row of the file.
Definition TableFile.cpp:48
void appendRows(const std::vector< Row > &rows) const
Adds several rows at the end of the file, in one write.
constexpr std::size_t kMaxRecordLength
Longest row: SYSTEM_TABLE keeps column positions in 5 characters.
Definition Session.cpp:84
std::string plural(std::size_t n, std::string_view word)
Writes a count with a noun in singular or plural.
Definition Session.cpp:75
constexpr std::size_t kMaxNameLength
Longest table or column name: the width of the TABLE and FIELD columns of SYSTEM_TABLE.
Definition Session.cpp:81
void checkName(std::string_view name, std::string_view what)
Checks a new table or column name.
Definition Session.cpp:91
bool matches(std::string_view fieldValue, const Condition &condition)
Applies a WHERE test to one value.
Definition Session.cpp:26
ResultSet fitted(std::vector< std::string > headers, std::vector< std::vector< std::string > > rows)
Builds a result whose column widths fit the headers and the values (used by DESCRIBE).
Definition Session.cpp:54
One <column> = <value> of an UPDATE's SET list.
Definition Statement.hpp:54
One column of a CREATE TABLE: <name> CHAR(<width>) or <name> NUMBER(<width>).
Definition Statement.hpp:73
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
One column of a table: the characters it occupies in every fixed-width row.
Definition Catalog.hpp:19
std::size_t width() const
Number of characters the column occupies.
Definition Catalog.hpp:27
std::size_t to
Last character of the column, inclusive.
Definition Catalog.hpp:23
std::size_t from
First character of the column in a row, counted from 0.
Definition Catalog.hpp:22
std::string name
Column name, e.g. NOME.
Definition Catalog.hpp:20
std::string type
N1 or S1: recorded in SYSTEM_TABLE, not enforced.
Definition Catalog.hpp:21
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
The rows produced by a SELECT or DESCRIBE. All per-column vectors have the same length.
Definition Session.hpp:19
std::vector< bool > literal
True for a 'literal' SELECT item.
Definition Session.hpp:22
std::vector< std::size_t > widths
Display width of each column in console output.
Definition Session.hpp:21
std::vector< std::string > headers
Column titles.
Definition Session.hpp:20
std::vector< std::size_t > storageWidths
Width of each column in the data file (0 for a literal).
Definition Session.hpp:23
std::vector< std::vector< std::string > > rows
The values, one inner vector per row.
Definition Session.hpp:24
std::optional< std::size_t > idColumn
Output column holding the row ID, if rows can be edited and deleted by ID.
Definition Session.hpp:25
One entry of a SELECT list: *, a column name, or a 'quoted literal' printed on every row.
Definition Statement.hpp:16
@ Column
A column name.
@ Star
*: every column of the table.
@ Literal
A 'quoted text', printed as-is on every row.
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
What running one statement produced: a table, a message, or an error.
Definition Session.hpp:29
std::string sql
The statement as it was run.
Definition Session.hpp:30
std::string tableName
The table the statement worked on, if any.
Definition Session.hpp:31
bool failed
True if the statement failed; message holds the error.
Definition Session.hpp:34
std::string message
E.g. "1 row inserted ...", or the error text.
Definition Session.hpp:33
std::optional< ResultSet > table
The rows, for SELECT and DESCRIBE.
Definition Session.hpp:32
bool silent
True for COLUMN and comments: nothing to show.
Definition Session.hpp:35
The definition of one table: its name, its columns, and the length of a row.
Definition Catalog.hpp:31
std::string name
Table name, e.g. EMPLOY.
Definition Catalog.hpp:32
std::optional< std::size_t > indexOf(std::string_view columnName) const
Finds a column by name.
Definition Catalog.cpp:60
bool hasAutoId() const
Tells whether rows are numbered automatically.
Definition Catalog.cpp:69
std::vector< Column > columns
Columns in file order.
Definition Catalog.hpp:33
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