Skip to content

Commit d3ee00d

Browse files
Add the support to compare the default constraints (fixes #7).
1 parent df8cd3f commit d3ee00d

17 files changed

Lines changed: 394 additions & 34 deletions

src/Testing.Databases.SqlServer/Comparer/ISqlObjectDifferencesVisitor.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ internal interface ISqlObjectDifferencesVisitor
1111
void Visit<TSqlObject>(SqlObjectDifferences<TSqlObject> differences)
1212
where TSqlObject : SqlObject;
1313

14+
void Visit(SqlColumnDifferences differences);
15+
1416
void Visit(SqlForeignKeyDifferences differences);
1517

1618
void Visit(SqlIndexDifferences differences);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//-----------------------------------------------------------------------
2+
// <copyright file="SqlColumnDifferences.cs" company="P.O.S Informatique">
3+
// Copyright (c) P.O.S Informatique. All rights reserved.
4+
// </copyright>
5+
//-----------------------------------------------------------------------
6+
7+
namespace PosInformatique.Testing.Databases
8+
{
9+
/// <summary>
10+
/// Represents the differences of a <see cref="SqlColumn"/> between two databases.
11+
/// </summary>
12+
public class SqlColumnDifferences : SqlObjectDifferences<SqlColumn>
13+
{
14+
internal SqlColumnDifferences(
15+
SqlColumn? source,
16+
SqlColumn? target,
17+
SqlObjectDifferenceType type,
18+
IReadOnlyList<SqlObjectPropertyDifference>? properties,
19+
SqlObjectDifferences<SqlDefaultConstraint>? defaultConstraint)
20+
: base(source, target, type, properties)
21+
{
22+
this.DefaultConstraint = defaultConstraint;
23+
}
24+
25+
internal SqlColumnDifferences(
26+
SqlObjectDifferences<SqlColumn> differences)
27+
: this(differences.Source, differences.Target, differences.Type, differences.Properties, null)
28+
{
29+
}
30+
31+
/// <summary>
32+
/// Gets the difference of the columns in the foreign key compared.
33+
/// </summary>
34+
public SqlObjectDifferences<SqlDefaultConstraint>? DefaultConstraint { get; }
35+
36+
internal override void Accept(ISqlObjectDifferencesVisitor visitor)
37+
{
38+
visitor.Visit(this);
39+
}
40+
}
41+
}

src/Testing.Databases.SqlServer/Comparer/SqlDatabaseComparisonResultsTextGenerator.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ public void Visit<TSqlObject>(SqlObjectDifferences<TSqlObject> differences)
6464
}
6565
}
6666

67+
public void Visit(SqlColumnDifferences differences)
68+
{
69+
this.Visit<SqlColumn>(differences);
70+
71+
this.Generate(differences.DefaultConstraint, "Default constraint");
72+
}
73+
6774
public void Visit(SqlForeignKeyDifferences differences)
6875
{
6976
this.WriteProperties(differences.Properties);
@@ -115,6 +122,17 @@ public void Visit(SqlUniqueConstraintDifferences differences)
115122
this.Generate(differences.Columns, "Columns");
116123
}
117124

125+
private void Generate<TSqlObject>(SqlObjectDifferences<TSqlObject>? difference, string typeName)
126+
where TSqlObject : SqlObject
127+
{
128+
if (difference is null)
129+
{
130+
return;
131+
}
132+
133+
this.Generate([difference], typeName);
134+
}
135+
118136
private void Generate<TSqlObject>(IEnumerable<SqlObjectDifferences<TSqlObject>> differences, string typeName)
119137
where TSqlObject : SqlObject
120138
{

src/Testing.Databases.SqlServer/Comparer/SqlObjectComparer.cs

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,11 @@ public static IList<SqlObjectDifferences<TSqlObject>> Compare<TSqlObject>(IReadO
2626
var keyValue = keySelector(targetObject);
2727
var sourceObject = Find(source, keySelector, keyValue);
2828

29-
if (sourceObject is null)
30-
{
31-
// Missing in the source.
32-
differences.Add(new SqlObjectDifferences<TSqlObject>(null, targetObject, SqlObjectDifferenceType.MissingInSource, null));
33-
}
34-
else
35-
{
36-
// Compare the object using visitor pattern.
37-
var difference = Compare(sourceObject, targetObject);
29+
var difference = Compare(sourceObject, targetObject);
3830

39-
if (difference is not null)
40-
{
41-
differences.Add(difference);
42-
}
31+
if (difference is not null)
32+
{
33+
differences.Add(difference);
4334
}
4435
}
4536

@@ -73,8 +64,10 @@ public static IList<SqlTableDifferences> Compare(IReadOnlyList<SqlTable> source,
7364

7465
public SqlObjectDifferences? Visit(SqlColumn column)
7566
{
76-
return this.CreateDifferences(
77-
column,
67+
var sourceColumn = (SqlColumn)this.source;
68+
69+
// Compare the properties
70+
var differenceProperties = GetPropertyDifferences(
7871
this.CompareProperty(column, t => t.Position, nameof(column.Position)),
7972
this.CompareProperty(column, t => t.MaxLength, nameof(column.MaxLength)),
8073
this.CompareProperty(column, t => t.Precision, nameof(column.Precision)),
@@ -84,6 +77,24 @@ public static IList<SqlTableDifferences> Compare(IReadOnlyList<SqlTable> source,
8477
this.CompareProperty(column, t => t.CollationName, nameof(column.CollationName)),
8578
this.CompareProperty(column, t => t.IsComputed, nameof(column.IsComputed)),
8679
this.CompareProperty(column, t => TsqlCodeHelper.RemoveNotUsefulCharacters(t.ComputedExpression), nameof(column.ComputedExpression), t => t.ComputedExpression));
80+
81+
// Compare the default constraint
82+
var defaultConstraintDifference = Compare(sourceColumn.DefaultConstraint, column.DefaultConstraint);
83+
84+
if (differenceProperties.Count > 0 || defaultConstraintDifference != null)
85+
{
86+
return new SqlColumnDifferences((SqlColumn)this.source, column, SqlObjectDifferenceType.Different, differenceProperties, defaultConstraintDifference);
87+
}
88+
89+
return null;
90+
}
91+
92+
public SqlObjectDifferences? Visit(SqlDefaultConstraint defaultConstraint)
93+
{
94+
return this.CreateDifferences(
95+
defaultConstraint,
96+
this.CompareProperty(defaultConstraint, df => df.Name, nameof(defaultConstraint.Name)),
97+
this.CompareProperty(defaultConstraint, df => TsqlCodeHelper.RemoveNotUsefulCharacters(df.Expression), nameof(defaultConstraint.Expression), df => df.Expression));
8798
}
8899

89100
public SqlObjectDifferences? Visit(SqlForeignKey foreignKey)
@@ -191,7 +202,7 @@ public static IList<SqlTableDifferences> Compare(IReadOnlyList<SqlTable> source,
191202
var checkConstraintDifferences = Compare(sourceTable.CheckConstraints, table.CheckConstraints, tr => tr.Name);
192203

193204
// Compare the columns
194-
var columnsDifferences = Compare(sourceTable.Columns, table.Columns, c => c.Name);
205+
var columnsDifferences = Compare(sourceTable.Columns, table.Columns, c => c.Name, diff => new SqlColumnDifferences(diff));
195206

196207
// Compare the foreign keys
197208
var foreignKeysDifferences = Compare(sourceTable.ForeignKeys, table.ForeignKeys, fk => fk.Name, diff => new SqlForeignKeyDifferences(diff));
@@ -259,9 +270,26 @@ public static IList<SqlTableDifferences> Compare(IReadOnlyList<SqlTable> source,
259270
this.CompareProperty(view, v => TsqlCodeHelper.RemoveNotUsefulCharacters(v.Code), nameof(view.Code), v => v.Code));
260271
}
261272

262-
private static SqlObjectDifferences<TSqlObject>? Compare<TSqlObject>(TSqlObject source, TSqlObject target)
273+
private static SqlObjectDifferences<TSqlObject>? Compare<TSqlObject>(TSqlObject? source, TSqlObject? target)
263274
where TSqlObject : SqlObject
264275
{
276+
if (source is null)
277+
{
278+
if (target is null)
279+
{
280+
return null;
281+
}
282+
283+
return new SqlObjectDifferences<TSqlObject>(null, target, SqlObjectDifferenceType.MissingInSource, null);
284+
}
285+
else
286+
{
287+
if (target is null)
288+
{
289+
return new SqlObjectDifferences<TSqlObject>(source, null, SqlObjectDifferenceType.MissingInTarget, null);
290+
}
291+
}
292+
265293
var visitor = new SqlObjectComparer(source);
266294

267295
return (SqlObjectDifferences<TSqlObject>?)target.Accept(visitor);

src/Testing.Databases.SqlServer/Comparer/SqlTableDifferences.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ internal SqlTableDifferences(
1919
SqlObjectDifferenceType type,
2020
IReadOnlyList<SqlObjectPropertyDifference>? properties,
2121
IList<SqlPrimaryKeyDifferences> primaryKeys,
22-
IList<SqlObjectDifferences<SqlColumn>> columns,
22+
IList<SqlColumnDifferences> columns,
2323
IList<SqlObjectDifferences<SqlTrigger>> triggers,
2424
IList<SqlObjectDifferences<SqlCheckConstraint>> checkConstraints,
2525
IList<SqlIndexDifferences> indexes,
@@ -28,7 +28,7 @@ internal SqlTableDifferences(
2828
: base(source, target, type, properties)
2929
{
3030
this.PrimaryKeys = new ReadOnlyCollection<SqlPrimaryKeyDifferences>(primaryKeys);
31-
this.Columns = new ReadOnlyCollection<SqlObjectDifferences<SqlColumn>>(columns);
31+
this.Columns = new ReadOnlyCollection<SqlColumnDifferences>(columns);
3232
this.Triggers = new ReadOnlyCollection<SqlObjectDifferences<SqlTrigger>>(triggers);
3333
this.CheckConstraints = new ReadOnlyCollection<SqlObjectDifferences<SqlCheckConstraint>>(checkConstraints);
3434
this.Indexes = new ReadOnlyCollection<SqlIndexDifferences>(indexes);
@@ -50,7 +50,7 @@ internal SqlTableDifferences(
5050
/// <summary>
5151
/// Gets the columns differences between the two SQL tables.
5252
/// </summary>
53-
public ReadOnlyCollection<SqlObjectDifferences<SqlColumn>> Columns { get; }
53+
public ReadOnlyCollection<SqlColumnDifferences> Columns { get; }
5454

5555
/// <summary>
5656
/// Gets the indexes differences between the two SQL tables.

src/Testing.Databases.SqlServer/ObjectModel/ISqlObjectVisitor.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ public interface ISqlObjectVisitor<TResult>
2626
/// <returns>The result of the visit.</returns>
2727
TResult Visit(SqlColumn column);
2828

29+
/// <summary>
30+
/// Visits the specified <paramref name="defaultConstraint"/>.
31+
/// </summary>
32+
/// <param name="defaultConstraint"><see cref="SqlDefaultConstraint"/> to visit.</param>
33+
/// <returns>The result of the visit.</returns>
34+
TResult Visit(SqlDefaultConstraint defaultConstraint);
35+
2936
/// <summary>
3037
/// Visits the specified <paramref name="foreignKey"/>.
3138
/// </summary>

src/Testing.Databases.SqlServer/ObjectModel/SqlColumn.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ internal SqlColumn(
8282
/// </summary>
8383
public string? ComputedExpression { get; internal set; }
8484

85+
/// <summary>
86+
/// Gets the default constraint of the column.
87+
/// </summary>
88+
public SqlDefaultConstraint? DefaultConstraint { get; internal set; }
89+
8590
/// <inheritdoc />
8691
public override TResult Accept<TResult>(ISqlObjectVisitor<TResult> visitor) => visitor.Visit(this);
8792

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//-----------------------------------------------------------------------
2+
// <copyright file="SqlDefaultConstraint.cs" company="P.O.S Informatique">
3+
// Copyright (c) P.O.S Informatique. All rights reserved.
4+
// </copyright>
5+
//-----------------------------------------------------------------------
6+
7+
namespace PosInformatique.Testing.Databases
8+
{
9+
/// <summary>
10+
/// Represents a default constraint of a <see cref="SqlColumn" />.
11+
/// </summary>
12+
public class SqlDefaultConstraint : SqlObject
13+
{
14+
internal SqlDefaultConstraint(string name, string expression)
15+
{
16+
this.Name = name;
17+
this.Expression = expression;
18+
}
19+
20+
/// <summary>
21+
/// Gets the name of the default constraint.
22+
/// </summary>
23+
public string Name { get; }
24+
25+
/// <summary>
26+
/// Gets the expression of the default constraint.
27+
/// </summary>
28+
public string Expression { get; }
29+
30+
/// <inheritdoc />
31+
public override TResult Accept<TResult>(ISqlObjectVisitor<TResult> visitor) => visitor.Visit(this);
32+
33+
/// <inheritdoc cref="Name"/>
34+
public override string ToString()
35+
{
36+
return this.Name;
37+
}
38+
}
39+
}

src/Testing.Databases.SqlServer/SqlServerDatabaseObjectExtensions.cs

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ ORDER BY
7575
// Gets the check constraints
7676
var allCheckConstraints = GetCheckConstraintsAsync(database, cancellationToken);
7777

78+
// Gets the default constraints
79+
var allDefaultConstraints = GetDefaultConstraintsAsync(database, cancellationToken);
80+
7881
// Gets the indexes
7982
var allForeignKeys = GetForeignKeysAsync(database, cancellationToken);
8083

@@ -90,7 +93,7 @@ ORDER BY
9093
// Gets the unique constraints
9194
var allUniqueConstraints = GetUniqueConstraintsAsync(database, cancellationToken);
9295

93-
await Task.WhenAll(allColumns, allCheckConstraints, allForeignKeys, allIndexes, allPrimaryKeys, allTriggers, allUniqueConstraints);
96+
await Task.WhenAll(allColumns, allCheckConstraints, allDefaultConstraints, allForeignKeys, allIndexes, allPrimaryKeys, allTriggers, allUniqueConstraints);
9497

9598
// Builds the SqlTable object
9699
foreach (var table in result.Rows.Cast<DataRow>())
@@ -108,12 +111,17 @@ ORDER BY
108111
var columnsTable = allColumns.Result[(int)table["Id"]];
109112
var columns = new List<SqlColumn>();
110113

114+
var defaultConstraintsTable = allDefaultConstraints.Result[(int)table["Id"]];
115+
111116
foreach (var column in columnsTable.OrderBy(r => r["Position"]))
112117
{
113-
columns.Add(ToColumn(column));
118+
var position = Convert.ToInt32(column["Position"], CultureInfo.InvariantCulture);
119+
var defaultConstraint = defaultConstraintsTable.SingleOrDefault(r => (int)r["ColumnId"] == position);
120+
121+
columns.Add(ToColumn(column, defaultConstraint));
114122
}
115123

116-
// Indexes
124+
// Foreign keys
117125
var foreignKeysTable = allForeignKeys.Result[(int)table["Id"]];
118126
var foreignKeys = new List<SqlForeignKey>();
119127

@@ -308,6 +316,28 @@ [sys].[types] AS [ty]
308316
return result.Rows.Cast<DataRow>().ToLookup(c => (int)c["TableId"]);
309317
}
310318

319+
private static async Task<ILookup<int, DataRow>> GetDefaultConstraintsAsync(SqlServerDatabase database, CancellationToken cancellationToken)
320+
{
321+
const string sql = @"
322+
SELECT
323+
[t].[object_id] AS [TableId],
324+
[df].[parent_column_id] AS [ColumnId],
325+
[df].[name] AS [Name],
326+
[df].[definition] AS [Expression]
327+
FROM
328+
[sys].[default_constraints] AS [df],
329+
[sys].[tables] AS [t]
330+
WHERE
331+
[df].[parent_object_id] = [t].[object_id]
332+
ORDER BY
333+
[t].[name],
334+
[df].[name]";
335+
336+
var result = await database.ExecuteQueryAsync(sql, cancellationToken);
337+
338+
return result.Rows.Cast<DataRow>().ToLookup(row => (int)row["TableId"]);
339+
}
340+
311341
private static async Task<ILookup<int, DataRow>> GetForeignKeysAsync(SqlServerDatabase database, CancellationToken cancellationToken)
312342
{
313343
const string sql = @"
@@ -468,7 +498,7 @@ private static SqlCheckConstraint ToCheckConstraint(DataRow row)
468498
return new SqlCheckConstraint((string)row["Name"], (string)row["Code"]);
469499
}
470500

471-
private static SqlColumn ToColumn(DataRow row)
501+
private static SqlColumn ToColumn(DataRow row, DataRow? defaultConstraintRow)
472502
{
473503
return new SqlColumn(
474504
(string)row["Name"],
@@ -480,12 +510,20 @@ private static SqlColumn ToColumn(DataRow row)
480510
{
481511
CollationName = NullIfDbNull<string>(row["CollationName"]),
482512
ComputedExpression = NullIfDbNull<string>(row["ComputedExpression"]),
513+
DefaultConstraint = defaultConstraintRow != null ? ToDefaultConstraint(defaultConstraintRow) : null,
483514
IsComputed = (bool)row["IsComputed"],
484515
IsIdentity = (bool)row["IsIdentity"],
485516
IsNullable = (bool)row["IsNullable"],
486517
};
487518
}
488519

520+
private static SqlDefaultConstraint ToDefaultConstraint(DataRow row)
521+
{
522+
return new SqlDefaultConstraint(
523+
(string)row["Name"],
524+
(string)row["Expression"]);
525+
}
526+
489527
private static SqlForeignKey ToForeignKey(DataRow row, IList<SqlForeignKeyColumn> columns)
490528
{
491529
return new SqlForeignKey((string)row["ForeignKeyName"], (string)row["ReferencedTableName"], (string)row["UpdateAction"], (string)row["DeleteAction"], columns);

tests/Testing.Databases.SqlServer.Tests.Source/Tables/TableDifference.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@
1010
[Computed] AS [Scale] + [Precision],
1111
[SourceColumn] INT NOT NULL,
1212
[IdenticalColumn] INT NOT NULL,
13+
[ColumnWithDefaultConstraint] VARCHAR(20) NOT NULL CONSTRAINT DF_TableDifference_ColumnWithDefaultConstraint DEFAULT 'Source expression',
14+
[ColumnWithMissingDefaultConstraint] VARCHAR(20) NOT NULL CONSTRAINT DF_TableDifference_ColumnWithMissingDefaultConstraint DEFAULT 'Default value',
15+
[ColumnWithOtherDefaultConstraintName] VARCHAR(20) NOT NULL CONSTRAINT DF_TableDifference_ColumnWithOtherDefaultConstraintName DEFAULT 'Same expression',
1316
)

0 commit comments

Comments
 (0)