Freebcp: "Данные Unicode - это размер нечетного байта для столбца. Должен быть даже размер байта"

Этот файл отлично работает (UTF-8):

$ cat ok.txt
291054  Ţawī Rifā

Этот файл вызывает ошибку (UTF-8):

$ cat bad.txt
291054  Ţawī Rifā‘

Здесь сообщение:

$ freebcp 'DB.dbo.table' in bad.txt ... -c
Starting copy...
Msg 20050, Level 4
Attempt to convert data stopped by syntax error in source field

Msg 4895, Level 16, State 2
Server '...', Line 1
    Unicode data is odd byte size for column 2. Should be even byte size.
Msg 20018, Level 16
General SQL Server error: Check messages from the SQL Server

Единственное отличие - это последний символ, который является unicode 2018 (левая одинарная кавычка)

Любая идея, что вызывает эту ошибку?

SQL Server использует UTF-16LE (хотя TDS начинается с UCS-2LE и, по-моему, переключается)

Соответствующая колонка nvarchar(200)

Здесь пакет, отправленный непосредственно перед ошибкой:

packet.c:741:Sending packet
0000 07 01 00 56 00 00 01 00-81 02 00 00 00 00 00 08 |...V.... ........|
0010 00 38 09 67 00 65 00 6f-00 6e 00 61 00 6d 00 65 |.8.g.e.o .n.a.m.e|
0020 00 69 00 64 00 00 00 00-00 09 00 e7 90 01 09 04 |.i.d.... ...ç....|
0030 d0 00 34 04 6e 00 61 00-6d 00 65 00 d1 ee 70 04 |Ð.4.n.a. m.e.Ñîp.|
0040 00 13 00 62 01 61 00 77-00 2b 01 20 00 52 00 69 |...b.a.w .+. .R.i|
0050 00 66 00 01 01 18      -                        |.f....|

Ответ 1

Эта проблема не имеет ничего общего с UTF-8, учитывая, что передаваемые данные, как показано в пакете передачи (внизу вопроса), это UTF-16 Little Endian (как ожидал SQL Server). И это отлично подходит для UTF-16LE, за исключением отсутствующего финального байта, как и сообщение об ошибке.

Проблема, скорее всего, является незначительной ошибкой в ​​ freetds, которая неправильно применяет логику, предназначенную для отсечения конечных пробелов из полей строки переменной длины. Вы говорите, нет конечных пробелов? Хорошо, если бы он не был отрублен, это было бы немного яснее (но если бы он не был отрублен, не было бы этой ошибки). Итак, давайте посмотрим, что пакет видит, можем ли мы его восстановить.

Ошибка в данных, вероятно, игнорируется, потому что пакет содержит четное количество байтов. Но не все поля имеют двоичный байт, поэтому не обязательно иметь четное число. Если мы знаем, какие хорошие данные (до ошибки), то мы можем найти начальную точку в данных и двигаться вперед. Лучше всего начинать с Ţ, поскольку он, мы надеемся, будет выше значения 255/FF и, следовательно, займет 2 байта. Все, что ниже, будет иметь 00, и многие из символов имеют это с обеих сторон. Хотя мы должны иметь возможность предполагать кодировку Little Endian, лучше знать наверняка. Для этого нам нужен хотя бы один символ с двумя байтами не 00 и байтами, которые отличаются (один из символов 01 для обоих байтов, и это не помогает определить порядок). Первый символ этого строкового поля, Ţ, подтверждает это, поскольку код Code 0162 показывается как 62 01 в пакете.

Ниже приведены символы в том же порядке, что и пакет, их значения UTF-16 LE и ссылка на их полную информацию. Первая символьная байтовая последовательность 62 01 дает нам отправную точку, поэтому мы можем игнорировать начальную 00 13 00 строки 0040 (они были удалены в приведенной ниже копии для удобочитаемости). Обратите внимание, что "перевод", показанный справа, не интерпретирует Юникод, поэтому 2-байтная последовательность 62 01 отображается как 62 сама по себе (т.е. латинская буква "b" ) и 01 сама по себе (т.е. непечатаемый символ, отображаемый как "." ).

0040 xx xx xx 62 01 61 00 77-00 2b 01 20 00 52 00 69 |...b.a.w .+. .R.i|  
0050 00 66 00 01 01 18 ??   -                        |.f....|

Как вы можете видеть, последний символ действительно 18 20 (т.е. байтовая замена 20 18 из-за кодировки Little Endian), а не 01 18, как может показаться, если вы читаете пакет, начинающийся в конце. Так или иначе, последний байт - hex 20 - отсутствует, поэтому ошибка Unicode data is odd byte size.

Теперь, 20 сам по себе или за которым следует 00, это пробел. Это объясняет, почему @GordThompson смог заставить его работать, добавив дополнительный символ в конец (последний символ больше не был поддающимся обработке). Это может быть дополнительно доказано, заканчивая другим символом, который является кодовой точкой U + 20xx. Например, если я прав об этом, то завершение с помощью - Fraction Slash U + 2044 - будет иметь ту же ошибку, заканчивая - Повернутый Sans-Serif Capital Y U + 2144 - даже с непосредственно перед ним, должен отлично работать (@GordThompson был достаточно любезен, чтобы доказать, что завершение с помощью действительно работало, а завершение с помощью приводило к той же ошибке).

Если входной файл null (т.е. 00) завершен, то это может быть просто конечная последовательность 20 00, которая делает это, и в этом случае завершение с помощью новой строки может исправить это. Это также можно проверить, протестировав файл с двумя строками: строка 1 - это существующая строка из bad.txt, а строка 2 - это строка, которая должна работать. Например:

291054  Ţawī Rifā‘
999999  test row, yo!

Если работает двухстрочный файл, показанный непосредственно выше, это доказывает, что это комбинация кодовой точки U + 20xx и что кодовая точка является последним символом (передачи больше, чем файла), который выдает ошибку, НО, если этот двухстрочный файл также получит ошибку, то это доказывает, что проблема с кодовой точкой U + 20xx как последним символом строкового поля является проблемой (и было бы разумно предположить, что эта ошибка произойдет, даже если поле строки не было окончательным полем строки, так как в этом случае в этом случае исключается нулевой ограничитель для передачи).

Кажется, это либо ошибка с freetds/freebcp, либо, возможно, есть опция конфигурации, чтобы не пытаться обрезать конечные пробелы или, может быть, способ заставить это поле выглядеть как NCHAR вместо NVARCHAR.

UPDATE

Оба @GordThompson и OP (@NeilMcGuigan) протестировали и подтвердили, что эта проблема существует независимо от того, где в файле находится строковое поле: в середине строки, в конце строки, в последней строке, а не в последней строке. Следовательно, это общая проблема.

И на самом деле, я нашел исходный код, и имеет смысл, что проблема возникнет, поскольку нет необходимости в многобайтовых наборах символов. Я напишу о проблеме в репозитории GitHub. Источник для функции rtrim находится здесь:

https://github.com/FreeTDS/freetds/blob/master/src/dblib/bcp.c#L2267


Относительно этого утверждения:

SQL Server использует UTF-16LE (хотя TDS начинается с UCS-2LE и, по-моему, переключается)

С точки зрения кодирования между UCS-2 и UTF-16 действительно нет никакой разницы. Последовательности байтов идентичны. Единственное различие заключается в интерпретации суррогатных пар (т.е. Кодовых баллов выше U + FFFF/65535). UCS-2 имеет кодовые очки, используемые для создания суррогатных пар, зарезервированных, но в то время никаких суррогатных пар не было. UTF-16 просто добавила реализацию суррогатных пар для создания дополнительных символов. Следовательно, SQL Server сохраняет и извлекает данные UTF-16 LE без проблем. Единственная проблема заключается в том, что встроенные функции не знают, как интерпретировать суррогатные пары, если Collation не заканчивается с _SC (для S дополнительных символов C) и эти Collations были представлены в SQL Server 2012.

Ответ 2

Обновление: эта проблема, по-видимому, была исправлена ​​в FreeTDS v1.00.16, выпущена в 2016-11-04.


Я могу воспроизвести вашу проблему, используя FreeTDS v1.00.15. Это определенно выглядит как ошибка в freebcp, которая приводит к сбою, когда последний символ текстового поля имеет кодовую точку Unicode формы U+20xx. (Спасибо @srutzky за исправление моего заключения относительно причины.) Как вы отметили, это работает...

291054  Ţawī Rifā

... и это не удается...

291054  Ţawī Rifā‘

... но я обнаружил, что это также работает:

291054  Ţawī Rifā‘x

Таким образом, уродливым обходным решением было бы запустить script против вашего входного файла, который добавит младший непересекающийся символ Юникода в каждое текстовое поле (например, x, которое равно U+0078, как в в последнем примере выше), используйте freebcp для загрузки данных, а затем запустите оператор UPDATE в отношении импортированных строк, чтобы удалить лишний символ.

Лично я был бы склонен переключиться с FreeTDS на ODBC-драйвер Microsoft SQL Server для Linux, который включает утилиты bcp и sqlcmd при установке с помощью инструкций, описанных здесь:

https://gallery.technet.microsoft.com/scriptcenter/SQLCMD-and-BCP-for-Ubuntu-c88a28cc

Я только что протестировал его под Xubuntu 16.04, и хотя мне пришлось немного подправить процедуру, чтобы использовать libssl.so.1.0.0 вместо libssl.so.0.9.8 (и то же самое для libcrypto), как только я установил ее bcp утилита от Microsoft преуспела там, где freebcp не удалось.

Если драйвер ODBC для SQL Server для Linux не будет работать на Mac, другой альтернативой будет использование Microsoft JDBC Driver 6.0 для SQL Server и немного кода Java, например:

connectionUrl = "jdbc:sqlserver://servername:49242"
        + ";databaseName=myDb"
        + ";integratedSecurity=false";
String myUserid = "sa", myPassword = "whatever";

String dataFileSpec = "C:/Users/Gord/Desktop/bad.txt";
try (
        Connection conn = DriverManager.getConnection(connectionUrl, myUserid, myPassword);
        SQLServerBulkCSVFileRecord fileRecord = new SQLServerBulkCSVFileRecord(dataFileSpec, "UTF-8", "\t", false);
        SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(conn)) {
    fileRecord.addColumnMetadata(1, "col1", java.sql.Types.NVARCHAR, 50, 0);
    fileRecord.addColumnMetadata(2, "col2", java.sql.Types.NVARCHAR, 50, 0);
    bulkCopy.setDestinationTableName("dbo.freebcptest");
    bulkCopy.writeToServer(fileRecord);
} catch (Exception e) {
    e.printStackTrace(System.err);
}

Ответ 3

Это может быть проблема с кодировкой исходного файла.

Поскольку вы используете нестандартные символы, исходный файл должен быть, вероятно, unicode. Другие кодировки используют различное количество байтов (от одного до трех) для кодирования одного символа. Например. ваш Unicode 2018 равен 0xE2 0x80 0x98 в UTF-8.

Ваш пакет заканчивается на .R.i.f....|, пока должен быть ваш ā‘. И ошибка показывает Server '...', Line 1.

Попробуйте узнать кодировку исходного файла (смотрите также big and little endian) и попробуйте преобразовать файл в надежный формат юникода.

Ответ 4

Это может решить проблему:

inf ваш /etc/freetds/freetds.conf

добавить:

client charset = UTF-8

также нашел этот о использовании флага utf-16

use utf-16 Вместо использования UCS-2 для широкой базы данных кодировка символов использует UTF-16. В более новых версиях Windows это вместо UCS-2. Это может привести к некоторым проблемам, если клиенты предположим, что символ всегда 2 байта.