From 86e2bea7f9a0d54441fe4c66d743337c847f7bae Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Wed, 4 Feb 2026 22:23:29 +1100 Subject: [PATCH 1/2] 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. --- readme.md | 10 ++-- ...CommandOrderByMultipleColumns.verified.txt | 9 +++ ...andOrderByMultipleColumnsDesc.verified.txt | 9 +++ ...ts.CommandOrderBySingleColumn.verified.txt | 7 +++ src/Tests/Tests.cs | 30 ++++++++++ .../FormattedScriptGenerator.cs | 55 +++++++++++++++++++ src/Verify.SqlServer/OrderByCollector.cs | 7 +++ src/Verify.SqlServer/SqlFormatter.cs | 20 ++----- 8 files changed, 127 insertions(+), 20 deletions(-) create mode 100644 src/Tests/Tests.CommandOrderByMultipleColumns.verified.txt create mode 100644 src/Tests/Tests.CommandOrderByMultipleColumnsDesc.verified.txt create mode 100644 src/Tests/Tests.CommandOrderBySingleColumn.verified.txt create mode 100644 src/Verify.SqlServer/FormattedScriptGenerator.cs create mode 100644 src/Verify.SqlServer/OrderByCollector.cs diff --git a/readme.md b/readme.md index 1f9e6ca..3ab1ebb 100644 --- a/readme.md +++ b/readme.md @@ -65,7 +65,7 @@ await Verify(connection) // include only tables and views .SchemaIncludes(DbObjects.Tables | DbObjects.Views); ``` -snippet source | anchor +snippet source | anchor Available values: @@ -103,7 +103,7 @@ await Verify(connection) _ => _ is TableViewBase || _.Name == "MyTrigger"); ``` -snippet source | anchor +snippet source | anchor @@ -125,7 +125,7 @@ command.CommandText = "select Value from MyTable"; var value = await command.ExecuteScalarAsync(); await Verify(value!); ``` -snippet source | anchor +snippet source | anchor Will result in the following verified file: @@ -180,7 +180,7 @@ await Verify( sqlEntries = entries }); ``` -snippet source | anchor +snippet source | anchor @@ -208,7 +208,7 @@ var sqlErrorsViaType = entries .Select(_ => _.Data) .OfType(); ``` -snippet source | anchor +snippet source | anchor diff --git a/src/Tests/Tests.CommandOrderByMultipleColumns.verified.txt b/src/Tests/Tests.CommandOrderByMultipleColumns.verified.txt new file mode 100644 index 0000000..a5bb272 --- /dev/null +++ b/src/Tests/Tests.CommandOrderByMultipleColumns.verified.txt @@ -0,0 +1,9 @@ +{ + Text: +select Id, + Name +from MyTable +order by Name, + Id, + HasTransaction: false +} \ No newline at end of file diff --git a/src/Tests/Tests.CommandOrderByMultipleColumnsDesc.verified.txt b/src/Tests/Tests.CommandOrderByMultipleColumnsDesc.verified.txt new file mode 100644 index 0000000..3232a76 --- /dev/null +++ b/src/Tests/Tests.CommandOrderByMultipleColumnsDesc.verified.txt @@ -0,0 +1,9 @@ +{ + Text: +select Id, + Name +from MyTable +order by Name desc, + Id asc, + HasTransaction: false +} \ No newline at end of file diff --git a/src/Tests/Tests.CommandOrderBySingleColumn.verified.txt b/src/Tests/Tests.CommandOrderBySingleColumn.verified.txt new file mode 100644 index 0000000..c98976b --- /dev/null +++ b/src/Tests/Tests.CommandOrderBySingleColumn.verified.txt @@ -0,0 +1,7 @@ +{ + Text: +select Value +from MyTable +order by Value, + HasTransaction: false +} \ No newline at end of file diff --git a/src/Tests/Tests.cs b/src/Tests/Tests.cs index 987bfa6..5c574e8 100644 --- a/src/Tests/Tests.cs +++ b/src/Tests/Tests.cs @@ -164,6 +164,36 @@ public async Task CommandFull() await Verify(command); } + [Test] + public async Task CommandOrderBySingleColumn() + { + var command = new SqlCommand + { + CommandText = "select Value from MyTable order by Value" + }; + await Verify(command); + } + + [Test] + public async Task CommandOrderByMultipleColumns() + { + var command = new SqlCommand + { + CommandText = "select Id, Name from MyTable order by Name, Id" + }; + await Verify(command); + } + + [Test] + public async Task CommandOrderByMultipleColumnsDesc() + { + var command = new SqlCommand + { + CommandText = "select Id, Name from MyTable order by Name desc, Id asc" + }; + await Verify(command); + } + [Test] public async Task Exception() { diff --git a/src/Verify.SqlServer/FormattedScriptGenerator.cs b/src/Verify.SqlServer/FormattedScriptGenerator.cs new file mode 100644 index 0000000..26cc059 --- /dev/null +++ b/src/Verify.SqlServer/FormattedScriptGenerator.cs @@ -0,0 +1,55 @@ +class FormattedScriptGenerator +{ + readonly Sql170ScriptGenerator generator = new( + new() + { + SqlVersion = SqlVersion.Sql170, + KeywordCasing = KeywordCasing.Lowercase, + IndentationSize = 2, + AlignClauseBodies = true + }); + + public string GenerateScript(TSqlFragment fragment) + { + generator.GenerateScript(fragment, out var script); + + var collector = new OrderByCollector(); + fragment.Accept(collector); + + foreach (var orderBy in collector.Clauses) + { + if (orderBy.OrderByElements.Count <= 1) + { + continue; + } + + var elements = new List(orderBy.OrderByElements.Count); + foreach (var element in orderBy.OrderByElements) + { + generator.GenerateScript(element, out var text); + elements.Add(text); + } + + var singleLine = "order by " + string.Join(", ", elements); + + var pos = script.IndexOf(singleLine, StringComparison.Ordinal); + if (pos == -1) + { + continue; + } + + var lineStart = script.LastIndexOf('\n', pos) + 1; + var indent = new string(' ', pos - lineStart + "order by ".Length); + + var multiLine = "order by " + string.Join(",\n" + indent, elements); + +#if NET48 + script = script.Substring(0, pos) + multiLine + script.Substring(pos + singleLine.Length); +#else + script = string.Concat(script.AsSpan(0, pos), multiLine, script.AsSpan(pos + singleLine.Length)); +#endif + } + + return script; + } +} \ No newline at end of file diff --git a/src/Verify.SqlServer/OrderByCollector.cs b/src/Verify.SqlServer/OrderByCollector.cs new file mode 100644 index 0000000..dc06243 --- /dev/null +++ b/src/Verify.SqlServer/OrderByCollector.cs @@ -0,0 +1,7 @@ +class OrderByCollector : TSqlFragmentVisitor +{ + public List Clauses { get; } = []; + + public override void Visit(OrderByClause node) => + Clauses.Add(node); +} \ No newline at end of file diff --git a/src/Verify.SqlServer/SqlFormatter.cs b/src/Verify.SqlServer/SqlFormatter.cs index 0d6c6dc..c76ff02 100644 --- a/src/Verify.SqlServer/SqlFormatter.cs +++ b/src/Verify.SqlServer/SqlFormatter.cs @@ -1,5 +1,7 @@ static class SqlFormatter { + static readonly FormattedScriptGenerator generator = new(); + public static StringBuilder Format(string input) { var parser = new TSql170Parser(false); @@ -23,24 +25,12 @@ Failed to parse sql. var visitor = new RemoveSquareBracketVisitor(); fragment.Accept(visitor); - var generator = new Sql170ScriptGenerator( - new() - { - SqlVersion = SqlVersion.Sql170, - KeywordCasing = KeywordCasing.Lowercase, - IndentationSize = 2, - AlignClauseBodies = true - }); - - var builder = new StringBuilder(); - using (var writer = new StringWriter(builder, CultureInfo.InvariantCulture)) - { - generator.GenerateScript(fragment, writer); - } + var script = generator.GenerateScript(fragment); + var builder = new StringBuilder(script); builder.TrimEnd(); // ReSharper disable once UseIndexFromEndExpression - if (builder[builder.Length -1] == ';') + if (builder[builder.Length - 1] == ';') { builder.Length--; } From e9d4b9a9897815e5b3a735da4bc8798192f36ded Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Wed, 4 Feb 2026 22:23:51 +1100 Subject: [PATCH 2/2] Update Directory.Build.props --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index c4e3321..13c1622 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ CS1591;CS0649;CS8632;NU1608;NU1109 - 11.2.0 + 11.3.0 preview 1.0.0 SqlServer, Verify