diff --git a/pages/design.md b/pages/design.md index 2259c99b..a75da739 100644 --- a/pages/design.md +++ b/pages/design.md @@ -44,6 +44,17 @@ The usual approach for consuming the API in a test project is as follows. This assumes that there is a schema and data (and DbContext in the EntityFramework context) used for all tests. If those caveats are not correct then multiple SqlInstances can be used. + +## Template database settings + +When the template is built, a few database-level settings are applied. These persist in the template's `.mdf`/`.ldf` files and are inherited by every database attached from the template. + + * `auto_update_statistics off` — avoids background statistics updates causing nondeterministic test timing. + * `read_committed_snapshot on` — `READ COMMITTED` uses row versioning instead of shared locks. Required to prevent S/X-lock deadlocks between parallel `[SharedDbWithTransaction]` tests against the same shared database. + +If the template files already exist on disk from a previous build, these settings are not reapplied — the template needs to be regenerated (delete the template files, or change the timestamp passed to `SqlInstance`) for new settings to take effect. + + More information: * [Raw SqlConnection Usage](/pages/raw-usage.md) diff --git a/pages/ef-usage.md b/pages/ef-usage.md index 149b8f75..c59a1167 100644 --- a/pages/ef-usage.md +++ b/pages/ef-usage.md @@ -292,7 +292,7 @@ public async Task SharedDatabase() AreEqual(0, count); } ``` -snippet source | anchor +snippet source | anchor Pass `useTransaction: true` to get an auto-rolling-back transaction, allowing writes without affecting other tests. @@ -318,7 +318,7 @@ public async Task SharedDatabase_WithTransaction() AreEqual(0, count); } ``` -snippet source | anchor +snippet source | anchor diff --git a/pages/mdsource/design.source.md b/pages/mdsource/design.source.md index 82888351..356d9216 100644 --- a/pages/mdsource/design.source.md +++ b/pages/mdsource/design.source.md @@ -37,6 +37,17 @@ The usual approach for consuming the API in a test project is as follows. This assumes that there is a schema and data (and DbContext in the EntityFramework context) used for all tests. If those caveats are not correct then multiple SqlInstances can be used. + +## Template database settings + +When the template is built, a few database-level settings are applied. These persist in the template's `.mdf`/`.ldf` files and are inherited by every database attached from the template. + + * `auto_update_statistics off` — avoids background statistics updates causing nondeterministic test timing. + * `read_committed_snapshot on` — `READ COMMITTED` uses row versioning instead of shared locks. Required to prevent S/X-lock deadlocks between parallel `[SharedDbWithTransaction]` tests against the same shared database. + +If the template files already exist on disk from a previous build, these settings are not reapplied — the template needs to be regenerated (delete the template files, or change the timestamp passed to `SqlInstance`) for new settings to take effect. + + More information: * [Raw SqlConnection Usage](/pages/raw-usage.md) diff --git a/pages/template-database-size.md b/pages/template-database-size.md index a0dc795d..bc73856e 100644 --- a/pages/template-database-size.md +++ b/pages/template-database-size.md @@ -21,7 +21,7 @@ To have a smaller file size [DBCC SHRINKFILE](https://docs.microsoft.com/en-us/s use model; dbcc shrinkfile(modeldev, {size}) ``` -snippet source | anchor +snippet source | anchor diff --git a/src/EfLocalDb.Tests/Tests.cs b/src/EfLocalDb.Tests/Tests.cs index 59e23dc0..3e17ba3a 100644 --- a/src/EfLocalDb.Tests/Tests.cs +++ b/src/EfLocalDb.Tests/Tests.cs @@ -78,6 +78,19 @@ await ThrowsTask(() => database.SaveChangesAsync()) .IgnoreStackTrace(); } + [Test] + public async Task TemplateHasReadCommittedSnapshot() + { + // Guards against accidental removal of the ALTER DATABASE statement that enables + // READ_COMMITTED_SNAPSHOT on the template — required to avoid S/X-lock deadlocks + // between parallel [SharedDbWithTransaction] tests on the same shared database. + await using var database = await instance.Build(); + await using var command = database.Connection.CreateCommand(); + command.CommandText = "select is_read_committed_snapshot_on from sys.databases where name = db_name()"; + var enabled = (bool) (await command.ExecuteScalarAsync())!; + True(enabled); + } + [Test] public async Task RemoveData() { diff --git a/src/LocalDb/SqlBuilder.cs b/src/LocalDb/SqlBuilder.cs index da7d3169..4b78b40c 100644 --- a/src/LocalDb/SqlBuilder.cs +++ b/src/LocalDb/SqlBuilder.cs @@ -50,6 +50,20 @@ create database [template] on execute sp_detach_db N'template', 'true'; """; + // Database-level settings applied to the template before detach. Persisted in the + // .mdf/.ldf files, so every database attached from the template inherits them. + // auto_update_statistics off - avoids background stats updates causing + // nondeterministic test timing. + // read_committed_snapshot on - READ COMMITTED uses row versioning instead of + // shared locks, preventing S/X-lock deadlocks + // between parallel [SharedDbWithTransaction] tests + // against the same shared database. + public static string TemplateSettingsCommand = + """ + alter database [template] set auto_update_statistics off; + alter database [template] set read_committed_snapshot on; + """; + public static string DetachAndShrinkTemplateCommand = """ use [template]; diff --git a/src/LocalDb/SqlExtensions.cs b/src/LocalDb/SqlExtensions.cs index b4742f97..b9b08be7 100644 --- a/src/LocalDb/SqlExtensions.cs +++ b/src/LocalDb/SqlExtensions.cs @@ -1,8 +1,8 @@ static class SqlExtensions { - public static async Task ExecuteCommandAsync(this SqlConnection connection, string commandText) + public static async Task ExecuteCommandAsync(this SqlConnection connection, params string[] commandTexts) { - commandText = commandText.Trim(); + var commandText = string.Join('\n', commandTexts).Trim(); try { diff --git a/src/LocalDb/Wrapper.cs b/src/LocalDb/Wrapper.cs index 2b58e3f5..764f73e7 100644 --- a/src/LocalDb/Wrapper.cs +++ b/src/LocalDb/Wrapper.cs @@ -260,10 +260,10 @@ async Task CreateAndDetachTemplate( await connection.ExecuteCommandAsync("checkpoint"); } - await masterConnection.ExecuteCommandAsync("alter database [template] set auto_update_statistics off"); - - // Detach the template database after callback completes - await masterConnection.ExecuteCommandAsync(SqlBuilder.DetachTemplateCommand); + // Apply template settings then detach in a single batch. + await masterConnection.ExecuteCommandAsync( + SqlBuilder.TemplateSettingsCommand, + SqlBuilder.DetachTemplateCommand); } } } @@ -299,8 +299,9 @@ async Task Rebuild(DateTime timestamp, Func buildTemplate, await connection.ExecuteCommandAsync("checkpoint"); } - await masterConnection.ExecuteCommandAsync("alter database [template] set auto_update_statistics off"); - await masterConnection.ExecuteCommandAsync(SqlBuilder.DetachAndShrinkTemplateCommand); + await masterConnection.ExecuteCommandAsync( + SqlBuilder.TemplateSettingsCommand, + SqlBuilder.DetachAndShrinkTemplateCommand); File.SetCreationTime(DataFile, timestamp); }