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. |
|
@edwardneal - New conflicts here. |
|
Thanks @paulmedynski - just merged. |
mdaigle
left a comment
There was a problem hiding this comment.
On second review, I don't think I'm comfortable with shifting so much parsing logic around without a long term plan or vision in place. I think the piece that clearly makes sense is a record to store and pass capabilities. Can we reduce the changes to just that?
| return new SqlMetaDataFactory(xmlStream, internalConnection.ServerVersion); | ||
|
|
||
| return new SqlMetaDataFactory(xmlStream, ((SqlConnectionInternal)internalConnection).Parser.Capabilities); |
There was a problem hiding this comment.
Can this be pushed up to some level where it's clear it will succeed? Then we can scope down the param type.
There was a problem hiding this comment.
I've pushed this up to DbConnectionInternal, returning null by default and matching the throwing/non-throwing behaviour on all derived classes.
| // Write to DNS Cache or clean up DNS Cache for TCP protocol | ||
| bool ret = false; | ||
| if (_connHandler._cleanSQLDNSCaching) | ||
| if (!Capabilities.DnsCaching) |
There was a problem hiding this comment.
Behavior change worth confirming: this was previously if (_connHandler._cleanSQLDNSCaching). That field defaults to false and was only set to true in OnFeatureExtAck when the server sent SQLDNSCACHING with data[0] == 0. So in main, connections to servers that don't send SQLDNSCACHING at all (i.e., non-Azure) did not call DeleteDNSInfo. With if (!Capabilities.DnsCaching) the inverse is true — those connections will now hit DeleteDNSInfo on every login. Intentional?
There was a problem hiding this comment.
Yes, correct - unintentional and removed. Thanks for pointing that out.
priyankatiwari08
left a comment
There was a problem hiding this comment.
Agree with @mdaigle's ask to reduce scope to just the capability record + SqlMetaDataFactory plumbing and defer the parsing relocation. Left one inline on TdsParser.cs about a DNS-caching cleanup behavior change in that relocated code — worth confirming before any of it lands.
|
Thanks both. I've pared this back, so |
|
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.
| // information provided by S. Ashwin | ||
| switch (majorMinor) | ||
| if (tdsVersion is not TdsEnums.SQL2005_VERSION | ||
| and not TdsEnums.SQL2008_VERSION |
There was a problem hiding this comment.
Can you take a close look at this and double check that this is safe? The masking behavior is making me think that there are a range of accepted server-side values.
There was a problem hiding this comment.
I've re-checked. That first impression was part of the reason why I've left it in here post-simplification - the switch is a little odd.
majorMinor and increment are calculated as below in TdsParser (lines 4321-4322):
// Bits 0-15 and 24-31
majorMinor = [TdsVersion] & 0xFF00FFFF;
// Shift right by 16 bits and take lower 8 bits. Equivalent maths is below.
increment = ([TdsVersion] >> 16) & 0xFF;
increment = ([TdsVersion] & 0x00FF0000) >> 16;There are four cases: one per combination of majorMinor. Each case has one valid increment value, and anything outside of these cases fails. They're present in TdsParser (lines 4332-4360.)
| SQL 2005 | SQL 2008 | SQL 2012 / TDS 7.x | TDS 8 | |
|---|---|---|---|---|
majorMinor expression |
(SQL2005_MAJOR << 24) OR SQL2005_RTM_MINOR |
(SQL2008_MAJOR << 24) OR SQL2008_MINOR |
(SQL2012_MAJOR << 24) OR SQL2012_MINOR |
(TDS8_MAJOR << 24) OR TDS8_MINOR |
| Major version constant | 0x72 | 0x73 | 0x74 | 0x08 |
| Minor version constant | 0x0002 | 0x0003 | 0x0004 | 0x0000 |
Resultant majorMinor |
0x72_00_0002 | 0x73_00_0003 | 0x74_00_0004 | 0x08_00_0000 |
Acceptable increment |
SQL2005_INCREMENT |
SQL2008_INCREMENT |
SQL2012_INCREMENT |
TDS8_INCREMENT |
Resultant increment |
0x09 | 0x0B | 0x00 | 0x00 |
Since both majorMinor and increment come from the same [TdsVersion] value, we can OR them together and perform the comparison in one step, resulting in four cases:
- SQL 2005: 0x72_09_0002 (
SQL2005_VERSION) - SQL 2008: 0x73_0B_0003 (
SQL2008_VERSION) - SQL 2012: 0x74_00_0004 (
TDS7X_VERSION) - TDS 8: 0x08_00_0000 (
TDS80_VERSION)
This corresponds to the values specified in MS-TDS for the LOGINACK token stream for every value besides TDS 8 - Appendix A, <72>.
There's also prior art in the tediousjs version mapping here and in mssql-jdbc here.
This switch also contains the logic determining whether the remote server is SQL 2022 or newer / SQL 2012 or newer / SQL 2008 R2 or newer. I've moved this logic to ConnectionCapabilities, lines 54-69.
There was a problem hiding this comment.
Makes sense, and the new code is much cleaner. Can we refactor this logic into a static method and add some unit testing to prove equivalency (copy old code into test and compare)? I think I need to see that to feel comfortable with this. Plus, it will give a good spot to verify version parsing if/when we add a new version in the future.
There was a problem hiding this comment.
I've just shuffled the validation logic and added a unit test in AdapterUtilTest.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3862 +/- ##
==========================================
+ Coverage 65.96% 69.55% +3.59%
==========================================
Files 275 273 -2
Lines 42993 70290 +27297
==========================================
+ Hits 28361 48892 +20531
- Misses 14632 21398 +6766
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:
|
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 parsingLOGINACKandFEATUREEXTACKtokens, then converting them into 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.LOGINACK parsing is handled by the new object's
ProcessLoginAckmethod. This will look very different to the originalTryProcessLoginAckmethod in TdsParser; the latter had a great deal of legacy baggage from needing to deal with SQL Server 2000 compatibility, which we no longer need to carry.FEATUREEXTACK parsing is handled by
ProcessFeatureExtAck. I've kept this fairly close to the originalOnFeatureExtAckmethod in SqlConnectionInternal.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
I'm planning to make the coverage more explicit with unit tests for each relevant FEATUREEXTACK, but the existing test coverage should be fine.
We already have coverage (ConnectionTests.ConnectionTestDeniedVersion and ConnectionTestPermittedVersion) which specifically tests the LOGINACK component. Existing test functionality should prove the FEATUREEXTACK parsing - if there are issues parsing the relevant FEATUREEXTACK structures, the AlwaysEncrypted/vector/JSON/etc. tests will fail.