diff --git a/.gitattributes b/.gitattributes
index 738dfdd..4da7422 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -3,6 +3,7 @@
*.snk binary
*.verified.* text eol=lf working-tree-encoding=UTF-8
+*.verified.png binary
.editorconfig text eol=lf working-tree-encoding=UTF-8
*.sln.DotSettings text eol=lf working-tree-encoding=UTF-8
diff --git a/readme.md b/readme.md
index b31c12d..be9c28b 100644
--- a/readme.md
+++ b/readme.md
@@ -69,7 +69,7 @@ await Verify(connection)
// include only tables and views
.SchemaIncludes(DbObjects.Tables | DbObjects.Views);
```
-snippet source | anchor
+snippet source | anchor
Available values:
@@ -107,10 +107,95 @@ await Verify(connection)
_ => _ is TableViewBase ||
_.Name == "MyTrigger");
```
-snippet source | anchor
+snippet source | anchor
+#### Diagram
+
+The tables of a schema can instead be verified as a [Mermaid ER diagram](https://mermaid.js.org/syntax/entityRelationshipDiagram.html), using [DbToMermaid](https://github.com/SimonCropp/DbToMermaid). Requires net10.0 or higher.
+
+
+
+```cs
+await Verify(connection)
+ .SchemaAsDiagram();
+```
+snippet source | anchor
+
+
+Result: [Tests.SchemaAsDiagram.verified.md](/src/Tests/Tests.SchemaAsDiagram.verified.md)
+
+The diagram can be verified as markdown, svg, png, or any combination. One file is verified for each format.
+
+
+
+```cs
+await Verify(connection)
+ .SchemaAsDiagram(
+ DiagramFormat.Markdown |
+ DiagramFormat.Svg |
+ DiagramFormat.Png);
+```
+snippet source | anchor
+
+
+Result:
+
+ * [Tests.SchemaAsDiagramFormats.verified.md](/src/Tests/Tests.SchemaAsDiagramFormats.verified.md)
+ * [Tests.SchemaAsDiagramFormats.verified.svg](/src/Tests/Tests.SchemaAsDiagramFormats.verified.svg)
+ * [Tests.SchemaAsDiagramFormats.verified.png](/src/Tests/Tests.SchemaAsDiagramFormats.verified.png)
+
+
+
+Available values:
+
+
+
+```cs
+namespace VerifyTests.SqlServer;
+
+///
+/// The files produced when a schema is verified as a diagram. Can be combined.
+///
+[Flags]
+public enum DiagramFormat
+{
+ ///
+ /// A Mermaid ER diagram in a markdown code block. Uses the `md` extension.
+ ///
+ Markdown = 1,
+
+ ///
+ /// The diagram rendered to an image. Uses the `svg` extension.
+ ///
+ Svg = 2,
+
+ ///
+ /// The diagram rendered to an image. Uses the `png` extension.
+ ///
+ Png = 4,
+
+ All = Markdown | Svg | Png
+}
+```
+snippet source | anchor
+
+
+`SchemaIncludes` and `SchemaFilter` do not apply to a diagram. All tables are included.
+
+A rendered png may not be pixel identical across machines, so compare using [ssim](https://github.com/VerifyTests/Verify/blob/main/docs/comparer.md):
+
+
+
+```cs
+VerifierSettings.UseSsimForPng();
+```
+snippet source | anchor
+
+
+The svg and png are rendered by [Naiad](https://github.com/Papyrine/Naiad). Its build check requires a sponsorship, or exemption, property to be set in the consuming project. The build error lists the options.
+
### Recording
@@ -129,7 +214,7 @@ command.CommandText = "select Value from MyTable";
var value = await command.ExecuteScalarAsync();
await Verify(value!);
```
-snippet source | anchor
+snippet source | anchor
Will result in the following verified file:
@@ -184,7 +269,7 @@ await Verify(
sqlEntries = entries
});
```
-snippet source | anchor
+snippet source | anchor
@@ -212,7 +297,7 @@ var sqlErrorsViaType = entries
.Select(_ => _.Data)
.OfType();
```
-snippet source | anchor
+snippet source | anchor
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 1b677cc..e20115f 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -2,7 +2,7 @@
CS1591;CS0649;CS8632;NU1608;NU1109
- 12.2.0
+ 12.3.0
preview
1.0.0
SqlServer, Verify
@@ -13,5 +13,7 @@
true
OpenSource
2027-08
+ OpenSource
+ 2027-08
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index cc1094e..6e937d9 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -12,6 +12,7 @@
+
diff --git a/src/Tests/ModuleInit.cs b/src/Tests/ModuleInit.cs
index a58af6f..84592a6 100644
--- a/src/Tests/ModuleInit.cs
+++ b/src/Tests/ModuleInit.cs
@@ -9,6 +9,14 @@ public static void Init() =>
#endregion
[ModuleInitializer]
- public static void InitOther() =>
+ public static void InitOther()
+ {
+ #region UseSsimForPng
+
+ VerifierSettings.UseSsimForPng();
+
+ #endregion
+
VerifierSettings.InitializePlugins();
+ }
}
\ No newline at end of file
diff --git a/src/Tests/Tests.SchemaAsDiagram.verified.md b/src/Tests/Tests.SchemaAsDiagram.verified.md
new file mode 100644
index 0000000..9c968e3
--- /dev/null
+++ b/src/Tests/Tests.SchemaAsDiagram.verified.md
@@ -0,0 +1,78 @@
+```mermaid
+erDiagram
+ TestSchema_SchemaTable["**TestSchema_SchemaTable**"] {
+ int Id pk
+ nvarchar(50) Name
+ }
+ ChildTable["**ChildTable**"] {
+ int Id pk
+ int ParentId
+ int Value
+ }
+ ColumnTypes["**ColumnTypes**"] {
+ int Id pk
+ nvarchar(100) Name
+ nvarchar(max)(nullable) Description
+ varchar(20)(nullable) Code
+ decimal(18,4) Amount
+ bit IsActive
+ datetime2 Created
+ timestamp RowVersion
+ money(nullable) Price
+ float(nullable) Ratio
+ smallint(nullable) SmallNum
+ tinyint(nullable) TinyNum
+ bigint(nullable) BigNum
+ uniqueidentifier(nullable) UniqueCode
+ text(nullable) Notes
+ varbinary(500)(nullable) BinaryData
+ varbinary(max)(nullable) MaxBinary
+ char(10)(nullable) FixedChar
+ binary(16)(nullable) FixedBinary
+ time(nullable) TimeOnly
+ date(nullable) DateOnly
+ ntext(nullable) NText
+ xml(nullable) Xml
+ }
+ CompositeKeyTable["**CompositeKeyTable**"] {
+ int Key1 pk
+ int Key2 pk
+ nvarchar(200)(nullable) Data
+ }
+ MultiIndexTable["**MultiIndexTable**"] {
+ int Id pk
+ nvarchar(100) Name
+ nvarchar(50) Category
+ int Status
+ }
+ MultiTriggerTable["**MultiTriggerTable**"] {
+ int Id pk
+ nvarchar(100)(nullable) Name
+ }
+ MyOtherTable["**MyOtherTable**"] {
+ int(nullable) Value
+ }
+ MyTable["**MyTable**"] {
+ int(nullable) Value
+ }
+ ParentTable["**ParentTable**"] {
+ int Id pk
+ nvarchar(100) Name
+ }
+ WithComputed["**WithComputed**"] {
+ int Id pk
+ nvarchar(50) FirstName
+ nvarchar(50) LastName
+ nvarchar(101) FullName "computed"
+ int Quantity
+ decimal(10,2) UnitPrice
+ decimal(21,2)(nullable) TotalPrice "computed"
+ }
+ WithDefaults["**WithDefaults**"] {
+ int Id pk
+ varchar(20) Status
+ datetime2 Created
+ int Score
+ }
+ ParentTable ||--o{ ChildTable : "FK_ChildTable_ParentTable"
+```
\ No newline at end of file
diff --git a/src/Tests/Tests.SchemaAsDiagramFormats.verified.md b/src/Tests/Tests.SchemaAsDiagramFormats.verified.md
new file mode 100644
index 0000000..9c968e3
--- /dev/null
+++ b/src/Tests/Tests.SchemaAsDiagramFormats.verified.md
@@ -0,0 +1,78 @@
+```mermaid
+erDiagram
+ TestSchema_SchemaTable["**TestSchema_SchemaTable**"] {
+ int Id pk
+ nvarchar(50) Name
+ }
+ ChildTable["**ChildTable**"] {
+ int Id pk
+ int ParentId
+ int Value
+ }
+ ColumnTypes["**ColumnTypes**"] {
+ int Id pk
+ nvarchar(100) Name
+ nvarchar(max)(nullable) Description
+ varchar(20)(nullable) Code
+ decimal(18,4) Amount
+ bit IsActive
+ datetime2 Created
+ timestamp RowVersion
+ money(nullable) Price
+ float(nullable) Ratio
+ smallint(nullable) SmallNum
+ tinyint(nullable) TinyNum
+ bigint(nullable) BigNum
+ uniqueidentifier(nullable) UniqueCode
+ text(nullable) Notes
+ varbinary(500)(nullable) BinaryData
+ varbinary(max)(nullable) MaxBinary
+ char(10)(nullable) FixedChar
+ binary(16)(nullable) FixedBinary
+ time(nullable) TimeOnly
+ date(nullable) DateOnly
+ ntext(nullable) NText
+ xml(nullable) Xml
+ }
+ CompositeKeyTable["**CompositeKeyTable**"] {
+ int Key1 pk
+ int Key2 pk
+ nvarchar(200)(nullable) Data
+ }
+ MultiIndexTable["**MultiIndexTable**"] {
+ int Id pk
+ nvarchar(100) Name
+ nvarchar(50) Category
+ int Status
+ }
+ MultiTriggerTable["**MultiTriggerTable**"] {
+ int Id pk
+ nvarchar(100)(nullable) Name
+ }
+ MyOtherTable["**MyOtherTable**"] {
+ int(nullable) Value
+ }
+ MyTable["**MyTable**"] {
+ int(nullable) Value
+ }
+ ParentTable["**ParentTable**"] {
+ int Id pk
+ nvarchar(100) Name
+ }
+ WithComputed["**WithComputed**"] {
+ int Id pk
+ nvarchar(50) FirstName
+ nvarchar(50) LastName
+ nvarchar(101) FullName "computed"
+ int Quantity
+ decimal(10,2) UnitPrice
+ decimal(21,2)(nullable) TotalPrice "computed"
+ }
+ WithDefaults["**WithDefaults**"] {
+ int Id pk
+ varchar(20) Status
+ datetime2 Created
+ int Score
+ }
+ ParentTable ||--o{ ChildTable : "FK_ChildTable_ParentTable"
+```
\ No newline at end of file
diff --git a/src/Tests/Tests.SchemaAsDiagramFormats.verified.png b/src/Tests/Tests.SchemaAsDiagramFormats.verified.png
new file mode 100644
index 0000000..ff86532
Binary files /dev/null and b/src/Tests/Tests.SchemaAsDiagramFormats.verified.png differ
diff --git a/src/Tests/Tests.SchemaAsDiagramFormats.verified.svg b/src/Tests/Tests.SchemaAsDiagramFormats.verified.svg
new file mode 100644
index 0000000..487d537
--- /dev/null
+++ b/src/Tests/Tests.SchemaAsDiagramFormats.verified.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/Tests/Tests.SchemaAsDiagramSvgOnly.verified.svg b/src/Tests/Tests.SchemaAsDiagramSvgOnly.verified.svg
new file mode 100644
index 0000000..487d537
--- /dev/null
+++ b/src/Tests/Tests.SchemaAsDiagramSvgOnly.verified.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/Tests/Tests.cs b/src/Tests/Tests.cs
index d036596..621fc03 100644
--- a/src/Tests/Tests.cs
+++ b/src/Tests/Tests.cs
@@ -333,6 +333,54 @@ await Verify(connection)
#endregion
}
+ [Test]
+ public async Task SchemaAsDiagram()
+ {
+ await using var database = await sqlInstance.Build();
+ var connection = database.Connection;
+
+ #region SqlServerSchemaAsDiagram
+
+ await Verify(connection)
+ .SchemaAsDiagram();
+
+ #endregion
+ }
+
+ [Test]
+ public async Task SchemaAsDiagramFormats()
+ {
+ await using var database = await sqlInstance.Build();
+ var connection = database.Connection;
+
+ #region SqlServerSchemaAsDiagramFormats
+
+ await Verify(connection)
+ .SchemaAsDiagram(
+ DiagramFormat.Markdown |
+ DiagramFormat.Svg |
+ DiagramFormat.Png);
+
+ #endregion
+ }
+
+ [Test]
+ public async Task SchemaAsDiagramSvgOnly()
+ {
+ await using var database = await sqlInstance.Build();
+ var connection = database.Connection;
+ await Verify(connection)
+ .SchemaAsDiagram(DiagramFormat.Svg);
+ }
+
+ [Test]
+ public async Task SchemaAsDiagramNoFormat()
+ {
+ var settings = new VerifySettings();
+ await Assert.That(() => settings.SchemaAsDiagram(0))
+ .Throws();
+ }
+
[Test]
public async Task SchemaInDynamic()
{
diff --git a/src/Verify.SqlServer/SchemaValidation/DiagramBuilder.cs b/src/Verify.SqlServer/SchemaValidation/DiagramBuilder.cs
new file mode 100644
index 0000000..18b8bf5
--- /dev/null
+++ b/src/Verify.SqlServer/SchemaValidation/DiagramBuilder.cs
@@ -0,0 +1,31 @@
+#if NET10_0_OR_GREATER
+using DbToMermaid;
+
+static class DiagramBuilder
+{
+ public static async Task Build(SqlConnection connection, DiagramFormat formats)
+ {
+ var targets = new List();
+
+ if (formats.HasFlag(DiagramFormat.Markdown))
+ {
+ var markdown = await SqlServerToMermaid.RenderMarkdown(connection);
+ targets.Add(new("md", markdown));
+ }
+
+ if (formats.HasFlag(DiagramFormat.Svg))
+ {
+ var svg = await SqlServerToMermaid.RenderSvg(connection);
+ targets.Add(new("svg", svg));
+ }
+
+ if (formats.HasFlag(DiagramFormat.Png))
+ {
+ var png = await SqlServerToMermaid.RenderPng(connection);
+ targets.Add(new("png", new MemoryStream(png)));
+ }
+
+ return new(null, targets);
+ }
+}
+#endif
diff --git a/src/Verify.SqlServer/SchemaValidation/DiagramFormat.cs b/src/Verify.SqlServer/SchemaValidation/DiagramFormat.cs
new file mode 100644
index 0000000..567463d
--- /dev/null
+++ b/src/Verify.SqlServer/SchemaValidation/DiagramFormat.cs
@@ -0,0 +1,25 @@
+namespace VerifyTests.SqlServer;
+
+///
+/// The files produced when a schema is verified as a diagram. Can be combined.
+///
+[Flags]
+public enum DiagramFormat
+{
+ ///
+ /// A Mermaid ER diagram in a markdown code block. Uses the `md` extension.
+ ///
+ Markdown = 1,
+
+ ///
+ /// The diagram rendered to an image. Uses the `svg` extension.
+ ///
+ Svg = 2,
+
+ ///
+ /// The diagram rendered to an image. Uses the `png` extension.
+ ///
+ Png = 4,
+
+ All = Markdown | Svg | Png
+}
diff --git a/src/Verify.SqlServer/SchemaValidation/SchemaSettings.cs b/src/Verify.SqlServer/SchemaValidation/SchemaSettings.cs
index 7ffe213..d4ccc4c 100644
--- a/src/Verify.SqlServer/SchemaValidation/SchemaSettings.cs
+++ b/src/Verify.SqlServer/SchemaValidation/SchemaSettings.cs
@@ -3,5 +3,9 @@ class SchemaSettings
public DbObjects Includes { get; set; } = DbObjects.All;
public Format Format { get; set; } = Format.Md;
public bool IsMd => Format == Format.Md;
+#if NET10_0_OR_GREATER
+ // when set, the schema is verified as a diagram instead of as scripts
+ public DiagramFormat? Diagram { get; set; }
+#endif
public Func IncludeItem { get; set; } = _ => true;
}
\ No newline at end of file
diff --git a/src/Verify.SqlServer/SchemaValidation/VerifySettingsExtensions.cs b/src/Verify.SqlServer/SchemaValidation/VerifySettingsExtensions.cs
index 038bb59..7ac3030 100644
--- a/src/Verify.SqlServer/SchemaValidation/VerifySettingsExtensions.cs
+++ b/src/Verify.SqlServer/SchemaValidation/VerifySettingsExtensions.cs
@@ -54,6 +54,35 @@ public static void SchemaAsSql(
GetOrAddSettings(settings)
.Format = Format.Sql;
+#if NET10_0_OR_GREATER
+ ///
+ /// Verify the schema as a Mermaid ER diagram of the tables, instead of as scripts.
+ ///
+ /// The settings to apply to.
+ /// The files to produce. Can be combined, with one file verified per format.
+ public static SettingsTask SchemaAsDiagram(
+ this SettingsTask settings,
+ DiagramFormat formats = DiagramFormat.Markdown)
+ {
+ settings.CurrentSettings.SchemaAsDiagram(formats);
+ return settings;
+ }
+
+ ///
+ public static void SchemaAsDiagram(
+ this VerifySettings settings,
+ DiagramFormat formats = DiagramFormat.Markdown)
+ {
+ if ((formats & DiagramFormat.All) == 0)
+ {
+ throw new ArgumentException("At least one format is required.", nameof(formats));
+ }
+
+ GetOrAddSettings(settings)
+ .Diagram = formats;
+ }
+#endif
+
static SchemaSettings GetOrAddSettings(VerifySettings settings)
{
var context = settings.Context;
diff --git a/src/Verify.SqlServer/Verify.SqlServer.csproj b/src/Verify.SqlServer/Verify.SqlServer.csproj
index f903682..58ea391 100644
--- a/src/Verify.SqlServer/Verify.SqlServer.csproj
+++ b/src/Verify.SqlServer/Verify.SqlServer.csproj
@@ -12,5 +12,7 @@
+
+
diff --git a/src/Verify.SqlServer/VerifySqlServer.cs b/src/Verify.SqlServer/VerifySqlServer.cs
index 55f037f..23acbf8 100644
--- a/src/Verify.SqlServer/VerifySqlServer.cs
+++ b/src/Verify.SqlServer/VerifySqlServer.cs
@@ -44,13 +44,19 @@ public static void Initialize(bool recordCommands)
}
}
- static ConversionResult ToSql(SqlConnection connection, IReadOnlyDictionary context)
+ static Task ToSql(SqlConnection connection, IReadOnlyDictionary context)
{
var settings = context.GetSchemaSettings();
+#if NET10_0_OR_GREATER
+ if (settings.Diagram is { } formats)
+ {
+ return DiagramBuilder.Build(connection, formats);
+ }
+#endif
var builder = new SqlScriptBuilder(settings);
var content = builder.BuildContent(connection);
var extension = GetExtension(settings);
- return new(null, extension, content);
+ return Task.FromResult(new ConversionResult(null, extension, content));
}
static string GetExtension(SchemaSettings settings)