SQLite: do not report success when a later statement or a busy commit fails - #38971
Merged
AndriySvyryd merged 2 commits intoSep 14, 2026
Merged
Conversation
- ExecuteNonQuery and ExecuteScalar left the statements after the first query to the reader's Dispose, which swallows their errors. They run them through NextResult now, so the error reaches the caller. - DisposeWithBusyHandling retried sqlite3_reset while it returned busy. reset only reports a busy commit once, so the second call returned OK with the write rolled back. It now retries by stepping, which retries the commit, and resets once. Fixes dotnet#32740
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The busy-wait tests are race-prone, and existing skipped tests retain obsolete expectations.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Balanced
- The release of the read lock now starts just before the call that has to wait, not before the second connection is opened, and each test asserts it was blocked for at least that long. On main the two waiting tests fail on that assertion, after 0.15s and 1ms. - The two tests skipped under dotnet#35585 ask for a busy error, so they set a command timeout shorter than the five seconds the reader is held. They pass both on main and with this change.
AndriySvyryd
approved these changes
Sep 14, 2026
AndriySvyryd
left a comment
Member
There was a problem hiding this comment.
Thanks for your contribution!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #32740
there are two places where the error is lost , and the reporter's program goes through both .
a statement after a query
ExecuteNonQueryisExecuteReader()thenDispose(). the reader stops at the first statement that returns columns , andDisposeruns the rest inside acatch { }. so this returns with no error , and because theCOMMITnever runs the row for 2 is rolled back :ExecuteScalarhas the same shape . both now run the remaining statements throughNextResult()before the reader is disposed , so the error comes out .Disposeitself is left alone .a busy commit after
RETURNINGthat alone did not fix the reporter's program , it still lost a row in 2 runs of 3 with no error . the insert has
RETURNING, so it is left unfinished and it commits whenNextResultresets it inDisposeWithBusyHandling. when that commit is busy the loop resets again , andsqlite3_resetonly reports the error once . i logged it : the reset codes were5,0,sqlite3_get_autocommitwas 1 afterwards , and the row was not there .this is not only the reporter's shape . on main ,
ExecuteReaderon anINSERT ... RETURNING,Read(), thenNextResult()while another connection holds a read lock returns false after about half a second with no error , and the row is not in the table .so instead of resetting again , it steps the statement to the end while it is busy , which retries the commit , and then resets once . that keeps the wait #36657 added , and when the timeout runs out the busy error is thrown instead of lost . it only applies to a write that is still unfinished , and not to the plain
Dispose()path .with both changes the reporter's program kept all 1000 rows in 20 runs of 20 . the commit was busy 5 times across those runs and each time it waited and committed .
tests
the busy tests hold a reader open on one connection , so the insert on the other one can run but cannot commit . no race , and each one uses its own file .
ExecuteNonQuery_throws_when_statement_after_query_fails,ExecuteNonQuery_does_not_lose_rows_silently_when_statement_after_query_fails,ExecuteScalar_throws_when_statement_after_query_fails: no exception on main , fixed by theSqliteCommandchangeNextResult_throws_instead_of_losing_the_write_when_commit_stays_busy_with_returning,NextResult_waits_for_the_commit_when_busy_with_returning: no exception and the row gone on main , still failing with only theSqliteCommandchange , fixed by theSqliteDataRecordchangeExecuteNonQuery_waits_for_the_commit_when_busy_with_returning: on main this throws busy at once instead of waiting . not data loss , but it is a change in behaviour ,ExecuteNonQuerynow waits up to the command timeout here like it does for other busy statementsMicrosoft.Data.Sqlite.sqlite3.Tests710 pass ,sqlite3mc711 pass ,EFCore.Sqlite.Tests896 pass ,EFCore.Sqlite.FunctionalTests38042 pass . the 177 functional failures here are allmod_spatialite.dylibnot being installed on this mac .because of that last point ,
ExecuteNonQuery_throws_when_busy_with_returningandExecuteScalar_throws_when_busy_with_returning, which are skipped under #35585 , would now fail if they were turned back on , since they expect a throw while the other connection holds the lock for 5 seconds . i left them as they are . if you would rather take the two halves separately , the busy part is only theSqliteDataRecordchange and the twoNextResulttests .🤖 Generated with Claude Code
https://claude.ai/code/session_01VfnFMKDUZWENKk7Vpe6GS7