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 字符附加到每个文本字段(例如,x
即 U+0078
,如上例所示),使用 freebcp
上传数据,然后 运行 对导入行的 UPDATE
语句剥离额外的字符。
就我个人而言,我倾向于从 FreeTDS 切换到 Microsoft 的 SQL Server ODBC Driver for 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
也是如此),一次我安装了 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....|
Ţ
-- 62 01
-- http://unicode-table.com/en/0162/
a
-- 61 00
-- http://unicode-table.com/en/0061/
w
-- 77 00
-- http://unicode-table.com/en/0077/
ī
-- 2B 01
-- http://unicode-table.com/en/012B/
</code> -- <code>20 00
-- http://unicode-table.com/en/0020/
R
-- 52 00
-- http://unicode-table.com/en/0052/
i
-- 69 00
-- http://unicode-table.com/en/0069/
f
-- 66 00
-- http://unicode-table.com/en/0066/
ā
-- 01 01
-- http://unicode-table.com/en/0101/
‘
-- 18 20
-- http://unicode-table.com/en/2018/
如您所见,最后一个字符实际上是 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 中引入的。
此文件工作正常 (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 字符附加到每个文本字段(例如,x
即 U+0078
,如上例所示),使用 freebcp
上传数据,然后 运行 对导入行的 UPDATE
语句剥离额外的字符。
就我个人而言,我倾向于从 FreeTDS 切换到 Microsoft 的 SQL Server ODBC Driver for 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
也是如此),一次我安装了 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....|
Ţ
--62 01
-- http://unicode-table.com/en/0162/a
--61 00
-- http://unicode-table.com/en/0061/w
--77 00
-- http://unicode-table.com/en/0077/ī
--2B 01
-- http://unicode-table.com/en/012B/</code> -- <code>20 00
-- http://unicode-table.com/en/0020/R
--52 00
-- http://unicode-table.com/en/0052/i
--69 00
-- http://unicode-table.com/en/0069/f
--66 00
-- http://unicode-table.com/en/0066/ā
--01 01
-- http://unicode-table.com/en/0101/‘
--18 20
-- http://unicode-table.com/en/2018/
如您所见,最后一个字符实际上是 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 中引入的。