diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs
index 0145978d72..b66c72774e 100644
--- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs
@@ -95,6 +95,7 @@ public static class DataTestUtility
// SQL Server capabilities
private static bool? s_isDataClassificationSupported;
private static bool? s_isVectorSupported;
+ private static bool? s_isVectorFloat16Supported;
// Azure Synapse EngineEditionId == 6
// More could be read at https://learn.microsoft.com/en-us/sql/t-sql/functions/serverproperty-transact-sql?view=sql-server-ver16#propertyname
@@ -156,6 +157,68 @@ public static string SQLServerVersion
s_isVectorSupported ??= IsTCPConnStringSetup() &&
IsTypePresent("vector");
+ ///
+ /// Determines whether the SQL Server supports the 'float16' base type for the 'vector' data type.
+ ///
+ ///
+ /// Probes the server by first executing
+ /// ALTER DATABASE SCOPED CONFIGURATION SET PREVIEW_FEATURES = ON; and then executing
+ /// DECLARE @v AS VECTOR(5, float16) = '[1.0, 1.0, 1.0, 1.0, 1.0]'; SELECT @v; .
+ /// As a side effect, this enables preview features for the current database. If the server does not
+ /// recognize float16 as a vector base type, if the server does not support the required syntax,
+ /// if the current principal lacks permission to change the database scoped configuration, or if another
+ /// server-side error occurs while running the probe, this method returns
+ /// . Implies .
+ ///
+ /// if the 'float16' vector base type is supported; otherwise, .
+ public static bool IsSqlVectorFloat16Supported =>
+ s_isVectorFloat16Supported ??= IsTCPConnStringSetup() &&
+ IsSqlVectorSupported &&
+ CheckVectorFloat16Supported();
+
+ private static bool CheckVectorFloat16Supported()
+ {
+ try
+ {
+ using SqlConnection connection = new(TCPConnectionString);
+ // Enable preview features (required while float16 is in preview), then declare a
+ // float16 vector and select it back. Without a negotiated float16 feature extension,
+ // the server returns the value as a varchar(max) JSON string, which the client can
+ // safely read. We use 1.0 (exactly representable in IEEE-754 binary16) so the
+ // round-tripped JSON matches the input bit-for-bit. Any failure (server parse
+ // error such as Msg 195 "'float16' is not a recognized vector base type.", lack of
+ // permission to set PREVIEW_FEATURES, etc.) means we cannot exercise the float16
+ // path.
+ float[] expected = { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f };
+ using SqlCommand command = new(
+ "ALTER DATABASE SCOPED CONFIGURATION SET PREVIEW_FEATURES = ON;" +
+ "DECLARE @v AS VECTOR(5, float16) = '[1.0, 1.0, 1.0, 1.0, 1.0]'; SELECT @v;",
+ connection);
+ connection.Open();
+ using SqlDataReader reader = command.ExecuteReader();
+ if (!reader.Read())
+ {
+ return false;
+ }
+ string json = reader.GetString(0);
+ float[] actual = System.Text.Json.JsonSerializer.Deserialize(json);
+ return actual != null && actual.Length == expected.Length
+ && actual.AsSpan().SequenceEqual(expected);
+ }
+ catch (SqlException)
+ {
+ // Server-side failure (e.g. Msg 195 "'float16' is not a recognized vector base
+ // type.", Msg 102 syntax errors on older servers, lack of permission to set
+ // PREVIEW_FEATURES) — float16 path is unavailable.
+ return false;
+ }
+ catch (System.Text.Json.JsonException)
+ {
+ // Server returned a payload we can't parse as a float[] JSON array.
+ return false;
+ }
+ }
+
static DataTestUtility()
{
Config c = Config.Load();
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj
index 1c1553e6c1..cf7bbf236a 100644
--- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj
@@ -243,8 +243,10 @@
+
+
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/Float16VectorTypeBackwardCompatibilityTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/Float16VectorTypeBackwardCompatibilityTests.cs
new file mode 100644
index 0000000000..2ea6a10ab8
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/Float16VectorTypeBackwardCompatibilityTests.cs
@@ -0,0 +1,106 @@
+// 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.Collections.Generic;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.Data.SqlClient.ManualTesting.Tests.SQL.VectorTest
+{
+ ///
+ /// Provides parameterized test data for backward compatibility tests that exchange
+ /// float16 vector data as varchar(max) JSON strings.
+ ///
+ public static class Float16VarcharVectorTestData
+ {
+ // Values chosen to be exactly representable in IEEE-754 binary16 (float16),
+ // so JSON round-trips through a vector(N, float16) column without precision loss.
+ public static readonly float[] TestData = { 1.0f, 2.0f, 3.0f };
+
+ ///
+ /// Generates test cases for all 4 SqlParameter construction patterns x 2 value types (non-null + null).
+ /// Each case yields: [int pattern, string jsonOrNull, float[] expectedData]
+ /// where jsonOrNull is null when testing DBNull insertion.
+ ///
+ public static IEnumerable GetVarcharVectorInsertTestData()
+ {
+ string json = JsonSerializer.Serialize(TestData);
+
+ // Pattern 1-4 with non-null JSON value
+ yield return new object[] { 1, json, TestData };
+ yield return new object[] { 2, json, TestData };
+ yield return new object[] { 3, json, TestData };
+ yield return new object[] { 4, json, TestData };
+
+ // Pattern 1-4 with null value
+ yield return new object[] { 1, null, null };
+ yield return new object[] { 2, null, null };
+ yield return new object[] { 3, null, null };
+ yield return new object[] { 4, null, null };
+ }
+ }
+
+ public sealed class Float16VectorTypeBackwardCompatibilityTests : VectorBackwardCompatTestBase
+ {
+ public Float16VectorTypeBackwardCompatibilityTests(ITestOutputHelper output)
+ : base(output, columnDefinition: "vector(3, float16)", namePrefix: "VectorF16")
+ {
+ }
+
+ protected override float[] GetPrepareTestValues(int i) =>
+ new float[] { i + 1, i + 2, i + 3 };
+
+ #region Insert Tests
+
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorFloat16Supported))]
+ [MemberData(nameof(Float16VarcharVectorTestData.GetVarcharVectorInsertTestData), MemberType = typeof(Float16VarcharVectorTestData), DisableDiscoveryEnumeration = true)]
+ public void TestVectorDataInsertionAsVarchar(int pattern, string jsonValue, float[] expectedData)
+ => InsertAndValidateAsVarchar(pattern, jsonValue, expectedData);
+
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorFloat16Supported))]
+ [MemberData(nameof(Float16VarcharVectorTestData.GetVarcharVectorInsertTestData), MemberType = typeof(Float16VarcharVectorTestData), DisableDiscoveryEnumeration = true)]
+ public async Task TestVectorDataInsertionAsVarcharAsync(int pattern, string jsonValue, float[] expectedData)
+ => await InsertAndValidateAsVarcharAsync(pattern, jsonValue, expectedData);
+
+ #endregion
+
+ #region Stored Procedure Tests
+
+ [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorFloat16Supported))]
+ public void TestStoredProcParamsForVectorAsVarchar()
+ => StoredProcRoundTrip(Float16VarcharVectorTestData.TestData);
+
+ [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorFloat16Supported))]
+ public async Task TestStoredProcParamsForVectorAsVarcharAsync()
+ => await StoredProcRoundTripAsync(Float16VarcharVectorTestData.TestData);
+
+ #endregion
+
+ #region Bulk Copy Tests
+
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorFloat16Supported))]
+ [InlineData(1)]
+ [InlineData(2)]
+ public void TestSqlBulkCopyForVectorAsVarchar(int bulkCopySourceMode)
+ => BulkCopyRoundTrip(bulkCopySourceMode, Float16VarcharVectorTestData.TestData);
+
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorFloat16Supported))]
+ [InlineData(1)]
+ [InlineData(2)]
+ public async Task TestSqlBulkCopyForVectorAsVarcharAsync(int bulkCopySourceMode)
+ => await BulkCopyRoundTripAsync(bulkCopySourceMode, Float16VarcharVectorTestData.TestData);
+
+ #endregion
+
+ #region Prepared Statement Tests
+
+ [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorFloat16Supported))]
+ public void TestInsertVectorsAsVarcharWithPrepare()
+ => PreparedInsertRoundTrip();
+
+ #endregion
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorBackwardCompatTestBase.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorBackwardCompatTestBase.cs
new file mode 100644
index 0000000000..1b2d4c792f
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorBackwardCompatTestBase.cs
@@ -0,0 +1,464 @@
+// 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;
+using System.Data;
+using System.Data.SqlTypes;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.Data.SqlClient.ManualTesting.Tests.SQL.VectorTest
+{
+ ///
+ /// Abstract base class for vector backward compatibility tests that exchange
+ /// vector data as varchar(max) JSON strings. Eliminates duplication between
+ /// float32 and float16 test classes while allowing each to specify its own
+ /// ConditionalFact/ConditionalTheory predicates.
+ ///
+ public abstract class VectorBackwardCompatTestBase : IDisposable
+ {
+ private static readonly string s_connectionString = DataTestUtility.TCPConnectionString;
+ private static readonly string s_vectorParamName = "@VectorData";
+
+ private readonly SqlConnection _connection;
+ private readonly Table _vectorTable;
+ private readonly Table _bulkCopySrcTable;
+ private readonly StoredProcedure _storedProc;
+
+ private readonly string _selectCmdString;
+ private readonly string _insertCmdString;
+
+ protected ITestOutputHelper Output { get; }
+
+ ///
+ /// Generates the test data values to use with Prepare tests.
+ /// Float32 uses fractional values (e.g. i+0.1f), float16 uses whole numbers (e.g. i+1).
+ ///
+ protected abstract float[] GetPrepareTestValues(int i);
+
+ protected VectorBackwardCompatTestBase(
+ ITestOutputHelper output,
+ string columnDefinition,
+ string namePrefix)
+ {
+ Output = output;
+ _connection = new SqlConnection(s_connectionString);
+ _connection.Open();
+
+ _vectorTable = new Table(_connection, namePrefix + "TestTable",
+ $"(Id INT PRIMARY KEY IDENTITY, VectorData {columnDefinition} NULL)");
+
+ _bulkCopySrcTable = new Table(_connection, namePrefix + "BulkCopyTestTable",
+ "(Id INT PRIMARY KEY IDENTITY, VectorData varchar(max) NULL)");
+
+ string storedProcBody = $@"
+ @InputVectorJson VARCHAR(MAX), -- Input: Serialized float[] as JSON string
+ @OutputVectorJson VARCHAR(MAX) OUTPUT -- Output: Echoed back from latest inserted row
+ AS
+ BEGIN
+ SET NOCOUNT ON;
+
+ -- Insert into vector table
+ INSERT INTO {_vectorTable.Name} (VectorData)
+ VALUES (@InputVectorJson);
+
+ -- Retrieve latest entry (assumes auto-incrementing ID)
+ SELECT TOP 1 @OutputVectorJson = VectorData
+ FROM {_vectorTable.Name}
+ ORDER BY Id DESC;
+ END;";
+
+ _storedProc = new StoredProcedure(_connection, namePrefix + "AsVarcharSp", storedProcBody);
+
+ _selectCmdString = $"SELECT VectorData FROM {_vectorTable.Name} ORDER BY Id DESC";
+ _insertCmdString = $"INSERT INTO {_vectorTable.Name} (VectorData) VALUES (@VectorData)";
+ }
+
+ public void Dispose()
+ {
+ // RAII objects drop themselves on Dispose in reverse order.
+ _storedProc?.Dispose();
+ _bulkCopySrcTable?.Dispose();
+ _vectorTable?.Dispose();
+ _connection?.Dispose();
+ }
+
+ #region Shared Helpers
+
+ ///
+ /// Creates a SqlParameter with varchar(max) type based on the specified pattern.
+ /// Mirrors the 4 parameter construction patterns tested in NativeVectorFloat32Tests
+ /// but uses SqlDbType.VarChar instead of SqlDbTypeExtensions.Vector.
+ ///
+ private static SqlParameter CreateVarcharParameter(int pattern, string jsonValue)
+ {
+ object value = jsonValue != null ? (object)jsonValue : DBNull.Value;
+
+ return pattern switch
+ {
+ // Pattern 1: Default constructor + property setters
+ 1 => new SqlParameter
+ {
+ ParameterName = s_vectorParamName,
+ SqlDbType = SqlDbType.VarChar,
+ Size = -1, // varchar(max)
+ Value = value
+ },
+ // Pattern 2: Name + value constructor
+ 2 => new SqlParameter(s_vectorParamName, value),
+ // Pattern 3: Name + SqlDbType constructor
+ 3 => new SqlParameter(s_vectorParamName, SqlDbType.VarChar) { Value = value },
+ // Pattern 4: Name + SqlDbType + Size constructor
+ 4 => new SqlParameter(s_vectorParamName, SqlDbType.VarChar, -1) { Value = value },
+ _ => throw new ArgumentOutOfRangeException(nameof(pattern), $"Unsupported pattern: {pattern}")
+ };
+ }
+
+ private void ValidateInsertedData(SqlConnection connection, float[] expectedData)
+ {
+ using var selectCmd = new SqlCommand(_selectCmdString, connection);
+ using var reader = selectCmd.ExecuteReader();
+ Assert.True(reader.Read(), "No data found in the table.");
+
+ if (expectedData != null)
+ {
+ // Validate non-null data through all string read APIs
+ string result = reader.GetString(0);
+ Assert.Equal(expectedData, JsonSerializer.Deserialize(result));
+
+ result = reader.GetSqlString(0).Value;
+ Assert.Equal(expectedData, JsonSerializer.Deserialize(result));
+
+ result = reader.GetFieldValue(0);
+ Assert.Equal(expectedData, JsonSerializer.Deserialize(result));
+ }
+ else
+ {
+ Assert.True(reader.IsDBNull(0), "Expected null but got non-null value.");
+ Assert.Equal(DBNull.Value, reader.GetValue(0));
+ Assert.Throws(() => reader.GetString(0));
+ Assert.Throws(() => reader.GetSqlString(0).Value);
+ Assert.Throws(() => reader.GetFieldValue(0));
+ }
+ }
+
+ private async Task ValidateInsertedDataAsync(SqlConnection connection, float[] expectedData)
+ {
+ using var selectCmd = new SqlCommand(_selectCmdString, connection);
+ using var reader = await selectCmd.ExecuteReaderAsync();
+ Assert.True(await reader.ReadAsync(), "No data found in the table.");
+
+ if (expectedData != null)
+ {
+ // Validate non-null data through all string read APIs
+ string result = reader.GetString(0);
+ Assert.Equal(expectedData, JsonSerializer.Deserialize(result));
+
+ result = reader.GetSqlString(0).Value;
+ Assert.Equal(expectedData, JsonSerializer.Deserialize(result));
+
+ result = await reader.GetFieldValueAsync(0);
+ Assert.Equal(expectedData, JsonSerializer.Deserialize(result));
+ }
+ else
+ {
+ Assert.True(await reader.IsDBNullAsync(0), "Expected null but got non-null value.");
+ Assert.Equal(DBNull.Value, reader.GetValue(0));
+ Assert.Throws(() => reader.GetString(0));
+ Assert.Throws(() => reader.GetSqlString(0).Value);
+ await Assert.ThrowsAsync(async () => await reader.GetFieldValueAsync(0));
+ }
+ }
+
+ #endregion
+
+ #region Test Implementations
+
+ protected void InsertAndValidateAsVarchar(int pattern, string jsonValue, float[] expectedData)
+ {
+ using var conn = new SqlConnection(s_connectionString);
+ conn.Open();
+
+ using var insertCmd = new SqlCommand(_insertCmdString, conn);
+ SqlParameter param = CreateVarcharParameter(pattern, jsonValue);
+ insertCmd.Parameters.Add(param);
+ Assert.Equal(1, insertCmd.ExecuteNonQuery());
+ insertCmd.Parameters.Clear();
+
+ ValidateInsertedData(conn, expectedData);
+ }
+
+ protected async Task InsertAndValidateAsVarcharAsync(int pattern, string jsonValue, float[] expectedData)
+ {
+ using var conn = new SqlConnection(s_connectionString);
+ await conn.OpenAsync();
+
+ using var insertCmd = new SqlCommand(_insertCmdString, conn);
+ SqlParameter param = CreateVarcharParameter(pattern, jsonValue);
+ insertCmd.Parameters.Add(param);
+ Assert.Equal(1, await insertCmd.ExecuteNonQueryAsync());
+ insertCmd.Parameters.Clear();
+
+ await ValidateInsertedDataAsync(conn, expectedData);
+ }
+
+ protected void StoredProcRoundTrip(float[] data)
+ {
+ string dataAsJson = JsonSerializer.Serialize(data);
+
+ using var conn = new SqlConnection(s_connectionString);
+ conn.Open();
+
+ using var command = new SqlCommand(_storedProc.Name, conn)
+ {
+ CommandType = CommandType.StoredProcedure
+ };
+
+ // Test with non-null value
+ var inputParam = new SqlParameter("@InputVectorJson", SqlDbType.VarChar, -1) { Value = dataAsJson };
+ command.Parameters.Add(inputParam);
+ var outputParam = new SqlParameter("@OutputVectorJson", SqlDbType.VarChar, -1)
+ {
+ Direction = ParameterDirection.Output
+ };
+ command.Parameters.Add(outputParam);
+
+ command.ExecuteNonQuery();
+
+ var dbDataAsJson = outputParam.Value as string;
+ Assert.NotNull(dbDataAsJson);
+ Assert.Equal(data, JsonSerializer.Deserialize(dbDataAsJson));
+
+ // Test with null value
+ command.Parameters.Clear();
+ inputParam.Value = DBNull.Value;
+ command.Parameters.Add(inputParam);
+ command.Parameters.Add(outputParam);
+ command.ExecuteNonQuery();
+
+ Assert.Equal(DBNull.Value, outputParam.Value);
+ }
+
+ protected async Task StoredProcRoundTripAsync(float[] data)
+ {
+ string dataAsJson = JsonSerializer.Serialize(data);
+
+ using var conn = new SqlConnection(s_connectionString);
+ await conn.OpenAsync();
+
+ using var command = new SqlCommand(_storedProc.Name, conn)
+ {
+ CommandType = CommandType.StoredProcedure
+ };
+
+ // Test with non-null value
+ var inputParam = new SqlParameter("@InputVectorJson", SqlDbType.VarChar, -1) { Value = dataAsJson };
+ command.Parameters.Add(inputParam);
+ var outputParam = new SqlParameter("@OutputVectorJson", SqlDbType.VarChar, -1)
+ {
+ Direction = ParameterDirection.Output
+ };
+ command.Parameters.Add(outputParam);
+
+ await command.ExecuteNonQueryAsync();
+
+ var dbDataAsJson = outputParam.Value as string;
+ Assert.NotNull(dbDataAsJson);
+ Assert.Equal(data, JsonSerializer.Deserialize(dbDataAsJson));
+
+ // Test with null value
+ command.Parameters.Clear();
+ inputParam.Value = DBNull.Value;
+ command.Parameters.Add(inputParam);
+ command.Parameters.Add(outputParam);
+
+ await command.ExecuteNonQueryAsync();
+
+ Assert.Equal(DBNull.Value, outputParam.Value);
+ }
+
+ protected void BulkCopyRoundTrip(int bulkCopySourceMode, float[] testData)
+ {
+ string testDataAsJson = JsonSerializer.Serialize(testData);
+
+ using var sourceConnection = new SqlConnection(s_connectionString);
+ sourceConnection.Open();
+ using var destinationConnection = new SqlConnection(s_connectionString);
+ destinationConnection.Open();
+
+ DataTable table = null;
+ switch (bulkCopySourceMode)
+ {
+ case 1:
+ // Use SQL Server table as source with varchar(max) data
+ using (var insertCmd = new SqlCommand($"insert into {_bulkCopySrcTable.Name} values (@VectorData)", sourceConnection))
+ {
+ var varcharVectorParam = new SqlParameter(s_vectorParamName, SqlDbType.VarChar, -1) { Value = testDataAsJson };
+ insertCmd.Parameters.Add(varcharVectorParam);
+ Assert.Equal(1, insertCmd.ExecuteNonQuery());
+ insertCmd.Parameters.Clear();
+ varcharVectorParam.Value = DBNull.Value;
+ insertCmd.Parameters.Add(varcharVectorParam);
+ Assert.Equal(1, insertCmd.ExecuteNonQuery());
+ }
+ break;
+ case 2:
+ // Use DataTable as source
+ table = new DataTable(_bulkCopySrcTable.Name);
+ table.Columns.Add("Id", typeof(int));
+ table.Columns.Add("VectorData", typeof(string));
+ table.Rows.Add(1, testDataAsJson);
+ table.Rows.Add(2, DBNull.Value);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(bulkCopySourceMode), $"Unsupported bulk copy source mode: {bulkCopySourceMode}");
+ }
+
+ // Verify that the destination table is empty before bulk copy
+ using SqlCommand countCommand = new SqlCommand($"SELECT COUNT(*) FROM {_vectorTable.Name}", destinationConnection);
+ Assert.Equal(0, Convert.ToInt16(countCommand.ExecuteScalar()));
+
+ // Initialize bulk copy configuration
+ using SqlBulkCopy bulkCopy = new SqlBulkCopy(destinationConnection)
+ {
+ DestinationTableName = _vectorTable.Name,
+ };
+
+ switch (bulkCopySourceMode)
+ {
+ case 1:
+ using (SqlCommand sourceDataCommand = new SqlCommand($"SELECT Id, VectorData FROM {_bulkCopySrcTable.Name}", sourceConnection))
+ using (SqlDataReader reader = sourceDataCommand.ExecuteReader())
+ {
+ bulkCopy.WriteToServer(reader);
+ }
+ break;
+ case 2:
+ bulkCopy.WriteToServer(table);
+ break;
+ }
+
+ // Verify that 2 rows were copied
+ Assert.Equal(2, Convert.ToInt16(countCommand.ExecuteScalar()));
+
+ // Read data from destination table and verify
+ using SqlCommand verifyCommand = new SqlCommand($"SELECT VectorData from {_vectorTable.Name}", destinationConnection);
+ using SqlDataReader verifyReader = verifyCommand.ExecuteReader();
+
+ Assert.True(verifyReader.Read(), "No data found in destination table after bulk copy.");
+ Assert.False(verifyReader.IsDBNull(0), "First row in the table is null.");
+ Assert.Equal(testData, JsonSerializer.Deserialize(verifyReader.GetString(0)));
+
+ Assert.True(verifyReader.Read(), "Second row not found in the table");
+ Assert.True(verifyReader.IsDBNull(0));
+ }
+
+ protected async Task BulkCopyRoundTripAsync(int bulkCopySourceMode, float[] testData)
+ {
+ string testDataAsJson = JsonSerializer.Serialize(testData);
+
+ using var sourceConnection = new SqlConnection(s_connectionString);
+ await sourceConnection.OpenAsync();
+ using var destinationConnection = new SqlConnection(s_connectionString);
+ await destinationConnection.OpenAsync();
+
+ DataTable table = null;
+ switch (bulkCopySourceMode)
+ {
+ case 1:
+ // Use SQL Server table as source with varchar(max) data
+ using (var insertCmd = new SqlCommand($"insert into {_bulkCopySrcTable.Name} values (@VectorData)", sourceConnection))
+ {
+ var varcharVectorParam = new SqlParameter(s_vectorParamName, SqlDbType.VarChar, -1) { Value = testDataAsJson };
+ insertCmd.Parameters.Add(varcharVectorParam);
+ Assert.Equal(1, await insertCmd.ExecuteNonQueryAsync());
+ insertCmd.Parameters.Clear();
+ varcharVectorParam.Value = DBNull.Value;
+ insertCmd.Parameters.Add(varcharVectorParam);
+ Assert.Equal(1, await insertCmd.ExecuteNonQueryAsync());
+ }
+ break;
+ case 2:
+ // Use DataTable as source
+ table = new DataTable(_bulkCopySrcTable.Name);
+ table.Columns.Add("Id", typeof(int));
+ table.Columns.Add("VectorData", typeof(string));
+ table.Rows.Add(1, testDataAsJson);
+ table.Rows.Add(2, DBNull.Value);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(bulkCopySourceMode), $"Unsupported bulk copy source mode: {bulkCopySourceMode}");
+ }
+
+ // Verify that the destination table is empty before bulk copy
+ using SqlCommand countCommand = new SqlCommand($"SELECT COUNT(*) FROM {_vectorTable.Name}", destinationConnection);
+ Assert.Equal(0, Convert.ToInt16(await countCommand.ExecuteScalarAsync()));
+
+ // Initialize bulk copy configuration
+ using SqlBulkCopy bulkCopy = new SqlBulkCopy(destinationConnection)
+ {
+ DestinationTableName = _vectorTable.Name,
+ };
+
+ switch (bulkCopySourceMode)
+ {
+ case 1:
+ using (SqlCommand sourceDataCommand = new SqlCommand($"SELECT Id, VectorData FROM {_bulkCopySrcTable.Name}", sourceConnection))
+ using (SqlDataReader reader = await sourceDataCommand.ExecuteReaderAsync())
+ {
+ await bulkCopy.WriteToServerAsync(reader);
+ }
+ break;
+ case 2:
+ await bulkCopy.WriteToServerAsync(table);
+ break;
+ }
+
+ // Verify that 2 rows were copied
+ Assert.Equal(2, Convert.ToInt16(await countCommand.ExecuteScalarAsync()));
+
+ // Read data from destination table and verify
+ using SqlCommand verifyCommand = new SqlCommand($"SELECT VectorData from {_vectorTable.Name}", destinationConnection);
+ using SqlDataReader verifyReader = await verifyCommand.ExecuteReaderAsync();
+
+ Assert.True(await verifyReader.ReadAsync(), "No data found in destination table after bulk copy.");
+ Assert.False(verifyReader.IsDBNull(0), "First row in the table is null.");
+ Assert.Equal(testData, JsonSerializer.Deserialize(verifyReader.GetString(0)));
+
+ Assert.True(await verifyReader.ReadAsync(), "Second row not found in the table");
+ Assert.True(await verifyReader.IsDBNullAsync(0));
+ }
+
+ protected void PreparedInsertRoundTrip()
+ {
+ using SqlConnection conn = new SqlConnection(s_connectionString);
+ conn.Open();
+ using SqlCommand command = new SqlCommand(_insertCmdString, conn);
+ SqlParameter vectorParam = new SqlParameter("@VectorData", SqlDbType.VarChar, -1);
+ command.Parameters.Add(vectorParam);
+ command.Prepare();
+ for (int i = 0; i < 10; i++)
+ {
+ vectorParam.Value = JsonSerializer.Serialize(GetPrepareTestValues(i));
+ command.ExecuteNonQuery();
+ }
+ using SqlCommand validateCommand = new SqlCommand($"SELECT VectorData FROM {_vectorTable.Name}", conn);
+ using SqlDataReader reader = validateCommand.ExecuteReader();
+ int rowcnt = 0;
+ while (reader.Read())
+ {
+ float[] expectedData = GetPrepareTestValues(rowcnt);
+ float[] dbData = JsonSerializer.Deserialize(reader.GetString(0))!;
+ Assert.Equal(expectedData, dbData);
+ rowcnt++;
+ }
+ Assert.Equal(10, rowcnt);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorTypeBackwardCompatibilityTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorTypeBackwardCompatibilityTests.cs
index b3f42a5617..9db91d767f 100644
--- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorTypeBackwardCompatibilityTests.cs
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorTypeBackwardCompatibilityTests.cs
@@ -2,10 +2,7 @@
// 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;
using System.Collections.Generic;
-using System.Data;
-using System.Data.SqlTypes;
using System.Text.Json;
using System.Threading.Tasks;
using Xunit;
@@ -13,604 +10,95 @@
namespace Microsoft.Data.SqlClient.ManualTesting.Tests.SQL.VectorTest
{
- public sealed class VectorTypeBackwardCompatibilityTests : IDisposable
+ ///
+ /// Provides parameterized test data for backward compatibility tests that exchange
+ /// vector data as varchar(max) JSON strings.
+ ///
+ public static class VarcharVectorTestData
{
- private readonly ITestOutputHelper _output;
- private static readonly string s_connectionString = ManualTesting.Tests.DataTestUtility.TCPConnectionString;
- private static readonly string s_tableName = DataTestUtility.GetShortName("VectorTestTable");
- private static readonly string s_bulkCopySrcTableName = DataTestUtility.GetShortName("VectorBulkCopyTestTable");
- private static readonly string s_bulkCopySrcTableDef = $@"(Id INT PRIMARY KEY IDENTITY, VectorData varchar(max) NULL)";
- private static readonly string s_tableDefinition = $@"(Id INT PRIMARY KEY IDENTITY, VectorData vector(3) NULL)";
- private static readonly string s_selectCmdString = $"SELECT VectorData FROM {s_tableName} ORDER BY Id DESC";
- private static readonly string s_insertCmdString = $"INSERT INTO {s_tableName} (VectorData) VALUES (@VectorData)";
- private static readonly string s_vectorParamName = $"@VectorData";
- private static readonly string s_storedProcName = DataTestUtility.GetShortName("VectorsAsVarcharSp");
- private static readonly string s_storedProcBody = $@"
- @InputVectorJson VARCHAR(MAX), -- Input: Serialized float[] as JSON string
- @OutputVectorJson VARCHAR(MAX) OUTPUT -- Output: Echoed back from latest inserted row
- AS
- BEGIN
- SET NOCOUNT ON;
-
- -- Insert into vector table
- INSERT INTO {s_tableName} (VectorData)
- VALUES (@InputVectorJson);
-
- -- Retrieve latest entry (assumes auto-incrementing ID)
- SELECT TOP 1 @OutputVectorJson = VectorData
- FROM {s_tableName}
- ORDER BY Id DESC;
- END;";
-
- public VectorTypeBackwardCompatibilityTests(ITestOutputHelper output)
- {
- _output = output;
- using var connection = new SqlConnection(s_connectionString);
- connection.Open();
- DataTestUtility.CreateTable(connection, s_tableName, s_tableDefinition);
- DataTestUtility.CreateTable(connection, s_bulkCopySrcTableName, s_bulkCopySrcTableDef);
- DataTestUtility.CreateSP(connection, s_storedProcName, s_storedProcBody);
- }
-
- public void Dispose()
- {
- using var connection = new SqlConnection(s_connectionString);
- connection.Open();
- DataTestUtility.DropTable(connection, s_tableName);
- DataTestUtility.DropTable(connection, s_bulkCopySrcTableName);
- DataTestUtility.DropStoredProcedure(connection, s_storedProcName);
- }
-
- private void ValidateInsertedData(SqlConnection connection, float[] expectedData)
- {
- using var selectCmd = new SqlCommand(s_selectCmdString, connection);
- using var reader = selectCmd.ExecuteReader();
- Assert.True(reader.Read(), "No data found in the table.");
-
- if (!reader.IsDBNull(0))
- {
- string jsonFromDb = reader.GetString(0);
- float[] deserialized = JsonSerializer.Deserialize(jsonFromDb)!;
- Assert.Equal(expectedData, deserialized);
- }
- else
- {
- Assert.Null(expectedData);
- var val = reader.GetValue(0);
- Assert.Equal(DBNull.Value, val);
- }
- }
-
- [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorSupported))]
- public void TestVectorDataInsertionAsVarchar()
+ public static readonly float[] TestData = { 1.1f, 2.2f, 3.3f };
+
+ ///
+ /// Generates test cases for all 4 SqlParameter construction patterns x 2 value types (non-null + null).
+ /// Each case yields: [int pattern, string jsonOrNull, float[] expectedData]
+ /// where jsonOrNull is null when testing DBNull insertion.
+ ///
+ public static IEnumerable GetVarcharVectorInsertTestData()
{
- float[] data = { 1.1f, 2.2f, 3.3f };
- string json = JsonSerializer.Serialize(data);
-
- using var conn = new SqlConnection(s_connectionString);
- conn.Open();
-
- using var insertCmd = new SqlCommand(s_insertCmdString, conn);
-
- // Pattern 1: Default constructor + property setters
- var p1 = new SqlParameter();
- p1.ParameterName = s_vectorParamName;
- p1.SqlDbType = SqlDbType.VarChar;
- p1.Size = -1; //varchar(max)
- p1.Value = json;
- insertCmd.Parameters.Add(p1);
- Assert.Equal(1, insertCmd.ExecuteNonQuery());
- insertCmd.Parameters.Clear();
- ValidateInsertedData(conn, data);
-
- var nullp1 = new SqlParameter();
- nullp1.ParameterName = s_vectorParamName;
- nullp1.SqlDbType = SqlDbType.VarChar;
- nullp1.Size = -1; //varchar(max)
- nullp1.Value = DBNull.Value;
- insertCmd.Parameters.Add(nullp1);
- Assert.Equal(1, insertCmd.ExecuteNonQuery());
- insertCmd.Parameters.Clear();
- ValidateInsertedData(conn, null);
-
- // Pattern 2: Name + value constructor
- var p2 = new SqlParameter(s_vectorParamName, json);
- insertCmd.Parameters.Add(p2);
- Assert.Equal(1, insertCmd.ExecuteNonQuery());
- insertCmd.Parameters.Clear();
- ValidateInsertedData(conn, data);
-
- var nullp2 = new SqlParameter(s_vectorParamName, DBNull.Value);
- insertCmd.Parameters.Add(nullp2);
- Assert.Equal(1, insertCmd.ExecuteNonQuery());
- insertCmd.Parameters.Clear();
- ValidateInsertedData(conn, null);
-
- // Pattern 3: Name + SqlDbType constructor
- var p3 = new SqlParameter(s_vectorParamName, SqlDbType.VarChar) { Value = json };
- insertCmd.Parameters.Add(p3);
- Assert.Equal(1, insertCmd.ExecuteNonQuery());
- insertCmd.Parameters.Clear();
- ValidateInsertedData(conn, data);
-
- var nullp3 = new SqlParameter(s_vectorParamName, SqlDbType.VarChar) { Value = DBNull.Value };
- insertCmd.Parameters.Add(nullp3);
- Assert.Equal(1, insertCmd.ExecuteNonQuery());
- insertCmd.Parameters.Clear();
- ValidateInsertedData(conn, null);
-
- // Pattern 4: Name + SqlDbType + Size constructor
- var p4 = new SqlParameter(s_vectorParamName, SqlDbType.VarChar, -1) { Value = json };
- insertCmd.Parameters.Add(p4);
- Assert.Equal(1, insertCmd.ExecuteNonQuery());
- insertCmd.Parameters.Clear();
- ValidateInsertedData(conn, data);
-
- var nullp4 = new SqlParameter(s_vectorParamName, SqlDbType.VarChar, -1) { Value = DBNull.Value };
- insertCmd.Parameters.Add(nullp4);
- Assert.Equal(1, insertCmd.ExecuteNonQuery());
- insertCmd.Parameters.Clear();
- ValidateInsertedData(conn, null);
- }
-
- private async Task ValidateInsertedDataAsync(SqlConnection connection, float[] expectedData)
- {
- using var selectCmd = new SqlCommand(s_selectCmdString, connection);
- using var reader = await selectCmd.ExecuteReaderAsync();
- Assert.True(await reader.ReadAsync(), "No data found in the table.");
-
- if (!await reader.IsDBNullAsync(0))
- {
- string jsonFromDb = reader.GetString(0);
- float[] deserialized = JsonSerializer.Deserialize(jsonFromDb)!;
- Assert.Equal(expectedData, deserialized);
- }
- else
- {
- Assert.Null(expectedData);
- var val = reader.GetValue(0);
- Assert.Equal(DBNull.Value, val);
- }
+ string json = JsonSerializer.Serialize(TestData);
+
+ // Pattern 1-4 with non-null JSON value
+ yield return new object[] { 1, json, TestData };
+ yield return new object[] { 2, json, TestData };
+ yield return new object[] { 3, json, TestData };
+ yield return new object[] { 4, json, TestData };
+
+ // Pattern 1-4 with null value
+ yield return new object[] { 1, null, null };
+ yield return new object[] { 2, null, null };
+ yield return new object[] { 3, null, null };
+ yield return new object[] { 4, null, null };
}
+ }
- [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorSupported))]
- public async Task TestVectorParameterInitializationAsync()
+ public sealed class VectorTypeBackwardCompatibilityTests : VectorBackwardCompatTestBase
+ {
+ public VectorTypeBackwardCompatibilityTests(ITestOutputHelper output)
+ : base(output, columnDefinition: "vector(3)", namePrefix: "Vector")
{
- float[] data = { 1.1f, 2.2f, 3.3f };
- string json = JsonSerializer.Serialize(data);
-
- using var conn = new SqlConnection(s_connectionString);
- await conn.OpenAsync();
-
- using var insertCmd = new SqlCommand(s_insertCmdString, conn);
-
- // Pattern 1: Default constructor + property setters
- var p1 = new SqlParameter();
- p1.ParameterName = s_vectorParamName;
- p1.SqlDbType = SqlDbType.VarChar;
- p1.Size = -1;
- p1.Value = json;
- insertCmd.Parameters.Add(p1);
- Assert.Equal(1, await insertCmd.ExecuteNonQueryAsync());
- insertCmd.Parameters.Clear();
- await ValidateInsertedDataAsync(conn, data);
-
- var nullp1 = new SqlParameter();
- nullp1.ParameterName = s_vectorParamName;
- nullp1.SqlDbType = SqlDbType.VarChar;
- nullp1.Size = -1;
- nullp1.Value = DBNull.Value;
- insertCmd.Parameters.Add(nullp1);
- Assert.Equal(1, await insertCmd.ExecuteNonQueryAsync());
- insertCmd.Parameters.Clear();
- await ValidateInsertedDataAsync(conn, null);
-
- // Pattern 2: Name + value constructor
- var p2 = new SqlParameter(s_vectorParamName, json);
- insertCmd.Parameters.Add(p2);
- Assert.Equal(1, await insertCmd.ExecuteNonQueryAsync());
- insertCmd.Parameters.Clear();
- await ValidateInsertedDataAsync(conn, data);
-
- var nullp2 = new SqlParameter(s_vectorParamName, DBNull.Value);
- insertCmd.Parameters.Add(nullp2);
- Assert.Equal(1, await insertCmd.ExecuteNonQueryAsync());
- insertCmd.Parameters.Clear();
- await ValidateInsertedDataAsync(conn, null);
-
- // Pattern 3: Name + SqlDbType constructor
- var p3 = new SqlParameter(s_vectorParamName, SqlDbType.VarChar) { Value = json };
- insertCmd.Parameters.Add(p3);
- Assert.Equal(1, await insertCmd.ExecuteNonQueryAsync());
- insertCmd.Parameters.Clear();
- await ValidateInsertedDataAsync(conn, data);
-
- var nullp3 = new SqlParameter(s_vectorParamName, SqlDbType.VarChar) { Value = DBNull.Value };
- insertCmd.Parameters.Add(nullp3);
- Assert.Equal(1, await insertCmd.ExecuteNonQueryAsync());
- insertCmd.Parameters.Clear();
- await ValidateInsertedDataAsync(conn, null);
-
- // Pattern 4: Name + SqlDbType + Size constructor
- var p4 = new SqlParameter(s_vectorParamName, SqlDbType.VarChar, -1) { Value = json };
- insertCmd.Parameters.Add(p4);
- Assert.Equal(1, await insertCmd.ExecuteNonQueryAsync());
- insertCmd.Parameters.Clear();
- await ValidateInsertedDataAsync(conn, data);
-
- var nullp4 = new SqlParameter(s_vectorParamName, SqlDbType.VarChar, -1) { Value = DBNull.Value };
- insertCmd.Parameters.Add(nullp4);
- Assert.Equal(1, await insertCmd.ExecuteNonQueryAsync());
- insertCmd.Parameters.Clear();
- await ValidateInsertedDataAsync(conn, null);
}
- [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorSupported))]
- public void TestVectorDataReadsAsVarchar()
- {
- float[] data = { 1.1f, 2.2f, 3.3f };
- string dataAsJson = JsonSerializer.Serialize(data);
-
- using var conn = new SqlConnection(s_connectionString);
- conn.Open();
-
- //Insert non-null values and validate APIs for reading vector data as varchar(max)
- using var insertCmd = new SqlCommand(s_insertCmdString, conn);
- var p1 = new SqlParameter(s_vectorParamName, SqlDbType.VarChar, -1) { Value = dataAsJson };
- insertCmd.Parameters.Add(p1);
- Assert.Equal(1, insertCmd.ExecuteNonQuery());
-
- //Validate Reader
- using SqlCommand verifyCommand = new SqlCommand(s_selectCmdString, conn);
- var reader = verifyCommand.ExecuteReader();
- Assert.True(reader.Read(), "No data found in the table.");
-
- //Read using GetString
- string result = reader.GetString(0);
- float[] dbData = JsonSerializer.Deserialize(result)!;
- Assert.Equal(data, dbData);
-
- //Read using GetSqlString
- result = reader.GetSqlString(0).Value;
- dbData = JsonSerializer.Deserialize(result)!;
- Assert.Equal(data, dbData);
+ protected override float[] GetPrepareTestValues(int i) =>
+ new float[] { i + 0.1f, i + 0.2f, i + 0.3f };
- //Read using GetFieldValue
- result = reader.GetFieldValue(0);
- dbData = JsonSerializer.Deserialize(result)!;
- Assert.Equal(data, dbData);
+ #region Insert Tests
- reader.Close();
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorSupported))]
+ [MemberData(nameof(VarcharVectorTestData.GetVarcharVectorInsertTestData), MemberType = typeof(VarcharVectorTestData), DisableDiscoveryEnumeration = true)]
+ public void TestVectorDataInsertionAsVarchar(int pattern, string jsonValue, float[] expectedData)
+ => InsertAndValidateAsVarchar(pattern, jsonValue, expectedData);
- // Validate For Null Value
- insertCmd.Parameters.Clear();
- p1.Value = DBNull.Value;
- insertCmd.Parameters.Add(p1);
- Assert.Equal(1, insertCmd.ExecuteNonQuery());
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorSupported))]
+ [MemberData(nameof(VarcharVectorTestData.GetVarcharVectorInsertTestData), MemberType = typeof(VarcharVectorTestData), DisableDiscoveryEnumeration = true)]
+ public async Task TestVectorDataInsertionAsVarcharAsync(int pattern, string jsonValue, float[] expectedData)
+ => await InsertAndValidateAsVarcharAsync(pattern, jsonValue, expectedData);
- //Validate Reader for null value
- reader = verifyCommand.ExecuteReader();
- Assert.True(reader.Read(), "No data found in the table.");
+ #endregion
- //Read using GetString
- Assert.Throws(() => reader.GetString(0));
-
- //Read using GetSqlString
- Assert.Throws(() => reader.GetString(0));
-
- //Read using GetFieldValue
- Assert.Throws(() => reader.GetFieldValue(0));
- }
-
- [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorSupported))]
- public async Task TestVectorDataReadsAsVarcharAsync()
- {
- float[] data = { 1.1f, 2.2f, 3.3f };
- string dataAsJson = JsonSerializer.Serialize(data);
-
- using var conn = new SqlConnection(s_connectionString);
- await conn.OpenAsync();
-
- //Insert non-null values and validate APIs for reading vector data as varchar(max)
- using var insertCmd = new SqlCommand(s_insertCmdString, conn);
- var p1 = new SqlParameter(s_vectorParamName, SqlDbType.VarChar, -1) { Value = dataAsJson };
- insertCmd.Parameters.Add(p1);
- Assert.Equal(1, await insertCmd.ExecuteNonQueryAsync());
-
- //Validate Reader
- using SqlCommand verifyCommand = new SqlCommand(s_selectCmdString, conn);
- using var reader = await verifyCommand.ExecuteReaderAsync();
- Assert.True(await reader.ReadAsync(), "No data found in the table.");
-
- //Read using GetString
- string result = reader.GetString(0);
- float[] dbData = JsonSerializer.Deserialize(result)!;
- Assert.Equal(data, dbData);
-
- //Read using GetSqlString
- result = reader.GetSqlString(0).Value;
- dbData = JsonSerializer.Deserialize(result)!;
- Assert.Equal(data, dbData);
-
- //Read using GetFieldValue
- result = await reader.GetFieldValueAsync(0);
- dbData = JsonSerializer.Deserialize(result)!;
- Assert.Equal(data, dbData);
-
- reader.Close();
-
- // Validate For Null Value
- insertCmd.Parameters.Clear();
- p1.Value = DBNull.Value;
- insertCmd.Parameters.Add(p1);
- Assert.Equal(1, await insertCmd.ExecuteNonQueryAsync());
-
- //Validate Reader for null value
- var reader2 = await verifyCommand.ExecuteReaderAsync();
- Assert.True(await reader2.ReadAsync(), "No data found in the table.");
-
- //Read using GetString
- Assert.Throws(() => reader2.GetString(0));
-
- //Read using GetSqlString
- Assert.Throws(() => reader2.GetString(0));
-
- //Read using GetFieldValueAsync
- await Assert.ThrowsAsync(async () => await reader2.GetFieldValueAsync(0));
- }
+ #region Stored Procedure Tests
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorSupported))]
public void TestStoredProcParamsForVectorAsVarchar()
- {
- // Test data
- float[] data = { 7.1f, 8.2f, 9.3f };
- string dataAsJson = JsonSerializer.Serialize(data);
-
- //Create SP for test
- using var conn = new SqlConnection(s_connectionString);
- conn.Open();
- DataTestUtility.CreateSP(conn, s_storedProcName, s_storedProcBody);
- using var command = new SqlCommand(s_storedProcName, conn)
- {
- CommandType = CommandType.StoredProcedure
- };
-
- // Set input and output parameters
- var inputParam = new SqlParameter("@InputVectorJson", SqlDbType.VarChar, -1);
- inputParam.Value = dataAsJson;
- command.Parameters.Add(inputParam);
- var outputParam = new SqlParameter("@OutputVectorJson", SqlDbType.VarChar, -1)
- {
- Direction = ParameterDirection.Output
- };
- command.Parameters.Add(outputParam);
-
- // Execute the stored procedure
- command.ExecuteNonQuery();
-
- // Validate the output parameter
- var dbDataAsJson = outputParam.Value as string;
- float[] dbData = JsonSerializer.Deserialize(dbDataAsJson)!;
- Assert.NotNull(dbDataAsJson);
- Assert.Equal(data, dbData);
-
- // Test with null value
- command.Parameters.Clear();
- inputParam.Value = DBNull.Value;
- command.Parameters.Add(inputParam);
- command.Parameters.Add(outputParam);
- command.ExecuteNonQuery();
-
- // Validate output paramter for null value
- Assert.True(outputParam.Value == DBNull.Value);
- }
+ => StoredProcRoundTrip(VarcharVectorTestData.TestData);
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorSupported))]
public async Task TestStoredProcParamsForVectorAsVarcharAsync()
- {
- // Test data
- float[] data = { 7.1f, 8.2f, 9.3f };
- string dataAsJson = JsonSerializer.Serialize(data);
-
- // Create SP for test
- using var conn = new SqlConnection(s_connectionString);
- await conn.OpenAsync();
- DataTestUtility.CreateSP(conn, s_storedProcName, s_storedProcBody);
-
- using var command = new SqlCommand(s_storedProcName, conn)
- {
- CommandType = CommandType.StoredProcedure
- };
-
- // Set input and output parameters
- var inputParam = new SqlParameter("@InputVectorJson", SqlDbType.VarChar, -1)
- {
- Value = dataAsJson
- };
- command.Parameters.Add(inputParam);
-
- var outputParam = new SqlParameter("@OutputVectorJson", SqlDbType.VarChar, -1)
- {
- Direction = ParameterDirection.Output
- };
- command.Parameters.Add(outputParam);
-
- // Execute the stored procedure
- await command.ExecuteNonQueryAsync();
-
- // Validate the output parameter
- var dbDataAsJson = outputParam.Value as string;
- float[] dbData = JsonSerializer.Deserialize(dbDataAsJson)!;
- Assert.NotNull(dbDataAsJson);
- Assert.Equal(data, dbData);
-
- // Test with null value
- command.Parameters.Clear();
- inputParam.Value = DBNull.Value;
- command.Parameters.Add(inputParam);
- command.Parameters.Add(outputParam);
+ => await StoredProcRoundTripAsync(VarcharVectorTestData.TestData);
- await command.ExecuteNonQueryAsync();
+ #endregion
- // Validate output parameter for null value
- Assert.True(outputParam.Value == DBNull.Value);
- }
-
- [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorSupported))]
- public void TestSqlBulkCopyForVectorAsVarchar()
- {
- //Setup source with test data and create destination table for bulkcopy.
- SqlConnection sourceConnection = new SqlConnection(s_connectionString);
- sourceConnection.Open();
- SqlConnection destinationConnection = new SqlConnection(s_connectionString);
- destinationConnection.Open();
- float[] testData = { 1.1f, 2.2f, 3.3f };
- string testDataAsJson = JsonSerializer.Serialize(testData);
- using var insertCmd = new SqlCommand($"insert into {s_bulkCopySrcTableName} values (@VectorData)", sourceConnection);
- var varcharVectorParam = new SqlParameter(s_vectorParamName, SqlDbType.VarChar, -1) { Value = testDataAsJson };
- insertCmd.Parameters.Add(varcharVectorParam);
- Assert.Equal(1, insertCmd.ExecuteNonQuery());
- insertCmd.Parameters.Clear();
- varcharVectorParam.Value = DBNull.Value;
- insertCmd.Parameters.Add(varcharVectorParam);
- Assert.Equal(1, insertCmd.ExecuteNonQuery());
-
- //Bulkcopy from sql server table to destination table
- using SqlCommand sourceDataCommand = new SqlCommand($"SELECT Id, VectorData FROM {s_bulkCopySrcTableName}", sourceConnection);
- using SqlDataReader reader = sourceDataCommand.ExecuteReader();
-
- // Verify that the destination table is empty before bulk copy
- using SqlCommand countCommand = new SqlCommand($"SELECT COUNT(*) FROM {s_tableName}", destinationConnection);
- Assert.Equal(0, Convert.ToInt16(countCommand.ExecuteScalar()));
-
- // Initialize bulk copy configuration
- using SqlBulkCopy bulkCopy = new SqlBulkCopy(destinationConnection)
- {
- DestinationTableName = s_tableName,
- };
-
- try
- {
- // Perform bulk copy from source to destination table
- bulkCopy.WriteToServer(reader);
- }
- catch (Exception ex)
- {
- // If bulk copy fails, fail the test with the exception message
- Assert.Fail($"Bulk copy failed: {ex.Message}");
- }
-
- // Verify that the 2 rows from the source table have been copied into the destination table.
- Assert.Equal(2, Convert.ToInt16(countCommand.ExecuteScalar()));
-
- // Read the data from destination table as varbinary to verify the UTF-8 byte sequence
- using SqlCommand verifyCommand = new SqlCommand($"SELECT VectorData from {s_tableName}", destinationConnection);
- using SqlDataReader verifyReader = verifyCommand.ExecuteReader();
-
- // Verify that we have data in the destination table
- Assert.True(verifyReader.Read(), "No data found in destination table after bulk copy.");
-
- // Validate first non-null value.
- Assert.True(!verifyReader.IsDBNull(0), "First row in the table is null.");
- Assert.Equal(testData, JsonSerializer.Deserialize(verifyReader.GetString(0)));
-
- // Verify that we have another row
- Assert.True(verifyReader.Read(), "Second row not found in the table");
-
- // Verify that we have encountered null.
- Assert.True(verifyReader.IsDBNull(0));
- }
-
- [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorSupported))]
- public async Task TestSqlBulkCopyForVectorAsVarcharAsync()
- {
- //Setup source with test data and create destination table for bulkcopy.
- SqlConnection sourceConnection = new SqlConnection(s_connectionString);
- await sourceConnection.OpenAsync();
- SqlConnection destinationConnection = new SqlConnection(s_connectionString);
- await destinationConnection.OpenAsync();
- float[] testData = { 1.1f, 2.2f, 3.3f };
- string testDataAsJson = JsonSerializer.Serialize(testData);
- using var insertCmd = new SqlCommand($"insert into {s_bulkCopySrcTableName} values (@VectorData)", sourceConnection);
- var varcharVectorParam = new SqlParameter(s_vectorParamName, SqlDbType.VarChar, -1) { Value = testDataAsJson };
- insertCmd.Parameters.Add(varcharVectorParam);
- Assert.Equal(1, await insertCmd.ExecuteNonQueryAsync());
- insertCmd.Parameters.Clear();
- varcharVectorParam.Value = DBNull.Value;
- insertCmd.Parameters.Add(varcharVectorParam);
- Assert.Equal(1, await insertCmd.ExecuteNonQueryAsync());
-
- //Bulkcopy from sql server table to destination table
- using SqlCommand sourceDataCommand = new SqlCommand($"SELECT Id, VectorData FROM {s_bulkCopySrcTableName}", sourceConnection);
- using SqlDataReader reader = await sourceDataCommand.ExecuteReaderAsync();
-
- // Verify that the destination table is empty before bulk copy
- using SqlCommand countCommand = new SqlCommand($"SELECT COUNT(*) FROM {s_tableName}", destinationConnection);
- Assert.Equal(0, Convert.ToInt16(await countCommand.ExecuteScalarAsync()));
-
- // Initialize bulk copy configuration
- using SqlBulkCopy bulkCopy = new SqlBulkCopy(destinationConnection)
- {
- DestinationTableName = s_tableName,
- };
+ #region Bulk Copy Tests
- try
- {
- // Perform bulk copy from source to destination table
- await bulkCopy.WriteToServerAsync(reader);
- }
- catch (Exception ex)
- {
- // If bulk copy fails, fail the test with the exception message
- Assert.Fail($"Bulk copy failed: {ex.Message}");
- }
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorSupported))]
+ [InlineData(1)]
+ [InlineData(2)]
+ public void TestSqlBulkCopyForVectorAsVarchar(int bulkCopySourceMode)
+ => BulkCopyRoundTrip(bulkCopySourceMode, VarcharVectorTestData.TestData);
- // Verify that the 2 rows from the source table have been copied into the destination table.
- Assert.Equal(2, Convert.ToInt16(await countCommand.ExecuteScalarAsync()));
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorSupported))]
+ [InlineData(1)]
+ [InlineData(2)]
+ public async Task TestSqlBulkCopyForVectorAsVarcharAsync(int bulkCopySourceMode)
+ => await BulkCopyRoundTripAsync(bulkCopySourceMode, VarcharVectorTestData.TestData);
- // Read the data from destination table as varbinary to verify the UTF-8 byte sequence
- using SqlCommand verifyCommand = new SqlCommand($"SELECT VectorData from {s_tableName}", destinationConnection);
- using SqlDataReader verifyReader = await verifyCommand.ExecuteReaderAsync();
+ #endregion
- // Verify that we have data in the destination table
- Assert.True(await verifyReader.ReadAsync(), "No data found in destination table after bulk copy.");
-
- // Validate first non-null value.
- Assert.True(!verifyReader.IsDBNull(0), "First row in the table is null.");
- Assert.Equal(testData, JsonSerializer.Deserialize(verifyReader.GetString(0)));
-
- // Verify that we have another row
- Assert.True(await verifyReader.ReadAsync(), "Second row not found in the table");
-
- // Verify that we have encountered null.
- Assert.True(await verifyReader.IsDBNullAsync(0));
- }
+ #region Prepared Statement Tests
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsSqlVectorSupported))]
public void TestInsertVectorsAsVarcharWithPrepare()
- {
- SqlConnection conn = new SqlConnection(s_connectionString);
- conn.Open();
- SqlCommand command = new SqlCommand(s_insertCmdString, conn);
- SqlParameter vectorParam = new SqlParameter("@VectorData", SqlDbType.VarChar, -1);
- command.Parameters.Add(vectorParam);
- command.Prepare();
- for (int i = 0; i < 10; i++)
- {
- vectorParam.Value = JsonSerializer.Serialize(new float[] { i + 0.1f, i + 0.2f, i + 0.3f });
- command.ExecuteNonQuery();
- }
- SqlCommand validateCommand = new SqlCommand($"SELECT VectorData FROM {s_tableName}", conn);
- using SqlDataReader reader = validateCommand.ExecuteReader();
- int rowcnt = 0;
- while (reader.Read())
- {
- float[] expectedData = new float[] { rowcnt + 0.1f, rowcnt + 0.2f, rowcnt + 0.3f };
- float[] dbData = JsonSerializer.Deserialize(reader.GetString(0))!;
- Assert.Equal(expectedData, dbData);
- rowcnt++;
- }
- Assert.Equal(10, rowcnt);
- }
+ => PreparedInsertRoundTrip();
+
+ #endregion
}
}