Skip to content

Cleanup | Consolidate connection capability metadata#3862

Open
edwardneal wants to merge 36 commits into
dotnet:mainfrom
edwardneal:cleanup/unified-connection-capabilities
Open

Cleanup | Consolidate connection capability metadata#3862
edwardneal wants to merge 36 commits into
dotnet:mainfrom
edwardneal:cleanup/unified-connection-capabilities

Conversation

@edwardneal

@edwardneal edwardneal commented Dec 28, 2025

Copy link
Copy Markdown
Contributor

Description

This PR emerges in part from discussion with @paulmedynski on #3858. We noticed that the json datatype wouldn't appear when running SqlConnection.GetSchema("DataTypes") against an Azure SQL instance. This is because of an underlying design choice in SqlMetaDataFactory: it only uses the SQL Server version number to control whether specific queries are used and specific records are returned. This isn't compatible with Azure SQL (which always returns a version of 12.x.) To fix this, we need SqlMetaDataFactory to be able to make decisions based upon the server capabilities, whether determined by the version, the presence of a FEATUREEXTACK token, or the contents of one of those tokens' values.

In this PR, we introduce a new type, ConnectionCapabilities. This class takes on the responsibility of hosting the various feature flags as an object which SqlConnectionInternal, SqlMetaDataFactory and TdsParser can interrogate to check for feature availability. For good measure, I then plumb this object through to SqlMetaDataFactory. A subsequent PR can work out the best way to implement filtering.

Other points of errata:

  • Moving the LOGINACK capability/version interrogation into the new type means that we no longer need to keep the original SqlLoginAck instance as a field on SqlConnectionInternal, which means that it never leaves the scope it was created in. I've turned it into a readonly ref struct.
  • 229a04d enables SqlClient to build under the Release configuration. This was necessary to get the benchmarking to work.
  • Benchmarking confirms that the CPU time, number of Gen0 GCs and memory usage remains identical (or very slightly better - slightly fewer Gen0 collections) between this branch and main.

This is a comparatively large PR, so I've tried to make it practical to move commit-by-commit.

Issues

Builds on a conversation in #3858.
Prerequisite to #3833.

Testing

This is covered by existing tests.

This class parses FEATUREEXTACK and LOGINACK streams, updating an object which specifies the capabilities of a connection.
This also means that we no longer need to hold the original SqlLoginAck record in memory.
This is adjacent cleanup which is best kept separate from the next step.
This includes the now-redundant checking (and associated exception) of the TDS version, and the assignment to _is20XX.
Remove the now-redundant _is20XX fields.
We only ever use two of these versions, and they're based on TDS versions rather than SQL Server versions.
Convert the Major/Minor/Increment bit-shifting to a constant value for clarity, and associate it with ConnectionCapabilities to eliminate duplication.
Also add explanatory comment to describe reason for big-endian vs. little-endian reads.
Move UTF8 feature detection handling to ConnectionCapabilities.
This was always equal to !Capability.DnsCaching.
This enables the if condition from OnFeatureExtAck to continue to apply to capability processing.

Also remove now-redundant comments, and clean up one reference to IsSQLDNSCachingSupported.
This is never persisted, and eliminates an allocation
One missing validation path.

Also switched conditions to use pattern matching and to better align with original method for easier comparison and better codegen.
@edwardneal edwardneal requested a review from a team as a code owner December 28, 2025 23:23
@github-project-automation github-project-automation Bot moved this to To triage in SqlClient Board Dec 28, 2025
@cheenamalhotra cheenamalhotra moved this from To triage to In review in SqlClient Board Jan 5, 2026
@cheenamalhotra cheenamalhotra added the Code Health 💊 Issues/PRs that are targeted to source code quality improvements. label Jan 13, 2026
@github-actions

Copy link
Copy Markdown

This pull request has been marked as stale due to inactivity for more than 30 days.

If you would like to keep this pull request open, please provide an update or respond to any comments. Otherwise, it will be closed automatically in 7 days.

@github-actions github-actions Bot added the Stale The Issue or PR has become stale and will be automatically closed shortly if no activity occurs. label Feb 12, 2026
@mdaigle

mdaigle commented May 15, 2026

Copy link
Copy Markdown
Contributor

Thanks, I will take another look on Monday.

@mdaigle

mdaigle commented May 15, 2026

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@mdaigle mdaigle 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.

Looking good. just feeling hesitant about the changes to server-side version calculation.

Comment thread src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs Outdated
@codecov

codecov Bot commented May 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.81%. Comparing base (44e1b72) to head (49df425).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
...Data/SqlClient/Connection/SqlConnectionInternal.cs 67.85% 9 Missing ⚠️
...ata/SqlClient/Connection/ConnectionCapabilities.cs 88.37% 5 Missing ⚠️
...qlClient/src/Microsoft/Data/SqlClient/TdsParser.cs 89.28% 3 Missing ⚠️
.../Microsoft/Data/ProviderBase/DbConnectionClosed.cs 0.00% 1 Missing ⚠️
...icrosoft/Data/ProviderBase/DbConnectionInternal.cs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3862      +/-   ##
==========================================
- Coverage   65.32%   63.81%   -1.51%     
==========================================
  Files         285      282       -3     
  Lines       43373    66592   +23219     
==========================================
+ Hits        28335    42499   +14164     
- Misses      15038    24093    +9055     
Flag Coverage Δ
CI-SqlClient ?
PR-SqlClient-Project 63.81% <83.33%> (?)

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.

mdaigle
mdaigle previously approved these changes May 19, 2026

@mdaigle mdaigle 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.

Looks good. Thanks!

@mdaigle

mdaigle commented May 21, 2026

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@mdaigle mdaigle requested a review from priyankatiwari08 May 26, 2026 20:35
@mdaigle

mdaigle commented May 26, 2026

Copy link
Copy Markdown
Contributor

@priyankatiwari08 this is ready for another review

@mdaigle mdaigle added the Author attention needed PRs that require author to respond or make updates to PR. label Jun 2, 2026
@mdaigle mdaigle added Author attention needed PRs that require author to respond or make updates to PR. and removed Author attention needed PRs that require author to respond or make updates to PR. labels Jun 3, 2026
@mdaigle mdaigle removed the Author attention needed PRs that require author to respond or make updates to PR. label Jun 3, 2026
@mdaigle

mdaigle commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@mdaigle

mdaigle commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

/azp run

@mdaigle mdaigle requested a review from paulmedynski June 24, 2026 18:05
@azure-pipelines

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

@paulmedynski paulmedynski 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.

Great changes overall and a good step towards encapsulating concerns in this old spaghetti code! A few questions/comments for your consideration.

/// then the connection is to SQL Server 2022 or newer.
/// </summary>
public bool Is2022OrNewer =>
TdsVersion == TdsEnums.TDS80_VERSION;

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.

Is it possible that we chose to negotiate TDS 7.4 when connecting to SqlServer 2022+ (i.e. for TLS version or session encryption reasons)?

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.

Absolutely. This is just a drop-in replacement for the existing _is2022 variable in TdsParser, which is itself used solely to make sure that TDS 8.0 doesn't break a few features which are gated upon SQL Server 2008 support (namely, UDTs and the XML data type.)

/// Indicates support for user-defined CLR types (up to a length of 8000
/// bytes.) This was introduced in SQL Server 2005.
/// </summary>
public bool UserDefinedTypes => true;

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.

Thank you for not prepending Is or Has to these 😄 . I would even be happy with AzureSql and AtLeastSql2022, but am fine with those as-is.

/// Describes the capabilities and related information (such as the
/// reported server version and TDS version) of the connection.
/// </summary>
internal sealed class ConnectionCapabilities

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.

Are these properties immutable once the session is established? If so, it would be nice for instances of this class to be immutable as well. I suspect I'll see you creating it default and then filling in the flags as we process the login flow, so it is indeed convenient for that bit-by-bit accumulation. I just worry about accidental changes post-login.

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.

You're correct, it'll need to be filled in as the login flow proceeds. It's pretty ugly, but it flows downstream from the wider design of the parser. SqlClient just processes FEATUREEXTACK token streams when it receives them. It could (in theory) receive a FEATUREEXTACK token stream from a broken server at any stage, and this would be processed without a hitch.

There's also the case of a client redirection to another server to consider. In such a situation, we might end up needing to make changes as the result of routing following the receipt of a LOGINACK token stream.

I suspect that actually fixing this (and hardening against noncompliant/malicious servers) might involve peeling some of this processing away from TdsParser. That'd be a good idea, but it needs far more thought and test coverage of the login flows first.

public string? ColumnEncryptionEnclaveType { get; set; }

/// <summary>
/// Returns the capability records to unset values.

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.

Intereseting - I was expecting this to live/die per-TDS-session+connection.

{
// Extract the type of enclave being used by the server.
_parser.EnclaveType = Encoding.Unicode.GetString(data, 2, data.Length - 2);
Capabilities.ColumnEncryptionEnclaveType = Encoding.Unicode.GetString(data, 2, data.Length - 2);

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.

One possible behavioral change in OnFeatureExtAck for FEATUREEXT_TCE: enclave-related state is now derived from TceVersionSupported (via Capabilities.ColumnEncryptionVersion) instead of being set independently.

In particular, previously enclave type assignment was effectively driven by payload length, while now enclave semantics are tied to CE version (v2/v3). That seems correct, but it does change edge-case behavior for malformed/nonstandard payloads (e.g., v1 with extra enclave bytes).

Could we add/point to a test that locks this in explicitly?

  • v1: column encryption supported, no enclave semantics
  • v2: enclave type present/usable
  • v3: enclave retries supported

That would make the “no regression for valid inputs, stricter handling for invalid/legacy edge cases” intent very clear.

Looking for commentary on this before any changes (if at all).

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.

Could you clarify this please? Lines 1458-1467 already guaranteed supportedTceVersion is strictly greater than zero and less than or equal to TdsEnums.MAX_SUPPORTED_TCE_VERSION (0x03) - so by the time we exit that if-condition and start setting Capabilities.ColumnEncryptionVersion, we already have a guarantee that the value we give to it will be 1, 2 or 3.

With that in mind:

  • ColumnEncryptionVersion replaces TceVersionSupported.
  • IsColumnEncryptionSupported makes more sense - it just tests to see if ColumnEncryptionVersion is greater than zero.
  • AreEnclaveRetriesSupported is currently set to true if TceVersionSupported == 3. This is replaced by an expression of ColumnEncryptionVersion >= 3 (essentially the same thing, given that we're constraining the TCE versions we support as per above.)

ColumnEncryptionEnclaveType continues to be set based upon token payload length.

As far as commentary goes, TCE is definitely a divergence from the TDS spec (link). I just didn't want to make a correctness change at the same time as a refactor.

// Clean up IsSQLDNSCachingSupported flag from previous status
_connHandler.IsSQLDNSCachingSupported = false;
// Clean up all server capabilities from previous status.
Capabilities.Reset();

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.

Why not Capabilities = new()? Is this trying to avoid allocations? Are Capabilities small enough to put on the stack (i.e. a struct)?

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.

The first reason is to avoid allocations. We've gained a new object instantiation in ConnectionCapabilities, but we're no longer creating one in SqlLoginAck so it roughly balances out.

Besides allocations though, this comes closest to mirroring the existing model of resetting existing capability flags.

We can't put Capabilities on the stack - it's modified by TdsParser.TryProcessFeatureExtAck (and from there, SqlConnectionInternal.OnFeatureExtAck) and thus needs to be a property on TdsParser.

return _is2008;
}
}
internal bool Is2008OrNewer => Capabilities.Is2008R2OrNewer;

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.

Can we remove this and let callers use TdsParser.Capabilities instead? They have the same scope/visibility.

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.

We can, but there are dozens of these call sites - sometimes for each specific capability. I'd like to replace them all at once in a follow-up.

@github-project-automation github-project-automation Bot moved this from In progress to Waiting for customer in SqlClient Board Jun 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Code Health 💊 Issues/PRs that are targeted to source code quality improvements.

Projects

Status: Waiting for customer

Development

Successfully merging this pull request may close these issues.

6 participants