Skip to content

Commit 86e2bea

Browse files
committed
Format multi-column ORDER BY and add tests
Introduce a FormattedScriptGenerator and OrderByCollector to improve formatting of multi-column ORDER BY clauses (split into indented, multi-line lists). Update SqlFormatter to reuse the new generator and produce the formatted script. Add three new command tests in Tests.cs (single, multiple, and multiple with DESC/ASC) and corresponding verified output files. Also update readme snippet anchor line numbers to match the new test locations.
1 parent c373fd0 commit 86e2bea

8 files changed

Lines changed: 127 additions & 20 deletions

readme.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ await Verify(connection)
6565
// include only tables and views
6666
.SchemaIncludes(DbObjects.Tables | DbObjects.Views);
6767
```
68-
<sup><a href='/src/Tests/Tests.cs#L393-L399' title='Snippet source file'>snippet source</a> | <a href='#snippet-SchemaInclude' title='Start of snippet'>anchor</a></sup>
68+
<sup><a href='/src/Tests/Tests.cs#L423-L429' title='Snippet source file'>snippet source</a> | <a href='#snippet-SchemaInclude' title='Start of snippet'>anchor</a></sup>
6969
<!-- endSnippet -->
7070

7171
Available values:
@@ -103,7 +103,7 @@ await Verify(connection)
103103
_ => _ is TableViewBase ||
104104
_.Name == "MyTrigger");
105105
```
106-
<sup><a href='/src/Tests/Tests.cs#L418-L426' title='Snippet source file'>snippet source</a> | <a href='#snippet-SchemaFilter' title='Start of snippet'>anchor</a></sup>
106+
<sup><a href='/src/Tests/Tests.cs#L448-L456' title='Snippet source file'>snippet source</a> | <a href='#snippet-SchemaFilter' title='Start of snippet'>anchor</a></sup>
107107
<!-- endSnippet -->
108108

109109

@@ -125,7 +125,7 @@ command.CommandText = "select Value from MyTable";
125125
var value = await command.ExecuteScalarAsync();
126126
await Verify(value!);
127127
```
128-
<sup><a href='/src/Tests/Tests.cs#L202-L212' title='Snippet source file'>snippet source</a> | <a href='#snippet-Recording' title='Start of snippet'>anchor</a></sup>
128+
<sup><a href='/src/Tests/Tests.cs#L232-L242' title='Snippet source file'>snippet source</a> | <a href='#snippet-Recording' title='Start of snippet'>anchor</a></sup>
129129
<!-- endSnippet -->
130130

131131
Will result in the following verified file:
@@ -180,7 +180,7 @@ await Verify(
180180
sqlEntries = entries
181181
});
182182
```
183-
<sup><a href='/src/Tests/Tests.cs#L279-L309' title='Snippet source file'>snippet source</a> | <a href='#snippet-RecordingSpecific' title='Start of snippet'>anchor</a></sup>
183+
<sup><a href='/src/Tests/Tests.cs#L309-L339' title='Snippet source file'>snippet source</a> | <a href='#snippet-RecordingSpecific' title='Start of snippet'>anchor</a></sup>
184184
<!-- endSnippet -->
185185

186186

@@ -208,7 +208,7 @@ var sqlErrorsViaType = entries
208208
.Select(_ => _.Data)
209209
.OfType<ErrorEntry>();
210210
```
211-
<sup><a href='/src/Tests/Tests.cs#L335-L354' title='Snippet source file'>snippet source</a> | <a href='#snippet-RecordingReadingResults' title='Start of snippet'>anchor</a></sup>
211+
<sup><a href='/src/Tests/Tests.cs#L365-L384' title='Snippet source file'>snippet source</a> | <a href='#snippet-RecordingReadingResults' title='Start of snippet'>anchor</a></sup>
212212
<!-- endSnippet -->
213213

214214

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
Text:
3+
select Id,
4+
Name
5+
from MyTable
6+
order by Name,
7+
Id,
8+
HasTransaction: false
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
Text:
3+
select Id,
4+
Name
5+
from MyTable
6+
order by Name desc,
7+
Id asc,
8+
HasTransaction: false
9+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
Text:
3+
select Value
4+
from MyTable
5+
order by Value,
6+
HasTransaction: false
7+
}

src/Tests/Tests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,36 @@ public async Task CommandFull()
164164
await Verify(command);
165165
}
166166

167+
[Test]
168+
public async Task CommandOrderBySingleColumn()
169+
{
170+
var command = new SqlCommand
171+
{
172+
CommandText = "select Value from MyTable order by Value"
173+
};
174+
await Verify(command);
175+
}
176+
177+
[Test]
178+
public async Task CommandOrderByMultipleColumns()
179+
{
180+
var command = new SqlCommand
181+
{
182+
CommandText = "select Id, Name from MyTable order by Name, Id"
183+
};
184+
await Verify(command);
185+
}
186+
187+
[Test]
188+
public async Task CommandOrderByMultipleColumnsDesc()
189+
{
190+
var command = new SqlCommand
191+
{
192+
CommandText = "select Id, Name from MyTable order by Name desc, Id asc"
193+
};
194+
await Verify(command);
195+
}
196+
167197
[Test]
168198
public async Task Exception()
169199
{
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
class FormattedScriptGenerator
2+
{
3+
readonly Sql170ScriptGenerator generator = new(
4+
new()
5+
{
6+
SqlVersion = SqlVersion.Sql170,
7+
KeywordCasing = KeywordCasing.Lowercase,
8+
IndentationSize = 2,
9+
AlignClauseBodies = true
10+
});
11+
12+
public string GenerateScript(TSqlFragment fragment)
13+
{
14+
generator.GenerateScript(fragment, out var script);
15+
16+
var collector = new OrderByCollector();
17+
fragment.Accept(collector);
18+
19+
foreach (var orderBy in collector.Clauses)
20+
{
21+
if (orderBy.OrderByElements.Count <= 1)
22+
{
23+
continue;
24+
}
25+
26+
var elements = new List<string>(orderBy.OrderByElements.Count);
27+
foreach (var element in orderBy.OrderByElements)
28+
{
29+
generator.GenerateScript(element, out var text);
30+
elements.Add(text);
31+
}
32+
33+
var singleLine = "order by " + string.Join(", ", elements);
34+
35+
var pos = script.IndexOf(singleLine, StringComparison.Ordinal);
36+
if (pos == -1)
37+
{
38+
continue;
39+
}
40+
41+
var lineStart = script.LastIndexOf('\n', pos) + 1;
42+
var indent = new string(' ', pos - lineStart + "order by ".Length);
43+
44+
var multiLine = "order by " + string.Join(",\n" + indent, elements);
45+
46+
#if NET48
47+
script = script.Substring(0, pos) + multiLine + script.Substring(pos + singleLine.Length);
48+
#else
49+
script = string.Concat(script.AsSpan(0, pos), multiLine, script.AsSpan(pos + singleLine.Length));
50+
#endif
51+
}
52+
53+
return script;
54+
}
55+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class OrderByCollector : TSqlFragmentVisitor
2+
{
3+
public List<OrderByClause> Clauses { get; } = [];
4+
5+
public override void Visit(OrderByClause node) =>
6+
Clauses.Add(node);
7+
}

src/Verify.SqlServer/SqlFormatter.cs

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
static class SqlFormatter
22
{
3+
static readonly FormattedScriptGenerator generator = new();
4+
35
public static StringBuilder Format(string input)
46
{
57
var parser = new TSql170Parser(false);
@@ -23,24 +25,12 @@ Failed to parse sql.
2325
var visitor = new RemoveSquareBracketVisitor();
2426
fragment.Accept(visitor);
2527

26-
var generator = new Sql170ScriptGenerator(
27-
new()
28-
{
29-
SqlVersion = SqlVersion.Sql170,
30-
KeywordCasing = KeywordCasing.Lowercase,
31-
IndentationSize = 2,
32-
AlignClauseBodies = true
33-
});
34-
35-
var builder = new StringBuilder();
36-
using (var writer = new StringWriter(builder, CultureInfo.InvariantCulture))
37-
{
38-
generator.GenerateScript(fragment, writer);
39-
}
28+
var script = generator.GenerateScript(fragment);
4029

30+
var builder = new StringBuilder(script);
4131
builder.TrimEnd();
4232
// ReSharper disable once UseIndexFromEndExpression
43-
if (builder[builder.Length -1] == ';')
33+
if (builder[builder.Length - 1] == ';')
4434
{
4535
builder.Length--;
4636
}

0 commit comments

Comments
 (0)