- BrainTools - https://www.braintools.ru -

DSL в эпоху ИИ: делать или не делать…

Искусственный интеллект создаёт код по заказу пользователя

Искусственный интеллект [1] создаёт код по заказу пользователя

Всем привет. Меня зовут Сергей и я руковожу разработкой автоматизации внутренних бизнес-процессов в рамках Центра управления процессами Московской Биржи.

В эпоху повсеместного распространения инструментов на базе искусственного интеллекта (ИИ) вопрос применения доменно-специфичных языков (DSL, domain specific language) вызывает неоднозначную реакцию [2]. С одной стороны, большие языковые модели прекрасно понимают широкое многообразие естественного языка и могут работать напрямую с ним. С другой стороны, присущая любому человеческому общению неоднозначность оставляет простор для неправильных трактовок и ошибок.

Введение

Как отмечают многие исследователи, кодовые агенты наиболее эффективно работают в хорошо отстроенной системе ограничений. Ограничения могут быть мягкими, рекомендательными, как промпты и скиллы, так и жёсткими, например, компилятор или автоматическое тестирование. Задача разработчика при использовании ИИ сводится не столько к написанию кода, сколько к формированию качественного контекста и набора ограничений. Ручное тестирование и выявление ошибок кардинально увеличивает цикл обратной связи для ИИ-агента и должно применяться только в крайних случаях, когда цена ошибки [3] слишком высока.

Мы в компании MOEX внимательно относимся к процессу разработки и рекомендациям службы информационной безопасности, чтобы предупредить последствия публикации некачественного кода. Это значит, что все наши проекты проходят проверки статическими и динамическими анализаторами кода, проходят композиционный анализ и тесты на проникновение. Со своей стороны мы делаем всё возможное, чтобы соответствовать положению 757-П Центрального Банка РФ «Об установлении обязательных для некредитных финансовых организаций требований к обеспечению защиты информации при осуществлении деятельности в сфере финансовых рынков в целях противодействия осуществлению незаконных финансовых операций».

Наиболее быструю обратную связь ИИ-агент получает от компилятора (интерпретатора) языка. Если мы хотим обеспечить строгую семантику критической части нашей логики, то есть проверенный способ построения доменно-специфичных языков на основе языков программирования общего назначения (GPPL, general purpose programming language). В статье рассмотрен подход по формированию типо-безопасного доступа к данным в условиях нестандартного интерфейса источника данных и его логических ограничений.

Каждый бэкенд-разработчик, работавший с реляционными базами, знает соблазн «просто собрать SQL-строку конкатенацией». Соблазн этот опасен: динамический SQL — классический вектор SQL-инъекций, и специалисты по информационной безопасности справедливо бьют по рукам за подобный код. Разбираемый в этой статье демо-проект [1] показывает элегантное решение: типо-безопасный DSL на базе библиотеки jOOQ, который превращает Java-код в SQL-запросы к резидентному хранилищу данных (In-Memory Data Grid, IMDG) вроде Apache Ignite, доступному только через кастомный REST-контракт. Ниже — разбор исторического контекста DSL, детальный анализ архитектуры проекта, аналогия с JPA (Java Persistence API) и взгляд на будущее DSL в эпоху ИИ-ассистированной разработки.

Исторические потребности и подходы к разработке DSL

Идея предметно-ориентированных языков не нова — она восходит к Lisp 1958 года, разработанному Джоном Маккарти, чья гомоиконность и макросистема позволяли встраивать мини-языки прямо в код общего назначения [2]. С тех пор DSL прошли путь от академических экспериментов до промышленного стандарта: SQL остаётся, вероятно, самым успешным DSL в истории, специализированным исключительно на манипуляции данными [3].

Классическая методология разработки DSL, зафиксированная ещё в работах по предметно-ориентированному программированию, включает три фазы [2][4]:

  • анализ — определение границ предметной области, сбор знаний экспертов, построение семантических нотаций;

  • реализация — создание библиотеки, воплощающей эти нотации, и компилятора/интерпретатора, транслирующего DSL-программы в вызовы этой библиотеки;

  • применение — написание и выполнение DSL-программ конечными разработчиками.

Исторически DSL делятся на внешние (со своим синтаксисом и парсером, как BNF (форма грамматики Бэкуса-Наура) или регулярные выражения) и внутренние — встроенные в синтаксис языка-хозяина через классы, методы и fluent-интерфейсы [5]. Второй подход резко снижает стоимость разработки: не нужен собственный парсер, IDE «из коробки» даёт автодополнение и проверку типов на этапе компиляции. Именно на этом принципе построены jOOQ, LINQ в .NET и Criteria API в JPA — все они реализуют внутренний DSL для построения запросов средствами языка общего назначения, избегая при этом уязвимостей текстовой конкатенации SQL. Ключевая мотивация [4] подобных решений всегда одна и та же: изолировать разработчика от синтаксических ошибок и рисков инъекций, зафиксировав грамматику допустимых операций на уровне API [6].

К 2020-м годам интерес [5] к DSL пережил новую волну — от финансовых расчётов до биоинформатики и низкокодовых платформ, а инструментарий для их создания расширился: ANTLR, Xtext, JetBrains MPS, встроенные метапрограммные возможности Kotlin и Scala [7]. Разбираемый ниже проект — характерный пример именно внутреннего DSL, решающего узкую, но болезненную задачу: безопасный доступ к нестандартному хранилищу данных.

Практический пример: DSL для IMDG на базе jOOQ

Постановка задачи и ограничения предметной области

В качестве примера возьмём структуру данных пользователей торговой системы Spectra срочного рынка Московской Биржи. Её контракт публичный и доступен для изучения всем желающим [17].

Проект jooq-dsl-demo [1] моделирует ситуацию, типичную для крупных финансовых и биржевых инфраструктур: данные хранятся в резидентной сетке (IMDG на основе Apache Ignite), доступ к которой предоставляется не через стандартный JDBC-драйвер, а через кастомный REST-эндпоинт /api/v2/queries/adhoc, принимающий SQL-текст в теле POST-запроса. Такая архитектура типична для Ignite: даже нативный REST API движка построен вокруг команд qryfldexe с SQL-подобным синтаксисом, инкапсулированным в HTTP-запрос [8].

При этом IMDG предметной области накладывает существенные ограничения на диалект SQL [1]:

  • отсутствует Data Definition Language (DDL) — схему нельзя изменить средствами SQL;

  • отсутствуют операции записи — INSERT, UPDATE, DELETE;

  • отсутствует поддержка JOIN между таблицами в SELECT-запросах;

  • имена «таблиц» фактически являются полностью квалифицированными именами Java-классов, а не классическими SQL-идентификаторами.

Эти ограничения — прямое следствие природы Ignite как объектно-ориентированного in-memory хранилища, где «таблица» — это по сути кэш объектов одного типа, а не запись в нормализованной реляционной схеме [9][10]. Именно поэтому официальная документация Ignite явно предупреждает: SqlFieldsQuery поддерживает произвольный SELECT, но объединения между кэшами требуют явного префиксирования именами кэшей, а не стандартного JOIN-синтаксиса [10].

Аналогия с JPA: разделение модели, транспорта и языка запросов

Архитектура DSL-доступа к IMDG через jOOQ

Архитектура DSL-доступа к IMDG через jOOQ

Схема архитектуры демо-проекта наглядно показывает три слоя: DSL-код на Java, runtime jOOQ, транспортный адаптер к REST и сам сервис IMDG. Разработчикам, знакомым с Java Persistence API, архитектура проекта покажется узнаваемой. JPA и Hibernate решают концептуально ту же задачу — дать разработчику безопасный, типизированный способ работать с хранилищем данных, не обращаясь к «сырому» SQL напрямую. Сопоставление слоёв даёт наглядную аналогию.

Слой ответственности

JPA / Hibernate

jOOQ-DSL для IMDG

Объектная модель

@Entity, @Table, аннотированные поля класса

Классы USER, UserRecord, сгенерированные jooq-codegen-maven из XML-схемы

Источник схемы

Аннотации в коде или обратная генерация из БД

Файл user-table.xml в формате information_schema, созданный вручную/через LLM из JSON-ответа сервиса

Транспортный слой

EntityManager + JDBC-драйвер

RestMockDataProvider, реализующий MockDataProvider и подменяющий JDBC-соединение HTTP-вызовом

Язык запросов

JPQL, Criteria API

DSLContext.select().from().where()— типобезопасный fluent DSL jOOQ

Контроль допустимых операций

Ограничения диалекта JPQL, каскады, @Transactional

ReadOnlyExecuteListener и NoJoinVisitListener — явные проверки AST-запроса

Центральная идея обеих экосистем — отделить бизнес-код от деталей транспорта и синтаксиса запроса. В JPA это достигается через EntityManager, который абстрагирует JDBC; в разбираемом проекте эту роль исполняет класс RestMockDataProvider, реализующий интерфейс MockDataProvider из тестового набора jOOQ [1]. Он подключается к MockConnection, которая реализует java.sql.Connection, поэтому весь остальной код — конфигурация DSLContext, построение запроса — работает так, будто использует настоящий JDBC-драйвер, хотя фактически SQL-текст, полученный из AST после inlineSql(), отправляется HTTP-клиентом в теле JSON-запроса на REST-эндпоинт сервиса.

Конвертация метаданных сервиса в XML-схему для jOOQ

Ключевой инженерный трюк проекта — обратная разработка (reverse engineering) схемы данных. Поскольку сервис не предоставляет стандартного механизма получения DDL, мы конвертируем метаданные ответа сервиса в формате JSON (файл user-data.json) в XML-описание таблицы user-table.xml, соответствующее XSD-схеме jooq-meta [1]. Указанная трансформация JSON → XML выполнена с применением LLM-модели [1] — то есть уже на этапе разработки инструмента искусственный интеллект использовался как ассистент кодогенерации метаданных.

Файл user-table.xml описывает каталог default, схему public и единственную таблицу user с двадцатью одной колонкой типов TIMESTAMP, BIGINT, INTEGER, SMALLINT и VARCHAR, а также первичный ключ по полю login. Именно этот XML-файл читает jooq-codegen-maven, встроенный в сборку проекта, и генерирует на его основе Java-классы USER (метаданные таблицы) и UserRecord (типизированная строка результата) — аналог того, как Hibernate по аннотациям @Entity строит внутреннюю объектную модель. Разница в том, что в мире IMDG обратный путь короче: нет команды SHOW CREATE TABLE, поэтому схему нужно либо получать из документации сервиса, либо — как в этом случае — синтезировать её из примера ответа API, распознав типы значений и структуру полей.

Реализация ограничений: только чтение и запрет JOIN

Ограничения предметной области — отсутствие записи и джойнов — реализованы не как декларативные настройки, а как явные перехватчики (listener), встроенные в конфигурацию DSLContext через провайдеры jOOQ [1]:

  • ReadOnlyExecuteListener расширяет DefaultExecuteListener и в методе renderStart проверяет тип выполняемой операции (ExecuteContext.type()); если тип равен ExecuteType.WRITE, выбрасывается DataAccessException с сообщением о запрете INSERT/UPDATE/DELETE

  • NoJoinVisitListener расширяет DefaultVisitListener и в методе visitStart анализирует узлы AST‑дерева запроса; при обнаружении узла Clause.TABLE_JOIN также выбрасывается исключение, блокирующее компиляцию запроса до отправки на сервер

Оба перехватчика подключаются к DefaultConfiguration через DefaultExecuteListenerProvider и DefaultVisitListenerProvider соответственно — паттерн, типичный для jOOQ, где поведение [6] библиотеки расширяется через слушатели жизненного цикла запроса, а не через модификацию генерируемого кода. Это принципиально отличает подход от «постфактум»-валидации SQL-строки регулярными выражениями: проверка происходит на уровне синтаксического дерева запроса, до его материализации в текст, что исключает как ложноположительные, так и ложноотрицательные срабатывания, характерные для парсинга готовой SQL-строки.

Third-party контракт REST API реализован в RestMockDataProvider.execute(): метод подставляет значения параметров прямо в SQL-текст (метод inlineSql), формирует JSON-тело с полем sql, отправляет POST-запрос на /api/v2/queries/adhoc через стандартный java.net.http.HttpClient и разбирает ответ, извлекая массив объектов из пути response.result в JSON. Далее полученные строки превращаются в jOOQ Result<Record> через вспомогательный DSLContext.newResult(), что позволяет использовать стандартный механизм result.into(UserRecord.class) для типизации итогового набора записей. Для локальной отладки без реального сервиса IMDG в проекте используется WireMock, эмулирующий REST-эндпоинт и возвращающий заранее подготовленный JSON-файл user-data.json [1].

В качестве развития инструмента можно рассмотреть доработку maven плагина кодогенерации, обеспечив возможность генерации кода из метаданных в формате JSON без промежуточного XML-представления.

Перспективы DSL в эпоху AI-ассистированной разработки

Использование LLM для генерации XML-схемы из JSON-примера в разбираемом проекте — не случайность [7], а симптом более широкой тенденции. Внутренние DSL традиционно были компромиссом между выразительностью внешнего языка и простотой встраивания в существующую кодовую базу; ограничение всегда заключалось в том, что написание самого DSL и правил его валидации требовало экспертизы и в предметной области, и в языковой инженерии одновременно [6].

AI-инструменты меняют это уравнение сразу в нескольких направлениях. Во-первых, LLM снижают порог входа в domain-анализ: там, где ранее аналитику требовались недели интервью с экспертами предметной области для формализации семантики (классический этап анализа в методологии DSL [4]), сейчас модель может за минуты предложить первичную схему данных на основе примеров вывода системы — именно так был получен user-table.xml из user-data.json в разбираемом проекте. Во-вторых, LLM способны генерировать сам код DSL-обёртки — классы записей, конфигурации RenderMapping, перехватчики валидации — по текстовому описанию ограничений домена, ускоряя фазу реализации.

Более глубокая перспектива касается роли DSL как «контракта» между человеком и AI-агентом, генерирующим код. Строго типизированный внутренний DSL, подобный jOOQ, естественно ограничивает пространство возможных ошибок AI-генерации: компилятор Java отклонит некорректный вызов select().from().where() на этапе сборки, тогда как ошибка в текстовой SQL-строке, сгенерированной LLM, обнаружится только в runtime или вовсе останется незамеченной до продакшена. Комбинация «AI генерирует вызовы DSL» + «DSL гарантирует безопасность на уровне типов и AST-валидации» — жизнеспособный паттерн для доверенной AI-кодогенерации в чувствительных доменах, включая финансовую инфраструктуру, где написан этот демо-проект.

Одновременно ограничения текущих LLM создают новые требования к дизайну DSL: языки и API должны быть спроектированы так, чтобы модели было легко их «понимать» и корректно применять — с предсказуемой, консистентной сигнатурой методов, богатой типизацией и минимумом неявных побочных эффектов. Это созвучно классическим принципам хорошего DSL — минимализму и ясности синтаксиса [7] — но добавляет новый критерий: предсказуемость для statistical pattern matching, лежащего в основе генерации кода современными моделями.

Перспективы DSL для языков программирования вне Java

Хотя разбираемый проект реализован на Java, сама идея — типобезопасная обёртка над нестандартным транспортом данных с проверкой AST на этапе построения запроса — универсальна и переносима на другие экосистемы.

Экосистема

Аналог внутреннего DSL для запросов

Особенности переноса подхода

.NET / C#

LINQ (Language Integrated Query)

Компилятор C# нативно поддерживает выражения‑деревья (Expression Trees), что упрощает статическую валидацию запросов без сторонних библиотек

Kotlin

Exposed, Ktorm

Kotlin DSL-builders на основе lambda-с-receiver дают синтаксис, близкий к SQL, при полной типобезопасности

Scala

Slick, Quill

Богатая система типов и макросы Scala позволяют генерировать SQL на этапе компиляции, а не runtime

Python

SQLAlchemy Core, Django ORM QuerySet

Динамическая типизация ограничивает compile-time проверки, но допускает runtime-валидацию через AST-подобные объекты выражений

Rust

Diesel, SeaORM

Строгая система типов и трейты позволяют закодировать ограничения предметной области (например, «только SELECT») прямо в типах на этапе компиляции

Go

Squirrel, GORM

Отсутствие обобщений в старых версиях ограничивало выразительность DSL; начиная с Go 1.18 обобщённые типы приближают библиотеки к возможностям jOOQ

Для функциональных языков, как Scala, стоит отметить, что их экосистема традиционно предлагает наиболее развитый инструментарий для встроенных DSL благодаря выразительной системе типов, неявным преобразованиям (implicit conversions) и макросам, что делает библиотеки Slick и Quill естественным аналогом jOOQ для доступа к нестандартным источникам данных, включая REST-обёртки над Ignite или другими IMDG. Общий паттерн — «сгенерировать метамодель из схемы (реальной или восстановленной), обернуть транспорт в адаптер к стандартному интерфейсу платформы, добавить перехватчики для доменных ограничений» — переносим почти без изменений: в Rust роль перехватчиков могут взять на себя типы-обёртки (newtype pattern), запрещающие построение запроса с недопустимой клаузой уже на этапе компиляции, а в динамических языках типа Python — runtime-валидаторы AST, аналогичные NoJoinVisitListener.

Ключевой вывод для мультиязычных команд: чем более выразительна система типов языка, тем раньше — в идеале на этапе компиляции — можно перехватывать нарушения контракта предметной области, будь то запрет JOIN, запрет операций записи или иные бизнес-ограничения нестандартного хранилища данных. Это делает инвестиции в разработку внутреннего DSL оправданными именно для языков со статической типизацией и богатой системой типов — Scala, Kotlin, Rust, TypeScript — где безопасность, продемонстрированная в разбираемом Java-проекте, достигается «бесплатно» на этапе сборки, а не ценой дополнительных runtime-проверок.

Заключение

В статье описан метод борьбы с распространённой уязвимостью «Инъекция» в сетевых сервисах. По данным популярного рейтинга безопасности «OWASP Top 10» за 2025 год проблема инъекций, по-прежнему, занимает стабильное место в середине списка. С учётом взрывного увеличения количества кода, сгенерированного ИИ, решение проблем защиты усложняется в разы. Своей целью в MOEX мы видим постоянный поиск и применение новых технологий, но со зрячим контролем результата и нивелированием возникающих вызовов для информационной безопасности.


Источники

  1. GitHub — Unlocker/jooq‑dsl‑demo — исходный репозиторий с демо‑проектом DSL для доступа к IMDG через jOOQ (README, исходный код Java‑классов, user‑table.xml, user‑data.json,pom.xml) — github [8]

  2. Domain Specific Language (DSL) Tools — методология анализа, реализации и применения DSL — 2006.secrus [9]

  3. Apache Ignite Quick Start Guide, O’Reilly — Field based query — описание SqlFieldsQuery и ограничений SELECT‑запросов в Ignite — oreilly [10]

  4. Концепция DSL и особенности практического применения — исторический и практический обзор DSL на русском языке  — rusnauka [11]

  5. Apache Ignite.NET — Class SqlFieldsQuery (официальная документация)  — ignite.apache [12]

  6. Apache Ignite — SQL API (официальная документация)  — ignite.apache [13]

  7. Querying with Apache Ignite (LinkedIn, Soumya Jyoti Banerjee) — практический разбор SQL‑запросов в Ignite — LinkedIn [14]

  8. GridGain Documentation — REST API — описание REST‑контракта для доступа к данным IMDG — gridgain [15]

  9. How to run Apache Ignite SQL queries over a REST API — stackoverflow [16]

  10. Создание DSL: как разработать собственный язык для специфической предметной области — it‑classic [17]

  11. GridGain SDK Javadoc — SqlFieldsQuery (Collocated Flag) — gridgain [18]

  12. Apache Ignite SQL query composed primary key.NET — stackoverflow [19]

  13. Предметно‑ориентированный язык — Википедия  — ru.wikipedia [20]

  14. Apache Ignite 3 Documentation — SQL API  — ignite.apache [21]

  15. Example of SQL query — Apache Mail Archives — lists.apache [22]

  16. When and how to develop domain‑specific languages — академическая работа о паттернах разработки DSL — dl.acm [23]

  17. Plaza II Шлюз. Версия 9.6 — Публичный FTP Московской Биржи — ftp.moex.com [24]

Автор: unlocker

Источник [25]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/36419

URLs in this post:

[1] интеллект: http://www.braintools.ru/article/7605

[2] реакцию: http://www.braintools.ru/article/1549

[3] ошибки: http://www.braintools.ru/article/4192

[4] мотивация: http://www.braintools.ru/article/9537

[5] интерес: http://www.braintools.ru/article/4220

[6] поведение: http://www.braintools.ru/article/9372

[7] случайность: http://www.braintools.ru/article/6560

[8] github: https://github.com/Unlocker/jooq-dsl-demo

[9] 2006.secrus: https://2006.secrus.org/upload/files/107.pdf

[10] oreilly: https://www.oreilly.com/library/view/apache-ignite-quick/9781789347531/1a7ffc0a-b55f-47d5-b8e1-7be0a636f7d5.xhtml

[11] rusnauka: http://www.rusnauka.com/29_DWS_2009/Informatica/53610.doc.htm

[12] ignite.apache: https://ignite.apache.org/releases/latest/dotnetdoc/api/Apache.Ignite.Core.Cache.Query.SqlFieldsQuery.html

[13] ignite.apache: https://ignite.apache.org/docs/latest/SQL/sql-api

[14] LinkedIn: https://www.linkedin.com/pulse/querying-apache-ignite-soumya-jyoti-banerjee

[15] gridgain: https://www.gridgain.com/docs/gridgain8/latest/developers-guide/restapi

[16] stackoverflow: https://stackoverflow.com/questions/48961733/how-to-run-apache-ignite-sql-queries-over-a-rest-api

[17] it‑classic: https://it-classic.ru/%D1%81%D0%BE%D0%B7%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5-dsl-%D0%BA%D0%B0%D0%BA-%D1%80%D0%B0%D0%B7%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%B0%D1%82%D1%8C-%D1%81%D0%BE%D0%B1%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D1%8B/

[18] gridgain: https://www.gridgain.com/sdk/latest/javadoc/org/apache/ignite/cache/query/SqlFieldsQuery.html

[19] stackoverflow: https://stackoverflow.com/questions/43210949/apache-ignite-sql-query-composed-primary-key-net/43212267

[20] ru.wikipedia: https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B5%D0%B4%D0%BC%D0%B5%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D1%8B%D0%B9_%D1%8F%D0%B7%D1%8B%D0%BA

[21] ignite.apache: https://ignite.apache.org/suggested-site/docs/ignite3/3.1.0/api-reference/native-clients/dotnet/sql-api/

[22] lists.apache: https://lists.apache.org/thread/m6fk3y7jf08fls76oy5nz0qdn2dy7lgv

[23] dl.acm: https://dl.acm.org/doi/10.1145/1118890.1118892

[24] ftp.moex.com: http://ftp.moex.com

[25] Источник: https://habr.com/ru/companies/moex/articles/1089584/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1089584

www.BrainTools.ru

Rambler's Top100