88
99namespace EntityFrameworkCore . Sqlite . Concurrency ;
1010
11- public class SqliteConcurrencyInterceptor : DbCommandInterceptor , IAsyncDisposable
11+ public class SqliteConcurrencyInterceptor : DbCommandInterceptor
1212{
1313 private readonly SqliteConcurrencyOptions _options ;
14- private readonly SemaphoreSlim _writeLock = new ( 1 , 1 ) ;
14+ private readonly SemaphoreSlim _writeLock ;
1515 private readonly Channel < Func < ValueTask > > _writeQueue ;
1616 private readonly Task _queueProcessor ;
17- private bool _disposed ;
1817
19- public SqliteConcurrencyInterceptor ( SqliteConcurrencyOptions options )
18+ public SqliteConcurrencyInterceptor ( SqliteConcurrencyOptions options , string connectionString )
2019 {
2120 _options = options ;
21+ _writeLock = SqliteConnectionEnhancer . GetWriteLock ( connectionString ) ;
2222 _writeQueue = Channel . CreateUnbounded < Func < ValueTask > > ( new UnboundedChannelOptions
2323 {
2424 SingleReader = true ,
@@ -36,41 +36,131 @@ public override async ValueTask<InterceptionResult<DbDataReader>> ReaderExecutin
3636 {
3737 if ( IsWriteCommand ( command . CommandText ) )
3838 {
39+ if ( SqliteConnectionEnhancer . IsWriteLockHeld . Value )
40+ {
41+ UpgradeToBeginImmediate ( command ) ;
42+ return await base . ReaderExecutingAsync ( command , eventData , result , cancellationToken ) ;
43+ }
44+
3945 var tcs = new TaskCompletionSource < InterceptionResult < DbDataReader > > ( ) ;
4046 await _writeQueue . Writer . WriteAsync ( async ( ) =>
4147 {
4248 try
4349 {
44- // NO BEGIN IMMEDIATE - let EF Core handle transactions
45- result = await base . ReaderExecutingAsync ( command , eventData , result , cancellationToken ) ;
46- tcs . SetResult ( result ) ;
50+ UpgradeToBeginImmediate ( command ) ;
51+ var r = await base . ReaderExecutingAsync ( command , eventData , result , cancellationToken ) ;
52+ tcs . SetResult ( r ) ;
4753 }
4854 catch ( Exception ex )
4955 {
5056 tcs . SetException ( ex ) ;
5157 }
5258 } , cancellationToken ) ;
5359
54- await tcs . Task ;
55- return result ;
60+ return await tcs . Task ;
5661 }
5762
5863 return await base . ReaderExecutingAsync ( command , eventData , result , cancellationToken ) ;
5964 }
6065
61- // Same pattern for NonQueryExecutingAsync and ScalarExecutingAsync...
66+ public override async ValueTask < InterceptionResult < int > > NonQueryExecutingAsync (
67+ DbCommand command ,
68+ CommandEventData eventData ,
69+ InterceptionResult < int > result ,
70+ CancellationToken cancellationToken = default )
71+ {
72+ if ( IsWriteCommand ( command . CommandText ) )
73+ {
74+ if ( SqliteConnectionEnhancer . IsWriteLockHeld . Value )
75+ {
76+ UpgradeToBeginImmediate ( command ) ;
77+ return await base . NonQueryExecutingAsync ( command , eventData , result , cancellationToken ) ;
78+ }
79+
80+ var tcs = new TaskCompletionSource < InterceptionResult < int > > ( ) ;
81+ await _writeQueue . Writer . WriteAsync ( async ( ) =>
82+ {
83+ try
84+ {
85+ UpgradeToBeginImmediate ( command ) ;
86+ var r = await base . NonQueryExecutingAsync ( command , eventData , result , cancellationToken ) ;
87+ tcs . SetResult ( r ) ;
88+ }
89+ catch ( Exception ex )
90+ {
91+ tcs . SetException ( ex ) ;
92+ }
93+ } , cancellationToken ) ;
94+
95+ return await tcs . Task ;
96+ }
97+
98+ return await base . NonQueryExecutingAsync ( command , eventData , result , cancellationToken ) ;
99+ }
100+
101+ public override async ValueTask < InterceptionResult < object > > ScalarExecutingAsync (
102+ DbCommand command ,
103+ CommandEventData eventData ,
104+ InterceptionResult < object > result ,
105+ CancellationToken cancellationToken = default )
106+ {
107+ if ( IsWriteCommand ( command . CommandText ) )
108+ {
109+ if ( SqliteConnectionEnhancer . IsWriteLockHeld . Value )
110+ {
111+ UpgradeToBeginImmediate ( command ) ;
112+ return await base . ScalarExecutingAsync ( command , eventData , result , cancellationToken ) ;
113+ }
114+
115+ var tcs = new TaskCompletionSource < InterceptionResult < object > > ( ) ;
116+ await _writeQueue . Writer . WriteAsync ( async ( ) =>
117+ {
118+ try
119+ {
120+ UpgradeToBeginImmediate ( command ) ;
121+ var r = await base . ScalarExecutingAsync ( command , eventData , result , cancellationToken ) ;
122+ tcs . SetResult ( r ) ;
123+ }
124+ catch ( Exception ex )
125+ {
126+ tcs . SetException ( ex ) ;
127+ }
128+ } , cancellationToken ) ;
129+
130+ return await tcs . Task ;
131+ }
132+
133+ return await base . ScalarExecutingAsync ( command , eventData , result , cancellationToken ) ;
134+ }
135+
136+ private static void UpgradeToBeginImmediate ( DbCommand command )
137+ {
138+ var text = command . CommandText . Trim ( ) ;
139+ if ( text . StartsWith ( "BEGIN" , StringComparison . OrdinalIgnoreCase ) &&
140+ ! text . Contains ( "IMMEDIATE" , StringComparison . OrdinalIgnoreCase ) &&
141+ ! text . Contains ( "EXCLUSIVE" , StringComparison . OrdinalIgnoreCase ) )
142+ {
143+ if ( text . Equals ( "BEGIN" , StringComparison . OrdinalIgnoreCase ) ||
144+ text . Equals ( "BEGIN TRANSACTION" , StringComparison . OrdinalIgnoreCase ) )
145+ {
146+ command . CommandText = "BEGIN IMMEDIATE" ;
147+ }
148+ }
149+ }
62150
63151 private async Task ProcessWriteQueue ( )
64152 {
65153 await foreach ( var writeOperation in _writeQueue . Reader . ReadAllAsync ( ) )
66154 {
67155 await _writeLock . WaitAsync ( ) ;
156+ SqliteConnectionEnhancer . IsWriteLockHeld . Value = true ;
68157 try
69158 {
70159 await writeOperation ( ) ;
71160 }
72161 finally
73162 {
163+ SqliteConnectionEnhancer . IsWriteLockHeld . Value = false ;
74164 _writeLock . Release ( ) ;
75165 }
76166 }
@@ -85,25 +175,20 @@ private static bool IsWriteCommand(string commandText)
85175
86176 // Skip SELECT and read-only PRAGMAs
87177 if ( normalized . StartsWith ( "SELECT" ) ||
88- ( normalized . StartsWith ( "PRAGMA" ) && normalized . Contains ( "TABLE_INFO" ) ) )
178+ ( normalized . StartsWith ( "PRAGMA" ) && ( normalized . Contains ( "TABLE_INFO" ) ||
179+ normalized . Contains ( "INDEX_LIST" ) ||
180+ normalized . Contains ( "INDEX_INFO" ) ||
181+ normalized . Contains ( "FOREIGN_KEY_LIST" ) ) ) )
89182 return false ;
90183
91184 return normalized . StartsWith ( "INSERT" ) ||
92185 normalized . StartsWith ( "UPDATE" ) ||
93186 normalized . StartsWith ( "DELETE" ) ||
94187 normalized . StartsWith ( "CREATE" ) ||
95188 normalized . StartsWith ( "DROP" ) ||
96- normalized . StartsWith ( "ALTER" ) ;
189+ normalized . StartsWith ( "ALTER" ) ||
190+ normalized . StartsWith ( "BEGIN" ) ||
191+ normalized . StartsWith ( "PRAGMA" ) ;
97192 }
98193
99- public async ValueTask DisposeAsync ( )
100- {
101- if ( _disposed ) return ;
102-
103- _writeQueue . Writer . Complete ( ) ;
104- await _queueProcessor ;
105- _writeLock . Dispose ( ) ;
106-
107- _disposed = true ;
108- }
109194}
0 commit comments