From 4c86da3b00c73ebbaeab9e056ba7578d243130b6 Mon Sep 17 00:00:00 2001 From: gumbarros Date: Sat, 27 Sep 2025 10:54:39 -0300 Subject: [PATCH 1/5] Add support for specifying non-clustered index sort direction --- .../Platform/SqlCreateTableWriter.cs | 2 +- .../Sinks/MSSqlServer/SqlColumn.cs | 6 ++++++ .../Sinks/MSSqlServer/SqlIndexDirection.cs | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/SqlIndexDirection.cs diff --git a/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/SqlCreateTableWriter.cs b/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/SqlCreateTableWriter.cs index e1ce1026..d0d3bcb6 100644 --- a/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/SqlCreateTableWriter.cs +++ b/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/SqlCreateTableWriter.cs @@ -54,7 +54,7 @@ public string GetSql() // collect non-PK indexes for separate output after the table DDL if (common != null && common.NonClusteredIndex && common != _columnOptions.PrimaryKey) - ix.AppendLine(Invariant($"CREATE NONCLUSTERED INDEX [IX{indexCount++}_{_tableName}] ON [{_schemaName}].[{_tableName}] ([{common.ColumnName}]);")); + ix.AppendLine(Invariant($"CREATE NONCLUSTERED INDEX [IX{indexCount++}_{_tableName}] ON [{_schemaName}].[{_tableName}] ([{common.ColumnName}] {common.NonClusteredIndexDirection});")); } } diff --git a/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/SqlColumn.cs b/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/SqlColumn.cs index 626e1961..340d20ca 100644 --- a/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/SqlColumn.cs +++ b/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/SqlColumn.cs @@ -100,6 +100,12 @@ public SqlDbType DataType /// public bool NonClusteredIndex { get; set; } + /// + /// Specifies the sort direction for a non-clustered index on the SQL column. + /// The default value is . This property is only used when auto-creating a log table. + /// + public SqlIndexDirection NonClusteredIndexDirection { get; set; } + /// /// The name of the Serilog property to use as the value when filling the DataTable. /// If not specified, the ColumnName and PropertyName are the same. diff --git a/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/SqlIndexDirection.cs b/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/SqlIndexDirection.cs new file mode 100644 index 00000000..247f154e --- /dev/null +++ b/src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/SqlIndexDirection.cs @@ -0,0 +1,18 @@ +namespace Serilog.Sinks.MSSqlServer +{ + /// + /// Defines the sort order for an SQL index. + /// + public enum SqlIndexDirection + { + /// + /// Represents the ascending direction for SQL indexing. + /// + Asc, + + /// + /// Represents the descending direction for SQL indexing. + /// + Desc + } +} From 814c331861955afa81665512e655cedc08bbd7b7 Mon Sep 17 00:00:00 2001 From: gumbarros Date: Sat, 27 Sep 2025 11:02:57 -0300 Subject: [PATCH 2/5] Added NonClusteredIndexDirection docs --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index e9a7091a..a41d41d5 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ columnOpts.Store.Add(StandardColumn.LogEvent); columnOpts.LogEvent.DataLength = 2048; columnOpts.PrimaryKey = columnOpts.TimeStamp; columnOpts.TimeStamp.NonClusteredIndex = true; +columnOpts.TimeStamp.NonClusteredIndexDirection = SqlIndexDirection.Desc; var log = new LoggerConfiguration() .WriteTo.MSSqlServer( @@ -349,6 +350,7 @@ Each Standard Column in the `ColumnOptions.Store` list and any custom columns yo * `AllowNull` * `DataLength` * `NonClusteredIndex` +* `NonClusteredIndexDirection` ### ColumnName @@ -412,6 +414,17 @@ Supported SQL column data types that use this property: Any individual column can be defined as a non-clustered index, including the table primary key. Use this with caution, indexing carries a relatively high write-throughput penalty. One way to mitigate this is to keep non-clustered indexes offline and use batch reindexing on a scheduled basis. +### NonClusteredIndexDirection + +Specifies the sort direction (`ASC` or `DESC`) for a non-clustered index on the SQL column. +The default value is `ASC`. + +It is especially useful for the timestamp column, +where choosing the correct sort direction can optimize query performance for workloads that typically scan +recent data first. + +This only has effect if `NonClusteredIndex` is true. + ## Standard Columns By default (and consistent with the SQL DDL to create a table shown earlier) these columns are included in a new `ColumnOptions.Store` list: From f84a32a0fccb644e2df7fdde34990777d2ff8ad6 Mon Sep 17 00:00:00 2001 From: gumbarros Date: Sat, 27 Sep 2025 11:02:57 -0300 Subject: [PATCH 3/5] Added NonClusteredIndexDirection docs --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e9a7091a..3c0c7934 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ columnOpts.Store.Add(StandardColumn.LogEvent); columnOpts.LogEvent.DataLength = 2048; columnOpts.PrimaryKey = columnOpts.TimeStamp; columnOpts.TimeStamp.NonClusteredIndex = true; +columnOpts.TimeStamp.NonClusteredIndexDirection = SqlIndexDirection.Desc; var log = new LoggerConfiguration() .WriteTo.MSSqlServer( @@ -349,6 +350,7 @@ Each Standard Column in the `ColumnOptions.Store` list and any custom columns yo * `AllowNull` * `DataLength` * `NonClusteredIndex` +* `NonClusteredIndexDirection` ### ColumnName @@ -412,9 +414,20 @@ Supported SQL column data types that use this property: Any individual column can be defined as a non-clustered index, including the table primary key. Use this with caution, indexing carries a relatively high write-throughput penalty. One way to mitigate this is to keep non-clustered indexes offline and use batch reindexing on a scheduled basis. +### NonClusteredIndexDirection + +Specifies the sort direction (`ASC` or `DESC`) for a non-clustered index on the SQL column. +The default value is `ASC`. + +It is especially useful for the timestamp column, +where choosing the correct sort direction can optimize query performance for workloads that typically scan +recent data first. + +This only has effect if `NonClusteredIndex` is `true`. + ## Standard Columns -By default (and consistent with the SQL DDL to create a table shown earlier) these columns are included in a new `ColumnOptions.Store` list: +By default (and consistent with the SQL DDL to create a table shown earlier), these columns are included in a new `ColumnOptions.Store` list: - `StandardColumn.Id` - `StandardColumn.Message` From 6509078524faf237de62dc65500cd295ef8b7aa5 Mon Sep 17 00:00:00 2001 From: gumbarros Date: Sat, 11 Oct 2025 11:03:46 -0300 Subject: [PATCH 4/5] Add support for configuring NonClusteredIndexDirection --- .../MicrosoftExtensionsColumnOptionsProvider.cs | 1 + .../SystemConfigurationColumnOptionsProvider.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/Microsoft.Extensions.Configuration/MicrosoftExtensionsColumnOptionsProvider.cs b/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/Microsoft.Extensions.Configuration/MicrosoftExtensionsColumnOptionsProvider.cs index ce585d3d..c2d30234 100644 --- a/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/Microsoft.Extensions.Configuration/MicrosoftExtensionsColumnOptionsProvider.cs +++ b/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/Microsoft.Extensions.Configuration/MicrosoftExtensionsColumnOptionsProvider.cs @@ -34,6 +34,7 @@ private static void SetCommonColumnOptions(IConfigurationSection section, SqlCol SetProperty.IfNotNull(section["allowNull"], (val) => target.AllowNull = val); SetProperty.IfNotNull(section["dataLength"], (val) => target.DataLength = val); SetProperty.IfNotNull(section["nonClusteredIndex"], (val) => target.NonClusteredIndex = val); + SetProperty.IfNotNull(section["nonClusteredIndexDirection"], (val) => target.NonClusteredIndexDirection = val); } private static void AddRemoveStandardColumns(IConfigurationSection config, ColumnOptions columnOptions) diff --git a/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/SystemConfigurationColumnOptionsProvider.cs b/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/SystemConfigurationColumnOptionsProvider.cs index 21d62d3c..79d89362 100644 --- a/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/SystemConfigurationColumnOptionsProvider.cs +++ b/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/SystemConfigurationColumnOptionsProvider.cs @@ -26,6 +26,7 @@ private static void SetCommonColumnOptions(ColumnConfig source, SqlColumn target SetProperty.IfProvided(source, nameof(target.AllowNull), value => target.AllowNull = value); SetProperty.IfProvided(source, nameof(target.DataLength), value => target.DataLength = value); SetProperty.IfProvided(source, nameof(target.NonClusteredIndex), value => target.NonClusteredIndex = value); + SetProperty.IfProvided(source, nameof(target.NonClusteredIndexDirection), value => target.NonClusteredIndexDirection = value); } private static void ReadPropertiesColumnOptions(MSSqlServerConfigurationSection config, ColumnOptions columnOptions) From 44c7bb37b83d82289986c8a5e7211909e69cb1ed Mon Sep 17 00:00:00 2001 From: Christian Kadluba <10721825+ckadluba@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:08:35 +0200 Subject: [PATCH 5/5] Add fixes and tests * Added unit tests for MicrosoftExtensionsColumnOptionsProvider, MicrosoftExtensionsColumnOptionsProviderTests and SqlCreateTableWriter * Added SetProperty.IfEnumNotNull() and SetProperty.IfEnumProvided() to handle enum column option reading from configuration * Use NonClusteredIndexDirection in WorkerServiceDemo Related issue: #636 --- sample/WorkerServiceDemo/appsettings.json | 11 ++- ...icrosoftExtensionsColumnOptionsProvider.cs | 3 +- .../Implementations/SetProperty.cs | 20 ++++- .../System.Configuration/ColumnConfig.cs | 10 +++ .../SetPropertyValueOrigin.cs | 16 ++++ ...ystemConfigurationColumnOptionsProvider.cs | 2 +- ...oftExtensionsColumnOptionsProviderTests.cs | 89 ++++++++++++++----- ...ConfigurationColumnOptionsProviderTests.cs | 33 +++++++ .../Platform/SqlCreateTableWriterTests.cs | 54 ++++++++++- 9 files changed, 210 insertions(+), 28 deletions(-) diff --git a/sample/WorkerServiceDemo/appsettings.json b/sample/WorkerServiceDemo/appsettings.json index 1ca39a65..72d4cc40 100644 --- a/sample/WorkerServiceDemo/appsettings.json +++ b/sample/WorkerServiceDemo/appsettings.json @@ -25,7 +25,10 @@ "removeStandardColumns": [ "MessageTemplate", "Properties" ], "timeStamp": { "columnName": "Timestamp", - "convertToUtc": false + "allowNulls": false, + "convertToUtc": false, + "nonClusteredIndex": true, + "nonClusteredIndexDirection": "Desc" }, "customColumns": [ { @@ -43,6 +46,12 @@ "propertyName": "NonstructuredProperty.WithNameContainingDots.Name", "resolveHierarchicalPropertyName": false, "dataType": "12" + }, + { + "columnName": "AdditionalColumn4", + "dataType": "0", + "nonClusteredIndex": true, + "nonClusteredIndexDirection": "Desc" } ] }, diff --git a/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/Microsoft.Extensions.Configuration/MicrosoftExtensionsColumnOptionsProvider.cs b/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/Microsoft.Extensions.Configuration/MicrosoftExtensionsColumnOptionsProvider.cs index c2d30234..dd59c704 100644 --- a/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/Microsoft.Extensions.Configuration/MicrosoftExtensionsColumnOptionsProvider.cs +++ b/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/Microsoft.Extensions.Configuration/MicrosoftExtensionsColumnOptionsProvider.cs @@ -34,7 +34,8 @@ private static void SetCommonColumnOptions(IConfigurationSection section, SqlCol SetProperty.IfNotNull(section["allowNull"], (val) => target.AllowNull = val); SetProperty.IfNotNull(section["dataLength"], (val) => target.DataLength = val); SetProperty.IfNotNull(section["nonClusteredIndex"], (val) => target.NonClusteredIndex = val); - SetProperty.IfNotNull(section["nonClusteredIndexDirection"], (val) => target.NonClusteredIndexDirection = val); + SetProperty.IfEnumNotNull(section["nonClusteredIndexDirection"], + (val) => target.NonClusteredIndexDirection = val); } private static void AddRemoveStandardColumns(IConfigurationSection config, ColumnOptions columnOptions) diff --git a/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/SetProperty.cs b/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/SetProperty.cs index 1d669c4d..fbb487b8 100644 --- a/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/SetProperty.cs +++ b/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/SetProperty.cs @@ -29,7 +29,25 @@ public static void IfNotNull(string value, PropertySetter setter) var setting = (T)Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture); setter(setting); } - // don't change the property if the conversion fails + // don't change the property if the conversion fails + catch (InvalidCastException) { } + catch (OverflowException) { } + } + + /// + /// This will only set a value (execute the PropertySetter delegate) if the value is non-null. + /// It also converts the provided value to the requested enum type. This allows configuration to only + /// apply property changes when external configuration has actually provided a value. + /// + public static void IfEnumNotNull(string value, PropertySetter setter) where T : System.Enum + { + if (value == null || setter == null) return; + try + { + var setting = (T)Enum.Parse(typeof(T), value, ignoreCase: true); + setter(setting); + } + // don't change the property if the conversion fails catch (InvalidCastException) { } catch (OverflowException) { } } diff --git a/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/ColumnConfig.cs b/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/ColumnConfig.cs index cd00467c..5b3cebbd 100644 --- a/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/ColumnConfig.cs +++ b/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/ColumnConfig.cs @@ -83,6 +83,13 @@ public string NonClusteredIndex set { this[nameof(NonClusteredIndex)] = value; } } + [ConfigurationProperty("NonClusteredIndexDirection")] + public string NonClusteredIndexDirection + { + get { return (string)this[nameof(NonClusteredIndexDirection)]; } + set { this[nameof(NonClusteredIndexDirection)] = value; } + } + internal SqlColumn AsSqlColumn() { var sqlColumn = new SqlColumn(); @@ -106,6 +113,9 @@ internal SqlColumn AsSqlColumn() SetProperty.IfProvided(this, nameof(NonClusteredIndex), (val) => sqlColumn.NonClusteredIndex = val); + SetProperty.IfEnumProvided(this, nameof(NonClusteredIndexDirection), + (val) => sqlColumn.NonClusteredIndexDirection = val); + return sqlColumn; } } diff --git a/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/SetPropertyValueOrigin.cs b/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/SetPropertyValueOrigin.cs index f29861ae..15173028 100644 --- a/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/SetPropertyValueOrigin.cs +++ b/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/SetPropertyValueOrigin.cs @@ -22,6 +22,22 @@ public static void IfProvided(ConfigurationElement element, string propertyNa IfNotNull((string)property.Value, setter); } + /// + /// Test the underlying enum property collection's value-origin flag for a non-default string value. Empty strings allowed. + /// + public static void IfEnumProvided(ConfigurationElement element, string propertyName, PropertySetter setter) + where T : System.Enum + { + if (element == null) + { + return; + } + + var property = element.ElementInformation.Properties[propertyName]; + if (property.ValueOrigin == PropertyValueOrigin.Default) return; + IfEnumNotNull((string)property.Value, setter); + } + /// /// Test the underlying property collection's value-origin flag for a non-default, non-null, non-empty string value. /// diff --git a/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/SystemConfigurationColumnOptionsProvider.cs b/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/SystemConfigurationColumnOptionsProvider.cs index 79d89362..42e675a7 100644 --- a/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/SystemConfigurationColumnOptionsProvider.cs +++ b/src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/SystemConfigurationColumnOptionsProvider.cs @@ -26,7 +26,7 @@ private static void SetCommonColumnOptions(ColumnConfig source, SqlColumn target SetProperty.IfProvided(source, nameof(target.AllowNull), value => target.AllowNull = value); SetProperty.IfProvided(source, nameof(target.DataLength), value => target.DataLength = value); SetProperty.IfProvided(source, nameof(target.NonClusteredIndex), value => target.NonClusteredIndex = value); - SetProperty.IfProvided(source, nameof(target.NonClusteredIndexDirection), value => target.NonClusteredIndexDirection = value); + SetProperty.IfEnumProvided(source, nameof(target.NonClusteredIndexDirection), value => target.NonClusteredIndexDirection = value); } private static void ReadPropertiesColumnOptions(MSSqlServerConfigurationSection config, ColumnOptions columnOptions) diff --git a/test/Serilog.Sinks.MSSqlServer.Tests/Configuration/Implementations/Microsoft.Extensions.Configuration/MicrosoftExtensionsColumnOptionsProviderTests.cs b/test/Serilog.Sinks.MSSqlServer.Tests/Configuration/Implementations/Microsoft.Extensions.Configuration/MicrosoftExtensionsColumnOptionsProviderTests.cs index 8c571c27..35d38161 100644 --- a/test/Serilog.Sinks.MSSqlServer.Tests/Configuration/Implementations/Microsoft.Extensions.Configuration/MicrosoftExtensionsColumnOptionsProviderTests.cs +++ b/test/Serilog.Sinks.MSSqlServer.Tests/Configuration/Implementations/Microsoft.Extensions.Configuration/MicrosoftExtensionsColumnOptionsProviderTests.cs @@ -105,15 +105,16 @@ public void ConfigureColumnOptionsAddsColumnIdWithSpecifiedOptions() var dataType = SqlDbType.Bit; var allowNull = false; var nonClusteredIndex = true; + var nonClusteredIndexDirection = SqlIndexDirection.Asc; SetupConfigurationSectionMocks(); - SetupColumnSectionMock("id", columnName, dataType, allowNull, nonClusteredIndex); + SetupColumnSectionMock("id", columnName, dataType, allowNull, nonClusteredIndex, nonClusteredIndexDirection); var sut = new MicrosoftExtensionsColumnOptionsProvider(); // Act var result = sut.ConfigureColumnOptions(new Serilog.Sinks.MSSqlServer.ColumnOptions(), _configurationSectionMock.Object); // Assert - AssertColumnSqlOptions(columnName, dataType, allowNull, nonClusteredIndex, result.Id); + AssertColumnSqlOptions(columnName, dataType, allowNull, nonClusteredIndex, nonClusteredIndexDirection, result.Id); } [Fact] @@ -157,15 +158,18 @@ public void ConfigureColumnOptionsAddsColumnLevelWithSpecifiedOptions() var dataType = SqlDbType.Bit; var allowNull = true; var nonClusteredIndex = true; + var nonClusteredIndexDirection = SqlIndexDirection.Asc; SetupConfigurationSectionMocks(); - SetupColumnSectionMock("level", columnName, dataType, allowNull, nonClusteredIndex); + SetupColumnSectionMock("level", columnName, dataType, allowNull, + nonClusteredIndex, nonClusteredIndexDirection); var sut = new MicrosoftExtensionsColumnOptionsProvider(); // Act var result = sut.ConfigureColumnOptions(new Serilog.Sinks.MSSqlServer.ColumnOptions(), _configurationSectionMock.Object); // Assert - AssertColumnSqlOptions(columnName, dataType, allowNull, nonClusteredIndex, result.Level); + AssertColumnSqlOptions(columnName, dataType, allowNull, + nonClusteredIndex, nonClusteredIndexDirection, result.Level); } [Fact] @@ -192,15 +196,18 @@ public void ConfigureColumnOptionsAddsColumnPropertiesWithSpecifiedOptions() var dataType = SqlDbType.Bit; var allowNull = true; var nonClusteredIndex = true; + var nonClusteredIndexDirection = SqlIndexDirection.Asc; SetupConfigurationSectionMocks(); - SetupColumnSectionMock("properties", columnName, dataType, allowNull, nonClusteredIndex); + SetupColumnSectionMock("properties", columnName, dataType, allowNull, + nonClusteredIndex, nonClusteredIndexDirection); var sut = new MicrosoftExtensionsColumnOptionsProvider(); // Act var result = sut.ConfigureColumnOptions(new Serilog.Sinks.MSSqlServer.ColumnOptions(), _configurationSectionMock.Object); // Assert - AssertColumnSqlOptions(columnName, dataType, allowNull, nonClusteredIndex, result.Properties); + AssertColumnSqlOptions(columnName, dataType, allowNull, nonClusteredIndex, + nonClusteredIndexDirection, result.Properties); } [Fact] @@ -410,15 +417,18 @@ public void ConfigureColumnOptionsAddsColumnTimeStampWithSpecifiedOptions() var dataType = SqlDbType.Bit; var allowNull = true; var nonClusteredIndex = true; + var nonClusteredIndexDirection = SqlIndexDirection.Desc; SetupConfigurationSectionMocks(); - SetupColumnSectionMock("timeStamp", columnName, dataType, allowNull, nonClusteredIndex); + SetupColumnSectionMock("timeStamp", columnName, dataType, allowNull, + nonClusteredIndex, nonClusteredIndexDirection); var sut = new MicrosoftExtensionsColumnOptionsProvider(); // Act var result = sut.ConfigureColumnOptions(new Serilog.Sinks.MSSqlServer.ColumnOptions(), _configurationSectionMock.Object); // Assert - AssertColumnSqlOptions(columnName, dataType, allowNull, nonClusteredIndex, result.TimeStamp); + AssertColumnSqlOptions(columnName, dataType, allowNull, + nonClusteredIndex, nonClusteredIndexDirection, result.TimeStamp); } [Fact] @@ -445,15 +455,18 @@ public void ConfigureColumnOptionsAddsColumnLogEventWithSpecifiedOptions() var dataType = SqlDbType.Bit; var allowNull = true; var nonClusteredIndex = true; + var nonClusteredIndexDirection = SqlIndexDirection.Asc; SetupConfigurationSectionMocks(); - SetupColumnSectionMock("logEvent", columnName, dataType, allowNull, nonClusteredIndex); + SetupColumnSectionMock("logEvent", columnName, dataType, allowNull, + nonClusteredIndex, nonClusteredIndexDirection); var sut = new MicrosoftExtensionsColumnOptionsProvider(); // Act var result = sut.ConfigureColumnOptions(new Serilog.Sinks.MSSqlServer.ColumnOptions(), _configurationSectionMock.Object); // Assert - AssertColumnSqlOptions(columnName, dataType, allowNull, nonClusteredIndex, result.LogEvent); + AssertColumnSqlOptions(columnName, dataType, allowNull, + nonClusteredIndex, nonClusteredIndexDirection, result.LogEvent); } [Fact] @@ -496,15 +509,18 @@ public void ConfigureColumnOptionsAddsColumnTraceIdWithSpecifiedOptions() var dataType = SqlDbType.NVarChar; var allowNull = true; var nonClusteredIndex = true; + var nonClusteredIndexDirection = SqlIndexDirection.Asc; SetupConfigurationSectionMocks(); - SetupColumnSectionMock("traceId", columnName, dataType, allowNull, nonClusteredIndex); + SetupColumnSectionMock("traceId", columnName, dataType, allowNull, + nonClusteredIndex, nonClusteredIndexDirection); var sut = new MicrosoftExtensionsColumnOptionsProvider(); // Act var result = sut.ConfigureColumnOptions(new Serilog.Sinks.MSSqlServer.ColumnOptions(), _configurationSectionMock.Object); // Assert - AssertColumnSqlOptions(columnName, dataType, allowNull, nonClusteredIndex, result.TraceId); + AssertColumnSqlOptions(columnName, dataType, allowNull, + nonClusteredIndex, nonClusteredIndexDirection, result.TraceId); } [Fact] @@ -515,15 +531,18 @@ public void ConfigureColumnOptionsAddsColumnSpanIdWithSpecifiedOptions() var dataType = SqlDbType.NVarChar; var allowNull = true; var nonClusteredIndex = true; + var nonClusteredIndexDirection = SqlIndexDirection.Desc; SetupConfigurationSectionMocks(); - SetupColumnSectionMock("spanId", columnName, dataType, allowNull, nonClusteredIndex); + SetupColumnSectionMock("spanId", columnName, dataType, allowNull, + nonClusteredIndex, nonClusteredIndexDirection); var sut = new MicrosoftExtensionsColumnOptionsProvider(); // Act var result = sut.ConfigureColumnOptions(new Serilog.Sinks.MSSqlServer.ColumnOptions(), _configurationSectionMock.Object); // Assert - AssertColumnSqlOptions(columnName, dataType, allowNull, nonClusteredIndex, result.SpanId); + AssertColumnSqlOptions(columnName, dataType, allowNull, + nonClusteredIndex, nonClusteredIndexDirection, result.SpanId); } [Fact] @@ -534,15 +553,18 @@ public void ConfigureColumnOptionsAddsColumnMessageWithSpecifiedOptions() var dataType = SqlDbType.Bit; var allowNull = true; var nonClusteredIndex = true; + var nonClusteredIndexDirection = SqlIndexDirection.Asc; SetupConfigurationSectionMocks(); - SetupColumnSectionMock("message", columnName, dataType, allowNull, nonClusteredIndex); + SetupColumnSectionMock("message", columnName, dataType, allowNull, + nonClusteredIndex, nonClusteredIndexDirection); var sut = new MicrosoftExtensionsColumnOptionsProvider(); // Act var result = sut.ConfigureColumnOptions(new Serilog.Sinks.MSSqlServer.ColumnOptions(), _configurationSectionMock.Object); // Assert - AssertColumnSqlOptions(columnName, dataType, allowNull, nonClusteredIndex, result.Message); + AssertColumnSqlOptions(columnName, dataType, allowNull, + nonClusteredIndex, nonClusteredIndexDirection, result.Message); } [Fact] @@ -553,15 +575,18 @@ public void ConfigureColumnOptionsAddsColumnExceptionWithSpecifiedOptions() var dataType = SqlDbType.Bit; var allowNull = true; var nonClusteredIndex = true; + var nonClusteredIndexDirection = SqlIndexDirection.Asc; SetupConfigurationSectionMocks(); - SetupColumnSectionMock("exception", columnName, dataType, allowNull, nonClusteredIndex); + SetupColumnSectionMock("exception", columnName, dataType, allowNull, + nonClusteredIndex, nonClusteredIndexDirection); var sut = new MicrosoftExtensionsColumnOptionsProvider(); // Act var result = sut.ConfigureColumnOptions(new Serilog.Sinks.MSSqlServer.ColumnOptions(), _configurationSectionMock.Object); // Assert - AssertColumnSqlOptions(columnName, dataType, allowNull, nonClusteredIndex, result.Exception); + AssertColumnSqlOptions(columnName, dataType, allowNull, + nonClusteredIndex, nonClusteredIndexDirection, result.Exception); } [Fact] @@ -572,15 +597,18 @@ public void ConfigureColumnOptionsAddsColumnMessageTemplateWithSpecifiedOptions( var dataType = SqlDbType.Bit; var allowNull = true; var nonClusteredIndex = true; + var nonClusteredIndexDirection = SqlIndexDirection.Asc; SetupConfigurationSectionMocks(); - SetupColumnSectionMock("messageTemplate", columnName, dataType, allowNull, nonClusteredIndex); + SetupColumnSectionMock("messageTemplate", columnName, dataType, allowNull, + nonClusteredIndex, nonClusteredIndexDirection); var sut = new MicrosoftExtensionsColumnOptionsProvider(); // Act var result = sut.ConfigureColumnOptions(new Serilog.Sinks.MSSqlServer.ColumnOptions(), _configurationSectionMock.Object); // Assert - AssertColumnSqlOptions(columnName, dataType, allowNull, nonClusteredIndex, result.MessageTemplate); + AssertColumnSqlOptions(columnName, dataType, allowNull, + nonClusteredIndex, nonClusteredIndexDirection, result.MessageTemplate); } [Fact] @@ -697,12 +725,19 @@ public void ConfigureColumnOptionsThrowsWhenSettingPrimaryKeyColumnNameToUndefin Assert.Throws(() => sut.ConfigureColumnOptions(columnOptions, _configurationSectionMock.Object)); } - private static void AssertColumnSqlOptions(string expectedColumnName, SqlDbType expectedDataType, bool expectedAllowNull, bool expectedNonClusteredIndex, SqlColumn actualColumn) + private static void AssertColumnSqlOptions( + string expectedColumnName, + SqlDbType expectedDataType, + bool expectedAllowNull, + bool expectedNonClusteredIndex, + SqlIndexDirection expectedNonClusteredIndexDirection, + SqlColumn actualColumn) { Assert.Equal(expectedColumnName, actualColumn.ColumnName); Assert.Equal(expectedDataType, actualColumn.DataType); Assert.Equal(expectedAllowNull, actualColumn.AllowNull); Assert.Equal(expectedNonClusteredIndex, actualColumn.NonClusteredIndex); + Assert.Equal(expectedNonClusteredIndexDirection, actualColumn.NonClusteredIndexDirection); } private void SetupConfigurationSectionMocks() @@ -723,7 +758,13 @@ private void SetupConfigurationSectionMocks() }); } - private Mock SetupColumnSectionMock(string columnSectionName, string columnName = null, SqlDbType? dataType = null, bool? allowNull = null, bool? nonClusteredIndex = null) + private Mock SetupColumnSectionMock( + string columnSectionName, + string columnName = null, + SqlDbType? dataType = null, + bool? allowNull = null, + bool? nonClusteredIndex = null, + SqlIndexDirection? nonClusteredIndexDirection = null) { var columnSectionMock = new Mock(); @@ -743,6 +784,10 @@ private Mock SetupColumnSectionMock(string columnSectionN { columnSectionMock.Setup(s => s["nonClusteredIndex"]).Returns(nonClusteredIndex.Value.ToString(CultureInfo.InvariantCulture)); } + if (nonClusteredIndexDirection != null) + { + columnSectionMock.Setup(s => s["nonClusteredIndexDirection"]).Returns(nonClusteredIndexDirection.Value.ToString()); + } _configurationSectionMock.Setup(s => s.GetSection(columnSectionName)).Returns(columnSectionMock.Object); diff --git a/test/Serilog.Sinks.MSSqlServer.Tests/Configuration/Implementations/System.Configuration/SystemConfigurationColumnOptionsProviderTests.cs b/test/Serilog.Sinks.MSSqlServer.Tests/Configuration/Implementations/System.Configuration/SystemConfigurationColumnOptionsProviderTests.cs index 3cf8eacd..079e429f 100644 --- a/test/Serilog.Sinks.MSSqlServer.Tests/Configuration/Implementations/System.Configuration/SystemConfigurationColumnOptionsProviderTests.cs +++ b/test/Serilog.Sinks.MSSqlServer.Tests/Configuration/Implementations/System.Configuration/SystemConfigurationColumnOptionsProviderTests.cs @@ -28,6 +28,8 @@ public void ConfigureColumnOptionsReadsTraceIdColumnOptions() _configurationSection.TraceId.ColumnName = columnName; _configurationSection.TraceId.AllowNull = "false"; _configurationSection.TraceId.DataType = "22"; // VarChar + _configurationSection.TraceId.NonClusteredIndex = "true"; + _configurationSection.TraceId.NonClusteredIndexDirection = "Desc"; var columnOptions = new MSSqlServer.ColumnOptions(); // Act @@ -37,6 +39,8 @@ public void ConfigureColumnOptionsReadsTraceIdColumnOptions() Assert.Equal(columnName, columnOptions.TraceId.ColumnName); Assert.False(columnOptions.TraceId.AllowNull); Assert.Equal(SqlDbType.VarChar, columnOptions.TraceId.DataType); + Assert.True(columnOptions.TraceId.NonClusteredIndex); + Assert.Equal(SqlIndexDirection.Desc, columnOptions.TraceId.NonClusteredIndexDirection); } [Fact] @@ -47,6 +51,8 @@ public void ConfigureColumnOptionsReadsSpanIdColumnOptions() _configurationSection.SpanId.ColumnName = columnName; _configurationSection.SpanId.AllowNull = "false"; _configurationSection.SpanId.DataType = "22"; // VarChar + _configurationSection.SpanId.NonClusteredIndex = "true"; + _configurationSection.SpanId.NonClusteredIndexDirection = "Desc"; var columnOptions = new MSSqlServer.ColumnOptions(); // Act @@ -56,6 +62,8 @@ public void ConfigureColumnOptionsReadsSpanIdColumnOptions() Assert.Equal(columnName, columnOptions.SpanId.ColumnName); Assert.False(columnOptions.SpanId.AllowNull); Assert.Equal(SqlDbType.VarChar, columnOptions.SpanId.DataType); + Assert.True(columnOptions.SpanId.NonClusteredIndex); + Assert.Equal(SqlIndexDirection.Desc, columnOptions.SpanId.NonClusteredIndexDirection); } [Fact] @@ -100,5 +108,30 @@ public void ConfigureColumnOptionsDefaultsAdditionalColumnsResolveHierarchicalPr additionalColumn1.Should().NotBeNull(); additionalColumn1.ResolveHierarchicalPropertyName.Should().Be(true); } + + [Fact] + public void ConfigureColumnOptionsReadsAdditionalColumnsNonClusteredIndex() + { + // Arrange + const string columnName = "AdditionalColumn1"; + var columnConfig = new ColumnConfig + { + ColumnName = columnName, + ResolveHierarchicalPropertyName = "false", + NonClusteredIndex = "true", + NonClusteredIndexDirection = "Desc" + }; + _configurationSection.Columns.Add(columnConfig); + var columnOptions = new MSSqlServer.ColumnOptions(); + + // Act + _sut.ConfigureColumnOptions(_configurationSection, columnOptions); + + // Assert + var additionalColumn1 = columnOptions.AdditionalColumns.SingleOrDefault(c => c.ColumnName == columnName); + additionalColumn1.Should().NotBeNull(); + additionalColumn1.NonClusteredIndex.Should().Be(true); + additionalColumn1.NonClusteredIndexDirection.Should().Be(SqlIndexDirection.Desc); + } } } diff --git a/test/Serilog.Sinks.MSSqlServer.Tests/Sinks/MSSqlServer/Platform/SqlCreateTableWriterTests.cs b/test/Serilog.Sinks.MSSqlServer.Tests/Sinks/MSSqlServer/Platform/SqlCreateTableWriterTests.cs index d97909fc..ae255d0f 100644 --- a/test/Serilog.Sinks.MSSqlServer.Tests/Sinks/MSSqlServer/Platform/SqlCreateTableWriterTests.cs +++ b/test/Serilog.Sinks.MSSqlServer.Tests/Sinks/MSSqlServer/Platform/SqlCreateTableWriterTests.cs @@ -160,8 +160,58 @@ public void GetSqlCreatesIdNonClusteredIndexColumnsCorrectly() + "[IndexCol2] NVARCHAR(50) NOT NULL\r\n" + " CONSTRAINT [PK_TestTableName] PRIMARY KEY CLUSTERED ([Id])\r\n" + ");\r\n" - + "CREATE NONCLUSTERED INDEX [IX1_TestTableName] ON [TestSchemaName].[TestTableName] ([IndexCol1]);\r\n" - + "CREATE NONCLUSTERED INDEX [IX2_TestTableName] ON [TestSchemaName].[TestTableName] ([IndexCol2]);\r\n" + + "CREATE NONCLUSTERED INDEX [IX1_TestTableName] ON [TestSchemaName].[TestTableName] ([IndexCol1] Asc);\r\n" + + "CREATE NONCLUSTERED INDEX [IX2_TestTableName] ON [TestSchemaName].[TestTableName] ([IndexCol2] Asc);\r\n" + + "END"; + SetupSut(); + + // Act + var result = _sut.GetSql(); + + // Assert + Assert.Contains(expectedResult, result, StringComparison.InvariantCulture); + } + + [Fact] + public void GetSqlCreatesIdNonClusteredIndexDirectionColumnCorrectly() + { + // Arrange + var sqlColumnId = new SqlColumn { AllowNull = false, ColumnName = "Id", DataType = SqlDbType.Int }; + _columnOptions = new Serilog.Sinks.MSSqlServer.ColumnOptions { PrimaryKey = sqlColumnId }; + var dataColumnId = new DataColumn(); + dataColumnId.ExtendedProperties["SqlColumn"] = sqlColumnId; + var dataColumnIndexCol1 = new DataColumn(); + var sqlColumnIndexCol1 = new SqlColumn + { + AllowNull = false, + ColumnName = "IndexCol1", + DataType = SqlDbType.NVarChar, + DataLength = 100, + NonClusteredIndex = true, + NonClusteredIndexDirection = SqlIndexDirection.Desc + }; + dataColumnIndexCol1.ExtendedProperties["SqlColumn"] = sqlColumnIndexCol1; + var dataColumnIndexCol2 = new DataColumn(); + var sqlColumnIndexCol2 = new SqlColumn + { + AllowNull = false, + ColumnName = "IndexCol2", + DataType = SqlDbType.NVarChar, + DataLength = 50, + NonClusteredIndex = true + }; + dataColumnIndexCol2.ExtendedProperties["SqlColumn"] = sqlColumnIndexCol2; + _dataTable.Columns.Add(dataColumnId); + _dataTable.Columns.Add(dataColumnIndexCol1); + _dataTable.Columns.Add(dataColumnIndexCol2); + var expectedResult = "CREATE TABLE [TestSchemaName].[TestTableName] ( \r\n" + + "[Id] INT NOT NULL,\r\n" + + "[IndexCol1] NVARCHAR(100) NOT NULL,\r\n" + + "[IndexCol2] NVARCHAR(50) NOT NULL\r\n" + + " CONSTRAINT [PK_TestTableName] PRIMARY KEY CLUSTERED ([Id])\r\n" + + ");\r\n" + + "CREATE NONCLUSTERED INDEX [IX1_TestTableName] ON [TestSchemaName].[TestTableName] ([IndexCol1] Desc);\r\n" + + "CREATE NONCLUSTERED INDEX [IX2_TestTableName] ON [TestSchemaName].[TestTableName] ([IndexCol2] Asc);\r\n" + "END"; SetupSut();