Skip to content

Fix | Write DateOnly instances to sql_variant columns of table-valued parameters as date values#4439

Open
edwardneal wants to merge 3 commits into
dotnet:mainfrom
edwardneal:fix/dateonly-tvp-variant
Open

Fix | Write DateOnly instances to sql_variant columns of table-valued parameters as date values#4439
edwardneal wants to merge 3 commits into
dotnet:mainfrom
edwardneal:fix/dateonly-tvp-variant

Conversation

@edwardneal

Copy link
Copy Markdown
Contributor

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 a DateOnly instance into this. It's supposed to be sent with a type of date, but is actually sent as a datetime.

Besides a point around correctness, it presents an issue when sending DateOnly instances which are valid values for date but not for datetime: these values overflow.

This PR fixes the issue, transporting all DateOnly instances with the date type on modern .NET. On .NET Framework, there's no DateOnly type - and thus there's no way for the client to encapsulate a date value within a sql_variant column.

@ErikEJ, this covers the only other way to write DateOnly instances to SQL Server and should hopefully completely close #3934. SqlClient will continue to read DateTime instances by default, but this is needed for backwards compatibility purposes.

NB: similar issues also exist with TimeOnly instances. TdsParser assumes that only TimeSpan instances 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.

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

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@ErikEJ ErikEJ left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@paulmedynski paulmedynski moved this from To triage to Backlog in SqlClient Board Jul 16, 2026
@cheenamalhotra cheenamalhotra added this to the 7.1.0-preview3 milestone Jul 20, 2026
@cheenamalhotra cheenamalhotra moved this from Backlog to In review in SqlClient Board Jul 20, 2026
@cheenamalhotra

Copy link
Copy Markdown
Member

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).

@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 37.50000% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.11%. Comparing base (fdebcd2) to head (3f540b5).
⚠️ Report is 21 commits behind head on main.

Files with missing lines Patch % Lines
...c/Microsoft/Data/SqlClient/Server/ValueUtilsSmi.cs 28.57% 5 Missing ⚠️
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     
Flag Coverage Δ
CI-SqlClient ?
PR-SqlClient-Project 65.11% <37.50%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

benrr101
benrr101 previously approved these changes Jul 22, 2026

@benrr101 benrr101 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 DateOnly written to sql_variant is transported with base type date (modern .NET only).
  • Relaxes variant metadata assertions to allow SqlDbType.Date where 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.

Comment on lines 1046 to 1053
case SqlDbType.Date:
#if NET
result = DateOnly.FromDateTime(GetDateTime_Unchecked(getters, ordinal));
break;
#endif
case SqlDbType.DateTime2:
result = GetDateTime_Unchecked(getters, ordinal);
break;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetSqlValue200 backs the public SqlDataRecord.GetSqlValue(int) (SqlDataRecord.cs:192), so returning DateOnly for date here changes its result for all date columns from DateTimeDateOnly 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)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

Support sql_variant containing DateOnly as native SQL date without client conversion

7 participants