При работе с консолью запросов на управляемых формах разработчики нередко сталкиваются с ситуацией, когда запрос, содержащий внутри строкового литерала или комментария символ ; (точка с запятой), ошибочно делится на несколько отдельных запросов. В результате консоль выдает синтаксическую ошибку или выполняет запрос некорректно.
Проанализируем причину такого поведения и подробно разберем все способы решения этой задачи.
Рассмотрим алгоритм, который традиционно используется во многих базовых и упрощенных консолях запросов для разделения текста на отдельные запросы пакета. Функция ищет символ точки с запятой с помощью простой функции Найти():
Функция ПостроитьМассивТекстовЗапросов(Знач ТекстЗапроса)
МассивТекстов = Новый Массив;
Пока Не ПустаяСтрока(ТекстЗапроса) Цикл
ТочкаСЗапятой = ";";
ПозицияТочкиСЗапятой = Найти(ТекстЗапроса, ТочкаСЗапятой);
Если ПозицияТочкиСЗапятой = 0 Тогда
ТекстОчередногоЗапроса = ТекстЗапроса;
ПозицияТочкиСЗапятой = СтрДлина(ТекстЗапроса);
Иначе
ТекстОчередногоЗапроса = Лев(ТекстЗапроса, ПозицияТочкиСЗапятой - 1);
КонецЕсли;
Если Не ПустаяСтрока(ТекстОчередногоЗапроса) Тогда
МассивТекстов.Добавить(СокрЛП(ТекстОчередногоЗапроса));
КонецЕсли;
ТекстЗапроса = Сред(ТекстЗапроса, ПозицияТочкиСЗапятой + 1);
КонецЦикла;
Возврат МассивТекстов;
КонецФункции
Выясним причину сбоя: такой алгоритм не анализирует синтаксический контекст языка запросов 1С. Для него любой символ ; является признаком окончания очередного запроса в пакете. Однако символ точки с запятой может легитимно находиться:
""), например: ВЫБРАТЬ "Строка; с точкой" КАК Поле.//), например: // Разделитель строк; комментарий.Разберем основные варианты исправления и обхода этой проблемы.
Если нет возможности или желания модифицировать код самой консоли, самым простым решением является вынос строковых литералов, содержащих символ ;, в параметры запроса.
Посмотрим на пример. Вместо явного указания текста в теле запроса:
ВЫБРАТЬ
"Значение1;Значение2" КАК ПолеРазделитель
Укажем параметр и передадим строку через табличную часть параметров консоли:
ВЫБРАТЬ
&СтрокаСРазделителем КАК ПолеРазделитель
В таблице параметров консоли устанавливаем значение параметра &СтрокаСРазделителем равным Значение1;Значение2. В этом случае парсер консоли не встретит символ ; в теле запроса и обработает пакет без ошибок.
Разберем профессиональный платформенный подход к разбору запросов без написания собственных строковых парсеров. Для разбора пакета запросов в платформе 1С:Предприятие 8 предусмотрен объект СхемаЗапроса.
Объект СхемаЗапроса полностью учитывает синтаксис языка: кавычки, комментарии, структуры вложенных запросов и объединений. Перепишем функцию разделения пакета с его помощью:
Функция ПостроитьМассивТекстовЗапросов(Знач ИсходныйТекстЗапроса) Экспорт
МассивТекстов = Новый Массив;
Схема = Новый СхемаЗапроса;
Попытка
Схема.УстановитьТекстЗапроса(ИсходныйТекстЗапроса);
Для Каждого ЗапросПакета Из Схема.ПакетЗапросов Цикл
МассивТекстов.Добавить(ЗапросПакета.ПолучитьТекстЗапроса());
КонецЦикла;
Исключение
// Если текст синтаксически некорректен или не может быть разобран схемой,
// возвращаем исходный текст одним элементом для штатной обработки ошибки
МассивТекстов.Добавить(ИсходныйТекстЗапроса);
КонецПопытки;
Возврат МассивТекстов;
КонецФункции
Важное ограничение: метод СхемаЗапроса.УстановитьТекстЗапроса() проверяет корректность синтаксиса и существование объектов метаданных. Если в запросе допущена синтаксическая ошибка, возникнет исключение, поэтому блок Попытка ... Исключение здесь обязателен.
Если требуется разделять даже синтаксически невалидные запросы, реализуем посимвольный обход текста с отслеживанием двух состояний: нахождение внутри строки и нахождение внутри комментария.
Разберем по шагам логику такого парсера:
" переключаем флаг ВнутриСтроки, учитывая экранирование кавычек.// взводим флаг ВнутриКомментария, который сбрасывается только при переходе на новую строку (Символы.ПС).Ложь.
Функция ПостроитьМассивТекстовЗапросов(Знач ИсходныйТекстЗапроса) Экспорт
МассивТекстов = Новый Массив;
ДлинаТекста = СтрДлина(ИсходныйТекстЗапроса);
Если ДлинаТекста = 0 Тогда
Возврат МассивТекстов;
КонецЕсли;
ВнутриСтроки = Ложь;
ВнутриКомментария = Ложь;
ПозицияНачалаТекущегоЗапроса = 1;
Индекс = 1;
Пока Индекс <= ДлинаТекста Цикл
Символ = Сред(ИсходныйТекстЗапроса, Индекс, 1);
СледующийСимвол = ?(Индекс < ДлинаТекста, Сред(ИсходныйТекстЗапроса, Индекс + 1, 1), "");
Если ВнутриКомментария Тогда
Если Символ = Символы.ПС Или Символ = Символы.ВК Тогда
ВнутриКомментария = Ложь;
КонецЕсли;
ИначеЕсли ВнутриСтроки Тогда
Если Символ = """" Тогда
Если СледующийСимвол = """" Тогда
// Экранированная кавычка внутри литерала - пропускаем следующий символ
Индекс = Индекс + 1;
Иначе
ВнутриСтроки = Ложь;
КонецЕсли;
КонецЕсли;
Иначе
Если Символ = "/" И СледующийСимвол = "/" Тогда
ВнутриКомментария = Истина;
Индекс = Индекс + 1;
ИначеЕсли Символ = """" Тогда
ВнутриСтроки = Истина;
ИначеЕсли Символ = ";" Тогда
ДлинаФрагмента = Индекс - ПозицияНачалаТекущегоЗапроса;
Фрагмент = СокрЛП(Сред(ИсходныйТекстЗапроса, ПозицияНачалаТекущегоЗапроса, ДлинаФрагмента));
Если Не ПустаяСтрока(Фрагмент) Тогда
МассивТекстов.Добавить(Фрагмент);
КонецЕсли;
ПозицияНачалаТекущегоЗапроса = Индекс + 1;
КонецЕсли;
КонецЕсли;
Индекс = Индекс + 1;
КонецЦикла;
// Добавляем оставшуюся часть текста после последней точки с запятой
Если ПозицияНачалаТекущегоЗапроса <= ДлинаТекста Тогда
Фрагмент = СокрЛП(Сред(ИсходныйТекстЗапроса, ПозицияНачалаТекущегоЗапроса));
Если Не ПустаяСтрока(Фрагмент) Тогда
МассивТекстов.Добавить(Фрагмент);
КонецЕсли;
КонецЕсли;
Возврат МассивТекстов;
КонецФункции
Во многих консолях запросов ручное разделение пакета делалось исключительно для того, чтобы последовательно выполнить запросы и показать разработчику содержимое временных таблиц.
В платформе 1С:Предприятие 8.3 для этого предусмотрен штатный метод ВыполнитьПакетСПромежуточнымиДанными() объекта Запрос. Он избавляет от необходимости вручную делить текст на отдельные запросы:
Запрос = Новый Запрос;
Запрос.Текст = ТекстПакетаЗапросов;
// Установка параметров
Запрос.УстановитьПараметр("Организация", ВыбОрганизация);
// Выполняем весь пакет целиком за один вызов сервера
МассивРезультатов = Запрос.ВыполнитьПакетСПромежуточнымиДанными();
Для Каждого Результат Из МассивРезультатов Цикл
Если Не Результат.Пустой() Тогда
Выборка = Результат.Выбрать();
// Обработка и вывод промежуточных таблиц и итогового результата
КонецЕсли;
КонецЦикла;
Метод ВыполнитьПакетСПромежуточнымиДанными() возвращает массив объектов РезультатЗапроса для всех частей пакета, включая запросы создания временных таблиц с директивой ПОМЕСТИТЬ. Это исключает любые ошибки ручного парсинга текста.