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 cc51670a6a..94fcc87210 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 @@ -213,7 +213,6 @@ - @@ -269,10 +268,6 @@ PreserveNewest DateTimeVariant.bsl - - PreserveNewest - SqlVariantParameter.bsl - PreserveNewest StreamInputParameter_DebugMode.bsl diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/SqlVariantParam.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/SqlVariantParam.cs deleted file mode 100644 index 78d3ebe23d..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/SqlVariantParam.cs +++ /dev/null @@ -1,256 +0,0 @@ -// 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 Microsoft.Data.SqlClient.Server; - -namespace Microsoft.Data.SqlClient.ManualTesting.Tests -{ - public static class SqlVariantParam - { - private static string s_connStr; - - /// - /// Tests all SqlTypes inside sql_variant to server using sql_variant parameter, SqlBulkCopy, and TVP parameter with sql_variant inside. - /// - public static void SendAllSqlTypesInsideVariant(string connStr) - { - s_connStr = connStr; - Console.WriteLine(""); - Console.WriteLine("Starting test 'SqlVariantParam'"); - SendVariant(new SqlSingle((float)123.45), "System.Data.SqlTypes.SqlSingle", "real"); - SendVariant(new SqlSingle((double)123.45), "System.Data.SqlTypes.SqlSingle", "real"); - SendVariant(new SqlString("hello"), "System.Data.SqlTypes.SqlString", "nvarchar"); - SendVariant(new SqlDouble((double)123.45), "System.Data.SqlTypes.SqlDouble", "float"); - SendVariant(new SqlBinary(new byte[] { 0x00, 0x11, 0x22 }), "System.Data.SqlTypes.SqlBinary", "varbinary"); - SendVariant(new SqlGuid(Guid.NewGuid()), "System.Data.SqlTypes.SqlGuid", "uniqueidentifier"); - SendVariant(new SqlBoolean(true), "System.Data.SqlTypes.SqlBoolean", "bit"); - SendVariant(new SqlBoolean(1), "System.Data.SqlTypes.SqlBoolean", "bit"); - SendVariant(new SqlByte(1), "System.Data.SqlTypes.SqlByte", "tinyint"); - SendVariant(new SqlInt16(1), "System.Data.SqlTypes.SqlInt16", "smallint"); - SendVariant(new SqlInt32(1), "System.Data.SqlTypes.SqlInt32", "int"); - SendVariant(new SqlInt64(1), "System.Data.SqlTypes.SqlInt64", "bigint"); - SendVariant(new SqlDecimal(1234.123M), "System.Data.SqlTypes.SqlDecimal", "numeric"); - SendVariant(new SqlDateTime(DateTime.Now), "System.Data.SqlTypes.SqlDateTime", "datetime"); - SendVariant(new SqlMoney(123.123M), "System.Data.SqlTypes.SqlMoney", "money"); - Console.WriteLine("End test 'SqlVariantParam'"); - } - /// - /// Returns a SqlDataReader with embedded sql_variant column with paramValue inside. - /// - /// object value to embed as sql_variant field value - /// Set to true to return optional BaseType column which extracts base type of sql_variant column. - /// - private static SqlDataReader GetReaderForVariant(object paramValue, bool includeBaseType) - { - SqlConnection conn = new(s_connStr); - conn.Open(); - SqlCommand cmd = conn.CreateCommand(); - cmd.CommandText = "select @p1 as f1"; - if (includeBaseType) - { - cmd.CommandText += ", sql_variant_property(@p1,'BaseType') as BaseType"; - } - - cmd.Parameters.Add("@p1", SqlDbType.Variant); - cmd.Parameters["@p1"].Value = paramValue; - SqlDataReader dr = cmd.ExecuteReader(CommandBehavior.CloseConnection); - return dr; - } - /// - /// Verifies if SqlDataReader returns expected SqlType and base type. - /// - /// Test tag to identify caller - /// SqlDataReader to verify - /// Expected type name (SqlType) - /// Expected base type name (Sql Server type name) - private static void VerifyReader(string tag, SqlDataReader dr, string expectedTypeName, string expectedBaseTypeName) - { - // select sql_variant_property(cast(cast(123.45 as money) as sql_variant),'BaseType' ) as f1 - dr.Read(); - string actualTypeName = dr.GetSqlValue(0).GetType().ToString(); - string actualBaseTypeName = dr.GetString(1); - Console.WriteLine("{0,-40} -> {1}:{2}", tag, actualTypeName, actualBaseTypeName); - if (!actualTypeName.Equals(expectedTypeName)) - { - Console.WriteLine(" --> ERROR: Expected type {0} does not match actual type {1}", - expectedTypeName, actualTypeName); - } - if (!actualBaseTypeName.Equals(expectedBaseTypeName)) - { - Console.WriteLine(" --> ERROR: Expected base type {0} does not match actual base type {1}", - expectedBaseTypeName, actualBaseTypeName); - } - } - /// - /// Round trips a sql_variant to server and verifies result. - /// - /// Value to send as sql_variant - /// Expected SqlType name to round trip - /// Expected base type name (SQL Server base type inside sql_variant) - private static void SendVariant(object paramValue, string expectedTypeName, string expectedBaseTypeName) - { - // Round trip using Bulk Copy, normal param, and TVP param. - SendVariantBulkCopy(paramValue, expectedTypeName, expectedBaseTypeName); - SendVariantParam(paramValue, expectedTypeName, expectedBaseTypeName); - SendVariantTvp(paramValue, expectedTypeName, expectedBaseTypeName); - } - /// - /// Round trip sql_variant value as normal parameter. - /// - private static void SendVariantParam(object paramValue, string expectedTypeName, string expectedBaseTypeName) - { - using SqlDataReader dr = GetReaderForVariant(paramValue, true); - VerifyReader("SendVariantParam", dr, expectedTypeName, expectedBaseTypeName); - } - /// - /// Round trip sql_variant value using SqlBulkCopy. - /// - private static void SendVariantBulkCopy(object paramValue, string expectedTypeName, string expectedBaseTypeName) - { - string bulkCopyTableName = DataTestUtility.GetLongName("bulkDest"); - - // Fetch reader using type. - using SqlDataReader dr = GetReaderForVariant(paramValue, false); - using SqlConnection connBulk = new(s_connStr); - connBulk.Open(); - - ExecuteSQL(connBulk, "create table dbo.{0} (f1 sql_variant)", bulkCopyTableName); - try - { - // Perform bulk copy to target. - using (SqlBulkCopy bulkCopy = new(connBulk)) - { - bulkCopy.BulkCopyTimeout = 60; - bulkCopy.BatchSize = 1; - bulkCopy.DestinationTableName = bulkCopyTableName; - bulkCopy.WriteToServer(dr); - } - - // Verify target. - using (SqlCommand cmd = connBulk.CreateCommand()) - { - cmd.CommandText = string.Format("select f1, sql_variant_property(f1,'BaseType') as BaseType from {0}", bulkCopyTableName); - using SqlDataReader drVerify = cmd.ExecuteReader(); - VerifyReader("SendVariantBulkCopy[SqlDataReader]", drVerify, expectedTypeName, expectedBaseTypeName); - } - - // Truncate target table for next pass. - ExecuteSQL(connBulk, "truncate table {0}", bulkCopyTableName); - - // Send using DataTable as source. - DataTable t = new(); - t.Columns.Add("f1", typeof(object)); - t.Rows.Add(new object[] { paramValue }); - - // Perform bulk copy to target. - using (SqlBulkCopy bulkCopy = new(connBulk)) - { - bulkCopy.BulkCopyTimeout = 60; - bulkCopy.BatchSize = 1; - bulkCopy.DestinationTableName = bulkCopyTableName; - bulkCopy.WriteToServer(t, DataRowState.Added); - } - - // Verify target. - using (SqlCommand cmd = connBulk.CreateCommand()) - { - cmd.CommandText = string.Format("select f1, sql_variant_property(f1,'BaseType') as BaseType from {0}", bulkCopyTableName); - using SqlDataReader drVerify = cmd.ExecuteReader(); - VerifyReader("SendVariantBulkCopy[DataTable]", drVerify, expectedTypeName, expectedBaseTypeName); - } - - // Truncate target table for next pass. - ExecuteSQL(connBulk, "truncate table {0}", bulkCopyTableName); - - // Send using DataRow as source. - DataRow[] rowToSend = t.Select(); - - // Perform bulk copy to target. - using (SqlBulkCopy bulkCopy = new(connBulk)) - { - bulkCopy.BulkCopyTimeout = 60; - bulkCopy.BatchSize = 1; - bulkCopy.DestinationTableName = bulkCopyTableName; - bulkCopy.WriteToServer(rowToSend); - } - - // Verify target. - using (SqlCommand cmd = connBulk.CreateCommand()) - { - cmd.CommandText = string.Format("select f1, sql_variant_property(f1,'BaseType') as BaseType from {0}", bulkCopyTableName); - using SqlDataReader drVerify = cmd.ExecuteReader(); - VerifyReader("SendVariantBulkCopy[DataRow]", drVerify, expectedTypeName, expectedBaseTypeName); - } - } - finally - { - // Cleanup target table. - ExecuteSQL(connBulk, "drop table {0}", bulkCopyTableName); - } - } - /// - /// Round trip sql_variant value using TVP. - /// - private static void SendVariantTvp(object paramValue, string expectedTypeName, string expectedBaseTypeName) - { - string tvpTypeName = DataTestUtility.GetLongName("tvpVariant"); - - using SqlConnection connTvp = new(s_connStr); - connTvp.Open(); - - ExecuteSQL(connTvp, "create type dbo.{0} as table (f1 sql_variant)", tvpTypeName); - try - { - // Send TVP using SqlMetaData. - SqlMetaData[] metadata = new SqlMetaData[1]; - metadata[0] = new SqlMetaData("f1", SqlDbType.Variant); - SqlDataRecord[] record = new SqlDataRecord[1]; - record[0] = new SqlDataRecord(metadata); - record[0].SetValue(0, paramValue); - - using (SqlCommand cmd = connTvp.CreateCommand()) - { - cmd.CommandText = "select f1, sql_variant_property(f1,'BaseType') as BaseType from @tvpParam"; - SqlParameter p = cmd.Parameters.AddWithValue("@tvpParam", record); - p.SqlDbType = SqlDbType.Structured; - p.TypeName = string.Format("dbo.{0}", tvpTypeName); - using SqlDataReader dr = cmd.ExecuteReader(); - VerifyReader("SendVariantTvp[SqlMetaData]", dr, expectedTypeName, expectedBaseTypeName); - } - - // Send TVP using SqlDataReader. - using (SqlDataReader dr = GetReaderForVariant(paramValue, false)) - { - using SqlCommand cmd = connTvp.CreateCommand(); - cmd.CommandText = "select f1, sql_variant_property(f1,'BaseType') as BaseType from @tvpParam"; - SqlParameter p = cmd.Parameters.AddWithValue("@tvpParam", dr); - p.SqlDbType = SqlDbType.Structured; - p.TypeName = string.Format("dbo.{0}", tvpTypeName); - using SqlDataReader dr2 = cmd.ExecuteReader(); - VerifyReader("SendVariantTvp[SqlDataReader]", dr2, expectedTypeName, expectedBaseTypeName); - } - } - finally - { - // Cleanup tvp type. - ExecuteSQL(connTvp, "drop type {0}", tvpTypeName); - } - } - /// - /// Helper to execute t-sql with variable object name. - /// - /// - /// Format string using {0} to designate where to place objectName - /// Variable object name for t-sql - private static void ExecuteSQL(SqlConnection conn, string formatSql, string objectName) - { - using SqlCommand cmd = conn.CreateCommand(); - cmd.CommandText = string.Format(formatSql, objectName); - cmd.ExecuteNonQuery(); - } - } -} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/SqlVariantParameter.bsl b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/SqlVariantParameter.bsl deleted file mode 100644 index 1b8f54d980..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/SqlVariantParameter.bsl +++ /dev/null @@ -1,99 +0,0 @@ - -Starting test 'SqlVariantParam' -SendVariantBulkCopy[SqlDataReader] -> System.Data.SqlTypes.SqlSingle:real -SendVariantBulkCopy[DataTable] -> System.Data.SqlTypes.SqlSingle:real -SendVariantBulkCopy[DataRow] -> System.Data.SqlTypes.SqlSingle:real -SendVariantParam -> System.Data.SqlTypes.SqlSingle:real -SendVariantTvp[SqlMetaData] -> System.Data.SqlTypes.SqlSingle:real -SendVariantTvp[SqlDataReader] -> System.Data.SqlTypes.SqlSingle:real -SendVariantBulkCopy[SqlDataReader] -> System.Data.SqlTypes.SqlSingle:real -SendVariantBulkCopy[DataTable] -> System.Data.SqlTypes.SqlSingle:real -SendVariantBulkCopy[DataRow] -> System.Data.SqlTypes.SqlSingle:real -SendVariantParam -> System.Data.SqlTypes.SqlSingle:real -SendVariantTvp[SqlMetaData] -> System.Data.SqlTypes.SqlSingle:real -SendVariantTvp[SqlDataReader] -> System.Data.SqlTypes.SqlSingle:real -SendVariantBulkCopy[SqlDataReader] -> System.Data.SqlTypes.SqlString:nvarchar -SendVariantBulkCopy[DataTable] -> System.Data.SqlTypes.SqlString:nvarchar -SendVariantBulkCopy[DataRow] -> System.Data.SqlTypes.SqlString:nvarchar -SendVariantParam -> System.Data.SqlTypes.SqlString:nvarchar -SendVariantTvp[SqlMetaData] -> System.Data.SqlTypes.SqlString:nvarchar -SendVariantTvp[SqlDataReader] -> System.Data.SqlTypes.SqlString:nvarchar -SendVariantBulkCopy[SqlDataReader] -> System.Data.SqlTypes.SqlDouble:float -SendVariantBulkCopy[DataTable] -> System.Data.SqlTypes.SqlDouble:float -SendVariantBulkCopy[DataRow] -> System.Data.SqlTypes.SqlDouble:float -SendVariantParam -> System.Data.SqlTypes.SqlDouble:float -SendVariantTvp[SqlMetaData] -> System.Data.SqlTypes.SqlDouble:float -SendVariantTvp[SqlDataReader] -> System.Data.SqlTypes.SqlDouble:float -SendVariantBulkCopy[SqlDataReader] -> System.Data.SqlTypes.SqlBinary:varbinary -SendVariantBulkCopy[DataTable] -> System.Data.SqlTypes.SqlBinary:varbinary -SendVariantBulkCopy[DataRow] -> System.Data.SqlTypes.SqlBinary:varbinary -SendVariantParam -> System.Data.SqlTypes.SqlBinary:varbinary -SendVariantTvp[SqlMetaData] -> System.Data.SqlTypes.SqlBinary:varbinary -SendVariantTvp[SqlDataReader] -> System.Data.SqlTypes.SqlBinary:varbinary -SendVariantBulkCopy[SqlDataReader] -> System.Data.SqlTypes.SqlGuid:uniqueidentifier -SendVariantBulkCopy[DataTable] -> System.Data.SqlTypes.SqlGuid:uniqueidentifier -SendVariantBulkCopy[DataRow] -> System.Data.SqlTypes.SqlGuid:uniqueidentifier -SendVariantParam -> System.Data.SqlTypes.SqlGuid:uniqueidentifier -SendVariantTvp[SqlMetaData] -> System.Data.SqlTypes.SqlGuid:uniqueidentifier -SendVariantTvp[SqlDataReader] -> System.Data.SqlTypes.SqlGuid:uniqueidentifier -SendVariantBulkCopy[SqlDataReader] -> System.Data.SqlTypes.SqlBoolean:bit -SendVariantBulkCopy[DataTable] -> System.Data.SqlTypes.SqlBoolean:bit -SendVariantBulkCopy[DataRow] -> System.Data.SqlTypes.SqlBoolean:bit -SendVariantParam -> System.Data.SqlTypes.SqlBoolean:bit -SendVariantTvp[SqlMetaData] -> System.Data.SqlTypes.SqlBoolean:bit -SendVariantTvp[SqlDataReader] -> System.Data.SqlTypes.SqlBoolean:bit -SendVariantBulkCopy[SqlDataReader] -> System.Data.SqlTypes.SqlBoolean:bit -SendVariantBulkCopy[DataTable] -> System.Data.SqlTypes.SqlBoolean:bit -SendVariantBulkCopy[DataRow] -> System.Data.SqlTypes.SqlBoolean:bit -SendVariantParam -> System.Data.SqlTypes.SqlBoolean:bit -SendVariantTvp[SqlMetaData] -> System.Data.SqlTypes.SqlBoolean:bit -SendVariantTvp[SqlDataReader] -> System.Data.SqlTypes.SqlBoolean:bit -SendVariantBulkCopy[SqlDataReader] -> System.Data.SqlTypes.SqlByte:tinyint -SendVariantBulkCopy[DataTable] -> System.Data.SqlTypes.SqlByte:tinyint -SendVariantBulkCopy[DataRow] -> System.Data.SqlTypes.SqlByte:tinyint -SendVariantParam -> System.Data.SqlTypes.SqlByte:tinyint -SendVariantTvp[SqlMetaData] -> System.Data.SqlTypes.SqlByte:tinyint -SendVariantTvp[SqlDataReader] -> System.Data.SqlTypes.SqlByte:tinyint -SendVariantBulkCopy[SqlDataReader] -> System.Data.SqlTypes.SqlInt16:smallint -SendVariantBulkCopy[DataTable] -> System.Data.SqlTypes.SqlInt16:smallint -SendVariantBulkCopy[DataRow] -> System.Data.SqlTypes.SqlInt16:smallint -SendVariantParam -> System.Data.SqlTypes.SqlInt16:smallint -SendVariantTvp[SqlMetaData] -> System.Data.SqlTypes.SqlInt16:smallint -SendVariantTvp[SqlDataReader] -> System.Data.SqlTypes.SqlInt16:smallint -SendVariantBulkCopy[SqlDataReader] -> System.Data.SqlTypes.SqlInt32:int -SendVariantBulkCopy[DataTable] -> System.Data.SqlTypes.SqlInt32:int -SendVariantBulkCopy[DataRow] -> System.Data.SqlTypes.SqlInt32:int -SendVariantParam -> System.Data.SqlTypes.SqlInt32:int -SendVariantTvp[SqlMetaData] -> System.Data.SqlTypes.SqlInt32:int -SendVariantTvp[SqlDataReader] -> System.Data.SqlTypes.SqlInt32:int -SendVariantBulkCopy[SqlDataReader] -> System.Data.SqlTypes.SqlInt64:bigint -SendVariantBulkCopy[DataTable] -> System.Data.SqlTypes.SqlInt64:bigint -SendVariantBulkCopy[DataRow] -> System.Data.SqlTypes.SqlInt64:bigint -SendVariantParam -> System.Data.SqlTypes.SqlInt64:bigint -SendVariantTvp[SqlMetaData] -> System.Data.SqlTypes.SqlInt64:bigint -SendVariantTvp[SqlDataReader] -> System.Data.SqlTypes.SqlInt64:bigint -SendVariantBulkCopy[SqlDataReader] -> System.Data.SqlTypes.SqlDecimal:numeric -SendVariantBulkCopy[DataTable] -> System.Data.SqlTypes.SqlDecimal:numeric -SendVariantBulkCopy[DataRow] -> System.Data.SqlTypes.SqlDecimal:numeric -SendVariantParam -> System.Data.SqlTypes.SqlDecimal:numeric -SendVariantTvp[SqlMetaData] -> System.Data.SqlTypes.SqlDecimal:numeric -SendVariantTvp[SqlDataReader] -> System.Data.SqlTypes.SqlDecimal:numeric -SendVariantBulkCopy[SqlDataReader] -> System.Data.SqlTypes.SqlDateTime:datetime -SendVariantBulkCopy[DataTable] -> System.Data.SqlTypes.SqlDateTime:datetime -SendVariantBulkCopy[DataRow] -> System.Data.SqlTypes.SqlDateTime:datetime -SendVariantParam -> System.Data.SqlTypes.SqlDateTime:datetime -SendVariantTvp[SqlMetaData] -> System.Data.SqlTypes.SqlDateTime:datetime -SendVariantTvp[SqlDataReader] -> System.Data.SqlTypes.SqlDateTime:datetime -SendVariantBulkCopy[SqlDataReader] -> System.Data.SqlTypes.SqlDecimal:numeric - --> ERROR: Expected type System.Data.SqlTypes.SqlMoney does not match actual type System.Data.SqlTypes.SqlDecimal - --> ERROR: Expected base type money does not match actual base type numeric -SendVariantBulkCopy[DataTable] -> System.Data.SqlTypes.SqlDecimal:numeric - --> ERROR: Expected type System.Data.SqlTypes.SqlMoney does not match actual type System.Data.SqlTypes.SqlDecimal - --> ERROR: Expected base type money does not match actual base type numeric -SendVariantBulkCopy[DataRow] -> System.Data.SqlTypes.SqlDecimal:numeric - --> ERROR: Expected type System.Data.SqlTypes.SqlMoney does not match actual type System.Data.SqlTypes.SqlDecimal - --> ERROR: Expected base type money does not match actual base type numeric -SendVariantParam -> System.Data.SqlTypes.SqlMoney:money -SendVariantTvp[SqlMetaData] -> System.Data.SqlTypes.SqlMoney:money -SendVariantTvp[SqlDataReader] -> System.Data.SqlTypes.SqlMoney:money -End test 'SqlVariantParam' diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/SqlVariantParameterTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/SqlVariantParameterTests.cs index 989e0b26fe..d6d9791738 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/SqlVariantParameterTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/SqlVariantParameterTests.cs @@ -3,125 +3,358 @@ // 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.Globalization; -using System.IO; -using System.Text; using System.Threading; +using Microsoft.Data.SqlClient.Server; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests { /// /// Tests for SQL Variant parameters. - /// These tests run independently with their own baseline comparison. + /// Tests all SqlTypes inside sql_variant to server using sql_variant parameter, SqlBulkCopy, and TVP parameter. /// - [Collection("ParameterBaselineTests")] - public class SqlVariantParameterTests + public sealed class SqlVariantParameterTests : IDisposable { private readonly string _connStr; + private readonly CultureInfo _previousCulture; public SqlVariantParameterTests() { _connStr = DataTestUtility.TCPConnectionString; + + // Work around a gap in ValueUtilsSmi.GetSqlValue where reading a SqlString + // back from a SqlDataRecord Variant column reconstructs it via new SqlString(string), + // which uses CultureInfo.CurrentCulture.LCID. On Linux, this LCID is 127 + // (InvariantCulture), which is not a valid SQL Server collation and causes + // "invalid TDS collation" errors in the TVP code path. + // SqlClient doesn't support invariant mode: + // https://github.com/dotnet/SqlClient/issues/3742 + _previousCulture = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US"); } - [Trait("Category", "flaky")] - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] - public void SqlVariantParameterTest() + public void Dispose() { - Assert.True(RunTestAndCompareWithBaseline()); + Thread.CurrentThread.CurrentCulture = _previousCulture; } - private bool RunTestAndCompareWithBaseline() + public static IEnumerable SqlTypeTestData() { - CultureInfo previousCulture = Thread.CurrentThread.CurrentCulture; - Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US"); - try - { - string outputPath = "SqlVariantParameter.out"; - string baselinePath = "SqlVariantParameter.bsl"; + yield return new object[] { new SqlSingle((float)123.45), "System.Data.SqlTypes.SqlSingle", "real" }; + yield return new object[] { new SqlSingle((double)123.45), "System.Data.SqlTypes.SqlSingle", "real" }; + yield return new object[] { new SqlString("hello"), "System.Data.SqlTypes.SqlString", "nvarchar" }; + yield return new object[] { new SqlDouble(123.45), "System.Data.SqlTypes.SqlDouble", "float" }; + yield return new object[] { new SqlBinary(new byte[] { 0x00, 0x11, 0x22 }), "System.Data.SqlTypes.SqlBinary", "varbinary" }; + yield return new object[] { new SqlGuid(Guid.NewGuid()), "System.Data.SqlTypes.SqlGuid", "uniqueidentifier" }; + yield return new object[] { new SqlBoolean(true), "System.Data.SqlTypes.SqlBoolean", "bit" }; + yield return new object[] { new SqlBoolean(1), "System.Data.SqlTypes.SqlBoolean", "bit" }; + yield return new object[] { new SqlByte(1), "System.Data.SqlTypes.SqlByte", "tinyint" }; + yield return new object[] { new SqlInt16(1), "System.Data.SqlTypes.SqlInt16", "smallint" }; + yield return new object[] { new SqlInt32(1), "System.Data.SqlTypes.SqlInt32", "int" }; + yield return new object[] { new SqlInt64(1), "System.Data.SqlTypes.SqlInt64", "bigint" }; + yield return new object[] { new SqlDecimal(1234.123M), "System.Data.SqlTypes.SqlDecimal", "numeric" }; + yield return new object[] { new SqlDateTime(DateTime.Now), "System.Data.SqlTypes.SqlDateTime", "datetime" }; + yield return new object[] { new SqlMoney(123.123M), "System.Data.SqlTypes.SqlMoney", "money" }; + } - var fstream = new FileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.Read); - var swriter = new StreamWriter(fstream, Encoding.UTF8); - var twriter = new TvpTest.CarriageReturnLineFeedReplacer(swriter); - Console.SetOut(twriter); + public static IEnumerable BulkCopySqlTypeTestData() + { + yield return new object[] { new SqlSingle((float)123.45), "System.Data.SqlTypes.SqlSingle", "real" }; + yield return new object[] { new SqlSingle((double)123.45), "System.Data.SqlTypes.SqlSingle", "real" }; + yield return new object[] { new SqlString("hello"), "System.Data.SqlTypes.SqlString", "nvarchar" }; + yield return new object[] { new SqlDouble(123.45), "System.Data.SqlTypes.SqlDouble", "float" }; + yield return new object[] { new SqlBinary(new byte[] { 0x00, 0x11, 0x22 }), "System.Data.SqlTypes.SqlBinary", "varbinary" }; + yield return new object[] { new SqlGuid(Guid.NewGuid()), "System.Data.SqlTypes.SqlGuid", "uniqueidentifier" }; + yield return new object[] { new SqlBoolean(true), "System.Data.SqlTypes.SqlBoolean", "bit" }; + yield return new object[] { new SqlBoolean(1), "System.Data.SqlTypes.SqlBoolean", "bit" }; + yield return new object[] { new SqlByte(1), "System.Data.SqlTypes.SqlByte", "tinyint" }; + yield return new object[] { new SqlInt16(1), "System.Data.SqlTypes.SqlInt16", "smallint" }; + yield return new object[] { new SqlInt32(1), "System.Data.SqlTypes.SqlInt32", "int" }; + yield return new object[] { new SqlInt64(1), "System.Data.SqlTypes.SqlInt64", "bigint" }; + yield return new object[] { new SqlDecimal(1234.123M), "System.Data.SqlTypes.SqlDecimal", "numeric" }; + yield return new object[] { new SqlDateTime(DateTime.Now), "System.Data.SqlTypes.SqlDateTime", "datetime" }; + // SqlMoney is coerced to decimal (numeric) by SqlBulkCopy (see https://github.com/dotnet/SqlClient/issues/4040). + // ValidateBulkCopyVariant strips all INullable SqlTypes to their CLR equivalents via + // MetaType.GetComValueFromSqlVariant, which converts SqlMoney to decimal. For most types + // the CLR value maps back to the same TDS type (e.g. SqlInt32 -> int -> SQLINT4), but + // decimal maps to SQLNUMERICN instead of SQLMONEY. The normal parameter path works + // around this in WriteSqlVariantValue using a length==8 heuristic, but + // WriteSqlVariantDataRowValue (used by bulk copy) has no such recovery logic. + yield return new object[] { new SqlMoney(123.123M), "System.Data.SqlTypes.SqlDecimal", "numeric" }; + } - // Run Test - SqlVariantParam.SendAllSqlTypesInsideVariant(_connStr); + /// + /// Round trip sql_variant value as normal parameter. + /// + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] + [MemberData(nameof(SqlTypeTestData), DisableDiscoveryEnumeration = true)] + public void SqlType_VariantParam_RoundTripsCorrectly(object paramValue, string expectedTypeName, string expectedBaseTypeName) + { + // Arrange + using var conn = new SqlConnection(_connStr); + conn.Open(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT @p1 AS f1, sql_variant_property(@p1,'BaseType') AS BaseType"; + cmd.Parameters.Add("@p1", SqlDbType.Variant).Value = paramValue; - Console.Out.Flush(); - Console.Out.Dispose(); + // Act + using var dr = cmd.ExecuteReader(); + Assert.True(dr.Read(), "Expected a row from parameter query"); + string actualTypeName = dr.GetSqlValue(0).GetType().ToString(); + string actualBaseTypeName = dr.GetString(1); - // Recover the standard output stream - StreamWriter standardOutput = new(Console.OpenStandardOutput()); - standardOutput.AutoFlush = true; - Console.SetOut(standardOutput); + // Assert + Assert.Equal(expectedTypeName, actualTypeName); + Assert.Equal(expectedBaseTypeName, actualBaseTypeName); + } - // Compare output file - var comparisonResult = FindDiffFromBaseline(baselinePath, outputPath); + /// + /// Round trip sql_variant value using SqlBulkCopy with a SqlDataReader source. + /// + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] + [MemberData(nameof(BulkCopySqlTypeTestData), DisableDiscoveryEnumeration = true)] + public void SqlType_BulkCopyFromReader_RoundTripsCorrectly(object paramValue, string expectedTypeName, string expectedBaseTypeName) + { + // Arrange + string tableName = DataTestUtility.GetLongName("bulkDest"); - if (string.IsNullOrEmpty(comparisonResult)) + using var conn = new SqlConnection(_connStr); + conn.Open(); + using (var createCmd = new SqlCommand($"CREATE TABLE dbo.{tableName} (f1 sql_variant)", conn)) { - return true; + createCmd.ExecuteNonQuery(); } - Console.WriteLine("SqlVariantParameterTest Failed!"); - Console.WriteLine("Please compare baseline: {0} with output: {1}", Path.GetFullPath(baselinePath), Path.GetFullPath(outputPath)); - Console.WriteLine("Comparison Results:"); - Console.WriteLine(comparisonResult); - return false; + try + { + // Act + using (var dr = GetReaderForVariant(paramValue)) + { + using var bulkCopy = new SqlBulkCopy(conn) { BulkCopyTimeout = 60, BatchSize = 1, DestinationTableName = $"dbo.{tableName}" }; + bulkCopy.WriteToServer(dr); + } + + // Assert + using (var verifyCmd = new SqlCommand($"SELECT f1, sql_variant_property(f1,'BaseType') AS BaseType FROM dbo.{tableName}", conn)) + using (var dr = verifyCmd.ExecuteReader()) + { + Assert.True(dr.Read(), "Expected a row from bulk copy query"); + string actualTypeName = dr.GetSqlValue(0).GetType().ToString(); + string actualBaseTypeName = dr.GetString(1); + + Assert.Equal(expectedTypeName, actualTypeName); + Assert.Equal(expectedBaseTypeName, actualBaseTypeName); + } } finally { - Thread.CurrentThread.CurrentCulture = previousCulture; + using var dropCmd = new SqlCommand($"DROP TABLE IF EXISTS dbo.{tableName}", conn); + dropCmd.ExecuteNonQuery(); } } - private static string FindDiffFromBaseline(string baselinePath, string outputPath) + /// + /// Round trip sql_variant value using SqlBulkCopy with a DataTable source. + /// + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] + [MemberData(nameof(BulkCopySqlTypeTestData), DisableDiscoveryEnumeration = true)] + public void SqlType_BulkCopyFromDataTable_RoundTripsCorrectly(object paramValue, string expectedTypeName, string expectedBaseTypeName) { - var expectedLines = File.ReadAllLines(baselinePath); - var outputLines = File.ReadAllLines(outputPath); - - var comparisonSb = new StringBuilder(); + // Arrange + string tableName = DataTestUtility.GetLongName("bulkDest"); - var expectedLength = expectedLines.Length; - var outputLength = outputLines.Length; - var findDiffLength = Math.Min(expectedLength, outputLength); + using var conn = new SqlConnection(_connStr); + conn.Open(); + using (var createCmd = new SqlCommand($"CREATE TABLE dbo.{tableName} (f1 sql_variant)", conn)) + { + createCmd.ExecuteNonQuery(); + } - for (var lineNo = 0; lineNo < findDiffLength; lineNo++) + try { - if (!expectedLines[lineNo].Equals(outputLines[lineNo])) + var table = new DataTable(); + table.Columns.Add("f1", typeof(object)); + table.Rows.Add(new object[] { paramValue }); + + // Act + using (var bulkCopy = new SqlBulkCopy(conn) { BulkCopyTimeout = 60, BatchSize = 1, DestinationTableName = $"dbo.{tableName}" }) + { + bulkCopy.WriteToServer(table, DataRowState.Added); + } + + // Assert + using (var verifyCmd = new SqlCommand($"SELECT f1, sql_variant_property(f1,'BaseType') AS BaseType FROM dbo.{tableName}", conn)) + using (var dr = verifyCmd.ExecuteReader()) { - comparisonSb.AppendFormat("** DIFF at line {0} \n", lineNo); - comparisonSb.AppendFormat("A : {0} \n", outputLines[lineNo]); - comparisonSb.AppendFormat("E : {0} \n", expectedLines[lineNo]); + Assert.True(dr.Read(), "Expected a row from bulk copy query"); + string actualTypeName = dr.GetSqlValue(0).GetType().ToString(); + string actualBaseTypeName = dr.GetString(1); + + Assert.Equal(expectedTypeName, actualTypeName); + Assert.Equal(expectedBaseTypeName, actualBaseTypeName); } } + finally + { + using var dropCmd = new SqlCommand($"DROP TABLE IF EXISTS dbo.{tableName}", conn); + dropCmd.ExecuteNonQuery(); + } + } - var startIndex = findDiffLength - 1; - if (startIndex < 0) + /// + /// Round trip sql_variant value using SqlBulkCopy with a DataRow[] source. + /// + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] + [MemberData(nameof(BulkCopySqlTypeTestData), DisableDiscoveryEnumeration = true)] + public void SqlType_BulkCopyFromDataRow_RoundTripsCorrectly(object paramValue, string expectedTypeName, string expectedBaseTypeName) + { + // Arrange + string tableName = DataTestUtility.GetLongName("bulkDest"); + + using var conn = new SqlConnection(_connStr); + conn.Open(); + using (var createCmd = new SqlCommand($"CREATE TABLE dbo.{tableName} (f1 sql_variant)", conn)) { - startIndex = 0; + createCmd.ExecuteNonQuery(); } - if (findDiffLength < expectedLength) + try { - comparisonSb.AppendFormat("** MISSING \n"); - for (var lineNo = startIndex; lineNo < expectedLength; lineNo++) + var table = new DataTable(); + table.Columns.Add("f1", typeof(object)); + table.Rows.Add(new object[] { paramValue }); + DataRow[] rows = table.Select(); + + // Act + using (var bulkCopy = new SqlBulkCopy(conn) { BulkCopyTimeout = 60, BatchSize = 1, DestinationTableName = $"dbo.{tableName}" }) { - comparisonSb.AppendFormat("{0} : {1}", lineNo, expectedLines[lineNo]); + bulkCopy.WriteToServer(rows); } - } - if (findDiffLength < outputLength) - { - comparisonSb.AppendFormat("** EXTRA \n"); - for (var lineNo = startIndex; lineNo < outputLength; lineNo++) + + // Assert + using (var verifyCmd = new SqlCommand($"SELECT f1, sql_variant_property(f1,'BaseType') AS BaseType FROM dbo.{tableName}", conn)) + using (var dr = verifyCmd.ExecuteReader()) { - comparisonSb.AppendFormat("{0} : {1}", lineNo, outputLines[lineNo]); + Assert.True(dr.Read(), "Expected a row from bulk copy query"); + string actualTypeName = dr.GetSqlValue(0).GetType().ToString(); + string actualBaseTypeName = dr.GetString(1); + + Assert.Equal(expectedTypeName, actualTypeName); + Assert.Equal(expectedBaseTypeName, actualBaseTypeName); } } + finally + { + using var dropCmd = new SqlCommand($"DROP TABLE IF EXISTS dbo.{tableName}", conn); + dropCmd.ExecuteNonQuery(); + } + } + + /// + /// Round trip sql_variant value using TVP with a SqlMetaData/SqlDataRecord source. + /// + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] + [MemberData(nameof(SqlTypeTestData), DisableDiscoveryEnumeration = true)] + public void SqlType_TvpFromSqlMetaData_RoundTripsCorrectly(object paramValue, string expectedTypeName, string expectedBaseTypeName) + { + // Arrange + string tvpTypeName = DataTestUtility.GetLongName("tvpVariant"); + + using var conn = new SqlConnection(_connStr); + conn.Open(); + using (var createCmd = new SqlCommand($"CREATE TYPE dbo.{tvpTypeName} AS TABLE (f1 sql_variant)", conn)) + { + createCmd.ExecuteNonQuery(); + } - return comparisonSb.ToString(); + try + { + var metadata = new SqlMetaData[] { new SqlMetaData("f1", SqlDbType.Variant) }; + var record = new SqlDataRecord(metadata); + record.SetValue(0, paramValue); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT f1, sql_variant_property(f1,'BaseType') AS BaseType FROM @tvpParam"; + var p = cmd.Parameters.AddWithValue("@tvpParam", new[] { record }); + p.SqlDbType = SqlDbType.Structured; + p.TypeName = $"dbo.{tvpTypeName}"; + + // Act + using var dr = cmd.ExecuteReader(); + Assert.True(dr.Read(), "Expected a row from TVP[SqlMetaData] query"); + string actualTypeName = dr.GetSqlValue(0).GetType().ToString(); + string actualBaseTypeName = dr.GetString(1); + + // Assert + Assert.Equal(expectedTypeName, actualTypeName); + Assert.Equal(expectedBaseTypeName, actualBaseTypeName); + } + finally + { + using var dropCmd = new SqlCommand($"DROP TYPE IF EXISTS dbo.{tvpTypeName}", conn); + dropCmd.ExecuteNonQuery(); + } } + + /// + /// Round trip sql_variant value using TVP with a SqlDataReader source. + /// + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] + [MemberData(nameof(SqlTypeTestData), DisableDiscoveryEnumeration = true)] + public void SqlType_TvpFromSqlDataReader_RoundTripsCorrectly(object paramValue, string expectedTypeName, string expectedBaseTypeName) + { + // Arrange + string tvpTypeName = DataTestUtility.GetLongName("tvpVariant"); + + using var conn = new SqlConnection(_connStr); + conn.Open(); + using (var createCmd = new SqlCommand($"CREATE TYPE dbo.{tvpTypeName} AS TABLE (f1 sql_variant)", conn)) + { + createCmd.ExecuteNonQuery(); + } + + try + { + using var drSource = GetReaderForVariant(paramValue); + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT f1, sql_variant_property(f1,'BaseType') AS BaseType FROM @tvpParam"; + var p = cmd.Parameters.AddWithValue("@tvpParam", drSource); + p.SqlDbType = SqlDbType.Structured; + p.TypeName = $"dbo.{tvpTypeName}"; + + // Act + using var dr = cmd.ExecuteReader(); + Assert.True(dr.Read(), "Expected a row from TVP[SqlDataReader] query"); + string actualTypeName = dr.GetSqlValue(0).GetType().ToString(); + string actualBaseTypeName = dr.GetString(1); + + // Assert + Assert.Equal(expectedTypeName, actualTypeName); + Assert.Equal(expectedBaseTypeName, actualBaseTypeName); + } + finally + { + using var dropCmd = new SqlCommand($"DROP TYPE IF EXISTS dbo.{tvpTypeName}", conn); + dropCmd.ExecuteNonQuery(); + } + } + + #region Helpers + + private SqlDataReader GetReaderForVariant(object paramValue) + { + var conn = new SqlConnection(_connStr); + conn.Open(); + var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT @p1 AS f1"; + cmd.Parameters.Add("@p1", SqlDbType.Variant).Value = paramValue; + return cmd.ExecuteReader(CommandBehavior.CloseConnection); + } + + #endregion } }