SQL 基于 .net ComputeHash 的 CLR 函数不适用于西里尔文

SQL CLR function based on .net ComputeHash is not working with Cyrrilic

我写了下面的 SQL CLR 函数来散列大于 8000 字节的字符串值(T-SQL 内置 HASHBYTES 函数的输入值的限制):

[SqlFunction(DataAccess = DataAccessKind.None, IsDeterministic = true)]
public static SqlBinary HashBytes(SqlString algorithm, SqlString value)
{
    HashAlgorithm algorithmType = HashAlgorithm.Create(algorithm.Value);

    if (algorithmType == null || value.IsNull)
    {
        return new SqlBinary();
    }
    else
    {
        byte[] bytes = Encoding.UTF8.GetBytes(value.Value);
        return new SqlBinary(algorithmType.ComputeHash(bytes));
    }
}

它适用于拉丁字符串。例如,以下哈希是相同的:

SELECT dbo.fn_Utils_GetHashBytes ('MD5', 'test'); -- 0x098F6BCD4621D373CADE4E832627B4F6
SELECT HASHBYTES ('MD5', 'test');                 -- 0x098F6BCD4621D373CADE4E832627B4F6

问题是它不适用于西里尔字符串。例如:

SELECT dbo.fn_Utils_GetHashBytes ('MD5 ', N'даровете на влъхвите') -- NULL
SELECT HashBytes ('MD5 ',N'даровете на влъхвите') -- 0x838B1B625A6074B2BE55CDB7FCEA2832

SELECT dbo.fn_Utils_GetHashBytes ('SHA256', N'даровете на влъхвите') -- 0xA1D65374A0B954F8291E00BC3DD9DF655D8A4A6BF127CFB15BBE794D2A098844
SELECT HashBytes ('SHA2_256',N'даровете на влъхвите') -- 0x375F6993E0ECE1864336E565C8E14848F2A4BAFCF60BC0C8F5636101DD15B25A 

我正在为 MD5 获取 NULL,尽管代码 returns 值是作为控制台应用程序执行的。谁能告诉我做错了什么?


此外,我从 here 那里得到了函数,其中一条评论说:

Careful with CLR SP parameters being silently truncated to 8000 bytes - I had to tag the parameter with [SqlFacet(MaxSize = -1)] otherwise bytes after the 8000th would simply be ignored!

但我已经对此进行了测试并且它工作正常。例如,如果我生成 8000 字节字符串的哈希值和同一字符串加一个符号的第二个哈希值,我得到的哈希值是不同的。

DECLARE @A VARCHAR(MAX) = '8000 bytes string...'
DECLARE @B VARCHAR(MAX) = @A + '1'
SELECT LEN(@A), LEN(@B)

SELECT IIF(dbo.fn_Utils_GetHashBytes ('MD5', @A + '1') = dbo.fn_Utils_GetHashBytes ('MD5', @B), 1, 0) -- 0

我应该担心这个吗?

 Encoding.UTF8.GetBytes(...)

SQL服务器没有UTF-8的概念。使用 UCS-2 (UTF-16) 或 ASCII。使用的编码必须与您传递给 HASHBYTES 的编码相匹配。您可以很容易地看到 HASHBYTESVARCHARNVARCHAR:

的散列不同
select HASHBYTES('MD5', 'Foo')  -- 0x1356C67D7AD1638D816BFB822DD2C25D
select HASHBYTES('MD5', N'Foo') -- 0xB25FF0AD90D09D395090E8A29FF4C63C

最好是更改 SQLCLR 函数以接受字节而不是字符串,并在调用者中处理转换为 VARBINARY

 SELECT dbo.fn_Utils_GetHashBytes ('MD5', CAST(N'даровете на влъхвите' AS VARBINARY(MAX));

仅供参考 SQL Server 2016 解除了 HASHBYTES 的 8000 字节限制:

For SQL Server 2014 and earlier, allowed input values are limited to 8000 bytes.

有关解释您为何看到差异的详细演练,请参阅我对以下问题的回答:

对于不想自己编译和部署它的任何人,此功能在 SQL# SQLCLR 函数、存储过程等库的免费版本中可用(其中我是的创造者,但 Util_HashUtil_HashBinary,以及许多其他的是免费的)。问题中显示的内容与 SQL# 中的两个 Util_Hash* 函数有一个区别:问题中显示的函数采用 NVARCHAR / SqlString 输入参数,而 SQL# 函数采用 VARBINARY / SqlBinary 输入。差异是:

  • 接受 VARBINARY 输入也适用于二进制源数据(文件、图像、加密值等)
  • 虽然接受 VARBINARY 输入确实需要在函数调用中执行 CONVERT(VARBINARY(MAX), source_string) 的额外步骤,但这样做会保留用于 VARCHAR 数据的任何代码页。虽然不经常使用,但在处理非 Unicode 数据时会很方便。

关于来自其他 post 的警告:

Careful with CLR SP parameters being silently truncated to 8000 bytes - I had to tag the parameter with [SqlFacet(MaxSize = -1)] otherwise bytes after the 8000th would simply be ignored!

但您没有遇到同样的事情:这是由于 SSDT 为 SQLCLR 对象生成 T-SQL 包装器对象的方式发生了变化。在早期版本中(尤其是那些在 VS 2013 之前带有 Visual Studio 的版本),默认行为是对 SqlChars 使用 NVARCHAR(MAX),对 SqlString 使用 NVARCHAR(4000) .但是后来在某些时候(我不想说从 VS 2013 开始,因为 Visual Studio 和 SSDT 是独立的产品,即使 VS 带有 SSDT)默认值被更改为对两者都使用 NVARCHAR(MAX) SqlCharsSqlString。 post 发出警告的人(2013 年 2 月 6 日)一定一直在使用早期版本的 SSDT。尽管如此,明确并使用 [SqlFacet(MaxSize = -1)].

并没有什么坏处(甚至是一个好习惯)

关于 if (algorithmType == null || value.IsNull) 逻辑:因为 NULL 中的任何一个都应该 return 和 NULL,您最好删除该逻辑并使用 WITH RETURNS NULL ON NULL INPUT CREATE FUNCTION 语句的选项。然而,不幸的是,这个选项不受任何 SSDT 构造的支持(即没有 SqlFacet)。因此,为了启用此选项,您可以创建一个 Post-Deployment SQL 脚本(它将在主脚本之后自动部署),它会发出具有所需定义的 ALTER FUNCTION。投票支持我的 Connect 建议以本机支持此选项不会有什么坏处:Implement OnNullCall property in SqlFunctionAttribute for RETURNS NULL ON NULL INPUT SQLCLR。在实际层面上,性能提升主要体现在您为 @value 参数传递大值但不知何故 @algorithmNULL 的情况下,因此您不会结束使用 @value 的值。使用 RETURNS NULL ON NULL INPUT 选项的原因是,当您调用传入 SqlStringSqlBinary 的 SQLCLR 函数时,整个值将被复制到应用程序域的内存中.那就是时间、内存和 CPU 如果您提前知道您不会使用它,则不需要浪费它:-)。您可能还会看到在 非常 频繁调用的函数上,即使传递较小的值也有好处。


关于警告和测试的附注:SQLCLR 不支持 VARCHAR,仅支持 NVARCHAR。因此,从来没有 8000 个字符的限制,因为如果 SSDT 没有自动使用 NVARCHAR(MAX),该限制将是 4000 个字符。因此,如果存在差异,那么首先会看到仅使用 4000 和 4001 个字符进行测试。


更新: 从 SQL Server 2019 开始,现在可以 use UTF-8 natively via the _UTF8 collations。但是,您仍然无法将 UTF-8 字符 字符串传递给 SQLCLR 对象,因为 SQLCLR API只处理 NVARCHAR 而不是 VARCHAR。因此,尝试传入 'UTF-8 encoded string' 仍将作为 UTF-16 LE 通过,因为它会在传入的过程中被隐式转换。将 UTF-8 编码字符转换为 SQLCLR 的唯一方法是首先将它们转换为 VARBINARY 并将这些字节传递给 SQLCLR 对象(如 VARBINARY -> SqlBinary / SqlBytes)。