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