Fix IsDBNull + stream read bug that skips column data#4082
Fix IsDBNull + stream read bug that skips column data#4082paulmedynski wants to merge 3 commits intomainfrom
Conversation
- Added a unit test that exposes the IsDBNullAsync() and GetFieldValueAsync() interaction problem.
…mlReader]>() that would skip column data.
There was a problem hiding this comment.
Pull request overview
Fixes a SqlDataReader sequential-access streaming regression where calling IsDBNull/IsDBNullAsync before GetFieldValue<T> for streaming types could consume the column payload and cause empty streams / asserts.
Changes:
- Updates
SqlDataReader.TryReadColumnInternalto avoid eagerly consuming column data whenforStreaming == trueand the column header was already read. - Adds new manual test coverage for
IsDBNull/IsDBNullAsync+ streamingGetFieldValue<T>combinations underCommandBehavior.SequentialAccess. - Adds test-utility configurability (env var override) and hardens an Always Encrypted CSP test’s data discovery on non-Windows.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs | Adjusts sequential-access read logic to preserve wire data for streaming readers after IsDBNull*. |
| src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderStreamsTest.cs | Adds manual regression tests for streaming reads after IsDBNull/IsDBNullAsync in sequential access. |
| src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Config.cs | Adds env-var override support for test config values. |
| src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/CspProviderExt.cs | Prevents registry access during test discovery on non-Windows. |
src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderStreamsTest.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Config.cs
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Config.cs
Show resolved
Hide resolved
Codecov Report✅ All modified and coverable lines are covered by tests.
Additional details and impacted files@@ Coverage Diff @@
## main #4082 +/- ##
==========================================
- Coverage 73.22% 68.10% -5.13%
==========================================
Files 280 275 -5
Lines 43000 66925 +23925
==========================================
+ Hits 31486 45578 +14092
- Misses 11514 21347 +9833
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
- Fixed a long-standing issue with test discovery failures in VS Code with the TestCommon project.
| <ItemGroup Condition="'$(TargetFramework)' == 'net462'"> | ||
| <PackageReference Include="Azure.Identity" /> | ||
| <PackageReference Include="Azure.Security.KeyVault.Keys" /> | ||
| <PackageReference Include="Microsoft.DotNet.XUnitExtensions" /> |
There was a problem hiding this comment.
This project doesn't use xUnit. Including these was causing VS Code test explorer to attempt test discovery, which failed consistently when builds occurred elsewhere.
| { | ||
| // xUnit evaluates MemberData during discovery, before [PlatformSpecific] | ||
| // can skip execution. Guard here to prevent Registry access on non-Windows. | ||
| if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
There was a problem hiding this comment.
The test class is gated by [PlatformSpecific(TestPlatforms.Windows)], but that only applies at test execution time. Test discovery occurs regardless of attributes like these, and in this case would fail on Unix due to the Windows-specific registry calls below.
| Assert.True(reader.IsDBNull(1)); | ||
| Assert.True(reader.IsDBNull(2)); | ||
| } | ||
| Assert.True(reader.Read()); |
There was a problem hiding this comment.
Fixed broken conditions here, that would pass the test if the Read() failed.
| } | ||
| #endif | ||
|
|
||
| #nullable enable |
There was a problem hiding this comment.
New tests that expose the bug behaviour. All of these tests fail without the change to SqlDataReader.
| } | ||
|
|
||
| // Allow environment variables to override individual config values. | ||
| SetFromEnv("MDS_TCPConnectionString", ref config.TCPConnectionString); |
There was a problem hiding this comment.
This makes it easy to run many tests (like the new ones above) with a single command:
MDS_TCPConnectionString="Server=tcp:127.0.0.1,1434;Database=Northwind;User ID=sa;Password=****;Encrypt=false;TrustServerCertificate=true" dotnet test --no-build src/Microsoft.Data.SqlClient/tests/ManualTests/ -f net10.0 --filter IsDBNull
No need to jump through the config file hoops.
There was a problem hiding this comment.
Please document the same in BUILDGUIDE so its easier for developers to know about these variables that can be used for local development.
Fix: IsDBNull/IsDBNullAsync breaks subsequent streaming reads in SequentialAccess mode
Fixes #3057
Problem
When using
CommandBehavior.SequentialAccess, callingIsDBNull()orIsDBNullAsync()on a column before reading it via a streaming type (GetFieldValue<Stream>,GetFieldValue<TextReader>,GetFieldValue<XmlReader>, or their async variants) causes:DebugAssertException— "Already read past column"Root Cause
TryReadColumnInternalinSqlDataReader.cshas two code paths for sequential access depending on whether the column header has already been read:Path A — header already read (
i < _nextColumnHeaderToRead):Path B — header not yet read (
_nextColumnHeaderToRead == i):Path B correctly checks both
!readHeaderOnlyand!forStreamingbefore consuming data. Path A only checks!readHeaderOnly, ignoringforStreaming.When
IsDBNull/IsDBNullAsyncis called first, it reads the column header (advancing_nextColumnHeaderToRead) but leaves data on the wire. The subsequent streamingGetFieldValue<Stream>call enters Path A (since the header is already read) and eagerly consumes all data viaTryReadColumnData(), leaving nothing for theSqlSequentialStreamto read.Fix
Add
&& (!forStreaming)to Path A's condition to match Path B's behavior:This ensures that when a streaming type is requested after
IsDBNullhas already read the column header, the column data stays on the wire for the sequential stream to consume incrementally.Compatibility
forStreaming == true, which is limited toGetFieldValue<T>/GetFieldValueAsync<T>withTbeingStream,TextReader, orXmlReader.forStreaming: false(the default) — zero behavior change.Tests
Added 7 new test methods (28 test cases) covering all combinations of:
IsDBNullAsyncGetFieldValueAsync<TextReader>IsDBNullAsyncGetFieldValueAsync<XmlReader>IsDBNull(sync)GetFieldValue<Stream>(sync)IsDBNull(sync)GetFieldValue<TextReader>(sync)IsDBNull(sync)GetFieldValue<XmlReader>(sync)IsDBNull(sync)GetFieldValueAsync<Stream>(async)IsDBNullAsyncGetFieldValue<Stream>(sync)Each test method has 4 inline data cases: null/non-null × checkNull true/false.
Verification:
checkNull: True(bug path)checkNull: False(control)Checklist