Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions pages/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions pages/ef-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ public async Task SharedDatabase()
AreEqual(0, count);
}
```
<sup><a href='/src/EfLocalDb.Tests/Tests.cs#L674-L684' title='Snippet source file'>snippet source</a> | <a href='#snippet-EfSharedDatabase' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/EfLocalDb.Tests/Tests.cs#L687-L697' title='Snippet source file'>snippet source</a> | <a href='#snippet-EfSharedDatabase' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Pass `useTransaction: true` to get an auto-rolling-back transaction, allowing writes without affecting other tests.
Expand All @@ -318,7 +318,7 @@ public async Task SharedDatabase_WithTransaction()
AreEqual(0, count);
}
```
<sup><a href='/src/EfLocalDb.Tests/Tests.cs#L698-L716' title='Snippet source file'>snippet source</a> | <a href='#snippet-EfSharedDatabase_WithTransaction' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/EfLocalDb.Tests/Tests.cs#L711-L729' title='Snippet source file'>snippet source</a> | <a href='#snippet-EfSharedDatabase_WithTransaction' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


Expand Down
11 changes: 11 additions & 0 deletions pages/mdsource/design.source.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pages/template-database-size.md
Original file line number Diff line number Diff line change
Expand Up @@ -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})
```
<sup><a href='/src/LocalDb/SqlBuilder.cs#L70-L73' title='Snippet source file'>snippet source</a> | <a href='#snippet-ShrinkModelDb' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/LocalDb/SqlBuilder.cs#L84-L87' title='Snippet source file'>snippet source</a> | <a href='#snippet-ShrinkModelDb' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


Expand Down
13 changes: 13 additions & 0 deletions src/EfLocalDb.Tests/Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
14 changes: 14 additions & 0 deletions src/LocalDb/SqlBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
4 changes: 2 additions & 2 deletions src/LocalDb/SqlExtensions.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down
13 changes: 7 additions & 6 deletions src/LocalDb/Wrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand Down Expand Up @@ -299,8 +299,9 @@ async Task Rebuild(DateTime timestamp, Func<SqlConnection, Task> 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);
}
Expand Down
Loading