Cleanup | Consolidate connection capability metadata#3862
Conversation
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.
|
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. |
|
Thanks, I will take another look on Monday. |
|
/azp run |
|
Azure Pipelines successfully started running 2 pipeline(s). |
mdaigle
left a comment
There was a problem hiding this comment.
Looking good. just feeling hesitant about the changes to server-side version calculation.
Codecov Report❌ Patch coverage is 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
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:
|
|
/azp run |
|
Azure Pipelines successfully started running 2 pipeline(s). |
|
@priyankatiwari08 this is ready for another review |
|
/azp run |
|
Azure Pipelines successfully started running 2 pipeline(s). |
…up/unified-connection-capabilities
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
paulmedynski
left a comment
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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:
ColumnEncryptionVersionreplacesTceVersionSupported.IsColumnEncryptionSupportedmakes more sense - it just tests to see ifColumnEncryptionVersionis greater than zero.AreEnclaveRetriesSupportedis currently set totrueifTceVersionSupported == 3. This is replaced by an expression ofColumnEncryptionVersion >= 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(); |
There was a problem hiding this comment.
Why not Capabilities = new()? Is this trying to avoid allocations? Are Capabilities small enough to put on the stack (i.e. a struct)?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Can we remove this and let callers use TdsParser.Capabilities instead? They have the same scope/visibility.
There was a problem hiding this comment.
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.
Description
This PR emerges in part from discussion with @paulmedynski on #3858. We noticed that the
jsondatatype wouldn't appear when runningSqlConnection.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:
SqlLoginAckinstance 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.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.