Fix | Write DateOnly instances to sql_variant columns of table-valued parameters as date values#4439
Fix | Write DateOnly instances to sql_variant columns of table-valued parameters as date values#4439edwardneal wants to merge 3 commits into
Conversation
Instances of DateOnly stored within a SqlDataRecord field of type Variant will now be sent as dates, not datetimes. By extension: sending DateOnly instances with values outside the acceptable range for a datetime will no longer throw overflow exceptions.
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
|
/azp run |
|
Azure Pipelines: Successfully started running 3 pipeline(s). |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4439 +/- ##
==========================================
- Coverage 65.83% 65.11% -0.73%
==========================================
Files 287 285 -2
Lines 43763 66891 +23128
==========================================
+ Hits 28812 43555 +14743
- Misses 14951 23336 +8385
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
benrr101
left a comment
There was a problem hiding this comment.
Small change but seems pretty important!
| set | ||
| { | ||
| Debug.Assert(value != null && (value.SqlDbType == SqlDbType.Money || value.SqlDbType == SqlDbType.NVarChar), | ||
| Debug.Assert(value != null && (value.SqlDbType == SqlDbType.Money || value.SqlDbType == SqlDbType.NVarChar || value.SqlDbType == SqlDbType.Date), |
There was a problem hiding this comment.
Nit: I think we can use && value is SqlDbType.Money or SqlDbType.NVarChar or SqlDbType.Date to make it slightly less redundant.
(nit, so not required)
There was a problem hiding this comment.
Pull request overview
This PR addresses a correctness issue when sending DateOnly values into sql_variant columns in table-valued parameters (TVPs): the value was being transported as datetime rather than date, which can overflow for DateOnly min/max values and yields an incorrect base type.
Changes:
- Updates TVP/SMI value-setting logic so
DateOnlywritten tosql_variantis transported with base typedate(modern .NET only). - Relaxes variant metadata assertions to allow
SqlDbType.Datewhere variant-type metadata is tracked. - Updates manual variant tests to remove the prior overflow/“datetime” exemptions now that the scenario should succeed.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/DateTimeVariantTests.cs | Removes expected overflow exceptions / base-type overrides for DateOnly in sql_variant TVP scenarios. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/ValueUtilsSmi.cs | Routes DateOnly through date-specific variant metadata handling for TVP writes; also adjusts sql-value retrieval logic for SqlDbType.Date. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/SqlRecordBuffer.cs | Allows SqlDbType.Date in the debug assertion for variant metadata. |
| case SqlDbType.Date: | ||
| #if NET | ||
| result = DateOnly.FromDateTime(GetDateTime_Unchecked(getters, ordinal)); | ||
| break; | ||
| #endif | ||
| case SqlDbType.DateTime2: | ||
| result = GetDateTime_Unchecked(getters, ordinal); | ||
| break; |
There was a problem hiding this comment.
GetSqlValue200 backs the public SqlDataRecord.GetSqlValue(int) (SqlDataRecord.cs:192), so returning DateOnly for date here changes its result for all date columns from DateTime → DateOnly on modern .NET. That's a potentially breaking change (existing (DateTime)record.GetSqlValue(i) would throw InvalidCastException), and it's inconsistent with GetValue200, which still returns DateTime. Worth noting the prior read behavior wasn't buggy — this is collateral from the write fix.
Since the goal is fixing the write path, could we keep the read unchanged and steer the write via a storage-type hint the way the SqlDataReader path does at line ~1847 (the SetCompatibleValueV200 overload that takes a SqlBuffer.StorageType)?
There was a problem hiding this comment.
Thanks for catching this - that's absolutely not what I intended! The underlying problem is that FillCompatibleSettersFromRecord tried to base the ExtendedClrTypeCode selection on the type of the value returned by GetSqlValue. Strictly speaking, it's not correct for us to return SqlDateTime (this was designed to represent datetime, not date, time or datetime2) but I agree that the breaking change would be much worse.
I've changed FillCompatibleSettersFromRecord to steer the write in a similar way to FillCompatibleSettersFromReader, and confirmed that GetSqlValue's no longer a breaking change.
Interestingly, the behaviour differs here: ..FromReader will steer based on the Date and DateTime2 paths, while ..FromRecord will only steer based on the Date path. I don't think this is correct, but I think the datetime2-based test variations in DateTimeVariantTests need a more sceptical view - and possibly a follow-up PR.
Ensure that SqlDataRecord.GetSqlValue doesn't return a DateOnly instance. Apply the same pattern as SqlDataReader - directly expose the original variant type metadata, and base the decision on that.
Description
This builds on #4294, and handles the only other issue I saw while investigating #3934.
If a user-defined table type exists with a column of type
sql_variant, client applications can insert aDateOnlyinstance into this. It's supposed to be sent with a type ofdate, but is actually sent as adatetime.Besides a point around correctness, it presents an issue when sending
DateOnlyinstances which are valid values fordatebut not fordatetime: these values overflow.This PR fixes the issue, transporting all
DateOnlyinstances with thedatetype on modern .NET. On .NET Framework, there's noDateOnlytype - and thus there's no way for the client to encapsulate adatevalue within asql_variantcolumn.@ErikEJ, this covers the only other way to write
DateOnlyinstances to SQL Server and should hopefully completely close #3934. SqlClient will continue to readDateTimeinstances by default, but this is needed for backwards compatibility purposes.NB: similar issues also exist with
TimeOnlyinstances. TdsParser assumes that onlyTimeSpaninstances will be written as times, so there are failures to cast in a few different places.Issues
Builds on #4294. Possible resolution to #3934.
Testing
#4294 added the relevant tests, with an exemption carved out for this use case. I've removed the exemption and verified that it covers the test case.