Раздел 10 · Databases

Своя СУБД на Haskell · урок 5: свой SQL, лексер и парсер

senior~100 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Своя СУБД на Haskell · урок 5: свой SQL, лексер и парсер

Кому это. Ты прошёл урок 4: хранилище готово, у таблиц есть индексы. Пользоваться им пока можно только из Haskell-кода. Сегодня начинается верхняя половина движка: строка SQL превратится в дерево, с которым движок сможет работать. Тот же урок есть в TS-треке, и диалект там тот же до последнего ключевого слова. Для Haskell это родная территория: лексер это сопоставление с образцом по списку символов, AST это алгебраические типы, парсер это монада над списком токенов, и все три шага это чистые функции с результатом Either DbError. Если ты писал парсер-комбинаторы с нуля, половина урока покажется знакомой.

Важно про код. В этом уроке нет ни одной строки в IO. Всё, что ты напишешь, проверяется в GHCi одной строкой: parse "SELECT * FROM t". Пользуйся этим, гоняй каждую функцию сразу, как напишешь.

Где мы в движке

┌────────────────────────────────────────────────┐
│  Бинарный протокол + TCP-сервер                  │  урок 11
├────────────────────────────────────────────────┤
│  Исполнитель (Volcano) + EXPLAIN                 │  урок 6
│  Парсер, лексер, pretty-printer                  │  урок 5  ← мы здесь
├────────────────────────────────────────────────┤
│  Vacuum                                          │  урок 10
│  Блокировки (2PL), граф ожидания                 │  урок 9
│  MVCC, менеджер транзакций                       │  урок 8
│  WAL (журнал, redo, recovery)                    │  урок 7
├────────────────────────────────────────────────┤
│  B+Tree индекс                                   │  урок 4
│  Куча (slotted pages, seq scan)                  │  урок 3
├────────────────────────────────────────────────┤
│  Буферный пул (кэш страниц, вытеснение)           │  урок 2
│  DiskManager (страницы поверх файла)              │  урок 1
└────────────────────────────────────────────────┘

Это первый слой, который ничего не знает про хранилище. Парсер не видит ни страниц, ни таблиц. Он даже не знает, существует ли таблица users: это проверит binder в уроке 6. Его работа одна: превратить строку в дерево или объяснить, где в строке ошибка.

Что мы парсим

Парсер начинается с грамматики. Мы берём небольшое подмножество SQL, которого хватает на весь воркшоп. Записывается грамматика в EBNF, и каждое правило станет одной функцией парсера.

command     = "BEGIN" | "COMMIT" | "ROLLBACK" | [ "EXPLAIN" ] statement ;
statement   = createTable | insert | select | update | delete ;

createTable = "CREATE" "TABLE" ident "(" columnDef { "," columnDef } ")" ;
columnDef   = ident type [ "PRIMARY" "KEY" | "NOT" "NULL" ] ;
type        = "INT" | "TEXT" | "BOOL" ;

insert      = "INSERT" "INTO" ident [ "(" ident { "," ident } ")" ]
              "VALUES" "(" literal { "," literal } ")" ;

select      = "SELECT" ( "*" | projection )
              "FROM" ident [ join ]
              [ "WHERE" expr ]
              [ "ORDER" "BY" ident [ "ASC" | "DESC" ] ]
              [ "LIMIT" number ] ;
projection  = expr [ "AS" ident ] { "," expr [ "AS" ident ] } ;
join        = ( "INNER" | "LEFT" ) "JOIN" ident "ON" expr ;

update      = "UPDATE" ident "SET" assignment { "," assignment } [ "WHERE" expr ] ;
delete      = "DELETE" "FROM" ident [ "WHERE" expr ] ;
assignment  = ident "=" expr ;

expr        = primary { binop primary } ;           (* приоритеты по таблице *)
binop       = "OR" | "AND" | "=" | "<>" | "<" | "<=" | ">" | ">=" | "LIKE"
            | "+" | "-" | "*" | "/" ;
primary     = literal | column | "(" expr ")" | "NOT" primary ;
column      = [ ident "." ] ident ;
literal     = number | string | "TRUE" | "FALSE" | "NULL" ;

Операторы начинаются с ключевого слова, по которому их сразу видно: посмотреть на первый токен и вызвать нужную функцию. Выражения устроены иначе. У операторов внутри выражения есть приоритеты: OR слабее AND, тот слабее сравнений, те слабее сложения, то слабее умножения. Вместо лесенки из пяти почти одинаковых функций мы разберём выражения одной функцией с таблицей приоритетов. Это Pratt-парсер, мы дойдём до него в середине урока.

В TS-эталоне EXPLAIN и транзакционные команды добавляются к парсеру позже, в уроках 6 и 7. В Haskell-порте верхнеуровневая command есть сразу, и тесты этого урока её уже проверяют. Это три строки кода, а не новая идея.

JOIN, ORDER BY и LIMIT парсер понимает, а движок в уроке 6 отвергнет их как упражнения. Парсить их сейчас полезно: грамматика сразу целая, и упражнения урока 6 не будут трогать фронтенд.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.