Skip to content

fix(sources): reserve the engine's cursor/pageSize bind names from run-time parameters - #253

Merged
thiagoluga merged 1 commit into
masterfrom
fix/reserved-run-parameters
Aug 3, 2026
Merged

fix(sources): reserve the engine's cursor/pageSize bind names from run-time parameters#253
thiagoluga merged 1 commit into
masterfrom
fix/reserved-run-parameters

Conversation

@thiagoluga

Copy link
Copy Markdown
Owner

What

Run-time (execution) parameters were bound before the engine's own @cursor and @pageSize, and AddParameter deliberately keeps whichever name was bound first (that's how "run-time beats the author's static parameter" works, WP4). Nothing reserved the engine's own names.

So a report run supplying a parameter named cursor pinned it for every page:

POST /api/reports/sales/run
{"parameters":{"cursor":null}}

A standard keyset query — WHERE (@cursor IS NULL OR Id > @cursor) ORDER BY Id — then returns the same first page forever. The runner's page loop only stops when the source reports no more data (ReportRunner while(true) + if (!batch.HasMore) break), and hasMore stays true, so the run never terminates and keeps appending to its staging file. Constant memory holds; disk does not.

A milder variant: {"parameters":{"pageSize":1}} makes records.Count != _pageSize on the first page, so the report silently completes with 1 row and status Completed.

Fix

Bind the engine's reserved names first, so the existing first-wins rule protects them. Run-time parameters still override the source's static ones for every other name — that behaviour is unchanged. Applied to both ReadBatchAsync and the row-count query (CountAsync), which had the same ordering.

SqlKeysetSource (SQL Server) and AdoNamedKeysetSource both delegate to AdoKeysetSource, so every relational provider is covered by the one change.

Verification

  • New Sqlite integration test A_run_time_parameter_cannot_take_over_the_engine_cursor: reads two pages while passing a hostile cursor parameter and asserts page 2 moves past page 1.
  • Verified to fail without the fix — page 2 repeated page 1 (should be greater than).
  • Sqlite integration suite 18 passed; full solution builds clean.

Found by a bug hunt over the HTTP API layer.

…n-time parameters

Run-time (execution) parameters were bound BEFORE the engine's own @cursor and
@pageSize, and AddParameter skips a name that is already bound — so a caller
passing a parameter named "cursor" pinned the keyset cursor to their value for
every page. A standard keyset query (WHERE (@cursor IS NULL OR Id > @cursor)
ORDER BY Id) then returns the same first page forever: the runner's page loop
only stops on HasMore=false, so the run never terminates and keeps appending to
its temp file. Reachable from one report-run request.

Bind the engine's reserved names first, so the existing first-wins rule protects
them; run-time parameters still beat the source's static ones for every other
name. Applied to both the read and the row-count query. Covered by a Sqlite
integration test verified to fail without the fix (page 2 repeated page 1).
@sonarqubecloud

sonarqubecloud Bot commented Aug 3, 2026

Copy link
Copy Markdown

@sonarqubecloud

sonarqubecloud Bot commented Aug 3, 2026

Copy link
Copy Markdown

@thiagoluga
thiagoluga merged commit f2c81b4 into master Aug 3, 2026
5 checks passed
@thiagoluga
thiagoluga deleted the fix/reserved-run-parameters branch August 3, 2026 20:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant