diff --git a/SqlScriptDom/ScriptDom/SqlServer/ClauseBodyAlignment.cs b/SqlScriptDom/ScriptDom/SqlServer/ClauseBodyAlignment.cs
new file mode 100644
index 0000000..05fcf69
--- /dev/null
+++ b/SqlScriptDom/ScriptDom/SqlServer/ClauseBodyAlignment.cs
@@ -0,0 +1,29 @@
+//------------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+//------------------------------------------------------------------------------
+
+namespace Microsoft.SqlServer.TransactSql.ScriptDom
+{
+ ///
+ /// Represents how the body of a clause (the part after FROM, WHERE, GROUP BY, etc.) is laid out
+ /// relative to its keyword.
+ ///
+ public enum ClauseBodyAlignment
+ {
+ ///
+ /// Keep the body on the keyword's line and line all clause bodies up under a shared column
+ /// past the widest keyword (the classic "rivers of whitespace" style).
+ ///
+ Aligned,
+
+ ///
+ /// Put the body on its own new line, indented one level ( /
+ /// IndentationSize) past the keyword, so nesting grows one step per level instead of drifting
+ /// right as keywords get wider. When this value is used, the AlignClauseBodies setting is
+ /// ignored.
+ ///
+ Indented
+ }
+}
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs
index 07e4628..347feaa 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs
@@ -93,6 +93,44 @@ public void NewLine()
}
}
+ // Returns true if the most recently added meaningful token is a semicolon, ignoring any
+ // trailing whitespace, newlines and alignment points. Used to avoid emitting a redundant
+ // separating semicolon when the previous statement already ended with one.
+ public Boolean LastMeaningfulTokenIsSemicolon()
+ {
+ for (Int32 index = _scriptWriterElements.Count - 1; index >= 0; --index)
+ {
+ ScriptWriterElement element = _scriptWriterElements[index];
+
+ if (element.ElementType == ScriptWriterElementType.NewLine ||
+ element.ElementType == ScriptWriterElementType.AlignmentPoint)
+ {
+ continue;
+ }
+
+ TokenWrapper tokenWrapper = element as TokenWrapper;
+ if (tokenWrapper != null)
+ {
+ TSqlTokenType tokenType = tokenWrapper.Token.TokenType;
+
+ // Skip trailing comments so a semicolon emitted before them is still detected;
+ // otherwise the caller would append the separator into the comment text.
+ if (tokenType == TSqlTokenType.WhiteSpace ||
+ tokenType == TSqlTokenType.SingleLineComment ||
+ tokenType == TSqlTokenType.MultilineComment)
+ {
+ continue;
+ }
+
+ return tokenType == TSqlTokenType.Semicolon;
+ }
+
+ return false;
+ }
+
+ return false;
+ }
+
public void Indent(Int32 size)
{
AddSpace(size);
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.CommonPhrases.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.CommonPhrases.cs
index ef8e600..5004cc2 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.CommonPhrases.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.CommonPhrases.cs
@@ -315,11 +315,18 @@ protected void GenerateNewLineOrSpace(Boolean newline)
}
}
- // mark an alignment point for clause body if it's configured so at a new line
+ // Mark the shared cross-clause "river" alignment point for a clause body, when the current
+ // options call for it.
+ //
+ // ClauseBodyAlignment.Indented never uses the river: a clause body either moves onto its own
+ // indented line (see GenerateClauseBodyStart) or stays on the keyword line separated by a
+ // single space. Marking in that mode would re-introduce the river padding the Indented layout
+ // deliberately drops, so the mode is part of the guard below instead of being repeated by
+ // every caller. Every other case - including the default (Aligned) - is unchanged.
protected void MarkClauseBodyAlignmentWhenNecessary(Boolean newline, AlignmentPoint ap)
{
- // If we didn't put a newline in, don't align, even if AlignClauseBodies is on
- if (newline && _options.AlignClauseBodies)
+ // If we didn't put a newline in, don't align, even if AlignClauseBodies is on.
+ if (newline && _options.AlignClauseBodies && _options.ClauseBodyAlignment != ClauseBodyAlignment.Indented)
{
#if !PIMODLANGUAGE
Debug.Assert(ap != null, "Alignment point should not be null");
@@ -331,6 +338,29 @@ protected void MarkClauseBodyAlignmentWhenNecessary(Boolean newline, AlignmentPo
}
}
+ // Handle the transition from a clause keyword to its body when the body would start on a new
+ // line (newline == true).
+ //
+ // Default (Aligned) mode: this is a pass-through - it marks the shared clause-body alignment
+ // point exactly as before (via MarkClauseBodyAlignmentWhenNecessary) and returns false, so the
+ // caller emits the usual separating space. The combination is identical to the original
+ // "MarkClauseBodyAlignmentWhenNecessary(...); GenerateSpace();" pair, so the default output
+ // does not change.
+ //
+ // Indented mode: the body is broken onto its own line, indented one level, and this returns
+ // true so the caller skips the separating space.
+ protected Boolean GenerateClauseBodyStart(Boolean newline, AlignmentPoint ap)
+ {
+ if (newline && _options.ClauseBodyAlignment == ClauseBodyAlignment.Indented)
+ {
+ NewLineAndIndent();
+ return true;
+ }
+
+ MarkClauseBodyAlignmentWhenNecessary(newline, ap);
+ return false;
+ }
+
protected void MarkInsertColumnsAlignmentPointWhenNecessary(AlignmentPoint ap)
{
#if !PIMODLANGUAGE
@@ -400,6 +430,53 @@ protected void GenerateQueryExpressionInParentheses(QueryExpression queryExpress
GenerateSymbol(TSqlTokenType.RightParenthesis);
}
+ // Emits the parameter list for a CREATE/ALTER PROCEDURE or FUNCTION statement.
+ //
+ // The default (MultilineProcedureParametersList == false) is intentionally the existing,
+ // unchanged behavior: all parameters are written on a single line - function parameters in
+ // parentheses, procedure parameters without. Multi-line output (one parameter per line,
+ // indented one level from the procedure/function name) is strictly opt-in via the option.
+ // CommaPlacement is honored by the underlying list generation when multi-line is enabled.
+ protected void GenerateProcedureOrFunctionParameters(IList parameters, bool parenthesized)
+ {
+ bool hasParameters = parameters != null && parameters.Count > 0;
+
+ // Default path: unchanged single-line behavior. Taken whenever the option is off (its
+ // default) or there is nothing to spread across multiple lines.
+ if (!_options.MultilineProcedureParametersList || !hasParameters)
+ {
+ if (parenthesized)
+ {
+ NewLine();
+ GenerateParenthesisedCommaSeparatedList(parameters);
+ if (!hasParameters)
+ {
+ GenerateSymbol(TSqlTokenType.LeftParenthesis);
+ GenerateSpaceAndSymbol(TSqlTokenType.RightParenthesis);
+ }
+ }
+ else if (hasParameters)
+ {
+ NewLine();
+ GenerateCommaSeparatedList(parameters);
+ }
+
+ return;
+ }
+
+ // Opt-in path: one parameter per line, indented one level.
+ if (parenthesized)
+ {
+ ListGenerationOption option = ListGenerationOption.CreateOptionFromFormattingConfig(_options);
+ GenerateFragmentList(parameters, option);
+ }
+ else
+ {
+ // The option produces its own leading new line before the first parameter.
+ GenerateFragmentList(parameters, ListGenerationOption.MultipleLineProcedureParameterOption);
+ }
+ }
+
// True while rendering a SELECT projection list (QuerySpecification.SelectElements).
// Restricts the "alias = expression" ColumnAliasStyle form to real SELECT projections,
// because OUTPUT, OUTPUT INTO and RECEIVE reuse SelectScalarExpression but do not
@@ -508,6 +585,36 @@ protected void GenerateSemiColonWhenNecessary(TSqlStatement node)
}
}
+ // Some statements must be preceded by a semicolon terminator to be valid in SQL Server:
+ // statements that begin with a WITH clause (common table expression / XMLNAMESPACES) and
+ // the THROW statement. When such a statement follows a statement that was not already
+ // terminated with a semicolon (for example an IF / BEGIN...END / WHILE / TRY...CATCH block,
+ // whose generated form ends with END and no terminator), the required separating semicolon
+ // is emitted so the generated script is valid for SQL Server, which enforces the terminator
+ // even though ScriptDom's own parser is lenient.
+ protected void GenerateSeparatingSemiColonWhenNecessary(TSqlStatement previous, TSqlStatement next)
+ {
+ if (previous != null &&
+ next != null &&
+ _generateSemiColon &&
+ StatementRequiresPrecedingSemiColon(next) &&
+ _writer.LastMeaningfulTokenIsSemicolon() == false)
+ {
+ GenerateSymbol(TSqlTokenType.Semicolon);
+ }
+ }
+
+ private static Boolean StatementRequiresPrecedingSemiColon(TSqlStatement statement)
+ {
+ if (statement is ThrowStatement)
+ {
+ return true;
+ }
+
+ StatementWithCtesAndXmlNamespaces statementWithCtes = statement as StatementWithCtesAndXmlNamespaces;
+ return statementWithCtes != null && statementWithCtes.WithCtesAndXmlNamespaces != null;
+ }
+
///
/// Generates a statement fragment with semicolon placed before any trailing comments.
/// This prevents semicolons from being appended after single-line comments (-- style),
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.ExternalFunctionStatement.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.ExternalFunctionStatement.cs
index 8ab01c6..d51f886 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.ExternalFunctionStatement.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.ExternalFunctionStatement.cs
@@ -35,8 +35,18 @@ private void GenerateExternalFunctionStatementBody(ExternalFunctionStatement nod
GenerateSpaceAndFragmentIfNotNull(node.Name);
if (node.Parameters != null && node.Parameters.Count > 0)
{
- GenerateSpace();
- GenerateParenthesisedCommaSeparatedList(node.Parameters);
+ if (_options.MultilineProcedureParametersList)
+ {
+ // Opt-in: one parameter per line, indented one level.
+ ListGenerationOption option = ListGenerationOption.CreateOptionFromFormattingConfig(_options);
+ GenerateFragmentList(node.Parameters, option);
+ }
+ else
+ {
+ // Default: unchanged single-line, parenthesized parameter list.
+ GenerateSpace();
+ GenerateParenthesisedCommaSeparatedList(node.Parameters);
+ }
}
if (node.ReturnType != null)
{
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs
index 41d4dae..c7c2fc6 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs
@@ -29,9 +29,10 @@ protected void GenerateFromClause(FromClause fromClause, AlignmentPoint clauseBo
GenerateKeyword(TSqlTokenType.From);
- MarkClauseBodyAlignmentWhenNecessary(_options.NewLineBeforeFromClause, clauseBody);
-
- GenerateSpace();
+ if (!GenerateClauseBodyStart(_options.NewLineBeforeFromClause, clauseBody))
+ {
+ GenerateSpace();
+ }
AlignmentPoint fromItems = new AlignmentPoint();
MarkAndPushAlignmentPoint(fromItems);
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FunctionStatementBody.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FunctionStatementBody.cs
index b3c03f2..8308326 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FunctionStatementBody.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FunctionStatementBody.cs
@@ -52,13 +52,7 @@ protected void GenerateFunctionStatementBody(FunctionStatementBody node)
GenerateSpaceAndFragmentIfNotNull(node.Name);
// parameters
- NewLine();
- GenerateParenthesisedCommaSeparatedList(node.Parameters);
- if (node.Parameters == null || node.Parameters.Count == 0)
- {
- GenerateSymbol(TSqlTokenType.LeftParenthesis);
- GenerateSpaceAndSymbol(TSqlTokenType.RightParenthesis);
- }
+ GenerateProcedureOrFunctionParameters(node.Parameters, parenthesized: true);
// RETURNS
NewLine();
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.GroupByClause.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.GroupByClause.cs
index 389ebbc..d3d7398 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.GroupByClause.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.GroupByClause.cs
@@ -22,9 +22,10 @@ public override void ExplicitVisit(GroupByClause node)
}
AlignmentPoint clauseBody = GetAlignmentPointForFragment(node, ClauseBody);
- MarkClauseBodyAlignmentWhenNecessary(_options.NewLineBeforeGroupByClause, clauseBody);
-
- GenerateSpace();
+ if (!GenerateClauseBodyStart(_options.NewLineBeforeGroupByClause, clauseBody))
+ {
+ GenerateSpace();
+ }
GenerateCommaSeparatedList(node.GroupingSpecifications);
if (node.GroupByOption != GroupByOption.None)
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.HavingClause.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.HavingClause.cs
index bfc3bb3..c2b3bfe 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.HavingClause.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.HavingClause.cs
@@ -17,9 +17,14 @@ public override void ExplicitVisit(HavingClause node)
GenerateKeyword(TSqlTokenType.Having);
AlignmentPoint clauseBody = GetAlignmentPointForFragment(node, ClauseBody);
- MarkClauseBodyAlignmentWhenNecessary(_options.NewLineBeforeHavingClause, clauseBody);
-
- GenerateSpaceAndFragmentIfNotNull(node.SearchCondition);
+ if (GenerateClauseBodyStart(_options.NewLineBeforeHavingClause, clauseBody))
+ {
+ GenerateFragmentIfNotNull(node.SearchCondition);
+ }
+ else
+ {
+ GenerateSpaceAndFragmentIfNotNull(node.SearchCondition);
+ }
PopAlignmentPoint();
}
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.InPredicate.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.InPredicate.cs
index 11e67fc..dba4c49 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.InPredicate.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.InPredicate.cs
@@ -22,8 +22,17 @@ public override void ExplicitVisit(InPredicate node)
if (node.Values.Count > 0)
{
- GenerateSpace();
- GenerateParenthesisedCommaSeparatedList(node.Values);
+ if (_options.MultilineInValuesList)
+ {
+ ListGenerationOption option = ListGenerationOption.CreateOptionFromFormattingConfig(_options);
+
+ GenerateFragmentList(node.Values, option);
+ }
+ else
+ {
+ GenerateSpace();
+ GenerateParenthesisedCommaSeparatedList(node.Values);
+ }
}
GenerateSpaceAndFragmentIfNotNull(node.Subquery);
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.InsertStatement.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.InsertStatement.cs
index 6577963..4595759 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.InsertStatement.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.InsertStatement.cs
@@ -66,8 +66,16 @@ public override void ExplicitVisit(InsertSpecification node)
if (node.Columns.Count > 0)
{
MarkInsertColumnsAlignmentPointWhenNecessary(insertColumns);
- GenerateSpace();
- GenerateParenthesisedCommaSeparatedList(node.Columns);
+ if (_options.MultilineInsertTargetsList)
+ {
+ ListGenerationOption option = ListGenerationOption.CreateOptionFromFormattingConfig(_options);
+ GenerateFragmentList(node.Columns, option);
+ }
+ else
+ {
+ GenerateSpace();
+ GenerateParenthesisedCommaSeparatedList(node.Columns);
+ }
}
}
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.ListGenerationOption.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.ListGenerationOption.cs
index fef0ebe..2fbb9c2 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.ListGenerationOption.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.ListGenerationOption.cs
@@ -32,7 +32,7 @@ internal enum SeparatorType
public Boolean NewLineBeforeItems { get; set; }
public int MultipleIndentItems { get; set; }
- public static ListGenerationOption MultipleLineSelectElementOption = new ListGenerationOption()
+ public static readonly ListGenerationOption MultipleLineSelectElementOption = new ListGenerationOption()
{
Parenthesised = false,
AlwaysGenerateParenthesis = false,
@@ -45,6 +45,23 @@ internal enum SeparatorType
MultipleIndentItems = 0,
};
+ // Non-parenthesized, one-item-per-line list indented a single level. Used for
+ // CREATE/ALTER PROCEDURE parameters, which (unlike function parameters) are not wrapped
+ // in parentheses. The leading new line is produced by the option itself
+ // (NewLineBeforeFirstItem), so callers must not emit their own new line first.
+ public static readonly ListGenerationOption MultipleLineProcedureParameterOption = new ListGenerationOption()
+ {
+ Parenthesised = false,
+ AlwaysGenerateParenthesis = false,
+ IndentParentheses = false,
+ AlignParentheses = false,
+
+ Separator = SeparatorType.Comma,
+ NewLineBeforeFirstItem = true,
+ NewLineBeforeItems = true,
+ MultipleIndentItems = 1,
+ };
+
public static ListGenerationOption CreateOptionFromFormattingConfig(SqlScriptGeneratorOptions formatting)
{
ListGenerationOption option = new ListGenerationOption();
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.MergeStatement.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.MergeStatement.cs
index 67c855c..20e497a 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.MergeStatement.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.MergeStatement.cs
@@ -115,8 +115,16 @@ public override void ExplicitVisit(InsertMergeAction node)
AddAlignmentPointForFragment(node.Source, clauseBody);
if (node.Columns.Count > 0)
{
- GenerateSpace();
- GenerateParenthesisedCommaSeparatedList(node.Columns);
+ if (_options.MultilineInsertTargetsList)
+ {
+ ListGenerationOption option = ListGenerationOption.CreateOptionFromFormattingConfig(_options);
+ GenerateFragmentList(node.Columns, option);
+ }
+ else
+ {
+ GenerateSpace();
+ GenerateParenthesisedCommaSeparatedList(node.Columns);
+ }
}
if (node.Source != null)
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OrderByClause.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OrderByClause.cs
index 9062718..c74949e 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OrderByClause.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OrderByClause.cs
@@ -18,9 +18,10 @@ public override void ExplicitVisit(OrderByClause node)
GenerateSpaceAndKeyword(TSqlTokenType.By);
AlignmentPoint clauseBody = GetAlignmentPointForFragment(node, ClauseBody);
- MarkClauseBodyAlignmentWhenNecessary(_options.NewLineBeforeOrderByClause, clauseBody);
-
- GenerateSpace();
+ if (!GenerateClauseBodyStart(_options.NewLineBeforeOrderByClause, clauseBody))
+ {
+ GenerateSpace();
+ }
GenerateCommaSeparatedList(node.OrderByElements);
PopAlignmentPoint();
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.ProcedureStatementBody.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.ProcedureStatementBody.cs
index db09332..e072810 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.ProcedureStatementBody.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.ProcedureStatementBody.cs
@@ -18,11 +18,7 @@ protected void GenerateProcedureStatementBody(ProcedureStatementBody node)
// name
GenerateSpaceAndFragmentIfNotNull(node.ProcedureReference);
- if (node.Parameters != null && node.Parameters.Count > 0)
- {
- NewLine();
- GenerateCommaSeparatedList(node.Parameters);
- }
+ GenerateProcedureOrFunctionParameters(node.Parameters, parenthesized: false);
GenerateCommaSeparatedWithClause(node.Options, false, false);
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.QuerySpecification.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.QuerySpecification.cs
index 5edb52f..5279250 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.QuerySpecification.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.QuerySpecification.cs
@@ -27,29 +27,56 @@ protected void GenerateQuerySpecification(QuerySpecification node, AlignmentPoin
GenerateKeyword(TSqlTokenType.Select);
+ // The select list cannot go through GenerateClauseBodyStart because any DISTINCT/TOP
+ // modifiers are emitted between the keyword and the list: the river has to be marked
+ // right after SELECT, while the indented line break only happens after the modifiers.
+ bool indentSelectList = _options.ClauseBodyAlignment == ClauseBodyAlignment.Indented;
+
MarkClauseBodyAlignmentWhenNecessary(true, clauseBody);
GenerateUniqueRowFilter(node.UniqueRowFilter, true);
GenerateSpaceAndFragmentIfNotNull(node.TopRowFilter);
- GenerateSpace();
+ // In Indented mode the select list starts on its own line, one level in, while any
+ // DISTINCT/TOP modifiers stay on the SELECT line. Every other (including the default
+ // Aligned) case falls through to the original "GenerateSpace()", so the default is unchanged.
+ if (indentSelectList)
+ {
+ NewLineAndIndent();
+ }
+ else
+ {
+ GenerateSpace();
+ }
GenerateSelectElementsList(node.SelectElements);
if (intoClause != null)
{
NewLine();
GenerateKeyword(TSqlTokenType.Into);
- MarkClauseBodyAlignmentWhenNecessary(true, clauseBody);
- GenerateSpaceAndFragmentIfNotNull(intoClause);
+ if (GenerateClauseBodyStart(true, clauseBody))
+ {
+ GenerateFragmentIfNotNull(intoClause);
+ }
+ else
+ {
+ GenerateSpaceAndFragmentIfNotNull(intoClause);
+ }
}
if (filegroupClause != null)
{
NewLine();
GenerateKeyword(TSqlTokenType.On);
- MarkClauseBodyAlignmentWhenNecessary(true, clauseBody);
- GenerateSpaceAndFragmentIfNotNull(filegroupClause);
+ if (GenerateClauseBodyStart(true, clauseBody))
+ {
+ GenerateFragmentIfNotNull(filegroupClause);
+ }
+ else
+ {
+ GenerateSpaceAndFragmentIfNotNull(filegroupClause);
+ }
}
GenerateFromClause(node.FromClause, clauseBody);
@@ -100,8 +127,10 @@ protected void GenerateQuerySpecification(QuerySpecification node, AlignmentPoin
{
NewLine();
GenerateKeyword(TSqlTokenType.For);
- MarkClauseBodyAlignmentWhenNecessary(true, clauseBody);
- GenerateSpace();
+ if (!GenerateClauseBodyStart(true, clauseBody))
+ {
+ GenerateSpace();
+ }
AlignmentPoint forBody = new AlignmentPoint();
MarkAndPushAlignmentPoint(forBody);
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.StatementList.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.StatementList.cs
index 4c87298..1cdbb2f 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.StatementList.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.StatementList.cs
@@ -15,6 +15,7 @@ public override void ExplicitVisit(StatementList node)
if (node.Statements != null)
{
Boolean first = true;
+ TSqlStatement previous = null;
foreach (TSqlStatement statement in node.Statements)
{
if (first)
@@ -23,6 +24,7 @@ public override void ExplicitVisit(StatementList node)
}
else
{
+ GenerateSeparatingSemiColonWhenNecessary(previous, statement);
for (var i = 0; i < _options.NumNewlinesAfterStatement; i++)
{
NewLine();
@@ -30,6 +32,7 @@ public override void ExplicitVisit(StatementList node)
}
GenerateStatementWithSemiColon(statement);
+ previous = statement;
}
}
}
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.TSqlBatch.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.TSqlBatch.cs
index 0420590..a5ed633 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.TSqlBatch.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.TSqlBatch.cs
@@ -11,10 +11,16 @@ partial class SqlScriptGeneratorVisitor
{
public override void ExplicitVisit(TSqlBatch node)
{
- foreach (TSqlStatement statement in node.Statements)
+ for (int index = 0; index < node.Statements.Count; index++)
{
+ TSqlStatement statement = node.Statements[index];
GenerateStatementWithSemiColon(statement);
+ if (index + 1 < node.Statements.Count)
+ {
+ GenerateSeparatingSemiColonWhenNecessary(statement, node.Statements[index + 1]);
+ }
+
if (statement is TSqlStatementSnippet == false)
{
NewLine();
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.WhereClause.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.WhereClause.cs
index 1f5644c..2e22211 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.WhereClause.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.WhereClause.cs
@@ -18,15 +18,29 @@ public override void ExplicitVisit(WhereClause node)
AlignmentPoint clauseBody = GetAlignmentPointForFragment(node, ClauseBody);
- MarkClauseBodyAlignmentWhenNecessary(_options.NewLineBeforeWhereClause, clauseBody);
+ bool indented = GenerateClauseBodyStart(_options.NewLineBeforeWhereClause, clauseBody);
if (node.SearchCondition != null)
{
- GenerateSpaceAndFragmentIfNotNull(node.SearchCondition);
+ if (indented)
+ {
+ GenerateFragmentIfNotNull(node.SearchCondition);
+ }
+ else
+ {
+ GenerateSpaceAndFragmentIfNotNull(node.SearchCondition);
+ }
}
else
{
- GenerateSpaceAndKeyword(TSqlTokenType.Current);
+ if (indented)
+ {
+ GenerateKeyword(TSqlTokenType.Current);
+ }
+ else
+ {
+ GenerateSpaceAndKeyword(TSqlTokenType.Current);
+ }
GenerateSpaceAndKeyword(TSqlTokenType.Of);
GenerateSpaceAndFragmentIfNotNull(node.Cursor);
}
diff --git a/SqlScriptDom/ScriptDom/SqlServer/Settings/SqlScriptGeneratorOptions.xml b/SqlScriptDom/ScriptDom/SqlServer/Settings/SqlScriptGeneratorOptions.xml
index 6225d16..e8b6bd1 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/Settings/SqlScriptGeneratorOptions.xml
+++ b/SqlScriptDom/ScriptDom/SqlServer/Settings/SqlScriptGeneratorOptions.xml
@@ -107,8 +107,13 @@
+
+
+
+ Gets or sets how a clause body (the part after FROM, WHERE, GROUP BY, etc.) is laid out. Aligned keeps the body on the keyword's line, lining all clause bodies up under a shared column past the widest keyword. Indented puts the body on its own new line, one indent level (IndentationSize) past the keyword, so nesting grows one step per level instead of drifting right. When Indented, AlignClauseBodies is ignored.
+
- Gets or sets a boolean indicating if the bodies of FROM, WHERE, GROUP BY, etc. clauses should be aligned
+ Gets or sets whether clause bodies are padded to line up under a shared column. Only applies when ClauseBodyAlignment is Aligned: true pads to the aligned column, false uses a single space after the keyword. Ignored when ClauseBodyAlignment is Indented.
@@ -118,6 +123,9 @@
Gets or sets a boolean indicating if WHERE predicates (expressions separated by AND, and OR) should be written on multiple lines
+
+ Gets or sets a boolean indicating if the values in an IN (values) predicate should be listed as a multi-line list. When false, the IN list is written on a single line.
+
@@ -148,13 +156,18 @@
-
+ Gets or sets a boolean indicating if the INSERT column targets list should be spread across multiple linesGets or sets a boolean indicating if the INSERT column sources list should be spread across multiple lines
+
+
+ Gets or sets a boolean indicating if procedure and function parameters should be listed on separate lines. When false (the default), all parameters are written on a single line, preserving the previous script-generation behavior. When true, each parameter is placed on its own line, indented one level from the procedure or function name. CommaPlacement applies to the parameter list. AlignColumnDefinitionFields does not apply to procedure or function parameters.
+
+ Gets or sets a boolean indicating if a newline should be placed before an open parenthesis when writing a multi-line list in parenthesis
diff --git a/Test/SqlDom/ScriptGenerator/ClauseBodyAlignmentTests.cs b/Test/SqlDom/ScriptGenerator/ClauseBodyAlignmentTests.cs
new file mode 100644
index 0000000..7de4b8d
--- /dev/null
+++ b/Test/SqlDom/ScriptGenerator/ClauseBodyAlignmentTests.cs
@@ -0,0 +1,887 @@
+//------------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+//------------------------------------------------------------------------------
+
+using Microsoft.SqlServer.TransactSql.ScriptDom;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using SqlStudio.Tests.AssemblyTools.TestCategory;
+using static SqlStudio.Tests.UTSqlScriptDom.ScriptGeneratorTestHelper;
+
+namespace SqlStudio.Tests.UTSqlScriptDom
+{
+ // Tests for the ClauseBodyAlignment script-generation option, which controls how the body of a
+ // clause (the part after FROM, WHERE, GROUP BY, etc.) is laid out: Aligned (the classic
+ // keyword-width "rivers of whitespace" style) or Indented (each body on its own line, one indent
+ // level past the keyword, so nesting grows linearly instead of drifting right).
+ //
+ // Work item: Formatter option: Indentation vs alignment (ClauseBodyAlignment)
+ [TestClass]
+ public class ClauseBodyAlignmentTests
+ {
+ // -----------------------------------------------------------------------------------------
+ // Default
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestClauseBodyAlignmentDefaultIsAligned()
+ {
+ Assert.AreEqual(ClauseBodyAlignment.Aligned, new SqlScriptGeneratorOptions().ClauseBodyAlignment);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedIgnoresAlignClauseBodies()
+ {
+ // When Indented, AlignClauseBodies has no effect: both settings produce the same output.
+ const string input = "SELECT a FROM t WHERE b = 1;";
+ var alignOn = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented, AlignClauseBodies = true };
+ var alignOff = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented, AlignClauseBodies = false };
+
+ Assert.AreEqual(Normalize(Generate(input, alignOn)), Normalize(Generate(input, alignOff)));
+ }
+
+ // --- Basic SELECT / FROM / WHERE ---------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignedMatchesLegacyRiverLayout()
+ {
+ // Aligned mode reproduces the existing keyword-width "river" alignment.
+ const string input = "SELECT a FROM t WHERE b = 1;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Aligned };
+ const string expected =
+@"
+SELECT a
+FROM t
+WHERE b = 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedPutsEachClauseBodyOnItsOwnLine()
+ {
+ // Indented mode drops each clause body onto its own line, one indent level past the keyword.
+ const string input = "SELECT a FROM t WHERE b = 1;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented };
+ const string expected =
+@"
+SELECT
+ a
+FROM
+ t
+WHERE
+ b = 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- Multi-column SELECT list ------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignedMultiColumnSelectList()
+ {
+ const string input = "SELECT a, b, c FROM t;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Aligned };
+ const string expected =
+@"
+SELECT a,
+ b,
+ c
+FROM t;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedMultiColumnSelectList()
+ {
+ const string input = "SELECT a, b, c FROM t;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented };
+ const string expected =
+@"
+SELECT
+ a,
+ b,
+ c
+FROM
+ t;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- DISTINCT / TOP stay on the SELECT line ----------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignedKeepsDistinctAndTopOnSelectLine()
+ {
+ const string input = "SELECT DISTINCT TOP 5 a FROM t;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Aligned };
+ const string expected =
+@"
+SELECT DISTINCT TOP 5 a
+FROM t;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedKeepsDistinctAndTopOnSelectLine()
+ {
+ const string input = "SELECT DISTINCT TOP 5 a FROM t;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented };
+ const string expected =
+@"
+SELECT DISTINCT TOP 5
+ a
+FROM
+ t;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- GROUP BY / HAVING / ORDER BY --------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignedGroupByAndHaving()
+ {
+ const string input = "SELECT a, COUNT(*) FROM t GROUP BY a HAVING COUNT(*) > 1 ORDER BY a;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Aligned };
+ const string expected =
+@"
+SELECT a,
+ COUNT(*)
+FROM t
+GROUP BY a
+HAVING COUNT(*) > 1
+ORDER BY a;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedGroupByAndHaving()
+ {
+ const string input = "SELECT a, COUNT(*) FROM t GROUP BY a HAVING COUNT(*) > 1 ORDER BY a;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented };
+ const string expected =
+@"
+SELECT
+ a,
+ COUNT(*)
+FROM
+ t
+GROUP BY
+ a
+HAVING
+ COUNT(*) > 1
+ORDER BY
+ a;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- Nested derived table (the motivating case: linear growth vs. rightward drift) -------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignedNestedDerivedTable()
+ {
+ // Aligned pushes the inner query to the right of the outer FROM keyword; Indented (below)
+ // adds a single fixed level per nesting instead.
+ const string input = "SELECT o.a FROM (SELECT x AS a FROM t1 WHERE x > 0) AS o WHERE o.a < 10;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Aligned };
+ const string expected =
+@"
+SELECT o.a
+FROM (SELECT x AS a
+ FROM t1
+ WHERE x > 0) AS o
+WHERE o.a < 10;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedNestedDerivedTableGrowsLinearly()
+ {
+ const string input = "SELECT o.a FROM (SELECT x AS a FROM t1 WHERE x > 0) AS o WHERE o.a < 10;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented };
+ const string expected =
+@"
+SELECT
+ o.a
+FROM
+ (SELECT
+ x AS a
+ FROM
+ t1
+ WHERE
+ x > 0) AS o
+WHERE
+ o.a < 10;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- UNION -------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignedUnion()
+ {
+ const string input = "SELECT a FROM t1 UNION SELECT b FROM t2;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Aligned };
+ const string expected =
+@"
+SELECT a
+FROM t1
+UNION
+SELECT b
+FROM t2;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedUnion()
+ {
+ const string input = "SELECT a FROM t1 UNION SELECT b FROM t2;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented };
+ const string expected =
+@"
+SELECT
+ a
+FROM
+ t1
+UNION
+SELECT
+ b
+FROM
+ t2;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- Common table expression -------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignedCommonTableExpression()
+ {
+ const string input = "WITH c AS (SELECT a FROM t) SELECT a FROM c;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Aligned };
+ const string expected =
+@"
+WITH c
+AS (SELECT a
+ FROM t)
+SELECT a
+FROM c;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedCommonTableExpression()
+ {
+ const string input = "WITH c AS (SELECT a FROM t) SELECT a FROM c;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented };
+ const string expected =
+@"
+WITH c
+AS (SELECT
+ a
+ FROM
+ t)
+SELECT
+ a
+FROM
+ c;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- Subquery in WHERE -------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignedSubqueryInWhere()
+ {
+ const string input = "SELECT a FROM t WHERE x IN (SELECT id FROM u);";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Aligned };
+ const string expected =
+@"
+SELECT a
+FROM t
+WHERE x IN (SELECT id
+ FROM u);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedSubqueryInWhere()
+ {
+ const string input = "SELECT a FROM t WHERE x IN (SELECT id FROM u);";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented };
+ const string expected =
+@"
+SELECT
+ a
+FROM
+ t
+WHERE
+ x IN (SELECT
+ id
+ FROM
+ u);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- Custom indentation size -------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignedIgnoresCustomIndentationSize()
+ {
+ // Aligned uses keyword width, not IndentationSize, so the size setting has no effect here:
+ // the output is identical to the default-IndentationSize layout.
+ const string input = "SELECT a FROM t WHERE b = 1;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Aligned, IndentationSize = 2 };
+ const string expected =
+@"
+SELECT a
+FROM t
+WHERE b = 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedHonorsCustomIndentationSize()
+ {
+ const string input = "SELECT a FROM t WHERE b = 1;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented, IndentationSize = 2 };
+ const string expected =
+@"
+SELECT
+ a
+FROM
+ t
+WHERE
+ b = 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- DELETE ... WHERE --------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignedDeleteWhereClause()
+ {
+ const string input = "DELETE FROM t WHERE x = 1;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Aligned };
+ const string expected =
+@"
+DELETE t
+WHERE x = 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedDeleteWhereClause()
+ {
+ const string input = "DELETE FROM t WHERE x = 1;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented };
+ const string expected =
+@"
+DELETE t
+WHERE
+ x = 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- UPDATE ------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignedUpdateStatement()
+ {
+ const string input = "UPDATE t SET a = 1 WHERE b = 2;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Aligned };
+ const string expected =
+@"
+UPDATE t
+SET a = 1
+WHERE b = 2;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedUpdateStatement()
+ {
+ const string input = "UPDATE t SET a = 1 WHERE b = 2;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented };
+ const string expected =
+@"
+UPDATE t
+SET a = 1
+WHERE
+ b = 2;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- SELECT ... INTO ---------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignedSelectInto()
+ {
+ const string input = "SELECT a INTO t2 FROM t1;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Aligned };
+ const string expected =
+@"
+SELECT a
+INTO t2
+FROM t1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedSelectInto()
+ {
+ const string input = "SELECT a INTO t2 FROM t1;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented };
+ const string expected =
+@"
+SELECT
+ a
+INTO
+ t2
+FROM
+ t1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- WHERE CURRENT OF --------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignedWhereCurrentOf()
+ {
+ const string input = "DELETE FROM t WHERE CURRENT OF c;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Aligned };
+ const string expected =
+@"
+DELETE t
+WHERE CURRENT OF c;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedWhereCurrentOf()
+ {
+ const string input = "DELETE FROM t WHERE CURRENT OF c;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented };
+ const string expected =
+@"
+DELETE t
+WHERE
+ CURRENT OF c;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- SELECT ... INTO ... ON filegroup ----------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignedSelectIntoOnFilegroup()
+ {
+ const string input = "SELECT c1 INTO t2 ON fg FROM t1;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Aligned };
+ const string expected =
+@"
+SELECT c1
+INTO t2
+ON fg
+FROM t1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedSelectIntoOnFilegroup()
+ {
+ const string input = "SELECT c1 INTO t2 ON fg FROM t1;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented };
+ const string expected =
+@"
+SELECT
+ c1
+INTO
+ t2
+ON
+ fg
+FROM
+ t1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- JOIN with ON (does the ON search condition follow the clause-body option?) ----------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignedInnerJoin()
+ {
+ const string input = "SELECT a.x FROM a INNER JOIN b ON a.id = b.id WHERE a.x > 0;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Aligned };
+ const string expected =
+@"
+SELECT a.x
+FROM a
+ INNER JOIN
+ b
+ ON a.id = b.id
+WHERE a.x > 0;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedInnerJoin()
+ {
+ const string input = "SELECT a.x FROM a INNER JOIN b ON a.id = b.id WHERE a.x > 0;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented };
+ const string expected =
+@"
+SELECT
+ a.x
+FROM
+ a
+ INNER JOIN
+ b
+ ON a.id = b.id
+WHERE
+ a.x > 0;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- JOIN with ON, NewLineBeforeJoinClause = false and NewLineBeforeOnClause = false ------
+ // (both the JOIN keyword and the ON search condition stay on the table-source line) ---------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignedInnerJoinNoNewLineBeforeJoin()
+ {
+ const string input = "SELECT a.x FROM a INNER JOIN b ON a.id = b.id WHERE a.x > 0;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Aligned, NewLineBeforeJoinClause = false, NewLineBeforeOnClause = false };
+ const string expected =
+@"
+SELECT a.x
+FROM a INNER JOIN
+ b ON a.id = b.id
+WHERE a.x > 0;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedInnerJoinNoNewLineBeforeJoin()
+ {
+ const string input = "SELECT a.x FROM a INNER JOIN b ON a.id = b.id WHERE a.x > 0;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented, NewLineBeforeJoinClause = false, NewLineBeforeOnClause = false };
+ const string expected =
+@"
+SELECT
+ a.x
+FROM
+ a INNER JOIN
+ b ON a.id = b.id
+WHERE
+ a.x > 0;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- NewLineBeforeXxxClause = false (the clause body stays on the keyword line) -----------
+ // Indented only moves a clause body onto its own line when that clause is configured to
+ // start on a new line. With the NewLineBefore* options off, FROM/WHERE keep their bodies
+ // inline exactly as in Aligned mode. The SELECT list has no NewLineBefore* option of its
+ // own, so it still breaks onto its own line.
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignedNewLineBeforeClauseOptionsDisabled()
+ {
+ const string input = "SELECT a FROM t WHERE b = 1;";
+ var options = new SqlScriptGeneratorOptions
+ {
+ ClauseBodyAlignment = ClauseBodyAlignment.Aligned,
+ NewLineBeforeFromClause = false,
+ NewLineBeforeWhereClause = false
+ };
+ const string expected = @"
+SELECT a FROM t WHERE b = 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedNewLineBeforeClauseOptionsDisabled()
+ {
+ const string input = "SELECT a FROM t WHERE b = 1;";
+ var options = new SqlScriptGeneratorOptions
+ {
+ ClauseBodyAlignment = ClauseBodyAlignment.Indented,
+ NewLineBeforeFromClause = false,
+ NewLineBeforeWhereClause = false
+ };
+ const string expected = @"
+SELECT
+ a FROM t WHERE b = 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- AlignClauseBodies = false in Aligned mode -------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignedWithAlignClauseBodiesDisabled()
+ {
+ // With AlignClauseBodies off, Aligned falls back to a single space after each keyword
+ // instead of padding to the shared column. The SELECT list still aligns under its own
+ // first item, which is a separate alignment point.
+ const string input = "SELECT a, b FROM t WHERE c = 1;";
+ var options = new SqlScriptGeneratorOptions
+ {
+ ClauseBodyAlignment = ClauseBodyAlignment.Aligned,
+ AlignClauseBodies = false
+ };
+ const string expected =
+@"
+SELECT a,
+ b
+FROM t
+WHERE c = 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- MultilineSelectElementsList = false --------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedWithSingleLineSelectElementsList()
+ {
+ // The select list stays on one line but is still indented one level under SELECT.
+ const string input = "SELECT a, b, c FROM t;";
+ var options = new SqlScriptGeneratorOptions
+ {
+ ClauseBodyAlignment = ClauseBodyAlignment.Indented,
+ MultilineSelectElementsList = false
+ };
+ const string expected =
+@"
+SELECT
+ a, b, c
+FROM
+ t;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- Interaction with IndentationMode.Tabs ------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedWithTabsIndentation()
+ {
+ // The indent that Indented adds is emitted through the normal indentation path, so it
+ // is rendered with tab characters when IndentationMode is Tabs. The expected constant
+ // below contains real tab characters.
+ const string input = "SELECT a FROM t WHERE b = 1;";
+ var options = new SqlScriptGeneratorOptions
+ {
+ ClauseBodyAlignment = ClauseBodyAlignment.Indented,
+ IndentationMode = IndentationMode.Tabs
+ };
+ const string expected =
+@"
+SELECT
+ a
+FROM
+ t
+WHERE
+ b = 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- Interaction with CommaPlacement.Leading ---------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedWithLeadingCommas()
+ {
+ // Leading commas reserve their width inside the indent, so every select item still
+ // starts at the same column as the first one.
+ const string input = "SELECT a, b, c FROM t;";
+ var options = new SqlScriptGeneratorOptions
+ {
+ ClauseBodyAlignment = ClauseBodyAlignment.Indented,
+ CommaPlacement = CommaPlacement.Leading
+ };
+ const string expected =
+@"
+SELECT
+ a
+ , b
+ , c
+FROM
+ t;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- OFFSET/FETCH is not a clause body and stays inline ----------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedOffsetFetchIsNotIndented()
+ {
+ // ORDER BY is a clause body and indents; the OFFSET/FETCH clause that follows it is
+ // not part of clause-body handling and keeps its existing inline layout.
+ const string input = "SELECT a FROM t ORDER BY a OFFSET 10 ROWS FETCH NEXT 5 ROWS ONLY;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented };
+ const string expected =
+@"
+SELECT
+ a
+FROM
+ t
+ORDER BY
+ a
+OFFSET 10 ROWS FETCH NEXT 5 ROWS ONLY;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // --- FOR clause --------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignedForClause()
+ {
+ const string input = "SELECT a FROM t FOR XML AUTO;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Aligned };
+ const string expected =
+@"
+SELECT a
+FROM t
+FOR XML AUTO;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIndentedForClause()
+ {
+ // The FOR clause body is treated like any other clause body, so it moves onto its own
+ // indented line rather than staying on the FOR line.
+ const string input = "SELECT a FROM t FOR XML AUTO;";
+ var options = new SqlScriptGeneratorOptions { ClauseBodyAlignment = ClauseBodyAlignment.Indented };
+ const string expected =
+@"
+SELECT
+ a
+FROM
+ t
+FOR
+ XML AUTO;";
+
+ AssertGenerated(input, options, expected);
+ }
+ }
+}
+
diff --git a/Test/SqlDom/ScriptGenerator/InValuesListFormattingTests.cs b/Test/SqlDom/ScriptGenerator/InValuesListFormattingTests.cs
new file mode 100644
index 0000000..0a76def
--- /dev/null
+++ b/Test/SqlDom/ScriptGenerator/InValuesListFormattingTests.cs
@@ -0,0 +1,346 @@
+//------------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+//------------------------------------------------------------------------------
+
+using Microsoft.SqlServer.TransactSql.ScriptDom;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using SqlStudio.Tests.AssemblyTools.TestCategory;
+using static SqlStudio.Tests.UTSqlScriptDom.ScriptGeneratorTestHelper;
+
+namespace SqlStudio.Tests.UTSqlScriptDom
+{
+ // Tests for the MultilineInValuesList script-generation option, which controls whether the
+ // values in an IN (values) predicate are written on a single line (default) or on multiple
+ // lines. Kept in a dedicated file to avoid churn in ScriptGeneratorTests.cs.
+ //
+ // Work item: Formatter option: IN (values) list width
+ [TestClass]
+ public class InValuesListFormattingTests
+ {
+ // Builds options that isolate the IN-list layout: clause bodies are not aligned and clauses
+ // are not broken onto their own lines, so the surrounding statement stays on one line and the
+ // expectations focus on the IN (values) list itself.
+ private static SqlScriptGeneratorOptions MakeOptions(bool multilineInValuesList)
+ {
+ return new SqlScriptGeneratorOptions
+ {
+ MultilineInValuesList = multilineInValuesList,
+ AlignClauseBodies = false,
+ NewLineBeforeFromClause = false,
+ NewLineBeforeWhereClause = false,
+ MultilineSelectElementsList = false,
+ MultilineWherePredicatesList = false,
+ };
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // Default
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineInValuesListDefaultIsFalse()
+ {
+ Assert.IsFalse(new SqlScriptGeneratorOptions().MultilineInValuesList);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestDefaultKeepsInListOnSingleLine()
+ {
+ // With the option at its default (false) the IN list stays on a single line.
+ const string input = "SELECT * FROM t WHERE x IN (1, 2, 3);";
+ var options = MakeOptions(false);
+ const string expected = @"SELECT * FROM t WHERE x IN (1, 2, 3);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // Multi-line layout
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineTrailingCommaPutsEachValueOnItsOwnLine()
+ {
+ // With the option enabled and the default (trailing) comma placement, each value is
+ // written on its own line with a trailing comma. The list is aligned under the WHERE
+ // clause body, with each value indented one level (4) past the closing parenthesis.
+ const string input = "SELECT * FROM t WHERE x IN (1, 2, 3);";
+ var options = MakeOptions(true);
+ const string expected = @"
+SELECT * FROM t WHERE x IN (
+ 1,
+ 2,
+ 3
+ );";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineLeadingCommaPlacement()
+ {
+ // CommaPlacement = Leading applies within the IN list: values sit at the list
+ // indentation level and each comma is indented two characters fewer.
+ const string input = "SELECT * FROM t WHERE x IN (1, 2, 3);";
+ var options = MakeOptions(true);
+ options.CommaPlacement = CommaPlacement.Leading;
+ const string expected = @"
+SELECT * FROM t WHERE x IN (
+ 1
+ , 2
+ , 3
+ );";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineNewLineBeforeOpenParenthesis()
+ {
+ // NewLineBeforeOpenParenthesisInMultilineList moves the open parenthesis onto its own line.
+ const string input = "SELECT * FROM t WHERE x IN (1, 2, 3);";
+ var options = MakeOptions(true);
+ options.NewLineBeforeOpenParenthesisInMultilineList = true;
+ const string expected = @"
+SELECT * FROM t WHERE x IN
+ (
+ 1,
+ 2,
+ 3
+ );";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineNoNewLineBeforeCloseParenthesis()
+ {
+ // NewLineBeforeCloseParenthesisInMultilineList = false keeps the close parenthesis on the
+ // same line as the last value.
+ const string input = "SELECT * FROM t WHERE x IN (1, 2, 3);";
+ var options = MakeOptions(true);
+ options.NewLineBeforeCloseParenthesisInMultilineList = false;
+ const string expected = @"
+SELECT * FROM t WHERE x IN (
+ 1,
+ 2,
+ 3);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // Variants and edge cases
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineAppliesToNotIn()
+ {
+ // The option also governs a negated (NOT IN) list.
+ const string input = "SELECT * FROM t WHERE x NOT IN (1, 2, 3);";
+ var options = MakeOptions(true);
+ const string expected = @"
+SELECT * FROM t WHERE x NOT IN (
+ 1,
+ 2,
+ 3
+ );";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineSingleValueStillWraps()
+ {
+ // A single-value IN list wraps too (consistent with the parenthesized-list behavior used
+ // for column lists).
+ const string input = "SELECT * FROM t WHERE x IN (1);";
+ var options = MakeOptions(true);
+ const string expected = @"
+SELECT * FROM t WHERE x IN (
+ 1
+ );";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineDoesNotAffectSubqueryInPredicate()
+ {
+ // An IN (subquery) predicate has no value list, so the option must not change it.
+ const string input = "SELECT * FROM t WHERE x IN (SELECT id FROM u);";
+ var options = MakeOptions(true);
+ const string expected = @"SELECT * FROM t WHERE x IN (SELECT id FROM u);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineWrapsStringAndExpressionValues()
+ {
+ // Values can be arbitrary scalar expressions (string literals, columns), not just integers.
+ const string input = "SELECT * FROM t WHERE x IN ('a', 'b', c);";
+ var options = MakeOptions(true);
+ const string expected = @"
+SELECT * FROM t WHERE x IN (
+ 'a',
+ 'b',
+ c
+ );";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineAppliesInDeleteWhereClause()
+ {
+ // The option applies wherever an IN (values) predicate appears, not only in SELECT. The
+ // list aligns under this statement's (shorter) WHERE clause body.
+ const string input = "DELETE FROM t WHERE x IN (1, 2, 3);";
+ var options = MakeOptions(true);
+ const string expected = @"
+DELETE t WHERE x IN (
+ 1,
+ 2,
+ 3
+ );";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineOnlyAffectsInListWhenOtherListsAreDefault()
+ {
+ // Isolation: enabling MultilineInValuesList on top of otherwise-default options wraps the
+ // IN list, while every other list keeps its own default behavior. Here the SELECT column
+ // list still wraps because MultilineSelectElementsList defaults to true, independently of
+ // this option.
+ const string input = "SELECT a, b FROM t WHERE x IN (1, 2, 3);";
+ var options = new SqlScriptGeneratorOptions { MultilineInValuesList = true };
+ const string expected = @"
+SELECT a,
+ b
+FROM t
+WHERE x IN (
+ 1,
+ 2,
+ 3
+);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // Indentation and multiple contexts
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineHonorsCustomIndentationSize()
+ {
+ // Teams that use a 2-space indent should see the wrapped IN values indented by that
+ // amount (IndentationSize) past the list's alignment level, not the default 4.
+ const string input = "SELECT * FROM t WHERE x IN (1, 2, 3);";
+ var options = MakeOptions(true);
+ options.IndentationSize = 2;
+ const string expected = @"
+SELECT * FROM t WHERE x IN (
+ 1,
+ 2,
+ 3
+ );";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineHonorsTabIndentation()
+ {
+ // With IndentationMode = Tabs the wrapped IN values are indented with tab characters.
+ const string input = "SELECT * FROM t WHERE x IN (1, 2, 3);";
+ var options = MakeOptions(true);
+ options.IndentationMode = IndentationMode.Tabs;
+ // The wrapped values are indented with tab characters (the literal tabs in the lines below).
+ const string expected = @"
+SELECT * FROM t WHERE x IN (
+ 1,
+ 2,
+ 3
+ );";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineAppliesInCheckConstraint()
+ {
+ // The option applies to DDL too: a CHECK constraint whose predicate is an IN (values)
+ // list wraps the values just like a DML WHERE clause.
+ const string input = "ALTER TABLE t ADD CONSTRAINT ck CHECK (x IN (1, 2, 3));";
+ var options = MakeOptions(true);
+ const string expected = @"
+ALTER TABLE t
+ ADD CONSTRAINT ck CHECK (x IN (
+ 1,
+ 2,
+ 3
+ ));";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineWrapsMultipleInListsIndependently()
+ {
+ // A realistic query filtering on two IN lists joined by AND: each list wraps on its own,
+ // and the surrounding predicate layout is preserved.
+ const string input = "SELECT * FROM t WHERE x IN (1, 2, 3) AND y IN (4, 5, 6);";
+ var options = MakeOptions(true);
+ const string expected = @"
+SELECT * FROM t WHERE x IN (
+ 1,
+ 2,
+ 3
+ ) AND y IN (
+ 4,
+ 5,
+ 6
+ );";
+
+ AssertGenerated(input, options, expected);
+ }
+ }
+}
diff --git a/Test/SqlDom/ScriptGenerator/InsertTargetsListFormattingTests.cs b/Test/SqlDom/ScriptGenerator/InsertTargetsListFormattingTests.cs
new file mode 100644
index 0000000..0b8250f
--- /dev/null
+++ b/Test/SqlDom/ScriptGenerator/InsertTargetsListFormattingTests.cs
@@ -0,0 +1,486 @@
+//------------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+//------------------------------------------------------------------------------
+
+using Microsoft.SqlServer.TransactSql.ScriptDom;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using SqlStudio.Tests.AssemblyTools.TestCategory;
+using static SqlStudio.Tests.UTSqlScriptDom.ScriptGeneratorTestHelper;
+
+namespace SqlStudio.Tests.UTSqlScriptDom
+{
+ // Tests for the MultilineInsertTargetsList script-generation option, which controls whether the
+ // INSERT column target list (the parenthesized list of columns after the target table) is
+ // written one column per line (true) as a multi-line parenthesized list - like the
+ // CREATE TABLE / VIEW column lists - or collapsed onto a single line (false, the default). The
+ // option only affects the INSERT target list; the INSERT source (VALUES / SELECT / EXECUTE) is
+ // unaffected.
+ [TestClass]
+ public class InsertTargetsListFormattingTests
+ {
+ // -----------------------------------------------------------------------------------------
+ // Default
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineInsertTargetsListDefaultIsFalse()
+ {
+ // The default preserves the historical single-line INSERT target list output.
+ Assert.AreEqual(false, new SqlScriptGeneratorOptions().MultilineInsertTargetsList);
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // VALUES source
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestSingleLineTargetsIsDefault()
+ {
+ // With default options the target list stays on a single line (unchanged behavior).
+ const string input = "INSERT INTO t (a, b, c) VALUES (1, 2, 3);";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+INSERT INTO t (a, b, c)
+VALUES (1, 2, 3);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineTargetsWhenEnabled()
+ {
+ // When the option is enabled each target column is placed on its own line inside the
+ // parentheses.
+ const string input = "INSERT INTO t (a, b, c) VALUES (1, 2, 3);";
+ var options = new SqlScriptGeneratorOptions { MultilineInsertTargetsList = true };
+ const string expected =
+@"
+INSERT INTO t (
+ a,
+ b,
+ c
+)
+VALUES (1, 2, 3);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestSingleLineTargetsWhenDisabled()
+ {
+ // With the option off the target list collapses onto a single line.
+ const string input = "INSERT INTO t (a, b, c) VALUES (1, 2, 3);";
+ var options = new SqlScriptGeneratorOptions { MultilineInsertTargetsList = false };
+ const string expected =
+@"
+INSERT INTO t (a, b, c)
+VALUES (1, 2, 3);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestSingleTargetMultiline()
+ {
+ // A single-column target list is still spread onto its own line when the option is on.
+ const string input = "INSERT INTO t (a) VALUES (1);";
+ var options = new SqlScriptGeneratorOptions { MultilineInsertTargetsList = true };
+ const string expected =
+@"
+INSERT INTO t (
+ a
+)
+VALUES (1);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestNoTargetsUnaffected()
+ {
+ // An INSERT with no column target list is unaffected by the option.
+ const string input = "INSERT INTO t VALUES (1, 2, 3);";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+INSERT INTO t
+VALUES (1, 2, 3);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestNoTargetsUnaffectedWhenDisabled()
+ {
+ // The same INSERT with no target list is identical whether the option is on or off.
+ const string input = "INSERT INTO t VALUES (1, 2, 3);";
+ var on = new SqlScriptGeneratorOptions { MultilineInsertTargetsList = true };
+ var off = new SqlScriptGeneratorOptions { MultilineInsertTargetsList = false };
+
+ Assert.AreEqual(Normalize(Generate(input, on)), Normalize(Generate(input, off)));
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // SELECT source
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineTargetsWithSelectSource()
+ {
+ // The multi-line target list works the same way when the source is a SELECT.
+ const string input = "INSERT INTO t (a, b) SELECT x, y FROM s;";
+ var options = new SqlScriptGeneratorOptions { MultilineInsertTargetsList = true };
+ const string expected =
+@"
+INSERT INTO t (
+ a,
+ b
+)
+SELECT x,
+ y
+FROM s;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestSingleLineTargetsWithSelectSourceWhenDisabled()
+ {
+ const string input = "INSERT INTO t (a, b) SELECT x, y FROM s;";
+ var options = new SqlScriptGeneratorOptions { MultilineInsertTargetsList = false };
+ const string expected =
+@"
+INSERT INTO t (a, b)
+SELECT x,
+ y
+FROM s;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // Comma placement
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestLeadingCommaMultilineTargets()
+ {
+ // With CommaPlacement = Leading each column's comma is emitted at the start of its line,
+ // indented two characters fewer than the column.
+ const string input = "INSERT INTO t (a, b, c) VALUES (1, 2, 3);";
+ var options = new SqlScriptGeneratorOptions { MultilineInsertTargetsList = true, CommaPlacement = CommaPlacement.Leading };
+ const string expected =
+@"
+INSERT INTO t (
+ a
+ , b
+ , c
+)
+VALUES (1, 2, 3);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestTrailingCommaMultilineTargets()
+ {
+ // With CommaPlacement = Trailing (default) each column's comma follows it.
+ const string input = "INSERT INTO t (a, b, c) VALUES (1, 2, 3);";
+ var options = new SqlScriptGeneratorOptions { MultilineInsertTargetsList = true, CommaPlacement = CommaPlacement.Trailing };
+ const string expected =
+@"
+INSERT INTO t (
+ a,
+ b,
+ c
+)
+VALUES (1, 2, 3);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // Parenthesis placement options
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineTargetsNewLineBeforeOpenParenthesis()
+ {
+ // NewLineBeforeOpenParenthesisInMultilineList moves the opening parenthesis to its own
+ // line for the multi-line target list.
+ const string input = "INSERT INTO t (a, b, c) VALUES (1, 2, 3);";
+ var options = new SqlScriptGeneratorOptions
+ {
+ MultilineInsertTargetsList = true,
+ NewLineBeforeOpenParenthesisInMultilineList = true,
+ };
+ const string expected =
+@"
+INSERT INTO t
+(
+ a,
+ b,
+ c
+)
+VALUES (1, 2, 3);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineTargetsNewLineBeforeCloseParenthesisDisabled()
+ {
+ // With NewLineBeforeCloseParenthesisInMultilineList off the closing parenthesis stays on
+ // the same line as the last column.
+ const string input = "INSERT INTO t (a, b, c) VALUES (1, 2, 3);";
+ var options = new SqlScriptGeneratorOptions
+ {
+ MultilineInsertTargetsList = true,
+ NewLineBeforeCloseParenthesisInMultilineList = false,
+ };
+ const string expected =
+@"
+INSERT INTO t (
+ a,
+ b,
+ c)
+VALUES (1, 2, 3);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // Interaction with other INSERT clauses / sources
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineTargetsWithOutputClause()
+ {
+ // The multi-line target list renders correctly when an OUTPUT clause follows it.
+ const string input = "INSERT INTO t (a, b) OUTPUT inserted.a, inserted.b VALUES (1, 2);";
+ var options = new SqlScriptGeneratorOptions { MultilineInsertTargetsList = true };
+ const string expected =
+@"
+INSERT INTO t (
+ a,
+ b
+)
+OUTPUT inserted.a, inserted.b
+VALUES (1, 2);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineTargetsWithOutputIntoClause()
+ {
+ // The multi-line target list renders correctly when an OUTPUT ... INTO clause follows it.
+ const string input = "INSERT INTO t (a, b) OUTPUT inserted.a INTO @log VALUES (1, 2);";
+ var options = new SqlScriptGeneratorOptions { MultilineInsertTargetsList = true };
+ const string expected =
+@"
+INSERT INTO t (
+ a,
+ b
+)
+OUTPUT inserted.a INTO @log
+VALUES (1, 2);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineTargetsWithExecuteSource()
+ {
+ // The multi-line target list works when the source is an EXECUTE statement.
+ const string input = "INSERT INTO t (a, b) EXEC('select 1, 2');";
+ var options = new SqlScriptGeneratorOptions { MultilineInsertTargetsList = true };
+ const string expected =
+@"
+INSERT INTO t (
+ a,
+ b
+)
+EXECUTE ('select 1, 2');";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineTargetsWithTopRowFilter()
+ {
+ // A TOP row filter before the target table does not interfere with the multi-line list.
+ const string input = "INSERT TOP (5) INTO t (a, b) SELECT x, y FROM s;";
+ var options = new SqlScriptGeneratorOptions { MultilineInsertTargetsList = true };
+ const string expected =
+@"
+INSERT TOP (5) INTO t (
+ a,
+ b
+)
+SELECT x,
+ y
+FROM s;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineTargetsWithCommonTableExpression()
+ {
+ // A leading WITH common table expression does not interfere with the multi-line list.
+ const string input = "WITH c AS (SELECT 1 x, 2 y) INSERT INTO t (a, b) SELECT x, y FROM c;";
+ var options = new SqlScriptGeneratorOptions { MultilineInsertTargetsList = true };
+ const string expected =
+@"
+WITH c
+AS (SELECT 1 AS x,
+ 2 AS y)
+INSERT INTO t (
+ a,
+ b
+)
+SELECT x,
+ y
+FROM c;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineTargetsWithMultiRowValues()
+ {
+ // Only the target list is affected; a multi-row VALUES source is left as-is.
+ const string input = "INSERT INTO t (a, b) VALUES (1, 2), (3, 4);";
+ var options = new SqlScriptGeneratorOptions { MultilineInsertTargetsList = true };
+ const string expected =
+@"
+INSERT INTO t (
+ a,
+ b
+)
+VALUES (1, 2),
+(3, 4);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // MERGE ... WHEN NOT MATCHED THEN INSERT
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMergeInsertTargetsSingleLineIsDefault()
+ {
+ // The INSERT action of a MERGE keeps its target list single-line by default (unchanged
+ // behavior), just like a top-level INSERT.
+ const string input = "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED THEN INSERT (a, b, c) VALUES (s.a, s.b, s.c);";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+MERGE INTO t
+
+USING s ON t.id = s.id
+WHEN NOT MATCHED THEN INSERT (a, b, c) VALUES (s.a, s.b, s.c);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMergeInsertTargetsMultilineWhenEnabled()
+ {
+ // The INSERT action of a MERGE honors MultilineInsertTargetsList, spreading its target
+ // list one column per line - parity with the top-level INSERT statement.
+ const string input = "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED THEN INSERT (a, b, c) VALUES (s.a, s.b, s.c);";
+ var options = new SqlScriptGeneratorOptions { MultilineInsertTargetsList = true };
+ const string expected =
+@"
+MERGE INTO t
+
+USING s ON t.id = s.id
+WHEN NOT MATCHED THEN INSERT (
+ a,
+ b,
+ c
+) VALUES (s.a, s.b, s.c);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMergeInsertTargetsMultilineRealWorld()
+ {
+ // A realistic MERGE with an aliased target/source and UPDATE / INSERT / DELETE actions:
+ // only the INSERT action's target list is spread onto multiple lines.
+ const string input =
+ "MERGE INTO dbo.Target AS tgt USING dbo.Source AS src ON tgt.Id = src.Id " +
+ "WHEN MATCHED THEN UPDATE SET tgt.Name = src.Name, tgt.Amount = src.Amount " +
+ "WHEN NOT MATCHED BY TARGET THEN INSERT (Id, Name, Amount) VALUES (src.Id, src.Name, src.Amount) " +
+ "WHEN NOT MATCHED BY SOURCE THEN DELETE;";
+ var options = new SqlScriptGeneratorOptions { MultilineInsertTargetsList = true };
+ const string expected =
+@"
+MERGE INTO dbo.Target
+ AS tgt
+USING dbo.Source AS src ON tgt.Id = src.Id
+WHEN MATCHED THEN UPDATE
+SET tgt.Name = src.Name,
+ tgt.Amount = src.Amount
+WHEN NOT MATCHED BY TARGET THEN INSERT (
+ Id,
+ Name,
+ Amount
+) VALUES (src.Id, src.Name, src.Amount)
+WHEN NOT MATCHED BY SOURCE THEN DELETE;";
+
+ AssertGenerated(input, options, expected);
+ }
+ }
+}
diff --git a/Test/SqlDom/ScriptGenerator/ProcedureParametersFormattingTests.cs b/Test/SqlDom/ScriptGenerator/ProcedureParametersFormattingTests.cs
new file mode 100644
index 0000000..1f50dcc
--- /dev/null
+++ b/Test/SqlDom/ScriptGenerator/ProcedureParametersFormattingTests.cs
@@ -0,0 +1,437 @@
+//------------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+//------------------------------------------------------------------------------
+
+using Microsoft.SqlServer.TransactSql.ScriptDom;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using SqlStudio.Tests.AssemblyTools.TestCategory;
+using System.Collections.Generic;
+using System.IO;
+using static SqlStudio.Tests.UTSqlScriptDom.ScriptGeneratorTestHelper;
+
+namespace SqlStudio.Tests.UTSqlScriptDom
+{
+ // Tests for the MultilineProcedureParametersList script-generation option, which controls
+ // whether CREATE/ALTER PROCEDURE and CREATE/ALTER FUNCTION parameters are written one per line
+ // (true) or collapsed onto a single line (false, the default). The default preserves the
+ // existing single-line behavior; multi-line output is opt-in. Procedure parameters are not
+ // wrapped in parentheses; function parameters are.
+ //
+ // Work item: Formatter option: Procedure/function parameters across multiple lines
+ [TestClass]
+ public class ProcedureParametersFormattingTests
+ {
+ // Options that opt in to the multi-line parameter layout, leaving everything else at default.
+ private static SqlScriptGeneratorOptions Multiline()
+ {
+ return new SqlScriptGeneratorOptions { MultilineProcedureParametersList = true };
+ }
+
+ // Generates a script for CREATE/ALTER EXTERNAL FUNCTION syntax, which is parsed and emitted
+ // by the Fabric DW parser/generator. Asserts the input parses and the output reparses.
+ private static string GenerateFabricDW(string sql, SqlScriptGeneratorOptions options)
+ {
+ var parser = new TSqlFabricDWParser(true);
+ TSqlFragment fragment = parser.Parse(new StringReader(sql), out IList errors);
+ Assert.AreEqual(0, errors.Count, "Input must parse without errors.");
+
+ var generator = new SqlFabricDWScriptGenerator(options);
+ generator.GenerateScript(fragment, out string generated);
+
+ var reparser = new TSqlFabricDWParser(true);
+ reparser.Parse(new StringReader(generated), out IList reErrors);
+ Assert.AreEqual(0, reErrors.Count, "Generated script must reparse without errors. Actual:\n" + generated);
+ return generated;
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // Default: the option is off, so existing single-line behavior is preserved.
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestMultilineProcedureParametersListDefaultIsFalse()
+ {
+ Assert.AreEqual(false, new SqlScriptGeneratorOptions().MultilineProcedureParametersList);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestProcedureDefaultKeepsParametersOnSingleLine()
+ {
+ // Default options must reproduce the previous behavior: all parameters on one line.
+ const string input = "CREATE PROCEDURE p1 @a INT, @b INT AS SELECT 1;";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+CREATE PROCEDURE p1
+@a INT, @b INT
+AS
+SELECT 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestFunctionDefaultKeepsParametersOnSingleLine()
+ {
+ // Default options must reproduce the previous behavior: parameters on one parenthesized line.
+ const string input = "CREATE FUNCTION dbo.f (@a INT, @b INT) RETURNS INT AS BEGIN RETURN 1; END";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+CREATE FUNCTION dbo.f
+(@a INT, @b INT)
+RETURNS INT
+AS
+BEGIN
+ RETURN 1;
+END";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestProcedureNoParametersUnaffected()
+ {
+ const string input = "CREATE PROCEDURE p1 AS SELECT 1;";
+ var options = Multiline();
+ const string expected =
+@"
+CREATE PROCEDURE p1
+AS
+SELECT 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestFunctionNoParametersUnaffected()
+ {
+ const string input = "CREATE FUNCTION dbo.f () RETURNS INT AS BEGIN RETURN 1; END";
+ var options = Multiline();
+ const string expected =
+@"
+CREATE FUNCTION dbo.f
+( )
+RETURNS INT
+AS
+BEGIN
+ RETURN 1;
+END";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // CREATE PROCEDURE with the option enabled (parameters are not parenthesized)
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestProcedureMultilineWhenEnabled()
+ {
+ const string input = "CREATE PROCEDURE p1 @a INT, @b INT AS SELECT 1;";
+ var options = Multiline();
+ const string expected =
+@"
+CREATE PROCEDURE p1
+ @a INT,
+ @b INT
+AS
+SELECT 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestProcedureSingleParameterMultilineWhenEnabled()
+ {
+ const string input = "CREATE PROCEDURE p1 @a INT AS SELECT 1;";
+ var options = Multiline();
+ const string expected =
+@"
+CREATE PROCEDURE p1
+ @a INT
+AS
+SELECT 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestProcedureMultilineLeadingComma()
+ {
+ const string input = "CREATE PROCEDURE p1 @a INT, @b INT AS SELECT 1;";
+ var options = Multiline();
+ options.CommaPlacement = CommaPlacement.Leading;
+ const string expected =
+@"
+CREATE PROCEDURE p1
+ @a INT
+ , @b INT
+AS
+SELECT 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCreateOrAlterProcedureMultilineWhenEnabled()
+ {
+ const string input = "CREATE OR ALTER PROCEDURE p1 @a INT, @b INT AS SELECT 1;";
+ var options = Multiline();
+ const string expected =
+@"
+CREATE OR ALTER PROCEDURE p1
+ @a INT,
+ @b INT
+AS
+SELECT 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // CREATE FUNCTION with the option enabled (parameters are parenthesized)
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestFunctionMultilineWhenEnabled()
+ {
+ const string input = "CREATE FUNCTION dbo.f (@a INT, @b INT) RETURNS INT AS BEGIN RETURN 1; END";
+ var options = Multiline();
+ const string expected =
+@"
+CREATE FUNCTION dbo.f (
+ @a INT,
+ @b INT
+)
+RETURNS INT
+AS
+BEGIN
+ RETURN 1;
+END";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestFunctionMultilineLeadingComma()
+ {
+ const string input = "CREATE FUNCTION dbo.f (@a INT, @b INT) RETURNS INT AS BEGIN RETURN 1; END";
+ var options = Multiline();
+ options.CommaPlacement = CommaPlacement.Leading;
+ const string expected =
+@"
+CREATE FUNCTION dbo.f (
+ @a INT
+ , @b INT
+)
+RETURNS INT
+AS
+BEGIN
+ RETURN 1;
+END";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlterFunctionMultilineWhenEnabled()
+ {
+ const string input = "ALTER FUNCTION dbo.f (@a INT, @b INT) RETURNS INT AS BEGIN RETURN 1; END";
+ var options = Multiline();
+ const string expected =
+@"
+ALTER FUNCTION dbo.f (
+ @a INT,
+ @b INT
+)
+RETURNS INT
+AS
+BEGIN
+ RETURN 1;
+END";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // AlignColumnDefinitionFields must not affect procedure/function parameters
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestAlignColumnDefinitionFieldsDoesNotAffectParameters()
+ {
+ const string input = "CREATE PROCEDURE p1 @a INT, @bbbbb NVARCHAR (20) AS SELECT 1;";
+ var options = Multiline();
+ options.AlignColumnDefinitionFields = true;
+ const string expected =
+@"
+CREATE PROCEDURE p1
+ @a INT,
+ @bbbbb NVARCHAR (20)
+AS
+SELECT 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // Parameter modifiers and parenthesis-placement interactions (multi-line enabled)
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestProcedureMultilinePreservesParameterModifiers()
+ {
+ // Each per-line parameter must retain its full modifiers (default value, OUTPUT, cursor).
+ const string input = "CREATE PROCEDURE p1 @a INT = 5 OUTPUT, @c CURSOR VARYING OUTPUT AS SELECT 1;";
+ var options = Multiline();
+ const string expected =
+@"
+CREATE PROCEDURE p1
+ @a INT=5 OUTPUT,
+ @c CURSOR VARYING OUTPUT
+AS
+SELECT 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestFunctionMultilineNewLineBeforeOpenParenthesis()
+ {
+ // NewLineBeforeOpenParenthesisInMultilineList moves the '(' onto its own line for the
+ // parenthesized (function) parameter list.
+ const string input = "CREATE FUNCTION dbo.f (@a INT, @b INT) RETURNS INT AS BEGIN RETURN 1; END";
+ var options = Multiline();
+ options.NewLineBeforeOpenParenthesisInMultilineList = true;
+ const string expected =
+@"
+CREATE FUNCTION dbo.f
+(
+ @a INT,
+ @b INT
+)
+RETURNS INT
+AS
+BEGIN
+ RETURN 1;
+END";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestFunctionMultilineNoNewLineBeforeCloseParenthesis()
+ {
+ // NewLineBeforeCloseParenthesisInMultilineList = false keeps ')' on the last parameter's line.
+ const string input = "CREATE FUNCTION dbo.f (@a INT, @b INT) RETURNS INT AS BEGIN RETURN 1; END";
+ var options = Multiline();
+ options.NewLineBeforeCloseParenthesisInMultilineList = false;
+ const string expected =
+@"
+CREATE FUNCTION dbo.f (
+ @a INT,
+ @b INT)
+RETURNS INT
+AS
+BEGIN
+ RETURN 1;
+END";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCreateOrAlterFunctionMultilineWhenEnabled()
+ {
+ const string input = "CREATE OR ALTER FUNCTION dbo.f (@a INT, @b INT) RETURNS INT AS BEGIN RETURN 1; END";
+ var options = Multiline();
+ const string expected =
+@"
+CREATE OR ALTER FUNCTION dbo.f (
+ @a INT,
+ @b INT
+)
+RETURNS INT
+AS
+BEGIN
+ RETURN 1;
+END";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // CREATE/ALTER EXTERNAL FUNCTION (Fabric DW; parameters are parenthesized)
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestExternalFunctionDefaultKeepsParametersOnSingleLine()
+ {
+ const string input = "CREATE FUNCTION dbo.fn (@x INT, @y NVARCHAR (50)) RETURNS INT AS EXTERNAL FUNCTION mySet.myFn;";
+ var options = new SqlScriptGeneratorOptions();
+ string generated = GenerateFabricDW(input, options);
+
+ const string expected =
+@"CREATE FUNCTION dbo.fn (@x INT, @y NVARCHAR (50)) RETURNS INT AS EXTERNAL FUNCTION mySet.myFn;";
+
+ Assert.AreEqual(Normalize(expected).Trim(), Normalize(generated).Trim());
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestExternalFunctionMultilineWhenEnabled()
+ {
+ const string input = "CREATE FUNCTION dbo.fn (@x INT, @y NVARCHAR (50)) RETURNS INT AS EXTERNAL FUNCTION mySet.myFn;";
+ var options = Multiline();
+ string generated = GenerateFabricDW(input, options);
+
+ const string expected =
+@"CREATE FUNCTION dbo.fn (
+ @x INT,
+ @y NVARCHAR (50)
+) RETURNS INT AS EXTERNAL FUNCTION mySet.myFn;";
+
+ Assert.AreEqual(Normalize(expected).Trim(), Normalize(generated).Trim());
+ }
+ }
+}
diff --git a/Test/SqlDom/ScriptGenerator/ScriptGeneratorTests.cs b/Test/SqlDom/ScriptGenerator/ScriptGeneratorTests.cs
index fdaf763..a153791 100644
--- a/Test/SqlDom/ScriptGenerator/ScriptGeneratorTests.cs
+++ b/Test/SqlDom/ScriptGenerator/ScriptGeneratorTests.cs
@@ -2385,11 +2385,10 @@ public void TestCommaPlacementTrailingParenthesizedList()
[SqlStudioTestCategory(Category.UnitTest)]
public void TestCommaPlacementLeadingInsertTargets()
{
- // The INSERT column target list is always emitted as a single-line parenthesized
- // list (via GenerateParenthesisedCommaSeparatedList), and CommaPlacement only affects
- // multi-line lists. MultilineInsertTargetsList is not consumed by the INSERT visitor,
- // so the targets stay on one line and CommaPlacement = Leading has no visual effect:
- // the output is identical to the trailing case below.
+ // With MultilineInsertTargetsList = true the INSERT column target list is emitted as a
+ // multi-line parenthesized list (like CREATE TABLE / VIEW columns): each column on its
+ // own line indented one level. CommaPlacement = Leading places each column's comma at
+ // the start of its line (indented two characters fewer than the column).
var sql = "INSERT INTO t (a, b, c) VALUES (1, 2, 3);";
var parser = new TSql170Parser(true);
var fragment = parser.Parse(new StringReader(sql), out var errors);
@@ -2403,7 +2402,11 @@ public void TestCommaPlacementLeadingInsertTargets()
generator.GenerateScript(fragment, out var generated);
string expected =
- "INSERT INTO t (a, b, c)" + Environment.NewLine +
+ "INSERT INTO t (" + Environment.NewLine +
+ " a" + Environment.NewLine +
+ " , b" + Environment.NewLine +
+ " , c" + Environment.NewLine +
+ ")" + Environment.NewLine +
"VALUES (1, 2, 3);" + Environment.NewLine + Environment.NewLine;
Assert.AreEqual(expected, generated);
@@ -2417,10 +2420,9 @@ public void TestCommaPlacementLeadingInsertTargets()
[SqlStudioTestCategory(Category.UnitTest)]
public void TestCommaPlacementTrailingInsertTargets()
{
- // Same output as the leading case: because the INSERT target list renders on a
- // single line (CommaPlacement only affects multi-line lists, and
- // MultilineInsertTargetsList is not consumed here), leading and trailing placement
- // produce identical text.
+ // With MultilineInsertTargetsList = true and CommaPlacement = Trailing (default) the
+ // INSERT column target list is emitted multi-line with each column on its own line and
+ // its comma trailing the column.
var sql = "INSERT INTO t (a, b, c) VALUES (1, 2, 3);";
var parser = new TSql170Parser(true);
var fragment = parser.Parse(new StringReader(sql), out var errors);
@@ -2434,7 +2436,11 @@ public void TestCommaPlacementTrailingInsertTargets()
generator.GenerateScript(fragment, out var generated);
string expected =
- "INSERT INTO t (a, b, c)" + Environment.NewLine +
+ "INSERT INTO t (" + Environment.NewLine +
+ " a," + Environment.NewLine +
+ " b," + Environment.NewLine +
+ " c" + Environment.NewLine +
+ ")" + Environment.NewLine +
"VALUES (1, 2, 3);" + Environment.NewLine + Environment.NewLine;
Assert.AreEqual(expected, generated);
@@ -2451,7 +2457,9 @@ public void TestCommaPlacementLeadingInsertSources()
// The INSERT source (VALUES) row list is a multi-line comma-separated list, so
// CommaPlacement = Leading places each continuation row's comma at the start of its
// line. (The rows are not aligned under the first row: this list is emitted via the
- // newline comma-list path, so continuation rows begin at column 0.)
+ // newline comma-list path, so continuation rows begin at column 0.) The target column
+ // list is emitted multi-line because MultilineInsertTargetsList is explicitly enabled
+ // (it is no longer the default).
var sql = "INSERT INTO t (a, b, c) VALUES (1, 2, 3), (4, 5, 6), (7, 8, 9);";
var parser = new TSql170Parser(true);
var fragment = parser.Parse(new StringReader(sql), out var errors);
@@ -2460,12 +2468,17 @@ public void TestCommaPlacementLeadingInsertSources()
var generator = new Sql170ScriptGenerator(new SqlScriptGeneratorOptions
{
CommaPlacement = CommaPlacement.Leading,
- MultilineInsertSourcesList = true
+ MultilineInsertSourcesList = true,
+ MultilineInsertTargetsList = true
});
generator.GenerateScript(fragment, out var generated);
string expected =
- "INSERT INTO t (a, b, c)" + Environment.NewLine +
+ "INSERT INTO t (" + Environment.NewLine +
+ " a" + Environment.NewLine +
+ " , b" + Environment.NewLine +
+ " , c" + Environment.NewLine +
+ ")" + Environment.NewLine +
"VALUES (1, 2, 3)" + Environment.NewLine +
", (4, 5, 6)" + Environment.NewLine +
", (7, 8, 9);" + Environment.NewLine + Environment.NewLine;
@@ -2482,7 +2495,9 @@ public void TestCommaPlacementLeadingInsertSources()
public void TestCommaPlacementTrailingInsertSources()
{
// The INSERT source (VALUES) row list with CommaPlacement = Trailing (default):
- // each row's comma follows it at the end of the line.
+ // each row's comma follows it at the end of the line. The target column list stays
+ // on a single line because MultilineInsertTargetsList is left at its default (false),
+ // exercising the common case of enabling only the source list.
var sql = "INSERT INTO t (a, b, c) VALUES (1, 2, 3), (4, 5, 6), (7, 8, 9);";
var parser = new TSql170Parser(true);
var fragment = parser.Parse(new StringReader(sql), out var errors);
@@ -2891,7 +2906,8 @@ public void TestCommaPlacementLeadingInsertSourcesMultilineFalse()
// The INSERT source (VALUES) row list is always emitted multi-line (the generator
// does not gate it on MultilineInsertSourcesList), so setting that option to false
// does NOT collapse it to one line: CommaPlacement = Leading still applies to the
- // row separators.
+ // row separators. The target column list stays on a single line because
+ // MultilineInsertTargetsList is left at its default (false).
var sql = "INSERT INTO t (a, b, c) VALUES (1, 2, 3), (4, 5, 6), (7, 8, 9);";
var parser = new TSql170Parser(true);
var fragment = parser.Parse(new StringReader(sql), out var errors);
diff --git a/Test/SqlDom/ScriptGenerator/SeparatingSemiColonTests.cs b/Test/SqlDom/ScriptGenerator/SeparatingSemiColonTests.cs
new file mode 100644
index 0000000..dba9f17
--- /dev/null
+++ b/Test/SqlDom/ScriptGenerator/SeparatingSemiColonTests.cs
@@ -0,0 +1,490 @@
+//------------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+//------------------------------------------------------------------------------
+
+using Microsoft.SqlServer.TransactSql.ScriptDom;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using SqlStudio.Tests.AssemblyTools.TestCategory;
+using static SqlStudio.Tests.UTSqlScriptDom.ScriptGeneratorTestHelper;
+
+namespace SqlStudio.Tests.UTSqlScriptDom
+{
+ // Tests for the separating-semicolon behavior of the script generator. SQL Server requires the
+ // statement that precedes a statement beginning with a WITH clause (a common table expression or
+ // XMLNAMESPACES) or a THROW statement to be terminated with a semicolon. The generator does not
+ // append a semicolon to block statements (IF / BEGIN...END / WHILE / TRY...CATCH), so when such a
+ // block is followed by a CTE or THROW the generator injects the required separating semicolon so
+ // the generated script is valid for SQL Server.
+ [TestClass]
+ public class SeparatingSemiColonTests
+ {
+ // -----------------------------------------------------------------------------------------
+ // CTE following a block
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCteAfterBeginEndBlockGetsSeparatingSemicolon()
+ {
+ const string input =
+@"DECLARE @x INT;
+IF @x IS NULL BEGIN SELECT 1; END
+;WITH cte AS (SELECT 1 AS c) SELECT * FROM cte";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+DECLARE @x AS INT;
+
+IF @x IS NULL
+ BEGIN
+ SELECT 1;
+ END;
+
+WITH cte
+AS (SELECT 1 AS c)
+SELECT *
+FROM cte;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCteAfterIfStatementWithoutBlockHasNoDoubleSemicolon()
+ {
+ // The IF's then-statement already ends with its own semicolon, so no additional
+ // separating semicolon is injected (no "SELECT 0;;").
+ const string input =
+@"DECLARE @x INT;
+IF @x IS NULL SELECT 0
+;WITH cte AS (SELECT 1 AS c) SELECT * FROM cte";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+DECLARE @x AS INT;
+
+IF @x IS NULL
+ SELECT 0;
+
+WITH cte
+AS (SELECT 1 AS c)
+SELECT *
+FROM cte;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCteAfterWhileBlockGetsSeparatingSemicolon()
+ {
+ const string input =
+@"DECLARE @x INT = 0;
+WHILE @x < 1 BEGIN SET @x = @x + 1; END
+;WITH cte AS (SELECT 1 AS c) SELECT * FROM cte";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+DECLARE @x AS INT = 0;
+
+WHILE @x < 1
+ BEGIN
+ SET @x = @x + 1;
+ END;
+
+WITH cte
+AS (SELECT 1 AS c)
+SELECT *
+FROM cte;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCteAfterTryCatchBlockGetsSeparatingSemicolon()
+ {
+ const string input =
+@"BEGIN TRY SELECT 1; END TRY BEGIN CATCH SELECT 2; END CATCH
+;WITH cte AS (SELECT 1 AS c) SELECT * FROM cte";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+BEGIN TRY
+ SELECT 1;
+END TRY
+BEGIN CATCH
+ SELECT 2;
+END CATCH;
+
+WITH cte
+AS (SELECT 1 AS c)
+SELECT *
+FROM cte;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestXmlNamespacesAfterBeginEndBlockGetsSeparatingSemicolon()
+ {
+ // A WITH XMLNAMESPACES statement (the other kind of statement that begins with WITH)
+ // requires the same preceding semicolon as a common table expression.
+ const string input =
+@"DECLARE @x INT;
+IF @x IS NULL BEGIN SELECT 1; END
+WITH XMLNAMESPACES ('uri' AS ns) SELECT 1 AS c";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+DECLARE @x AS INT;
+
+IF @x IS NULL
+ BEGIN
+ SELECT 1;
+ END;
+
+WITH XMLNAMESPACES ('uri' AS ns)
+SELECT 1 AS c;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCteInsertAfterBeginEndBlockGetsSeparatingSemicolon()
+ {
+ // A common table expression can feed any DML statement, not just SELECT. Here the CTE
+ // feeds an INSERT that follows a block, so the separating semicolon is still required.
+ const string input =
+@"DECLARE @x INT;
+IF @x IS NULL BEGIN SELECT 1; END
+WITH cte AS (SELECT 1 AS c) INSERT INTO t (c) SELECT c FROM cte";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+DECLARE @x AS INT;
+
+IF @x IS NULL
+ BEGIN
+ SELECT 1;
+ END;
+
+WITH cte
+AS (SELECT 1 AS c)
+INSERT INTO t (c)
+SELECT c
+FROM cte;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCteAfterIfElseBlockGetsSeparatingSemicolon()
+ {
+ // The IF statement ends with the ELSE branch's END (no terminator), so a CTE that
+ // follows the whole IF...ELSE still needs the separating semicolon.
+ const string input =
+@"DECLARE @x INT;
+IF @x = 1 BEGIN SELECT 1; END ELSE BEGIN SELECT 2; END
+WITH cte AS (SELECT 1 AS c) SELECT * FROM cte";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+DECLARE @x AS INT;
+
+IF @x = 1
+ BEGIN
+ SELECT 1;
+ END
+ELSE
+ BEGIN
+ SELECT 2;
+ END;
+
+WITH cte
+AS (SELECT 1 AS c)
+SELECT *
+FROM cte;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // THROW following a block
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestThrowAfterBeginEndBlockGetsSeparatingSemicolon()
+ {
+ const string input =
+@"DECLARE @x INT;
+IF @x IS NULL BEGIN SELECT 1; END
+;THROW 50001, 'e', 1";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+DECLARE @x AS INT;
+
+IF @x IS NULL
+ BEGIN
+ SELECT 1;
+ END;
+
+THROW 50001, 'e', 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestThrowAfterWhileBlockGetsSeparatingSemicolon()
+ {
+ const string input =
+@"DECLARE @x INT = 0;
+WHILE @x < 1 BEGIN SET @x = @x + 1; END
+THROW 50001, 'e', 1";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+DECLARE @x AS INT = 0;
+
+WHILE @x < 1
+ BEGIN
+ SET @x = @x + 1;
+ END;
+
+THROW 50001, 'e', 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // Cases that must NOT get an extra semicolon
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCteAfterTerminatedStatementHasNoDoubleSemicolon()
+ {
+ // The preceding SELECT already ends with a semicolon, so no separating semicolon is added.
+ const string input =
+@"SELECT 1
+;WITH cte AS (SELECT 1 AS c) SELECT * FROM cte";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+SELECT 1;
+
+WITH cte
+AS (SELECT 1 AS c)
+SELECT *
+FROM cte;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestThrowAfterTerminatedStatementHasNoDoubleSemicolon()
+ {
+ // The preceding SELECT already ends with a semicolon, so no separating semicolon is
+ // added before the THROW.
+ const string input =
+@"SELECT 1
+;THROW 50001, 'e', 1";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+SELECT 1;
+
+THROW 50001, 'e', 1;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCteAsFirstStatementHasNoLeadingSemicolon()
+ {
+ const string input = "WITH cte AS (SELECT 1 AS c) SELECT * FROM cte";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+WITH cte
+AS (SELECT 1 AS c)
+SELECT *
+FROM cte;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // Nested statement list (inside a procedure body)
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCteAfterBlockInsideProcedureBodyGetsSeparatingSemicolon()
+ {
+ // The input intentionally omits the semicolon before WITH. ScriptDom's parser is lenient
+ // and accepts this, and the block statement's AST does not carry a terminator, so the
+ // generator is responsible for injecting the semicolon SQL Server requires. (A leading
+ // semicolon in the input would be discarded during parsing, so its presence or absence in
+ // the input does not affect the generated output.)
+ const string input =
+@"CREATE PROCEDURE p @x INT AS BEGIN
+IF @x IS NULL BEGIN SELECT 1; END
+WITH cte AS (SELECT 1 AS c) SELECT * FROM cte
+END";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+CREATE PROCEDURE p
+@x INT
+AS
+BEGIN
+ IF @x IS NULL
+ BEGIN
+ SELECT 1;
+ END;
+ WITH cte
+ AS (SELECT 1 AS c)
+ SELECT *
+ FROM cte;
+END";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // Original reported repro: THROW inside a BEGIN...END block, followed by a CTE after the
+ // block. Without the injected separating semicolon, SQL Server rejects the generated script
+ // with error 319 ("Incorrect syntax near the keyword 'WITH'").
+ // -----------------------------------------------------------------------------------------
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestOriginalReproThrowInBlockFollowedByCteGetsSeparatingSemicolon()
+ {
+ const string input =
+@"declare @ClientID varchar(100) = ''
+IF @ClientID IS NULL
+BEGIN
+;THROW 50001, 'Client with PolicyID ''0001'' not found.', 1;
+END
+;WITH cte
+AS (SELECT 1 AS Column1)
+select * from cte";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+DECLARE @ClientID AS VARCHAR (100) = '';
+
+IF @ClientID IS NULL
+ BEGIN
+ THROW 50001, 'Client with PolicyID ''0001'' not found.', 1;
+ END;
+
+WITH cte
+AS (SELECT 1 AS Column1)
+SELECT *
+FROM cte;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCteAfterTerminatedStatementWithTrailingCommentHasNoDoubleSemicolon()
+ {
+ // With PreserveComments on, the previous statement's terminating semicolon is followed
+ // by a trailing single-line comment. The separator scan must look past the comment and
+ // detect the semicolon, so no redundant semicolon is written into the comment text.
+ const string input =
+@"SELECT 1; -- keep me
+WITH cte AS (SELECT 1 AS c) SELECT * FROM cte";
+ var options = new SqlScriptGeneratorOptions { PreserveComments = true };
+ const string expected =
+@"SELECT 1; -- keep me
+
+WITH cte
+AS (SELECT 1 AS c)
+SELECT *
+FROM cte;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCteAfterTerminatedStatementWithTrailingBlockCommentHasNoDoubleSemicolon()
+ {
+ // Same as the single-line case but with a trailing multi-line comment, exercising the
+ // MultilineComment branch of the separator scan.
+ const string input =
+@"SELECT 1; /* keep me */
+WITH cte AS (SELECT 1 AS c) SELECT * FROM cte";
+ var options = new SqlScriptGeneratorOptions { PreserveComments = true };
+ const string expected =
+@"SELECT 1; /* keep me */
+
+WITH cte
+AS (SELECT 1 AS c)
+SELECT *
+FROM cte;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCteAfterBlockWithTrailingCommentGetsSeparatingSemicolon()
+ {
+ // A block ends with END (no terminator) and carries a trailing comment. The separator
+ // scan must look past the comment, find END (not a semicolon), and inject the required
+ // semicolon so the following CTE is valid, without corrupting the comment.
+ const string input =
+@"DECLARE @x INT;
+IF @x IS NULL BEGIN SELECT 1; END -- trailing note
+WITH cte AS (SELECT 1 AS c) SELECT * FROM cte";
+ var options = new SqlScriptGeneratorOptions { PreserveComments = true };
+ const string expected =
+@"DECLARE @x AS INT;
+
+IF @x IS NULL
+ BEGIN
+ SELECT 1;
+ END; -- trailing note
+
+WITH cte
+AS (SELECT 1 AS c)
+SELECT *
+FROM cte;";
+
+ AssertGenerated(input, options, expected);
+ }
+ }
+}
diff --git a/release-notes/180/180.78.1.md b/release-notes/180/180.78.1.md
new file mode 100644
index 0000000..f30a2d4
--- /dev/null
+++ b/release-notes/180/180.78.1.md
@@ -0,0 +1,31 @@
+# Release Notes
+
+## Microsoft.SqlServer.TransactSql.ScriptDom 180.78.1
+This update brings the following changes over the previous release:
+
+### Target Platform Support
+
+* .NET Framework 4.7.2 (Windows x86, Windows x64)
+* .NET 8 (Windows x86, Windows x64, Linux, macOS)
+* .NET Standard 2.0+ (Windows x86, Windows x64, Linux, macOS)
+
+### Dependencies
+* None
+
+#### .NET Framework
+#### .NET Core
+
+### New Features
+* Adds MultilineInValuesList formatter option for IN (values) predicates.
+* Adds ClauseBodyAlignment script generation option (Aligned or Indented).
+* Adds MultilineProcedureParametersList formatter option for procedure and function parameters.
+* Adds MultilineInsertTargetsList formatter option for INSERT and MERGE targets.
+
+### Fixed
+* Injects a separating semicolon before CTE WITH and before THROW after block statements.
+
+### Changes
+* None
+
+### Known Issues
+* None