SQL 格式化的 C# 源代码






4.15/5 (5投票s)
SQL 格式化源代码(现在支持格式化选项)——而不是工具、Web 服务或插件/附加组件...
引言
一个简单通用的 SQL 格式化类的源代码。
例如
WITH Sales_CTE(SalesPersonID, SalesOrderID, SalesYear)
AS(SELECT SalesPersonID, SalesOrderID, YEAR(OrderDate) AS SalesYear
FROM Sales.SalesOrderHeader WHERE SalesPersonID IS NOT NULL)
SELECT SalesPersonID, COUNT(SalesOrderID) AS TotalSales, SalesYear
FROM Sales_CTE GROUP BY SalesYear, SalesPersonID ORDER BY SalesPersonID, SalesYear;
变成
with Sales_CTE(SalesPersonID, SalesOrderID, SalesYear) as
(
select SalesPersonID,
SalesOrderID,
Year(OrderDate) as SalesYear
from Sales.SalesOrderHeader
where SalesPersonID is not NULL
)
select SalesPersonID,
Count(SalesOrderID) as TotalSales,
SalesYear
from Sales_CTE
group by SalesYear, SalesPersonID
order by SalesPersonID, SalesYear;
背景
我最近管理了一个数据集成程序员团队,他们编写了大量复杂的 SQL。为了让我更容易理解他们的代码——并希望整个团队能够更标准化地进行格式化,我开始寻找 SQL 格式化程序。市面上的格式化程序很多,但...
我需要可以集成到我们自己工具和实用程序开发中的源代码;可以根据我们的偏好进行修改;不受特定方言限制——当然,必须是免费的。我可能错过了,但我找不到令人满意的解决方案,于是我花了几晚和周末时间,弄出了这个。
我分享这个,希望其他人能从中受益,同时也希望学习一些东西。这个问题相当棘手,让我感觉可能有更好的(更正式、更结构化的)方法来解决,但我却忽略了。
Using the Code
使用该类非常简单
var fmtr = new SQL_Formatter.Formatter();
var formattedStatement = fmtr.Format("... unformatted statement..." [, "... options..."]);
if (fmtr.Success)
... use the result ...;
else
throw new exception(fmtr.LastResult);
该类不会抛出解析错误,而是将 `Success` 布尔属性设置为 `false`,并将 `LastResult` 属性设置为一条信息性消息。
方法
`Format` 方法首先调用 `private` 的 `UnFormat` 方法来折叠并添加空格,以便将输入解析为一系列空格分隔的元素。然后,每个元素被提取到 `token` 变量中,并根据其语法重要性进行检查。
public string Format(string sql)
...
// Remove formatting
try { sql = UnFormat(sql); }
catch (Exception ex)
{ return Fail(ex.Message); }
// Parse the statement
while (stmtIndex < sql.Length)
{
// Skip leading spaces
while (stmtIndex < sql.Length && sql.Substring(stmtIndex, 1) == " ")
stmtIndex++;
// Grab the next token, space delimited
if (sql.IndexOf(" ", stmtIndex) > -1)
token = sql.Substring(stmtIndex, sql.IndexOf(" ", stmtIndex) - stmtIndex);
else
token = sql.Substring(stmtIndex, sql.Length - stmtIndex);
...
当元素标识了注释或引号字面量的开始时,它会被扩展——所有后续元素都被简单地添加到其中——直到找到该构造的结束。然后将其附加到结果中,并继续解析下一个元素。
引入多词 SQL 关键字(例如,“`left`”在“`left join`”中)的元素被保存在 `previousToken` 变量中,并与后续元素合并,直到关键字完成。
然后,每个元素都会被考虑其在格式化(增加或减少缩进、插入换行符)中的作用,然后再添加到格式化后的输出中。意外的元素,例如没有前面“`case`”的“`when`”,会导致格式化失败。助手函数 `Fail` 实现上述错误处理。
缩进级别受关键字、括号等影响,这些在本地变量中单独跟踪:`tabLevel`、`parenLevel` 等。`netParens` 函数评估每个元素因括号引起的缩进影响,而 `Tabs` 函数会考虑这些变量,在将每个元素添加到结果时返回适当的缩进和垂直空格。当达到语句末尾时,变量中任何非零值都表示输入中的语法无效(括号不匹配等),格式化会失败。
`currentKeyword` 堆栈,由 `tabLevel` 索引,跟踪 SQL 构造的嵌套,这反映在缩进中。CTE 和 `case` 语句也需要特别考虑。
实际上,需要考虑许多变体和细微差别才能获得所需的结果。例如,找到一个“`select`”元素后,它可能是 CTE 的一部分、一个新的语句的开始、受 T-SQL 或 PL/SQL 条件的影响等等。考虑本文开头处的示例,以及处理“`select`”的部分代码。
...
case "select":
// Begin a select statement
// Pushes occur below, see tabLevel
if (cte == tabLevel)
{
// Keep together--prevent the default vertical whitespace
token = Tabs(true) + token.ToLower();
cte = -1;
}
else if (currentKeyword[tabLevel] == "")
// New statement
token = Tabs() + token.ToLower();
else if (currentKeyword[tabLevel] == "if")
// SQL conditional
token = Tabs(true) + "\t" + token.ToLower();
else if (!currentKeyword[tabLevel].In(new string[] _
{ "select", "insert", "insert into", "if" }))
// Force vertical whitespace
token = (result.gtr("") & _
result.Right(4) != Str.Repeat(Str.NewLine, 2) ? Str.NewLine : "") + _
Tabs(true, 1) + token.ToLower();
else
// Newline only
token = Tabs(true) + token.ToLower();
...
以“`with`”关键字开始的示例语句表明正在构建一个 CTE,并且 `cte` 被设置为反映当前的 `tabLevel`——当第二个“`select`”关键字(引用)需要格式化时,就需要此信息。CTE 定义中的关键字按常规进行格式化,第一个“`select`”被识别为需要前导缩进,因为它前面有一个开括号。
...
// Increase tab level -- select
if (token.Equals("(select", Str.IgnoreCase))
{
tabLevel++;
token = (result.Right(1) != "\t" ? Tabs(true) : "") + "(" + Str.NewLine + Tabs() + "select";
currentKeyword.Add("select");
currentParens = parenLevel;
}
...
将括号作为找到的元素的一部分来处理似乎不够优雅,但实际上效果很好——这是空格分隔元素策略的自然结果,并且有助于区分组成语句的括号和函数调用中出现的括号。
格式化选项以等号 / 分号分隔的字符串形式传递。当前支持的选项和默认值是:
LeadingCommas = false
LeadingJoins = true
RemoveComments = false
前两个选项反映了开发人员常用的格式化 SQL 的做法,以便可以轻松地为部分 SQL 添加注释以进行调试——这与我最初使 SQL 更易于阅读的意图相反。
随着时间的推移,我认为 `Dialect` 选项会变得必要或有用,但我目前还没有看到这种需求。
Debug
在调试时,格式化程序会添加一个信息性标题,如下例所示:
/*
Formatted -- https://codeproject.org.cn/Articles/1275027/Csharp-source-for-SQL-formatting
Length: 273
Elapsed: 46 milliseconds
*/
演示
解决方案包括一个非常简单的 winforms Demo 可执行文件以及 `Formatter` 类。
关注点
虽然旨在支持多种方言,但目前 T-SQL 的倾向性非常明显。
格式化程序的实现采用了来自不相关库代码的许多例程;我已将这些片段提取到一个附加文件中:*LIB.cs*。特别是,用于管理格式化选项的 `KVP.List` 类提供了基于键值对的广泛功能,但在 *LIB.cs* 中被简单地模拟以支持此格式化逻辑中使用的唯一方法:`GetBoolean`。
注释
注释带来了非常特殊且有趣挑战。此代码不尝试格式化注释,但它们带来的麻烦让我怀疑这是否是正确的选择。
这个问题的一个有趣组成部分是,与注释相关的格式化通常出现在指示它正在解析注释的分隔符*外部*——如下面示例中的换行符所示。
select * /* first comment */
from table
/* second comment */
此外,注释倾向于在未格式化(或者更确切地说,“原始格式化”)的代码中进行格式化以提高可读性。当您更改格式化时,注释的格式会变得随意,从而分散注意力,大大降低其帮助性。
我还没有找到解决这个问题的最佳方法,目前只能手动清理。
历史
- 2019 年 1 月 24 日
- 首次发布
- 2019 年 1 月 27 日
- 修正了源代码中的差异
- 增加了对格式化选项的支持