Skip to content

Commit 12cb1b6

Browse files
committed
Improve SQLite execution performance
- add state-aware connection lock and reader overloads - add transaction execution overloads with explicit state and isolation level - use static callbacks to avoid closure allocations - optimize command, adapter, context, migration, and schema inspection paths - preserve existing locking, transaction, cancellation, and rollback behavior - update public API documentation
1 parent 79d482a commit 12cb1b6

10 files changed

Lines changed: 735 additions & 143 deletions

‎README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
- **Transaction Scope Management**: Provides explicit transaction state tracking, prevents nested transactions, preserves original exceptions when rollback fails, and supports coordinated execution through reentrant connection locks.
1515
- **Database Context Management**: Manages connection state, transaction lifetime, commit, rollback, and deterministic resource cleanup.
1616
- **ADO.NET Utilities**: Provides helpers for commands, parameters, readers, data adapters, schema inspection, and common SQLite operations.
17+
- **Allocation-Conscious APIs**: Provides span-based parameters, `ValueTask`-based coordination, and explicit-state callbacks that help avoid closure allocations.
1718
- **Database Migrations**: Applies ordered schema and data migrations transactionally, records migration history with optional version metadata, and validates that the applied history matches the configured migration sequence.
1819

1920
## Why NuExt.System.Data.SQLite?
2021

21-
`NuExt.System.Data.SQLite` is designed as a performance-oriented infrastructure layer over ADO.NET rather than an ORM. It keeps SQL, commands, parameters, readers, and transactions explicit while removing repetitive connection, transaction, and resource lifecycle code.
22+
`NuExt.System.Data.SQLite` is designed as a performance-oriented infrastructure layer over ADO.NET rather than an ORM. It keeps SQL, commands, parameters, readers, and transactions explicit while removing repetitive connection, transaction, and resource lifecycle code. It also preserves SQLite's synchronous execution model instead of wrapping database operations in artificial asynchronous work.
2223

2324
The library supports both standalone DAL operations and coordinated units of work, protects complete database operation scopes during concurrent and reentrant execution, and provides forward-only migrations with deterministic ordering and strict history validation.
2425

‎src/System/Data/SQLite/SQLiteDbConnection.Adapters.cs‎

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public SQLiteDataAdapter CreateAdapter(SQLiteCommand command)
4242
/// </summary>
4343
/// <param name="tableName">The name of the table.</param>
4444
/// <param name="fields">The fields to select.</param>
45-
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
45+
/// <param name="cancellationToken">A token to observe while waiting to acquire the connection lock.</param>
4646
/// <returns>A table containing the selected data.</returns>
4747
public DataTable Select(string tableName, IEnumerable<string> fields, CancellationToken cancellationToken = default)
4848
{
@@ -73,7 +73,7 @@ public DataTable Select(string tableName, IEnumerable<string> fields, string? ex
7373
/// <param name="tableName">The name of the table.</param>
7474
/// <param name="fields">The fields to select.</param>
7575
/// <param name="expr">An optional SQL expression appended to the command.</param>
76-
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
76+
/// <param name="cancellationToken">A token to observe while waiting to acquire the connection lock.</param>
7777
/// <param name="parameters">The parameters to add to the command.</param>
7878
/// <returns>A table containing the selected data.</returns>
7979
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -110,7 +110,7 @@ public DataTable Select(string sql, params SQLiteParameter[] parameters)
110110
/// Executes the specified query and returns the selected data.
111111
/// </summary>
112112
/// <param name="sql">The SQL statement to execute.</param>
113-
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
113+
/// <param name="cancellationToken">A token to observe while waiting to acquire the connection lock.</param>
114114
/// <param name="parameters">The parameters to add to the command.</param>
115115
/// <returns>A table containing the selected data.</returns>
116116
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -131,7 +131,7 @@ public DataTable Select(string sql, CancellationToken cancellationToken, params
131131
/// Executes the specified command and returns the selected data.
132132
/// </summary>
133133
/// <param name="command">The SQLite command to execute.</param>
134-
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
134+
/// <param name="cancellationToken">A token to observe while waiting to acquire the connection lock.</param>
135135
/// <returns>A table containing the selected data.</returns>
136136
public DataTable Select(SQLiteCommand command, CancellationToken cancellationToken = default)
137137
{
@@ -162,7 +162,7 @@ public DataTable Select(SQLiteCommand command, CancellationToken cancellationTok
162162
/// </summary>
163163
/// <param name="tableName">The name of the table.</param>
164164
/// <param name="fields">The fields to select.</param>
165-
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
165+
/// <param name="cancellationToken">A token to observe while waiting to acquire the connection lock.</param>
166166
/// <returns>
167167
/// The configured data adapter and selected data table.
168168
/// The caller is responsible for disposing the returned adapter.
@@ -178,7 +178,7 @@ public DataTable Select(SQLiteCommand command, CancellationToken cancellationTok
178178
/// <param name="tableName">The name of the table.</param>
179179
/// <param name="fields">The fields to select.</param>
180180
/// <param name="generateCommands"><see langword="true"/> to generate insert, update, and delete commands; otherwise, <see langword="false"/>.</param>
181-
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
181+
/// <param name="cancellationToken">A token to observe while waiting to acquire the connection lock.</param>
182182
/// <returns>
183183
/// The configured data adapter and selected data table.
184184
/// The caller is responsible for disposing the returned adapter.
@@ -215,7 +215,7 @@ public DataTable Select(SQLiteCommand command, CancellationToken cancellationTok
215215
/// <param name="tableName">The name of the table.</param>
216216
/// <param name="fields">The fields to select.</param>
217217
/// <param name="expr">An optional SQL expression appended to the command.</param>
218-
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
218+
/// <param name="cancellationToken">A token to observe while waiting to acquire the connection lock.</param>
219219
/// <param name="parameters">The parameters to add to the command.</param>
220220
/// <returns>
221221
/// The configured data adapter and selected data table.
@@ -260,7 +260,7 @@ public DataTable Select(SQLiteCommand command, CancellationToken cancellationTok
260260
/// <param name="fields">The fields to select.</param>
261261
/// <param name="expr">An optional SQL expression appended to the command.</param>
262262
/// <param name="generateCommands"><see langword="true"/> to generate insert, update, and delete commands; otherwise, <see langword="false"/>.</param>
263-
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
263+
/// <param name="cancellationToken">A token to observe while waiting to acquire the connection lock.</param>
264264
/// <param name="parameters">The parameters to add to the command.</param>
265265
/// <returns>
266266
/// The configured data adapter and selected data table.
@@ -302,7 +302,7 @@ public DataTable Select(SQLiteCommand command, CancellationToken cancellationTok
302302
/// Executes the specified query and creates an adapter that can update the returned data.
303303
/// </summary>
304304
/// <param name="sql">The SQL statement to execute.</param>
305-
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
305+
/// <param name="cancellationToken">A token to observe while waiting to acquire the connection lock.</param>
306306
/// <param name="parameters">The parameters to add to the command.</param>
307307
/// <returns>
308308
/// The configured data adapter and selected data table.
@@ -343,7 +343,7 @@ public DataTable Select(SQLiteCommand command, CancellationToken cancellationTok
343343
/// </summary>
344344
/// <param name="sql">The SQL statement to execute.</param>
345345
/// <param name="generateCommands"><see langword="true"/> to generate insert, update, and delete commands; otherwise, <see langword="false"/>.</param>
346-
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
346+
/// <param name="cancellationToken">A token to observe while waiting to acquire the connection lock.</param>
347347
/// <param name="parameters">The parameters to add to the command.</param>
348348
/// <returns>
349349
/// The configured data adapter and selected data table.
@@ -366,7 +366,7 @@ public DataTable Select(SQLiteCommand command, CancellationToken cancellationTok
366366
/// Executes the specified command and creates an adapter that can update the returned data.
367367
/// </summary>
368368
/// <param name="command">The SQLite command to execute.</param>
369-
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
369+
/// <param name="cancellationToken">A token to observe while waiting to acquire the connection lock.</param>
370370
/// <returns>
371371
/// The configured data adapter and selected data table.
372372
/// The caller is responsible for disposing the returned adapter.
@@ -382,7 +382,7 @@ public DataTable Select(SQLiteCommand command, CancellationToken cancellationTok
382382
/// </summary>
383383
/// <param name="command">The SQLite command to execute.</param>
384384
/// <param name="generateCommands"><see langword="true"/> to generate insert, update, and delete commands; otherwise, <see langword="false"/>.</param>
385-
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
385+
/// <param name="cancellationToken">A token to observe while waiting to acquire the connection lock.</param>
386386
/// <returns>
387387
/// The configured data adapter and selected data table.
388388
/// The caller is responsible for disposing the returned adapter.
@@ -429,14 +429,14 @@ public DataTable Select(SQLiteCommand command, CancellationToken cancellationTok
429429
/// </summary>
430430
/// <param name="adapter">The data adapter used to retrieve data.</param>
431431
/// <param name="table">The data table to fill.</param>
432-
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
432+
/// <param name="cancellationToken">A token to observe while waiting to acquire the connection lock.</param>
433433
public void Fill(SQLiteDataAdapter adapter, DataTable table, CancellationToken cancellationToken = default)
434434
{
435435
ArgumentNullException.ThrowIfNull(adapter);
436436
ArgumentNullException.ThrowIfNull(table);
437437
cancellationToken.ThrowIfCancellationRequested();
438438

439-
AcquireLock(() => adapter.Fill(table), cancellationToken);
439+
AcquireLock(static state => state.Adapter.Fill(state.Table), (Adapter: adapter, Table: table), cancellationToken);
440440
}
441441

442442
#endregion
@@ -448,7 +448,7 @@ public void Fill(SQLiteDataAdapter adapter, DataTable table, CancellationToken c
448448
/// </summary>
449449
/// <param name="adapter">The data adapter used to update the database.</param>
450450
/// <param name="table">The data table containing the changes.</param>
451-
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
451+
/// <param name="cancellationToken">A token to observe while waiting to acquire the connection lock.</param>
452452
/// <returns>The number of rows successfully updated.</returns>
453453
public int Update(SQLiteDataAdapter adapter, DataTable table, CancellationToken cancellationToken = default)
454454
{
@@ -457,7 +457,7 @@ public int Update(SQLiteDataAdapter adapter, DataTable table, CancellationToken
457457
cancellationToken.ThrowIfCancellationRequested();
458458

459459
// Update requires a valid UpdateCommand when passed a DataRow collection with modified rows.
460-
return AcquireLock(() => adapter.Update(table), cancellationToken);
460+
return AcquireLock(static state => state.Adapter.Update(state.Table), (Adapter: adapter, Table: table), cancellationToken);
461461
}
462462

463463
#endregion

‎src/System/Data/SQLite/SQLiteDbConnection.AsyncLock.cs‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ public void AcquireLock(Action action, CancellationToken cancellationToken = def
1616
GetSyncRoot().Mutex.Acquire(action, cancellationToken);
1717
}
1818

19+
/// <summary>
20+
/// Acquires a lock and executes the specified action with the provided state while holding the lock.
21+
/// </summary>
22+
/// <typeparam name="TState">The type of the state passed to the action.</typeparam>
23+
/// <param name="action">The action to execute while holding the lock.</param>
24+
/// <param name="state">The state to pass to the action.</param>
25+
/// <param name="cancellationToken">A token to observe while waiting for the lock.</param>
26+
/// <exception cref="ArgumentNullException"><paramref name="action"/> is <see langword="null"/>.</exception>
27+
/// <exception cref="ObjectDisposedException">The connection has been disposed.</exception>
28+
public void AcquireLock<TState>(Action<TState> action, TState state, CancellationToken cancellationToken = default)
29+
{
30+
GetSyncRoot().Mutex.Acquire(action, state, cancellationToken);
31+
}
32+
1933
/// <summary>
2034
/// Acquires a lock and executes the specified function while holding the lock.
2135
/// </summary>
@@ -30,6 +44,22 @@ public T AcquireLock<T>(Func<T> func, CancellationToken cancellationToken = defa
3044
return GetSyncRoot().Mutex.Acquire(func, cancellationToken);
3145
}
3246

47+
/// <summary>
48+
/// Acquires a lock and executes the specified function with the provided state while holding the lock.
49+
/// </summary>
50+
/// <typeparam name="TState">The type of the state passed to the function.</typeparam>
51+
/// <typeparam name="TResult">The type of the result returned by the function.</typeparam>
52+
/// <param name="func">The function to execute while holding the lock.</param>
53+
/// <param name="state">The state to pass to the function.</param>
54+
/// <param name="cancellationToken">A token to observe while waiting for the lock.</param>
55+
/// <returns>The result of the function executed while holding the lock.</returns>
56+
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
57+
/// <exception cref="ObjectDisposedException">The connection has been disposed.</exception>
58+
public TResult AcquireLock<TState, TResult>(Func<TState, TResult> func, TState state, CancellationToken cancellationToken = default)
59+
{
60+
return GetSyncRoot().Mutex.Acquire(func, state, cancellationToken);
61+
}
62+
3363
/// <summary>
3464
/// Asynchronously acquires a lock and executes the specified asynchronous function while holding the lock.
3565
/// </summary>
@@ -43,6 +73,21 @@ public ValueTask AcquireLockAsync(Func<ValueTask> func, CancellationToken cancel
4373
return GetSyncRoot().Mutex.AcquireAsync(func, cancellationToken);
4474
}
4575

76+
/// <summary>
77+
/// Asynchronously acquires a lock and executes the specified asynchronous function with the provided state while holding the lock.
78+
/// </summary>
79+
/// <typeparam name="TState">The type of the state passed to the function.</typeparam>
80+
/// <param name="func">The asynchronous function to execute while holding the lock.</param>
81+
/// <param name="state">The state to pass to the function.</param>
82+
/// <param name="cancellationToken">A token to observe while waiting for the lock.</param>
83+
/// <returns>A task that represents the asynchronous operation.</returns>
84+
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
85+
/// <exception cref="ObjectDisposedException">The connection has been disposed.</exception>
86+
public ValueTask AcquireLockAsync<TState>(Func<TState, ValueTask> func, TState state, CancellationToken cancellationToken = default)
87+
{
88+
return GetSyncRoot().Mutex.AcquireAsync(func, state, cancellationToken);
89+
}
90+
4691
/// <summary>
4792
/// Asynchronously acquires a lock and executes the specified asynchronous function while holding the lock.
4893
/// </summary>
@@ -57,6 +102,22 @@ public ValueTask<T> AcquireLockAsync<T>(Func<ValueTask<T>> func, CancellationTok
57102
return GetSyncRoot().Mutex.AcquireAsync(func, cancellationToken);
58103
}
59104

105+
/// <summary>
106+
/// Asynchronously acquires a lock and executes the specified asynchronous function with the provided state while holding the lock.
107+
/// </summary>
108+
/// <typeparam name="TState">The type of the state passed to the function.</typeparam>
109+
/// <typeparam name="TResult">The type of the result returned by the function.</typeparam>
110+
/// <param name="func">The asynchronous function to execute while holding the lock.</param>
111+
/// <param name="state">The state to pass to the function.</param>
112+
/// <param name="cancellationToken">A token to observe while waiting for the lock.</param>
113+
/// <returns>A task that represents the asynchronous operation and contains the result of the function.</returns>
114+
/// <exception cref="ArgumentNullException"><paramref name="func"/> is <see langword="null"/>.</exception>
115+
/// <exception cref="ObjectDisposedException">The connection has been disposed.</exception>
116+
public ValueTask<TResult> AcquireLockAsync<TState, TResult>(Func<TState, ValueTask<TResult>> func, TState state, CancellationToken cancellationToken = default)
117+
{
118+
return GetSyncRoot().Mutex.AcquireAsync(func, state, cancellationToken);
119+
}
120+
60121
[MethodImpl(MethodImplOptions.AggressiveInlining)]
61122
private SQLiteDbConnectionFactory.AsyncLockCounter GetSyncRoot()
62123
{

0 commit comments

Comments
 (0)