Несколько запросов в редакторе запросов консоли

Редактор запросов Rockset Console позволяет пользователям вводить и выполнять запросы к коллекциям. Однако до сих пор все, что было напечатано в редакторе, выполнялось и анализировалось как один запрос. Это означает, что для пользователя было не так просто переключаться между несколькими запросами в нашем редакторе. Им придется закомментировать запросы, которые они не хотят выполнять, или хранить все свои запросы в отдельном текстовом файле и копировать те, которые они хотят выполнять, по одному.

Чтобы упростить переключение между несколькими запросами, мы решили разрешить множественные запросы в нашем редакторе, разделенные точкой с запятой, завершающим оператором SQL. Для этого наш редактор должен был бы понимать, где начинаются и заканчиваются запросы. Наш наивный первоначальный подход состоял в том, чтобы разбить строку всего текста редактора точкой с запятой (или, возможно, точкой с запятой + новая строка), и понимать каждую строку, полученную в результате этого разделения, как отдельный запрос. Конечно, этот наивный подход не был полностью надежным, потому что точка с запятой могла существовать в строке или в комментарии внутри SQL-запроса. Мы бы не хотели разбиваться на такой точке с запятой.

Наивного разбиения на точки с запятой было недостаточно. Наш редактор должен был бы понимать комментарии и строки SQL, чтобы знать, когда точка с запятой находится внутри одного из них, а не используется для обозначения конца оператора. Именно тогда, чтобы реализовать это более глубокое понимание текста редактора, мы решили использовать ANTLR во внешнем интерфейсе.

ANTLR — базовое понимание SQL-запросов

ANTLR — мощный инструмент для анализа языка. Из указанной грамматики (набора правил) ANTLR генерирует лексер и парсер, которые вместе могут построить дерево из входных данных (в нашем случае строки SQL), и прослушиватель, который может выполнять логику при посещении этого дерева. На самом деле, мы уже используем ANTLR для разбора операторов SQL в бэкэнде, и дерево, которое мы получаем из него, используется для понимания и выполнения пользовательских операторов SQL!

Грамматика

Начнем с самого начала. После загрузки ANTLR нам нужно сделать файл грамматики. В качестве примера ознакомьтесь с Грамматикой Presto SQL ANTLR. Грамматика Rockset для синтаксического анализа запросов в серверной части на самом деле является модифицированной версией этой грамматики Presto. Однако для разделения запросов во внешнем интерфейсе нас в основном интересуют правила грамматики для комментариев и строк:

STRING
: '\'' ( ~'\'' | '\'\'' )* '\''
;
COMMENT
: SIMPLE_COMMENT | BRACKETED_COMMENT
;
fragment SIMPLE_COMMENT
: '--' ~[\r\n]* '\r'? '\n'?
;
fragment BRACKETED_COMMENT
: '/*' .*? '*/'
;

Суть происходящего здесь заключается в определении последовательности символов строки, составляющих правило, т. е. строки или комментария. Эти правила грамматики точно скопированы из нашего внутреннего файла грамматики, поэтому мы можем быть уверены, что грамматика разделения запросов во внешнем интерфейсе понимает комментарии и строки точно так же, как и наша внутренняя система. Глядя на остальные наши правила грамматики:

queriesText
: statement* EOF
;
statement
: ';'* (CHAR | STRING | COMMENT)+ ';'*
;
CHAR
: ~';'
;

Мы можем видеть, что весь queryText просто определен как серия из 0 или более операторов, и эти операторы определены как смесь символов (в нашем случае ничего, кроме точек с запятой), комментариев и строк, которые находятся между точками с запятой. Ключевым моментом здесь является то, что точки с запятой могут располагаться внутри комментариев и строк и быть частью операторов, как и должно быть.

Здесь — это полный файл грамматики со всеми вышеперечисленными правилами.

Построение дерева

Сила ANTLR заключается в создании файлов из этой грамматики, с помощью которых мы можем выполнять нашу логику.

antlr4 -Dlanguage=JavaScript <GrammarFileName>.g4

это команда, используемая для указания того, что нашим целевым языком является JavaScript, и ее запуск создает множество файлов для использования в JavaScript, включая лексер, синтаксический анализатор и прослушиватель. См. эту документацию для более подробной информации. Давайте посмотрим на функцию, которую мы в итоге написали, используя все эти файлы, чтобы понять, что они делают.

Здесь мы видим, что лексер и синтаксический анализатор помогают нам построить дерево из строкового ввода, используя нашу грамматику. Возьмем '[statement1][statement2][statement3]' в качестве примера строкового ввода, где каждый [statement] — это часть строки, которая соответствует правилу грамматики для выражения, например 'abc/*comment;;*/def;'. Входная строка с тремя операторами будет преобразована в дерево, которое выглядит следующим образом:

Каждый узел дерева основан на правиле грамматики, которое встречается во входной строке, а потомки этого узла — это компоненты (основанные на других правилах грамматики), которые содержит этот узел. Корневой узел основан на правиле грамматики queryText, которое существует во входной строке как вся строка. Таким образом, корневой узел представляет всю входную строку. Его дочерние элементы являются компонентами, составляющими этот узел. Мы можем видеть из правила грамматики queryText, что queryText состоит из ряда утверждений. Таким образом, в дереве дочерние узлы корневого узла queryText являются узлами для каждого из этих операторов. Точно так же дочерние элементы каждого узла оператора являются компонентами этого оператора, то есть конкретными символами, строками и комментариями в этом операторе.

Здесь важно отметить, что операторы, которые существуют во входном дереве, успешно разбираются на основе нашей грамматики в отдельные узлы дерева. Разделение операторов во входной строке было нашей первоначальной целью.

Слушатель (прогулка по дереву)

Теперь, когда у нас есть работающее дерево, пришло время проделать над ним логику. Слушатель помогает нам в этом. Возвращаясь к функции SplitQueries, мы видим, что мы «используем» дерево, проходя по нему с помощью CustomListener.

CustomListener — это файл, который мы пишем, наследуя от сгенерированного файла слушателя. Сгенерированный базовый прослушиватель предоставляет функции enter и exit для каждого проанализированного узла, который находится в дереве. Именно эти функции выполняются при обходе дерева. Это работает следующим образом:

Во время обхода дерева вход/выход из каждого узла вызывает соответствующую функцию входа/выхода этого узла в прослушивателе, в нашем случае CustomListener (слушателе, с которым мы проходим по дереву). Функции входа/выхода в сгенерированном базовом классе прослушивателя пусты и ничего не делают, поэтому, если мы хотим выполнить логику при входе или выходе из узла во время обхода, нам нужно переопределить эти функции в CustomListener и наши пользовательские функции. будет казнен.

Единственная функция, которую нам нужно переопределить в CustomListener, — это функция exitStatement, функция, вызываемая при выходе из узла оператора в дереве. Помните, благодаря нашей грамматике узел оператора в дереве точно представляет оператор в нашем входном тексте. После выхода из узла оператора мы хотим получить местоположение этого оператора во входной текстовой строке. К счастью, функция слушателя принимает в качестве аргумента ctx узла, который содержит много информации, но особенно полезный для нас, ctx содержит начальный и конечный индексы этого узла во входной строке.

Итак, реализация функции exitStatement становится очень простой: записывайте и сохраняйте начальные и конечные индексы каждого оператора при выходе из него, чтобы в конце обхода у нас был набор всех начальных и конечных индексов, сообщающих нам где каждый оператор запроса начинается и заканчивается входной строкой.

Вот наш CustomListener с функцией exitStatement:

Функция SplitQueries

В заключение нашего использования ANTLR давайте в последний раз взглянем на функцию SplitQueries. Во-первых, мы создаем грамматику, определяющую простые операторы SQL и некоторые их основные компоненты. Затем из этой грамматики мы генерируем лексер, синтаксический анализатор и слушатель. В нашей функции SplitQueries мы берем на вход строку и используем лексер и синтаксический анализатор, чтобы построить из нее дерево, используя правила грамматики. По-прежнему внутри функции SplitQueries мы затем выполняем обход дерева, используя наш customListener, который записывает начальный и конечный индексы каждого оператора. SplitQueries возвращает этот набор индексов.

Из нашего входного текста мы получаем вывод местоположений индексов наших отдельных операторов SQL, операторов SQL, которые заканчиваются точкой с запятой, но могут содержать их в своих комментариях и строках, как определено грамматикой ANTLR.

Выбор пользовательского запроса с помощью SplitQueries и положения курсора

С грамматикой ANTLR и возможностями синтаксического анализа, упакованными в функцию SplitQueries, пришло время завершить этот проект, используя эту функцию для выбора одного пользовательского запроса из текста многих запросов в редакторе запросов консоли. Что мы делаем для этого:

  1. Используйте SplitQueries для введенного пользователем текста, чтобы получить начальный и конечный индексы каждого запроса в этом тексте.
  2. Выберите запрос, начальный и конечный индексы которого содержат позицию курсора пользователя, то есть мы выбираем запрос, внутри которого щелкнул пользователь. (В некоторых особых случаях, например при выборе пустой строки, мы вместо этого выбираем соседний запрос.)

Делая это, мы можем выбрать пользовательский запрос, разделенный точкой с запятой, даже если в этом запросе есть точка с запятой в строках или комментариях, и все потому, что SplitQueries информируется о наших правилах грамматики ANTLR при вычислении границ запроса.

Вывод

Мы рады, что в интерфейсе реализован строгий анализ грамматики с использованием ANTLR. Попробуйте ввести несколько запросов в Rockset Console, чтобы увидеть, как работает эта функция! Теперь, когда ANTLR встроен в наш интерфейс, мы также с нетерпением ждем возможности выполнять различные виды синтаксического анализа в консоли.

Ресурсы для ANTLR в JavaScript:

Вот копия кода ANTLR/js, используемого в Rockset Console. Это включает в себя грамматику, сгенерированные файлы, CustomListener и функцию SplitQueries. Не стесняйтесь возиться с грамматикой и регенерировать файлы, используя:

antlr4 -Dlanguage=JavaScript QuerySeparationGrammar.g4

Вы можете запускать скрипт SplitQueries на своих собственных входных строках!

Первоначально опубликовано на https://rockset.com 26 июля 2019 г.