Skip to content

Commit b1722bb

Browse files
committed
AutomaticTransactionAbortSqlCodeAnalysisRule
1 parent f64594c commit b1722bb

10 files changed

Lines changed: 184 additions & 6 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using Microsoft.SqlServer.TransactSql.ScriptDom;
2+
3+
namespace Dibix.Sdk.CodeAnalysis.Rules
4+
{
5+
[SqlCodeAnalysisRule(id: 43, SupportsScriptArtifacts = false)]
6+
public sealed class AutomaticTransactionAbortSqlCodeAnalysisRule : SqlCodeAnalysisRule
7+
{
8+
protected override string ErrorMessageTemplate => "SET XACT_ABORT ON when using multiple write statements";
9+
10+
public AutomaticTransactionAbortSqlCodeAnalysisRule(SqlCodeAnalysisContext context) : base(context) { }
11+
12+
public override void Visit(TSqlBatch node)
13+
{
14+
BatchVisitor visitor = new BatchVisitor();
15+
node.Accept(visitor);
16+
17+
if (!visitor.Valid)
18+
Fail(node);
19+
}
20+
21+
private sealed class BatchVisitor : TSqlFragmentVisitor
22+
{
23+
private int _modificationStatementCount;
24+
private bool _foundSetXactAbort;
25+
private bool _withinTransaction;
26+
27+
public bool Valid => _modificationStatementCount < 2 || _foundSetXactAbort;
28+
29+
public override void ExplicitVisit(PredicateSetStatement node)
30+
{
31+
if (node.Options == SetOptions.XactAbort && node.IsOn)
32+
_foundSetXactAbort = true;
33+
34+
base.ExplicitVisit(node);
35+
}
36+
37+
public override void ExplicitVisit(BeginTransactionStatement node)
38+
{
39+
_withinTransaction = true;
40+
base.ExplicitVisit(node);
41+
}
42+
43+
public override void ExplicitVisit(CommitTransactionStatement node)
44+
{
45+
_withinTransaction = false;
46+
base.ExplicitVisit(node);
47+
}
48+
49+
public override void ExplicitVisit(RollbackTransactionStatement node)
50+
{
51+
_withinTransaction = false;
52+
base.ExplicitVisit(node);
53+
}
54+
55+
public override void Visit(DataModificationSpecification node)
56+
{
57+
if (!_withinTransaction && node.Target is not VariableTableReference)
58+
_modificationStatementCount++;
59+
60+
base.Visit(node);
61+
}
62+
}
63+
}
64+
}

src/Dibix.Sdk.CodeAnalysis/SqlCodeAnalysisContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public sealed class SqlCodeAnalysisContext
1313

1414
public SqlModel Model { get; }
1515
public TSqlFragment Fragment { get; }
16+
public bool IsScriptArtifact { get; }
1617
public SqlCodeAnalysisConfiguration Configuration { get; }
1718

1819
public SqlCodeAnalysisContext
@@ -31,6 +32,7 @@ TSqlModel model
3132
_logger = logger;
3233
Model = new SqlModel(source, fragment, isScriptArtifact, configuration.IsEmbedded, configuration.LimitDdlStatements, model, logger);
3334
Fragment = fragment;
35+
IsScriptArtifact = isScriptArtifact;
3436
Configuration = configuration;
3537
}
3638

src/Dibix.Sdk.CodeAnalysis/SqlCodeAnalysisRuleAttribute.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ public sealed class SqlCodeAnalysisRuleAttribute : Attribute
77
{
88
public int Id { get; }
99
public bool IsEnabled { get; set; } = true;
10+
public bool SupportsScriptArtifacts { get; set; } = true;
1011

1112
public SqlCodeAnalysisRuleAttribute(int id)
1213
{

src/Dibix.Sdk.CodeAnalysis/SqlCodeAnalysisRuleEngine.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public static SqlCodeAnalysisRuleEngine Create(TSqlModel model, SqlCodeAnalysisC
4242
public IEnumerable<SqlCodeAnalysisError> AnalyzeScript(string source, string content)
4343
{
4444
TSqlFragment fragment = ScriptDomFacade.Parse(content);
45-
return Analyze(source, fragment, isScriptArtifact: true, SqlCodeAnalysisRuleMap.EnabledRules);
45+
return Analyze(source, fragment, isScriptArtifact: true, SqlCodeAnalysisRuleMap.EnabledScriptArtifactRules);
4646
}
4747
#endregion
4848

src/Dibix.Sdk.CodeAnalysis/SqlCodeAnalysisRuleMap.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ internal static class SqlCodeAnalysisRuleMap
1010
{
1111
private static readonly IDictionary<Type, RuleRegistration> RuleMap = ScanRules().ToDictionary(x => x.Type);
1212

13-
public static IReadOnlyCollection<Type> AllRules { get; } = RuleMap.Values.Where(x => x.IsEnabled).Select(x => x.Type).ToArray();
14-
public static IReadOnlyCollection<Type> EnabledRules { get; } = RuleMap.Values.Where(x => x.IsEnabled).Select(x => x.Type).ToArray();
13+
public static IEnumerable<Type> AllRules { get; } = RuleMap.Values.Where(x => x.IsEnabled).Select(x => x.Type);
14+
public static IEnumerable<Type> EnabledRules { get; } = RuleMap.Values.Where(x => x.IsEnabled).Select(x => x.Type);
15+
public static IEnumerable<Type> EnabledScriptArtifactRules { get; } = RuleMap.Values.Where(x => x is { IsEnabled: true, SupportsScriptArtifacts: true }).Select(x => x.Type);
1516

1617
public static int GetRuleId(Type type) => RuleMap[type].Id;
1718

@@ -36,7 +37,7 @@ private static IEnumerable<RuleRegistration> ScanRules()
3637
throw new InvalidOperationException($"The rule '{conflictingRule}' is already registered for id '{attribute.Id}'");
3738

3839
ruleMap.Add(attribute.Id, type);
39-
yield return new RuleRegistration(attribute.Id, type, attribute.IsEnabled, CompileRuleInvoker(type));
40+
yield return new RuleRegistration(attribute.Id, type, attribute.IsEnabled, attribute.SupportsScriptArtifacts, CompileRuleInvoker(type));
4041
}
4142
}
4243

@@ -58,13 +59,15 @@ private struct RuleRegistration
5859
public int Id { get; }
5960
public Type Type { get; }
6061
public bool IsEnabled { get; }
62+
public bool SupportsScriptArtifacts { get; }
6163
public Func<SqlCodeAnalysisContext, IEnumerable<SqlCodeAnalysisError>> Handler { get; }
6264

63-
public RuleRegistration(int id, Type type, bool isEnabled, Func<SqlCodeAnalysisContext, IEnumerable<SqlCodeAnalysisError>> handler)
65+
public RuleRegistration(int id, Type type, bool isEnabled, bool supportsScriptArtifacts, Func<SqlCodeAnalysisContext, IEnumerable<SqlCodeAnalysisError>> handler)
6466
{
6567
Id = id;
6668
Type = type;
6769
IsEnabled = isEnabled;
70+
SupportsScriptArtifacts = supportsScriptArtifacts;
6871
Handler = handler;
6972
}
7073
}

tests/Dibix.Generators.Tests/Resources/SqlCodeAnalysisRuleTests.g.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ public sealed partial class SqlCodeAnalysisRuleTests
2424
[global::Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod]
2525
public void AmbiguousCheckConstraintSqlCodeAnalysisRule() => this.Execute();
2626

27+
[global::System.CodeDom.Compiler.GeneratedCode("Dibix.Testing.Generators", "%GENERATORVERSION%")]
28+
[global::System.Diagnostics.DebuggerNonUserCode]
29+
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
30+
[global::Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod]
31+
public void AutomaticTransactionAbortSqlCodeAnalysisRule() => this.Execute();
32+
2733
[global::System.CodeDom.Compiler.GeneratedCode("Dibix.Testing.Generators", "%GENERATORVERSION%")]
2834
[global::System.Diagnostics.DebuggerNonUserCode]
2935
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
CREATE PROCEDURE [dbo].[dbx_codeanalysis_error_043_needed1]
2+
AS
3+
BEGIN
4+
INSERT INTO [dbo].[dbx_table]([id])
5+
VALUES (1)
6+
UPDATE [dbo].[dbx_anothertable] SET [name] = N'x' WHERE [id] = 1
7+
END
8+
GO
9+
CREATE PROCEDURE [dbo].[dbx_codeanalysis_error_043_needed2]
10+
AS
11+
BEGIN
12+
MERGE [dbo].[dbx_anothertable] AS [T]
13+
USING (VALUES (1, N'x')) AS [S]([id], [name])
14+
ON ([T].[id] = [S].[id])
15+
WHEN NOT MATCHED BY SOURCE THEN
16+
DELETE
17+
;
18+
19+
DELETE FROM [dbo].[dbx_table] WHERE [id] = 1
20+
END
21+
GO
22+
CREATE PROCEDURE [dbo].[dbx_codeanalysis_error_043_notneeded]
23+
AS
24+
BEGIN
25+
DELETE FROM [dbo].[dbx_table] WHERE [id] = 1
26+
27+
DECLARE @table TABLE ([id] INT NOT NULL, PRIMARY KEY([id]))
28+
INSERT INTO @table ([id])
29+
VALUES (1)
30+
END
31+
GO
32+
CREATE PROCEDURE [dbo].[dbx_codeanalysis_error_043_missingtransaction]
33+
AS
34+
BEGIN
35+
BEGIN TRANSACTION
36+
MERGE [dbo].[dbx_anothertable] AS [T]
37+
USING (VALUES (1, N'x')) AS [S]([id], [name])
38+
ON ([T].[id] = [S].[id])
39+
WHEN NOT MATCHED BY SOURCE THEN
40+
DELETE
41+
;
42+
COMMIT
43+
44+
INSERT INTO [dbo].[dbx_table]([id])
45+
VALUES (1)
46+
UPDATE [dbo].[dbx_anothertable] SET [name] = N'x' WHERE [id] = 1
47+
END
48+
GO
49+
CREATE PROCEDURE [dbo].[dbx_codeanalysis_error_043_valid]
50+
AS
51+
BEGIN
52+
SET XACT_ABORT ON
53+
54+
MERGE [dbo].[dbx_anothertable] AS [T]
55+
USING (VALUES (1, N'x')) AS [S]([id], [name])
56+
ON ([T].[id] = [S].[id])
57+
WHEN NOT MATCHED BY SOURCE THEN
58+
DELETE
59+
;
60+
61+
DELETE FROM [dbo].[dbx_table] WHERE [id] = 1
62+
END
63+
GO
64+
CREATE PROCEDURE [dbo].[dbx_codeanalysis_error_043_alsovalid]
65+
AS
66+
BEGIN
67+
BEGIN TRANSACTION
68+
INSERT INTO [dbo].[dbx_table]([id])
69+
VALUES (1)
70+
71+
BEGIN TRANSACTION
72+
MERGE [dbo].[dbx_anothertable] AS [T]
73+
USING (VALUES (1, N'x')) AS [S]([id], [name])
74+
ON ([T].[id] = [S].[id])
75+
WHEN NOT MATCHED BY SOURCE THEN
76+
DELETE
77+
;
78+
79+
DELETE FROM [dbo].[dbx_table] WHERE [id] = 1
80+
COMMIT
81+
COMMIT
82+
END

tests/Dibix.Sdk.Tests.Database/Dibix.Sdk.Tests.Database.sqlproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@
160160
<None Include="Scripts\PostDeploy\PostDeployC.sql" />
161161
<None Include="Scripts\PostDeploy\PostDeployD.sql" />
162162
<Build Include="Types\dbx_codeanalysis_udt_inttwo.sql" />
163+
<Build Include="CodeAnalysis\dbx_codeanalysis_error_043.sql" />
163164
</ItemGroup>
164165
<ItemGroup>
165166
<Folder Include="CodeAnalysis" />
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
CREATE TABLE [dbo].[dbx_anothertable]
22
(
3-
[id] INT NOT NULL
3+
[id] INT NOT NULL
4+
, [name] NVARCHAR(100) NULL
45
, CONSTRAINT [PK_dbx_anothertable] PRIMARY KEY ([id])
56
)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<errors>
3+
<error
4+
ruleid="43"
5+
message="SET XACT_ABORT ON when using multiple write statements"
6+
line="1"
7+
column="1" />
8+
<error
9+
ruleid="43"
10+
message="SET XACT_ABORT ON when using multiple write statements"
11+
line="9"
12+
column="1" />
13+
<error
14+
ruleid="43"
15+
message="SET XACT_ABORT ON when using multiple write statements"
16+
line="32"
17+
column="1" />
18+
</errors>

0 commit comments

Comments
 (0)