Skip to content

Commit 1fb1d80

Browse files
committed
chore: update documentation and tests for improved compatibility handling
1 parent 492c4f6 commit 1fb1d80

4 files changed

Lines changed: 92 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
3737
- Treat legacy CLR table-valued function return-column collation clauses as compatible during comparison when SQL Server ignores them in the effective return shape.
3838
- Treat legacy explicit `NULL` tokens on CLR table-valued function return columns as compatible during comparison and preserve them during compatibility reconciliation when the rest of the definition matches.
3939
- Treat equivalent legacy `Assembly` scripts as compatible during comparison when they differ only by banner comments, wrapped or case-varied hex payload formatting, `PERMISSION_SET` spacing, or quoted versus bracketed `ADD FILE` names.
40+
- Rewrite programmable-object declaration lines to the current metadata name when SQL Server stores stale module text after an object rename.
41+
- Fix table-trigger scripting after the declaration rewrite change by resolving trigger schema without referencing a non-existent `sys.triggers.schema_id` column.
4042
- Trailing semicolon differences on `INSERT` statement lines in data scripts are now suppressed during comparison normalization; scripts emitted with and without statement terminators compare as compatible (#47).
4143
- Legacy `TableData` scripts now compare as compatible when they differ from canonical output only by `SET IDENTITY_INSERT` semicolons or top-level `N'...'` string literal prefixes, including inside multi-line `INSERT ... VALUES (...)` statements.
4244
- Empty separator lines are now ignored during `status` and `diff`, and whitespace-only separator lines compare as compatible after normalization.

specs/04-scripting.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ The following types are defined in this specification family and not fully imple
110110
- `SET QUOTED_IDENTIFIER <ON|OFF>` + `GO`
111111
- `SET ANSI_NULLS <ON|OFF>` + `GO`
112112
- Programmable-object body MUST be followed by `GO`.
113+
- Programmable-object declaration lines MUST reflect the current metadata schema/name even when the stored module text is stale after an object rename.
113114
- Object-level permissions and extended properties MUST be emitted after object DDL body.
114115
- Canonical programmable-object whitespace MUST be:
115116
- no blank line between the final header `GO` and the first definition line,

src/SqlChangeTracker/Sql/SqlServerScripter.cs

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ internal class SqlServerScripter
1919
@"^\s*\[[^\]]+\]\s+AS\s+",
2020
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
2121
private static readonly Regex ModuleDeclarationLineRegex = new(
22-
@"^(?<prefix>CREATE\s+(?:PROC(?:EDURE)?|FUNCTION|VIEW|TRIGGER)\s+)(?<name>(?:\[[^\]]+(?:\]\])*\]|[A-Za-z_][\w@#$]*)(?:\.(?:\[[^\]]+(?:\]\])*\]|[A-Za-z_][\w@#$]*))*)(?<suffix>.*)$",
22+
@"^(?<indent>\s*)(?<prefix>CREATE(?:\s+OR\s+ALTER)?\s+(?:PROC(?:EDURE)?|FUNCTION|VIEW|TRIGGER)\s+)(?<name>(?:\[[^\]]+(?:\]\])*\]|[A-Za-z_][\w@#$]*)(?:\.(?:\[[^\]]+(?:\]\])*\]|[A-Za-z_][\w@#$]*))*)(?<suffix>.*)$",
2323
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
2424
private static readonly Regex ClrTableValuedFunctionReturnColumnNullRegex = new(
2525
@"^(?<prefix>\s*(?:\[[^\]]+\]|[A-Za-z_][\w@#$]*)\s+(?:(?:\[[^\]]+\]|[A-Za-z_][\w@#$]*)(?:\.(?:\[[^\]]+\]|[A-Za-z_][\w@#$]*))?)(?:\s*\([^)]*\))?)\s+NULL(?<suffix>\s*,?\s*)$",
@@ -631,6 +631,8 @@ FROM sys.objects o
631631
definitionText = BuildClrTableValuedFunctionDefinition(connection, objectId, obj.Schema, obj.Name);
632632
}
633633

634+
definitionText = RewriteModuleDeclarationLine(definitionText, obj.Schema, obj.Name);
635+
634636
var (lines, hasGoAfterDefinition) = BuildProgrammableObjectLines(
635637
definitionText,
636638
ansiNulls,
@@ -1039,6 +1041,8 @@ FROM sys.objects o
10391041
var quotedIdentifier = reader.IsDBNull(2) || reader.GetBoolean(2);
10401042
reader.Close();
10411043

1044+
definitionText = RewriteModuleDeclarationLine(definitionText, obj.Schema, obj.Name);
1045+
10421046
var (lines, hasGoAfterDefinition) = BuildProgrammableObjectLines(
10431047
definitionText,
10441048
ansiNulls,
@@ -3885,7 +3889,7 @@ private static IEnumerable<string> ReadTableTriggers(
38853889
{
38863890
using var command = connection.CreateCommand();
38873891
command.CommandText = @"
3888-
SELECT t.name, m.definition, m.uses_ansi_nulls, m.uses_quoted_identifier
3892+
SELECT OBJECT_SCHEMA_NAME(t.object_id), t.name, m.definition, m.uses_ansi_nulls, m.uses_quoted_identifier
38893893
FROM sys.triggers t
38903894
JOIN sys.sql_modules m ON m.object_id = t.object_id
38913895
WHERE t.parent_class_desc = 'OBJECT_OR_COLUMN'
@@ -3898,10 +3902,12 @@ FROM sys.triggers t
38983902
using var reader = command.ExecuteReader();
38993903
while (reader.Read())
39003904
{
3901-
var triggerName = reader.GetString(0);
3902-
var definitionText = reader.IsDBNull(1) ? string.Empty : reader.GetString(1);
3903-
var ansiNulls = reader.IsDBNull(2) || reader.GetBoolean(2);
3904-
var quotedIdentifier = reader.IsDBNull(3) || reader.GetBoolean(3);
3905+
var triggerSchema = reader.IsDBNull(0) ? string.Empty : reader.GetString(0);
3906+
var triggerName = reader.GetString(1);
3907+
var definitionText = reader.IsDBNull(2) ? string.Empty : reader.GetString(2);
3908+
var ansiNulls = reader.IsDBNull(3) || reader.GetBoolean(3);
3909+
var quotedIdentifier = reader.IsDBNull(4) || reader.GetBoolean(4);
3910+
definitionText = RewriteModuleDeclarationLine(definitionText, triggerSchema, triggerName);
39053911
var triggerReferenceLines = TryGetReferenceTriggerBlock(referenceLines, triggerName);
39063912
var (triggerLines, _) = BuildProgrammableObjectLines(
39073913
definitionText,
@@ -6452,6 +6458,33 @@ internal static string ApplyDefinitionFormatting(string definition, string[]? re
64526458
return string.Join(Environment.NewLine, lines);
64536459
}
64546460

6461+
internal static string RewriteModuleDeclarationLine(string definition, string schema, string name)
6462+
{
6463+
if (string.IsNullOrWhiteSpace(definition))
6464+
{
6465+
return definition;
6466+
}
6467+
6468+
var lines = definition.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None);
6469+
for (var i = 0; i < lines.Length; i++)
6470+
{
6471+
var match = ModuleDeclarationLineRegex.Match(lines[i]);
6472+
if (!match.Success)
6473+
{
6474+
continue;
6475+
}
6476+
6477+
lines[i] =
6478+
match.Groups["indent"].Value +
6479+
match.Groups["prefix"].Value +
6480+
$"{QuoteIdentifier(schema)}.{QuoteIdentifier(name)}" +
6481+
match.Groups["suffix"].Value;
6482+
break;
6483+
}
6484+
6485+
return string.Join(Environment.NewLine, lines);
6486+
}
6487+
64556488
private static string[]? GetReferenceDefinitionBlock(string[]? referenceLines)
64566489
{
64576490
var range = TryGetReferenceDefinitionRange(referenceLines);

tests/SqlChangeTracker.Tests/Sql/SqlServerScripterCompatibilityTests.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,56 @@ public void ApplyDefinitionFormatting_PreservesReferenceCreateLineIdentifierQuot
113113
Assert.Equal("CREATE PROCEDURE [Reporting].[Sample_Proc]", createLine);
114114
}
115115

116+
[Fact]
117+
public void RewriteModuleDeclarationLine_ReplacesStaleStoredProcedureNameWithCurrentObjectName()
118+
{
119+
var definition = string.Join(Environment.NewLine, new[]
120+
{
121+
"CREATE PROCEDURE [Accounting].[LegacyProcedure] @BatchId int",
122+
"AS",
123+
"SELECT @BatchId"
124+
});
125+
126+
var rewritten = SqlServerScripter.RewriteModuleDeclarationLine(
127+
definition,
128+
"Accounting",
129+
"CurrentProcedure__1_12_3_0");
130+
131+
Assert.Equal(
132+
string.Join(Environment.NewLine, new[]
133+
{
134+
"CREATE PROCEDURE [Accounting].[CurrentProcedure__1_12_3_0] @BatchId int",
135+
"AS",
136+
"SELECT @BatchId"
137+
}),
138+
rewritten);
139+
}
140+
141+
[Fact]
142+
public void RewriteModuleDeclarationLine_PreservesCreateOrAlterPrefix_WhenReplacingCurrentName()
143+
{
144+
var definition = string.Join(Environment.NewLine, new[]
145+
{
146+
"CREATE OR ALTER VIEW [Reporting].[LegacyView]",
147+
"AS",
148+
"SELECT 1"
149+
});
150+
151+
var rewritten = SqlServerScripter.RewriteModuleDeclarationLine(
152+
definition,
153+
"Reporting",
154+
"CurrentView");
155+
156+
Assert.Equal(
157+
string.Join(Environment.NewLine, new[]
158+
{
159+
"CREATE OR ALTER VIEW [Reporting].[CurrentView]",
160+
"AS",
161+
"SELECT 1"
162+
}),
163+
rewritten);
164+
}
165+
116166
[Fact]
117167
public void ApplyDefinitionFormatting_PreservesCompatibleClrFunctionReferenceDefinition()
118168
{

0 commit comments

Comments
 (0)