Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions SqlScriptDom/ScriptDom/SqlServer/ClauseBodyAlignment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//------------------------------------------------------------------------------
// <copyright file="ClauseBodyAlignment.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//------------------------------------------------------------------------------

namespace Microsoft.SqlServer.TransactSql.ScriptDom
{
/// <summary>
/// Represents how the body of a clause (the part after FROM, WHERE, GROUP BY, etc.) is laid out
/// relative to its keyword.
/// </summary>
public enum ClauseBodyAlignment
{
/// <summary>
/// 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).
/// </summary>
Aligned,

/// <summary>
/// Put the body on its own new line, indented one level (<see cref="IndentationMode"/> /
/// 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.
/// </summary>
Indented
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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
Expand Down Expand Up @@ -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<ProcedureParameter> 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
Expand Down Expand Up @@ -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;
}

/// <summary>
/// Generates a statement fragment with semicolon placed before any trailing comments.
/// This prevents semicolons from being appended after single-line comments (-- style),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading