From a6646c0a43a4593901feb8373c9749be4f3547dc Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 27 Dec 2025 15:00:15 +0000 Subject: [PATCH 01/23] Add ConnectionCapabilities class This class parses FEATUREEXTACK and LOGINACK streams, updating an object which specifies the capabilities of a connection. --- .../src/Microsoft.Data.SqlClient.csproj | 3 + .../netfx/src/Microsoft.Data.SqlClient.csproj | 3 + .../Connection/ConnectionCapabilities.cs | 295 ++++++++++++++++++ 3 files changed, 301 insertions(+) create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index c7983fcc15..c2e88310da 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -272,6 +272,9 @@ Microsoft\Data\SqlClient\Connection\CachedContexts.cs + + Microsoft\Data\SqlClient\Connection\ConnectionCapabilities.cs + Microsoft\Data\SqlClient\Connection\ServerInfo.cs diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index ea6b7de6ce..afe4b6b02c 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -342,6 +342,9 @@ Microsoft\Data\SqlClient\Connection\CachedContexts.cs + + Microsoft\Data\SqlClient\Connection\ConnectionCapabilities.cs + Microsoft\Data\SqlClient\Connection\ServerInfo.cs diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs new file mode 100644 index 0000000000..6ace475f69 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs @@ -0,0 +1,295 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +#nullable enable + +namespace Microsoft.Data.SqlClient; + +/// +/// Describes the capabilities and related information (such as the +/// reported server version and TDS version) of the connection. +/// +internal sealed class ConnectionCapabilities +{ + /// + /// This TDS version is used by SQL Server 2008 R2. + /// + private const uint SqlServer2008R2TdsVersion = 0x73_0B_0003; + /// + /// This TDS version is used by SQL Server 2012 and onwards. + /// In SQL Server 2022 and SQL Server 2025, this is used when + /// the SQL Server instance is responding with the TDS 7.x + /// protocol. + /// + private const uint SqlServer2012TdsVersion = 0x74_00_0004; + /// + /// This TDS version is used by SQL Server 2022 and onwards, + /// when responding with the TDS 8.x protocol. + /// + private const uint SqlServer2022TdsVersion = 0x08_00_0000; + + private readonly int _objectId; + + /// + /// The TDS version reported by the LoginAck response + /// from the server. + /// + public uint TdsVersion { get; private set; } + + /// + /// The SQL Server major version reported by the LoginAck + /// response from the server. + /// + public byte ServerMajorVersion { get; private set; } + + /// + /// The SQL Server minor version reported by the LoginAck + /// response from the server. + /// + public byte ServerMinorVersion { get; private set; } + + /// + /// The SQL Server build number reported by the LoginAck + /// response from the server. + /// + public ushort ServerBuildNumber { get; private set; } + + /// + /// The user-friendly SQL Server version reported by the + /// LoginAck response from the server. + /// + public string ServerVersion => + $"{ServerMajorVersion:00}.{ServerMinorVersion:00}.{ServerBuildNumber:0000}"; + + /// + /// If true (as determined by the value of ) + /// then the connection is to SQL Server 2008 R2 or newer. + /// + public bool Is2008R2OrNewer => + Is2012OrNewer || TdsVersion == SqlServer2008R2TdsVersion; + + /// + /// If true (as determined by the value of ) + /// then the connection is to SQL Server 2012 or newer. + /// + public bool Is2012OrNewer => + Is2022OrNewer || TdsVersion == SqlServer2012TdsVersion; + + /// + /// If true (as determined by the value of ) + /// then the connection is to SQL Server 2022 or newer. + /// + public bool Is2022OrNewer => + TdsVersion == SqlServer2022TdsVersion; + + /// + /// If true, this connection is to an Azure SQL instance. This is determined + /// by the receipt of a FEATUREEXTACK token of value 0x08. + /// + public bool IsAzureSql { get; private set; } + + /// + /// Indicates support for user-defined CLR types (up to a length of 8000 + /// bytes.) This was introduced in SQL Server 2005. + /// + public bool UserDefinedTypes => true; + + /// + /// Indicates support for the xml data type. This was introduced + /// in SQL Server 2005. + /// + public bool XmlDataType => true; + + /// + /// Indicates support for the date, time, datetime2 + /// and datetimeoffset data types. These were introduced in SQL + /// Server 2008. + /// + public bool ExpandedDateTimeDataTypes => Is2008R2OrNewer; + + /// + /// Indicates support for user-defined CLR types of any length. This + /// was introduced in SQL Server 2008. + /// + public bool LargeUserDefinedTypes => Is2008R2OrNewer; + + /// + /// Indicates support for the client to include a TDS trace header, + /// which is surfaced in XEvents traces to correlate events between + /// the client and the server. This was introduced in SQL Server 2012. + /// + public bool TraceHeader => Is2012OrNewer; + + /// + /// Indicates support for UTF8 collations. This was introduced in SQL + /// Server 2019, and is only available if a FEATUREEXTACK token of value + /// 0x0A is received. + /// + public bool Utf8 { get; private set; } + + /// + /// Indicates support for the client to cache DNS resolution responses for + /// the server. This is only supported by Azure SQL, and is only available + /// if a FEATUREEXTACK token of value 0x0B is received. + /// + public bool DnsCaching { get; private set; } + + /// + /// Indicates support for Data Classification and specifies the version of + /// Data Classification which is supported. This was introduced in SQL + /// Server 2019, and is only available if a FEATUREEXTACK token of value + /// 0x09 is received. + /// + /// + /// This should only be 1 or 2. + /// + public byte DataClassificationVersion { get; private set; } + + /// + /// Indicates support for Global Transactions. This is only supported by + /// Azure SQL, and is only available if a FEATUREEXTACK token of value + /// 0x05 is received. + /// + public bool GlobalTransactions { get; private set; } + + /// + /// Indicates support for Enhanced Routing. This is only supported by + /// Azure SQL, and is only available if a FEATUREEXTACK token of value + /// 0x0F is received. + /// + public bool EnhancedRouting { get; private set; } + + /// + /// Indicates support for connecting to the current connection's failover + /// partner with an Application Intent of ReadOnly. This is only supported + /// by Azure SQL, and is only available if a FEATUREEXTACK token of value + /// 0x08 is received, and if bit zero of this token's data is set. + /// + public bool ReadOnlyFailoverPartnerConnection { get; private set; } + + /// + /// Indicates support for the vector data type, with a backing type + /// of float32. This was introduced in SQL Server 2022, and is only + /// available if a FEATUREEXTACK token of value 0x0E is received, and + /// if the version in this token's data is greater than or equal to 1. + /// + public bool Float32VectorType { get; private set; } + + /// + /// Indicates support for the json data type. This was introduced in + /// SQL Server 2022, and is only available if a FEATUREEXTACK token of value + /// 0x0D is received, and if the version in this token's data is + /// greater than or equal to 1. + /// + public bool JsonType { get; private set; } + + public ConnectionCapabilities(int parentObjectId) + { + _objectId = parentObjectId; + } + + /// + /// Returns the capability records to unset values. + /// + public void Reset() + { + TdsVersion = 0; + ServerMajorVersion = 0; + ServerMinorVersion = 0; + ServerBuildNumber = 0; + + IsAzureSql = false; + Utf8 = false; + DnsCaching = false; + DataClassificationVersion = TdsEnums.DATA_CLASSIFICATION_NOT_ENABLED; + GlobalTransactions = false; + EnhancedRouting = false; + ReadOnlyFailoverPartnerConnection = false; + Float32VectorType = false; + JsonType = false; + } + + /// + /// Updates the connection capability record based upon the LOGINACK + /// token stream sent by the server. + /// + /// The LOGINACK token stream sent by the server + public void ProcessLoginAck(SqlLoginAck loginAck) + { + if (loginAck.tdsVersion is not SqlServer2008R2TdsVersion + and not SqlServer2012TdsVersion + and not SqlServer2022TdsVersion) + { + throw SQL.InvalidTDSVersion(); + } + + TdsVersion = loginAck.tdsVersion; + ServerMajorVersion = loginAck.majorVersion; + ServerMinorVersion = loginAck.minorVersion; + ServerBuildNumber = (ushort)loginAck.buildNum; + } + + public void ProcessFeatureExtAck(byte featureId, ReadOnlySpan featureData) + { + switch (featureId) + { + case TdsEnums.FEATUREEXT_UTF8SUPPORT: + // The server can send and receive UTF8-encoded data if bit 0 of the + // feature data is set. + Utf8 = !featureData.IsEmpty && (featureData[0] & 0x01) == 0x01; + break; + + case TdsEnums.FEATUREEXT_SQLDNSCACHING: + // The client may cache DNS resolution responses if bit 0 of the feature + // data is set. + DnsCaching = !featureData.IsEmpty && (featureData[0] & 0x01) == 0x01; + break; + + case TdsEnums.FEATUREEXT_DATACLASSIFICATION: + // The feature data is comprised of a single byte containing the version, + // followed by another byte indicating whether or not data classification is + // enabled. + DataClassificationVersion = + featureData.Length == 2 + && featureData[1] == 0x00 + && featureData[0] > 0x00 + && featureData[0] <= TdsEnums.DATA_CLASSIFICATION_VERSION_MAX_SUPPORTED + ? featureData[0] + : TdsEnums.DATA_CLASSIFICATION_NOT_ENABLED; + break; + + case TdsEnums.FEATUREEXT_GLOBALTRANSACTIONS: + // Feature data is comprised of a single byte which indicates whether + // global transactions are available. + GlobalTransactions = !featureData.IsEmpty && featureData[0] == 0x01; + break; + + case TdsEnums.FEATUREEXT_AZURESQLSUPPORT: + IsAzureSql = true; + // Clients can connect to the failover partner with a read-only AppIntent if bit 0 + // of the only byte in the feature data is set. + ReadOnlyFailoverPartnerConnection = !featureData.IsEmpty && (featureData[0] & 0x01) == 0x01; + break; + + case TdsEnums.FEATUREEXT_VECTORSUPPORT: + // Feature data is comprised of a single byte which specifies the version of the vector + // type which is available. + Float32VectorType = !featureData.IsEmpty && featureData[0] != 0x00 + && featureData[0] <= TdsEnums.MAX_SUPPORTED_VECTOR_VERSION; + break; + + case TdsEnums.FEATUREEXT_JSONSUPPORT: + // Feature data is comprised of a single byte which specifies the version of the JSON + // type which is available. + JsonType = !featureData.IsEmpty && featureData[0] != 0x00 + && featureData[0] <= TdsEnums.MAX_SUPPORTED_JSON_VERSION; + break; + + default: + throw SQL.ParsingError(ParsingErrorState.UnrequestedFeatureAckReceived); + } + } +} From e37f3a28e6a48d08e87d6a783e6f34399207b79f Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 27 Dec 2025 20:10:00 +0000 Subject: [PATCH 02/23] Hook LOGINACK handling This also means that we no longer need to hold the original SqlLoginAck record in memory. --- .../SqlClient/Connection/SqlConnectionInternal.cs | 11 ++--------- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 10 ++++++++-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs index 5567dac6d5..9fc173376c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs @@ -266,8 +266,6 @@ internal class SqlConnectionInternal : DbConnectionInternal, IDisposable private string _instanceName = string.Empty; - private SqlLoginAck _loginAck; - /// /// This is used to preserve the authentication context object if we decide to cache it for /// subsequent connections in the same pool. This will finally end up in @@ -500,10 +498,8 @@ internal SqlConnectionInternal( #region Properties // @TODO: Make internal - public override string ServerVersion - { - get => $"{_loginAck.majorVersion:00}.{(short)_loginAck.minorVersion:00}.{_loginAck.buildNum:0000}"; - } + public override string ServerVersion => + _parser.Capabilities.ServerVersion; /// /// Gets the collection of async call contexts that belong to this connection. @@ -1025,8 +1021,6 @@ public override void Dispose() finally { // Close will always close, even if exception is thrown. - // Remember to null out any object references. - _loginAck = null; // Mark internal connection as closed _fConnectionOpen = false; @@ -1943,7 +1937,6 @@ internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo) internal void OnLoginAck(SqlLoginAck rec) { - _loginAck = rec; if (_recoverySessionData != null) { if (_recoverySessionData._tdsVersion != rec.tdsVersion) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index 07ab80b96a..e659ff8887 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -112,8 +112,6 @@ internal sealed partial class TdsParser internal TdsParserSessionPool _sessionPool = null; // initialized only when we're a MARS parser. - // Version variables - private bool _is2008 = false; private bool _is2012 = false; @@ -154,6 +152,9 @@ internal sealed partial class TdsParser // NOTE: You must take the internal connection's _parserLock before modifying this internal bool _asyncWrite = false; + // Capability records + internal ConnectionCapabilities Capabilities { get; } + /// /// Get or set if column encryption is supported by the server. /// @@ -205,6 +206,8 @@ internal TdsParser(bool MARS, bool fAsynchronous) { _fMARS = MARS; // may change during Connect to pre 2005 servers + Capabilities = new(ObjectID); + _physicalStateObj = TdsParserStateObjectFactory.Singleton.CreateTdsParserStateObject(this); DataClassificationVersion = TdsEnums.DATA_CLASSIFICATION_NOT_ENABLED; } @@ -1552,6 +1555,8 @@ internal void Disconnect() _defaultEncoding = null; _defaultCollation = null; + + Capabilities.Reset(); } // Fires a single InfoMessageEvent @@ -2763,6 +2768,7 @@ internal TdsOperationStatus TryRun(RunBehavior runBehavior, SqlCommand cmdHandle return result; } + Capabilities.ProcessLoginAck(ack); _connHandler.OnLoginAck(ack); break; } From de5c285abd0c832743a460ecc709295e95ee7339 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 27 Dec 2025 20:17:00 +0000 Subject: [PATCH 03/23] Update reference to _is20XX to reference Capabilities property --- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index e659ff8887..d48a8877cf 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -265,13 +265,7 @@ internal EncryptionOptions EncryptionOptions } } - internal bool Is2008OrNewer - { - get - { - return _is2008; - } - } + internal bool Is2008OrNewer => Capabilities.Is2008R2OrNewer; internal bool MARSOn { @@ -326,13 +320,7 @@ internal SqlStatistics Statistics } } - private bool IncludeTraceHeader - { - get - { - return (_is2012 && SqlClientEventSource.Log.IsEnabled()); - } - } + private bool IncludeTraceHeader => Capabilities.Is2012OrNewer && SqlClientEventSource.Log.IsEnabled(); internal int IncrementNonTransactedOpenResultCount() { @@ -10205,7 +10193,7 @@ internal Task TdsExecuteRPC(SqlCommand cmd, IList<_SqlRPC> rpcArray, int timeout continue; } - if (!_is2008 && !mt.Is90Supported) + if (!Is2008OrNewer && !mt.Is90Supported) { throw ADP.VersionDoesNotSupportDataType(mt.TypeName); } @@ -10959,7 +10947,7 @@ private void WriteSmiParameter(SqlParameter param, int paramIndex, bool sendDefa ParameterPeekAheadValue peekAhead; SmiParameterMetaData metaData = param.MetaDataForSmi(out peekAhead); - if (!_is2008) + if (!Is2008OrNewer) { MetaType mt = MetaType.GetMetaTypeFromSqlDbType(metaData.SqlDbType, metaData.IsMultiValued); throw ADP.VersionDoesNotSupportDataType(mt.TypeName); From 65e701f0369a6989bef5623f42a7b42907d46cb1 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 27 Dec 2025 20:18:53 +0000 Subject: [PATCH 04/23] Errata: use BinaryPrimitives This is adjacent cleanup which is best kept separate from the next step. --- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index d48a8877cf..e394bbe3bb 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -4194,7 +4194,7 @@ private TdsOperationStatus TryProcessLoginAck(TdsParserStateObject stateObj, out { return result; } - a.tdsVersion = (uint)((((((b[0] << 8) | b[1]) << 8) | b[2]) << 8) | b[3]); // bytes are in motorola order (high byte first) + a.tdsVersion = BinaryPrimitives.ReadUInt32BigEndian(b); uint majorMinor = a.tdsVersion & 0xff00ffff; uint increment = (a.tdsVersion >> 16) & 0xff; From 943c7db94f1eb8a6711e73d957743d7335d66299 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 27 Dec 2025 20:22:47 +0000 Subject: [PATCH 05/23] Move handling of SqlLoginAck to ConnectionCapabilities This includes the now-redundant checking (and associated exception) of the TDS version, and the assignment to _is20XX. Remove the now-redundant _is20XX fields. --- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 52 +------------------ 1 file changed, 2 insertions(+), 50 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index e394bbe3bb..98f9c6ae64 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -112,12 +112,6 @@ internal sealed partial class TdsParser internal TdsParserSessionPool _sessionPool = null; // initialized only when we're a MARS parser. - private bool _is2008 = false; - - private bool _is2012 = false; - - private bool _is2022 = false; - // SqlStatistics private SqlStatistics _statistics = null; @@ -2756,7 +2750,6 @@ internal TdsOperationStatus TryRun(RunBehavior runBehavior, SqlCommand cmdHandle return result; } - Capabilities.ProcessLoginAck(ack); _connHandler.OnLoginAck(ack); break; } @@ -4195,49 +4188,6 @@ private TdsOperationStatus TryProcessLoginAck(TdsParserStateObject stateObj, out return result; } a.tdsVersion = BinaryPrimitives.ReadUInt32BigEndian(b); - uint majorMinor = a.tdsVersion & 0xff00ffff; - uint increment = (a.tdsVersion >> 16) & 0xff; - - // Server responds: - // 0x07000000 -> 7.0 // Notice server response format is different for bwd compat - // 0x07010000 -> 2000 RTM // Notice server response format is different for bwd compat - // 0x71000001 -> 2000 SP1 - // 0x72xx0002 -> 2005 RTM - // information provided by S. Ashwin - switch (majorMinor) - { - case TdsEnums.SQL2005_MAJOR << 24 | TdsEnums.SQL2005_RTM_MINOR: // 2005 - if (increment != TdsEnums.SQL2005_INCREMENT) - { - throw SQL.InvalidTDSVersion(); - } - break; - case TdsEnums.SQL2008_MAJOR << 24 | TdsEnums.SQL2008_MINOR: - if (increment != TdsEnums.SQL2008_INCREMENT) - { - throw SQL.InvalidTDSVersion(); - } - _is2008 = true; - break; - case TdsEnums.SQL2012_MAJOR << 24 | TdsEnums.SQL2012_MINOR: - if (increment != TdsEnums.SQL2012_INCREMENT) - { - throw SQL.InvalidTDSVersion(); - } - _is2012 = true; - break; - case TdsEnums.TDS8_MAJOR << 24 | TdsEnums.TDS8_MINOR: - if (increment != TdsEnums.TDS8_INCREMENT) - { - throw SQL.InvalidTDSVersion(); - } - _is2022 = true; - break; - default: - throw SQL.InvalidTDSVersion(); - } - _is2012 |= _is2022; - _is2008 |= _is2012; stateObj._outBytesUsed = stateObj._outputHeaderLen; byte len; @@ -4276,6 +4226,8 @@ private TdsOperationStatus TryProcessLoginAck(TdsParserStateObject stateObj, out a.buildNum = (short)((buildNumHi << 8) + buildNumLo); + Capabilities.ProcessLoginAck(a); + Debug.Assert(_state == TdsParserState.OpenNotLoggedIn, "ProcessLoginAck called with state not TdsParserState.OpenNotLoggedIn"); _state = TdsParserState.OpenLoggedIn; From c4ab86d546eba4be18ae5cc6f57d80e53936b4d1 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 27 Dec 2025 20:59:50 +0000 Subject: [PATCH 06/23] TdsEnums.cs constants cleanup 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. --- .../Connection/ConnectionCapabilities.cs | 4 +- .../src/Microsoft/Data/SqlClient/TdsEnums.cs | 57 +------------------ .../src/Microsoft/Data/SqlClient/TdsParser.cs | 12 +++- 3 files changed, 15 insertions(+), 58 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs index 6ace475f69..89c7a3bacd 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs @@ -24,12 +24,12 @@ internal sealed class ConnectionCapabilities /// the SQL Server instance is responding with the TDS 7.x /// protocol. /// - private const uint SqlServer2012TdsVersion = 0x74_00_0004; + private const uint SqlServer2012TdsVersion = TdsEnums.TDS7X_VERSION; /// /// This TDS version is used by SQL Server 2022 and onwards, /// when responding with the TDS 8.x protocol. /// - private const uint SqlServer2022TdsVersion = 0x08_00_0000; + private const uint SqlServer2022TdsVersion = TdsEnums.TDS80_VERSION; private readonly int _objectId; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs index f6df0a82fe..4571e802b6 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -314,62 +314,11 @@ public enum ActiveDirectoryWorkflow : byte public const int TEXT_TIME_STAMP_LEN = 8; public const int COLLATION_INFO_LEN = 4; - /* - public const byte INT4_LSB_HI = 0; // lsb is low byte (e.g. 68000) - // public const byte INT4_LSB_LO = 1; // lsb is low byte (e.g. VAX) - public const byte INT2_LSB_HI = 2; // lsb is low byte (e.g. 68000) - // public const byte INT2_LSB_LO = 3; // lsb is low byte (e.g. VAX) - public const byte FLT_IEEE_HI = 4; // lsb is low byte (e.g. 68000) - public const byte CHAR_ASCII = 6; // ASCII character set - public const byte TWO_I4_LSB_HI = 8; // lsb is low byte (e.g. 68000 - // public const byte TWO_I4_LSB_LO = 9; // lsb is low byte (e.g. VAX) - // public const byte FLT_IEEE_LO = 10; // lsb is low byte (e.g. MSDOS) - public const byte FLT4_IEEE_HI = 12; // IEEE 4-byte floating point -lsb is high byte - // public const byte FLT4_IEEE_LO = 13; // IEEE 4-byte floating point -lsb is low byte - public const byte TWO_I2_LSB_HI = 16; // lsb is high byte - // public const byte TWO_I2_LSB_LO = 17; // lsb is low byte - - public const byte LDEFSQL = 0; // server sends its default - public const byte LDEFUSER = 0; // regular old user - public const byte LINTEGRATED = 8; // integrated security login - */ - - /* Versioning scheme table: - - Client sends: - 0x70000000 -> 7.0 - 0x71000000 -> 2000 RTM - 0x71000001 -> 2000 SP1 - 0x72xx0002 -> 2005 RTM - - Server responds: - 0x07000000 -> 7.0 // Notice server response format is different for bwd compat - 0x07010000 -> 2000 RTM // Notice server response format is different for bwd compat - 0x71000001 -> 2000 SP1 - 0x72xx0002 -> 2005 RTM - */ - - // Majors: - // For 2000 SP1 and later the versioning schema changed and - // the high-byte is sufficient to distinguish later versions - public const int SQL2005_MAJOR = 0x72; - public const int SQL2008_MAJOR = 0x73; - public const int SQL2012_MAJOR = 0x74; - public const int TDS8_MAJOR = 0x08; // TDS8 version to be used at login7 + // Versions to be used on login for TDS 7.x and TDS 8.0 + public const uint TDS7X_VERSION = 0x74_00_0004; + public const uint TDS80_VERSION = 0x08_00_0000; public const string TDS8_Protocol = "tds/8.0"; //TDS8 - // Increments: - public const int SQL2005_INCREMENT = 0x09; - public const int SQL2008_INCREMENT = 0x0b; - public const int SQL2012_INCREMENT = 0x00; - public const int TDS8_INCREMENT = 0x00; - - // Minors: - public const int SQL2005_RTM_MINOR = 0x0002; - public const int SQL2008_MINOR = 0x0003; - public const int SQL2012_MINOR = 0x0004; - public const int TDS8_MINOR = 0x00; - public const int ORDER_68000 = 1; public const int USE_DB_ON = 1; public const int INIT_DB_FATAL = 1; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index 98f9c6ae64..0362adb736 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -4187,6 +4187,14 @@ private TdsOperationStatus TryProcessLoginAck(TdsParserStateObject stateObj, out { return result; } + // When connecting to SQL Server 2000, the TDS version sent + // by the client would be a completely different value to the + // TDS version received by the server. + // From SQL Server 2000 SP1, the TDS version is identical. As + // an artifact of this historical difference, the client sends + // its TDS version to the server in a little-endian layout, and + // receives the server's TDS version in a big-endian layout. + // Reference: MS-TDS, 2.2.7.14, footnote on TDSVersion field. a.tdsVersion = BinaryPrimitives.ReadUInt32BigEndian(b); stateObj._outBytesUsed = stateObj._outputHeaderLen; @@ -9187,11 +9195,11 @@ private void WriteLoginData(SqlLogin rec, { if (encrypt == SqlConnectionEncryptOption.Strict) { - WriteInt((TdsEnums.TDS8_MAJOR << 24) | (TdsEnums.TDS8_INCREMENT << 16) | TdsEnums.TDS8_MINOR, _physicalStateObj); + WriteUnsignedInt(TdsEnums.TDS80_VERSION, _physicalStateObj); } else { - WriteInt((TdsEnums.SQL2012_MAJOR << 24) | (TdsEnums.SQL2012_INCREMENT << 16) | TdsEnums.SQL2012_MINOR, _physicalStateObj); + WriteUnsignedInt(TdsEnums.TDS7X_VERSION, _physicalStateObj); } } else From d68b90c30480a3daf4e0f00406f3b422d736bace Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 27 Dec 2025 21:24:07 +0000 Subject: [PATCH 07/23] Hook FEATUREEXT handling Move UTF8 feature detection handling to ConnectionCapabilities. --- .../Connection/ConnectionCapabilities.cs | 20 +++++++++++++++---- .../Connection/SqlConnectionInternal.cs | 18 ----------------- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 1 + 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs index 89c7a3bacd..f0fd7527f4 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs @@ -237,9 +237,24 @@ public void ProcessFeatureExtAck(byte featureId, ReadOnlySpan featureData) switch (featureId) { case TdsEnums.FEATUREEXT_UTF8SUPPORT: + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ADV | " + + $"Object ID {_objectId}, " + + $"Received feature extension acknowledgement for UTF8 support"); + + if (featureData.Length < 1) + { + SqlClientEventSource.Log.TryTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ERR | " + + $"Object ID {_objectId}, " + + $"Unknown value for UTF8 support"); + + throw SQL.ParsingError(); + } + // The server can send and receive UTF8-encoded data if bit 0 of the // feature data is set. - Utf8 = !featureData.IsEmpty && (featureData[0] & 0x01) == 0x01; + Utf8 = (featureData[0] & 0x01) == 0x01; break; case TdsEnums.FEATUREEXT_SQLDNSCACHING: @@ -287,9 +302,6 @@ public void ProcessFeatureExtAck(byte featureId, ReadOnlySpan featureData) JsonType = !featureData.IsEmpty && featureData[0] != 0x00 && featureData[0] <= TdsEnums.MAX_SUPPORTED_JSON_VERSION; break; - - default: - throw SQL.ParsingError(ParsingErrorState.UnrequestedFeatureAckReceived); } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs index 9fc173376c..8b3ff450ae 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs @@ -1620,24 +1620,6 @@ internal void OnFeatureExtAck(int featureId, byte[] data) break; } - case TdsEnums.FEATUREEXT_UTF8SUPPORT: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + - $"Object ID {ObjectID}, " + - $"Received feature extension acknowledgement for UTF8 support"); - - if (data.Length < 1) - { - SqlClientEventSource.Log.TryTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + - $"Object ID {ObjectID}, " + - $"Unknown value for UTF8 support", ObjectID); - - throw SQL.ParsingError(); - } - break; - } case TdsEnums.FEATUREEXT_SQLDNSCACHING: { SqlClientEventSource.Log.TryAdvancedTraceEvent( diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index 0362adb736..cbe3a62a82 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -3725,6 +3725,7 @@ private TdsOperationStatus TryProcessFeatureExtAck(TdsParserStateObject stateObj return result; } } + Capabilities.ProcessFeatureExtAck(featureId, data); _connHandler.OnFeatureExtAck(featureId, data); } } while (featureId != TdsEnums.FEATUREEXT_TERMINATOR); From 936218ea5c5e96cb30ae90b10a8805e841c0ddde Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 27 Dec 2025 21:42:57 +0000 Subject: [PATCH 08/23] Move JSON feature detection handling --- .../Connection/ConnectionCapabilities.cs | 27 +++++++++++++- .../Connection/SqlConnectionInternal.cs | 37 ------------------- 2 files changed, 26 insertions(+), 38 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs index f0fd7527f4..8dd02cb7f5 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs @@ -297,10 +297,35 @@ public void ProcessFeatureExtAck(byte featureId, ReadOnlySpan featureData) break; case TdsEnums.FEATUREEXT_JSONSUPPORT: + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ADV | " + + $"Object ID {_objectId}, " + + $"Received feature extension acknowledgement for JSONSUPPORT"); + + if (featureData.Length != 1) + { + SqlClientEventSource.Log.TryTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ERR | " + + $"Object ID {_objectId}, " + + $"Unknown token for JSONSUPPORT"); + + throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); + } + // Feature data is comprised of a single byte which specifies the version of the JSON // type which is available. - JsonType = !featureData.IsEmpty && featureData[0] != 0x00 + JsonType = featureData[0] != 0x00 && featureData[0] <= TdsEnums.MAX_SUPPORTED_JSON_VERSION; + + if (!JsonType) + { + SqlClientEventSource.Log.TryTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ERR | " + + $"Object ID {_objectId}, " + + $"Invalid version number for JSONSUPPORT"); + + throw SQL.ParsingError(); + } break; } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs index 8b3ff450ae..a131513466 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs @@ -199,12 +199,6 @@ internal class SqlConnectionInternal : DbConnectionInternal, IDisposable // @TODO: Should be private and accessed via internal property internal bool _federatedAuthenticationRequested; - /// - /// Flag indicating whether JSON objects are supported by the server. - /// - // @TODO: Should be private and accessed via internal property - internal bool IsJsonSupportEnabled = false; - /// /// Flag indicating whether vector objects are supported by the server. /// @@ -1660,37 +1654,6 @@ internal void OnFeatureExtAck(int featureId, byte[] data) // generate pendingSQLDNSObject and turn on IsSQLDNSRetryEnabled flag break; } - case TdsEnums.FEATUREEXT_JSONSUPPORT: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + - $"Object ID {ObjectID}, " + - $"Received feature extension acknowledgement for JSONSUPPORT"); - - if (data.Length != 1) - { - SqlClientEventSource.Log.TryTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + - $"Object ID {ObjectID}, " + - $"Unknown token for JSONSUPPORT"); - - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - - byte jsonSupportVersion = data[0]; - if (jsonSupportVersion == 0 || jsonSupportVersion > TdsEnums.MAX_SUPPORTED_JSON_VERSION) - { - SqlClientEventSource.Log.TryTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + - $"Object ID {ObjectID}, " + - $"Invalid version number for JSONSUPPORT"); - - throw SQL.ParsingError(); - } - - IsJsonSupportEnabled = true; - break; - } case TdsEnums.FEATUREEXT_VECTORSUPPORT: { SqlClientEventSource.Log.TryAdvancedTraceEvent( From 53f89471ce1ca6448d58b08dafe7d4f7a2771933 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 27 Dec 2025 21:47:27 +0000 Subject: [PATCH 09/23] Move float32 vector feature detection handling --- .../Connection/ConnectionCapabilities.cs | 28 ++++++++++++- .../Connection/SqlConnectionInternal.cs | 39 ------------------- 2 files changed, 27 insertions(+), 40 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs index 8dd02cb7f5..1539e8e14e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs @@ -290,10 +290,36 @@ public void ProcessFeatureExtAck(byte featureId, ReadOnlySpan featureData) break; case TdsEnums.FEATUREEXT_VECTORSUPPORT: + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ADV | " + + $"Object ID {_objectId}, " + + $"Received feature extension acknowledgement for VECTORSUPPORT"); + + if (featureData.Length != 1) + { + SqlClientEventSource.Log.TryTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ERR | " + + $"Object ID {_objectId}, " + + $"Unknown token for VECTORSUPPORT"); + + throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); + } + // Feature data is comprised of a single byte which specifies the version of the vector // type which is available. - Float32VectorType = !featureData.IsEmpty && featureData[0] != 0x00 + Float32VectorType = featureData[0] != 0x00 && featureData[0] <= TdsEnums.MAX_SUPPORTED_VECTOR_VERSION; + + if (!Float32VectorType) + { + SqlClientEventSource.Log.TryTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ERR | " + + $"Object ID {_objectId}, " + + $"Invalid version number {featureData[0]} for VECTORSUPPORT, " + + $"Max supported version is {TdsEnums.MAX_SUPPORTED_VECTOR_VERSION}"); + + throw SQL.ParsingError(); + } break; case TdsEnums.FEATUREEXT_JSONSUPPORT: diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs index a131513466..7b4e31ea01 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs @@ -199,12 +199,6 @@ internal class SqlConnectionInternal : DbConnectionInternal, IDisposable // @TODO: Should be private and accessed via internal property internal bool _federatedAuthenticationRequested; - /// - /// Flag indicating whether vector objects are supported by the server. - /// - // @TODO: Should be private and accessed via internal property - internal bool IsVectorSupportEnabled = false; - // @TODO: This should be private internal readonly SyncAsyncLock _parserLock = new SyncAsyncLock(); @@ -1654,39 +1648,6 @@ internal void OnFeatureExtAck(int featureId, byte[] data) // generate pendingSQLDNSObject and turn on IsSQLDNSRetryEnabled flag break; } - case TdsEnums.FEATUREEXT_VECTORSUPPORT: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + - $"Object ID {ObjectID}, " + - $"Received feature extension acknowledgement for VECTORSUPPORT"); - - if (data.Length != 1) - { - SqlClientEventSource.Log.TryTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + - $"Object ID {ObjectID}, " + - $"Unknown token for VECTORSUPPORT"); - - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - - byte vectorSupportVersion = data[0]; - if (vectorSupportVersion == 0 || vectorSupportVersion > TdsEnums.MAX_SUPPORTED_VECTOR_VERSION) - { - SqlClientEventSource.Log.TryTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + - $"Object ID {ObjectID}, " + - $"Invalid version number {vectorSupportVersion} for VECTORSUPPORT, " + - $"Max supported version is {TdsEnums.MAX_SUPPORTED_VECTOR_VERSION}"); - - throw SQL.ParsingError(); - } - - IsVectorSupportEnabled = true; - - break; - } case TdsEnums.FEATUREEXT_USERAGENT: { // Unexpected ack from server but we ignore it entirely From 8ee0fb47aa125d9414157e4c903c4147b98b41fc Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 27 Dec 2025 21:51:39 +0000 Subject: [PATCH 10/23] Move Azure SQL feature detection handling --- .../Connection/ConnectionCapabilities.cs | 20 ++++++++++- .../Connection/SqlConnectionInternal.cs | 34 +------------------ 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs index 1539e8e14e..6d9aa6d4fa 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs @@ -283,10 +283,28 @@ public void ProcessFeatureExtAck(byte featureId, ReadOnlySpan featureData) break; case TdsEnums.FEATUREEXT_AZURESQLSUPPORT: + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ADV | " + + $"Object ID {_objectId}, " + + $"Received feature extension acknowledgement for AzureSQLSupport"); + + if (featureData.Length < 1) + { + throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); + } + IsAzureSql = true; // Clients can connect to the failover partner with a read-only AppIntent if bit 0 // of the only byte in the feature data is set. - ReadOnlyFailoverPartnerConnection = !featureData.IsEmpty && (featureData[0] & 0x01) == 0x01; + ReadOnlyFailoverPartnerConnection = (featureData[0] & 0x01) == 0x01; + + if (ReadOnlyFailoverPartnerConnection && SqlClientEventSource.Log.IsTraceEnabled()) + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ADV | " + + $"Object ID {_objectId}, " + + $"FailoverPartner enabled with Readonly intent for AzureSQL DB"); + } break; case TdsEnums.FEATUREEXT_VECTORSUPPORT: diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs index 7b4e31ea01..c5a1911117 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs @@ -798,12 +798,6 @@ private SqlInternalTransaction AvailableInternalTransaction get => _parser._fResetConnection ? null : CurrentTransaction; } - /// - /// Whether this connection is to an Azure SQL Database. - /// - // @TODO: Make private field. - private bool IsAzureSqlConnection { get; set; } - #endregion #region Public and Internal Methods @@ -1533,32 +1527,6 @@ internal void OnFeatureExtAck(int featureId, byte[] data) } break; } - case TdsEnums.FEATUREEXT_AZURESQLSUPPORT: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + - $"Object ID {ObjectID}, " + - $"Received feature extension acknowledgement for AzureSQLSupport"); - - if (data.Length < 1) - { - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - - IsAzureSqlConnection = true; - - // Bit 0 for RO/FP support - // @TODO: Add a constant somewhere for that - if ((data[0] & 1) == 1 && SqlClientEventSource.Log.IsTraceEnabled()) - { - SqlClientEventSource.Log.TryAdvancedTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + - $"Object ID {ObjectID}, " + - $"FailoverPartner enabled with Readonly intent for AzureSQL DB"); - } - - break; - } case TdsEnums.FEATUREEXT_DATACLASSIFICATION: { SqlClientEventSource.Log.TryAdvancedTraceEvent( @@ -3773,7 +3741,7 @@ private void OpenLoginEnlist( timeout); } - if (!IsAzureSqlConnection) + if (!_parser.Capabilities.IsAzureSql) { // If not a connection to Azure SQL, Readonly with FailoverPartner is not supported if (ConnectionOptions.ApplicationIntent == ApplicationIntent.ReadOnly) From 7713018c32e02e62f58cb56195d497c7a7fce384 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 27 Dec 2025 21:57:20 +0000 Subject: [PATCH 11/23] Add additional detection logic for Global Transactions --- .../Connection/ConnectionCapabilities.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs index 6d9aa6d4fa..3092ce55dc 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs @@ -148,12 +148,19 @@ internal sealed class ConnectionCapabilities /// public byte DataClassificationVersion { get; private set; } + /// + /// Indicates that Global Transactions are available (even if not currently enabled.) + /// Global Transactions are only supported by Azure SQL, and are only available if a + /// FEATUREEXTACK token of value 0x05 is received. + /// + public bool GlobalTransactionsAvailable { get; private set; } + /// /// Indicates support for Global Transactions. This is only supported by /// Azure SQL, and is only available if a FEATUREEXTACK token of value /// 0x05 is received. /// - public bool GlobalTransactions { get; private set; } + public bool GlobalTransactionsSupported { get; private set; } /// /// Indicates support for Enhanced Routing. This is only supported by @@ -205,7 +212,8 @@ public void Reset() Utf8 = false; DnsCaching = false; DataClassificationVersion = TdsEnums.DATA_CLASSIFICATION_NOT_ENABLED; - GlobalTransactions = false; + GlobalTransactionsAvailable = false; + GlobalTransactionsSupported = false; EnhancedRouting = false; ReadOnlyFailoverPartnerConnection = false; Float32VectorType = false; @@ -279,7 +287,9 @@ public void ProcessFeatureExtAck(byte featureId, ReadOnlySpan featureData) case TdsEnums.FEATUREEXT_GLOBALTRANSACTIONS: // Feature data is comprised of a single byte which indicates whether // global transactions are available. - GlobalTransactions = !featureData.IsEmpty && featureData[0] == 0x01; + GlobalTransactionsAvailable = true; + + GlobalTransactionsSupported = !featureData.IsEmpty && featureData[0] == 0x01; break; case TdsEnums.FEATUREEXT_AZURESQLSUPPORT: From 0e1fcc509ef9c11a0c10874ca428af6a7178edde Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 27 Dec 2025 22:01:35 +0000 Subject: [PATCH 12/23] Move Global Transactions feature detection handling --- .../Connection/ConnectionCapabilities.cs | 17 +++++++++- .../Connection/SqlConnectionInternal.cs | 33 ++----------------- 2 files changed, 19 insertions(+), 31 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs index 3092ce55dc..c40cdef9d3 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs @@ -285,11 +285,26 @@ public void ProcessFeatureExtAck(byte featureId, ReadOnlySpan featureData) break; case TdsEnums.FEATUREEXT_GLOBALTRANSACTIONS: + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ADV | " + + $"Object ID {_objectId}, " + + $"Received feature extension acknowledgement for GlobalTransactions"); + + if (featureData.Length < 1) + { + SqlClientEventSource.Log.TryTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ERR | " + + $"Object ID {_objectId}, " + + $"Unknown version number for GlobalTransactions"); + + throw SQL.ParsingError(); + } + // Feature data is comprised of a single byte which indicates whether // global transactions are available. GlobalTransactionsAvailable = true; - GlobalTransactionsSupported = !featureData.IsEmpty && featureData[0] == 0x01; + GlobalTransactionsSupported = featureData[0] == 0x01; break; case TdsEnums.FEATUREEXT_AZURESQLSUPPORT: diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs index c5a1911117..b267cc6131 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs @@ -623,16 +623,15 @@ internal bool IsDNSCachingBeforeRedirectSupported internal bool IsEnlistedInTransaction { get; private set; } /// - /// Whether this is a Global Transaction (Non-MSDTC, Azure SQL DB Transaction) - /// TODO: overlaps with IsGlobalTransactionsEnabledForServer, need to consolidate to avoid bugs + /// Whether the server is capable of supporting a Global Transaction (Non-MSDTC, Azure SQL DB Transaction) /// - internal bool IsGlobalTransaction { get; private set; } + internal bool IsGlobalTransaction => _parser.Capabilities.GlobalTransactionsAvailable; /// /// Whether Global Transactions are enabled. Only supported by Azure SQL. False if disabled /// or connected to on-prem SQL Server. /// - internal bool IsGlobalTransactionsEnabledForServer { get; private set; } + internal bool IsGlobalTransactionsEnabledForServer => _parser.Capabilities.GlobalTransactionsSupported; /// /// Whether this connection is locked for bulk copy operations. @@ -1361,32 +1360,6 @@ internal void OnFeatureExtAck(int featureId, byte[] data) break; } - case TdsEnums.FEATUREEXT_GLOBALTRANSACTIONS: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + - $"Object ID {ObjectID}, " + - $"Received feature extension acknowledgement for GlobalTransactions"); - - if (data.Length < 1) - { - SqlClientEventSource.Log.TryTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + - $"Object ID {ObjectID}, " + - $"Unknown version number for GlobalTransactions"); - - throw SQL.ParsingError(); - } - - IsGlobalTransaction = true; - if (data[0] == 0x01) - { - IsGlobalTransactionsEnabledForServer = true; - } - - break; - } - case TdsEnums.FEATUREEXT_FEDAUTH: { SqlClientEventSource.Log.TryAdvancedTraceEvent( From ade233619f82483b6b51c57f6d3e62d9e422a459 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 27 Dec 2025 22:18:10 +0000 Subject: [PATCH 13/23] Move data classification feature detection handling --- .../Connection/ConnectionCapabilities.cs | 40 +++++++++++++--- .../Connection/SqlConnectionInternal.cs | 48 ------------------- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 2 +- 3 files changed, 34 insertions(+), 56 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs index c40cdef9d3..216d44345e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs @@ -272,16 +272,42 @@ public void ProcessFeatureExtAck(byte featureId, ReadOnlySpan featureData) break; case TdsEnums.FEATUREEXT_DATACLASSIFICATION: + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ADV | " + + $"Object ID {_objectId}, " + + $"Received feature extension acknowledgement for DATACLASSIFICATION"); + + if (featureData.Length != 2) + { + SqlClientEventSource.Log.TryTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ERR | " + + $"Object ID {_objectId}, " + + $"Unknown token for DATACLASSIFICATION"); + + throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); + } + + byte dcVersion = featureData[0]; + + if (dcVersion == 0x00 || + dcVersion > TdsEnums.DATA_CLASSIFICATION_VERSION_MAX_SUPPORTED) + { + SqlClientEventSource.Log.TryTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ERR | " + + $"Object ID {_objectId}, " + + $"Invalid version number for DATACLASSIFICATION"); + + throw SQL.ParsingErrorValue( + ParsingErrorState.DataClassificationInvalidVersion, + dcVersion); + } + // The feature data is comprised of a single byte containing the version, // followed by another byte indicating whether or not data classification is // enabled. - DataClassificationVersion = - featureData.Length == 2 - && featureData[1] == 0x00 - && featureData[0] > 0x00 - && featureData[0] <= TdsEnums.DATA_CLASSIFICATION_VERSION_MAX_SUPPORTED - ? featureData[0] - : TdsEnums.DATA_CLASSIFICATION_NOT_ENABLED; + DataClassificationVersion = featureData[1] == 0x00 + ? TdsEnums.DATA_CLASSIFICATION_NOT_ENABLED + : dcVersion; break; case TdsEnums.FEATUREEXT_GLOBALTRANSACTIONS: diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs index b267cc6131..f242d4d912 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs @@ -1500,54 +1500,6 @@ internal void OnFeatureExtAck(int featureId, byte[] data) } break; } - case TdsEnums.FEATUREEXT_DATACLASSIFICATION: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + - $"Object ID {ObjectID}, " + - $"Received feature extension acknowledgement for DATACLASSIFICATION"); - - if (data.Length < 1) - { - SqlClientEventSource.Log.TryTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + - $"Object ID {ObjectID}, " + - $"Unknown token for DATACLASSIFICATION"); - - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - - byte supportedDataClassificationVersion = data[0]; - if (supportedDataClassificationVersion == 0 || - supportedDataClassificationVersion > TdsEnums.DATA_CLASSIFICATION_VERSION_MAX_SUPPORTED) - { - SqlClientEventSource.Log.TryTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + - $"Object ID {ObjectID}, " + - $"Invalid version number for DATACLASSIFICATION"); - - throw SQL.ParsingErrorValue( - ParsingErrorState.DataClassificationInvalidVersion, - supportedDataClassificationVersion); - } - - if (data.Length != 2) - { - SqlClientEventSource.Log.TryTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + - $"Object ID {ObjectID}, " + - $"Unknown token for DATACLASSIFICATION"); - - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - - byte enabled = data[1]; - _parser.DataClassificationVersion = enabled == 0 - ? TdsEnums.DATA_CLASSIFICATION_NOT_ENABLED - : supportedDataClassificationVersion; - - break; - } case TdsEnums.FEATUREEXT_SQLDNSCACHING: { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index cbe3a62a82..f27fdb9fca 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -181,7 +181,7 @@ internal sealed partial class TdsParser /// /// Get or set data classification version. A value of 0 means that sensitivity classification is not enabled. /// - internal int DataClassificationVersion { get; set; } + internal int DataClassificationVersion => Capabilities.DataClassificationVersion; private SqlCollation _cachedCollation; From ab9ebff9df8383abc491e86a491da2cbe6f5e5da Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 28 Dec 2025 00:45:35 +0000 Subject: [PATCH 14/23] Move initial SQL DNS caching feature detection handling --- .../Connection/ConnectionCapabilities.cs | 17 ++++++++++++- .../Connection/SqlConnectionInternal.cs | 25 ++----------------- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 3 --- 3 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs index 216d44345e..2833275f21 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs @@ -266,9 +266,24 @@ public void ProcessFeatureExtAck(byte featureId, ReadOnlySpan featureData) break; case TdsEnums.FEATUREEXT_SQLDNSCACHING: + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ADV | " + + $"Object ID {_objectId}, " + + $"Received feature extension acknowledgement for SQLDNSCACHING"); + + if (featureData.Length < 1) + { + SqlClientEventSource.Log.TryTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ERR | " + + $"Object ID {_objectId}, " + + $"Unknown token for SQLDNSCACHING"); + + throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); + } + // The client may cache DNS resolution responses if bit 0 of the feature // data is set. - DnsCaching = !featureData.IsEmpty && (featureData[0] & 0x01) == 0x01; + DnsCaching = (featureData[0] & 0x01) == 0x01; break; case TdsEnums.FEATUREEXT_DATACLASSIFICATION: diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs index f242d4d912..11fd70ac5c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs @@ -645,11 +645,7 @@ internal bool IsLockedForBulkCopy /// Get or set if SQLDNSCaching is supported by the server. /// // @TODO: Make auto-property - internal bool IsSQLDNSCachingSupported - { - get => _serverSupportsDNSCaching; - set => _serverSupportsDNSCaching = value; - } + internal bool IsSQLDNSCachingSupported => _parser.Capabilities.DnsCaching; /// /// Get or set if we need retrying with IP received from FeatureExtAck. @@ -1503,24 +1499,8 @@ internal void OnFeatureExtAck(int featureId, byte[] data) case TdsEnums.FEATUREEXT_SQLDNSCACHING: { - SqlClientEventSource.Log.TryAdvancedTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + - $"Object ID {ObjectID}, " + - $"Received feature extension acknowledgement for SQLDNSCACHING"); - - if (data.Length < 1) - { - SqlClientEventSource.Log.TryTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + - $"Object ID {ObjectID}, " + - $"Unknown token for SQLDNSCACHING"); - - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - - if (data[0] == 1) + if (IsSQLDNSCachingSupported) { - IsSQLDNSCachingSupported = true; _cleanSQLDNSCaching = false; if (RoutingInfo != null) @@ -1531,7 +1511,6 @@ internal void OnFeatureExtAck(int featureId, byte[] data) else { // we receive the IsSupported whose value is 0 - IsSQLDNSCachingSupported = false; _cleanSQLDNSCaching = true; } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index f27fdb9fca..78495d482c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -376,9 +376,6 @@ bool withFailover _connHandler = connHandler; _loginWithFailover = withFailover; - // Clean up IsSQLDNSCachingSupported flag from previous status - _connHandler.IsSQLDNSCachingSupported = false; - uint sniStatus = TdsParserStateObjectFactory.Singleton.SNIStatus; if (sniStatus != TdsEnums.SNI_SUCCESS) From abd69b757980af6011b2e05cef9369f68f10395f Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 28 Dec 2025 00:55:07 +0000 Subject: [PATCH 15/23] Remove unnecessary _cleanSQLDNSCaching member This was always equal to !Capability.DnsCaching. --- .../Connection/SqlConnectionInternal.cs | 18 ++---------------- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 2 +- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs index 11fd70ac5c..b545bb4214 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs @@ -165,10 +165,6 @@ internal class SqlConnectionInternal : DbConnectionInternal, IDisposable // @TODO: Probably a good idea to introduce a delegate type internal readonly Func> _accessTokenCallback; - // @TODO: Should be private and accessed via internal property - // @TODO: Rename to match naming conventions - internal bool _cleanSQLDNSCaching = false; - internal Guid _clientConnectionId = Guid.Empty; /// @@ -1499,19 +1495,9 @@ internal void OnFeatureExtAck(int featureId, byte[] data) case TdsEnums.FEATUREEXT_SQLDNSCACHING: { - if (IsSQLDNSCachingSupported) - { - _cleanSQLDNSCaching = false; - - if (RoutingInfo != null) - { - IsDNSCachingBeforeRedirectSupported = true; - } - } - else + if (IsSQLDNSCachingSupported && RoutingInfo != null) { - // we receive the IsSupported whose value is 0 - _cleanSQLDNSCaching = true; + IsDNSCachingBeforeRedirectSupported = true; } // TODO: need to add more steps for phase 2 diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index 78495d482c..01b0576dd7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -3729,7 +3729,7 @@ private TdsOperationStatus TryProcessFeatureExtAck(TdsParserStateObject stateObj // Write to DNS Cache or clean up DNS Cache for TCP protocol bool ret = false; - if (_connHandler._cleanSQLDNSCaching) + if (!Capabilities.DnsCaching) { ret = SQLFallbackDNSCache.Instance.DeleteDNSInfo(FQDNforDNSCache); } From 50ccf4762537d7d97a82e8bb48f26ce192a0aec1 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 28 Dec 2025 15:57:59 +0000 Subject: [PATCH 16/23] Move column encryption feature detection handling --- .../Connection/ConnectionCapabilities.cs | 67 +++++++++++++++++++ .../Connection/SqlConnectionInternal.cs | 50 -------------- .../src/Microsoft/Data/SqlClient/TdsEnums.cs | 2 + .../src/Microsoft/Data/SqlClient/TdsParser.cs | 11 +-- 4 files changed, 75 insertions(+), 55 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs index 2833275f21..a0abdf5905 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Text; #nullable enable @@ -193,6 +194,26 @@ internal sealed class ConnectionCapabilities /// public bool JsonType { get; private set; } + /// + /// Indicates support for column encryption and specifies the version of column + /// encryption which is supported. This was introduced in SQL Server 2016, and is + /// only available if a FEATUREEXTACK token of value 0x04 is received. + /// + /// + /// This should only be 1, 2 or 3. v1 is supported from SQL + /// Server 2016 upwards, v2 is supported from SQL Server 2019 upwards, v3 is supported + /// from SQL Server 2022 upwards. + /// + public byte ColumnEncryptionVersion { get; private set; } + + /// + /// If column encryption is enabled, the type of enclave reported by the server. This + /// was introduced in SQL Server 2019, and is only available if a FEATUREEXTACK token + /// of value 0x04 is received, and the resultant + /// is 2 or 3. + /// + public string? ColumnEncryptionEnclaveType { get; private set; } + public ConnectionCapabilities(int parentObjectId) { _objectId = parentObjectId; @@ -218,6 +239,8 @@ public void Reset() ReadOnlyFailoverPartnerConnection = false; Float32VectorType = false; JsonType = false; + ColumnEncryptionVersion = TdsEnums.TCE_NOT_ENABLED; + ColumnEncryptionEnclaveType = null; } /// @@ -437,6 +460,50 @@ public void ProcessFeatureExtAck(byte featureId, ReadOnlySpan featureData) throw SQL.ParsingError(); } break; + + case TdsEnums.FEATUREEXT_TCE: + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ERR | " + + $"Object ID {_objectId}, " + + $"Received feature extension acknowledgement for TCE"); + + if (featureData.Length < 1) + { + SqlClientEventSource.Log.TryTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ERR | " + + $"Object ID {_objectId}, " + + $"Unknown version number for TCE"); + + throw SQL.ParsingError(ParsingErrorState.TceUnknownVersion); + } + + // Feature data begins with one byte containing the column encryption version. If + // this version is 2 or 3, the version is followed by a B_NVARCHAR (i.e., a one-byte + // string length followed by a Unicode-encoded string containing the enclave type.) + // NB 1: the MS-TDS specification requires that a client must throw an exception if + // the column encryption version is 2 and no enclave type is specified. We do not do + // this. + // NB 2: although the length is specified, we assume that everything from position 2 + // of the packet and forwards is a Unicode-encoded string. + ColumnEncryptionVersion = featureData[0]; + + if (featureData.Length > 1) + { + ReadOnlySpan enclaveTypeSpan = featureData.Slice(2); +#if NET + ColumnEncryptionEnclaveType = Encoding.Unicode.GetString(enclaveTypeSpan); +#else + unsafe + { + fixed (byte* fDataPtr = enclaveTypeSpan) + { + ColumnEncryptionEnclaveType = Encoding.Unicode.GetString(fDataPtr, enclaveTypeSpan.Length); + } + } +#endif + } + + break; } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs index b545bb4214..ae5c8ea8cc 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs @@ -204,12 +204,6 @@ internal class SqlConnectionInternal : DbConnectionInternal, IDisposable // @TODO: Should be private and accessed via internal property internal readonly SspiContextProvider _sspiContextProvider; - /// - /// TCE flags supported by the server. - /// - // @TODO: Should be private and accessed via internal property - internal byte _tceVersionSupported; - private readonly ActiveDirectoryAuthenticationTimeoutRetryHelper _activeDirectoryAuthTimeoutRetryHelper; /// @@ -1448,50 +1442,6 @@ internal void OnFeatureExtAck(int featureId, byte[] data) break; } - case TdsEnums.FEATUREEXT_TCE: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + - $"Object ID {ObjectID}, " + - $"Received feature extension acknowledgement for TCE"); - - if (data.Length < 1) - { - SqlClientEventSource.Log.TryTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + - $"Object ID {ObjectID}, " + - $"Unknown version number for TCE"); - - throw SQL.ParsingError(ParsingErrorState.TceUnknownVersion); - } - - byte supportedTceVersion = data[0]; - if (supportedTceVersion == 0 || supportedTceVersion > TdsEnums.MAX_SUPPORTED_TCE_VERSION) - { - SqlClientEventSource.Log.TryTraceEvent( - $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + - $"Object ID {ObjectID}, " + - $"Invalid version number for TCE"); - - throw SQL.ParsingErrorValue(ParsingErrorState.TceInvalidVersion, supportedTceVersion); - } - - _tceVersionSupported = supportedTceVersion; - - Debug.Assert(_tceVersionSupported <= TdsEnums.MAX_SUPPORTED_TCE_VERSION, - "Client support TCE version 2"); - - _parser.IsColumnEncryptionSupported = true; - _parser.TceVersionSupported = _tceVersionSupported; - _parser.AreEnclaveRetriesSupported = _tceVersionSupported == 3; - - if (data.Length > 1) - { - // Extract the type of enclave being used by the server. - _parser.EnclaveType = Encoding.Unicode.GetString(data, 2, data.Length - 2); - } - break; - } case TdsEnums.FEATUREEXT_SQLDNSCACHING: { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs index 4571e802b6..5128b7900a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -940,8 +940,10 @@ internal enum FedAuthInfoId : byte internal const byte SUPPORTED_USER_AGENT_VERSION = 0x01; // TCE Related constants + internal const byte TCE_NOT_ENABLED = 0x00; internal const byte MAX_SUPPORTED_TCE_VERSION = 0x03; // max version internal const byte MIN_TCE_VERSION_WITH_ENCLAVE_SUPPORT = 0x02; // min version with enclave support + internal const byte MIN_TCE_VERSION_WITH_ENCLAVE_RETRY_SUPPORT = 0x03; internal const ushort MAX_TCE_CIPHERINFO_SIZE = 2048; // max size of cipherinfo blob internal const long MAX_TCE_CIPHERTEXT_SIZE = 2147483648; // max size of encrypted blob- currently 2GB. internal const byte CustomCipherAlgorithmId = 0; // Id used for custom encryption algorithm. diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index 01b0576dd7..b934f29eff 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -152,22 +152,24 @@ internal sealed partial class TdsParser /// /// Get or set if column encryption is supported by the server. /// - internal bool IsColumnEncryptionSupported { get; set; } = false; + internal bool IsColumnEncryptionSupported => + TceVersionSupported != TdsEnums.TCE_NOT_ENABLED; /// /// TCE version supported by the server /// - internal byte TceVersionSupported { get; set; } + internal byte TceVersionSupported => Capabilities.ColumnEncryptionVersion; /// /// Server supports retrying when the enclave CEKs sent by the client do not match what is needed for the query to run. /// - internal bool AreEnclaveRetriesSupported { get; set; } + internal bool AreEnclaveRetriesSupported => + TceVersionSupported >= TdsEnums.MIN_TCE_VERSION_WITH_ENCLAVE_RETRY_SUPPORT; /// /// Type of enclave being used by the server /// - internal string EnclaveType { get; set; } + internal string EnclaveType => Capabilities.ColumnEncryptionEnclaveType; internal bool isTcpProtocol { get; set; } internal string FQDNforDNSCache { get; set; } @@ -203,7 +205,6 @@ internal TdsParser(bool MARS, bool fAsynchronous) Capabilities = new(ObjectID); _physicalStateObj = TdsParserStateObjectFactory.Singleton.CreateTdsParserStateObject(this); - DataClassificationVersion = TdsEnums.DATA_CLASSIFICATION_NOT_ENABLED; } internal SqlConnectionInternal Connection From 24caa2a31a991c2cbd0bbe2cfe5e01b79629aab1 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 28 Dec 2025 17:59:09 +0000 Subject: [PATCH 17/23] Move logic to throw on unknown FEATUREEXT tokens into TdsParser --- .../SqlClient/Connection/SqlConnectionInternal.cs | 8 -------- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs index ae5c8ea8cc..4eb62de128 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs @@ -278,9 +278,6 @@ internal class SqlConnectionInternal : DbConnectionInternal, IDisposable // @TODO: Rename to match naming conventions private bool _SQLDNSRetryEnabled = false; - // @TODO: Rename to match naming conventions - private bool _serverSupportsDNSCaching = false; - private bool _sessionRecoveryRequested; private int _threadIdOwningParserLock = -1; @@ -1466,11 +1463,6 @@ internal void OnFeatureExtAck(int featureId, byte[] data) break; } - default: - { - // Unknown feature ack - throw SQL.ParsingError(); - } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index b934f29eff..b1242e23ea 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -3708,6 +3708,12 @@ private TdsOperationStatus TryProcessFeatureExtAck(TdsParserStateObject stateObj } if (featureId != TdsEnums.FEATUREEXT_TERMINATOR) { + // Unknown feature ack + if (!IsFeatureExtSupported(featureId)) + { + throw SQL.ParsingError(); + } + uint dataLen; result = stateObj.TryReadUInt32(out dataLen); if (result != TdsOperationStatus.Done) @@ -3723,6 +3729,7 @@ private TdsOperationStatus TryProcessFeatureExtAck(TdsParserStateObject stateObj return result; } } + Capabilities.ProcessFeatureExtAck(featureId, data); _connHandler.OnFeatureExtAck(featureId, data); } @@ -3791,6 +3798,13 @@ private TdsOperationStatus TryProcessFeatureExtAck(TdsParserStateObject stateObj } return TdsOperationStatus.Done; + + static bool IsFeatureExtSupported(byte fId) => + fId is TdsEnums.FEATUREEXT_SRECOVERY or TdsEnums.FEATUREEXT_FEDAUTH or TdsEnums.FEATUREEXT_TCE + or TdsEnums.FEATUREEXT_GLOBALTRANSACTIONS or TdsEnums.FEATUREEXT_AZURESQLSUPPORT + or TdsEnums.FEATUREEXT_DATACLASSIFICATION or TdsEnums.FEATUREEXT_UTF8SUPPORT + or TdsEnums.FEATUREEXT_SQLDNSCACHING or TdsEnums.FEATUREEXT_JSONSUPPORT + or TdsEnums.FEATUREEXT_VECTORSUPPORT or TdsEnums.FEATUREEXT_USERAGENT; } private bool IsValidAttestationProtocol(SqlConnectionAttestationProtocol attestationProtocol, string enclaveType) From 984ac2ea1f60b515decd49daf772bfd101ab8dfd Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 28 Dec 2025 18:09:28 +0000 Subject: [PATCH 18/23] Refactor parsing condition into ShouldProcessFeatureExtAck 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. --- .../SqlClient/Connection/SqlConnectionInternal.cs | 15 +++------------ .../src/Microsoft/Data/SqlClient/TdsParser.cs | 9 ++++++--- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs index 4eb62de128..50f2b2eb33 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs @@ -1273,16 +1273,12 @@ internal void OnError(SqlException exception, bool breakConnection, Action + RoutingInfo is null || featureId is TdsEnums.FEATUREEXT_SQLDNSCACHING; + // @TODO: This class should not do low-level parsing of data from the server. internal void OnFeatureExtAck(int featureId, byte[] data) { - if (RoutingInfo != null && featureId != TdsEnums.FEATUREEXT_SQLDNSCACHING) - { - return; - } - switch (featureId) { case TdsEnums.FEATUREEXT_SRECOVERY: @@ -1446,11 +1442,6 @@ internal void OnFeatureExtAck(int featureId, byte[] data) { IsDNSCachingBeforeRedirectSupported = true; } - - // TODO: need to add more steps for phase 2 - // get IPv4 + IPv6 + Port number - // not put them in the DNS cache at this point but need to store them somewhere - // generate pendingSQLDNSObject and turn on IsSQLDNSRetryEnabled flag break; } case TdsEnums.FEATUREEXT_USERAGENT: diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index b1242e23ea..36c82c1510 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -3730,8 +3730,11 @@ private TdsOperationStatus TryProcessFeatureExtAck(TdsParserStateObject stateObj } } - Capabilities.ProcessFeatureExtAck(featureId, data); - _connHandler.OnFeatureExtAck(featureId, data); + if (_connHandler.ShouldProcessFeatureExtAck(featureId)) + { + Capabilities.ProcessFeatureExtAck(featureId, data); + _connHandler.OnFeatureExtAck(featureId, data); + } } } while (featureId != TdsEnums.FEATUREEXT_TERMINATOR); @@ -3742,7 +3745,7 @@ private TdsOperationStatus TryProcessFeatureExtAck(TdsParserStateObject stateObj ret = SQLFallbackDNSCache.Instance.DeleteDNSInfo(FQDNforDNSCache); } - if (_connHandler.IsSQLDNSCachingSupported && _connHandler.pendingSQLDNSObject != null + if (Capabilities.DnsCaching && _connHandler.pendingSQLDNSObject != null && !SQLFallbackDNSCache.Instance.IsDuplicate(_connHandler.pendingSQLDNSObject)) { ret = SQLFallbackDNSCache.Instance.AddDNSInfo(_connHandler.pendingSQLDNSObject); From 9380711f7e6ccc75f7b0452e5c113a57d07d3cc9 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 28 Dec 2025 18:34:08 +0000 Subject: [PATCH 19/23] Maintain original server version behaviour --- .../SqlClient/Connection/ConnectionCapabilities.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs index a0abdf5905..f4ba9aa23f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs @@ -15,10 +15,16 @@ namespace Microsoft.Data.SqlClient; /// internal sealed class ConnectionCapabilities { + /// + /// This TDS version is used by SQL Server 2005. + /// + private const uint SqlServer2005TdsVersion = 0x72_09_0002; + /// /// This TDS version is used by SQL Server 2008 R2. /// private const uint SqlServer2008R2TdsVersion = 0x73_0B_0003; + /// /// This TDS version is used by SQL Server 2012 and onwards. /// In SQL Server 2022 and SQL Server 2025, this is used when @@ -26,6 +32,7 @@ internal sealed class ConnectionCapabilities /// protocol. /// private const uint SqlServer2012TdsVersion = TdsEnums.TDS7X_VERSION; + /// /// This TDS version is used by SQL Server 2022 and onwards, /// when responding with the TDS 8.x protocol. @@ -250,7 +257,8 @@ public void Reset() /// The LOGINACK token stream sent by the server public void ProcessLoginAck(SqlLoginAck loginAck) { - if (loginAck.tdsVersion is not SqlServer2008R2TdsVersion + if (loginAck.tdsVersion is not SqlServer2005TdsVersion + and not SqlServer2008R2TdsVersion and not SqlServer2012TdsVersion and not SqlServer2022TdsVersion) { From 95ac4f514fdf6a80e502dca352fbbb4a3ab6d26f Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 28 Dec 2025 18:56:30 +0000 Subject: [PATCH 20/23] Performance: convert SqlLoginAck to a readonly ref struct This is never persisted, and eliminates an allocation --- .../Connection/ConnectionCapabilities.cs | 10 ++++---- .../Connection/SqlConnectionInternal.cs | 4 +-- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 25 ++++++++----------- .../Data/SqlClient/TdsParserHelperClasses.cs | 18 +++++++++---- 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs index f4ba9aa23f..f6f829c260 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs @@ -257,7 +257,7 @@ public void Reset() /// The LOGINACK token stream sent by the server public void ProcessLoginAck(SqlLoginAck loginAck) { - if (loginAck.tdsVersion is not SqlServer2005TdsVersion + if (loginAck.TdsVersion is not SqlServer2005TdsVersion and not SqlServer2008R2TdsVersion and not SqlServer2012TdsVersion and not SqlServer2022TdsVersion) @@ -265,10 +265,10 @@ and not SqlServer2012TdsVersion throw SQL.InvalidTDSVersion(); } - TdsVersion = loginAck.tdsVersion; - ServerMajorVersion = loginAck.majorVersion; - ServerMinorVersion = loginAck.minorVersion; - ServerBuildNumber = (ushort)loginAck.buildNum; + TdsVersion = loginAck.TdsVersion; + ServerMajorVersion = loginAck.MajorVersion; + ServerMinorVersion = loginAck.MinorVersion; + ServerBuildNumber = loginAck.BuildNumber; } public void ProcessFeatureExtAck(byte featureId, ReadOnlySpan featureData) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs index 50f2b2eb33..9600d3486c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs @@ -1636,7 +1636,7 @@ internal void OnLoginAck(SqlLoginAck rec) { if (_recoverySessionData != null) { - if (_recoverySessionData._tdsVersion != rec.tdsVersion) + if (_recoverySessionData._tdsVersion != rec.TdsVersion) { throw SQL.CR_TDSVersionNotPreserved(this); } @@ -1644,7 +1644,7 @@ internal void OnLoginAck(SqlLoginAck rec) if (_currentSessionData != null) { - _currentSessionData._tdsVersion = rec.tdsVersion; + _currentSessionData._tdsVersion = rec.TdsVersion; } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index 36c82c1510..fd936351b2 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -2741,8 +2741,7 @@ internal TdsOperationStatus TryRun(RunBehavior runBehavior, SqlCommand cmdHandle case TdsEnums.SQLLOGINACK: { SqlClientEventSource.Log.TryTraceEvent(" Received login acknowledgement token"); - SqlLoginAck ack; - result = TryProcessLoginAck(stateObj, out ack); + result = TryProcessLoginAck(stateObj, out SqlLoginAck ack); if (result != TdsOperationStatus.Done) { return result; @@ -4186,9 +4185,7 @@ private TdsOperationStatus TryProcessSessionState(TdsParserStateObject stateObj, private TdsOperationStatus TryProcessLoginAck(TdsParserStateObject stateObj, out SqlLoginAck sqlLoginAck) { - SqlLoginAck a = new SqlLoginAck(); - - sqlLoginAck = null; + sqlLoginAck = default; // read past interface type and version TdsOperationStatus result = stateObj.TrySkipBytes(1); @@ -4211,7 +4208,7 @@ private TdsOperationStatus TryProcessLoginAck(TdsParserStateObject stateObj, out // its TDS version to the server in a little-endian layout, and // receives the server's TDS version in a big-endian layout. // Reference: MS-TDS, 2.2.7.14, footnote on TDSVersion field. - a.tdsVersion = BinaryPrimitives.ReadUInt32BigEndian(b); + uint tdsVersion = BinaryPrimitives.ReadUInt32BigEndian(b); stateObj._outBytesUsed = stateObj._outputHeaderLen; byte len; @@ -4226,31 +4223,32 @@ private TdsOperationStatus TryProcessLoginAck(TdsParserStateObject stateObj, out { return result; } - result = stateObj.TryReadByte(out a.majorVersion); + result = stateObj.TryReadByte(out byte majorVersion); if (result != TdsOperationStatus.Done) { return result; } - result = stateObj.TryReadByte(out a.minorVersion); + result = stateObj.TryReadByte(out byte minorVersion); if (result != TdsOperationStatus.Done) { return result; } - byte buildNumHi, buildNumLo; - result = stateObj.TryReadByte(out buildNumHi); + result = stateObj.TryReadByte(out byte buildNumHi); if (result != TdsOperationStatus.Done) { return result; } - result = stateObj.TryReadByte(out buildNumLo); + result = stateObj.TryReadByte(out byte buildNumLo); if (result != TdsOperationStatus.Done) { return result; } - a.buildNum = (short)((buildNumHi << 8) + buildNumLo); + ushort buildNumber = (ushort)((buildNumHi << 8) | buildNumLo); + + sqlLoginAck = new SqlLoginAck(majorVersion, minorVersion, buildNumber, tdsVersion); - Capabilities.ProcessLoginAck(a); + Capabilities.ProcessLoginAck(sqlLoginAck); Debug.Assert(_state == TdsParserState.OpenNotLoggedIn, "ProcessLoginAck called with state not TdsParserState.OpenNotLoggedIn"); _state = TdsParserState.OpenLoggedIn; @@ -4270,7 +4268,6 @@ private TdsOperationStatus TryProcessLoginAck(TdsParserStateObject stateObj, out ThrowExceptionAndWarning(stateObj); } - sqlLoginAck = a; return TdsOperationStatus.Done; } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs index 8f0bb915c0..6f2059e73b 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs @@ -120,12 +120,20 @@ internal sealed class SqlLogin internal SecureString newSecurePassword; } - internal sealed class SqlLoginAck + internal readonly ref struct SqlLoginAck { - internal byte majorVersion; - internal byte minorVersion; - internal short buildNum; - internal uint tdsVersion; + public readonly byte MajorVersion; + public readonly byte MinorVersion; + public readonly ushort BuildNumber; + public readonly uint TdsVersion; + + public SqlLoginAck(byte majorVersion, byte minorVersion, ushort buildNumber, uint tdsVersion) + { + MajorVersion = majorVersion; + MinorVersion = minorVersion; + BuildNumber = buildNumber; + TdsVersion = tdsVersion; + } } internal sealed class SqlFedAuthInfo From 229a04de860d9b76e49cc3aeda4553326bd2fd80 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 28 Dec 2025 20:06:15 +0000 Subject: [PATCH 21/23] Enable Release mode build --- .../Data/SqlClient/ManagedSni/SniPhysicalHandle.netcore.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniPhysicalHandle.netcore.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniPhysicalHandle.netcore.cs index 7e7b35655f..1c9d8a2d4a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniPhysicalHandle.netcore.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniPhysicalHandle.netcore.cs @@ -16,9 +16,7 @@ internal abstract class SniPhysicalHandle : SniHandle { protected const int DefaultPoolSize = 4; -#if DEBUG private static int s_packetId; -#endif private ObjectPool _pool; protected SniPhysicalHandle(int poolSize = DefaultPoolSize) From 25b01d4edb14d23e617b912332e2f92f81330be5 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 28 Dec 2025 20:25:49 +0000 Subject: [PATCH 22/23] Plumb new ConnectionCapabilities to SqlMetaDataFactory --- .../src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs | 5 +++-- .../src/Microsoft/Data/SqlClient/SqlMetaDataFactory.cs | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs index 79c1c4a239..b4a3cccaae 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs @@ -758,11 +758,12 @@ private static DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions(Sql private static SqlMetaDataFactory CreateMetaDataFactory(DbConnectionInternal internalConnection) { Debug.Assert(internalConnection is not null, "internalConnection may not be null."); + Debug.Assert(internalConnection as SqlConnectionInternal is not null, "innerConnection must be a SqlConnectionInternal."); Stream xmlStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Microsoft.Data.SqlClient.SqlMetaData.xml"); Debug.Assert(xmlStream is not null, $"{nameof(xmlStream)} may not be null."); - - return new SqlMetaDataFactory(xmlStream, internalConnection.ServerVersion); + + return new SqlMetaDataFactory(xmlStream, ((SqlConnectionInternal)internalConnection).Parser.Capabilities); } private Task CreateReplaceConnectionContinuation( diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlMetaDataFactory.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlMetaDataFactory.cs index ca3dec0658..b79437d943 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlMetaDataFactory.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlMetaDataFactory.cs @@ -38,12 +38,13 @@ internal sealed class SqlMetaDataFactory : IDisposable private readonly DataSet _collectionDataSet; private readonly string _serverVersion; - public SqlMetaDataFactory(Stream xmlStream, string serverVersion) + public SqlMetaDataFactory(Stream xmlStream, ConnectionCapabilities connectionCapabilities) { ADP.CheckArgumentNull(xmlStream, nameof(xmlStream)); - ADP.CheckArgumentNull(serverVersion, nameof(serverVersion)); + ADP.CheckArgumentNull(connectionCapabilities, nameof(connectionCapabilities)); + ADP.CheckArgumentNull(connectionCapabilities.ServerVersion, nameof(connectionCapabilities.ServerVersion)); - _serverVersion = serverVersion; + _serverVersion = connectionCapabilities.ServerVersion; _collectionDataSet = LoadDataSetFromXml(xmlStream); } From 6560f5f446f00ddd04268bdac126b9aa0c5d3683 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 28 Dec 2025 21:50:28 +0000 Subject: [PATCH 23/23] Cross-check FEATUREEXT validation to original OnFeatureExtAck One missing validation path. Also switched conditions to use pattern matching and to better align with original method for easier comparison and better codegen. --- .../Connection/ConnectionCapabilities.cs | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs index f6f829c260..df9da18e2e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs @@ -335,8 +335,8 @@ public void ProcessFeatureExtAck(byte featureId, ReadOnlySpan featureData) byte dcVersion = featureData[0]; - if (dcVersion == 0x00 || - dcVersion > TdsEnums.DATA_CLASSIFICATION_VERSION_MAX_SUPPORTED) + if (dcVersion is TdsEnums.DATA_CLASSIFICATION_NOT_ENABLED + or > TdsEnums.DATA_CLASSIFICATION_VERSION_MAX_SUPPORTED) { SqlClientEventSource.Log.TryTraceEvent( $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ERR | " + @@ -422,10 +422,8 @@ public void ProcessFeatureExtAck(byte featureId, ReadOnlySpan featureData) // Feature data is comprised of a single byte which specifies the version of the vector // type which is available. - Float32VectorType = featureData[0] != 0x00 - && featureData[0] <= TdsEnums.MAX_SUPPORTED_VECTOR_VERSION; - - if (!Float32VectorType) + if (featureData[0] is 0x00 + or > TdsEnums.MAX_SUPPORTED_VECTOR_VERSION) { SqlClientEventSource.Log.TryTraceEvent( $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ERR | " + @@ -435,6 +433,8 @@ public void ProcessFeatureExtAck(byte featureId, ReadOnlySpan featureData) throw SQL.ParsingError(); } + + Float32VectorType = true; break; case TdsEnums.FEATUREEXT_JSONSUPPORT: @@ -455,10 +455,8 @@ public void ProcessFeatureExtAck(byte featureId, ReadOnlySpan featureData) // Feature data is comprised of a single byte which specifies the version of the JSON // type which is available. - JsonType = featureData[0] != 0x00 - && featureData[0] <= TdsEnums.MAX_SUPPORTED_JSON_VERSION; - - if (!JsonType) + if (featureData[0] is 0x00 + or > TdsEnums.MAX_SUPPORTED_JSON_VERSION) { SqlClientEventSource.Log.TryTraceEvent( $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ERR | " + @@ -467,6 +465,8 @@ public void ProcessFeatureExtAck(byte featureId, ReadOnlySpan featureData) throw SQL.ParsingError(); } + + JsonType = true; break; case TdsEnums.FEATUREEXT_TCE: @@ -485,6 +485,17 @@ public void ProcessFeatureExtAck(byte featureId, ReadOnlySpan featureData) throw SQL.ParsingError(ParsingErrorState.TceUnknownVersion); } + if (featureData[0] is TdsEnums.TCE_NOT_ENABLED + or > TdsEnums.MAX_SUPPORTED_TCE_VERSION) + { + SqlClientEventSource.Log.TryTraceEvent( + $"{nameof(ConnectionCapabilities)}.{nameof(ProcessFeatureExtAck)} | ERR | " + + $"Object ID {_objectId}, " + + $"Invalid version number for TCE"); + + throw SQL.ParsingErrorValue(ParsingErrorState.TceInvalidVersion, featureData[0]); + } + // Feature data begins with one byte containing the column encryption version. If // this version is 2 or 3, the version is followed by a B_NVARCHAR (i.e., a one-byte // string length followed by a Unicode-encoded string containing the enclave type.)