Skip to content

Commit 2e421b0

Browse files
Pull Request updates
1 parent 828a897 commit 2e421b0

5 files changed

Lines changed: 88 additions & 11 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,7 @@ Sqlite-Concurrency/obj/
88
EntityFrameworkCore.Sqlite.Concurrency/obj/
99

1010
EntityFrameworkCore.Sqlite.Concurrency/bin/
11+
12+
Tests/bin/
13+
14+
Tests/obj/

EntityFrameworkCore.Sqlite.Concurrency/src/Models/SqliteConcurrencyOptions.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ namespace EntityFrameworkCore.Sqlite.Concurrency.Models;
33
/// <summary>
44
/// Options for configuring SQLite concurrency and performance.
55
/// </summary>
6-
public class SqliteConcurrencyOptions
6+
public class SqliteConcurrencyOptions : IEquatable<SqliteConcurrencyOptions>
77
{
88
/// <summary>
99
/// The maximum number of retry attempts for SQLITE_BUSY errors.
@@ -24,4 +24,30 @@ public class SqliteConcurrencyOptions
2424
/// The number of pages for WAL auto-checkpoint.
2525
/// </summary>
2626
public int WalAutoCheckpoint { get; set; } = 1000;
27+
28+
/// <inheritdoc />
29+
public bool Equals(SqliteConcurrencyOptions? other)
30+
{
31+
if (other is null) return false;
32+
if (ReferenceEquals(this, other)) return true;
33+
return MaxRetryAttempts == other.MaxRetryAttempts &&
34+
BusyTimeout.Equals(other.BusyTimeout) &&
35+
CommandTimeout == other.CommandTimeout &&
36+
WalAutoCheckpoint == other.WalAutoCheckpoint;
37+
}
38+
39+
/// <inheritdoc />
40+
public override bool Equals(object? obj)
41+
{
42+
if (obj is null) return false;
43+
if (ReferenceEquals(this, obj)) return true;
44+
if (obj.GetType() != GetType()) return false;
45+
return Equals((SqliteConcurrencyOptions)obj);
46+
}
47+
48+
/// <inheritdoc />
49+
public override int GetHashCode()
50+
{
51+
return HashCode.Combine(MaxRetryAttempts, BusyTimeout, CommandTimeout, WalAutoCheckpoint);
52+
}
2753
}

EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ public static async Task BulkInsertOptimizedAsync<T>(
9292
{
9393
await context.AddRangeAsync(entities, cancellationToken);
9494
await context.SaveChangesAsync(cancellationToken);
95+
context.ChangeTracker.Clear();
9596
return;
9697
}
9798

@@ -114,6 +115,7 @@ public static async Task BulkInsertOptimizedAsync<T>(
114115
{
115116
await context.AddRangeAsync(batch, cancellationToken);
116117
await context.SaveChangesAsync(cancellationToken);
118+
context.ChangeTracker.Clear();
117119
}
118120

119121
await transaction.CommitAsync(cancellationToken);

EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyInterceptor.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ public class SqliteConcurrencyInterceptor : DbCommandInterceptor, IDbConnectionI
1414
private readonly SemaphoreSlim _writeLock;
1515
private readonly string _connectionString;
1616

17+
/// <summary>
18+
/// Gets the concurrency options configured for this interceptor.
19+
/// </summary>
20+
public SqliteConcurrencyOptions Options => _options;
21+
1722
/// <summary>
1823
/// Initializes a new instance of the <see cref="SqliteConcurrencyInterceptor"/> class.
1924
/// </summary>

EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConnectionEnhancer.cs

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,37 @@ public static SemaphoreSlim GetWriteLock(string connectionString)
5757
/// <param name="connectionString">The connection string.</param>
5858
/// <param name="options">The concurrency options.</param>
5959
/// <returns>A <see cref="SqliteConcurrencyInterceptor"/> instance.</returns>
60+
/// <exception cref="ArgumentException">Thrown when the provided <paramref name="options"/> do not match the options of an existing interceptor for the same <paramref name="connectionString"/>.</exception>
61+
/// <remarks>
62+
/// Callers must use consistent options for the same connection string, as interceptors are cached and shared.
63+
/// </remarks>
6064
public static SqliteConcurrencyInterceptor GetInterceptor(string connectionString, SqliteConcurrencyOptions options)
6165
{
66+
if (_interceptors.TryGetValue(connectionString, out var existingInterceptor))
67+
{
68+
if (!existingInterceptor.Options.Equals(options))
69+
{
70+
throw new ArgumentException(
71+
$"Mismatched SqliteConcurrencyOptions for connection string. " +
72+
$"Existing options: {FormatOptions(existingInterceptor.Options)}, " +
73+
$"Incoming options: {FormatOptions(options)}. " +
74+
$"Interceptors are shared per connection string and must be configured consistently.",
75+
nameof(options));
76+
}
77+
return existingInterceptor;
78+
}
79+
6280
return _interceptors.GetOrAdd(connectionString, cs => new SqliteConcurrencyInterceptor(options, cs));
6381
}
6482

83+
private static string FormatOptions(SqliteConcurrencyOptions options)
84+
{
85+
return $"[MaxRetryAttempts={options.MaxRetryAttempts}, " +
86+
$"BusyTimeout={options.BusyTimeout}, " +
87+
$"CommandTimeout={options.CommandTimeout}, " +
88+
$"WalAutoCheckpoint={options.WalAutoCheckpoint}]";
89+
}
90+
6591
private static string ComputeOptimizedConnectionString(string originalConnectionString)
6692
{
6793
var builder = new SqliteConnectionStringBuilder(originalConnectionString)
@@ -98,20 +124,34 @@ public static void ApplyRuntimePragmas(DbConnection connection, SqliteConcurrenc
98124
var dataSource = builder.DataSource;
99125

100126
// 1. Database-scoped Pragmas - Run once per process
101-
if (_initializedDatabases.TryAdd(dataSource, true))
127+
if (!_initializedDatabases.ContainsKey(dataSource))
102128
{
103129
var lockObj = _pragmaLocks.GetOrAdd(dataSource, _ => new object());
104130
lock (lockObj)
105131
{
106-
using var initCommand = sqliteConnection.CreateCommand();
107-
initCommand.CommandText = $@"
108-
PRAGMA journal_mode = WAL;
109-
PRAGMA page_size = 4096;
110-
PRAGMA auto_vacuum = INCREMENTAL;
111-
PRAGMA journal_size_limit = 134217728;
112-
PRAGMA wal_autocheckpoint = {options.WalAutoCheckpoint};
113-
";
114-
initCommand.ExecuteNonQuery();
132+
if (!_initializedDatabases.ContainsKey(dataSource))
133+
{
134+
try
135+
{
136+
using var initCommand = sqliteConnection.CreateCommand();
137+
initCommand.CommandText = $@"
138+
PRAGMA journal_mode = WAL;
139+
PRAGMA page_size = 4096;
140+
PRAGMA auto_vacuum = INCREMENTAL;
141+
PRAGMA journal_size_limit = 134217728;
142+
PRAGMA wal_autocheckpoint = {options.WalAutoCheckpoint};
143+
";
144+
initCommand.ExecuteNonQuery();
145+
146+
_initializedDatabases.TryAdd(dataSource, true);
147+
}
148+
catch
149+
{
150+
// Ensure we don't leave it marked as initialized if it failed
151+
_initializedDatabases.TryRemove(dataSource, out _);
152+
throw;
153+
}
154+
}
115155
}
116156
}
117157

0 commit comments

Comments
 (0)