Skip to content

Commit 8128dda

Browse files
committed
Refs: #60 Fix async paged-query infinite loop when PageSize is zero and add regression tests
1 parent c173137 commit 8128dda

10 files changed

Lines changed: 104 additions & 4 deletions

File tree

‎Changelog.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
### BugFix
1414

1515
- Fix cross-bucket contamination in snapshot candidate query [#59](https://github.com/NEventStore/NEventStore.Persistence.SQL/issues/59)
16+
- Fix async paged-query infinite loop when PageSize is zero [#60](https://github.com/NEventStore/NEventStore.Persistence.SQL/issues/60)
1617

1718
### Breaking Changes
1819

‎docs/Project-Analysis-Issue-Drafts.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,27 @@ Validation:
279279
- Result: 141 passed, 0 failed, 0 skipped.
280280
- `dotnet build .\src\NEventStore.Persistence.Sql.Core.sln -c Release --no-restore /p:ContinuousIntegrationBuild=true -m:1 -nr:false`
281281
- Result: passed.
282+
283+
### Issue #60: Async Infinite Page Size Empty Reads
284+
285+
Status: completed locally on 2026-06-11. GitHub issue update is pending review.
286+
287+
Async paged reads now complete when the configured page size is `0`. For pageable dialects, `ExecutePagedQueryAsync` still binds the SQL limit parameter with an effectively unbounded value, but keeps the local paging size at `0` so continuation does not request another page.
288+
289+
Implementation notes:
290+
291+
- Added async regression coverage under `src/NEventStore.Persistence.Sql.Tests/PersistenceTests.InfinitePageSize.Async.cs` and linked it into all provider test projects.
292+
- The regression initializes the provider fixture with page size `0` and verifies both `GetFromAsync(0, observer, token)` and `GetFromAsync(Bucket.Default, 0, observer, token)` complete over an empty store.
293+
- The production fix is scoped to async paged reads in `CommonDbStatement`; sync paging behavior was left unchanged.
294+
295+
Validation:
296+
297+
- Test-first check: SQLite focused regression failed before the fix with missing `@Limit` binding for page size `0`, then passed after the fix.
298+
- `dotnet test .\src\NEventStore.Persistence.Sqlite.Tests\NEventStore.Persistence.Sqlite.Core.Tests.csproj -c Release -f net8.0 --filter "FullyQualifiedName~when_reading_empty_async_pages_with_infinite_page_size"`
299+
- Result: 2 passed, 0 failed, 0 skipped.
300+
- `dotnet test .\src\NEventStore.Persistence.Sqlite.Tests\NEventStore.Persistence.Sqlite.Core.Tests.csproj -c Release --no-build -f net8.0`
301+
- Result: 143 passed, 0 failed, 0 skipped.
302+
- `dotnet test .\src\NEventStore.Persistence.Oracle.Tests\NEventStore.Persistence.Oracle.Core.Tests.csproj -c Release -f net8.0 --no-build --filter "FullyQualifiedName~when_reading_empty_async_pages_with_infinite_page_size"`
303+
- Result: 2 passed, 0 failed, 0 skipped.
304+
- `dotnet build .\src\NEventStore.Persistence.Sql.Core.sln -c Release --no-restore /p:ContinuousIntegrationBuild=true -m:1 -nr:false`
305+
- Result: passed.

‎src/NEventStore.Persistence.MsSql.Tests/NEventStore.Persistence.MsSql.Core.Tests.csproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
<Compile Include="..\..\dependencies\NEventStore\src\NEventStore.Persistence.AcceptanceTests\PersistenceTests.Async.cs" Link="PersistenceTests.Async.cs" />
4848
<Compile Include="..\NEventStore.Persistence.Sql.Tests\PersistenceTests.Transactions.cs" Link="PersistenceTests.Transactions.cs" />
4949
<Compile Include="..\NEventStore.Persistence.Sql.Tests\PersistenceTests.Transactions.Async.cs" Link="PersistenceTests.Transactions.Async.cs" />
50+
<Compile Include="..\NEventStore.Persistence.Sql.Tests\PersistenceTests.InfinitePageSize.Async.cs" Link="PersistenceTests.InfinitePageSize.Async.cs" />
5051
<Compile Include="..\NEventStore.Persistence.Sql.Tests\PersistenceTests.Snapshots.cs" Link="PersistenceTests.Snapshots.cs" />
5152
<Compile Include="..\NEventStore.Persistence.Sql.Tests\PersistenceTests.Snapshots.Async.cs" Link="PersistenceTests.Snapshots.Async.cs" />
5253
</ItemGroup>

‎src/NEventStore.Persistence.MySql.Tests/NEventStore.Persistence.MySql.Core.Tests.csproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
<ItemGroup>
4747
<Compile Include="..\..\dependencies\NEventStore\src\NEventStore.Persistence.AcceptanceTests\PersistenceTests.cs" Link="PersistenceTests.cs" />
4848
<Compile Include="..\..\dependencies\NEventStore\src\NEventStore.Persistence.AcceptanceTests\PersistenceTests.Async.cs" Link="PersistenceTests.Async.cs" />
49+
<Compile Include="..\NEventStore.Persistence.Sql.Tests\PersistenceTests.InfinitePageSize.Async.cs" Link="PersistenceTests.InfinitePageSize.Async.cs" />
4950
<Compile Include="..\NEventStore.Persistence.Sql.Tests\PersistenceTests.Snapshots.cs" Link="PersistenceTests.Snapshots.cs" />
5051
<Compile Include="..\NEventStore.Persistence.Sql.Tests\PersistenceTests.Snapshots.Async.cs" Link="PersistenceTests.Snapshots.Async.cs" />
5152
</ItemGroup>

‎src/NEventStore.Persistence.Oracle.Tests/NEventStore.Persistence.Oracle.Core.Tests.csproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
<ItemGroup>
5050
<Compile Include="..\..\dependencies\NEventStore\src\NEventStore.Persistence.AcceptanceTests\PersistenceTests.cs" Link="PersistenceTests.cs" />
5151
<Compile Include="..\..\dependencies\NEventStore\src\NEventStore.Persistence.AcceptanceTests\PersistenceTests.Async.cs" Link="PersistenceTests.Async.cs" />
52+
<Compile Include="..\NEventStore.Persistence.Sql.Tests\PersistenceTests.InfinitePageSize.Async.cs" Link="PersistenceTests.InfinitePageSize.Async.cs" />
5253
<Compile Include="..\NEventStore.Persistence.Sql.Tests\PersistenceTests.Snapshots.cs" Link="PersistenceTests.Snapshots.cs" />
5354
<Compile Include="..\NEventStore.Persistence.Sql.Tests\PersistenceTests.Snapshots.Async.cs" Link="PersistenceTests.Snapshots.Async.cs" />
5455
</ItemGroup>

‎src/NEventStore.Persistence.PostgreSql.Tests/NEventStore.Persistence.PostgreSql.Core.Tests.csproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
<ItemGroup>
4444
<Compile Include="..\..\dependencies\NEventStore\src\NEventStore.Persistence.AcceptanceTests\PersistenceTests.cs" Link="PersistenceTests.cs" />
4545
<Compile Include="..\..\dependencies\NEventStore\src\NEventStore.Persistence.AcceptanceTests\PersistenceTests.Async.cs" Link="PersistenceTests.Async.cs" />
46+
<Compile Include="..\NEventStore.Persistence.Sql.Tests\PersistenceTests.InfinitePageSize.Async.cs" Link="PersistenceTests.InfinitePageSize.Async.cs" />
4647
<Compile Include="..\NEventStore.Persistence.Sql.Tests\PersistenceTests.Snapshots.cs" Link="PersistenceTests.Snapshots.cs" />
4748
<Compile Include="..\NEventStore.Persistence.Sql.Tests\PersistenceTests.Snapshots.Async.cs" Link="PersistenceTests.Snapshots.Async.cs" />
4849
</ItemGroup>

‎src/NEventStore.Persistence.Sql.Tests/NEventStore.Persistence.Sql.Core.Tests.csproj‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<ItemGroup>
1111
<Compile Remove="PersistenceTests.Transactions.Async.cs" />
1212
<Compile Remove="PersistenceTests.Transactions.cs" />
13+
<Compile Remove="PersistenceTests.InfinitePageSize.Async.cs" />
1314
<Compile Remove="PersistenceTests.Snapshots.Async.cs" />
1415
<Compile Remove="PersistenceTests.Snapshots.cs" />
1516
</ItemGroup>
@@ -21,6 +22,7 @@
2122
<ItemGroup>
2223
<None Include="PersistenceTests.Transactions.Async.cs" />
2324
<None Include="PersistenceTests.Transactions.cs" />
25+
<None Include="PersistenceTests.InfinitePageSize.Async.cs" />
2426
<None Include="PersistenceTests.Snapshots.Async.cs" />
2527
<None Include="PersistenceTests.Snapshots.cs" />
2628
</ItemGroup>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#pragma warning disable IDE1006 // Naming Styles
2+
3+
using FluentAssertions;
4+
using NEventStore.Persistence.AcceptanceTests.BDD;
5+
6+
#if MSTEST
7+
using Microsoft.VisualStudio.TestTools.UnitTesting;
8+
#endif
9+
#if NUNIT
10+
using NUnit.Framework;
11+
#endif
12+
#if XUNIT
13+
using Xunit;
14+
#endif
15+
16+
namespace NEventStore.Persistence.AcceptanceTests.Async
17+
{
18+
#if MSTEST
19+
[TestClass]
20+
#endif
21+
public class when_reading_empty_async_pages_with_infinite_page_size : PersistenceEngineConcernAsync
22+
{
23+
private CommitStreamObserver? _checkpointObserver;
24+
private CommitStreamObserver? _bucketObserver;
25+
private Exception? _checkpointException;
26+
private Exception? _bucketException;
27+
28+
protected override void Context()
29+
{
30+
Fixture!.Dispose();
31+
Fixture.Initialize(0);
32+
}
33+
34+
protected override async Task BecauseAsync()
35+
{
36+
_checkpointObserver = new CommitStreamObserver();
37+
_bucketObserver = new CommitStreamObserver();
38+
39+
_checkpointException = await Catch.ExceptionAsync(() =>
40+
ReadWithTimeout(token => Persistence.GetFromAsync(0, _checkpointObserver, token)));
41+
42+
_bucketException = await Catch.ExceptionAsync(() =>
43+
ReadWithTimeout(token => Persistence.GetFromAsync(Bucket.Default, 0, _bucketObserver, token)));
44+
}
45+
46+
[Fact]
47+
public void should_complete_checkpoint_reads_without_waiting_for_another_empty_page()
48+
{
49+
_checkpointException.Should().BeNull();
50+
_checkpointObserver!.Commits.Should().BeEmpty();
51+
}
52+
53+
[Fact]
54+
public void should_complete_bucket_checkpoint_reads_without_waiting_for_another_empty_page()
55+
{
56+
_bucketException.Should().BeNull();
57+
_bucketObserver!.Commits.Should().BeEmpty();
58+
}
59+
60+
private static async Task ReadWithTimeout(Func<CancellationToken, Task> read)
61+
{
62+
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(2));
63+
await read(timeout.Token);
64+
}
65+
}
66+
}
67+
68+
#pragma warning restore IDE1006 // Naming Styles

‎src/NEventStore.Persistence.Sql/SqlDialects/CommonDbStatement.cs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -331,13 +331,13 @@ public async Task ExecuteWithQueryAsync(string queryText, IAsyncObserver<IDataRe
331331
public async Task ExecutePagedQueryAsync(string queryText, NextPageDelegate nextPage, IAsyncObserver<IDataRecord> asyncObserver, CancellationToken cancellationToken)
332332
{
333333
int pageSize = Dialect.CanPage ? PageSize : InfinitePageSize;
334-
if (pageSize > 0)
334+
if (Dialect.CanPage)
335335
{
336336
if (Logger.IsEnabled(LogLevel.Trace))
337337
{
338338
Logger.LogTrace(Messages.MaxPageSize, pageSize);
339339
}
340-
Parameters.Add(Dialect.Limit, Tuple.Create((object)pageSize, (DbType?)null));
340+
Parameters.Add(Dialect.Limit, Tuple.Create((object)(pageSize > 0 ? pageSize : int.MaxValue), (DbType?)null));
341341
}
342342
Parameters.Add(Dialect.Skip, Tuple.Create((object)0, (DbType?)null));
343343
using (var command = BuildCommand(queryText))
@@ -369,7 +369,7 @@ public async Task ExecutePagedQueryAsync(string queryText, NextPageDelegate next
369369
}
370370
}
371371
}
372-
while (Dialect.CanPage && recordsRead == PageSize);
372+
while (Dialect.CanPage && pageSize > 0 && recordsRead == pageSize);
373373

374374
await asyncObserver.OnCompletedAsync(cancellationToken).ConfigureAwait(false);
375375
}
@@ -381,4 +381,4 @@ public async Task ExecutePagedQueryAsync(string queryText, NextPageDelegate next
381381
}
382382
}
383383
}
384-
}
384+
}

‎src/NEventStore.Persistence.Sqlite.Tests/NEventStore.Persistence.Sqlite.Core.Tests.csproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
<ItemGroup>
5151
<Compile Include="..\..\dependencies\NEventStore\src\NEventStore.Persistence.AcceptanceTests\PersistenceTests.cs" Link="PersistenceTests.cs" />
5252
<Compile Include="..\..\dependencies\NEventStore\src\NEventStore.Persistence.AcceptanceTests\PersistenceTests.Async.cs" Link="PersistenceTests.Async.cs" />
53+
<Compile Include="..\NEventStore.Persistence.Sql.Tests\PersistenceTests.InfinitePageSize.Async.cs" Link="PersistenceTests.InfinitePageSize.Async.cs" />
5354
<Compile Include="..\NEventStore.Persistence.Sql.Tests\PersistenceTests.Snapshots.cs" Link="PersistenceTests.Snapshots.cs" />
5455
<Compile Include="..\NEventStore.Persistence.Sql.Tests\PersistenceTests.Snapshots.Async.cs" Link="PersistenceTests.Snapshots.Async.cs" />
5556
</ItemGroup>

0 commit comments

Comments
 (0)