Commit 0f91d19b authored by 段廷瑞's avatar 段廷瑞

pref: 格式化优化(缩进层级,字符串前导字符识别,换行等)

parent b6d55a05
...@@ -319,14 +319,19 @@ namespace Tools.UI ...@@ -319,14 +319,19 @@ namespace Tools.UI
} }
/// <summary> /// <summary>
/// SQL格式化辅助类 /// SQL格式化辅助类 — 支持 SqlServer / MySQL / PostgreSQL / Oracle / SQLite
/// 格式化规则参考 db_helper: ///
/// - 复合关键字(LEFT JOIN, GROUP BY等)作为整体换行 /// 核心规则:
/// - 短字段列表(≤3)内联, 长字段列表(≥4)每字段一行 /// 1. 复合关键字整体换行:LEFT JOIN, GROUP BY, ORDER BY, UNION ALL 等合并为单个 Token
/// - JOIN + ON 同行 /// 2. 子句首尾同层级:SELECT / FROM / WHERE / GROUP BY / ORDER BY / HAVING 同级缩进
/// - WHERE 首个条件与WHERE同行, 后续AND/OR换行缩进 /// 3. 子句内容缩进一层(4 空格),AND/OR 与内容同级
/// - GROUP BY / ORDER BY 紧凑同行 /// 4. 左括号开始层级、右括号结束层级,括号内逗号不换行(函数参数同行)
/// - 子查询缩进 /// 5. 子查询在前序基础上递进缩进(子查询 SELECT 缩进于括号层级)
/// 6. JOIN + ON 同行;JOIN / ON 条件中的 AND/OR 同行
/// 7. WHERE 首个条件同行,后续 AND/OR 换行缩进(与 WHERE 内容同级)
/// 8. 语义段不拆行:BETWEEN...AND、N'...'、isnull(a, '') 等保持完整
/// 9. 字符串前缀 N 与紧随的字面量之间不插入空格
/// 10. GROUP BY / ORDER BY 紧凑同行
/// </summary> /// </summary>
private static class SqlFormatHelper private static class SqlFormatHelper
{ {
...@@ -403,6 +408,8 @@ namespace Tools.UI ...@@ -403,6 +408,8 @@ namespace Tools.UI
"ILIKE", "LATERAL", "ILIKE", "LATERAL",
"CONNECT", "START", "PRIOR", "CONNECT", "START", "PRIOR",
"NOCYCLE", "NOCYCLE",
// 字符串前缀(各方言)
"N", "E", "X", "B", "Q",
}; };
/// <summary> /// <summary>
...@@ -826,17 +833,26 @@ namespace Tools.UI ...@@ -826,17 +833,26 @@ namespace Tools.UI
} }
/// <summary> /// <summary>
/// 将 Token 列表格式化为 db_helper 风格 SQL /// 将 Token 列表格式化为统一风格 SQL(适配多方言)
/// </summary> /// </summary>
private static StringBuilder FormatTokens(List<Token> tokens) private static StringBuilder FormatTokens(List<Token> tokens)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
int indent = 0; int indent = 0; // 括号嵌套缩进(仅受 OpenParen/CloseParen 影响)
int clauseBase = 0; // 当前子句基准缩进(子查询内缩进基于此前序)
int clauseParenBase = 0; // clauseBase 设定时的 parenDepth(计算局部嵌套)
var clauseBaseStack = new System.Collections.Generic.Stack<int>(); // 子查询退出时恢复外层 clauseBase
var clauseParenStack = new System.Collections.Generic.Stack<int>(); // 同步恢复 clauseParenBase
int parenDepth = 0; // 括号嵌套深度(函数参数内逗号不换行)
bool seenBetween = false; // BETWEEN...AND 视为语义段不拆行
bool newline = true; bool newline = true;
TokenType prevType = TokenType.Whitespace; TokenType prevType = TokenType.Whitespace;
string prevValue = ""; string prevValue = "";
ClauseCtx ctx = ClauseCtx.None; ClauseCtx ctx = ClauseCtx.None;
// 局部函数:计算内容缩进 = clauseBase + IndentSize * (局部嵌套层数 + 1)
int ContentIndent() => clauseBase + IndentSize * (parenDepth - clauseParenBase + 1);
int maxIdx = tokens.Count; int maxIdx = tokens.Count;
for (int ti = 0; ti < maxIdx; ti++) for (int ti = 0; ti < maxIdx; ti++)
{ {
...@@ -866,9 +882,9 @@ namespace Tools.UI ...@@ -866,9 +882,9 @@ namespace Tools.UI
{ {
if (ctx == ClauseCtx.WhereClause) if (ctx == ClauseCtx.WhereClause)
{ {
// db_helper: WHERE 首个条件同行, AND/OR 换行缩进 // WHERE 内 AND/OR 与条件首行同级缩进
if (!newline) { sb.AppendLine(); newline = true; } if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, indent + IndentSize); WriteIndent(sb, ref newline, ContentIndent());
sb.Append(kw); sb.Append(kw);
sb.Append(' '); sb.Append(' ');
newline = false; newline = false;
...@@ -889,11 +905,24 @@ namespace Tools.UI ...@@ -889,11 +905,24 @@ namespace Tools.UI
continue; continue;
} }
// 其他位置: 换行缩进 // 其他位置: 换行缩进
if (!newline) { sb.AppendLine(); newline = true; } if (seenBetween && kw == "AND")
WriteIndent(sb, ref newline, indent + IndentSize); {
sb.Append(kw); // BETWEEN...AND 语义段内不换行
sb.Append(' '); if (sb.Length > 0 && sb[sb.Length - 1] != ' ')
newline = false; sb.Append(' ');
sb.Append(kw);
sb.Append(' ');
newline = false;
seenBetween = false;
}
else
{
if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, ContentIndent());
sb.Append(kw);
sb.Append(' ');
newline = false;
}
prevType = TokenType.Keyword; prevType = TokenType.Keyword;
prevValue = kw; prevValue = kw;
continue; continue;
...@@ -917,7 +946,7 @@ namespace Tools.UI ...@@ -917,7 +946,7 @@ namespace Tools.UI
if (kw == "WHEN" || kw == "ELSE") if (kw == "WHEN" || kw == "ELSE")
{ {
if (!newline) { sb.AppendLine(); newline = true; } if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, indent + IndentSize); WriteIndent(sb, ref newline, ContentIndent());
sb.Append(kw); sb.Append(kw);
sb.Append(' '); sb.Append(' ');
newline = false; newline = false;
...@@ -938,7 +967,7 @@ namespace Tools.UI ...@@ -938,7 +967,7 @@ namespace Tools.UI
if (kw == "END") if (kw == "END")
{ {
if (!newline) { sb.AppendLine(); newline = true; } if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, indent); WriteIndent(sb, ref newline, clauseBase);
sb.Append(kw); sb.Append(kw);
sb.Append(' '); sb.Append(' ');
newline = false; newline = false;
...@@ -951,10 +980,11 @@ namespace Tools.UI ...@@ -951,10 +980,11 @@ namespace Tools.UI
if (IsJoinKeyword(kw)) if (IsJoinKeyword(kw))
{ {
if (!newline) { sb.AppendLine(); newline = true; } if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, IndentSize); WriteIndent(sb, ref newline, clauseBase);
sb.Append(kw); sb.Append(kw);
sb.Append(' '); sb.Append(' ');
newline = false; newline = false;
ctx = ClauseCtx.JoinClause; ctx = ClauseCtx.JoinClause;
prevType = TokenType.Keyword; prevType = TokenType.Keyword;
prevValue = kw; prevValue = kw;
...@@ -965,10 +995,11 @@ namespace Tools.UI ...@@ -965,10 +995,11 @@ namespace Tools.UI
if (kw == "SET" || kw == "VALUES") if (kw == "SET" || kw == "VALUES")
{ {
if (!newline) { sb.AppendLine(); newline = true; } if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, IndentSize); WriteIndent(sb, ref newline, clauseBase);
sb.Append(kw); sb.Append(kw);
sb.Append(' '); sb.Append(' ');
newline = false; newline = false;
ctx = kw == "SET" ? ClauseCtx.SetClause : ClauseCtx.ValuesClause; ctx = kw == "SET" ? ClauseCtx.SetClause : ClauseCtx.ValuesClause;
prevType = TokenType.Keyword; prevType = TokenType.Keyword;
prevValue = kw; prevValue = kw;
...@@ -979,10 +1010,11 @@ namespace Tools.UI ...@@ -979,10 +1010,11 @@ namespace Tools.UI
if (kw == "GROUP BY" || kw == "ORDER BY" || kw == "HAVING") if (kw == "GROUP BY" || kw == "ORDER BY" || kw == "HAVING")
{ {
if (!newline) { sb.AppendLine(); newline = true; } if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, IndentSize); WriteIndent(sb, ref newline, clauseBase);
sb.Append(kw); sb.Append(kw);
sb.Append(' '); sb.Append(' ');
newline = false; newline = false;
ctx = kw == "GROUP BY" ? ClauseCtx.GroupBy : ClauseCtx.OrderBy; ctx = kw == "GROUP BY" ? ClauseCtx.GroupBy : ClauseCtx.OrderBy;
prevType = TokenType.Keyword; prevType = TokenType.Keyword;
prevValue = kw; prevValue = kw;
...@@ -993,10 +1025,11 @@ namespace Tools.UI ...@@ -993,10 +1025,11 @@ namespace Tools.UI
if (kw == "WHERE") if (kw == "WHERE")
{ {
if (!newline) { sb.AppendLine(); newline = true; } if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, IndentSize); WriteIndent(sb, ref newline, clauseBase);
sb.Append(kw); sb.Append(kw);
sb.Append(' '); sb.Append(' ');
newline = false; newline = false;
ctx = ClauseCtx.WhereClause; ctx = ClauseCtx.WhereClause;
prevType = TokenType.Keyword; prevType = TokenType.Keyword;
prevValue = kw; prevValue = kw;
...@@ -1007,10 +1040,11 @@ namespace Tools.UI ...@@ -1007,10 +1040,11 @@ namespace Tools.UI
if (kw == "FROM") if (kw == "FROM")
{ {
if (!newline) { sb.AppendLine(); newline = true; } if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, IndentSize); WriteIndent(sb, ref newline, clauseBase);
sb.Append(kw); sb.Append(kw);
sb.Append(' '); sb.Append(' ');
newline = false; newline = false;
ctx = ClauseCtx.FromClause; ctx = ClauseCtx.FromClause;
prevType = TokenType.Keyword; prevType = TokenType.Keyword;
prevValue = kw; prevValue = kw;
...@@ -1021,11 +1055,18 @@ namespace Tools.UI ...@@ -1021,11 +1055,18 @@ namespace Tools.UI
if (IsTopLevelKeyword(kw)) if (IsTopLevelKeyword(kw))
{ {
if (!newline) { sb.AppendLine(); newline = true; } if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, 0); // 子查询内保存外层 clauseBase/clauseParenBase,退出括号时恢复
if (parenDepth > 0)
{
clauseBaseStack.Push(clauseBase);
clauseParenStack.Push(clauseParenBase);
}
clauseBase = indent;
clauseParenBase = parenDepth;
WriteIndent(sb, ref newline, clauseBase);
sb.Append(kw); sb.Append(kw);
sb.Append(' '); sb.Append(' ');
newline = false; newline = false;
indent = IndentSize;
ctx = kw == "SELECT" ? ClauseCtx.SelectList : ClauseCtx.None; ctx = kw == "SELECT" ? ClauseCtx.SelectList : ClauseCtx.None;
prevType = TokenType.Keyword; prevType = TokenType.Keyword;
prevValue = kw; prevValue = kw;
...@@ -1038,11 +1079,10 @@ namespace Tools.UI ...@@ -1038,11 +1079,10 @@ namespace Tools.UI
if (!newline) { sb.AppendLine(); newline = true; } if (!newline) { sb.AppendLine(); newline = true; }
sb.AppendLine(); sb.AppendLine();
newline = true; newline = true;
WriteIndent(sb, ref newline, 0); WriteIndent(sb, ref newline, clauseBase);
sb.Append(kw); sb.Append(kw);
sb.AppendLine(); sb.AppendLine();
newline = true; newline = true;
indent = IndentSize;
ctx = ClauseCtx.None; ctx = ClauseCtx.None;
prevType = TokenType.Keyword; prevType = TokenType.Keyword;
prevValue = kw; prevValue = kw;
...@@ -1053,7 +1093,7 @@ namespace Tools.UI ...@@ -1053,7 +1093,7 @@ namespace Tools.UI
if (kw == "LIMIT" || kw == "OFFSET" || kw == "FETCH NEXT") if (kw == "LIMIT" || kw == "OFFSET" || kw == "FETCH NEXT")
{ {
if (!newline) { sb.AppendLine(); newline = true; } if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, IndentSize); WriteIndent(sb, ref newline, clauseBase);
sb.Append(kw); sb.Append(kw);
sb.Append(' '); sb.Append(' ');
newline = false; newline = false;
...@@ -1066,10 +1106,11 @@ namespace Tools.UI ...@@ -1066,10 +1106,11 @@ namespace Tools.UI
if (ClauseStarters.Contains(kw)) if (ClauseStarters.Contains(kw))
{ {
if (!newline) { sb.AppendLine(); newline = true; } if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, indent); WriteIndent(sb, ref newline, clauseBase);
sb.Append(kw); sb.Append(kw);
sb.Append(' '); sb.Append(' ');
newline = false; newline = false;
prevType = TokenType.Keyword; prevType = TokenType.Keyword;
prevValue = kw; prevValue = kw;
continue; continue;
...@@ -1079,7 +1120,12 @@ namespace Tools.UI ...@@ -1079,7 +1120,12 @@ namespace Tools.UI
if (NeedsLeadingSpace(prevType)) if (NeedsLeadingSpace(prevType))
sb.Append(' '); sb.Append(' ');
sb.Append(kw); sb.Append(kw);
sb.Append(' '); // 字符串前缀(N等)与紧随的字符串字面量之间不能有空格
if (!IsStringPrefix(kw) || !NextIsStringLiteral(tokens, ti))
sb.Append(' ');
// 标记 BETWEEN...AND 语义段
if (kw == "BETWEEN")
seenBetween = true;
newline = false; newline = false;
prevType = TokenType.Keyword; prevType = TokenType.Keyword;
prevValue = kw; prevValue = kw;
...@@ -1089,9 +1135,18 @@ namespace Tools.UI ...@@ -1089,9 +1135,18 @@ namespace Tools.UI
case TokenType.Comma: case TokenType.Comma:
{ {
sb.Append(','); sb.Append(',');
// 逗号后换行缩进 if (parenDepth > 0)
sb.AppendLine(); {
newline = true; // 函数参数内逗号不换行
sb.Append(' ');
newline = false;
}
else
{
// 顶层逗号(SELECT 字段列表等)换行缩进
sb.AppendLine();
newline = true;
}
prevType = TokenType.Comma; prevType = TokenType.Comma;
prevValue = ","; prevValue = ",";
break; break;
...@@ -1100,6 +1155,7 @@ namespace Tools.UI ...@@ -1100,6 +1155,7 @@ namespace Tools.UI
case TokenType.OpenParen: case TokenType.OpenParen:
sb.Append('('); sb.Append('(');
indent = Math.Min(indent + IndentSize, MaxIndent); indent = Math.Min(indent + IndentSize, MaxIndent);
parenDepth++;
newline = false; newline = false;
prevType = TokenType.OpenParen; prevType = TokenType.OpenParen;
prevValue = "("; prevValue = "(";
...@@ -1107,6 +1163,13 @@ namespace Tools.UI ...@@ -1107,6 +1163,13 @@ namespace Tools.UI
case TokenType.CloseParen: case TokenType.CloseParen:
indent = Math.Max(0, indent - IndentSize); indent = Math.Max(0, indent - IndentSize);
parenDepth = Math.Max(0, parenDepth - 1);
// 恢复外层 clauseBase/clauseParenBase(子查询退出时)
if (clauseBaseStack.Count > parenDepth)
{
clauseBase = clauseBaseStack.Pop();
clauseParenBase = clauseParenStack.Pop();
}
if (newline) { WriteIndent(sb, ref newline, indent); } if (newline) { WriteIndent(sb, ref newline, indent); }
sb.Append(')'); sb.Append(')');
newline = false; newline = false;
...@@ -1137,7 +1200,7 @@ namespace Tools.UI ...@@ -1137,7 +1200,7 @@ namespace Tools.UI
case TokenType.BacktickIdentifier: case TokenType.BacktickIdentifier:
case TokenType.Number: case TokenType.Number:
case TokenType.Identifier: case TokenType.Identifier:
if (newline) { WriteIndent(sb, ref newline, indent); } if (newline) { WriteIndent(sb, ref newline, ContentIndent()); }
// 仅由当前 token 根据前驱类型决定是否补前导空格 // 仅由当前 token 根据前驱类型决定是否补前导空格
// Keyword/Operator 已在自身末尾追加空格,故排除,避免双空格 // Keyword/Operator 已在自身末尾追加空格,故排除,避免双空格
if (NeedsLeadingSpace(prevType)) if (NeedsLeadingSpace(prevType))
...@@ -1149,7 +1212,7 @@ namespace Tools.UI ...@@ -1149,7 +1212,7 @@ namespace Tools.UI
break; break;
case TokenType.Operator: case TokenType.Operator:
if (newline) { WriteIndent(sb, ref newline, indent); } if (newline) { WriteIndent(sb, ref newline, ContentIndent()); }
sb.Append(' '); sb.Append(' ');
sb.Append(token.Value); sb.Append(token.Value);
sb.Append(' '); sb.Append(' ');
...@@ -1167,7 +1230,7 @@ namespace Tools.UI ...@@ -1167,7 +1230,7 @@ namespace Tools.UI
break; break;
default: default:
if (newline) { WriteIndent(sb, ref newline, indent); } if (newline) { WriteIndent(sb, ref newline, ContentIndent()); }
sb.Append(token.Value); sb.Append(token.Value);
newline = false; newline = false;
prevType = token.Type; prevType = token.Type;
...@@ -1230,6 +1293,31 @@ namespace Tools.UI ...@@ -1230,6 +1293,31 @@ namespace Tools.UI
|| kw == "DECLARE" || kw == "BEGIN" || kw == "GO" || kw == "DECLARE" || kw == "BEGIN" || kw == "GO"
|| kw == "RETURN" || kw == "IF"; || kw == "RETURN" || kw == "IF";
} }
/// <summary>
/// 关键字是否为字符串前缀,各方言:
/// SqlServer: N
/// MySQL: N, X, B
/// PostgreSQL: E, B, X
/// Oracle: N, Q
/// SQLite: N, X, B
/// </summary>
private static bool IsStringPrefix(string kw)
{
return kw == "N" || kw == "E" || kw == "X" || kw == "B" || kw == "Q";
}
/// <summary>当前关键字后紧跟的下一个有效 token 是否为字符串字面量</summary>
private static bool NextIsStringLiteral(List<Token> tokens, int currentIndex)
{
for (int i = currentIndex + 1; i < tokens.Count; i++)
{
if (tokens[i].Type == TokenType.Whitespace)
continue;
return tokens[i].Type == TokenType.StringLiteral;
}
return false;
}
} }
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment