DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

SQLite LIKE 搜索西里尔字母时忽略大小写失效

SQLite 内置 LIKE 和 upper() 默认只转换 ASCII,西里尔字母搜索可能漏匹配。用常量定位,在 Microsoft.Data.Sqlite 中注册专用转换函数,并核对连接、语言规则与通配符边界。

表里有 Газпром,搜索 %газ% 却没有结果;把条件改成 UPPER(title) LIKE UPPER($pattern) 后仍然查不到。若当前 SQLite 使用内置的 LIKE 和 upper(),这是它们默认只处理 ASCII 大小写的限制,不是把字段声明成 TEXT 就能解决的问题。

下面先用常量区分匹配规则与数据问题,再给出 Microsoft.Data.Sqlite 应用中的处理方式。已经加载 ICU 或覆盖相关函数的连接可能有不同结果,不能只凭 SQLite 版本号判断。

用同一个连接确认大小写规则

在应用实际查询所用的连接上检查:

SELECT
  'a' LIKE 'A' AS ascii_match,
  'г' LIKE 'Г' AS cyrillic_match,
  upper('газ') AS upper_result;

按默认内置行为,前两项预期为 1、0,最后一项仍是 газ。这说明 ASCII 的大小写匹配正常,但西里尔字母没有经过所需的转换。若常量已经能正确匹配,而真实记录仍查不到,应继续核对实际字符、传入参数及其他过滤条件。

COLLATE NOCASE 也不能替这里的 LIKE 补上 Unicode 转换。SQLite 内置 NOCASE 的大小写处理同样限于 ASCII;而且 LIKE 不使用排序规则。即便应用定义了能比较 Unicode 的排序规则,也不能据此断定 LIKE 已采用它。

在 Microsoft.Data.Sqlite 中注册专用转换函数

可以让应用负责统一大小写,再由 LIKE 处理模式。下面示例要求项目已引用提供 CreateFunction 的 Microsoft.Data.Sqlite,使用 C# 8 或以上语法。它只查询内存连接中的常量,不修改现有数据库。

#nullable enable
using System;
using Microsoft.Data.Sqlite;

class Program
{
    static void Main()
    {
        using var connection = new SqliteConnection("Data Source=:memory:");
        connection.Open();

        connection.CreateFunction<string?, string?>(
            "unicode_upper",
            value => value?.ToUpperInvariant());

        using var command = connection.CreateCommand();
        command.CommandText = @"
            WITH samples(title) AS (
                VALUES ('Газпром'), ('Мечта ювелира'), (NULL)
            )
            SELECT title
            FROM samples
            WHERE unicode_upper(title) LIKE unicode_upper($pattern);
        ";
        command.Parameters.AddWithValue("$pattern", "%газ%");

        using var reader = command.ExecuteReader();
        while (reader.Read())
            Console.WriteLine(reader.GetString(0));
    }
}

这里的关键是两边都调用同一个 unicode_upper。只转换参数,列中的混合大小写仍然可能不匹配。预期 %газ% 匹配 Газпром,将参数换成 %ЮВЕЛИР% 时匹配 Мечта ювелира。NULL 保持缺失值,不变成空字符串,也不会被这个条件选中。

ToUpperInvariant() 使用固定的 invariant culture 转换规则,避免结果随当前区域设置变化。它不等于完整的 Unicode case folding,也不负责去重音或统一组合字符。若产品需要特定语言的大小写规则,应先确定那套规则,并为相关字符单独验收,不能把这个示例当作所有语言的等价关系。

部署环境的 .NET 全球化配置也应纳入测试。精简运行环境若启用了 globalization-invariant 模式,不应只在开发电脑上确认转换;在实际部署运行时核对目标字符的转换与查询结果。调用 ToUpperInvariant() 和切换整个运行时的全球化模式是两件事。

把注册放进实际连接初始化流程

自定义函数属于连接,并没有写进数据库文件。所有执行这条查询的连接都需要完成注册;不能只在启动时打开的一个连接上注册,再让请求使用另一个未初始化的连接。连接池或封装层存在时,把注册纳入应用取得查询连接的既有流程。

接入实际表时,只需把常量数据部分换成查询目标,例如:

SELECT title
FROM products
WHERE unicode_upper(title) LIKE unicode_upper($pattern);

products 和 title 是示例名称。参数仍通过驱动绑定,不能把搜索词直接拼进 SQL。示例有意允许 % 和 _ 作为模式通配符;参数绑定不会取消它们的匹配含义。如果业务承诺按输入文字搜索,还需单独规定通配符转义方式。

采用独立函数名能把变更限制在明确使用它的查询中,无需覆盖全连接的内置 upper() 或 like()。其他客户端直接打开同一个文件时不会自动获得这个函数;需要在这些入口同步实现,或保留不依赖它的查询。

验收结果和撤回范围

用目标语言的大写、小写、混合大小写、无匹配文本和 NULL 分别检查查询。再对照应用返回结果,确认它使用的确实是已注册函数的连接。如果出现 no such function: unicode_upper,先检查连接初始化,不要去修改表编码。

列外层增加函数后,不应假定原来的索引访问方式保持不变。大表应在测试副本上查看查询计划与实际开销,再决定是否采用这条查询;示例不承诺性能改善。

此方案只改变查询和连接注册,不需要把历史标题批量改成大写。撤回时先恢复应用查询,确认不再引用 unicode_upper,再撤去对应注册代码。原始文字保持不变,后续仍可按新的语言规则重新设计检索。