При интеграции 1С с внешними базами данных с использованием технологии ADO (ActiveX Data Objects) разработчики часто сталкиваются со специфическими ошибками СУБД. Одна из самых распространенных ошибок при работе с MS SQL Server — это сообщение «Incorrect syntax near ')'». Эта ошибка происходит на стороне СУБД во время выполнения метода Execute у объекта подключения, когда переданный текст SQL-запроса содержит синтаксические нарушения в районе закрывающей скобки.
В этой статье мы подробно разберем, почему возникает данная ошибка, проанализируем неочевидные дефекты в алгоритме выгрузки и приведем полностью исправленный, безопасный код для работы с внешними базами данных.
Давайте проанализируем ситуацию. Ошибка СУБД указывает на некорректный синтаксис около закрывающей скобки. Если посмотреть на исходный шаблон SQL-запроса в коде:
ТекстЗапроса = "select * where Id1C in ( " + СсылкаНаДокумент + ")";
Мы увидим сразу несколько критических уязвимостей и ошибок проектирования, которые приводят к падению программы.
1. Отсутствие разделителей (запятых) в списке параметров
В первоначальном коде заполнение переменной ДокументыДляВыгрузки (которая затем передается в качестве параметра СсылкаНаДокумент) выполнялось следующим образом:
Если СтрокаДанных.ЕстьФайлы Тогда
ДокументыДляВыгрузки = ДокументыДляВыгрузки + Запятая;
ДокументыДляВыгрузки = ДокументыДляВыгрузки + "'" + Строка(СтрокаДанных.Документ.УникальныйИдентификатор())+ "'";
Запятая = "'";
КонецЕсли;
Обратите внимание на строку Запятая = "'". При первой итерации цикла переменная Запятая пустая, а на всех последующих итерациях в качестве разделителя подставляется одиночный апостроф вместо запятой. В результате строка идентификаторов принимает вид:
'GUID-1''GUID-2''GUID-3'
В SQL-запросе это превращается в кашу, нарушающую строковые границы, что гарантированно приводит к синтаксической ошибке при парсинге запроса парсером MS SQL Server.
2. Пустой список внутри оператора IN
Если в обрабатываемой таблице БюджетныеОперации не окажется ни одной строки, для которой флаг ЕстьФайлы равен Истина, то переменная СсылкаНаДокумент останется пустой строкой. В этом случае сформированный SQL-запрос будет выглядеть так:
select * where Id1C in ( )
В языке Transact-SQL (T-SQL) использование пустых скобок внутри конструкции IN () является грубейшей синтаксической ошибкой. СУБД ожидает внутри скобок хотя бы одно значение или подзапрос, и не обнаружив их, возвращает исключение «Incorrect syntax near ')'».
3. Отсутствие обязательной секции FROM
Даже если список уникальных идентификаторов будет заполнен идеально, запрос автора все равно упадет. В конструкции:
select * where Id1C in (...)
отсутствует указание таблицы-источника (секция FROM). В отличие от некоторых СУБД, которые позволяют делать выборки системных функций без указания таблицы, классический запрос на выборку данных в MS SQL Server обязан содержать имя таблицы. Правильный синтаксис выглядит так:
SELECT * FROM ИмяТаблицы WHERE Id1C IN (...)
---
Рассмотрим подробнее, как шаг за шагом исправить логику выгрузки документов и сделать код устойчивым к ошибкам.
Для корректного разделения GUID-ов запятыми в цикле необходимо правильно инициализировать и перезаписывать переменную-разделитель. Перепишем логику накопления строки:
ДокументыДляВыгрузки = "";
Разделитель = ""; // Используем понятное имя переменной
Для каждого СтрокаДанных из БюджетныеОперации Цикл
// ... логика проверки сценария ...
Если СтрокаДанных.ЕстьФайлы Тогда
Идентификатор1С = Строка(СтрокаДанных.Документ.УникальныйИдентификатор());
ДокументыДляВыгрузки = ДокументыДляВыгрузки + Разделитель + "'" + Идентификатор1С + "'";
Разделитель = ", "; // После первого элемента разделителем всегда будет запятая с пробелом
КонецЕсли;
КонецЦикла;
Чтобы избежать выполнения заведомо ошибочного запроса с пустым списком IN (), в функции ПолучитьФайлДокумента обязательно нужно добавить проверку на заполненность строки с идентификаторами:
Если Не ЗначениеЗаполнено(СсылкаНаДокумент) Тогда
// Если список документов пуст, просто выходим из функции, чтобы не вызывать ошибку СУБД
Возврат Неопределено;
КонецЕсли;
Добавим в текст запроса секцию FROM с указанием реальной таблицы вашей внешней базы данных (например, dbo.StoredFiles):
ТекстЗапроса = "SELECT FileData, FileExtension, FileName FROM dbo.StoredFiles WHERE Id1C IN (" + СсылкаНаДокумент + ")";
Совет: Старайтесь избегать использования символа * (выбрать все поля) в реальных интеграционных задачах. Лучше явно перечислить имена колонок (например, поле с двоичными данными файла, расширение и имя файла). Это снизит нагрузку на сеть и СУБД.
---
При детальном аудите исходного листинга выявляется еще несколько скрытых дефектов, которые могут привести к падению программы или некорректному поведению при промышленной эксплуатации:
СтрокаДанных.Сценраий. Очевидно, что буквы «а» и «и» перепутаны местами. Если в структуре данных имя колонки написано правильно (Сценарий), то выполнение этого кода прервется с ошибкой «Поле объекта не обнаружено».ФлагСценарий). Однако после окончания цикла последняя порция документов выгружается по пути, содержащему НаименованиеФайлаВыгрузки (которое к этому моменту хранит имя последнего обработанного документа). Из-за этого нарушается структура каталогов выгрузки.Реестр.xslx допущена опечатка в расширении файла Microsoft Excel. Правильное написание расширения современного формата — .xlsx (буква «x» на конце, а не «lx»). Из-за этой опечатки операционная система не сможет автоматически открыть сохраненный файл через Excel по двойному клику.Подключение.Close() / УстановитьПодключение()) прямо посреди обхода коллекции — это крайне опасная практика. Если в этот момент существуют открытые наборы записей (Recordset), это вызовет их уничтожение или зависание транзакций. Гораздо надежнее держать соединение открытым на протяжении всей работы обработки и корректно закрывать его в блоке Попытка...Исключение...КонецПопытки в самом конце.---
С учетом всех найденных ошибок и замечаний, мы полностью переработали архитектуру выгрузки. Ниже представлен чистый, безопасный и готовый к использованию код.
Основная процедура выгрузки:
Процедура ДействияФормыВыгрузитьФайлы(Кнопка)
// Инициализируем базовые переменные
ТекущийСценарий = Справочники.СценарииПланирования.ПустаяСсылка();
ДокументыДляВыгрузки = "";
Разделитель = "";
ПутСохранениеФайлов = ЭлементыФормы.Путь.Значение;
// Обеспечиваем корректное завершение пути слэшем
Если Прав(ПутСохранениеФайлов, 1) <> "\" Тогда
ПутСохранениеФайлов = ПутСохранениеФайлов + "\";
КонецЕсли;
Для каждого СтрокаДанных из БюджетныеОперации Цикл
// Исправлена опечатка в имени свойства "Сценарий"
Если ТекущийСценарий <> СтрокаДанных.Сценарий Тогда
// Если сценарий изменился, выгружаем накопленные документы для предыдущего сценария
Если ТекущийСценарий <> Справочники.СценарииПланирования.ПустаяСсылка() Тогда
Если ЗначениеЗаполнено(ДокументыДляВыгрузки) Тогда
ИмяПапкиСценария = СтрЗаменить(Строка(ТекущийСценарий), Символ(34), "");
КаталогНазначения = ПутСохранениеФайлов + ИмяПапкиСценария + "\";
СоздатьКаталог(КаталогНазначения);
ПолучитьФайлДокумента(ДокументыДляВыгрузки, КаталогНазначения);
КонецЕсли;
// Печать реестра для закрытого сценария (исправлена опечатка в расширении .xlsx)
ПечатьРеестра(ТекущийСценарий, ПутСохранениеФайлов + ИмяПапкиСценария + "\Реестр.xlsx");
// Сбрасываем буфер накопления
ДокументыДляВыгрузки = "";
Разделитель = "";
КонецЕсли;
КонецЕсли;
ТекущийСценарий = СтрокаДанных.Сценарий;
Если СтрокаДанных.ЕстьФайлы Тогда
GUIDДокумента = Строка(СтрокаДанных.Документ.УникальныйИдентификатор());
ДокументыДляВыгрузки = ДокументыДляВыгрузки + Разделитель + "'" + GUIDДокумента + "'";
Разделитель = ", ";
КонецЕсли;
КонецЦикла;
// Обрабатываем последнюю порцию данных после выхода из цикла
Если ЗначениеЗаполнено(ДокументыДляВыгрузки) И (ТекущийСценарий <> Справочники.СценарииПланирования.ПустаяСсылка()) Тогда
ИмяПапкиСценария = СтрЗаменить(Строка(ТекущийСценарий), Символ(34), "");
КаталогНазначения = ПутСохранениеФайлов + ИмяПапкиСценария + "\";
СоздатьКаталог(КаталогНазначения);
ПолучитьФайлДокумента(ДокументыДляВыгрузки, КаталогНазначения);
ПечатьРеестра(ТекущийСценарий, КаталогНазначения + "Реестр.xlsx");
КонецЕсли;
Сообщить("Выгрузка данных успешно завершена!");
КонецПроцедуры
Исправленная функция работы с ADO и выгрузки файлов:
Функция ПолучитьФайлДокумента(СсылкаНаДокумент, КаталогСохранения, Открыть = Ложь)
// Защита от пустого списка параметров (предотвращает синтаксическую ошибку в SQL)
Если Не ЗначениеЗаполнено(СсылкаНаДокумент) Тогда
Возврат "";
КонецЕсли;
// Формируем корректный SQL-запрос с указанием таблицы-источника (например, dbo.StoredFiles)
ТекстЗапроса = "SELECT FileData, FileExtension, FileName FROM dbo.StoredFiles WHERE Id1C IN (" + СсылкаНаДокумент + ")";
Попытка
НаборЗаписей = Подключение.Execute(ТекстЗапроса);
Исключение
Сообщить("Ошибка выполнения SQL-запроса: " + ОписаниеОшибки(), СтатусСообщения.Важное);
ВызватьИсключение;
КонецПопытки;
// Проверяем, вернул ли запрос какие-либо записи
Если НаборЗаписей.EOF И НаборЗаписей.BOF Тогда
НаборЗаписей.Close();
Возврат "";
КонецЕсли;
НаборЗаписей.MoveFirst();
ПолныйПутьКФайлу = "";
Пока НаборЗаписей.EOF = 0 Цикл
ЗначениеИзБД = НаборЗаписей.Fields("FileData").Value;
Расширение = НаборЗаписей.Fields("FileExtension").Value;
Имя = НаборЗаписей.Fields("FileName").Value;
// Очищаем имя файла от недопустимых символов
Имя = СтрЗаменить(Имя, "?", "");
Имя = СтрЗаменить(Имя, "/", "");
Имя = СтрЗаменить(Имя, "\", "");
// Создаем массив байт и пишем в поток ADODB.Stream
локПотокБайт = Новый COMSafeArray(ЗначениеИзБД);
Поток = СоздатьПоток(Неопределено); // Предполагается, что функция СоздатьПоток создает объект "ADODB.Stream"
Поток.Write(локПотокБайт);
ПолныйПутьКФайлу = КаталогСохранения + Имя + Расширение;
// Сохраняем файл на диск (параметр 2 означает перезапись существующего файла)
Поток.SaveToFile(ПолныйПутьКФайлу, 2);
Поток.Close();
НаборЗаписей.MoveNext();
КонецЦикла;
НаборЗаписей.Close();
Если Открыть И ЗначениеЗаполнено(ПолныйПутьКФайлу) Тогда
ЗапуститьПриложение(ПолныйПутьКФайлу);
КонецЕсли;
Возврат ПолныйПутьКФайлу;
КонецФункции
Проанализировав данное решение, мы видим, что соблюдение элементарных правил построения динамических SQL-запросов (таких как валидация параметров на пустоту, строгое соблюдение синтаксиса операторов СУБД и контроль структуры каталогов файловой системы) позволяет полностью исключить возникновение ошибок синтаксиса и гарантировать стабильную работу интеграционных механизмов в 1С.