freebcp: "Unicode data is odd byte size for column. Should be even byte size"

freebcp: "Unicode data is odd byte size for column. Should be even byte size"

此文件工作正常 (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 服务器使用 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....|

这可能是源文件的编码问题。

由于您使用的是非标准字符,源文件本身应该是unicode。其他编码使用不同的字节数(一到三个)来编码一个字符。例如。你的 Unicode 2018 在 UTF-8 中是 0xE2 0x80 0x98

您的数据包以 .R.i.f....| 结尾,而应该是您的 ā‘。错误显示 Server '...', Line 1.

尝试找出您的源文件的编码(也请查看 big and little endian)并尝试将您的文件转换为确定的 unicode 格式。

更新:这个问题显然已在 2016-11-04 发布的 FreeTDS v1.00.16 中得到修复。


我可以使用 FreeTDS v1.00.15 重现您的问题。它看起来确实像 freebcp 中的一个错误,导致它在文本字段的最后一个字符具有形式为 U+20xx 的 Unicode 代码点时失败。 (感谢@srutzky 纠正了我关于原因的结论。)正如你所指出的,这是有效的...

291054  Ţawī Rifā

...这失败了...

291054  Ţawī Rifā‘

...但我发现这也有效:

291054  Ţawī Rifā‘x

因此,一个丑陋的解决方法是 运行 针对您的输入文件编写一个脚本,该脚本会将低位非 space Unicode 字符附加到每个文本字段(例如,xU+0078,如上例所示),使用 freebcp 上传数据,然后 运行 对导入行的 UPDATE 语句剥离额外的字符。

就我个人而言,我倾向于从 FreeTDS 切换到 Microsoft 的 SQL Server ODBC Driver for Linux,其中包括 bcpsqlcmd 安装时使用的实用程序此处描述的说明:

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

我刚刚在 Xubuntu 16.04 下测试了它,尽管我不得不稍微调整程序以使用 libssl.so.1.0.0 而不是 libssl.so.0.9.8libcrypto 也是如此),一次我安装了 Microsoft 的 bcp 实用程序,但 freebcp 失败了。

如果 Linux 的 SQL 服务器 ODBC 驱动程序无法在 Mac 上运行,那么另一种选择是使用 Microsoft JDBC 驱动程序 6.0 SQL 服务器和一些 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);
}

这可能会解决问题:

inf 你的 /etc/freetds/freetds.conf

添加:

client charset = UTF-8

还发现 this 关于标志的使用 utf-16

use utf-16 Instead of using UCS-2 for database wide character encoding use UTF-16. Newer Windows versions use this encoding instead of UCS-2. This could result in some issues if clients assume that a character is always 2 bytes.

这个问题与 UTF-8 无关,因为传输数据包(问题底部)中显示的数据是 UTF-16 Little Endian(就像 SQL 服务器会期待)。它是完美的 UTF-16LE,除了缺少最后一个字节,就像错误消息所暗示的那样。

问题很可能是 freetds 中的一个小错误,它错误地应用了旨在从可变长度字符串字段中删除尾随 space 的逻辑。你说没有尾随的 spaces ?好吧,如果它没有被砍掉那么它会更清楚一点(但是,如果它没有被砍掉就不会有这个错误)。那么,让我们看看这个数据包是什么,看看我们是否可以重建它。

数据中的错误可能被忽略了,因为数据包包含偶数个字节。但并非所有字段都是双字节的,因此 不需要 为偶数。如果我们知道好的数据是什么(在错误之前),那么我们就可以在数据中找到一个起点并继续前进。最好从 Ţ 开始,因为它有望高于 255 / FF 值,因此占用 2 个字节。下面的任何内容都会有一个 00 并且许多字符的两边都有。虽然我们应该能够假设 Little Endian 编码,但最好确定。为此,我们至少需要一个具有两个非 00 字节和不同字节的字符(其中一个字符对于两个字节都是 01,这无助于确定排序)。此字符串字段的第一个字符 Ţ 证实了这一点,因为它是代码点 0162,但在数据包中显示为 62 01

下面是字符,与数据包的顺序相同,它们的 UTF-16 LE 值,以及 link 的完整详细信息。 62 01 的第一个字符的字节序列为我们提供了起点,因此我们可以忽略第 0040 行的初始 00 13 00(为了便于阅读,它们已在下面的副本中删除)。请注意右侧显示的 "translation" 不解释 Unicode,因此 62 01 的 2 字节序列本身显示为 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(即由于 Little Endian 编码,字节交换 20 18),而不是 01 18,如果从末尾开始读取数据包。不知何故,最后一个字节——十六进制 20——丢失了,因此出现 Unicode data is odd byte size 错误。

现在,20 本身,或后跟 00,是一个 space。这可以解释为什么@GordThompson 能够通过在末尾添加一个额外的字符来让它工作(最后一个字符不再是可修剪的)。这可以通过以 U+20xx 代码点的另一个字符结尾来进一步证明。例如,如果我对此是正确的,那么以 -- Fraction Slash U+2044 -- would have the same error, while ending with -- Turned Sans-Serif Capital Y U+2144 结尾 -- 即使在它之前有 ,也应该可以正常工作(@GordThompson 非常友好地证明以 结尾确实有效,而以 结尾导致相同的错误)。

如果输入文件是 null(即 00)终止,那么它可能只是 20 00 终止序列,在这种情况下可能以换行符结尾修理它。这也可以通过测试包含两行的文件来证明:第 1 行是 bad.txt 中的现有行,第 2 行是应该有效的行。例如:

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

如果上面直接显示的两行文件有效,则证明它是 U+20xx 代码点和[=127=的组合 ] 该代码点是暴露错误的最后一个字符(传输的字符多于文件的字符)。但是,如果这个两行文件也出现错误,那么它证明将 U+20xx 代码点作为字符串字段的最后一个字符是问题所在(并且可以合理地假设即使字符串字段不是该行的最后一个字段,因为在这种情况下已经排除了传输的空终止符)。

这似乎是 freetds / freebcp 的错误,或者可能有一个配置选项不让它尝试修剪尾随 spaces,或者可能是让它看到这个字段的方法作为 NCHAR 而不是 NVARCHAR.

更新

@GordThompson 和 O.P。 (@NeilMcGuigan) 已经测试并确认无论字符串字段在文件中的什么位置,这个问题都存在:在行的中间,在行的末尾,在最后一行,而不是在最后一行。因此这是一个普遍的问题。

事实上,我找到了源代码,并且由于没有考虑多字节字符集,因此出现问题是有道理的。我将在 GitHub 存储库上提交一个问题。 rtrim 函数的源代码在这里:

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


关于此声明:

The SQL Server uses UTF-16LE (though TDS starts with UCS-2LE and switches over I believe)

从编码的角度来看,UCS-2 和 UTF-16 之间确实没有区别。字节序列是相同的。唯一的区别在于代理对的解释(即 U+FFFF / 65535 以上的代码点)。 UCS-2 保留了用于构造代理对的代码点,但当时没有任何代理对的实现。 UTF-16 只是添加了代理对的实现以创建补充字符。因此,SQL 服务器可以毫无问题地存储和检索 UTF-16 LE 数据。唯一的问题是内置函数不知道如何解释代理对,除非排序规则以 _SC 结尾(对于 S 补充 C 个字符),这些排序规则是在 SQL Server 2012 中引入的。