diff --git a/Changelog.md b/Changelog.md index f9d0557..e298766 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,7 @@ - Target Framework supported: netstandard2.0, net462 - Updated System.Data.SqlClient 4.8.5 +- Added Azure SQL support to MsSqlDialect [#31](https://github.com/NEventStore/NEventStore.Persistence.SQL/issues/31) - Fix: NEventStore constraint failed with MySql 8.x (works with 5.7) [#487](https://github.com/NEventStore/NEventStore/issues/487) ### Breaking Change @@ -11,6 +12,7 @@ - The fix for [#487](https://github.com/NEventStore/NEventStore/issues/487) changed how the `Commits` table is created for MySql 8.x: to update an existing database in order to run on 8.x you need to manually update the `Commits` table schema and change the constraint of the `CommitId` column from: `CommitId binary(16) NOT NULL CHECK (CommitId != 0)` to: `CommitId binary(16) NOT NULL CHECK (CommitId <> 0x00)`. +- MsSqlDialects now have an `useAzureSql` parameter, if set to `true` the statement `WITH (READCOMMITTEDLOCK)` will be added to any `FROM Commits` query. ## 9.0.1 diff --git a/src/NEventStore.Persistence.MsSql.Tests/PersistenceEngineFixture.cs b/src/NEventStore.Persistence.MsSql.Tests/PersistenceEngineFixture.cs index 0e62580..6ac6d95 100644 --- a/src/NEventStore.Persistence.MsSql.Tests/PersistenceEngineFixture.cs +++ b/src/NEventStore.Persistence.MsSql.Tests/PersistenceEngineFixture.cs @@ -1,20 +1,20 @@ using NEventStore.Persistence.Sql.Tests; -namespace NEventStore.Persistence.AcceptanceTests -{ +namespace NEventStore.Persistence.AcceptanceTests { using NEventStore.Persistence.Sql; using NEventStore.Persistence.Sql.SqlDialects; using NEventStore.Serialization; using System.Data.SqlClient; using System.Transactions; - public partial class PersistenceEngineFixture - { + public partial class PersistenceEngineFixture { + public ISqlDialect SqlDialect { get; set; } = new MsSqlDialect(); + /// /// this mimic the current NEventStore default values which is run outside any transaction (creates a scope that /// suppresses any transaction) /// - public TransactionScopeOption? ScopeOption { get; set; } = null; // the old default: TransactionScopeOption.Suppress; + public TransactionScopeOption? ScopeOption { get; set; } // the old default: TransactionScopeOption.Suppress; public PersistenceEngineFixture() { @@ -22,7 +22,7 @@ public PersistenceEngineFixture() _createPersistence = pageSize => new SqlPersistenceFactory(new EnviromentConnectionFactory("MsSql", "System.Data.SqlClient"), new BinarySerializer(), - new MsSqlDialect(), + SqlDialect, pageSize: pageSize, scopeOption: ScopeOption ).Build(); @@ -30,7 +30,7 @@ public PersistenceEngineFixture() _createPersistence = pageSize => new SqlPersistenceFactory(new EnviromentConnectionFactory("MsSql", SqlClientFactory.Instance), new BinarySerializer(), - new MsSqlDialect(), + SqlDialect, pageSize: pageSize, scopeOption: ScopeOption ).Build(); diff --git a/src/NEventStore.Persistence.Sql.Tests/PersistenceTests.Transactions.cs b/src/NEventStore.Persistence.Sql.Tests/PersistenceTests.Transactions.cs index 63fd41e..2f1333b 100644 --- a/src/NEventStore.Persistence.Sql.Tests/PersistenceTests.Transactions.cs +++ b/src/NEventStore.Persistence.Sql.Tests/PersistenceTests.Transactions.cs @@ -25,6 +25,10 @@ namespace NEventStore.Persistence.AcceptanceTests using Xunit.Should; #endif + // The following tests actually works well only for MsSqlServer + // we'll need some refactoring to have initialization work + // for different of kinds of databases. + public enum TransactionScopeConcern { NoTransaction = 0, @@ -319,11 +323,11 @@ public class Unsupported_Multiple_Completing_TransactionScopes_When_EnlistInAmbi public Unsupported_Multiple_Completing_TransactionScopes_When_EnlistInAmbientTransaction_is_and_IsolationLevel_is( TransactionScopeConcern enlistInAmbientTransaction, IsolationLevel transationIsolationLevel - ) : base(enlistInAmbientTransaction, transationIsolationLevel, true) + ) : base(enlistInAmbientTransaction, transationIsolationLevel, completeTransaction: true) { } [Fact] - public void should_throw_an_Exception_only_if_no_transaction_or_enlist_in_ambient_transaction_and_IsolationLevel_is_Serializable() + public void Should_throw_an_Exception_only_if_no_transaction_or_enlist_in_ambient_transaction_and_IsolationLevel_is_Serializable() { _thrown.Should().BeOfType(); _thrown.InnerException.Should().BeOfType(); diff --git a/src/NEventStore.Persistence.Sql/SqlDialects/MsSqlDialect.cs b/src/NEventStore.Persistence.Sql/SqlDialects/MsSqlDialect.cs index 0de1364..061d8cc 100644 --- a/src/NEventStore.Persistence.Sql/SqlDialects/MsSqlDialect.cs +++ b/src/NEventStore.Persistence.Sql/SqlDialects/MsSqlDialect.cs @@ -2,78 +2,77 @@ using System.Transactions; using IsolationLevel = System.Data.IsolationLevel; -namespace NEventStore.Persistence.Sql.SqlDialects -{ +namespace NEventStore.Persistence.Sql.SqlDialects { using System; using System.Data.SqlClient; - public class MsSqlDialect : CommonSqlDialect - { + public class MsSqlDialect : CommonSqlDialect { private const int UniqueIndexViolation = 2601; private const int UniqueKeyViolation = 2627; - public override string InitializeStorage - { + /// + /// Add "WITH (READCOMMITTEDLOCK)" hint to any "FROM Commits" clause + /// (#31) Make MsSqlDialect compatible with AzureSql and READ COMMITTED SNAPSHOT + /// + private readonly bool _addReadCommittedLockToFromCommits; + + /// + /// Constructor + /// + /// Add "WITH (READCOMMITTEDLOCK)" hint to any "FROM Commits" clause, can have an impact on multiple transactions scenarios + public MsSqlDialect(bool addReadCommittedLockToFromCommits = false) { + _addReadCommittedLockToFromCommits = addReadCommittedLockToFromCommits; + } + + public override string InitializeStorage { get { return MsSqlStatements.InitializeStorage; } } - public override string GetSnapshot - { + public override string GetSnapshot { get { return "SET ROWCOUNT 1;\n" + base.GetSnapshot.Replace("LIMIT 1;", ";"); } } - public override string GetCommitsFromStartingRevision - { - get { return NaturalPaging(base.GetCommitsFromStartingRevision); } + public override string GetCommitsFromStartingRevision { + get { return AddReadCommittedLockToFromCommits(NaturalPaging(base.GetCommitsFromStartingRevision)); } } - public override string GetCommitsFromInstant - { - get { return CommonTableExpressionPaging(base.GetCommitsFromInstant); } + public override string GetCommitsFromInstant { + get { return AddReadCommittedLockToFromCommits(CommonTableExpressionPaging(base.GetCommitsFromInstant)); } } - public override string GetCommitsFromToInstant - { - get { return CommonTableExpressionPaging(base.GetCommitsFromToInstant); } + public override string GetCommitsFromToInstant { + get { return AddReadCommittedLockToFromCommits(CommonTableExpressionPaging(base.GetCommitsFromToInstant)); } } - public override string PersistCommit - { + public override string PersistCommit { get { return MsSqlStatements.PersistCommits; } } - public override string GetCommitsFromCheckpoint - { - get { return CommonTableExpressionPaging(base.GetCommitsFromCheckpoint); } + public override string GetCommitsFromCheckpoint { + get { return AddReadCommittedLockToFromCommits(CommonTableExpressionPaging(base.GetCommitsFromCheckpoint)); } } - public override string GetCommitsFromToCheckpoint - { - get { return CommonTableExpressionPaging(base.GetCommitsFromToCheckpoint); } + public override string GetCommitsFromToCheckpoint { + get { return AddReadCommittedLockToFromCommits(CommonTableExpressionPaging(base.GetCommitsFromToCheckpoint)); } } - public override string GetCommitsFromBucketAndCheckpoint - { - get { return CommonTableExpressionPaging(base.GetCommitsFromBucketAndCheckpoint); } + public override string GetCommitsFromBucketAndCheckpoint { + get { return AddReadCommittedLockToFromCommits(CommonTableExpressionPaging(base.GetCommitsFromBucketAndCheckpoint)); } } - public override string GetCommitsFromToBucketAndCheckpoint - { - get { return CommonTableExpressionPaging(base.GetCommitsFromToBucketAndCheckpoint); } + public override string GetCommitsFromToBucketAndCheckpoint { + get { return AddReadCommittedLockToFromCommits(CommonTableExpressionPaging(base.GetCommitsFromToBucketAndCheckpoint)); } } - public override string GetStreamsRequiringSnapshots - { + public override string GetStreamsRequiringSnapshots { get { return NaturalPaging(base.GetStreamsRequiringSnapshots); } } - private static string NaturalPaging(string query) - { + private static string NaturalPaging(string query) { return "SET ROWCOUNT @Limit;\n" + RemovePaging(query); } - private static string CommonTableExpressionPaging(string query) - { + private static string CommonTableExpressionPaging(string query) { query = RemovePaging(query); int orderByIndex = query.IndexOf("ORDER BY"); string orderBy = query.Substring(orderByIndex).Replace(";", string.Empty); @@ -83,37 +82,45 @@ private static string CommonTableExpressionPaging(string query) string from = query.Substring(fromIndex); string select = query.Substring(0, fromIndex); - string value = MsSqlStatements.PagedQueryFormat.FormatWith(select, orderBy, from); - return value; + return MsSqlStatements.PagedQueryFormat.FormatWith(select, orderBy, from); } - private static string RemovePaging(string query) - { + private static string RemovePaging(string query) { return query .Replace("\n LIMIT @Limit OFFSET @Skip;", ";") .Replace("\n LIMIT @Limit;", ";"); } - public override bool IsDuplicate(Exception exception) - { + public override bool IsDuplicate(Exception exception) { var dbException = exception as SqlException; return dbException != null && (dbException.Number == UniqueIndexViolation || dbException.Number == UniqueKeyViolation); } - public override IDbTransaction OpenTransaction(IDbConnection connection) - { + public override IDbTransaction OpenTransaction(IDbConnection connection) { if (Transaction.Current == null) return connection.BeginTransaction(IsolationLevel.ReadCommitted); return base.OpenTransaction(connection); } + + /// + /// (#31) Add 'WITH (READCOMMITTEDLOCK)' to all 'FROM Commits' statements + /// + /// + private string AddReadCommittedLockToFromCommits(string query) { + if (!_addReadCommittedLockToFromCommits) { + return query; + } + return query.Replace("FROM Commits", "FROM Commits WITH (READCOMMITTEDLOCK)"); + } } - public class MsSql2005Dialect : MsSqlDialect - { - public override DbType GetDateTimeDbType() - { + public class MsSql2005Dialect : MsSqlDialect { + public MsSql2005Dialect(bool addReadCommittedLockToFromCommits = false) : base(addReadCommittedLockToFromCommits) { + } + + public override DbType GetDateTimeDbType() { return DbType.DateTime; } }