From df5993bca8e5f8222ea534b61b9fc9b62837b3fe Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Mon, 20 Jun 2022 08:07:08 +0200 Subject: [PATCH] V10: Fix build warnings umbraco.persistence (#12502) * Run code cleanup * Run dotnet format * Manual cleanup * Fix [..] to substring * Fix after merge * Update src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerBulkSqlInsertProvider.cs Co-authored-by: Paul Johnson Co-authored-by: Nikolaj Geisle Co-authored-by: Zeegaan Co-authored-by: Paul Johnson --- .../Constants.cs | 4 +- .../Dtos/ColumnInSchemaDto.cs | 31 +- .../Dtos/ConstraintPerColumnDto.cs | 19 +- .../Dtos/ConstraintPerTableDto.cs | 17 +- .../Dtos/DefaultConstraintPerColumnDto.cs | 22 +- .../Dtos/DefinedIndexDto.cs | 24 +- .../SqlServerAddMiniProfilerInterceptor.cs | 3 +- .../SqlServerAddRetryPolicyInterceptor.cs | 8 +- .../Services/BulkDataReader.cs | 2721 ++++++++--------- .../MicrosoftSqlSyntaxProviderBase.cs | 374 +-- .../Services/PocoDataDataReader.cs | 266 +- .../SqlAzureDatabaseProviderMetadata.cs | 18 +- .../SqlLocalDbDatabaseProviderMetadata.cs | 4 +- .../SqlServerBulkSqlInsertProvider.cs | 117 +- .../Services/SqlServerDatabaseCreator.cs | 91 +- .../SqlServerDatabaseProviderMetadata.cs | 4 +- .../SqlServerDistributedLockingMechanism.cs | 31 +- .../Services/SqlServerSyntaxProvider.cs | 776 ++--- .../SqlServerComposer.cs | 2 +- .../UmbracoBuilderExtensions.cs | 25 +- .../Constants.cs | 4 +- .../SqliteAddMiniProfilerInterceptor.cs | 3 +- .../SqliteAddPreferDeferredInterceptor.cs | 3 +- .../Mappers/SqliteGuidScalarMapper.cs | 2 +- .../Mappers/SqlitePocoGuidMapper.cs | 4 +- .../Services/SqliteBulkSqlInsertProvider.cs | 20 +- .../Services/SqliteDatabaseCreator.cs | 42 +- .../SqliteDatabaseProviderMetadata.cs | 16 +- .../SqliteDistributedLockingMechanism.cs | 15 +- .../Services/SqliteExceptionExtensions.cs | 7 +- ...itePreferDeferredTransactionsConnection.cs | 69 +- .../Services/SqliteSpecificMapperFactory.cs | 4 +- .../Services/SqliteSyntaxProvider.cs | 36 +- .../SqliteComposer.cs | 2 +- .../UmbracoBuilderExtensions.cs | 27 +- 35 files changed, 2434 insertions(+), 2377 deletions(-) diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs b/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs index 19ec0738c8..ae16a9735f 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs @@ -1,12 +1,12 @@ namespace Umbraco.Cms.Persistence.SqlServer; /// -/// Constants related to SQLite. +/// Constants related to SQLite. /// public static class Constants { /// - /// SQLite provider name. + /// SQLite provider name. /// public const string ProviderName = "Microsoft.Data.SqlClient"; } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ColumnInSchemaDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ColumnInSchemaDto.cs index 65bd0b5d65..0c09f87d51 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ColumnInSchemaDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ColumnInSchemaDto.cs @@ -1,25 +1,24 @@ using NPoco; -namespace Umbraco.Cms.Persistence.SqlServer.Dtos +namespace Umbraco.Cms.Persistence.SqlServer.Dtos; + +internal class ColumnInSchemaDto { - internal class ColumnInSchemaDto - { - [Column("TABLE_NAME")] - public string TableName { get; set; } = null!; + [Column("TABLE_NAME")] + public string TableName { get; set; } = null!; - [Column("COLUMN_NAME")] - public string ColumnName { get; set; } = null!; + [Column("COLUMN_NAME")] + public string ColumnName { get; set; } = null!; - [Column("ORDINAL_POSITION")] - public int OrdinalPosition { get; set; } + [Column("ORDINAL_POSITION")] + public int OrdinalPosition { get; set; } - [Column("COLUMN_DEFAULT")] - public string ColumnDefault { get; set; } = null!; + [Column("COLUMN_DEFAULT")] + public string ColumnDefault { get; set; } = null!; - [Column("IS_NULLABLE")] - public string IsNullable { get; set; } = null!; + [Column("IS_NULLABLE")] + public string IsNullable { get; set; } = null!; - [Column("DATA_TYPE")] - public string DataType { get; set; } = null!; - } + [Column("DATA_TYPE")] + public string DataType { get; set; } = null!; } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerColumnDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerColumnDto.cs index 351979570c..b0299a489d 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerColumnDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerColumnDto.cs @@ -1,16 +1,15 @@ using NPoco; -namespace Umbraco.Cms.Persistence.SqlServer.Dtos +namespace Umbraco.Cms.Persistence.SqlServer.Dtos; + +internal class ConstraintPerColumnDto { - internal class ConstraintPerColumnDto - { - [Column("TABLE_NAME")] - public string TableName { get; set; } = null!; + [Column("TABLE_NAME")] + public string TableName { get; set; } = null!; - [Column("COLUMN_NAME")] - public string ColumnName { get; set; } = null!; + [Column("COLUMN_NAME")] + public string ColumnName { get; set; } = null!; - [Column("CONSTRAINT_NAME")] - public string ConstraintName { get; set; } = null!; - } + [Column("CONSTRAINT_NAME")] + public string ConstraintName { get; set; } = null!; } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerTableDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerTableDto.cs index 3a633d4e0e..fe87ef2909 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerTableDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerTableDto.cs @@ -1,13 +1,12 @@ using NPoco; -namespace Umbraco.Cms.Persistence.SqlServer.Dtos -{ - internal class ConstraintPerTableDto - { - [Column("TABLE_NAME")] - public string TableName { get; set; } = null!; +namespace Umbraco.Cms.Persistence.SqlServer.Dtos; - [Column("CONSTRAINT_NAME")] - public string ConstraintName { get; set; } = null!; - } +internal class ConstraintPerTableDto +{ + [Column("TABLE_NAME")] + public string TableName { get; set; } = null!; + + [Column("CONSTRAINT_NAME")] + public string ConstraintName { get; set; } = null!; } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefaultConstraintPerColumnDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefaultConstraintPerColumnDto.cs index e0e1dfbe2f..a1bde415a3 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefaultConstraintPerColumnDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefaultConstraintPerColumnDto.cs @@ -1,18 +1,18 @@ using NPoco; -namespace Umbraco.Cms.Persistence.SqlServer.Dtos +namespace Umbraco.Cms.Persistence.SqlServer.Dtos; + +internal class DefaultConstraintPerColumnDto { - internal class DefaultConstraintPerColumnDto - { - [Column("TABLE_NAME")] public string TableName { get; set; } = null!; + [Column("TABLE_NAME")] + public string TableName { get; set; } = null!; - [Column("COLUMN_NAME")] - public string ColumnName { get; set; } = null!; + [Column("COLUMN_NAME")] + public string ColumnName { get; set; } = null!; - [Column("NAME")] - public string Name { get; set; } = null!; + [Column("NAME")] + public string Name { get; set; } = null!; - [Column("DEFINITION")] - public string Definition { get; set; } = null!; - } + [Column("DEFINITION")] + public string Definition { get; set; } = null!; } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefinedIndexDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefinedIndexDto.cs index e78f354e46..e85d91f1dd 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefinedIndexDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefinedIndexDto.cs @@ -1,20 +1,18 @@ using NPoco; -namespace Umbraco.Cms.Persistence.SqlServer.Dtos +namespace Umbraco.Cms.Persistence.SqlServer.Dtos; + +internal class DefinedIndexDto { - internal class DefinedIndexDto - { + [Column("TABLE_NAME")] + public string TableName { get; set; } = null!; - [Column("TABLE_NAME")] - public string TableName { get; set; } = null!; + [Column("INDEX_NAME")] + public string IndexName { get; set; } = null!; - [Column("INDEX_NAME")] - public string IndexName { get; set; } = null!; + [Column("COLUMN_NAME")] + public string ColumnName { get; set; } = null!; - [Column("COLUMN_NAME")] - public string ColumnName { get; set; } = null!; - - [Column("UNIQUE")] - public short Unique { get; set; } - } + [Column("UNIQUE")] + public short Unique { get; set; } } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddMiniProfilerInterceptor.cs b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddMiniProfilerInterceptor.cs index 7c5df6c497..43541ec2a3 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddMiniProfilerInterceptor.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddMiniProfilerInterceptor.cs @@ -1,11 +1,12 @@ using System.Data.Common; using NPoco; using StackExchange.Profiling; +using StackExchange.Profiling.Data; namespace Umbraco.Cms.Persistence.SqlServer.Interceptors; public class SqlServerAddMiniProfilerInterceptor : SqlServerConnectionInterceptor { public override DbConnection OnConnectionOpened(IDatabase database, DbConnection conn) - => new StackExchange.Profiling.Data.ProfiledDbConnection(conn, MiniProfiler.Current); + => new ProfiledDbConnection(conn, MiniProfiler.Current); } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddRetryPolicyInterceptor.cs b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddRetryPolicyInterceptor.cs index bdf5745d42..139efea85f 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddRetryPolicyInterceptor.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddRetryPolicyInterceptor.cs @@ -21,8 +21,12 @@ public class SqlServerAddRetryPolicyInterceptor : SqlServerConnectionInterceptor return conn; } - RetryPolicy? connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(_connectionStrings.CurrentValue.ConnectionString); - RetryPolicy? commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(_connectionStrings.CurrentValue.ConnectionString); + RetryPolicy? connectionRetryPolicy = + RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(_connectionStrings.CurrentValue + .ConnectionString); + RetryPolicy? commandRetryPolicy = + RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(_connectionStrings.CurrentValue + .ConnectionString); if (connectionRetryPolicy == null && commandRetryPolicy == null) { diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/BulkDataReader.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/BulkDataReader.cs index e9784ce270..d74511bf11 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/BulkDataReader.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/BulkDataReader.cs @@ -3,1503 +3,1490 @@ using System.Data; using System.Data.Common; using System.Diagnostics; using System.Globalization; +using System.Text; using Microsoft.Data.SqlClient; -namespace Umbraco.Cms.Persistence.SqlServer.Services +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +/// +/// A base implementation of that is suitable for +/// . +/// +/// +/// Borrowed from Microsoft: +/// See: https://blogs.msdn.microsoft.com/anthonybloesch/2013/01/23/bulk-loading-data-with-idatareader-and-sqlbulkcopy/ +/// This implementation is designed to be very memory efficient requiring few memory resources and to support +/// rapid transfer of data to SQL Server. +/// Subclasses should implement , , +/// , , +/// . +/// If they contain disposable resources they should override . +/// SD: Alternatively, we could have used a LinqEntityDataReader which is nicer to use but it uses quite a lot of +/// reflection and +/// I thought this would just be quicker. +/// Simple example of that: +/// https://github.com/gridsum/DataflowEx/blob/master/Gridsum.DataflowEx/Databases/BulkDataReader.cs +/// Full example of that: +/// https://github.com/matthewschrager/Repository/blob/master/Repository.EntityFramework/EntityDataReader.cs +/// So we know where to find that if we ever need it, these would convert any Linq data source to an IDataReader +/// +internal abstract class BulkDataReader : IDataReader { + #region Fields + /// - /// A base implementation of that is suitable for . + /// The containing the input row set's schema information + /// + /// requires to function correctly. + /// + private DataTable? _schemaTable = new(); + + /// + /// The mapping from the row set input to the target table's columns. + /// + private List? _columnMappings = new(); + + #endregion + + #region Subclass utility routines + + /// + /// The mapping from the row set input to the target table's columns. /// /// - /// - /// Borrowed from Microsoft: - /// See: https://blogs.msdn.microsoft.com/anthonybloesch/2013/01/23/bulk-loading-data-with-idatareader-and-sqlbulkcopy/ - /// - /// This implementation is designed to be very memory efficient requiring few memory resources and to support - /// rapid transfer of data to SQL Server. - /// - /// Subclasses should implement , , - /// , , . - /// If they contain disposable resources they should override . - /// - /// SD: Alternatively, we could have used a LinqEntityDataReader which is nicer to use but it uses quite a lot of reflection and - /// I thought this would just be quicker. - /// Simple example of that: https://github.com/gridsum/DataflowEx/blob/master/Gridsum.DataflowEx/Databases/BulkDataReader.cs - /// Full example of that: https://github.com/matthewschrager/Repository/blob/master/Repository.EntityFramework/EntityDataReader.cs - /// So we know where to find that if we ever need it, these would convert any Linq data source to an IDataReader - /// + /// If necessary, will be called to initialize the mapping. /// - internal abstract class BulkDataReader : IDataReader + public ReadOnlyCollection ColumnMappings { - - #region Fields - - /// - /// The containing the input row set's schema information - /// requires to function correctly. - /// - private DataTable? _schemaTable = new DataTable(); - - /// - /// The mapping from the row set input to the target table's columns. - /// - private List? _columnMappings = new List(); - - #endregion - - #region Subclass utility routines - - /// - /// The mapping from the row set input to the target table's columns. - /// - /// - /// If necessary, will be called to initialize the mapping. - /// - public ReadOnlyCollection ColumnMappings + get { - get + if (_columnMappings?.Count == 0) { - if (this._columnMappings?.Count == 0) + // Need to add the column definitions and mappings. + AddSchemaTableRows(); + + if (_columnMappings.Count == 0) { - // Need to add the column definitions and mappings. - AddSchemaTableRows(); - - if (this._columnMappings.Count == 0) - { - throw new InvalidOperationException("AddSchemaTableRows did not add rows."); - } - - Debug.Assert(this._schemaTable?.Rows.Count == FieldCount); + throw new InvalidOperationException("AddSchemaTableRows did not add rows."); } - return new ReadOnlyCollection(_columnMappings!); + Debug.Assert(_schemaTable?.Rows.Count == FieldCount); + } + + return new ReadOnlyCollection(_columnMappings!); + } + } + + /// + /// The name of the input row set's schema. + /// + /// + /// This may be different from the target schema but usually they are identical. + /// + protected abstract string SchemaName + { + get; + } + + /// + /// The name of the input row set's table. + /// + /// + /// This may be different from the target table but usually they are identical. + /// + protected abstract string TableName + { + get; + } + + /// + /// Adds the input row set's schema to the object. + /// + /// + /// Call + /// + /// to do this for each row. + /// + /// + protected abstract void AddSchemaTableRows(); + + /// + /// For each , the optional columns that may have values. + /// + /// + /// This is used for checking the parameters of + /// + /// . + /// + /// + private static readonly Dictionary> AllowedOptionalColumnCombinations = new() + { + {SqlDbType.BigInt, new List()}, + {SqlDbType.Binary, new List {SchemaTableColumn.ColumnSize}}, + {SqlDbType.Bit, new List()}, + {SqlDbType.Char, new List {SchemaTableColumn.ColumnSize}}, + {SqlDbType.Date, new List()}, + {SqlDbType.DateTime, new List()}, + {SqlDbType.DateTime2, new List {SchemaTableColumn.NumericPrecision}}, + {SqlDbType.DateTimeOffset, new List {SchemaTableColumn.NumericPrecision}}, + {SqlDbType.Decimal, new List {SchemaTableColumn.NumericPrecision, SchemaTableColumn.NumericScale}}, + {SqlDbType.Float, new List {SchemaTableColumn.NumericPrecision, SchemaTableColumn.NumericScale}}, + {SqlDbType.Image, new List()}, + {SqlDbType.Int, new List()}, + {SqlDbType.Money, new List()}, + {SqlDbType.NChar, new List {SchemaTableColumn.ColumnSize}}, + {SqlDbType.NText, new List()}, + {SqlDbType.NVarChar, new List {SchemaTableColumn.ColumnSize}}, + {SqlDbType.Real, new List()}, + {SqlDbType.SmallDateTime, new List()}, + {SqlDbType.SmallInt, new List()}, + {SqlDbType.SmallMoney, new List()}, + {SqlDbType.Structured, new List()}, + {SqlDbType.Text, new List()}, + {SqlDbType.Time, new List {SchemaTableColumn.NumericPrecision}}, + {SqlDbType.Timestamp, new List()}, + {SqlDbType.TinyInt, new List()}, + {SqlDbType.Udt, new List {DataTypeNameSchemaColumn}}, + {SqlDbType.UniqueIdentifier, new List()}, + {SqlDbType.VarBinary, new List {SchemaTableColumn.ColumnSize}}, + {SqlDbType.VarChar, new List {SchemaTableColumn.ColumnSize}}, + {SqlDbType.Variant, new List()}, + { + SqlDbType.Xml, + new List + { + XmlSchemaCollectionDatabaseSchemaColumn, + XmlSchemaCollectionOwningSchemaSchemaColumn, + XmlSchemaCollectionNameSchemaColumn } } + }; - /// - /// The name of the input row set's schema. - /// - /// - /// This may be different from the target schema but usually they are identical. - /// - protected abstract string SchemaName + /// + /// A helper method to support . + /// + /// + /// This methods does extensive argument checks. These errors will cause hard to diagnose exceptions in latter + /// processing so it is important to detect them when they can be easily associated with the code defect. + /// + /// + /// The combination of values for the parameters is not supported. + /// + /// + /// A null value for the parameter is not supported. + /// + /// + /// The name of the column. + /// + /// + /// The size of the column which may be null if not applicable. + /// + /// + /// The precision of the column which may be null if not applicable. + /// + /// + /// The scale of the column which may be null if not applicable. + /// + /// + /// Are the column values unique (i.e. never duplicated)? + /// + /// + /// Is the column part of the primary key? + /// + /// + /// Is the column nullable (i.e. optional)? + /// + /// + /// The corresponding . + /// + /// + /// The schema name of the UDT. + /// + /// + /// The type name of the UDT. + /// + /// + /// For XML columns the schema collection's database name. Otherwise, null. + /// + /// + /// For XML columns the schema collection's schema name. Otherwise, null. + /// + /// + /// For XML columns the schema collection's name. Otherwise, null. + /// + /// + protected void AddSchemaTableRow( + string columnName, + int? columnSize, + short? numericPrecision, + short? numericScale, + bool isUnique, + bool isKey, + bool allowDbNull, + SqlDbType providerType, + string? udtSchema, + string? udtType, + string? xmlSchemaCollectionDatabase, + string? xmlSchemaCollectionOwningSchema, + string? xmlSchemaCollectionName) + { + if (string.IsNullOrEmpty(columnName)) { - get; + throw new ArgumentException("columnName must be a nonempty string."); } - /// - /// The name of the input row set's table. - /// - /// - /// This may be different from the target table but usually they are identical. - /// - protected abstract string TableName + if (columnSize.HasValue && columnSize.Value <= 0) { - get; + throw new ArgumentOutOfRangeException("columnSize"); } - /// - /// Adds the input row set's schema to the object. - /// - /// - /// Call - /// to do this for each row. - /// - /// - protected abstract void AddSchemaTableRows(); - - /// - /// For each , the optional columns that may have values. - /// - /// - /// This is used for checking the parameters of . - /// - /// - private static readonly Dictionary> AllowedOptionalColumnCombinations = new Dictionary> + if (numericPrecision.HasValue && numericPrecision.Value <= 0) { - { SqlDbType.BigInt, new List { } }, - { SqlDbType.Binary, new List { SchemaTableColumn.ColumnSize } }, - { SqlDbType.Bit, new List { } }, - { SqlDbType.Char, new List { SchemaTableColumn.ColumnSize } }, - { SqlDbType.Date, new List { } }, - { SqlDbType.DateTime, new List { } }, - { SqlDbType.DateTime2, new List { SchemaTableColumn.NumericPrecision } }, - { SqlDbType.DateTimeOffset, new List { SchemaTableColumn.NumericPrecision } }, - { SqlDbType.Decimal, new List { SchemaTableColumn.NumericPrecision, SchemaTableColumn.NumericScale } }, - { SqlDbType.Float, new List { SchemaTableColumn.NumericPrecision, SchemaTableColumn.NumericScale } }, - { SqlDbType.Image, new List { } }, - { SqlDbType.Int, new List { } }, - { SqlDbType.Money, new List { } }, - { SqlDbType.NChar, new List { SchemaTableColumn.ColumnSize } }, - { SqlDbType.NText, new List { } }, - { SqlDbType.NVarChar, new List { SchemaTableColumn.ColumnSize } }, - { SqlDbType.Real, new List { } }, - { SqlDbType.SmallDateTime, new List { } }, - { SqlDbType.SmallInt, new List { } }, - { SqlDbType.SmallMoney, new List { } }, - { SqlDbType.Structured, new List { } }, - { SqlDbType.Text, new List { } }, - { SqlDbType.Time, new List { SchemaTableColumn.NumericPrecision } }, - { SqlDbType.Timestamp, new List { } }, - { SqlDbType.TinyInt, new List { } }, - { SqlDbType.Udt, new List { BulkDataReader.DataTypeNameSchemaColumn } }, - { SqlDbType.UniqueIdentifier, new List { } }, - { SqlDbType.VarBinary, new List { SchemaTableColumn.ColumnSize } }, - { SqlDbType.VarChar, new List { SchemaTableColumn.ColumnSize } }, - { SqlDbType.Variant, new List { } }, - { SqlDbType.Xml, new List { BulkDataReader.XmlSchemaCollectionDatabaseSchemaColumn, BulkDataReader.XmlSchemaCollectionOwningSchemaSchemaColumn, BulkDataReader.XmlSchemaCollectionNameSchemaColumn } } - }; + throw new ArgumentOutOfRangeException("numericPrecision"); + } - /// - /// A helper method to support . - /// - /// - /// This methods does extensive argument checks. These errors will cause hard to diagnose exceptions in latter - /// processing so it is important to detect them when they can be easily associated with the code defect. - /// - /// - /// The combination of values for the parameters is not supported. - /// - /// - /// A null value for the parameter is not supported. - /// - /// - /// The name of the column. - /// - /// - /// The size of the column which may be null if not applicable. - /// - /// - /// The precision of the column which may be null if not applicable. - /// - /// - /// The scale of the column which may be null if not applicable. - /// - /// - /// Are the column values unique (i.e. never duplicated)? - /// - /// - /// Is the column part of the primary key? - /// - /// - /// Is the column nullable (i.e. optional)? - /// - /// - /// The corresponding . - /// - /// - /// The schema name of the UDT. - /// - /// - /// The type name of the UDT. - /// - /// - /// For XML columns the schema collection's database name. Otherwise, null. - /// - /// - /// For XML columns the schema collection's schema name. Otherwise, null. - /// - /// - /// For XML columns the schema collection's name. Otherwise, null. - /// - /// - protected void AddSchemaTableRow(string columnName, - int? columnSize, - short? numericPrecision, - short? numericScale, - bool isUnique, - bool isKey, - bool allowDbNull, - SqlDbType providerType, - string? udtSchema, - string? udtType, - string? xmlSchemaCollectionDatabase, - string? xmlSchemaCollectionOwningSchema, - string? xmlSchemaCollectionName) + if (numericScale.HasValue && numericScale.Value < 0) { - if (string.IsNullOrEmpty(columnName)) - { - throw new ArgumentException("columnName must be a nonempty string."); - } - else if (columnSize.HasValue && columnSize.Value <= 0) - { - throw new ArgumentOutOfRangeException("columnSize"); - } - else if (numericPrecision.HasValue && numericPrecision.Value <= 0) - { - throw new ArgumentOutOfRangeException("numericPrecision"); - } - else if (numericScale.HasValue && numericScale.Value < 0) - { - throw new ArgumentOutOfRangeException("columnSize"); - } + throw new ArgumentOutOfRangeException("columnSize"); + } - List? allowedOptionalColumnList; - if (BulkDataReader.AllowedOptionalColumnCombinations.TryGetValue(providerType, out allowedOptionalColumnList)) + if (AllowedOptionalColumnCombinations.TryGetValue(providerType, out List? allowedOptionalColumnList)) + { + if ((columnSize.HasValue && !allowedOptionalColumnList.Contains(SchemaTableColumn.ColumnSize)) || + (numericPrecision.HasValue && + !allowedOptionalColumnList.Contains(SchemaTableColumn.NumericPrecision)) || + (numericScale.HasValue && !allowedOptionalColumnList.Contains(SchemaTableColumn.NumericScale)) || + (udtSchema != null && !allowedOptionalColumnList.Contains(DataTypeNameSchemaColumn)) || + (udtType != null && !allowedOptionalColumnList.Contains(DataTypeNameSchemaColumn)) || + (xmlSchemaCollectionDatabase != null && + !allowedOptionalColumnList.Contains(XmlSchemaCollectionDatabaseSchemaColumn)) || + (xmlSchemaCollectionOwningSchema != null && + !allowedOptionalColumnList.Contains(XmlSchemaCollectionOwningSchemaSchemaColumn)) || + (xmlSchemaCollectionName != null && + !allowedOptionalColumnList.Contains(XmlSchemaCollectionNameSchemaColumn))) { - if ((columnSize.HasValue && !allowedOptionalColumnList.Contains(SchemaTableColumn.ColumnSize)) || - (numericPrecision.HasValue && !allowedOptionalColumnList.Contains(SchemaTableColumn.NumericPrecision)) || - (numericScale.HasValue && !allowedOptionalColumnList.Contains(SchemaTableColumn.NumericScale)) || - (udtSchema != null && !allowedOptionalColumnList.Contains(BulkDataReader.DataTypeNameSchemaColumn)) || - (udtType != null && !allowedOptionalColumnList.Contains(BulkDataReader.DataTypeNameSchemaColumn)) || - (xmlSchemaCollectionDatabase != null && !allowedOptionalColumnList.Contains(BulkDataReader.XmlSchemaCollectionDatabaseSchemaColumn)) || - (xmlSchemaCollectionOwningSchema != null && !allowedOptionalColumnList.Contains(BulkDataReader.XmlSchemaCollectionOwningSchemaSchemaColumn)) || - (xmlSchemaCollectionName != null && !allowedOptionalColumnList.Contains(BulkDataReader.XmlSchemaCollectionNameSchemaColumn))) + throw new ArgumentException("Columns are set that are incompatible with the value of providerType."); + } + } + else + { + throw new ArgumentException("providerType is unsupported."); + } + + Type dataType; // Corresponding CLR type. + string dataTypeName; // Corresponding SQL Server type. + var isLong = false; // Is the column a large value column (e.g. nvarchar(max))? + + switch (providerType) + { + case SqlDbType.BigInt: + dataType = typeof(long); + dataTypeName = "bigint"; + break; + + case SqlDbType.Binary: + dataType = typeof(byte[]); + + if (!columnSize.HasValue) { - throw new ArgumentException("Columns are set that are incompatible with the value of providerType."); + throw new ArgumentException("columnSize must be specified for \"binary\" type columns."); } - } - else - { - throw new ArgumentException("providerType is unsupported."); - } - Type dataType; // Corresponding CLR type. - string dataTypeName; // Corresponding SQL Server type. - bool isLong = false; // Is the column a large value column (e.g. nvarchar(max))? + if (columnSize > 8000) + { + throw new ArgumentOutOfRangeException("columnSize"); + } - switch (providerType) - { - case SqlDbType.BigInt: - dataType = typeof(long); - dataTypeName = "bigint"; - break; + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "binary({0})", + columnSize.Value); + break; - case SqlDbType.Binary: - dataType = typeof(byte[]); + case SqlDbType.Bit: + dataType = typeof(bool); + dataTypeName = "bit"; + break; - if (!columnSize.HasValue) - { - throw new ArgumentException("columnSize must be specified for \"binary\" type columns."); - } - else if (columnSize > 8000) - { - throw new ArgumentOutOfRangeException("columnSize"); - } + case SqlDbType.Char: + dataType = typeof(string); - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "binary({0})", - columnSize.Value); - break; + if (!columnSize.HasValue) + { + throw new ArgumentException("columnSize must be specified for \"char\" type columns."); + } - case SqlDbType.Bit: - dataType = typeof(bool); - dataTypeName = "bit"; - break; + if (columnSize > 8000) + { + throw new ArgumentOutOfRangeException("columnSize"); + } - case SqlDbType.Char: - dataType = typeof(string); + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "char({0})", + columnSize.Value); + break; - if (!columnSize.HasValue) - { - throw new ArgumentException("columnSize must be specified for \"char\" type columns."); - } - else if (columnSize > 8000) - { - throw new ArgumentOutOfRangeException("columnSize"); - } + case SqlDbType.Date: + dataType = typeof(DateTime); + dataTypeName = "date"; + break; - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "char({0})", - columnSize.Value); - break; + case SqlDbType.DateTime: + dataType = typeof(DateTime); + dataTypeName = "datetime"; + break; - case SqlDbType.Date: - dataType = typeof(DateTime); - dataTypeName = "date"; - break; + case SqlDbType.DateTime2: + dataType = typeof(DateTime); - case SqlDbType.DateTime: - dataType = typeof(DateTime); - dataTypeName = "datetime"; - break; - - case SqlDbType.DateTime2: - dataType = typeof(DateTime); - - if (numericPrecision.HasValue) - { - if (numericPrecision.Value > 7) - { - throw new ArgumentOutOfRangeException("numericPrecision"); - } - - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "datetime2({0})", - numericPrecision.Value); - } - else - { - dataTypeName = "datetime2"; - } - break; - - case SqlDbType.DateTimeOffset: - dataType = typeof(DateTimeOffset); - - if (numericPrecision.HasValue) - { - if (numericPrecision.Value > 7) - { - throw new ArgumentOutOfRangeException("numericPrecision"); - } - - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "datetimeoffset({0})", - numericPrecision.Value); - } - else - { - dataTypeName = "datetimeoffset"; - } - break; - - case SqlDbType.Decimal: - dataType = typeof(decimal); - - if (!numericPrecision.HasValue || !numericScale.HasValue) - { - throw new ArgumentException("numericPrecision and numericScale must be specified for \"decimal\" type columns."); - } - else if (numericPrecision > 38) - { - throw new ArgumentOutOfRangeException("numericPrecision"); - } - else if (numericScale.Value > numericPrecision.Value) - { - throw new ArgumentException("numericScale must not be larger than numericPrecision for \"decimal\" type columns."); - } - - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "decimal({0}, {1})", - numericPrecision.Value, - numericScale.Value); - break; - - case SqlDbType.Float: - dataType = typeof(double); - - if (!numericPrecision.HasValue) - { - throw new ArgumentException("numericPrecision must be specified for \"float\" type columns"); - } - else if (numericPrecision > 53) + if (numericPrecision.HasValue) + { + if (numericPrecision.Value > 7) { throw new ArgumentOutOfRangeException("numericPrecision"); } - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "float({0})", - numericPrecision.Value); - break; + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "datetime2({0})", + numericPrecision.Value); + } + else + { + dataTypeName = "datetime2"; + } - case SqlDbType.Image: - dataType = typeof(byte[]); - dataTypeName = "image"; - break; + break; - case SqlDbType.Int: - dataType = typeof(int); - dataTypeName = "int"; - break; + case SqlDbType.DateTimeOffset: + dataType = typeof(DateTimeOffset); - case SqlDbType.Money: - dataType = typeof(decimal); - dataTypeName = "money"; - break; - - case SqlDbType.NChar: - dataType = typeof(string); - - if (!columnSize.HasValue) + if (numericPrecision.HasValue) + { + if (numericPrecision.Value > 7) { - throw new ArgumentException("columnSize must be specified for \"nchar\" type columns"); + throw new ArgumentOutOfRangeException("numericPrecision"); } - else if (columnSize > 4000) + + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "datetimeoffset({0})", + numericPrecision.Value); + } + else + { + dataTypeName = "datetimeoffset"; + } + + break; + + case SqlDbType.Decimal: + dataType = typeof(decimal); + + if (!numericPrecision.HasValue || !numericScale.HasValue) + { + throw new ArgumentException( + "numericPrecision and numericScale must be specified for \"decimal\" type columns."); + } + + if (numericPrecision > 38) + { + throw new ArgumentOutOfRangeException("numericPrecision"); + } + + if (numericScale.Value > numericPrecision.Value) + { + throw new ArgumentException( + "numericScale must not be larger than numericPrecision for \"decimal\" type columns."); + } + + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "decimal({0}, {1})", + numericPrecision.Value, + numericScale.Value); + break; + + case SqlDbType.Float: + dataType = typeof(double); + + if (!numericPrecision.HasValue) + { + throw new ArgumentException("numericPrecision must be specified for \"float\" type columns"); + } + + if (numericPrecision > 53) + { + throw new ArgumentOutOfRangeException("numericPrecision"); + } + + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "float({0})", + numericPrecision.Value); + break; + + case SqlDbType.Image: + dataType = typeof(byte[]); + dataTypeName = "image"; + break; + + case SqlDbType.Int: + dataType = typeof(int); + dataTypeName = "int"; + break; + + case SqlDbType.Money: + dataType = typeof(decimal); + dataTypeName = "money"; + break; + + case SqlDbType.NChar: + dataType = typeof(string); + + if (!columnSize.HasValue) + { + throw new ArgumentException("columnSize must be specified for \"nchar\" type columns"); + } + + if (columnSize > 4000) + { + throw new ArgumentOutOfRangeException("columnSize"); + } + + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "nchar({0})", + columnSize.Value); + break; + + case SqlDbType.NText: + dataType = typeof(string); + dataTypeName = "ntext"; + break; + + case SqlDbType.NVarChar: + dataType = typeof(string); + + if (columnSize.HasValue) + { + if (columnSize > 4000) { throw new ArgumentOutOfRangeException("columnSize"); } - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "nchar({0})", - columnSize.Value); - break; + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "nvarchar({0})", + columnSize.Value); + } + else + { + isLong = true; - case SqlDbType.NText: - dataType = typeof(string); - dataTypeName = "ntext"; - break; + dataTypeName = "nvarchar(max)"; + } - case SqlDbType.NVarChar: - dataType = typeof(string); + break; - if (columnSize.HasValue) + case SqlDbType.Real: + dataType = typeof(float); + dataTypeName = "real"; + break; + + case SqlDbType.SmallDateTime: + dataType = typeof(DateTime); + dataTypeName = "smalldatetime"; + break; + + case SqlDbType.SmallInt: + dataType = typeof(short); + dataTypeName = "smallint"; + break; + + case SqlDbType.SmallMoney: + dataType = typeof(decimal); + dataTypeName = "smallmoney"; + break; + + // SqlDbType.Structured not supported because it related to nested rowsets. + + case SqlDbType.Text: + dataType = typeof(string); + dataTypeName = "text"; + break; + + case SqlDbType.Time: + dataType = typeof(TimeSpan); + + if (numericPrecision.HasValue) + { + if (numericPrecision > 7) { - if (columnSize > 4000) - { - throw new ArgumentOutOfRangeException("columnSize"); - } - - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "nvarchar({0})", - columnSize.Value); - } - else - { - isLong = true; - - dataTypeName = "nvarchar(max)"; - } - break; - - case SqlDbType.Real: - dataType = typeof(float); - dataTypeName = "real"; - break; - - case SqlDbType.SmallDateTime: - dataType = typeof(DateTime); - dataTypeName = "smalldatetime"; - break; - - case SqlDbType.SmallInt: - dataType = typeof(short); - dataTypeName = "smallint"; - break; - - case SqlDbType.SmallMoney: - dataType = typeof(decimal); - dataTypeName = "smallmoney"; - break; - - // SqlDbType.Structured not supported because it related to nested rowsets. - - case SqlDbType.Text: - dataType = typeof(string); - dataTypeName = "text"; - break; - - case SqlDbType.Time: - dataType = typeof(TimeSpan); - - if (numericPrecision.HasValue) - { - if (numericPrecision > 7) - { - throw new ArgumentOutOfRangeException("numericPrecision"); - } - - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "time({0})", - numericPrecision.Value); - } - else - { - dataTypeName = "time"; - } - break; - - - // SqlDbType.Timestamp not supported because rowversions are not settable. - - case SqlDbType.TinyInt: - dataType = typeof(byte); - dataTypeName = "tinyint"; - break; - - case SqlDbType.Udt: - if (string.IsNullOrEmpty(udtSchema)) - { - throw new ArgumentException("udtSchema must be nonnull and nonempty for \"UDT\" columns."); - } - else if (string.IsNullOrEmpty(udtType)) - { - throw new ArgumentException("udtType must be nonnull and nonempty for \"UDT\" columns."); + throw new ArgumentOutOfRangeException("numericPrecision"); } - dataType = typeof(object); - using (SqlCommandBuilder commandBuilder = new SqlCommandBuilder()) + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "time({0})", + numericPrecision.Value); + } + else + { + dataTypeName = "time"; + } + + break; + + + // SqlDbType.Timestamp not supported because rowversions are not settable. + + case SqlDbType.TinyInt: + dataType = typeof(byte); + dataTypeName = "tinyint"; + break; + + case SqlDbType.Udt: + if (string.IsNullOrEmpty(udtSchema)) + { + throw new ArgumentException("udtSchema must be nonnull and nonempty for \"UDT\" columns."); + } + + if (string.IsNullOrEmpty(udtType)) + { + throw new ArgumentException("udtType must be nonnull and nonempty for \"UDT\" columns."); + } + + dataType = typeof(object); + using (var commandBuilder = new SqlCommandBuilder()) + { + dataTypeName = commandBuilder.QuoteIdentifier(udtSchema) + "." + + commandBuilder.QuoteIdentifier(udtType); + } + + break; + + case SqlDbType.UniqueIdentifier: + dataType = typeof(Guid); + dataTypeName = "uniqueidentifier"; + break; + + case SqlDbType.VarBinary: + dataType = typeof(byte[]); + + if (columnSize.HasValue) + { + if (columnSize > 8000) { - dataTypeName = commandBuilder.QuoteIdentifier(udtSchema) + "." + commandBuilder.QuoteIdentifier(udtType); + throw new ArgumentOutOfRangeException("columnSize"); } - break; - case SqlDbType.UniqueIdentifier: - dataType = typeof(Guid); - dataTypeName = "uniqueidentifier"; - break; + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "varbinary({0})", + columnSize.Value); + } + else + { + isLong = true; - case SqlDbType.VarBinary: - dataType = typeof(byte[]); + dataTypeName = "varbinary(max)"; + } - if (columnSize.HasValue) + break; + + case SqlDbType.VarChar: + dataType = typeof(string); + + if (columnSize.HasValue) + { + if (columnSize > 8000) { - if (columnSize > 8000) - { - throw new ArgumentOutOfRangeException("columnSize"); - } - - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "varbinary({0})", - columnSize.Value); + throw new ArgumentOutOfRangeException("columnSize"); } - else + + dataTypeName = string.Format( + CultureInfo.InvariantCulture, + "varchar({0})", + columnSize.Value); + } + else + { + isLong = true; + + dataTypeName = "varchar(max)"; + } + + break; + + case SqlDbType.Variant: + dataType = typeof(object); + dataTypeName = "sql_variant"; + break; + + case SqlDbType.Xml: + dataType = typeof(string); + + if (xmlSchemaCollectionName == null) + { + if (xmlSchemaCollectionDatabase != null || xmlSchemaCollectionOwningSchema != null) { - isLong = true; - - dataTypeName = "varbinary(max)"; + throw new ArgumentException( + "xmlSchemaCollectionDatabase and xmlSchemaCollectionOwningSchema must be null if xmlSchemaCollectionName is null for \"xml\" columns."); } - break; - case SqlDbType.VarChar: - dataType = typeof(string); - - if (columnSize.HasValue) + dataTypeName = "xml"; + } + else + { + if (xmlSchemaCollectionName.Length == 0) { - if (columnSize > 8000) - { - throw new ArgumentOutOfRangeException("columnSize"); - } - - dataTypeName = string.Format(CultureInfo.InvariantCulture, - "varchar({0})", - columnSize.Value); + throw new ArgumentException( + "xmlSchemaCollectionName must be nonempty or null for \"xml\" columns."); } - else + + if (xmlSchemaCollectionDatabase != null && + xmlSchemaCollectionDatabase.Length == 0) { - isLong = true; - - dataTypeName = "varchar(max)"; + throw new ArgumentException( + "xmlSchemaCollectionDatabase must be null or nonempty for \"xml\" columns."); } - break; - case SqlDbType.Variant: - dataType = typeof(object); - dataTypeName = "sql_variant"; - break; - - case SqlDbType.Xml: - dataType = typeof(string); - - if (xmlSchemaCollectionName == null) + if (xmlSchemaCollectionOwningSchema != null && + xmlSchemaCollectionOwningSchema.Length == 0) { - if (xmlSchemaCollectionDatabase != null || xmlSchemaCollectionOwningSchema != null) - { - throw new ArgumentException("xmlSchemaCollectionDatabase and xmlSchemaCollectionOwningSchema must be null if xmlSchemaCollectionName is null for \"xml\" columns."); - } - - dataTypeName = "xml"; + throw new ArgumentException( + "xmlSchemaCollectionOwningSchema must be null or nonempty for \"xml\" columns."); } - else + + var schemaCollection = new StringBuilder("xml("); + + if (xmlSchemaCollectionDatabase != null) { - if (xmlSchemaCollectionName.Length == 0) - { - throw new ArgumentException("xmlSchemaCollectionName must be nonempty or null for \"xml\" columns."); - } - else if (xmlSchemaCollectionDatabase != null && - xmlSchemaCollectionDatabase.Length == 0) - { - throw new ArgumentException("xmlSchemaCollectionDatabase must be null or nonempty for \"xml\" columns."); - } - else if (xmlSchemaCollectionOwningSchema != null && - xmlSchemaCollectionOwningSchema.Length == 0) - { - throw new ArgumentException("xmlSchemaCollectionOwningSchema must be null or nonempty for \"xml\" columns."); - } - - System.Text.StringBuilder schemaCollection = new System.Text.StringBuilder("xml("); - - if (xmlSchemaCollectionDatabase != null) - { - schemaCollection.Append("[" + xmlSchemaCollectionDatabase + "]"); - } - - schemaCollection.Append("[" + (xmlSchemaCollectionOwningSchema == null ? SchemaName : xmlSchemaCollectionOwningSchema) + "]"); - schemaCollection.Append("[" + xmlSchemaCollectionName + "]"); - - dataTypeName = schemaCollection.ToString(); + schemaCollection.Append("[" + xmlSchemaCollectionDatabase + "]"); } - break; - default: - throw new ArgumentOutOfRangeException("providerType"); + schemaCollection.Append("[" + (xmlSchemaCollectionOwningSchema ?? SchemaName) + "]"); + schemaCollection.Append("[" + xmlSchemaCollectionName + "]"); - } + dataTypeName = schemaCollection.ToString(); + } - this._schemaTable?.Rows.Add(columnName, - _schemaTable.Rows.Count, - columnSize, - numericPrecision, - numericScale, - isUnique, - isKey, - "TraceServer", - "TraceWarehouse", - columnName, - SchemaName, - TableName, - dataType, - allowDbNull, - providerType, - false, // isAliased - false, // isExpression - false, // isIdentity, - false, // isAutoIncrement, - false, // isRowVersion, - false, // isHidden, - isLong, - true, // isReadOnly, - dataType, - dataTypeName, - xmlSchemaCollectionDatabase, - xmlSchemaCollectionOwningSchema, - xmlSchemaCollectionName); + break; - this._columnMappings?.Add(new SqlBulkCopyColumnMapping(columnName, columnName)); + default: + throw new ArgumentOutOfRangeException("providerType"); } - #endregion + _schemaTable?.Rows.Add( + columnName, + _schemaTable.Rows.Count, + columnSize, + numericPrecision, + numericScale, + isUnique, + isKey, + "TraceServer", + "TraceWarehouse", + columnName, + SchemaName, + TableName, + dataType, + allowDbNull, + providerType, + false, // isAliased + false, // isExpression + false, // isIdentity, + false, // isAutoIncrement, + false, // isRowVersion, + false, // isHidden, + isLong, + true, // isReadOnly, + dataType, + dataTypeName, + xmlSchemaCollectionDatabase, + xmlSchemaCollectionOwningSchema, + xmlSchemaCollectionName); - #region Constructors + _columnMappings?.Add(new SqlBulkCopyColumnMapping(columnName, columnName)); + } - private const string IsIdentitySchemaColumn = "IsIdentity"; + #endregion - private const string DataTypeNameSchemaColumn = "DataTypeName"; + #region Constructors - private const string XmlSchemaCollectionDatabaseSchemaColumn = "XmlSchemaCollectionDatabase"; + private const string IsIdentitySchemaColumn = "IsIdentity"; - private const string XmlSchemaCollectionOwningSchemaSchemaColumn = "XmlSchemaCollectionOwningSchema"; + private const string DataTypeNameSchemaColumn = "DataTypeName"; - private const string XmlSchemaCollectionNameSchemaColumn = "XmlSchemaCollectionName"; + private const string XmlSchemaCollectionDatabaseSchemaColumn = "XmlSchemaCollectionDatabase"; - /// - /// Constructor. - /// - protected BulkDataReader() + private const string XmlSchemaCollectionOwningSchemaSchemaColumn = "XmlSchemaCollectionOwningSchema"; + + private const string XmlSchemaCollectionNameSchemaColumn = "XmlSchemaCollectionName"; + + /// + /// Constructor. + /// + protected BulkDataReader() + { + _schemaTable.Locale = CultureInfo.InvariantCulture; + + DataColumnCollection columns = _schemaTable.Columns; + + columns.Add(SchemaTableColumn.ColumnName, typeof(string)); + columns.Add(SchemaTableColumn.ColumnOrdinal, typeof(int)); + columns.Add(SchemaTableColumn.ColumnSize, typeof(int)); + columns.Add(SchemaTableColumn.NumericPrecision, typeof(short)); + columns.Add(SchemaTableColumn.NumericScale, typeof(short)); + columns.Add(SchemaTableColumn.IsUnique, typeof(bool)); + columns.Add(SchemaTableColumn.IsKey, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.BaseServerName, typeof(string)); + columns.Add(SchemaTableOptionalColumn.BaseCatalogName, typeof(string)); + columns.Add(SchemaTableColumn.BaseColumnName, typeof(string)); + columns.Add(SchemaTableColumn.BaseSchemaName, typeof(string)); + columns.Add(SchemaTableColumn.BaseTableName, typeof(string)); + columns.Add(SchemaTableColumn.DataType, typeof(Type)); + columns.Add(SchemaTableColumn.AllowDBNull, typeof(bool)); + columns.Add(SchemaTableColumn.ProviderType, typeof(int)); + columns.Add(SchemaTableColumn.IsAliased, typeof(bool)); + columns.Add(SchemaTableColumn.IsExpression, typeof(bool)); + columns.Add(IsIdentitySchemaColumn, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.IsAutoIncrement, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.IsRowVersion, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.IsHidden, typeof(bool)); + columns.Add(SchemaTableColumn.IsLong, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.IsReadOnly, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.ProviderSpecificDataType, typeof(Type)); + columns.Add(DataTypeNameSchemaColumn, typeof(string)); + columns.Add(XmlSchemaCollectionDatabaseSchemaColumn, typeof(string)); + columns.Add(XmlSchemaCollectionOwningSchemaSchemaColumn, typeof(string)); + columns.Add(XmlSchemaCollectionNameSchemaColumn, typeof(string)); + } + + #endregion + + #region IDataReader + + /// + /// Gets a value indicating the depth of nesting for the current row. (Inherited from .) + /// + /// + /// does not support nested result sets so this method always returns 0. + /// + /// + public int Depth => 0; + + /// + /// Gets the number of columns in the current row. (Inherited from .) + /// + /// + public int FieldCount => GetSchemaTable().Rows.Count; + + /// + /// Is the bulk copy process open? + /// + private bool _isOpen = true; + + /// + /// Gets a value indicating whether the data reader is closed. (Inherited from .) + /// + /// + public bool IsClosed => !_isOpen; + + /// + /// Gets the column located at the specified index. (Inherited from .) + /// + /// + /// No column with the specified index was found. + /// + /// + /// The zero-based index of the column to get. + /// + /// + /// The column located at the specified index as an . + /// + /// + public object this[int i] => GetValue(i); + + /// + /// Gets the column with the specified name. (Inherited from .) + /// + /// + /// No column with the specified name was found. + /// + /// + /// The name of the column to find. + /// + /// + /// The column located at the specified name as an . + /// + /// + public object this[string name] => GetValue(GetOrdinal(name)); + + /// + /// Gets the number of rows changed, inserted, or deleted by execution of the SQL statement. (Inherited from + /// .) + /// + /// + /// Always returns -1 which is the expected behaviour for statements. + /// + /// + public virtual int RecordsAffected => -1; + + /// + /// Closes the . (Inherited from .) + /// + /// + public void Close() => _isOpen = false; + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public bool GetBoolean(int i) => (bool)GetValue(i); + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public byte GetByte(int i) => (byte)GetValue(i); + + /// + /// Reads a stream of bytes from the specified column offset into the buffer as an array, starting at the given buffer + /// offset. + /// (Inherited from .) + /// + /// + /// If you pass a buffer that is null, returns the length of the row in bytes. + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The index within the field from which to start the read operation. + /// + /// + /// The buffer into which to read the stream of bytes. + /// + /// + /// The index for buffer to start the read operation. + /// + /// + /// The number of bytes to read. + /// + /// + /// The actual number of bytes read. + /// + /// + public long GetBytes( + int i, + long fieldOffset, + byte[]? buffer, + int bufferoffset, + int length) + { + var data = (byte[])GetValue(i); + + if (buffer != null) { - this._schemaTable.Locale = System.Globalization.CultureInfo.InvariantCulture; - - DataColumnCollection columns = _schemaTable.Columns; - - columns.Add(SchemaTableColumn.ColumnName, typeof(string)); - columns.Add(SchemaTableColumn.ColumnOrdinal, typeof(int)); - columns.Add(SchemaTableColumn.ColumnSize, typeof(int)); - columns.Add(SchemaTableColumn.NumericPrecision, typeof(short)); - columns.Add(SchemaTableColumn.NumericScale, typeof(short)); - columns.Add(SchemaTableColumn.IsUnique, typeof(bool)); - columns.Add(SchemaTableColumn.IsKey, typeof(bool)); - columns.Add(SchemaTableOptionalColumn.BaseServerName, typeof(string)); - columns.Add(SchemaTableOptionalColumn.BaseCatalogName, typeof(string)); - columns.Add(SchemaTableColumn.BaseColumnName, typeof(string)); - columns.Add(SchemaTableColumn.BaseSchemaName, typeof(string)); - columns.Add(SchemaTableColumn.BaseTableName, typeof(string)); - columns.Add(SchemaTableColumn.DataType, typeof(Type)); - columns.Add(SchemaTableColumn.AllowDBNull, typeof(bool)); - columns.Add(SchemaTableColumn.ProviderType, typeof(int)); - columns.Add(SchemaTableColumn.IsAliased, typeof(bool)); - columns.Add(SchemaTableColumn.IsExpression, typeof(bool)); - columns.Add(BulkDataReader.IsIdentitySchemaColumn, typeof(bool)); - columns.Add(SchemaTableOptionalColumn.IsAutoIncrement, typeof(bool)); - columns.Add(SchemaTableOptionalColumn.IsRowVersion, typeof(bool)); - columns.Add(SchemaTableOptionalColumn.IsHidden, typeof(bool)); - columns.Add(SchemaTableColumn.IsLong, typeof(bool)); - columns.Add(SchemaTableOptionalColumn.IsReadOnly, typeof(bool)); - columns.Add(SchemaTableOptionalColumn.ProviderSpecificDataType, typeof(Type)); - columns.Add(BulkDataReader.DataTypeNameSchemaColumn, typeof(string)); - columns.Add(BulkDataReader.XmlSchemaCollectionDatabaseSchemaColumn, typeof(string)); - columns.Add(BulkDataReader.XmlSchemaCollectionOwningSchemaSchemaColumn, typeof(string)); - columns.Add(BulkDataReader.XmlSchemaCollectionNameSchemaColumn, typeof(string)); + Array.Copy(data, fieldOffset, buffer, bufferoffset, length); } - #endregion + return data.LongLength; + } - #region IDataReader + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public char GetChar(int i) + { + char result; - /// - /// Gets a value indicating the depth of nesting for the current row. (Inherited from .) - /// - /// - /// does not support nested result sets so this method always returns 0. - /// - /// - public int Depth + var data = GetValue(i); + var dataAsChar = data as char?; + + if (dataAsChar.HasValue) { - get { return 0; } + result = dataAsChar.Value; + } + else if (data is char[] dataAsCharArray && + dataAsCharArray.Length == 1) + { + result = dataAsCharArray[0]; + } + else if (data is string dataAsString && + dataAsString.Length == 1) + { + result = dataAsString[0]; + } + else + { + throw new InvalidOperationException("GetValue did not return a Char compatible type."); } - /// - /// Gets the number of columns in the current row. (Inherited from .) - /// - /// - public int FieldCount + return result; + } + + /// + /// Reads a stream of characters from the specified column offset into the buffer as an array, starting at the given + /// buffer offset. + /// (Inherited from .) + /// + /// + /// If you pass a buffer that is null, returns the length of the row in bytes. + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The index within the field from which to start the read operation. + /// + /// + /// The buffer into which to read the stream of characters. + /// + /// + /// The index for buffer to start the read operation. + /// + /// + /// The number of characters to read. + /// + /// + /// The actual number of characters read. + /// + /// + public long GetChars( + int i, + long fieldoffset, + char[]? buffer, + int bufferoffset, + int length) + { + var data = GetValue(i); + + var dataAsCharArray = data as char[]; + + if (data is string dataAsString) { - get { return GetSchemaTable().Rows.Count; } + dataAsCharArray = dataAsString.ToCharArray((int)fieldoffset, length); + } + else if (dataAsCharArray == null) + { + throw new InvalidOperationException("GetValue did not return either a Char array or a String."); } - /// - /// Is the bulk copy process open? - /// - bool _isOpen = true; - - /// - /// Gets a value indicating whether the data reader is closed. (Inherited from .) - /// - /// - public bool IsClosed + if (buffer != null) { - get { return !_isOpen; } + Array.Copy(dataAsCharArray, fieldoffset, buffer, bufferoffset, length); } - /// - /// Gets the column located at the specified index. (Inherited from .) - /// - /// - /// No column with the specified index was found. - /// - /// - /// The zero-based index of the column to get. - /// - /// - /// The column located at the specified index as an . - /// - /// - public object this[int i] + return dataAsCharArray.LongLength; + } + + /// + /// Returns an IDataReader for the specified column ordinal. (Inherited from .) + /// + /// + /// does not support nested result sets so this method always returns null. + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The for the specified column ordinal (null). + /// + /// + public IDataReader GetData(int i) + { + if (i < 0 || i >= FieldCount) { - get { return GetValue(i); } + throw new ArgumentOutOfRangeException("i"); } - /// - /// Gets the column with the specified name. (Inherited from .) - /// - /// - /// No column with the specified name was found. - /// - /// - /// The name of the column to find. - /// - /// - /// The column located at the specified name as an . - /// - /// - public object this[string name] + return null!; + } + + /// + /// The data type information for the specified field. (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The data type information for the specified field. + /// + /// + public string GetDataTypeName(int i) => GetFieldType(i).Name; + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public DateTime GetDateTime(int i) => (DateTime)GetValue(i); + + /// + /// Gets the value of the specified column as a . + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + public DateTimeOffset GetDateTimeOffset(int i) => (DateTimeOffset)GetValue(i); + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public decimal GetDecimal(int i) => (decimal)GetValue(i); + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public double GetDouble(int i) => (double)GetValue(i); + + /// + /// Gets the information corresponding to the type of that would be returned + /// from . + /// (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The information corresponding to the type of that would be returned from + /// . + /// + /// + public Type GetFieldType(int i) => (Type)GetSchemaTable().Rows[i][SchemaTableColumn.DataType]; + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public float GetFloat(int i) => (float)this[i]; + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public Guid GetGuid(int i) => (Guid)GetValue(i); + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public short GetInt16(int i) => (short)GetValue(i); + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public int GetInt32(int i) => (int)GetValue(i); + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public long GetInt64(int i) => (long)GetValue(i); + + /// + /// Gets the name for the field to find. (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The name of the field or the empty string (""), if there is no value to return. + /// + /// + public string GetName(int i) => (string)GetSchemaTable().Rows[i][SchemaTableColumn.ColumnName]; + + /// + /// Return the index of the named field. (Inherited from .) + /// + /// + /// The index of the named field was not found. + /// + /// + /// The name of the field to find. + /// + /// + /// The index of the named field. + /// + /// + public int GetOrdinal(string name) + { + if (name == null) // Empty strings are handled as a IndexOutOfRangeException. { - get { return GetValue(GetOrdinal(name)); } + throw new ArgumentNullException("name"); } - /// - /// Gets the number of rows changed, inserted, or deleted by execution of the SQL statement. (Inherited from .) - /// - /// - /// Always returns -1 which is the expected behaviour for statements. - /// - /// - public virtual int RecordsAffected - { - get { return -1; } - } + var result = -1; - /// - /// Closes the . (Inherited from .) - /// - /// - public void Close() - { - this._isOpen = false; - } + var rowCount = FieldCount; - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public bool GetBoolean(int i) - { - return (bool)GetValue(i); - } + DataRowCollection schemaRows = GetSchemaTable().Rows; - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public byte GetByte(int i) + // Case sensitive search + for (var ordinal = 0; ordinal < rowCount; ordinal++) { - return (byte)GetValue(i); - } - - /// - /// Reads a stream of bytes from the specified column offset into the buffer as an array, starting at the given buffer offset. - /// (Inherited from .) - /// - /// - /// If you pass a buffer that is null, returns the length of the row in bytes. - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The index within the field from which to start the read operation. - /// - /// - /// The buffer into which to read the stream of bytes. - /// - /// - /// The index for buffer to start the read operation. - /// - /// - /// The number of bytes to read. - /// - /// - /// The actual number of bytes read. - /// - /// - public long GetBytes(int i, - long fieldOffset, - byte[]? buffer, - int bufferoffset, - int length) - { - byte[] data = (byte[])GetValue(i); - - if (buffer != null) + if (string.Equals((string)schemaRows[ordinal][SchemaTableColumn.ColumnName], name, StringComparison.Ordinal)) { - Array.Copy(data, fieldOffset, buffer, bufferoffset, length); + result = ordinal; } - - return data.LongLength; } - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public char GetChar(int i) + if (result == -1) { - char result; - - object data = GetValue(i); - char? dataAsChar = data as char?; - char[]? dataAsCharArray = data as char[]; - string? dataAsString = data as string; - - if (dataAsChar.HasValue) + // Case insensitive search. + for (var ordinal = 0; ordinal < rowCount; ordinal++) { - result = dataAsChar.Value; - } - else if (dataAsCharArray != null && - dataAsCharArray.Length == 1) - { - result = dataAsCharArray[0]; - } - else if (dataAsString != null && - dataAsString.Length == 1) - { - result = dataAsString[0]; - } - else - { - throw new InvalidOperationException("GetValue did not return a Char compatible type."); - } - - return result; - } - - /// - /// Reads a stream of characters from the specified column offset into the buffer as an array, starting at the given buffer offset. - /// (Inherited from .) - /// - /// - /// If you pass a buffer that is null, returns the length of the row in bytes. - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The index within the field from which to start the read operation. - /// - /// - /// The buffer into which to read the stream of characters. - /// - /// - /// The index for buffer to start the read operation. - /// - /// - /// The number of characters to read. - /// - /// - /// The actual number of characters read. - /// - /// - public long GetChars(int i, - long fieldoffset, - char[]? buffer, - int bufferoffset, - int length) - { - object data = GetValue(i); - - string? dataAsString = data as string; - char[]? dataAsCharArray = data as char[]; - - if (dataAsString != null) - { - dataAsCharArray = dataAsString.ToCharArray((int)fieldoffset, length); - } - else if (dataAsCharArray == null) - { - throw new InvalidOperationException("GetValue did not return either a Char array or a String."); - } - - if (buffer != null) - { - Array.Copy(dataAsCharArray, fieldoffset, buffer, bufferoffset, length); - } - - return dataAsCharArray.LongLength; - } - - /// - /// Returns an IDataReader for the specified column ordinal. (Inherited from .) - /// - /// - /// does not support nested result sets so this method always returns null. - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The for the specified column ordinal (null). - /// - /// - public IDataReader GetData(int i) - { - if (i < 0 || i >= this.FieldCount) - { - throw new ArgumentOutOfRangeException("i"); - } - - return null!; - } - - /// - /// The data type information for the specified field. (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The data type information for the specified field. - /// - /// - public string GetDataTypeName(int i) - { - return GetFieldType(i).Name; - } - - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public DateTime GetDateTime(int i) - { - return (DateTime)GetValue(i); - } - - /// - /// Gets the value of the specified column as a . - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - public DateTimeOffset GetDateTimeOffset(int i) - { - return (DateTimeOffset)GetValue(i); - } - - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public decimal GetDecimal(int i) - { - return (decimal)GetValue(i); - } - - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public double GetDouble(int i) - { - return (double)GetValue(i); - } - - /// - /// Gets the information corresponding to the type of that would be returned from . - /// (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The information corresponding to the type of that would be returned from . - /// - /// - public Type GetFieldType(int i) - { - return (Type)GetSchemaTable().Rows[i][SchemaTableColumn.DataType]; - } - - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public float GetFloat(int i) - { - return (float)this[i]; - } - - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public Guid GetGuid(int i) - { - return (Guid)GetValue(i); - } - - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public short GetInt16(int i) - { - return (short)GetValue(i); - } - - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public int GetInt32(int i) - { - return (int)GetValue(i); - } - - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public long GetInt64(int i) - { - return (long)GetValue(i); - } - - /// - /// Gets the name for the field to find. (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The name of the field or the empty string (""), if there is no value to return. - /// - /// - public string GetName(int i) - { - return (string)GetSchemaTable().Rows[i][SchemaTableColumn.ColumnName]; - } - - /// - /// Return the index of the named field. (Inherited from .) - /// - /// - /// The index of the named field was not found. - /// - /// - /// The name of the field to find. - /// - /// - /// The index of the named field. - /// - /// - public int GetOrdinal(string name) - { - if (name == null) // Empty strings are handled as a IndexOutOfRangeException. - { - throw new ArgumentNullException("name"); - } - - int result = -1; - - int rowCount = FieldCount; - - DataRowCollection schemaRows = GetSchemaTable().Rows; - - // Case sensitive search - for (int ordinal = 0; ordinal < rowCount; ordinal++) - { - if (String.Equals((string)schemaRows[ordinal][SchemaTableColumn.ColumnName], name, StringComparison.Ordinal)) + if (string.Equals((string)schemaRows[ordinal][SchemaTableColumn.ColumnName], name, StringComparison.OrdinalIgnoreCase)) { result = ordinal; } } - - if (result == -1) - { - // Case insensitive search. - for (int ordinal = 0; ordinal < rowCount; ordinal++) - { - if (String.Equals((string)schemaRows[ordinal][SchemaTableColumn.ColumnName], name, StringComparison.OrdinalIgnoreCase)) - { - result = ordinal; - } - } - } - - if (result == -1) - { - throw new IndexOutOfRangeException(name); - } - - return result; } - /// - /// Returns a that describes the column metadata of the . (Inherited from .) - /// - /// - /// The is closed. - /// - /// - /// A that describes the column metadata. - /// - /// - public DataTable GetSchemaTable() + if (result == -1) { - if (IsClosed) - { - throw new InvalidOperationException("The IDataReader is closed."); - } - - if (_schemaTable?.Rows.Count == 0) - { - // Need to add the column definitions and mappings - _schemaTable.TableName = TableName; - - AddSchemaTableRows(); - - Debug.Assert(_schemaTable.Rows.Count == FieldCount); - } - - return _schemaTable!; + throw new IndexOutOfRangeException(name); } - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public string GetString(int i) - { - return (string)GetValue(i); - } - - /// - /// Gets the value of the specified column as a . - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - public TimeSpan GetTimeSpan(int i) - { - return (TimeSpan)GetValue(i); - } - - /// - /// Gets the value of the specified column as a . (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// The value of the column. - /// - /// - public abstract object GetValue(int i); - - /// - /// Populates an array of objects with the column values of the current record. (Inherited from .) - /// - /// - /// was null. - /// - /// - /// An array of to copy the attribute fields into. - /// - /// - /// The number of instances of in the array. - /// - /// - public int GetValues(object[] values) - { - if (values == null) - { - throw new ArgumentNullException("values"); - } - - int fieldCount = Math.Min(FieldCount, values.Length); - - for (int i = 0; i < fieldCount; i++) - { - values[i] = GetValue(i); - } - - return fieldCount; - } - - /// - /// Return whether the specified field is set to null. (Inherited from .) - /// - /// - /// The index passed was outside the range of 0 through . - /// - /// - /// The zero-based column ordinal. - /// - /// - /// True if the specified field is set to null; otherwise, false. - /// - /// - public bool IsDBNull(int i) - { - object data = GetValue(i); - - return data == null || Convert.IsDBNull(data); - } - - /// - /// Advances the data reader to the next result, when reading the results of batch SQL statements. (Inherited from .) - /// - /// - /// for returns a single result set so false is always returned. - /// - /// - /// True if there are more rows; otherwise, false. for returns a single result set so false is always returned. - /// - /// - public bool NextResult() - { - return false; - } - - /// - /// Advances the to the next record. (Inherited from .) - /// - /// - /// True if there are more rows; otherwise, false. - /// - /// - public abstract bool Read(); - - #endregion - - #region IDisposable - - /// - /// Has the object been disposed? - /// - bool _disposed = false; - - /// - /// Dispose of any disposable and expensive resources. - /// - /// - /// Is this call the result of a call? - /// - protected virtual void Dispose(bool disposing) - { - if (!this._disposed) - { - this._disposed = true; - - if (disposing) - { - if (_schemaTable != null) - { - _schemaTable.Dispose(); - this._schemaTable = null; - } - - this._columnMappings = null; - - this._isOpen = false; - - GC.SuppressFinalize(this); - } - } - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. (Inherited from .) - /// - /// - public void Dispose() - { - Dispose(true); - } - - /// - /// Finalizer - /// - /// - /// has no unmanaged resources but a subclass may thus a finalizer is required. - /// - ~BulkDataReader() - { - Dispose(false); - } - - #endregion - + return result; } + + /// + /// Returns a that describes the column metadata of the . (Inherited + /// from .) + /// + /// + /// The is closed. + /// + /// + /// A that describes the column metadata. + /// + /// + public DataTable GetSchemaTable() + { + if (IsClosed) + { + throw new InvalidOperationException("The IDataReader is closed."); + } + + if (_schemaTable?.Rows.Count == 0) + { + // Need to add the column definitions and mappings + _schemaTable.TableName = TableName; + + AddSchemaTableRows(); + + Debug.Assert(_schemaTable.Rows.Count == FieldCount); + } + + return _schemaTable!; + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public string GetString(int i) => (string)GetValue(i); + + /// + /// Gets the value of the specified column as a . + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + public TimeSpan GetTimeSpan(int i) => (TimeSpan)GetValue(i); + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public abstract object GetValue(int i); + + /// + /// Populates an array of objects with the column values of the current record. (Inherited from + /// .) + /// + /// + /// was null. + /// + /// + /// An array of to copy the attribute fields into. + /// + /// + /// The number of instances of in the array. + /// + /// + public int GetValues(object[] values) + { + if (values == null) + { + throw new ArgumentNullException("values"); + } + + var fieldCount = Math.Min(FieldCount, values.Length); + + for (var i = 0; i < fieldCount; i++) + { + values[i] = GetValue(i); + } + + return fieldCount; + } + + /// + /// Return whether the specified field is set to null. (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// True if the specified field is set to null; otherwise, false. + /// + /// + public bool IsDBNull(int i) + { + var data = GetValue(i); + + return data == null || Convert.IsDBNull(data); + } + + /// + /// Advances the data reader to the next result, when reading the results of batch SQL statements. (Inherited from + /// .) + /// + /// + /// for returns a single result set so false is always returned. + /// + /// + /// True if there are more rows; otherwise, false. for returns a + /// single result set so false is always returned. + /// + /// + public bool NextResult() => false; + + /// + /// Advances the to the next record. (Inherited from .) + /// + /// + /// True if there are more rows; otherwise, false. + /// + /// + public abstract bool Read(); + + #endregion + + #region IDisposable + + /// + /// Has the object been disposed? + /// + private bool _disposed; + + /// + /// Dispose of any disposable and expensive resources. + /// + /// + /// Is this call the result of a call? + /// + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + _disposed = true; + + if (disposing) + { + if (_schemaTable != null) + { + _schemaTable.Dispose(); + _schemaTable = null; + } + + _columnMappings = null; + + _isOpen = false; + + GC.SuppressFinalize(this); + } + } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. (Inherited + /// from .) + /// + /// + public void Dispose() => Dispose(true); + + /// + /// Finalizer + /// + /// + /// has no unmanaged resources but a subclass may thus a finalizer is required. + /// + ~BulkDataReader() => Dispose(false); + + #endregion } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/MicrosoftSqlSyntaxProviderBase.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/MicrosoftSqlSyntaxProviderBase.cs index 4856e1e117..7256317c15 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/MicrosoftSqlSyntaxProviderBase.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/MicrosoftSqlSyntaxProviderBase.cs @@ -6,216 +6,218 @@ using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Persistence.SqlServer.Services +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +/// +/// Abstract class for defining MS sql implementations +/// +/// +public abstract class MicrosoftSqlSyntaxProviderBase : SqlSyntaxProviderBase + where TSyntax : ISqlSyntaxProvider { - /// - /// Abstract class for defining MS sql implementations - /// - /// - public abstract class MicrosoftSqlSyntaxProviderBase : SqlSyntaxProviderBase - where TSyntax : ISqlSyntaxProvider + private readonly ILogger _logger; + + protected MicrosoftSqlSyntaxProviderBase() { - private readonly ILogger _logger; + _logger = StaticApplicationLogging.CreateLogger(); - protected MicrosoftSqlSyntaxProviderBase() + AutoIncrementDefinition = "IDENTITY(1,1)"; + GuidColumnDefinition = "UniqueIdentifier"; + RealColumnDefinition = "FLOAT"; + BoolColumnDefinition = "BIT"; + DecimalColumnDefinition = "DECIMAL(38,6)"; + TimeColumnDefinition = "TIME"; //SQLSERVER 2008+ + BlobColumnDefinition = "VARBINARY(MAX)"; + } + + public override string RenameTable => "sp_rename '{0}', '{1}'"; + + public override string AddColumn => "ALTER TABLE {0} ADD {1}"; + + public override string GetQuotedTableName(string? tableName) + { + if (tableName?.Contains(".") == false) { - _logger = StaticApplicationLogging.CreateLogger(); - - AutoIncrementDefinition = "IDENTITY(1,1)"; - GuidColumnDefinition = "UniqueIdentifier"; - RealColumnDefinition = "FLOAT"; - BoolColumnDefinition = "BIT"; - DecimalColumnDefinition = "DECIMAL(38,6)"; - TimeColumnDefinition = "TIME"; //SQLSERVER 2008+ - BlobColumnDefinition = "VARBINARY(MAX)"; + return $"[{tableName}]"; } - public override string RenameTable => "sp_rename '{0}', '{1}'"; + var tableNameParts = tableName?.Split(Core.Constants.CharArrays.Period, 2); + return $"[{tableNameParts?[0]}].[{tableNameParts?[1]}]"; + } - public override string AddColumn => "ALTER TABLE {0} ADD {1}"; + public override string GetQuotedColumnName(string? columnName) => $"[{columnName}]"; - public override string GetQuotedTableName(string? tableName) + public override string GetQuotedName(string? name) => $"[{name}]"; + + public override string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType) + { + switch (columnType) { - if (tableName?.Contains(".") == false) - return $"[{tableName}]"; + case TextColumnType.NVarchar: + return base.GetStringColumnEqualComparison(column, paramIndex, columnType); + case TextColumnType.NText: + //MSSQL doesn't allow for = comparison with NText columns but allows this syntax + return $"{column} LIKE @{paramIndex}"; + default: + throw new ArgumentOutOfRangeException(nameof(columnType)); + } + } - var tableNameParts = tableName?.Split(Cms.Core.Constants.CharArrays.Period, 2); - return $"[{tableNameParts?[0]}].[{tableNameParts?[1]}]"; + public override string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType) + { + switch (columnType) + { + case TextColumnType.NVarchar: + return base.GetStringColumnWildcardComparison(column, paramIndex, columnType); + case TextColumnType.NText: + //MSSQL doesn't allow for upper methods with NText columns + return $"{column} LIKE @{paramIndex}"; + default: + throw new ArgumentOutOfRangeException(nameof(columnType)); + } + } + + /// + /// This uses a the DbTypeMap created and custom mapping to resolve the SqlDbType + /// + /// + /// + public virtual SqlDbType GetSqlDbType(Type clrType) + { + DbType dbType = DbTypeMap.ColumnDbTypeMap[clrType]; + return GetSqlDbType(dbType); + } + + /// + /// Returns the mapped SqlDbType for the DbType specified + /// + /// + /// + public virtual SqlDbType GetSqlDbType(DbType dbType) + { + SqlDbType sqlDbType; + + //SEE: https://msdn.microsoft.com/en-us/library/cc716729(v=vs.110).aspx + // and https://msdn.microsoft.com/en-us/library/yy6y35y8%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396 + switch (dbType) + { + case DbType.AnsiString: + sqlDbType = SqlDbType.VarChar; + break; + case DbType.Binary: + sqlDbType = SqlDbType.VarBinary; + break; + case DbType.Byte: + sqlDbType = SqlDbType.TinyInt; + break; + case DbType.Boolean: + sqlDbType = SqlDbType.Bit; + break; + case DbType.Currency: + sqlDbType = SqlDbType.Money; + break; + case DbType.Date: + sqlDbType = SqlDbType.Date; + break; + case DbType.DateTime: + sqlDbType = SqlDbType.DateTime; + break; + case DbType.Decimal: + sqlDbType = SqlDbType.Decimal; + break; + case DbType.Double: + sqlDbType = SqlDbType.Float; + break; + case DbType.Guid: + sqlDbType = SqlDbType.UniqueIdentifier; + break; + case DbType.Int16: + sqlDbType = SqlDbType.SmallInt; + break; + case DbType.Int32: + sqlDbType = SqlDbType.Int; + break; + case DbType.Int64: + sqlDbType = SqlDbType.BigInt; + break; + case DbType.Object: + sqlDbType = SqlDbType.Variant; + break; + case DbType.SByte: + throw new NotSupportedException("Inferring a SqlDbType from SByte is not supported."); + case DbType.Single: + sqlDbType = SqlDbType.Real; + break; + case DbType.String: + sqlDbType = SqlDbType.NVarChar; + break; + case DbType.Time: + sqlDbType = SqlDbType.Time; + break; + case DbType.UInt16: + throw new NotSupportedException("Inferring a SqlDbType from UInt16 is not supported."); + case DbType.UInt32: + throw new NotSupportedException("Inferring a SqlDbType from UInt32 is not supported."); + case DbType.UInt64: + throw new NotSupportedException("Inferring a SqlDbType from UInt64 is not supported."); + case DbType.VarNumeric: + throw new NotSupportedException("Inferring a VarNumeric from UInt64 is not supported."); + case DbType.AnsiStringFixedLength: + sqlDbType = SqlDbType.Char; + break; + case DbType.StringFixedLength: + sqlDbType = SqlDbType.NChar; + break; + case DbType.Xml: + sqlDbType = SqlDbType.Xml; + break; + case DbType.DateTime2: + sqlDbType = SqlDbType.DateTime2; + break; + case DbType.DateTimeOffset: + sqlDbType = SqlDbType.DateTimeOffset; + break; + default: + throw new ArgumentOutOfRangeException(); } - public override string GetQuotedColumnName(string? columnName) => $"[{columnName}]"; + return sqlDbType; + } - public override string GetQuotedName(string? name) => $"[{name}]"; + public override void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false) + { + var createSql = Format(tableDefinition); + var createPrimaryKeySql = FormatPrimaryKey(tableDefinition); + List foreignSql = Format(tableDefinition.ForeignKeys); - public override string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType) + _logger.LogInformation("Create table:\n {Sql}", createSql); + database.Execute(new Sql(createSql)); + + if (skipKeysAndIndexes) { - switch (columnType) - { - case TextColumnType.NVarchar: - return base.GetStringColumnEqualComparison(column, paramIndex, columnType); - case TextColumnType.NText: - //MSSQL doesn't allow for = comparison with NText columns but allows this syntax - return $"{column} LIKE @{paramIndex}"; - default: - throw new ArgumentOutOfRangeException(nameof(columnType)); - } + return; } - public override string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType) + //If any statements exists for the primary key execute them here + if (string.IsNullOrEmpty(createPrimaryKeySql) == false) { - switch (columnType) - { - case TextColumnType.NVarchar: - return base.GetStringColumnWildcardComparison(column, paramIndex, columnType); - case TextColumnType.NText: - //MSSQL doesn't allow for upper methods with NText columns - return $"{column} LIKE @{paramIndex}"; - default: - throw new ArgumentOutOfRangeException(nameof(columnType)); - } + _logger.LogInformation("Create Primary Key:\n {Sql}", createPrimaryKeySql); + database.Execute(new Sql(createPrimaryKeySql)); } - /// - /// This uses a the DbTypeMap created and custom mapping to resolve the SqlDbType - /// - /// - /// - public virtual SqlDbType GetSqlDbType(Type clrType) + List indexSql = Format(tableDefinition.Indexes); + //Loop through index statements and execute sql + foreach (var sql in indexSql) { - var dbType = DbTypeMap.ColumnDbTypeMap[clrType]; - return GetSqlDbType(dbType); + _logger.LogInformation("Create Index:\n {Sql}", sql); + database.Execute(new Sql(sql)); } - /// - /// Returns the mapped SqlDbType for the DbType specified - /// - /// - /// - public virtual SqlDbType GetSqlDbType(DbType dbType) + //Loop through foreignkey statements and execute sql + foreach (var sql in foreignSql) { - SqlDbType sqlDbType; - - //SEE: https://msdn.microsoft.com/en-us/library/cc716729(v=vs.110).aspx - // and https://msdn.microsoft.com/en-us/library/yy6y35y8%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396 - switch (dbType) - { - case DbType.AnsiString: - sqlDbType = SqlDbType.VarChar; - break; - case DbType.Binary: - sqlDbType = SqlDbType.VarBinary; - break; - case DbType.Byte: - sqlDbType = SqlDbType.TinyInt; - break; - case DbType.Boolean: - sqlDbType = SqlDbType.Bit; - break; - case DbType.Currency: - sqlDbType = SqlDbType.Money; - break; - case DbType.Date: - sqlDbType = SqlDbType.Date; - break; - case DbType.DateTime: - sqlDbType = SqlDbType.DateTime; - break; - case DbType.Decimal: - sqlDbType = SqlDbType.Decimal; - break; - case DbType.Double: - sqlDbType = SqlDbType.Float; - break; - case DbType.Guid: - sqlDbType = SqlDbType.UniqueIdentifier; - break; - case DbType.Int16: - sqlDbType = SqlDbType.SmallInt; - break; - case DbType.Int32: - sqlDbType = SqlDbType.Int; - break; - case DbType.Int64: - sqlDbType = SqlDbType.BigInt; - break; - case DbType.Object: - sqlDbType = SqlDbType.Variant; - break; - case DbType.SByte: - throw new NotSupportedException("Inferring a SqlDbType from SByte is not supported."); - case DbType.Single: - sqlDbType = SqlDbType.Real; - break; - case DbType.String: - sqlDbType = SqlDbType.NVarChar; - break; - case DbType.Time: - sqlDbType = SqlDbType.Time; - break; - case DbType.UInt16: - throw new NotSupportedException("Inferring a SqlDbType from UInt16 is not supported."); - case DbType.UInt32: - throw new NotSupportedException("Inferring a SqlDbType from UInt32 is not supported."); - case DbType.UInt64: - throw new NotSupportedException("Inferring a SqlDbType from UInt64 is not supported."); - case DbType.VarNumeric: - throw new NotSupportedException("Inferring a VarNumeric from UInt64 is not supported."); - case DbType.AnsiStringFixedLength: - sqlDbType = SqlDbType.Char; - break; - case DbType.StringFixedLength: - sqlDbType = SqlDbType.NChar; - break; - case DbType.Xml: - sqlDbType = SqlDbType.Xml; - break; - case DbType.DateTime2: - sqlDbType = SqlDbType.DateTime2; - break; - case DbType.DateTimeOffset: - sqlDbType = SqlDbType.DateTimeOffset; - break; - default: - throw new ArgumentOutOfRangeException(); - } - return sqlDbType; - } - - public override void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false) - { - var createSql = Format(tableDefinition); - var createPrimaryKeySql = FormatPrimaryKey(tableDefinition); - List foreignSql = Format(tableDefinition.ForeignKeys); - - _logger.LogInformation("Create table:\n {Sql}", createSql); - database.Execute(new Sql(createSql)); - - if (skipKeysAndIndexes) - { - return; - } - - //If any statements exists for the primary key execute them here - if (string.IsNullOrEmpty(createPrimaryKeySql) == false) - { - _logger.LogInformation("Create Primary Key:\n {Sql}", createPrimaryKeySql); - database.Execute(new Sql(createPrimaryKeySql)); - } - - List indexSql = Format(tableDefinition.Indexes); - //Loop through index statements and execute sql - foreach (var sql in indexSql) - { - _logger.LogInformation("Create Index:\n {Sql}", sql); - database.Execute(new Sql(sql)); - } - - //Loop through foreignkey statements and execute sql - foreach (var sql in foreignSql) - { - _logger.LogInformation("Create Foreign Key:\n {Sql}", sql); - database.Execute(new Sql(sql)); - } + _logger.LogInformation("Create Foreign Key:\n {Sql}", sql); + database.Execute(new Sql(sql)); } } } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/PocoDataDataReader.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/PocoDataDataReader.cs index 8a05e78258..2b9d35b959 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/PocoDataDataReader.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/PocoDataDataReader.cs @@ -4,142 +4,166 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Persistence.SqlServer.Services +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +/// +/// A data reader used for reading collections of PocoData entity types +/// +/// +/// We are using a custom data reader so that tons of memory is not consumed when rebuilding this table, previously +/// we'd generate SQL insert statements, but we'd have to put all of the XML structures into memory first. +/// Alternatively +/// we can use .net's DataTable, but this also requires putting everything into memory. By using a DataReader we don't +/// have to +/// store every content item and it's XML structure in memory to get it into the DB, we can stream it into the db with +/// this +/// reader. +/// +internal class PocoDataDataReader : BulkDataReader + where TSyntax : ISqlSyntaxProvider { - /// - /// A data reader used for reading collections of PocoData entity types - /// - /// - /// We are using a custom data reader so that tons of memory is not consumed when rebuilding this table, previously - /// we'd generate SQL insert statements, but we'd have to put all of the XML structures into memory first. Alternatively - /// we can use .net's DataTable, but this also requires putting everything into memory. By using a DataReader we don't have to - /// store every content item and it's XML structure in memory to get it into the DB, we can stream it into the db with this - /// reader. - /// - internal class PocoDataDataReader : BulkDataReader - where TSyntax : ISqlSyntaxProvider + private readonly ColumnDefinition[] _columnDefinitions; + private readonly IEnumerator _enumerator; + private readonly PocoColumn[] _readerColumns; + private readonly MicrosoftSqlSyntaxProviderBase _sqlSyntaxProvider; + private readonly TableDefinition _tableDefinition; + private int _recordsAffected = -1; + + public PocoDataDataReader( + IEnumerable dataSource, + PocoData pd, + MicrosoftSqlSyntaxProviderBase sqlSyntaxProvider) { - private readonly MicrosoftSqlSyntaxProviderBase _sqlSyntaxProvider; - private readonly TableDefinition _tableDefinition; - private readonly PocoColumn[] _readerColumns; - private readonly IEnumerator _enumerator; - private readonly ColumnDefinition[] _columnDefinitions; - private int _recordsAffected = -1; - - public PocoDataDataReader( - IEnumerable dataSource, - PocoData pd, - MicrosoftSqlSyntaxProviderBase sqlSyntaxProvider) + if (dataSource == null) { - if (dataSource == null) throw new ArgumentNullException(nameof(dataSource)); - if (sqlSyntaxProvider == null) throw new ArgumentNullException(nameof(sqlSyntaxProvider)); - - _tableDefinition = DefinitionFactory.GetTableDefinition(pd.Type, sqlSyntaxProvider); - if (_tableDefinition == null) throw new InvalidOperationException("No table definition found for type " + pd.Type); - - // only real columns, exclude result/computed columns - // Like NPoco does: https://github.com/schotime/NPoco/blob/5117a55fde57547e928246c044fd40bd00b2d7d1/src/NPoco.SqlServer/SqlBulkCopyHelper.cs#L59 - _readerColumns = pd.Columns - .Where(x => x.Value.ResultColumn == false && x.Value.ComputedColumn == false) - .Select(x => x.Value) - .ToArray(); - - _sqlSyntaxProvider = sqlSyntaxProvider; - _enumerator = dataSource.GetEnumerator(); - _columnDefinitions = _tableDefinition.Columns.ToArray(); - + throw new ArgumentNullException(nameof(dataSource)); } - protected override string SchemaName => _tableDefinition.SchemaName; - - protected override string TableName => _tableDefinition.Name; - - public override int RecordsAffected => _recordsAffected <= 0 ? -1 : _recordsAffected; - - /// - /// This will automatically add the schema rows based on the Poco table definition and the columns passed in - /// - protected override void AddSchemaTableRows() + if (sqlSyntaxProvider == null) { - //var colNames = _readerColumns.Select(x => x.ColumnName).ToArray(); - //foreach (var col in _columnDefinitions.Where(x => colNames.Contains(x.Name, StringComparer.OrdinalIgnoreCase))) - foreach (var col in _columnDefinitions) + throw new ArgumentNullException(nameof(sqlSyntaxProvider)); + } + + _tableDefinition = DefinitionFactory.GetTableDefinition(pd.Type, sqlSyntaxProvider); + if (_tableDefinition == null) + { + throw new InvalidOperationException("No table definition found for type " + pd.Type); + } + + // only real columns, exclude result/computed columns + // Like NPoco does: https://github.com/schotime/NPoco/blob/5117a55fde57547e928246c044fd40bd00b2d7d1/src/NPoco.SqlServer/SqlBulkCopyHelper.cs#L59 + _readerColumns = pd.Columns + .Where(x => x.Value.ResultColumn == false && x.Value.ComputedColumn == false) + .Select(x => x.Value) + .ToArray(); + + _sqlSyntaxProvider = sqlSyntaxProvider; + _enumerator = dataSource.GetEnumerator(); + _columnDefinitions = _tableDefinition.Columns.ToArray(); + } + + protected override string SchemaName => _tableDefinition.SchemaName; + + protected override string TableName => _tableDefinition.Name; + + public override int RecordsAffected => _recordsAffected <= 0 ? -1 : _recordsAffected; + + /// + /// This will automatically add the schema rows based on the Poco table definition and the columns passed in + /// + protected override void AddSchemaTableRows() + { + //var colNames = _readerColumns.Select(x => x.ColumnName).ToArray(); + //foreach (var col in _columnDefinitions.Where(x => colNames.Contains(x.Name, StringComparer.OrdinalIgnoreCase))) + foreach (ColumnDefinition col in _columnDefinitions) + { + SqlDbType sqlDbType; + if (col.CustomDbType.HasValue) { - SqlDbType sqlDbType; - if (col.CustomDbType.HasValue) + //get the SqlDbType from the 'special type' + switch (col.CustomDbType) { - //get the SqlDbType from the 'special type' - switch (col.CustomDbType) - { - case var x when x == SpecialDbType.NTEXT: - sqlDbType = SqlDbType.NText; - break; - case var x when x == SpecialDbType.NCHAR: - sqlDbType = SqlDbType.NChar; - break; - case var x when x == SpecialDbType.NVARCHARMAX: - sqlDbType = SqlDbType.NVarChar; - break; - default: - throw new ArgumentOutOfRangeException("The custom DB type " + col.CustomDbType + " is not supported for bulk import statements."); - } + case var x when x == SpecialDbType.NTEXT: + sqlDbType = SqlDbType.NText; + break; + case var x when x == SpecialDbType.NCHAR: + sqlDbType = SqlDbType.NChar; + break; + case var x when x == SpecialDbType.NVARCHARMAX: + sqlDbType = SqlDbType.NVarChar; + break; + default: + throw new ArgumentOutOfRangeException("The custom DB type " + col.CustomDbType + + " is not supported for bulk import statements."); } - else if (col.Type.HasValue) - { - //get the SqlDbType from the DbType - sqlDbType = _sqlSyntaxProvider.GetSqlDbType(col.Type.Value); - } - else - { - //get the SqlDbType from the CLR type - sqlDbType = _sqlSyntaxProvider.GetSqlDbType(col.PropertyType); - } - - AddSchemaTableRow( - col.Name, - col.Size > 0 ? (int?)col.Size : null, - col.Precision > 0 ? (short?)col.Precision : null, - null, col.IsUnique, col.IsIdentity, col.IsNullable, sqlDbType, - null, null, null, null, null); } - } - - /// - /// Get the value from the column index for the current object - /// - /// - /// - public override object GetValue(int i) - { - return _enumerator.Current == null ? null! : _readerColumns[i].GetValue(_enumerator.Current); - } - - /// - /// Advance the cursor - /// - /// - public override bool Read() - { - var result = _enumerator.MoveNext(); - if (result) + else if (col.Type.HasValue) { - if (_recordsAffected == -1) - _recordsAffected = 0; - _recordsAffected++; + //get the SqlDbType from the DbType + sqlDbType = _sqlSyntaxProvider.GetSqlDbType(col.Type.Value); } - return result; + else + { + //get the SqlDbType from the CLR type + sqlDbType = _sqlSyntaxProvider.GetSqlDbType(col.PropertyType); + } + + AddSchemaTableRow( + col.Name, + col.Size > 0 ? col.Size : null, + col.Precision > 0 ? (short?)col.Precision : null, + null, + col.IsUnique, + col.IsIdentity, + col.IsNullable, + sqlDbType, + null, + null, + null, + null, + null); + } + } + + /// + /// Get the value from the column index for the current object + /// + /// + /// + public override object GetValue(int i) => + _enumerator.Current == null ? null! : _readerColumns[i].GetValue(_enumerator.Current); + + /// + /// Advance the cursor + /// + /// + public override bool Read() + { + var result = _enumerator.MoveNext(); + if (result) + { + if (_recordsAffected == -1) + { + _recordsAffected = 0; + } + + _recordsAffected++; } - /// - /// Ensure the enumerator is disposed - /// - /// - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); + return result; + } - if (disposing) - _enumerator.Dispose(); + /// + /// Ensure the enumerator is disposed + /// + /// + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + _enumerator.Dispose(); } } } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs index cf74a8549f..0dbc62fb49 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs @@ -6,13 +6,13 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Persistence.SqlServer.Services; /// -/// Provider metadata for SQL Azure +/// Provider metadata for SQL Azure /// [DataContract] public class SqlAzureDatabaseProviderMetadata : IDatabaseProviderMetadata { /// - public Guid Id => new ("7858e827-8951-4fe0-a7fe-6883011b1f1b"); + public Guid Id => new("7858e827-8951-4fe0-a7fe-6883011b1f1b"); /// public int SortOrder => 3; @@ -59,37 +59,49 @@ public class SqlAzureDatabaseProviderMetadata : IDatabaseProviderMetadata var password = databaseModel.Password; if (server.Contains(".") && ServerStartsWithTcp(server) == false) + { server = $"tcp:{server}"; + } if (server.Contains(".") == false && ServerStartsWithTcp(server)) { - string serverName = server.Contains(",") + var serverName = server.Contains(",") ? server.Substring(0, server.IndexOf(",", StringComparison.Ordinal)) : server; var portAddition = string.Empty; if (server.Contains(",")) + { portAddition = server.Substring(server.IndexOf(",", StringComparison.Ordinal)); + } server = $"{serverName}.database.windows.net{portAddition}"; } if (ServerStartsWithTcp(server) == false) + { server = $"tcp:{server}.database.windows.net"; + } if (server.Contains(",") == false) + { server = $"{server},1433"; + } if (user.Contains("@") == false) { var userDomain = server; if (ServerStartsWithTcp(server)) + { userDomain = userDomain.Substring(userDomain.IndexOf(":", StringComparison.Ordinal) + 1); + } if (userDomain.Contains(".")) + { userDomain = userDomain.Substring(0, userDomain.IndexOf(".", StringComparison.Ordinal)); + } user = $"{user}@{userDomain}"; } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs index 1741b8ffe1..30a503e5f9 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs @@ -7,13 +7,13 @@ using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Cms.Persistence.SqlServer.Services; /// -/// Provider metadata for SQL Server LocalDb +/// Provider metadata for SQL Server LocalDb /// [DataContract] public class SqlLocalDbDatabaseProviderMetadata : IDatabaseProviderMetadata { /// - public Guid Id => new ("05a7e9ed-aa6a-43af-a309-63422c87c675"); + public Guid Id => new("05a7e9ed-aa6a-43af-a309-63422c87c675"); /// public int SortOrder => 1; diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerBulkSqlInsertProvider.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerBulkSqlInsertProvider.cs index cfd30bbd90..dbaab82ad4 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerBulkSqlInsertProvider.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerBulkSqlInsertProvider.cs @@ -1,74 +1,85 @@ using System.Data; +using System.Data.Common; using Microsoft.Data.SqlClient; using NPoco; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Extensions; -namespace Umbraco.Cms.Persistence.SqlServer.Services +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +/// +/// A bulk sql insert provider for Sql Server +/// +public class SqlServerBulkSqlInsertProvider : IBulkSqlInsertProvider { - /// - /// A bulk sql insert provider for Sql Server - /// - public class SqlServerBulkSqlInsertProvider : IBulkSqlInsertProvider + public string ProviderName => Constants.ProviderName; + + public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) { - public string ProviderName => Constants.ProviderName; - - public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) + T[] recordsA = records.ToArray(); + if (recordsA.Length == 0) { - var recordsA = records.ToArray(); - if (recordsA.Length == 0) return 0; - - var pocoData = database.PocoDataFactory.ForType(typeof(T)); - if (pocoData == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); - - return BulkInsertRecordsSqlServer(database, pocoData, recordsA); + return 0; } - /// - /// Bulk-insert records using SqlServer BulkCopy method. - /// - /// The type of the records. - /// The database. - /// The PocoData object corresponding to the record's type. - /// The records. - /// The number of records that were inserted. - private int BulkInsertRecordsSqlServer(IUmbracoDatabase database, PocoData pocoData, IEnumerable records) + PocoData? pocoData = database.PocoDataFactory.ForType(typeof(T)); + if (pocoData == null) { - // TODO: The main reason this exists is because the NPoco InsertBulk method doesn't return the number of items. - // It is worth investigating the performance of this vs NPoco's because we use a custom BulkDataReader - // which in theory should be more efficient than NPocos way of building up an in-memory DataTable. + throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); + } - // create command against the original database.Connection - using (var command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty)) + return BulkInsertRecordsSqlServer(database, pocoData, recordsA); + } + + /// + /// Bulk-insert records using SqlServer BulkCopy method. + /// + /// The type of the records. + /// The database. + /// The PocoData object corresponding to the record's type. + /// The records. + /// The number of records that were inserted. + private int BulkInsertRecordsSqlServer(IUmbracoDatabase database, PocoData pocoData, IEnumerable records) + { + // TODO: The main reason this exists is because the NPoco InsertBulk method doesn't return the number of items. + // It is worth investigating the performance of this vs NPoco's because we use a custom BulkDataReader + // which in theory should be more efficient than NPocos way of building up an in-memory DataTable. + + // create command against the original database.Connection + using (DbCommand command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty)) + { + // use typed connection and transaction or SqlBulkCopy + SqlConnection tConnection = NPocoDatabaseExtensions.GetTypedConnection(database.Connection); + SqlTransaction tTransaction = + NPocoDatabaseExtensions.GetTypedTransaction(command.Transaction); + var tableName = pocoData.TableInfo.TableName; + + if (database.SqlContext.SqlSyntax is not SqlServerSyntaxProvider syntax) { - // use typed connection and transaction or SqlBulkCopy - var tConnection = NPocoDatabaseExtensions.GetTypedConnection(database.Connection); - var tTransaction = NPocoDatabaseExtensions.GetTypedTransaction(command.Transaction); - var tableName = pocoData.TableInfo.TableName; + throw new NotSupportedException("SqlSyntax must be SqlServerSyntaxProvider."); + } - var syntax = database.SqlContext.SqlSyntax as SqlServerSyntaxProvider; - if (syntax == null) throw new NotSupportedException("SqlSyntax must be SqlServerSyntaxProvider."); + using (var copy = new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction) + { + // 0 = no bulk copy timeout. If a timeout occurs it will be an connection/command timeout. + BulkCopyTimeout = 0, + DestinationTableName = tableName, - using (var copy = new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction) + // be consistent with NPoco: https://github.com/schotime/NPoco/blob/5117a55fde57547e928246c044fd40bd00b2d7d1/src/NPoco.SqlServer/SqlBulkCopyHelper.cs#L50 + BatchSize = 4096, + }) + using (var bulkReader = new PocoDataDataReader(records, pocoData, syntax)) + { + // we need to add column mappings here because otherwise columns will be matched by their order and if the order of them are different in the DB compared + // to the order in which they are declared in the model then this will not work, so instead we will add column mappings by name so that this explicitly uses + // the names instead of their ordering. + foreach (SqlBulkCopyColumnMapping col in bulkReader.ColumnMappings) { - BulkCopyTimeout = 0, // 0 = no bulk copy timeout. If a timeout occurs it will be an connection/command timeout. - DestinationTableName = tableName, - // be consistent with NPoco: https://github.com/schotime/NPoco/blob/5117a55fde57547e928246c044fd40bd00b2d7d1/src/NPoco.SqlServer/SqlBulkCopyHelper.cs#L50 - BatchSize = 4096 - }) - using (var bulkReader = new PocoDataDataReader(records, pocoData, syntax)) - { - //we need to add column mappings here because otherwise columns will be matched by their order and if the order of them are different in the DB compared - //to the order in which they are declared in the model then this will not work, so instead we will add column mappings by name so that this explicitly uses - //the names instead of their ordering. - foreach (var col in bulkReader.ColumnMappings) - { - copy.ColumnMappings.Add(col.DestinationColumn, col.DestinationColumn); - } - - copy.WriteToServer(bulkReader); - return bulkReader.RecordsAffected; + copy.ColumnMappings.Add(col.DestinationColumn, col.DestinationColumn); } + + copy.WriteToServer(bulkReader); + return bulkReader.RecordsAffected; } } } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseCreator.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseCreator.cs index 205519d0b1..dd092e820d 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseCreator.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseCreator.cs @@ -1,61 +1,60 @@ using Microsoft.Data.SqlClient; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Persistence.SqlServer.Services +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +public class SqlServerDatabaseCreator : IDatabaseCreator { - public class SqlServerDatabaseCreator : IDatabaseCreator + public string ProviderName => Constants.ProviderName; + + public void Create(string connectionString) { - public string ProviderName => Constants.ProviderName; + var builder = new SqlConnectionStringBuilder(connectionString); - public void Create(string connectionString) + // Get connection string without database specific information + var masterBuilder = new SqlConnectionStringBuilder(builder.ConnectionString) { - var builder = new SqlConnectionStringBuilder(connectionString); + AttachDBFilename = string.Empty, + InitialCatalog = string.Empty + }; + var masterConnectionString = masterBuilder.ConnectionString; - // Get connection string without database specific information - var masterBuilder = new SqlConnectionStringBuilder(builder.ConnectionString) + string fileName = builder.AttachDBFilename, + database = builder.InitialCatalog; + + // Create database + if (!string.IsNullOrEmpty(fileName) && !File.Exists(fileName)) + { + if (string.IsNullOrWhiteSpace(database)) { - AttachDBFilename = string.Empty, - InitialCatalog = string.Empty - }; - var masterConnectionString = masterBuilder.ConnectionString; - - string fileName = builder.AttachDBFilename, - database = builder.InitialCatalog; - - // Create database - if (!string.IsNullOrEmpty(fileName) && !File.Exists(fileName)) - { - if (string.IsNullOrWhiteSpace(database)) - { - // Use a temporary database name - database = "Umbraco-" + Guid.NewGuid(); - } - - using var connection = new SqlConnection(masterConnectionString); - connection.Open(); - - using var command = new SqlCommand( - $"CREATE DATABASE [{database}] ON (NAME='{database}', FILENAME='{fileName}');" + - $"ALTER DATABASE [{database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;" + - $"EXEC sp_detach_db @dbname='{database}';", - connection); - command.ExecuteNonQuery(); - - connection.Close(); + // Use a temporary database name + database = "Umbraco-" + Guid.NewGuid(); } - else if (!string.IsNullOrEmpty(database)) - { - using var connection = new SqlConnection(masterConnectionString); - connection.Open(); - using var command = new SqlCommand( - $"IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = '{database}') " + - $"CREATE DATABASE [{database}];", - connection); - command.ExecuteNonQuery(); + using var connection = new SqlConnection(masterConnectionString); + connection.Open(); - connection.Close(); - } + using var command = new SqlCommand( + $"CREATE DATABASE [{database}] ON (NAME='{database}', FILENAME='{fileName}');" + + $"ALTER DATABASE [{database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;" + + $"EXEC sp_detach_db @dbname='{database}';", + connection); + command.ExecuteNonQuery(); + + connection.Close(); + } + else if (!string.IsNullOrEmpty(database)) + { + using var connection = new SqlConnection(masterConnectionString); + connection.Open(); + + using var command = new SqlCommand( + $"IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = '{database}') " + + $"CREATE DATABASE [{database}];", + connection); + command.ExecuteNonQuery(); + + connection.Close(); } } } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs index 8c840f1778..8b36736804 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs @@ -5,13 +5,13 @@ using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Cms.Persistence.SqlServer.Services; /// -/// Provider metadata for SQL Server +/// Provider metadata for SQL Server /// [DataContract] public class SqlServerDatabaseProviderMetadata : IDatabaseProviderMetadata { /// - public Guid Id => new ("5e1ad149-1951-4b74-90bf-2ac2aada9e73"); + public Guid Id => new("5e1ad149-1951-4b74-90bf-2ac2aada9e73"); /// public int SortOrder => 2; diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs index 7c8effb2b3..12a0afea47 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs @@ -13,17 +13,17 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Persistence.SqlServer.Services; /// -/// SQL Server implementation of . +/// SQL Server implementation of . /// public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism { + private readonly IOptionsMonitor _connectionStrings; + private readonly IOptionsMonitor _globalSettings; private readonly ILogger _logger; private readonly Lazy _scopeAccessor; // Hooray it's a circular dependency. - private readonly IOptionsMonitor _globalSettings; - private readonly IOptionsMonitor _connectionStrings; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public SqlServerDistributedLockingMechanism( ILogger logger, @@ -104,11 +104,9 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism public DistributedLockType LockType { get; } - public void Dispose() - { + public void Dispose() => // Mostly no op, cleaned up by completing transaction in scope. _parent._logger.LogDebug("Dropped {lockType} for id {id}", LockType, LockId); - } public override string ToString() => $"SqlServerDistributedLock({LockId}, {LockType}"; @@ -124,19 +122,21 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism if (!db.InTransaction) { - throw new InvalidOperationException("SqlServerDistributedLockingMechanism requires a transaction to function."); + throw new InvalidOperationException( + "SqlServerDistributedLockingMechanism requires a transaction to function."); } if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) { - throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); + throw new InvalidOperationException( + "A transaction with minimum ReadCommitted isolation level is required."); } const string query = "SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id"; db.Execute("SET LOCK_TIMEOUT " + _timeout.TotalMilliseconds + ";"); - var i = db.ExecuteScalar(query, new {id = LockId}); + var i = db.ExecuteScalar(query, new { id = LockId }); if (i == null) { @@ -156,19 +156,22 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism if (!db.InTransaction) { - throw new InvalidOperationException("SqlServerDistributedLockingMechanism requires a transaction to function."); + throw new InvalidOperationException( + "SqlServerDistributedLockingMechanism requires a transaction to function."); } if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) { - throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); + throw new InvalidOperationException( + "A transaction with minimum ReadCommitted isolation level is required."); } - const string query = @"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id"; + const string query = + @"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id"; db.Execute("SET LOCK_TIMEOUT " + _timeout.TotalMilliseconds + ";"); - var i = db.Execute(query, new {id = LockId}); + var i = db.Execute(query, new { id = LockId }); if (i == 0) { diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs index a4fc33d98c..64ed9e3566 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs @@ -1,4 +1,5 @@ using System.Data; +using System.Data.Common; using System.Diagnostics.CodeAnalysis; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; @@ -12,146 +13,133 @@ using Umbraco.Cms.Persistence.SqlServer.Dtos; using Umbraco.Extensions; using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo; -namespace Umbraco.Cms.Persistence.SqlServer.Services +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +/// +/// Represents an SqlSyntaxProvider for Sql Server. +/// +public class SqlServerSyntaxProvider : MicrosoftSqlSyntaxProviderBase { - /// - /// Represents an SqlSyntaxProvider for Sql Server. - /// - public class SqlServerSyntaxProvider : MicrosoftSqlSyntaxProviderBase + public enum EngineEdition { - private readonly IOptions _globalSettings; - private readonly ILogger _logger; + Unknown = 0, + Desktop = 1, + Standard = 2, + Enterprise = 3, // Also developer edition + Express = 4, + Azure = 5, + } - public SqlServerSyntaxProvider(IOptions globalSettings) - : this(globalSettings, StaticApplicationLogging.CreateLogger()) + public enum VersionName + { + Invalid = -1, + Unknown = 0, + V7 = 1, + V2000 = 2, + V2005 = 3, + V2008 = 4, + V2012 = 5, + V2014 = 6, + V2016 = 7, + V2017 = 8, + V2019 = 9, + Other = 99, + } + + private readonly IOptions _globalSettings; + private readonly ILogger _logger; + + public SqlServerSyntaxProvider(IOptions globalSettings) + : this(globalSettings, StaticApplicationLogging.CreateLogger()) + { + } + + public SqlServerSyntaxProvider(IOptions globalSettings, ILogger logger) + { + _globalSettings = globalSettings; + _logger = logger; + } + + public override string ProviderName => Constants.ProviderName; + + public ServerVersionInfo? ServerVersion { get; private set; } + + public override string DbProvider => ServerVersion?.IsAzure ?? false ? "SqlAzure" : "SqlServer"; + + public override IsolationLevel DefaultIsolationLevel => IsolationLevel.ReadCommitted; + + public override string DeleteDefaultConstraint => "ALTER TABLE {0} DROP CONSTRAINT {2}"; + + public override string DropIndex => "DROP INDEX {0} ON {1}"; + + public override string RenameColumn => "sp_rename '{0}.{1}', '{2}', 'COLUMN'"; + + public override string CreateIndex => "CREATE {0}{1}INDEX {2} ON {3} ({4}){5}"; + + public override DatabaseType GetUpdatedDatabaseType(DatabaseType current, string? connectionString) + { + var setting = _globalSettings.Value.DatabaseFactoryServerVersion; + var fromSettings = false; + + if (setting.IsNullOrWhiteSpace() || !setting.StartsWith("SqlServer.") + || !Enum.TryParse(setting.Substring("SqlServer.".Length), out VersionName versionName, true)) { + versionName = GetSetVersion(connectionString, ProviderName, _logger).ProductVersionName; } - public SqlServerSyntaxProvider(IOptions globalSettings, ILogger logger) + _logger.LogDebug("SqlServer {SqlServerVersion}, DatabaseType is {DatabaseType} ({Source}).", versionName, DatabaseType.SqlServer2012, fromSettings ? "settings" : "detected"); + + return DatabaseType.SqlServer2012; + } + + private static VersionName MapProductVersion(string productVersion) + { + var firstPart = string.IsNullOrWhiteSpace(productVersion) + ? "??" + : productVersion.Split(Core.Constants.CharArrays.Period)[0]; + switch (firstPart) { - _globalSettings = globalSettings; - _logger = logger; + case "??": + return VersionName.Invalid; + case "15": + return VersionName.V2019; + case "14": + return VersionName.V2017; + case "13": + return VersionName.V2016; + case "12": + return VersionName.V2014; + case "11": + return VersionName.V2012; + case "10": + return VersionName.V2008; + case "9": + return VersionName.V2005; + case "8": + return VersionName.V2000; + case "7": + return VersionName.V7; + default: + return VersionName.Other; + } + } + + internal ServerVersionInfo GetSetVersion(string? connectionString, string? providerName, ILogger logger) + { + // var factory = DbProviderFactories.GetFactory(providerName); + SqlClientFactory? factory = SqlClientFactory.Instance; + DbConnection? connection = factory.CreateConnection(); + + if (connection == null) + { + throw new InvalidOperationException($"Could not create a connection for provider \"{providerName}\"."); } - public override string ProviderName => Constants.ProviderName; + // Edition: "Express Edition", "Windows Azure SQL Database..." + // EngineEdition: 1/Desktop 2/Standard 3/Enterprise 4/Express 5/Azure + // ProductLevel: RTM, SPx, CTP... - public ServerVersionInfo? ServerVersion { get; private set; } - - public enum VersionName - { - Invalid = -1, - Unknown = 0, - V7 = 1, - V2000 = 2, - V2005 = 3, - V2008 = 4, - V2012 = 5, - V2014 = 6, - V2016 = 7, - V2017 = 8, - V2019 = 9, - Other = 99 - } - - public enum EngineEdition - { - Unknown = 0, - Desktop = 1, - Standard = 2, - Enterprise = 3,// Also developer edition - Express = 4, - Azure = 5 - } - - public override DatabaseType GetUpdatedDatabaseType(DatabaseType current, string? connectionString) - { - var setting = _globalSettings.Value.DatabaseFactoryServerVersion; - var fromSettings = false; - - if (setting.IsNullOrWhiteSpace() || !setting.StartsWith("SqlServer.") - || !Enum.TryParse(setting.Substring("SqlServer.".Length), out var versionName, true)) - { - versionName = GetSetVersion(connectionString, ProviderName, _logger).ProductVersionName; - } - - _logger.LogDebug("SqlServer {SqlServerVersion}, DatabaseType is {DatabaseType} ({Source}).", versionName, DatabaseType.SqlServer2012, fromSettings ? "settings" : "detected"); - - return DatabaseType.SqlServer2012; - } - - public class ServerVersionInfo - { - public ServerVersionInfo() - { - ProductVersionName = VersionName.Unknown; - EngineEdition = EngineEdition.Unknown; - } - - public ServerVersionInfo(string edition, string instanceName, string productVersion, EngineEdition engineEdition, string machineName, string productLevel) - { - Edition = edition; - InstanceName = instanceName; - ProductVersion = productVersion; - ProductVersionName = MapProductVersion(ProductVersion); - EngineEdition = engineEdition; - MachineName = machineName; - ProductLevel = productLevel; - } - - public string? Edition { get; } - public string? InstanceName { get; } - public string? ProductVersion { get; } - public VersionName ProductVersionName { get; } - public EngineEdition EngineEdition { get; } - public bool IsAzure => EngineEdition == EngineEdition.Azure; - public string? MachineName { get; } - public string? ProductLevel { get; } - } - - private static VersionName MapProductVersion(string productVersion) - { - var firstPart = string.IsNullOrWhiteSpace(productVersion) ? "??" : productVersion.Split(Cms.Core.Constants.CharArrays.Period)[0]; - switch (firstPart) - { - case "??": - return VersionName.Invalid; - case "15": - return VersionName.V2019; - case "14": - return VersionName.V2017; - case "13": - return VersionName.V2016; - case "12": - return VersionName.V2014; - case "11": - return VersionName.V2012; - case "10": - return VersionName.V2008; - case "9": - return VersionName.V2005; - case "8": - return VersionName.V2000; - case "7": - return VersionName.V7; - default: - return VersionName.Other; - } - } - - internal ServerVersionInfo GetSetVersion(string? connectionString, string? providerName, ILogger logger) - { - //var factory = DbProviderFactories.GetFactory(providerName); - var factory = SqlClientFactory.Instance; - var connection = factory.CreateConnection(); - - if (connection == null) - throw new InvalidOperationException($"Could not create a connection for provider \"{providerName}\"."); - - // Edition: "Express Edition", "Windows Azure SQL Database..." - // EngineEdition: 1/Desktop 2/Standard 3/Enterprise 4/Express 5/Azure - // ProductLevel: RTM, SPx, CTP... - - const string sql = @"select + const string sql = @"select SERVERPROPERTY('Edition') Edition, SERVERPROPERTY('EditionID') EditionId, SERVERPROPERTY('InstanceName') InstanceName, @@ -163,99 +151,102 @@ namespace Umbraco.Cms.Persistence.SqlServer.Services SERVERPROPERTY('ResourceLastUpdateDateTime') ResourceLastUpdateDateTime, SERVERPROPERTY('ProductLevel') ProductLevel;"; - string GetString(IDataReader reader, int ordinal, string defaultValue) - => reader.IsDBNull(ordinal) ? defaultValue : reader.GetString(ordinal); + string GetString(IDataReader reader, int ordinal, string defaultValue) + { + return reader.IsDBNull(ordinal) ? defaultValue : reader.GetString(ordinal); + } - int GetInt32(IDataReader reader, int ordinal, int defaultValue) - => reader.IsDBNull(ordinal) ? defaultValue : reader.GetInt32(ordinal); + int GetInt32(IDataReader reader, int ordinal, int defaultValue) + { + return reader.IsDBNull(ordinal) ? defaultValue : reader.GetInt32(ordinal); + } - connection.ConnectionString = connectionString; - ServerVersionInfo version; - using (connection) + connection.ConnectionString = connectionString; + ServerVersionInfo version; + using (connection) + { + try { - try + connection.Open(); + DbCommand command = connection.CreateCommand(); + command.CommandText = sql; + using (DbDataReader reader = command.ExecuteReader()) { - connection.Open(); - var command = connection.CreateCommand(); - command.CommandText = sql; - using (var reader = command.ExecuteReader()) - { - reader.Read(); - // InstanceName can be NULL for the default instance - version = new ServerVersionInfo( - GetString(reader, 0, "Unknown"), - GetString(reader, 2, "(default)"), - GetString(reader, 3, string.Empty), - (EngineEdition) GetInt32(reader, 5, 0), - GetString(reader, 7, "DEFAULT"), - GetString(reader, 9, "Unknown")); - } - connection.Close(); - } - catch (Exception e) - { - logger.LogError(e, "Failed to detected SqlServer version."); - version = new ServerVersionInfo(); // all unknown + reader.Read(); + // InstanceName can be NULL for the default instance + version = new ServerVersionInfo( + GetString(reader, 0, "Unknown"), + GetString(reader, 2, "(default)"), + GetString(reader, 3, string.Empty), + (EngineEdition)GetInt32(reader, 5, 0), + GetString(reader, 7, "DEFAULT"), + GetString(reader, 9, "Unknown")); } + + connection.Close(); + } + catch (Exception e) + { + logger.LogError(e, "Failed to detected SqlServer version."); + version = new ServerVersionInfo(); // all unknown } - - return ServerVersion = version; } - /// - /// SQL Server stores default values assigned to columns as constraints, it also stores them with named values, this is the only - /// server type that does this, therefore this method doesn't exist on any other syntax provider - /// - /// - public IEnumerable> GetDefaultConstraintsPerColumn(IDatabase db) - { - var items = db.Fetch("SELECT TableName = t.Name, ColumnName = c.Name, dc.Name, dc.[Definition] FROM sys.tables t INNER JOIN sys.default_constraints dc ON t.object_id = dc.parent_object_id INNER JOIN sys.columns c ON dc.parent_object_id = c.object_id AND c.column_id = dc.parent_column_id INNER JOIN sys.schemas as s on t.[schema_id] = s.[schema_id] WHERE s.name = (SELECT SCHEMA_NAME())"); - return items.Select(x => new Tuple(x.TableName, x.ColumnName, x.Name, x.Definition)); - } + return ServerVersion = version; + } - public override string DbProvider => ServerVersion?.IsAzure ?? false ? "SqlAzure" : "SqlServer"; + /// + /// SQL Server stores default values assigned to columns as constraints, it also stores them with named values, this is + /// the only + /// server type that does this, therefore this method doesn't exist on any other syntax provider + /// + /// + public IEnumerable> GetDefaultConstraintsPerColumn(IDatabase db) + { + List? items = db.Fetch( + "SELECT TableName = t.Name, ColumnName = c.Name, dc.Name, dc.[Definition] FROM sys.tables t INNER JOIN sys.default_constraints dc ON t.object_id = dc.parent_object_id INNER JOIN sys.columns c ON dc.parent_object_id = c.object_id AND c.column_id = dc.parent_column_id INNER JOIN sys.schemas as s on t.[schema_id] = s.[schema_id] WHERE s.name = (SELECT SCHEMA_NAME())"); + return items.Select(x => + new Tuple(x.TableName, x.ColumnName, x.Name, x.Definition)); + } - public override IEnumerable GetTablesInSchema(IDatabase db) - { - return db.Fetch("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())"); - } + public override IEnumerable GetTablesInSchema(IDatabase db) => db.Fetch( + "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())"); - public override IsolationLevel DefaultIsolationLevel => IsolationLevel.ReadCommitted; + public override IEnumerable GetColumnsInSchema(IDatabase db) + { + List? items = db.Fetch( + "SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())"); + return + items.Select( + item => + new ColumnInfo(item.TableName, item.ColumnName, item.OrdinalPosition, item.ColumnDefault, item.IsNullable, item.DataType)).ToList(); + } - public override IEnumerable GetColumnsInSchema(IDatabase db) - { - var items = db.Fetch("SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())"); - return - items.Select( - item => - new ColumnInfo(item.TableName, item.ColumnName, item.OrdinalPosition, item.ColumnDefault, - item.IsNullable, item.DataType)).ToList(); - } + /// + public override IEnumerable> GetConstraintsPerTable(IDatabase db) + { + List items = + db.Fetch( + "SELECT TABLE_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_TABLE_USAGE WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())"); + return items.Select(item => new Tuple(item.TableName, item.ConstraintName)).ToList(); + } - /// - public override IEnumerable> GetConstraintsPerTable(IDatabase db) - { - var items = - db.Fetch( - "SELECT TABLE_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_TABLE_USAGE WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())"); - return items.Select(item => new Tuple(item.TableName, item.ConstraintName)).ToList(); - } + /// + public override IEnumerable> GetConstraintsPerColumn(IDatabase db) + { + List? items = + db.Fetch( + "SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())"); + return items.Select(item => + new Tuple(item.TableName, item.ColumnName, item.ConstraintName)).ToList(); + } - /// - public override IEnumerable> GetConstraintsPerColumn(IDatabase db) - { - var items = - db.Fetch( - "SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())"); - return items.Select(item => new Tuple(item.TableName, item.ColumnName, item.ConstraintName)).ToList(); - } - - /// - public override IEnumerable> GetDefinedIndexes(IDatabase db) - { - var items = - db.Fetch( - @"select T.name as TABLE_NAME, I.name as INDEX_NAME, AC.Name as COLUMN_NAME, + /// + public override IEnumerable> GetDefinedIndexes(IDatabase db) + { + List? items = + db.Fetch( + @"select T.name as TABLE_NAME, I.name as INDEX_NAME, AC.Name as COLUMN_NAME, CASE WHEN I.is_unique_constraint = 1 OR I.is_unique = 1 THEN 1 ELSE 0 END AS [UNIQUE] from sys.tables as T inner join sys.indexes as I on T.[object_id] = I.[object_id] inner join sys.index_columns as IC on IC.[object_id] = I.[object_id] and IC.[index_id] = I.[index_id] @@ -263,182 +254,203 @@ from sys.tables as T inner join sys.indexes as I on T.[object_id] = I.[object_id inner join sys.schemas as S on T.[schema_id] = S.[schema_id] WHERE S.name = (SELECT SCHEMA_NAME()) AND I.is_primary_key = 0 order by T.name, I.name"); - return items.Select(item => new Tuple(item.TableName, item.IndexName, item.ColumnName, - item.Unique == 1)).ToList(); + return items.Select(item => new Tuple(item.TableName, item.IndexName, item.ColumnName, item.Unique == 1)).ToList(); + } - } - - /// - public override bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, [MaybeNullWhen(false)] out string constraintName) - { - constraintName = db.Fetch(@"select con.[name] as [constraintName] + /// + public override bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, [MaybeNullWhen(false)] out string constraintName) + { + constraintName = db.Fetch( + @"select con.[name] as [constraintName] from sys.default_constraints con join sys.columns col on con.object_id=col.default_object_id join sys.tables tbl on col.object_id=tbl.object_id -where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) - .FirstOrDefault(); - return !constraintName.IsNullOrWhiteSpace(); - } - - public override bool DoesTableExist(IDatabase db, string tableName) - { - var result = - db.ExecuteScalar("SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @TableName AND TABLE_SCHEMA = (SELECT SCHEMA_NAME())", - new { TableName = tableName }); - - return result > 0; - } - - public override string FormatColumnRename(string? tableName, string? oldName, string? newName) - { - return string.Format(RenameColumn, tableName, oldName, newName); - } - - public override string FormatTableRename(string? oldName, string? newName) - { - return string.Format(RenameTable, oldName, newName); - } - - protected override string FormatIdentity(ColumnDefinition column) - { - return column.IsIdentity ? GetIdentityString(column) : string.Empty; - } - - public override Sql SelectTop(Sql sql, int top) - { - return new Sql(sql.SqlContext, sql.SQL.Insert(sql.SQL.IndexOf(' '), " TOP " + top), sql.Arguments); - } - - private static string GetIdentityString(ColumnDefinition column) - { - return "IDENTITY(1,1)"; - } - - protected override string? FormatSystemMethods(SystemMethods systemMethod) - { - switch (systemMethod) - { - case SystemMethods.NewGuid: - return "NEWID()"; - case SystemMethods.CurrentDateTime: - return "GETDATE()"; - //case SystemMethods.NewSequentialId: - // return "NEWSEQUENTIALID()"; - //case SystemMethods.CurrentUTCDateTime: - // return "GETUTCDATE()"; - } - - return null; - } - - public override string DeleteDefaultConstraint => "ALTER TABLE {0} DROP CONSTRAINT {2}"; - - public override string DropIndex => "DROP INDEX {0} ON {1}"; - - public override string RenameColumn => "sp_rename '{0}.{1}', '{2}', 'COLUMN'"; - - public override string CreateIndex => "CREATE {0}{1}INDEX {2} ON {3} ({4}){5}"; - public override string Format(IndexDefinition index) - { - var name = string.IsNullOrEmpty(index.Name) - ? $"IX_{index.TableName}_{index.ColumnName}" - : index.Name; - - var columns = index.Columns.Any() - ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) - : GetQuotedColumnName(index.ColumnName); - - var includeColumns = index.IncludeColumns?.Any() ?? false - ? $" INCLUDE ({string.Join(",", index.IncludeColumns.Select(x => GetQuotedColumnName(x.Name)))})" - : string.Empty; - - return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), - GetQuotedTableName(index.TableName), columns, includeColumns); - } - - - public override Sql InsertForUpdateHint(Sql sql) - { - // go find the first FROM clause, and append the lock hint - Sql s = sql; - var updated = false; - - while (s != null) - { - var sqlText = SqlInspector.GetSqlText(s); - if (sqlText.StartsWith("FROM ", StringComparison.OrdinalIgnoreCase)) - { - SqlInspector.SetSqlText(s, sqlText + " WITH (UPDLOCK)"); - updated = true; - break; - } - - s = SqlInspector.GetSqlRhs(sql); - } - - if (updated) - SqlInspector.Reset(sql); - - return sql; - } - - public override Sql AppendForUpdateHint(Sql sql) - => sql.Append(" WITH (UPDLOCK) "); - - public override Sql.SqlJoinClause LeftJoinWithNestedJoin( - Sql sql, - Func, - Sql> nestedJoin, - string? alias = null) - { - Type type = typeof(TDto); - - var tableName = GetQuotedTableName(type.GetTableName()); - var join = tableName; - - if (alias != null) - { - var quotedAlias = GetQuotedTableName(alias); - join += " " + quotedAlias; - } - - var nestedSql = new Sql(sql.SqlContext); - nestedSql = nestedJoin(nestedSql); - - Sql.SqlJoinClause sqlJoin = sql.LeftJoin(join); - sql.Append(nestedSql); - return sqlJoin; - } - - #region Sql Inspection - - private static SqlInspectionUtilities? _sqlInspector; - - private static SqlInspectionUtilities SqlInspector => _sqlInspector ?? (_sqlInspector = new SqlInspectionUtilities()); - - private class SqlInspectionUtilities - { - private readonly Func _getSqlText; - private readonly Action _setSqlText; - private readonly Func _getSqlRhs; - private readonly Action _setSqlFinal; - - public SqlInspectionUtilities() - { - (_getSqlText, _setSqlText) = ReflectionUtilities.EmitFieldGetterAndSetter("_sql"); - _getSqlRhs = ReflectionUtilities.EmitFieldGetter("_rhs"); - _setSqlFinal = ReflectionUtilities.EmitFieldSetter("_sqlFinal"); - } - - public string GetSqlText(Sql sql) => _getSqlText(sql); - - public void SetSqlText(Sql sql, string sqlText) => _setSqlText(sql, sqlText); - - public Sql GetSqlRhs(Sql sql) => _getSqlRhs(sql); - - public void Reset(Sql sql) => _setSqlFinal(sql, null); - } - - #endregion +where tbl.[name]=@0 and col.[name]=@1;", + tableName, + columnName) + .FirstOrDefault(); + return !constraintName.IsNullOrWhiteSpace(); } + + public override bool DoesTableExist(IDatabase db, string tableName) + { + var result = + db.ExecuteScalar( + "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @TableName AND TABLE_SCHEMA = (SELECT SCHEMA_NAME())", + new { TableName = tableName }); + + return result > 0; + } + + public override string FormatColumnRename(string? tableName, string? oldName, string? newName) => + string.Format(RenameColumn, tableName, oldName, newName); + + public override string FormatTableRename(string? oldName, string? newName) => + string.Format(RenameTable, oldName, newName); + + protected override string FormatIdentity(ColumnDefinition column) => + column.IsIdentity ? GetIdentityString(column) : string.Empty; + + public override Sql SelectTop(Sql sql, int top) => new Sql(sql.SqlContext, sql.SQL.Insert(sql.SQL.IndexOf(' '), " TOP " + top), sql.Arguments); + + private static string GetIdentityString(ColumnDefinition column) => "IDENTITY(1,1)"; + + protected override string? FormatSystemMethods(SystemMethods systemMethod) + { + switch (systemMethod) + { + case SystemMethods.NewGuid: + return "NEWID()"; + case SystemMethods.CurrentDateTime: + return "GETDATE()"; + + // case SystemMethods.NewSequentialId: + // return "NEWSEQUENTIALID()"; + // case SystemMethods.CurrentUTCDateTime: + // return "GETUTCDATE()"; + } + + return null; + } + + public override string Format(IndexDefinition index) + { + var name = string.IsNullOrEmpty(index.Name) + ? $"IX_{index.TableName}_{index.ColumnName}" + : index.Name; + + var columns = index.Columns.Any() + ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) + : GetQuotedColumnName(index.ColumnName); + + var includeColumns = index.IncludeColumns?.Any() ?? false + ? $" INCLUDE ({string.Join(",", index.IncludeColumns.Select(x => GetQuotedColumnName(x.Name)))})" + : string.Empty; + + return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), GetQuotedTableName(index.TableName), columns, includeColumns); + } + + + public override Sql InsertForUpdateHint(Sql sql) + { + // go find the first FROM clause, and append the lock hint + Sql s = sql; + var updated = false; + + while (s != null) + { + var sqlText = SqlInspector.GetSqlText(s); + if (sqlText.StartsWith("FROM ", StringComparison.OrdinalIgnoreCase)) + { + SqlInspector.SetSqlText(s, sqlText + " WITH (UPDLOCK)"); + updated = true; + break; + } + + s = SqlInspector.GetSqlRhs(sql); + } + + if (updated) + { + SqlInspector.Reset(sql); + } + + return sql; + } + + public override Sql AppendForUpdateHint(Sql sql) + => sql.Append(" WITH (UPDLOCK) "); + + public override Sql.SqlJoinClause LeftJoinWithNestedJoin( + Sql sql, + Func, + Sql> nestedJoin, + string? alias = null) + { + Type type = typeof(TDto); + + var tableName = GetQuotedTableName(type.GetTableName()); + var join = tableName; + + if (alias != null) + { + var quotedAlias = GetQuotedTableName(alias); + join += " " + quotedAlias; + } + + var nestedSql = new Sql(sql.SqlContext); + nestedSql = nestedJoin(nestedSql); + + Sql.SqlJoinClause sqlJoin = sql.LeftJoin(join); + sql.Append(nestedSql); + return sqlJoin; + } + + public class ServerVersionInfo + { + public ServerVersionInfo() + { + ProductVersionName = VersionName.Unknown; + EngineEdition = EngineEdition.Unknown; + } + + public ServerVersionInfo(string edition, string instanceName, string productVersion, EngineEdition engineEdition, string machineName, string productLevel) + { + Edition = edition; + InstanceName = instanceName; + ProductVersion = productVersion; + ProductVersionName = MapProductVersion(ProductVersion); + EngineEdition = engineEdition; + MachineName = machineName; + ProductLevel = productLevel; + } + + public string? Edition { get; } + + public string? InstanceName { get; } + + public string? ProductVersion { get; } + + public VersionName ProductVersionName { get; } + + public EngineEdition EngineEdition { get; } + + public bool IsAzure => EngineEdition == EngineEdition.Azure; + + public string? MachineName { get; } + + public string? ProductLevel { get; } + } + + #region Sql Inspection + + private static SqlInspectionUtilities? _sqlInspector; + + private static SqlInspectionUtilities SqlInspector => +_sqlInspector ??= new SqlInspectionUtilities(); + + private class SqlInspectionUtilities + { + private readonly Func _getSqlRhs; + private readonly Func _getSqlText; + private readonly Action _setSqlFinal; + private readonly Action _setSqlText; + + public SqlInspectionUtilities() + { + (_getSqlText, _setSqlText) = ReflectionUtilities.EmitFieldGetterAndSetter("_sql"); + _getSqlRhs = ReflectionUtilities.EmitFieldGetter("_rhs"); + _setSqlFinal = ReflectionUtilities.EmitFieldSetter("_sqlFinal"); + } + + public string GetSqlText(Sql sql) => _getSqlText(sql); + + public void SetSqlText(Sql sql, string sqlText) => _setSqlText(sql, sqlText); + + public Sql GetSqlRhs(Sql sql) => _getSqlRhs(sql); + + public void Reset(Sql sql) => _setSqlFinal(sql, null); + } + + #endregion } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/SqlServerComposer.cs b/src/Umbraco.Cms.Persistence.SqlServer/SqlServerComposer.cs index 60d64c09df..e81a65e74f 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/SqlServerComposer.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/SqlServerComposer.cs @@ -4,7 +4,7 @@ using Umbraco.Cms.Core.DependencyInjection; namespace Umbraco.Cms.Persistence.SqlServer; /// -/// Automatically adds SQL Server support to Umbraco when this project is referenced. +/// Automatically adds SQL Server support to Umbraco when this project is referenced. /// public class SqlServerComposer : IComposer { diff --git a/src/Umbraco.Cms.Persistence.SqlServer/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Persistence.SqlServer/UmbracoBuilderExtensions.cs index 4b9fb8d11a..a47e92bf2e 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/UmbracoBuilderExtensions.cs @@ -13,27 +13,34 @@ using Umbraco.Cms.Persistence.SqlServer.Services; namespace Umbraco.Cms.Persistence.SqlServer; /// -/// SQLite support extensions for IUmbracoBuilder. +/// SQLite support extensions for IUmbracoBuilder. /// public static class UmbracoBuilderExtensions { /// - /// Add required services for SQL Server support. + /// Add required services for SQL Server support. /// public static IUmbracoBuilder AddUmbracoSqlServerSupport(this IUmbracoBuilder builder) { builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); DbProviderFactories.UnregisterFactory(Constants.ProviderName); DbProviderFactories.RegisterFactory(Constants.ProviderName, SqlClientFactory.Instance); diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Constants.cs b/src/Umbraco.Cms.Persistence.Sqlite/Constants.cs index 76e408423c..63f0d8f9fe 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Constants.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Constants.cs @@ -1,12 +1,12 @@ namespace Umbraco.Cms.Persistence.Sqlite; /// -/// Constants related to SQLite. +/// Constants related to SQLite. /// public static class Constants { /// - /// SQLite provider name. + /// SQLite provider name. /// public const string ProviderName = "Microsoft.Data.SQLite"; } diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddMiniProfilerInterceptor.cs b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddMiniProfilerInterceptor.cs index eb76319040..9d7f3b29ad 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddMiniProfilerInterceptor.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddMiniProfilerInterceptor.cs @@ -1,11 +1,12 @@ using System.Data.Common; using NPoco; using StackExchange.Profiling; +using StackExchange.Profiling.Data; namespace Umbraco.Cms.Persistence.Sqlite.Interceptors; public class SqliteAddMiniProfilerInterceptor : SqliteConnectionInterceptor { public override DbConnection OnConnectionOpened(IDatabase database, DbConnection conn) - => new StackExchange.Profiling.Data.ProfiledDbConnection(conn, MiniProfiler.Current); + => new ProfiledDbConnection(conn, MiniProfiler.Current); } diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddPreferDeferredInterceptor.cs b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddPreferDeferredInterceptor.cs index ef22e9c0b6..5c195291b0 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddPreferDeferredInterceptor.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddPreferDeferredInterceptor.cs @@ -8,5 +8,6 @@ namespace Umbraco.Cms.Persistence.Sqlite.Interceptors; public class SqliteAddPreferDeferredInterceptor : SqliteConnectionInterceptor { public override DbConnection OnConnectionOpened(IDatabase database, DbConnection conn) - => new SqlitePreferDeferredTransactionsConnection(conn as SqliteConnection ?? throw new InvalidOperationException()); + => new SqlitePreferDeferredTransactionsConnection(conn as SqliteConnection ?? + throw new InvalidOperationException()); } diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqliteGuidScalarMapper.cs b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqliteGuidScalarMapper.cs index bd9bb1924d..eac4152df4 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqliteGuidScalarMapper.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqliteGuidScalarMapper.cs @@ -14,7 +14,7 @@ public class SqliteNullableGuidScalarMapper : ScalarMapper { if (value is null || value == DBNull.Value) { - return default(Guid?); + return default; } return Guid.TryParse($"{value}", out Guid result) diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs index f7b2836f1a..930478e453 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs @@ -8,7 +8,7 @@ public class SqlitePocoGuidMapper : DefaultMapper { if (destType == typeof(Guid)) { - return (value) => + return value => { var result = Guid.Parse($"{value}"); return result; @@ -17,7 +17,7 @@ public class SqlitePocoGuidMapper : DefaultMapper if (destType == typeof(Guid?)) { - return (value) => + return value => { if (Guid.TryParse($"{value}", out Guid result)) { diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteBulkSqlInsertProvider.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteBulkSqlInsertProvider.cs index 895ee21ef6..ff0a64a74b 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteBulkSqlInsertProvider.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteBulkSqlInsertProvider.cs @@ -4,7 +4,7 @@ using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Cms.Persistence.Sqlite.Services; /// -/// Implements for SQLite. +/// Implements for SQLite. /// public class SqliteBulkSqlInsertProvider : IBulkSqlInsertProvider { @@ -12,17 +12,23 @@ public class SqliteBulkSqlInsertProvider : IBulkSqlInsertProvider public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) { - var recordsA = records.ToArray(); - if (recordsA.Length == 0) return 0; + T[] recordsA = records.ToArray(); + if (recordsA.Length == 0) + { + return 0; + } - var pocoData = database.PocoDataFactory.ForType(typeof(T)); - if (pocoData == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); + PocoData? pocoData = database.PocoDataFactory.ForType(typeof(T)); + if (pocoData == null) + { + throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); + } return BulkInsertRecordsSqlite(database, pocoData, recordsA); } /// - /// Bulk-insert records using SqlServer BulkCopy method. + /// Bulk-insert records using SqlServer BulkCopy method. /// /// The type of the records. /// The database. @@ -39,7 +45,7 @@ public class SqliteBulkSqlInsertProvider : IBulkSqlInsertProvider database.BeginTransaction(); } - foreach (var record in records) + foreach (T record in records) { database.Insert(record); count++; diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs index 84c8eea1db..54d6063ad7 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; using Umbraco.Cms.Infrastructure.Persistence; @@ -6,38 +5,35 @@ using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Cms.Persistence.Sqlite.Services; /// -/// Implements for SQLite. +/// Implements for SQLite. /// public class SqliteDatabaseCreator : IDatabaseCreator { private readonly ILogger _logger; + public SqliteDatabaseCreator(ILogger logger) => _logger = logger; + /// public string ProviderName => Constants.ProviderName; - public SqliteDatabaseCreator(ILogger logger) - { - _logger = logger; - } - /// - /// Creates a SQLite database file. + /// Creates a SQLite database file. /// /// - /// - /// With journal_mode = wal we have snapshot isolation. - /// - /// - /// Concurrent read/write can take occur, committing a write transaction will have no impact - /// on open read transactions as they see only committed data from the point in time that they began reading. - /// - /// - /// A write transaction still requires exclusive access to database files so concurrent writes are not possible. - /// - /// - /// Read more Isolation in SQLite
- /// Read more Write-Ahead Logging - ///
+ /// + /// With journal_mode = wal we have snapshot isolation. + /// + /// + /// Concurrent read/write can take occur, committing a write transaction will have no impact + /// on open read transactions as they see only committed data from the point in time that they began reading. + /// + /// + /// A write transaction still requires exclusive access to database files so concurrent writes are not possible. + /// + /// + /// Read more Isolation in SQLite
+ /// Read more Write-Ahead Logging + ///
///
public void Create(string connectionString) { @@ -88,7 +84,7 @@ public class SqliteDatabaseCreator : IDatabaseCreator // Copy our blank(ish) wal mode sqlite database to its final location. try { - File.Copy(tempFile, original.DataSource, overwrite: true); + File.Copy(tempFile, original.DataSource, true); } catch (Exception ex) { diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs index 0b684551c7..54a7a5cb1d 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs @@ -10,7 +10,7 @@ namespace Umbraco.Cms.Persistence.Sqlite.Services; public class SqliteDatabaseProviderMetadata : IDatabaseProviderMetadata { /// - public Guid Id => new ("530386a2-b219-4d5f-b68c-b965e14c9ac9"); + public Guid Id => new("530386a2-b219-4d5f-b68c-b965e14c9ac9"); /// public int SortOrder => -1; @@ -47,12 +47,12 @@ public class SqliteDatabaseProviderMetadata : IDatabaseProviderMetadata /// /// - /// - /// Required to ensure database creator is used regardless of configured InstallMissingDatabase value. - /// - /// - /// Ensures database setup with journal_mode = wal; - /// + /// + /// Required to ensure database creator is used regardless of configured InstallMissingDatabase value. + /// + /// + /// Ensures database setup with journal_mode = wal; + /// /// public bool ForceCreateDatabase => true; @@ -64,7 +64,7 @@ public class SqliteDatabaseProviderMetadata : IDatabaseProviderMetadata DataSource = $"{ConnectionStrings.DataDirectoryPlaceholder}/{databaseModel.DatabaseName}.sqlite.db", ForeignKeys = true, Pooling = true, - Cache = SqliteCacheMode.Shared, + Cache = SqliteCacheMode.Shared }; return builder.ConnectionString; diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs index 4a47d41846..c704bb8272 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs @@ -4,7 +4,6 @@ using Microsoft.Data.SqlClient; using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DistributedLocking; using Umbraco.Cms.Core.DistributedLocking.Exceptions; @@ -17,10 +16,10 @@ namespace Umbraco.Cms.Persistence.Sqlite.Services; public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism { - private readonly ILogger _logger; - private readonly Lazy _scopeAccessor; private readonly IOptionsMonitor _connectionStrings; private readonly IOptionsMonitor _globalSettings; + private readonly ILogger _logger; + private readonly Lazy _scopeAccessor; public SqliteDistributedLockingMechanism( ILogger logger, @@ -101,11 +100,9 @@ public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism public DistributedLockType LockType { get; } - public void Dispose() - { + public void Dispose() => // Mostly no op, cleaned up by completing transaction in scope. _parent._logger.LogDebug("Dropped {lockType} for id {id}", LockType, LockId); - } public override string ToString() => $"SqliteDistributedLock({LockId})"; @@ -123,7 +120,8 @@ public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism if (!db.InTransaction) { - throw new InvalidOperationException("SqliteDistributedLockingMechanism requires a transaction to function."); + throw new InvalidOperationException( + "SqliteDistributedLockingMechanism requires a transaction to function."); } } @@ -140,7 +138,8 @@ public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism if (!db.InTransaction) { - throw new InvalidOperationException("SqliteDistributedLockingMechanism requires a transaction to function."); + throw new InvalidOperationException( + "SqliteDistributedLockingMechanism requires a transaction to function."); } var query = @$"UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id = {LockId}"; diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteExceptionExtensions.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteExceptionExtensions.cs index 4076718266..acc8afb508 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteExceptionExtensions.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteExceptionExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.Data.Sqlite; +using SQLitePCL; namespace Umbraco.Cms.Persistence.Sqlite.Services; @@ -6,7 +7,7 @@ public static class SqliteExceptionExtensions { public static bool IsBusyOrLocked(this SqliteException ex) => ex.SqliteErrorCode - is SQLitePCL.raw.SQLITE_BUSY - or SQLitePCL.raw.SQLITE_LOCKED - or SQLitePCL.raw.SQLITE_LOCKED_SHAREDCACHE; + is raw.SQLITE_BUSY + or raw.SQLITE_LOCKED + or raw.SQLITE_LOCKED_SHAREDCACHE; } diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqlitePreferDeferredTransactionsConnection.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqlitePreferDeferredTransactionsConnection.cs index 4e126056d6..7c5a9f56f8 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqlitePreferDeferredTransactionsConnection.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqlitePreferDeferredTransactionsConnection.cs @@ -9,22 +9,7 @@ public class SqlitePreferDeferredTransactionsConnection : DbConnection { private readonly SqliteConnection _inner; - public SqlitePreferDeferredTransactionsConnection(SqliteConnection inner) - { - _inner = inner; - } - - protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) - => _inner.BeginTransaction(isolationLevel, deferred: true); // <-- The important bit - - public override void ChangeDatabase(string databaseName) - => _inner.ChangeDatabase(databaseName); - - public override void Close() - => _inner.Close(); - - public override void Open() - => _inner.Open(); + public SqlitePreferDeferredTransactionsConnection(SqliteConnection inner) => _inner = inner; public override string Database => _inner.Database; @@ -38,9 +23,6 @@ public class SqlitePreferDeferredTransactionsConnection : DbConnection public override string ServerVersion => _inner.ServerVersion; - protected override DbCommand CreateDbCommand() - => new CommandWrapper(_inner.CreateCommand()); - [AllowNull] public override string ConnectionString { @@ -48,26 +30,26 @@ public class SqlitePreferDeferredTransactionsConnection : DbConnection set => _inner.ConnectionString = value; } + protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) + => _inner.BeginTransaction(isolationLevel, true); // <-- The important bit + + public override void ChangeDatabase(string databaseName) + => _inner.ChangeDatabase(databaseName); + + public override void Close() + => _inner.Close(); + + public override void Open() + => _inner.Open(); + + protected override DbCommand CreateDbCommand() + => new CommandWrapper(_inner.CreateCommand()); + private class CommandWrapper : DbCommand { private readonly DbCommand _inner; - public CommandWrapper(DbCommand inner) - { - _inner = inner; - } - - public override void Cancel() - => _inner.Cancel(); - - public override int ExecuteNonQuery() - => _inner.ExecuteNonQuery(); - - public override object? ExecuteScalar() - => _inner.ExecuteScalar(); - - public override void Prepare() - => _inner.Prepare(); + public CommandWrapper(DbCommand inner) => _inner = inner; [AllowNull] public override string CommandText @@ -97,10 +79,7 @@ public class SqlitePreferDeferredTransactionsConnection : DbConnection protected override DbConnection? DbConnection { get => _inner.Connection; - set - { - _inner.Connection = (value as SqlitePreferDeferredTransactionsConnection)?._inner; - } + set => _inner.Connection = (value as SqlitePreferDeferredTransactionsConnection)?._inner; } protected override DbParameterCollection DbParameterCollection @@ -118,6 +97,18 @@ public class SqlitePreferDeferredTransactionsConnection : DbConnection set => _inner.DesignTimeVisible = value; } + public override void Cancel() + => _inner.Cancel(); + + public override int ExecuteNonQuery() + => _inner.ExecuteNonQuery(); + + public override object? ExecuteScalar() + => _inner.ExecuteScalar(); + + public override void Prepare() + => _inner.Prepare(); + protected override DbParameter CreateDbParameter() => _inner.CreateParameter(); diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs index cf1d707d69..66f542712a 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs @@ -4,7 +4,7 @@ using Umbraco.Cms.Persistence.Sqlite.Mappers; namespace Umbraco.Cms.Persistence.Sqlite.Services; /// -/// Implements for SQLite. +/// Implements for SQLite. /// public class SqliteSpecificMapperFactory : IProviderSpecificMapperFactory { @@ -12,5 +12,5 @@ public class SqliteSpecificMapperFactory : IProviderSpecificMapperFactory public string ProviderName => Constants.ProviderName; /// - public NPocoMapperCollection Mappers => new NPocoMapperCollection(() => new[] { new SqlitePocoGuidMapper() }); + public NPocoMapperCollection Mappers => new(() => new[] { new SqlitePocoGuidMapper() }); } diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs index 412be347a6..960ef4451c 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs @@ -54,14 +54,12 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase /// public override string DbProvider => Constants.ProviderName; - /// public override bool SupportsIdentityInsert() => false; /// public override bool SupportsClustered() => false; - public override string GetIndexType(IndexTypes indexTypes) { switch (indexTypes) @@ -143,7 +141,9 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase public override string ConvertIntegerToOrderableString => "substr('0000000000'||'{0}', -10, 10)"; + public override string ConvertDecimalToOrderableString => "substr('0000000000'||'{0}', -10, 10)"; + public override string ConvertDateToOrderableString => "{0}"; /// @@ -153,16 +153,14 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase public override string GetSpecialDbType(SpecialDbType dbType, int customSize) => GetSpecialDbType(dbType); /// - public override bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, - out string constraintName) + public override bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, out string constraintName) { // TODO: SQLite constraintName = string.Empty; return false; } - public override string GetFieldNameForUpdate(Expression> fieldSelector, - string? tableAlias = null) + public override string GetFieldNameForUpdate(Expression> fieldSelector, string? tableAlias = null) { var field = ExpressionHelper.FindProperty(fieldSelector).Item1 as PropertyInfo; var fieldName = GetColumnName(field!); @@ -242,14 +240,12 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase return $"CONSTRAINT {constraintName} {constraintType} ({columns})"; } - /// public override Sql SelectTop(Sql sql, int top) { // SQLite uses LIMIT as opposed to TOP // SELECT TOP 5 * FROM My_Table // SELECT * FROM My_Table LIMIT 5; - return sql.Append($"LIMIT {top}"); } @@ -264,8 +260,7 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase return sb.ToString().TrimStart(','); } - public override void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, - bool skipKeysAndIndexes = false) + public override void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false) { var columns = Format(tableDefinition.Columns); var primaryKey = FormatPrimaryKey(tableDefinition); @@ -334,17 +329,16 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase } } - /// public override IEnumerable> GetConstraintsPerColumn(IDatabase db) { - var items = db.Fetch("select * from sqlite_master where type = 'table'") + IEnumerable items = db.Fetch("select * from sqlite_master where type = 'table'") .Where(x => !x.Name.StartsWith("sqlite_")); List foundConstraints = new(); foreach (SqliteMaster row in items) { - var altPk = Regex.Match(row.Sql, @"CONSTRAINT (?PK_\w+)\s.*UNIQUE \(""(?.+?)""\)"); + Match altPk = Regex.Match(row.Sql, @"CONSTRAINT (?PK_\w+)\s.*UNIQUE \(""(?.+?)""\)"); if (altPk.Success) { var field = altPk.Groups["field"].Value; @@ -353,14 +347,14 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase } else { - var identity = Regex.Match(row.Sql, @"""(?.+)"".*AUTOINCREMENT"); + Match identity = Regex.Match(row.Sql, @"""(?.+)"".*AUTOINCREMENT"); if (identity.Success) { foundConstraints.Add(new Constraint(row.Name, identity.Groups["field"].Value, $"PK_{row.Name}")); } } - var pk = Regex.Match(row.Sql, @"CONSTRAINT (?\w+)\s.*PRIMARY KEY \(""(?.+?)""\)"); + Match pk = Regex.Match(row.Sql, @"CONSTRAINT (?\w+)\s.*PRIMARY KEY \(""(?.+?)""\)"); if (pk.Success) { var field = pk.Groups["field"].Value; @@ -369,9 +363,9 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase } var fkRegex = new Regex(@"CONSTRAINT (?\w+) FOREIGN KEY \(""(?.+?)""\) REFERENCES"); - var foreignKeys = fkRegex.Matches(row.Sql).Cast(); + IEnumerable foreignKeys = fkRegex.Matches(row.Sql).Cast(); { - foreach (var fk in foreignKeys) + foreach (Match fk in foreignKeys) { var field = fk.Groups["field"].Value; var constraint = fk.Groups["constraint"].Value; @@ -381,8 +375,7 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase } // item.TableName, item.ColumnName, item.ConstraintName - return foundConstraints - .Select(x => Tuple.Create(x.TableName, x.ColumnName, x.ConstraintName)); + return foundConstraints.Select(x => Tuple.Create(x.TableName, x.ColumnName, x.ConstraintName)); } public override Sql.SqlJoinClause LeftJoinWithNestedJoin( @@ -435,15 +428,20 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase private class SqliteMaster { public string Type { get; set; } = null!; + public string Name { get; set; } = null!; + public string Sql { get; set; } = null!; } private class IndexMeta { public string TableName { get; set; } = null!; + public string IndexName { get; set; } = null!; + public string ColumnName { get; set; } = null!; + public bool IsUnique { get; set; } } } diff --git a/src/Umbraco.Cms.Persistence.Sqlite/SqliteComposer.cs b/src/Umbraco.Cms.Persistence.Sqlite/SqliteComposer.cs index d638f713a2..f9aa15bd77 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/SqliteComposer.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/SqliteComposer.cs @@ -4,7 +4,7 @@ using Umbraco.Cms.Core.DependencyInjection; namespace Umbraco.Cms.Persistence.Sqlite; /// -/// Automatically adds SQLite support to Umbraco when this project is referenced. +/// Automatically adds SQLite support to Umbraco when this project is referenced. /// public class SqliteComposer : IComposer { diff --git a/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs index 8843844818..b617831b86 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs @@ -14,30 +14,37 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Persistence.Sqlite; /// -/// SQLite support extensions for IUmbracoBuilder. +/// SQLite support extensions for IUmbracoBuilder. /// public static class UmbracoBuilderExtensions { /// - /// Add required services for SQLite support. + /// Add required services for SQLite support. /// public static IUmbracoBuilder AddUmbracoSqliteSupport(this IUmbracoBuilder builder) { // TryAddEnumerable takes both TService and TImplementation into consideration (unlike TryAddSingleton) builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); DbProviderFactories.UnregisterFactory(Constants.ProviderName); - DbProviderFactories.RegisterFactory(Constants.ProviderName, Microsoft.Data.Sqlite.SqliteFactory.Instance); + DbProviderFactories.RegisterFactory(Constants.ProviderName, SqliteFactory.Instance); // Prevent accidental creation of SQLite database files builder.Services.PostConfigureAll(options =>