sqlite UTF-8编码数据库中多字节字符的SUBSTR和INSTR问题

cgfeq70w  于 2023-05-18  发布在  SQLite
关注(0)|答案(1)|浏览(128)

编辑:由nbk在一条评论中解决,到最后寻找答案
我有一个UTF-8编码的SQLite数据库,其中SUBSTR的行为似乎是处理字节,而不是处理字符,这使得它基本上毫无用处。我不知道我是否偶然发现了SQLite的bug/限制,或者是否有一些语法可以强制SUBSTR做正确的事情。我试图提取一个子字符串,其结尾由INSTR()计算。INSTR()似乎基于字符(Unicode码点)返回结果,而SUBSTR()似乎对字节起作用,使得它在UTF-8中基本无用。
以下是一个非常简化的测试用例,展示了该问题:

PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE test (title TEXT);
INSERT INTO test VALUES(X'3232313031315f313335365f417072c3a8732d6d6964695f31312d31302d32303232220d');
INSERT INTO test VALUES(X'3232313031335f313535315f5f5245504f52544147455f31372d30312d32303230220d');
COMMIT;

相关信息:

sqlite> .version
SQLite 3.26.0 2018-12-01 12:34:55 bf8c1b2b7a5960c282e543b9c293686dccff272512d08865f4600fb58238alt1
zlib version 1.2.11
gcc-8.3.1 20191121 (Red Hat 8.3.1-5)
sqlite> PRAGMA encoding;
UTF-8
sqlite> .schema
CREATE TABLE test (title TEXT);
sqlite> select * from test;
221011_1356_Après-midi_11-10-2022"
221013_1551__REPORTAGE_17-01-2020"

这两个字符串的字符长度相同(Unicode码点数相同),但第一个字符串长了一个字节(八位字节),这是因为存在用两个字节编码的'è'字符。
下面的查询显示INSTR()考虑字符,SUBSTR()处理字节。(查询试图在末尾去掉多余的'"',但由于存在多字节字符,结果删除了太多。
这使得在INSTR()计算的边界上使用SUBSTR()在存在多字节字符时不可用。

sqlite> select INSTR(title, '"'), SUBSTR(title, 0, INSTR(title, '"')) from test;
34|221011_1356_Après-midi_11-10-202
34|221013_1551__REPORTAGE_17-01-2020

有没有一种方法可以让SUBSTR()和INSTR()很好地结合在一起?
为了记录在案,相同的,但在Windows下的最新版本,3.41.2

SQLite version 3.41.2 2023-03-22 11:56:21
Enter ".help" for usage hints.
sqlite> .schema
sqlite> PRAGMA foreign_keys=OFF;
sqlite> BEGIN TRANSACTION;
sqlite> CREATE TABLE test (title TEXT);
sqlite> INSERT INTO test VALUES(X'3232313031315f313335365f417072c3a8732d6d6964695f31312d31302d32303232220d');
sqlite> INSERT INTO test VALUES(X'3232313031335f313535315f5f5245504f52544147455f31372d30312d32303230220d');
sqlite> COMMIT;
sqlite> .schema
CREATE TABLE test (title TEXT);
sqlite> .version
SQLite 3.41.2 2023-03-22 11:56:21 0d1fc92f94cb6b76bffe3ec34d69cffde2924203304e8ffc4155597af0c191da
zlib version 1.2.11
gcc-5.2.0
sqlite> PRAGMA encoding;
UTF-8
sqlite> select * from test;
221011_1356_Après-midi_11-10-2022"
221013_1551__REPORTAGE_17-01-2020"
sqlite> select INSTR(title, '"'), SUBSTR(title, 0, INSTR(title, '"')) from test;
34|221011_1356_Après-midi_11-10-202
34|221013_1551__REPORTAGE_17-01-2020
sqlite> select INSTR(cast(title as varchar), '"'), SUBSTR(cast(title as varchar), 0, INSTR(cast(title as varchar), '"')) from test; -- nbk solved it, the key is casting to varchar for SUBSTR to work properly it seems
34|221011_1356_Après-midi_11-10-2022
34|221013_1551__REPORTAGE_17-01-2020
3yhwsihp

3yhwsihp1#

问题的根源是您将列title定义为TEXT,但插入了BLOB值。
它在数据类型在SQLite/列亲和行为示例中有明确的记录:
BLOB始终存储为BLOB,而不考虑列关联性。
这意味着,如果你插入BLOB s,你也会在SELECT语句中得到BLOB s,当与SQLite的字符串函数一起使用时,这些语句 * 可能 * 或 * 可能 * 不会 * 转换为TEXT,这取决于每个函数处理BLOB s的记录行为。
下面是SUBSTR()的行为描述:
substr(X,Y,Z)函数返回输入字符串X的一个子字符串,该子字符串从第Y个字符开始,长度为Z个字符。
............
如果X是字符串,则字符索引引用实际的UTF-8字符。
如果X是BLOB,则索引引用bytes
因此,您得到的结果没有什么奇怪的,您所能做的最好的事情就是重新创建表并插入正确的TEXT值。
参见demo
如果保留表的当前数据,则必须始终将BLOB s的值转换为TEXT(而不是VARCHAR,因为SQLite中没有这种数据类型,尽管它可以工作,因为 * VARCHAR类型包含字符串“CHAR”,因此被分配了TEXT亲和性 *)

相关问题