我为什么选择 T-SQL:坦白






2.67/5 (4投票s)
愚蠢的 SQL 技巧、错误和其他胡言乱语
引言
很久以前,我还在那个糟糕的年代,一些公司使用存储过程通过“DMZ”中的服务器访问任何东西。这意味着在大多数情况下不能直接访问表,并且所有数据都需要进行验证。目的是限制访问,以防例如发生服务器被攻破并被用来向数据库发出命令的情况。
直到今天我才了解到,现在有更好的方法来解决这个问题,而且旧方法在很大程度上已经过时了。这是在我构建了 Reggie 之后,它同时针对 T-SQL 和 C#。
不过,我很高兴我付出了这些努力,并且我打算通过 Norm 同样的过程,让它在 T-SQL 代码中运行。
请容我解释。
诅咒 SQL。我爱 SQL。
如果您从未写过存储过程,那您真是太幸运了。它们缺少您在其他语言中理所当然拥有的东西,例如高效的字符串操作或数组,而且它们通常笨拙且编写起来很奇怪。语法太糟糕了,根本记不住,而且几乎和向从未用过它的人解释一样困难。
这太完美了。
这种语言编写起来非常糟糕,如果我能用我的代码生成工具来针对它,那么我几乎可以针对任何其他语言。这就像针对 COBOL 或其他什么语言一样。
SET @ch1 = SUBSTRING(@value, @index, 1)
SET @ch = UNICODE(@ch1)
SET @tch = @ch - 0xd800
IF @tch < 0 SET @tch = @tch + 2147483648
IF @tch < 2048
BEGIN
SET @ch = @ch * 1024
SET @index = @index + 1
IF @index >= @valueEnd RETURN -1
SET @ch2 = SUBSTRING(@value, @index, 1)
SET @ch = @ch + UNICODE(@ch2) - 0x35fdc00
END
这是从 NTEXT
或 NVARCHAR
字符输入(上面用 @value
表示)中读取一个 UTF-32 代码点 (@ch
) 的代码。这很糟糕,而且很大程度上是因为没有无符号整数,没有位移操作,也没有直接(读取效率高)从字符串中获取单个字符的方法。
导航状态转换的 SQL 语句也并无更好之处
SELECT @toState = [SqlTableMatcherStateTransition].[ToStateId]
FROM [SqlTableMatcherState]
INNER JOIN [SqlTableMatcherStateTransition] ON
[SqlTableMatcherState].[StateId]=[SqlTableMatcherStateTransition].[StateId] AND
[SqlTableMatcherState].[SymbolId]=[SqlTableMatcherStateTransition].[SymbolId] AND
[SqlTableMatcherStateTransition].[BlockEndId]=[SqlTableMatcherState].[BlockEndId]
WHERE [SqlTableMatcherState].[SymbolId] = @symbolId AND
[SqlTableMatcherState].[StateId] = @state AND
[SqlTableMatcherState].[BlockEndId] = @blockId AND
[SqlTableMatcherStateTransition].[SymbolId] = @symbolId AND
@ch BETWEEN [dbo].[SqlTableMatcherStateTransition].[Min]
AND [dbo].[SqlTableMatcherStateTransition].[Max]
在 C# 中,它看起来更合理一些,即使状态表被折叠并简单地合并到一个整数数组中
acc = dfa[state++];
tlen = dfa[state++];
for (i = 0; i < tlen; ++i) {
tto = dfa[state++];
prlen = dfa[state++];
for (j = 0; j < prlen; ++j) {
pmin = dfa[state++];
pmax = dfa[state++];
if (ch < pmin) break;
if (ch <= pmax) {
// tto contains the index to
// the valid destination state
// here is where the result
// of the SQL query lands
}
}
}
有趣的地方来了。
多语言定向。说真的。
关键是,上面所有的混乱都是使用 Reggie 中的同一个“TableMatcher.template”模板渲染出来的。这里是完整的代码供参考。您可以看到它有多么复杂——有很多调用,其中大多数调用了为特定目标编写的模板
dynamic a=Arguments;
a.Comment("Matches text based on a DFA table and block end DFA table");
a.MethodPrologue("None",true,"MatcherMatchReturn","TableMatch","TableMatcherMatchImplParams");
a.MatcherCreateResultList();
a.TableMatcherMatchDeclarations();
a.ReadCodePoint(false);
a.InputLoopPrologue();
a.MatcherResetMatch();
a.TableMachineLoopPrologue();
a.TableMove(false,false,true);
a.TableMachineLoopEpilogue();
a.TableAcceptPrologue();
a.TableCheckerMatcherGetBlockEnd();
a.TableIfBlockEndPrologue();
a.TableStateReset();
a.InputLoopPrologue();
a.TableMachineLoopPrologue();
a.TableMove(true,false,true);
a.TableMachineLoopEpilogue();
a.TableAcceptPrologue();
a.MatcherYieldResult();
a.BreakInputLoop();
a.TableAcceptEpilogue();
a.TableRejectPrologue();
a.UpdateLineAny();
a.AppendCapture();
a.ReadCodepoint(false);
a.AdvanceCursor();
a.TableRejectEpilogue();
a.TableStateReset();
a.InputLoopEpilogue();
a.TableStateReset();
a.ContinueInputLoop();
a.TableIfBlockEndEpilogue();
a.TableIfNotBlockEndPrologue();
a.MatcherYieldNonEmptyResult();
a.TableIfNotBlockEndEpilogue();
a.TableAcceptEpilogue();
a.UpdateLineAny();
a.ReadCodepoint(false);
a.AdvanceCursor();
a.TableStateReset();
a.InputLoopEpilogue();
a.MatcherReturnResultList();
a.MethodEpilogue();
for(var i = 0;i<((string[])a._symbolTable).Length;++i) {
var s = ((string[])a._symbolTable)[i];
if(s!=null) {
a._symbol = s; // usually needed for the documentation template
a.MethodPrologue("MatcherMatchDocumentation",false,
"MatcherMatchReturn","Match"+s,"MatcherMatchParams");
a.TableMatcherMatchImplForward(s,i);
a.MethodEpilogue();
}
}
这是 C# 的“TableMove”模板(上面调用了,我尝试将其加粗显示)
<%@param name="isBlockEnd" type="bool"%>
<%@param name="isChecker" type="bool"%>
<%@param name="isMatcher" type="bool"%><%
dynamic a = Arguments;
string array;
string labelName = isBlockEnd?"block_end":"dfa";
if(!(bool)a.lexer) {
array = isBlockEnd?"blockEnd":"dfa";
} else {
array = isBlockEnd?"blockEnd":"TokenizeDfaStateTable";
}
a.Label("start_"+labelName);
%>done = true;
acc = <%=array%>[state++];
tlen = <%=array%>[state++];
for (i = 0; i < tlen; ++i) {
tto = <%=array%>[state++];
prlen = <%=array%>[state++];
for (j = 0; j < prlen; ++j) {
pmin = <%=array%>[state++];
pmax = <%=array%>[state++];
if(ch < pmin) break;
if (ch <= pmax) {
<%a._indent=(int)a._indent+3;
if(!isChecker) {
a.UpdateLineAny();
a.AppendCapture();
}
a.ReadCodepoint(isChecker);
if(!isChecker) {
a.AdvanceCursor();
}
%>state = tto;
done = false;
<%
if(!isMatcher ) {
a.SetMatched();
}
%> goto start_<%=labelName%>;<%a._indent=(int)a._indent-3;%>
}
}
}
这个模板是 ASP/ASP.NET 风格的,但它是我自己的工具,使用这些文件。请原谅格式,因为代码的格式在一定程度上取决于输出本身的格式,更改它会改变输出。
这里是用 T-SQL 表达的完全相同的“表移动”操作
<%@param name="isBlockEnd" type="bool"%>
<%@param name="isChecker" type="bool"%>
<%@param name="isMatcher" type="bool"%><%
dynamic a = Arguments;
string labelName = isBlockEnd?"block_end":"dfa";
a.Label("start_"+labelName);
%>SET @done = 1
SET @toState = -1
<%if((bool)a.lexer) {
%>SELECT @toState = [<%=a.@class%>TokenizeStateTransition].[ToStateId] FROM [<%=a.@class%>TokenizeState] INNER JOIN [<%=a.@class%>TokenizeStateTransition] ON [<%=a.@class%>TokenizeState].[StateId]=[<%=a.@class%>TokenizeStateTransition].[StateId] WHERE [<%=a.@class%>TokenizeStateTransition].[BlockEndId]=@blockId AND [<%=a.@class%>TokenizeState].[StateId]=@state AND [<%=a.@class%>TokenizeState].[BlockEndId] = @blockId AND @ch BETWEEN [<%=a.@class%>TokenizeStateTransition].[Min] AND [<%=a.@class%>TokenizeStateTransition].[Max]<%
} else {
%>SELECT @toState = [<%=a.@class%>StateTransition].[ToStateId] FROM [<%=a.@class%>State] INNER JOIN [<%=a.@class%>StateTransition] ON [<%=a.@class%>State].[StateId]=[<%=a.@class%>StateTransition].[StateId] AND [<%=a.@class%>State].[SymbolId]=[<%=a.@class%>StateTransition].[SymbolId] AND [<%=a.@class%>StateTransition].[BlockEndId]=[<%=a.@class%>State].[BlockEndId] WHERE [<%=a.@class%>State].[SymbolId] = @symbolId AND [<%=a.@class%>State].[StateId] = @state AND [<%=a.@class%>State].[BlockEndId] = @blockId AND [<%=a.@class%>StateTransition].[SymbolId] = @symbolId AND @ch BETWEEN [<%=a.@class%>StateTransition].[Min] AND [<%=a.@class%>StateTransition].[Max]<%
}%>
IF @toState <> -1
BEGIN<%a._indent = ((int)a._indent) + 1;%>
SET @state = @toState
SET @done = 0<%
if(!isMatcher) {%>
SET @matched = 1
<%}
if(!isChecker) {
a.UpdateLineAny();
a.AppendCapture();
}
a.ReadCodepoint(isChecker);
if(!isChecker) {
a.AdvanceCursor();
%>GOTO start_<%=labelName%>
<%}
a._indent = ((int)a._indent) - 1;%>END
这里很长的行完全是我的错。实际上,使用这些模板进行多行语句格式化非常困难,其中大约有 120 个 SQL 模板。我累了。不过,通过这些,我们生成了之前显示的(以及更多)代码(因为我在上面的初始演示中省略了一些代码)。
我没有写一个大的 T-SQL 匹配器模板和一个 C# 匹配器模板,是有原因的。原因是我最初确实是这样做的,结果测试失败了,因为要让这两个截然不同的编程环境在生成的代码方面表现相同极其困难。这样一来,我就可以控制代码的结构和流程,并以相同的方式在每种语言中调用主要的运算——即使是 SQL。
哎呀,我的错
起初,我认为针对 T-SQL 会很有用。这大概是因为我已经离开那个特定的行业十多年了。那时候,在某些情况下,我们需要在存储过程中进行大量的字符串操作,以便执行诸如更新多行之类的操作,因为您无法直接访问表本身。
听起来,如今所有这些都可以留在中间件中,并归其应属之处。
我写了 120 个模板来针对 SQL。这并不容易。但也没有浪费。
要点
我打算让 Norm 和 Reggie 最终覆盖多种语言和编程环境,例如 Python、各种 .NET 语言、可能是 JS 和 C 或 C++,从而为多种语言和平台开放简单的解析和高效的基于 DFA 的正则表达式匹配和标记**,让您可以在异构环境中使用相同的输入规范,并获得一致的行为。
(与 .NET 正则表达式相比快 3 倍,Reggie 生成的 C# 代码)以及大多数正则表达式引擎默认不提供的标记功能。
要做到这一点,我需要一套非常灵活的模板来针对每种语言。通过针对 SQL,我能想到最奇怪的流行语言家族,这迫使我改进了这些模板,使它们很可能适用于我列出的任何语言,只需复制现有树(例如针对 C# 的那个),然后移植每个单独的文件。除了这些语言,我可能还能针对许多其他语言,可能包括函数式语言,如 Haskell。
错误要么是完全的损失,要么是有价值的,这取决于您从中选择什么。我学到了一些关于现代数据库中关注点分离的知识,并且由于这个错误,我的代码也变得比以前更灵活。这绝非完全的损失,实际上让我取得了一些进步,因为我允许了。
历史
- 2021 年 11 月 11 日 - 初始提交