На втором этапе реализуем:
- Логику создания и удаления таблиц
- Начнем писать парсер SQL запросов
- Добавим в проект ручки (endpoint'ы) для исполнения SQL запросов и несколько дополнительных для получения списка таблиц и схемы таблиц
В этой части работы поддержим две команды: создание и удаление таблиц. В SQL виде они выглядят так:
-- Создание таблицы
CREATE TABLE [IF NOT EXISTS] table_name (
column1_name TYPE [PRIMARY KEY] [NOT NULL] [DEFAULT value],
column2_name TYPE [NOT NULL] [DEFAULT value],
...
columnN_name TYPE [NOT NULL] [DEFAULT value],
);
-- Удаление таблицы
DROP TABLE [IF EXISTS] table_name;Немного расшифрую написанное:
- ';' в конце запроса ставится обычно только в скриптах в которых может быть много последовательных SQL команд, однако т.к. мы делаем ручку которая будет исполнять только одину команду за раз, то ';' в конце - должна просто игнорироваться вашим парсером
- ключевые слова SQL записаны большими буквами, имена/значения маленькими, однако ключевые слова SQL регистронезависимы, потому если переписать все маленькими буквами, то все должно парсится корректно, при этом учтите, что данные, например value у ключевого слова DEFAULT являются чувствительными к регистру учитывайте это при реализации парсера
- ключевые слова взятые в [] являются необязательной частью SQL выражения, но тут как всегда есть куча частных случаев
- IF NOT EXISTS - для CREATE TABLE, если присутствует, то ручка всегда должна возвращать ответ OK(200) и результатом того что таблица была создана или нет, если отсутствует, то в случае попытки создать таблицу с именем уже существующей, необходимо вернуть из ручки Conflict(409)
- PRIMARY KEY - признак того, что данное поле является ключом, это значит, что оно должно иметь уникальное значение для каждой записи в таблице, не может быть NULL, т.е. NOT NULL - применяется к данной колонке автоматически и его можно не писать
- NOT NULL - признак того, что значения в данной колонке не могут быть NULL'ами, т.е. при добавлении данных (эту команду мы будем реализовывать в будущем) всегда требуется указывать значение конкретное значение, которое не NULL
- DEFAULT - это ключевое слово, которое задает значение данного поля по умолчанию, используется при добавлении в таблицу новых записей (строк), если при добавлении оно не указано, то в таблицу необходимо записать именно это значение по умолчанию
- IF EXISTS - для DROP TABLE, если присутствует, то ручка всегда должна возвражать OK(200) и результат true/false в зависимости от наличия таблицы, если не указан, тогда ручка должна вернуть ошибку NotFound(404)
Пара слов о том, какие типы мы будем поддерживать в нашей базе, ниже приведена таблица с пояснением:
SQL тип C# тип Комментарий
-----------------------------------
BOOLEAN Boolean В SQL есть два ключевых слова TRUE и FALSE для задания значений
INTEGER Int64 Знаковое 64-х битное число
FLOAT Double 64-х битное с плавающей точкой
STRING String Unicode-строка (кодировка: utf16-le)
SERIAL Int64 Счетчик, который используется в качестве
PRIMARY KEY для идентификации записей в таблице.
Каждый раз при добавлении новой записи в таблицу инкрементируется.
Для хранения текущего значения счетчика обычно СУБД создает отдельную таблицу
в которой одна колонка с типом INTEGER и одна запись с текущим значением
счетчика. Давайте договоримся, что в нашем SQL диалекте будет возмжно
использовать данный тип только совместно с PRIMARY KEY. Для хранения значения
думаю можно поступить как и другие базы данных.
Теперь приведу несколько примеров корректных запросов на создание таблицы:
CREATE TABLE tab1 (id SERIAL PRIMARY KEY);
create table tab2 (name string not null, description string);
CREATE TABLE tab3 (
"id" String Primary Key,
column BOOLEAN DEFAULT TRUE
);Как видно SQL запрос так же может быть разбит на строки, а может быть записан в одну строку, грубо говоря, переводы строк можно интерпретировать как пробелы. А теперь несколько примеров некорректных запросов (парсер должен корректно обрабатывать все такие случаи, приведенные здесь и не только):
-- Согласно нашей договоренности id - должен быть помечен как PRIMARY KEY
CREATE TABLE table (id SERIAL);
-- Может быть только один PRIMARY KEY в таблице
CREATE TABLE table (
id1 STRING PRIMARY KEY,
id2 INTEGER PRIMARY KEY
);
-- Неизвестный тип
CREATE TABLE table (id BIGINT);
-- Некорректный тип значения по умолчанию (уже присутствует пометка о том, что не может быть NULL)
CREATE TABLE table (name STRING NOT NULL DEFAULT NULL);
-- Некорректный тип значения по умолчанию (не соответсвуют типы)
-- (обратите внимание на то, что значения строк берутся в одинарные кавычки, а не в двойные)
CREATE TABLE table (name INTEGER NOT NULL DEFAULT 'Hello world!');GET /api/v1/tables/list
input: нет входных аргументов
output:
{
tables: ["table1", "table2", ..., "tableN"]
}
Данная ручка должна возвращать список имен таблиц, которые в данный момент созданы в нашей базе. Чтобы получить такой JSON на выходе можно в качестве результат использовать класс вида:
public class GetTablesOutput
{
[Required] public String[] Tables { get; set; }
}POST /api/v1/tables/schema
input:
{
"name": "my_table_name"
}
output:
{
"schema": {
"columns": [ // список информации по колонкам (в порядке их добавления при создании таблицы)
{
"name": "id", // имя колонки
"type": "serial", // тип данныз в колонке
"isPKey": true, // признак того, что это PRIMARY KEY
"isNullable": false, // признак того, что поле зануляемое
"defaultValue": { // информация о значении по умолчанию
"isSpecified": false, // признак того, что DEFAUL был задан для колонки
"isNull": false, // признак того, что значение по умолчанию = NULL
"value": "" // значение по умолчанию в строковом виде
}
},
{
"name": "description",
"type": "string",
"isPKey": false,
"isNullable": true,
"defaultValue": {
"isSpecified": true,
"isNull": true,
"value": ""
}
},
... // и так далее
]
}
}
В виде классов со стороны сервиса подойдет такое представление:
public class DefaultValueInfo
{
[Required] public Boolean IsSpecified { get; set; }
[Required] public Boolean IsNull { get; set; }
[Required] public String Value { get; set; }
}
public class TableSchemaColumnInfo
{
[Required] public String Name { get; set; }
[Required] public String Type { get; set; }
[Required] public Boolean IsPKey { get; set; }
[Required] public Boolean IsNullable { get; set; }
[Required] public DefaultValueInfo DefaultValue { get; set; }
}
public class TableSchemaInfo
{
[Required] public TableSchemaColumnInfo[] Columns { get; set; }
}
public class PostTablesSchemaOutput
{
[Required] public TableSchemaInfo Schema { get; set; }
}Ручка должна по имени таблицы возвращать схему таблицы, которая представляет из себя набор информации о таблице и колонках. Давайте рассмотрим пример запроса которым создается таблица и результата получения схемы:
CREATE TABLE goods (
"id" SERIAL PRIMARY KEY,
"name" STRING NOT NULL,
"description" STRING DEFAULT NULL,
"price" INTEGER NOT NULL DEFAULT 10,
"stock" INTEGER,
"is_foreign" BOOLEAN NOT NULL
)Обратите внимание на то, что в SQL что-то взятое в двойные кавычки - это имя (например колонки или таблицы). Если же вам нужно задать значение для строки, то следует использовать одинарные кавычки. Двойные кавычки обычно используются для разрешения конфликта имен с ключевыми словами. Например слово user может быть зарезервировано СУБД под переменную хранящую текущее имя пользователя от которого выполняется запрос. Однако если его взять в двойные кавычки "user" - то интерпретатор запроса однозначно определит его как имя колонки или таблицы, или какого-то другого объекта, но не как ключевое слово языка. А так в целом, двойные кавычки можно не ставить. При этом поддержать эту особенность синтаксиса, при разборе запросов - нужно. Для созданной таким образом таблицы, мы должны получить такой результат при запросе ее схемы:
{
"schema": {
"columns": [
{
"name": "id",
"type": "serial",
"isPKey": true,
"isNullable": false,
"defaultValue": {
"isSpecified": false,
"isNull": false,
"value": ""
}
},
{
"name": "name",
"type": "string",
"isPKey": false,
"isNullable": false,
"defaultValue": {
"isSpecified": false,
"isNull": false,
"value": ""
}
},
{
"name": "description",
"type": "string",
"isPKey": false,
"isNullable": true,
"defaultValue": {
"isSpecified": true,
"isNull": true,
"value": ""
}
},
{
"name": "price",
"type": "integer",
"isPKey": false,
"isNullable": false,
"defaultValue": {
"isSpecified": true,
"isNull": false,
"value": "10"
}
},
{
"name": "stock",
"type": "integer",
"isPKey": false,
"isNullable": true,
"defaultValue": {
"isSpecified": false,
"isNull": false,
"value": ""
}
},
{
"name": "is_foreign",
"type": "boolean",
"isPKey": false,
"isNullable": false,
"defaultValue": {
"isSpecified": false,
"isNull": false,
"value": ""
}
}
]
}
}POST /api/v1/query
input:
{
"query": "SELECT id, name FROM table"
}
output:
{
"schema": {
"columns": [
{
"name": "id",
"type": "serial",
"isPKey": true,
"isNullable": false,
"defaultValue": {
"isSpecified": false,
"isNull": false,
"value": ""
}
},
{
"name": "name",
"type": "string",
"isPKey": false,
"isNullable": false,
"defaultValue": {
"isSpecified": false,
"isNull": false,
"value": ""
}
}
]
},
"result": [
["1", "record1"],
["2", null],
["3", "some name"]
]
}
Здесь приведен общий вид ответа на произвольные SQL запросы. Фактически в ответе необходимо выдать схему таблицы которую мы получили при выполнении запроса и строки с данными. Схема формируется теми же структурами, что и в предыдущей ручку. А массив "result" содержит строки, каждое из значений которого соответсвует схеме. Обратите внимание в результате все значения отдаются строками - это нужно чтобы упростить формирование ответа на стороне сервиса. Так же зануляемые типы могут содержать null - это тоже будет работать из коробки, достаточно просто присвоить null соответсвующей строке при возврате результата.
В текущей части работы у нас не будет таких "больших" и "сложных" таблиц в результате, т.к. мы реализуем только создание и удаление таблиц. Операции CREATE TABLE и DROP TABLE должны возвращать таблицу состоящую из единственной колонки "result" и единственной строки в которой будет результат выполнения команды true - в случае успешного создания/удаления таблицы false - в обратном случае.
Вот так должен выглядеть ответ для этих запросов. Сразу делайте обобщенный код для возврата значения для произвольной таблицы.
{
"schema": {
"columns": [
{
"name": "result",
"type": "boolean",
"isPKey": false,
"isNullable": false,
"defaultValue": {
"isSpecified": false,
"isNull": false,
"value": ""
}
}
]
},
"result": [
["true"]
]
}Итак подведем черту, что необходимо сделать:
- Проработать классы Table и TableSchema, которые будут хранить данные таблицы и схему данных таблицы соответсвенно.
- Реализовать парсер SQL команд CREATE TABLE и DROP TABLE, который превращает запросы в исполняемые команды. Возможный вариант организации интерфеса команд и пример мы рассматривали на практике.
- Реализовать 3 новые ручки с логикой работы как описано в задании.