Интерпретатор языка PL/SQL

2fb914e0cb04f55c3ce01db84b332e92

Здравствуйте! Меня зовут Алексей Калинин. Я Oracle PL/SQL-разработчик. Это моя первая статья. По работе 90% времени занимает написание серверного кода на языке PL/SQL. Всем хорош PL/SQL — и простой синтаксис, и неявное преобразование типов, и поддержка SQL-инструкция внутри кода для обработки данных в БД. Из минусов только то, что PL/SQL— код может выполнятся только в БД Oracle. А хотелось, чтобы был бы язык с синтаксисом, похожим на PL/SQL, но выполнялся он на клиенте (не был привязан к какой-нибудь БД). Умел бы соединяться с различными БД для выполнения операций с данными.

Для чего же можно было бы применять этот клиентский PL/SQL.

  • Для того, чтобы можно было бы работать с локальными файлами: загружать данные из файлов напрямик в БД, выгружать данные из БД в локальные файлы.

  • Открывать в одном PL/SQL-коде несколько соединений к различным БД (Oracle, PostgreSQL, MsSQL, MySQL, SQLLite и т.д.) для сверки, анализа данных, перегрузки данных между различными БД и т.д.

Тема парсинга и интерпретации программного кода интересовала мне давно. И я решил попробовать реализовать интерпретатор языка, похожего по синтаксису на PL/SQL. В качестве языка реализации интерпретатора я выбрал Delphi.

Описание придуманного языка программирования

По синтаксису данный язык очень похож на Oracle PL/SQL. Что то было привнесено в него из PLpg/SQL для Postgres.

Опишу только то, что из базового синтаксиса PL/SQL было реализовано, и что было изменено и добавлено.

1. Описание типов данных (какие типы данных поддерживаются).

  • VARCHAR2 — Этот тип данных хранит строку. Если указана длина строки — то будет происходить проверка длины в коде, если длина не указана — то можно записывать данные длиной не более 100000000.

  • INTEGER — Этот тип данных используется для работы с полями, которые будут содержать только целые числовые данные.

  • NUMBER — Этот тип данных используется для работы с полями, которые будут содержать только числовые данные с фиксированной или плавающей точкой.

  • DATE — Этот тип данных хранит значения в формате даты (dd.mm.yyyy hh24: mi: ss).

  • JSON — Этот тип данных является форматом для хранения и передачи данных в форме объектов, массивов и других значений, которые можно легко преобразовать в текстовую строку.

  • RECORD — Этот тип данных похож на переменную строкового типа, но он не имеют предопределённой структуры. Он приобретают фактическую структуру от строки, которая им присваивается командами SELECT или FOR. Структура переменной типа record может меняться каждый раз при присвоении значения.

  • BOOLEAN — Этот тип данных хранит логические значения (true/false).

2. Курсоры.

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

Пользователю доступны следующие способы выполнения запроса к базе данных:

  • Неявные курсоры — простой оператор SELECT … INTO извлекает одну строку данных непосредственно в переменные локальной программы. Это удобный (и часто наиболее эффективный) способ доступа к данным, использование которого, однако, может приводить к необходимости повторного кодирования оператора SELECT (или похожих операторов) в нескольких местах программы.

  • Явные курсоры — вы можете явно объявить курсов в разделе объявлений. В этом случае курсор можно будет открывать и извлекать данные в одной или нескольких программах, причем возможности контроля будут шире, чем при использовании неявных курсоров.

3. Операторы FOR, WHILE, OPEN, IF.

В данной реализации можно использовать следующие операторы:

  • Операторы цикла: FOR, LOOP, WHILE.

  • Условный оператор: IF THEN ELSE.

  • Оператор: OPEN FETCH.

4. Также добавлены встроенные процедуры/функции.

  • dbms_output.put_line (text) — вывод текстового сообщения (как в Oracle).

  • upper (text) — перевод текста в верхний регистр.

  • lower (text) — перевод текста в нижний регистр.

  • rtrim (text) — отрезать пробелы справа.

  • ltrim (text) — отрезать пробелы слева.

  • trim (text) — отрезать пробелы со всех сторон.

  • replace (text, text_to_replace, replacement_text) — замена текста по образцу.

  • substr (text, start_position, length) — извлечь подстроку из строки.

  • instr (text, substring) — возвращает n-е вхождение подстроки в строке.

  • length (text) — возвращает длину строки.

  • chr (number_code) — возвращает символ, который основан на числовом коде.

  • host (command, work_dir) — выполним cmd-команду и вернем результат (work_dir — рабочий каталог (например: c:\)).

Также были реализованы дополнительные пакеты для работы с локальными файлами, для отправки http/https запросов, для работы с json-объектами.

5. Пакет TEXT_IO.

Для работы с локальными файлами имеется встроенный пакет — TEXT_IO. (похожий пакет когда-то был в Oracle Forms)

Он содержит следующие методы:

  • text_io.fopen ('полный путь к файлу', 'ключ', кодировка) — Открытие файла. Ключ содержит следующие значения — 'r' — только чтение файла, 'w' — только запись в файл. Кодировка — UTF8 или ANSI.

  • text_io.count_lines (file1) — Прочитать количество строк из файла.

  • text_io.get_line (file1, index) — Прочитать строку из файла по индексу (index).

  • text_io.get_file_content (file1) — Прочитать все содержимое файла.

  • text_io.put_line (file1, 'строка') — Записать строку в файл.

  • text_io.fclose (file1) — Закрытие файла.

6. Пакет HTTP_IO.

(Что то похожее также есть в Oracle)

Для доступа к данным в Интернете через протокол HTTP/HTTPS имеется встроенный пакет — HTTP_IO. Я его использую для отладки http/https-запросов к rest-серверам. Он содержит следующие методы:

  • http_io.begin_request (url, method) — инициализация переменной.

  • http_io.set_header (http_req, 'ключ', 'значение') — установить заголовок

  • http_io.set_resp_encoding (http_req, 'UTF8') — установить кодировку ответа.

  • http_io.write_text (http_req, 'тело отправляемого сообщения') — установить тело отправляемого сообщения.

  • http_io.get_response (http_req) — перейдем по url и получим ответ.

  • status_code — код ответа

  • response_text — текст ответа

7. Пакет JSON_IO.

Для работы с JSON-объектами имеется встроенный пакет — JSON_IO. Он содержит следующие методы:

  • json_io.add (js_obj1, 'ключ', 'значение') — добавить в json-объект пару ключ-значение.

  • json_io.format (js_obj1) — вывод json-кода в отформатированном виде.

  • json_io.record_to_json (rec) — преобразовать значение переменной типа record в json-формат.

  • json_io.count_keys (js_obj1) — вывести количество пар ключ-значение.

  • json_io.count_rows (js_obj1) — вывести количество значений внутри json-массива.

  • json_io.get_key_name (js_obj1, index) — вывести название ключа по индексу (index).

  • json_io.get_key_value (js_obj1, index) — вывести значение ключа по индексу (index).

  • json_io.get_row (js_obj1, index) — вывести json-код элемента массива по индексу (index).

8. Пакет RECORD_IO.

Для работы с переменными типа record имеется встроенный пакет — RECORD_IO. Он содержит следующие методы:

  • record_io.add_field (rec1, ['название_поля', 'тип_поля']) — добавить в переменную типа record новое поле (новые поля…)

  • record_io.print_record (rec1) — вывод содержимого переменной типа record (rec1).

  • record_io.json_to_record (obj1, rec1) — преобразовать значение переменной типа json (obj1) в переменную типа record (rec1).

Так как интерпретируемый код выполняется на клиенте, то он не знает в какой БД выполнять SQL-инструкцию.

Для этого было добавлено следующее обязательное условие:

Внутри описания любой SQL-инструкции (курсора, select/insert/update/delete-выражения) в обязательном порядке должна быть указана метка /*##db=dbname##*/, в которой указано название БД, для которой будет выполнятся данная конструкция. Описание параметров соединения с БД заранее передаются в интерпретатор.

Описание работы интерпретатора

Реализация интерпретатора была разделена на три этапа:

На первом этапе мною был реализован парсер текста выполняемой программы. Весь тест был разделен на токены и помещен в массив. В интернете масса примеров кода парсеров, которые разделяют код на токены разного типа.

На втором этапе по разобранным токенам я построил AST (абстрактное синтаксическое дерево) дерево.

На третьем этапе реализуется сам интерпретатор, который обходит это дерево и выполняет его. Если AST-дерево построено достаточно подробно — то сам механизм интерпретации получается несложным.

Рассмотрим пример:

Интерпретируемый (выполняемый) код

declare 
  ind integer; 
  header_id_ number(10);
   
  cursor cur_main_1(arg_ varchar2) is 
      select * 
          from /*##db=db_name##*/ 
              tbl_name d 
          where d.status = 0 and 
              d.billhead_id = header_id_ and 
              (arg_ is null or d.name like '%'||arg_||'%') 
          order by d.name;
           
  cnt integer; 
  patt_name varchar2(255); 
begin 
  header_id_ := 911988; 
  patt_name := 'L-тироксин 150'; 
  dbms_output.put_line('---Test 1-----');
   
  -- Откроем курсор и внутри цикла for выполним курсор 
  for rec in cur_main_1(patt_name) loop 
      select count(*) 
          into cnt 
      from /*##db=db_name##*/ 
           tbl_name_2 mr 
      where mr.name = rec.name; 
      dbms_output.put_line(' Name='||rec.name||', cnt='||cnt); 
      dbms_output.put_line('rec.id='||rec.id); 
  end loop; 
end;

Разделяем текст на токены (парсинг текста) и строим абстрактное синтаксическое дерево (AST). Чем точнее написано AST дерево — тем проще дальнейшая интерпретация (выполнение) кода.

Построенное AST-дерево.