Как исправить ошибку «Incorrect syntax near ')'» при выгрузке данных через ADO в 1С?

Программист 1С v8.3 (Обычные формы) Управленческий учет IT и автоматизация бизнеса
← На главную

При интеграции 1С с внешними базами данных с использованием технологии ADO (ActiveX Data Objects) разработчики часто сталкиваются со специфическими ошибками СУБД. Одна из самых распространенных ошибок при работе с MS SQL Server — это сообщение «Incorrect syntax near ')'». Эта ошибка происходит на стороне СУБД во время выполнения метода Execute у объекта подключения, когда переданный текст SQL-запроса содержит синтаксические нарушения в районе закрывающей скобки.

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

Выясняем причины ошибки: почему падает 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 (...) ---

Пошаговое исправление ошибок в коде

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

Шаг 1: Исправление формирования списка идентификаторов

Для корректного разделения GUID-ов запятыми в цикле необходимо правильно инициализировать и перезаписывать переменную-разделитель. Перепишем логику накопления строки:


ДокументыДляВыгрузки = "";
Разделитель = ""; // Используем понятное имя переменной

Для каждого СтрокаДанных из БюджетныеОперации Цикл
    // ... логика проверки сценария ...
    
    Если СтрокаДанных.ЕстьФайлы Тогда 
        Идентификатор1С = Строка(СтрокаДанных.Документ.УникальныйИдентификатор());
        ДокументыДляВыгрузки = ДокументыДляВыгрузки + Разделитель + "'" + Идентификатор1С + "'";
        Разделитель = ", "; // После первого элемента разделителем всегда будет запятая с пробелом
    КонецЕсли;
КонецЦикла;

Шаг 2: Валидация входных параметров перед SQL-запросом

Чтобы избежать выполнения заведомо ошибочного запроса с пустым списком IN (), в функции ПолучитьФайлДокумента обязательно нужно добавить проверку на заполненность строки с идентификаторами:


Если Не ЗначениеЗаполнено(СсылкаНаДокумент) Тогда
    // Если список документов пуст, просто выходим из функции, чтобы не вызывать ошибку СУБД
    Возврат Неопределено; 
КонецЕсли;

Шаг 3: Корректировка шаблона SQL-запроса

Добавим в текст запроса секцию FROM с указанием реальной таблицы вашей внешней базы данных (например, dbo.StoredFiles):


ТекстЗапроса = "SELECT FileData, FileExtension, FileName FROM dbo.StoredFiles WHERE Id1C IN (" + СсылкаНаДокумент + ")";

Совет: Старайтесь избегать использования символа * (выбрать все поля) в реальных интеграционных задачах. Лучше явно перечислить имена колонок (например, поле с двоичными данными файла, расширение и имя файла). Это снизит нагрузку на сеть и СУБД.

---

Дополнительные логические ошибки в коде автора

При детальном аудите исходного листинга выявляется еще несколько скрытых дефектов, которые могут привести к падению программы или некорректному поведению при промышленной эксплуатации:

---

Оптимизированный и исправленный вариант кода

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

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


Процедура ДействияФормыВыгрузитьФайлы(Кнопка)
    
    // Инициализируем базовые переменные
    ТекущийСценарий = Справочники.СценарииПланирования.ПустаяСсылка();
    ДокументыДляВыгрузки = "";
    Разделитель = "";
    ПутСохранениеФайлов = ЭлементыФормы.Путь.Значение;
    
    // Обеспечиваем корректное завершение пути слэшем
    Если Прав(ПутСохранениеФайлов, 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С.

← На главную