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

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

parent b6d55a05
......@@ -319,14 +319,19 @@ namespace Tools.UI
}
/// <summary>
/// SQL格式化辅助类
/// 格式化规则参考 db_helper:
/// - 复合关键字(LEFT JOIN, GROUP BY等)作为整体换行
/// - 短字段列表(≤3)内联, 长字段列表(≥4)每字段一行
/// - JOIN + ON 同行
/// - WHERE 首个条件与WHERE同行, 后续AND/OR换行缩进
/// - GROUP BY / ORDER BY 紧凑同行
/// - 子查询缩进
/// SQL格式化辅助类 — 支持 SqlServer / MySQL / PostgreSQL / Oracle / SQLite
///
/// 核心规则:
/// 1. 复合关键字整体换行:LEFT JOIN, GROUP BY, ORDER BY, UNION ALL 等合并为单个 Token
/// 2. 子句首尾同层级:SELECT / FROM / WHERE / GROUP BY / ORDER BY / HAVING 同级缩进
/// 3. 子句内容缩进一层(4 空格),AND/OR 与内容同级
/// 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>
private static class SqlFormatHelper
{
......@@ -403,6 +408,8 @@ namespace Tools.UI
"ILIKE", "LATERAL",
"CONNECT", "START", "PRIOR",
"NOCYCLE",
// 字符串前缀(各方言)
"N", "E", "X", "B", "Q",
};
/// <summary>
......@@ -826,17 +833,26 @@ namespace Tools.UI
}
/// <summary>
/// 将 Token 列表格式化为 db_helper 风格 SQL
/// 将 Token 列表格式化为统一风格 SQL(适配多方言)
/// </summary>
private static StringBuilder FormatTokens(List<Token> tokens)
{
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;
TokenType prevType = TokenType.Whitespace;
string prevValue = "";
ClauseCtx ctx = ClauseCtx.None;
// 局部函数:计算内容缩进 = clauseBase + IndentSize * (局部嵌套层数 + 1)
int ContentIndent() => clauseBase + IndentSize * (parenDepth - clauseParenBase + 1);
int maxIdx = tokens.Count;
for (int ti = 0; ti < maxIdx; ti++)
{
......@@ -866,9 +882,9 @@ namespace Tools.UI
{
if (ctx == ClauseCtx.WhereClause)
{
// db_helper: WHERE 首个条件同行, AND/OR 换行缩进
// WHERE 内 AND/OR 与条件首行同级缩进
if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, indent + IndentSize);
WriteIndent(sb, ref newline, ContentIndent());
sb.Append(kw);
sb.Append(' ');
newline = false;
......@@ -889,11 +905,24 @@ namespace Tools.UI
continue;
}
// 其他位置: 换行缩进
if (seenBetween && kw == "AND")
{
// BETWEEN...AND 语义段内不换行
if (sb.Length > 0 && sb[sb.Length - 1] != ' ')
sb.Append(' ');
sb.Append(kw);
sb.Append(' ');
newline = false;
seenBetween = false;
}
else
{
if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, indent + IndentSize);
WriteIndent(sb, ref newline, ContentIndent());
sb.Append(kw);
sb.Append(' ');
newline = false;
}
prevType = TokenType.Keyword;
prevValue = kw;
continue;
......@@ -917,7 +946,7 @@ namespace Tools.UI
if (kw == "WHEN" || kw == "ELSE")
{
if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, indent + IndentSize);
WriteIndent(sb, ref newline, ContentIndent());
sb.Append(kw);
sb.Append(' ');
newline = false;
......@@ -938,7 +967,7 @@ namespace Tools.UI
if (kw == "END")
{
if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, indent);
WriteIndent(sb, ref newline, clauseBase);
sb.Append(kw);
sb.Append(' ');
newline = false;
......@@ -951,10 +980,11 @@ namespace Tools.UI
if (IsJoinKeyword(kw))
{
if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, IndentSize);
WriteIndent(sb, ref newline, clauseBase);
sb.Append(kw);
sb.Append(' ');
newline = false;
ctx = ClauseCtx.JoinClause;
prevType = TokenType.Keyword;
prevValue = kw;
......@@ -965,10 +995,11 @@ namespace Tools.UI
if (kw == "SET" || kw == "VALUES")
{
if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, IndentSize);
WriteIndent(sb, ref newline, clauseBase);
sb.Append(kw);
sb.Append(' ');
newline = false;
ctx = kw == "SET" ? ClauseCtx.SetClause : ClauseCtx.ValuesClause;
prevType = TokenType.Keyword;
prevValue = kw;
......@@ -979,10 +1010,11 @@ namespace Tools.UI
if (kw == "GROUP BY" || kw == "ORDER BY" || kw == "HAVING")
{
if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, IndentSize);
WriteIndent(sb, ref newline, clauseBase);
sb.Append(kw);
sb.Append(' ');
newline = false;
ctx = kw == "GROUP BY" ? ClauseCtx.GroupBy : ClauseCtx.OrderBy;
prevType = TokenType.Keyword;
prevValue = kw;
......@@ -993,10 +1025,11 @@ namespace Tools.UI
if (kw == "WHERE")
{
if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, IndentSize);
WriteIndent(sb, ref newline, clauseBase);
sb.Append(kw);
sb.Append(' ');
newline = false;
ctx = ClauseCtx.WhereClause;
prevType = TokenType.Keyword;
prevValue = kw;
......@@ -1007,10 +1040,11 @@ namespace Tools.UI
if (kw == "FROM")
{
if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, IndentSize);
WriteIndent(sb, ref newline, clauseBase);
sb.Append(kw);
sb.Append(' ');
newline = false;
ctx = ClauseCtx.FromClause;
prevType = TokenType.Keyword;
prevValue = kw;
......@@ -1021,11 +1055,18 @@ namespace Tools.UI
if (IsTopLevelKeyword(kw))
{
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(' ');
newline = false;
indent = IndentSize;
ctx = kw == "SELECT" ? ClauseCtx.SelectList : ClauseCtx.None;
prevType = TokenType.Keyword;
prevValue = kw;
......@@ -1038,11 +1079,10 @@ namespace Tools.UI
if (!newline) { sb.AppendLine(); newline = true; }
sb.AppendLine();
newline = true;
WriteIndent(sb, ref newline, 0);
WriteIndent(sb, ref newline, clauseBase);
sb.Append(kw);
sb.AppendLine();
newline = true;
indent = IndentSize;
ctx = ClauseCtx.None;
prevType = TokenType.Keyword;
prevValue = kw;
......@@ -1053,7 +1093,7 @@ namespace Tools.UI
if (kw == "LIMIT" || kw == "OFFSET" || kw == "FETCH NEXT")
{
if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, IndentSize);
WriteIndent(sb, ref newline, clauseBase);
sb.Append(kw);
sb.Append(' ');
newline = false;
......@@ -1066,10 +1106,11 @@ namespace Tools.UI
if (ClauseStarters.Contains(kw))
{
if (!newline) { sb.AppendLine(); newline = true; }
WriteIndent(sb, ref newline, indent);
WriteIndent(sb, ref newline, clauseBase);
sb.Append(kw);
sb.Append(' ');
newline = false;
prevType = TokenType.Keyword;
prevValue = kw;
continue;
......@@ -1079,7 +1120,12 @@ namespace Tools.UI
if (NeedsLeadingSpace(prevType))
sb.Append(' ');
sb.Append(kw);
// 字符串前缀(N等)与紧随的字符串字面量之间不能有空格
if (!IsStringPrefix(kw) || !NextIsStringLiteral(tokens, ti))
sb.Append(' ');
// 标记 BETWEEN...AND 语义段
if (kw == "BETWEEN")
seenBetween = true;
newline = false;
prevType = TokenType.Keyword;
prevValue = kw;
......@@ -1089,9 +1135,18 @@ namespace Tools.UI
case TokenType.Comma:
{
sb.Append(',');
// 逗号后换行缩进
if (parenDepth > 0)
{
// 函数参数内逗号不换行
sb.Append(' ');
newline = false;
}
else
{
// 顶层逗号(SELECT 字段列表等)换行缩进
sb.AppendLine();
newline = true;
}
prevType = TokenType.Comma;
prevValue = ",";
break;
......@@ -1100,6 +1155,7 @@ namespace Tools.UI
case TokenType.OpenParen:
sb.Append('(');
indent = Math.Min(indent + IndentSize, MaxIndent);
parenDepth++;
newline = false;
prevType = TokenType.OpenParen;
prevValue = "(";
......@@ -1107,6 +1163,13 @@ namespace Tools.UI
case TokenType.CloseParen:
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); }
sb.Append(')');
newline = false;
......@@ -1137,7 +1200,7 @@ namespace Tools.UI
case TokenType.BacktickIdentifier:
case TokenType.Number:
case TokenType.Identifier:
if (newline) { WriteIndent(sb, ref newline, indent); }
if (newline) { WriteIndent(sb, ref newline, ContentIndent()); }
// 仅由当前 token 根据前驱类型决定是否补前导空格
// Keyword/Operator 已在自身末尾追加空格,故排除,避免双空格
if (NeedsLeadingSpace(prevType))
......@@ -1149,7 +1212,7 @@ namespace Tools.UI
break;
case TokenType.Operator:
if (newline) { WriteIndent(sb, ref newline, indent); }
if (newline) { WriteIndent(sb, ref newline, ContentIndent()); }
sb.Append(' ');
sb.Append(token.Value);
sb.Append(' ');
......@@ -1167,7 +1230,7 @@ namespace Tools.UI
break;
default:
if (newline) { WriteIndent(sb, ref newline, indent); }
if (newline) { WriteIndent(sb, ref newline, ContentIndent()); }
sb.Append(token.Value);
newline = false;
prevType = token.Type;
......@@ -1230,6 +1293,31 @@ namespace Tools.UI
|| kw == "DECLARE" || kw == "BEGIN" || kw == "GO"
|| 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