Данная часть работы будет посвещена добавлению поддержки вложенных запросов и объединения таблиц. Чтобы реализовать задуманное необходимо будет проделать три шага:
- Поддержать оператор AS для переименования поля в выходной таблице
- Поддержать вложенный запрос SELECT
- Поддержать команды [INNER] JOIN, LEFT JOIN и RIGHT JOIN
SELECT
field1 AS id,
field2,
field3 AS description
FROM
table1 Оператор AS может быть указан после имени колонки, которая выбирается из таблицы. Если он указан то в результирующей таблице соответствующая колонка должна отдаваться с новым именем.
В нашем SQL-диалекте будет единственное место, в котором можно будет использовать вложенный запрос, рассмотрим пару примеров:
-- 1
SELECT name FROM (SELECT id, name FROM tab1 WHERE id < 5);
-- 2
SELECT name FROM (SELECT id, name FROM tab1) WHERE id < 5;
-- 3
SELECT name FROM SELECT id, name FROM tab1 WHERE id < 5;Все 3 запроса корректны, при этом первый эквивалентен третьему запросу. Иными словами, на месте, где у нас может быть вложенный запрос мы можем либо поставить скобки, либо нет. Если скобки стоят, то ко вложенному запросу относится только то, что написано в скобках. Если нет, тогда все что написано после второго SELECT относится ко вложенному запросу.
Будем поддерживать только один вложенный запрос.
Теперь о главном этапе данной части работы. Если вы до этого организовали код таким образом, как мы обсуждали на занятиях. Т.е. сделали парсер, который на выходе отдает команду спрятанную за интерфейсом, тогда добавление JOIN'ов будет намного проще. Если же нет, тогда стоит остановиться и отрефакторить код, введя в программу необходимые интерфейсы.
Команда JOIN по отношению к SELECT'у будет являться вложенной, потому ее добавление должно затронуть лишь парсер и реализацию дополнительной команды, которая будет объеденять две таблицы.
Первым делом разберемся чем отличаются INNER, LEFT и RIGHT JOIN'ы. Допустим у нас есть две таблицы:
table: tab_names
----------------
| id | name |
----------------
| 1 | 'name1' |
| 2 | 'name2' |
| 5 | 'name5' |
----------------
table: tab_last_names
----------------------------
| id | name_id | last_name |
----------------------------
| 10 | 1 | 'ln1' |
| 20 | 2 | 'ln2' |
| 30 | 3 | 'ln3' |
----------------------------
Посмотрим на результат выполнения следующих запросов, я специально взял вложенный запрос на объединение таблиц в (), хотя здесь их можно опустить, чтобы показать границы самой команды JOIN:
-- INNER JOIN (ключевое слово INNER может быть опущено)
SELECT
tab_names.name AS first_name,
tab_last_names.last_name AS second_name
FROM
(
tab_names JOIN tab_last_names
ON tab_names.id = tab_last_names.name_id
)
-- LEFT JOIN
SELECT
tab_names.name AS first_name,
tab_last_names.last_name AS second_name
FROM
(
tab_names LEFT JOIN tab_last_names
ON tab_names.id = tab_last_names.name_id
)
-- RIGHT JOIN
SELECT
tab_names.name AS first_name,
tab_last_names.last_name AS second_name
FROM
(
tab_names RIGHT JOIN tab_last_names
ON tab_names.id = tab_last_names.name_id
)Все JOIN'ы - это бинарные операторы. Они принимают на вход две таблицы и возвращают объединенную таблицу, в соответствии с указанным классом эквивалентности, при помощи оператора ON. В нашем диалекте мы будем поддерживать только сравнение по одному конкретному полю. Поля должны иметь один тип. Т.к. в общем случае значение в колонке может повторяться (т.к. мы не делали индексы и ограничения на данные), то матчинг осуществляем с первой попавшейся (любой) строкой. В тестах, я гарантирую уникальность значений в колонках по которым будет осуществляться объединение. Так же в тестах будут только связи по типу 1 к 1. Итак ниже приведены два набора таблиц, первый набор - результат объединения. Второй набор - результат всего запроса.
Результат [INNER] JOIN
---------------------------------------------------------------------------------------------------------
| tab_names.id | tab_names.name | tab_last_names.id | tab_last_names.name_id | tab_last_names.last_name |
---------------------------------------------------------------------------------------------------------
| 1 | 'name1' | 10 | 1 | 'ln1' |
| 2 | 'name2' | 20 | 2 | 'ln2' |
---------------------------------------------------------------------------------------------------------
Результат LEFT JOIN
---------------------------------------------------------------------------------------------------------
| tab_names.id | tab_names.name | tab_last_names.id | tab_last_names.name_id | tab_last_names.last_name |
---------------------------------------------------------------------------------------------------------
| 1 | 'name1' | 10 | 1 | 'ln1' |
| 2 | 'name2' | 20 | 2 | 'ln2' |
| 5 | 'name5' | NULL | NULL | NULL |
---------------------------------------------------------------------------------------------------------
Результат RIGHT JOIN
---------------------------------------------------------------------------------------------------------
| tab_names.id | tab_names.name | tab_last_names.id | tab_last_names.name_id | tab_last_names.last_name |
---------------------------------------------------------------------------------------------------------
| 1 | 'name1' | 10 | 1 | 'ln1' |
| 2 | 'name2' | 20 | 2 | 'ln2' |
| NULL | NULL | 30 | 3 | 'ln3' |
---------------------------------------------------------------------------------------------------------
Результат запроса использующего [INNER] JOIN
--------------------------
| first_name | last_name |
--------------------------
| 'name1' | 'ln1' |
| 'name2' | 'ln2' |
--------------------------
Результат запроса использующего LEFT JOIN
--------------------------
| first_name | last_name |
--------------------------
| 'name1' | 'ln1' |
| 'name2' | 'ln2' |
| 'name5' | NULL |
--------------------------
Результат запроса использующего RIGHT JOIN
--------------------------
| first_name | last_name |
--------------------------
| 'name1' | 'ln1' |
| 'name2' | 'ln2' |
| NULL | 'ln3' |
--------------------------
Так же стоит отметить два важных момента. Во-первых схема результата после объединения уже не будет соответствовать оригинальным таблицам.
- При выполнении любых JOIN'ов все поля имеющие PRIMARY KEY должны этот признак потерять.
- При выполнении LEFT и RIGHT JOIN'ов все поля должны стать зануляемыми.
Для упрощения реализации логики, колонки временной таблицы, образующейся после слияния двух таблиц следует именовать в виде 'table_name.column_name'. А точку в имени интерпретировать как часть имени. Я со своей стороны гарантирую этот аспект в тестах. Так же если в SELECT'е не будет указан оператор AS, то в результирующей схеме колонка должна именоваться именно так, как она указана, например "tab_names.name".