diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs index 5cb28ee4..684f4cea 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs @@ -8631,6 +8631,11 @@ namespace Microsoft.SqlTools.ServiceLayer return Keys.GetString(Keys.QueryServiceQueryFailed, message); } + public static string QueryServiceUnsupportedSqlVariantType(string underlyingType, string columnName) + { + return Keys.GetString(Keys.QueryServiceUnsupportedSqlVariantType, underlyingType, columnName); + } + public static string QueryServiceSaveAsFail(string fileName, string message) { return Keys.GetString(Keys.QueryServiceSaveAsFail, fileName, message); @@ -9011,6 +9016,9 @@ namespace Microsoft.SqlTools.ServiceLayer public const string QueryServiceResultSetTooLarge = "QueryServiceResultSetTooLarge"; + public const string QueryServiceUnsupportedSqlVariantType = "QueryServiceUnsupportedSqlVariantType"; + + public const string QueryServiceSaveAsResultSetNotComplete = "QueryServiceSaveAsResultSetNotComplete"; diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx index a2e5b6db..74435378 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx @@ -303,6 +303,11 @@ Result set has too many rows to be safely loaded + + The underlying type "{0}" for sql variant column "{1}" could not be resolved. + . + Parameters: 0 - underlyingType (string), 1 - columnName (string) + Result cannot be saved until query execution has completed diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings index 72c0a886..60d6183d 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings @@ -124,6 +124,8 @@ QueryServiceResultSetHasNoResults = Query has no results to return QueryServiceResultSetTooLarge = Result set has too many rows to be safely loaded +QueryServiceUnsupportedSqlVariantType(string underlyingType, string columnName) = The underlying type "{0}" for sql variant column "{1}" could not be resolved. + ### Save As Requests QueryServiceSaveAsResultSetNotComplete = Result cannot be saved until query execution has completed diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs index de22f564..e46e630e 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Data; using System.Data.SqlTypes; using System.IO; using System.Text; @@ -40,6 +41,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage private readonly Dictionary readMethods; + private readonly Dictionary sqlDBTypeMap; + #endregion /// @@ -98,6 +101,37 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage {typeof(SqlGuid), (o, id, col) => ReadGuid(o, id)}, {typeof(SqlMoney), (o, id, col) => ReadMoney(o, id)}, }; + + sqlDBTypeMap = new Dictionary { + {SqlDbType.BigInt, typeof(SqlInt64)}, + {SqlDbType.Binary, typeof(SqlBinary)}, + {SqlDbType.Bit, typeof(SqlBoolean)}, + {SqlDbType.Char, typeof(SqlString)}, + {SqlDbType.Date, typeof(SqlDateTime)}, + {SqlDbType.DateTime, typeof(SqlDateTime)}, + {SqlDbType.DateTime2, typeof(SqlDateTime)}, + {SqlDbType.DateTimeOffset, typeof(DateTimeOffset)}, + {SqlDbType.Decimal, typeof(SqlDecimal)}, + {SqlDbType.Float, typeof(SqlDouble)}, + {SqlDbType.Image, typeof(SqlBinary)}, + {SqlDbType.Int, typeof(SqlInt32)}, + {SqlDbType.Money, typeof(SqlMoney)}, + {SqlDbType.NChar, typeof(SqlString)}, + {SqlDbType.NText, typeof(SqlString)}, + {SqlDbType.NVarChar, typeof(SqlString)}, + {SqlDbType.Real, typeof(SqlSingle)}, + {SqlDbType.SmallDateTime, typeof(SqlDateTime)}, + {SqlDbType.SmallInt, typeof(SqlInt16)}, + {SqlDbType.SmallMoney, typeof(SqlMoney)}, + {SqlDbType.Text, typeof(SqlString)}, + {SqlDbType.Time, typeof(TimeSpan)}, + {SqlDbType.Timestamp, typeof(SqlBinary)}, + {SqlDbType.TinyInt, typeof(SqlByte)}, + {SqlDbType.UniqueIdentifier, typeof(SqlGuid)}, + {SqlDbType.VarBinary, typeof(SqlBinary)}, + {SqlDbType.VarChar, typeof(SqlString)}, + {SqlDbType.Xml, typeof(SqlString)} + }; } #region IFileStreamStorage Implementation @@ -134,19 +168,25 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage continue; } - // The typename is stored in the string - colType = Type.GetType(sqlVariantType); - - // Workaround .NET bug, see sqlbu# 440643 and vswhidbey# 599834 - // TODO: Is this workaround necessary for .NET Core? - if (colType == null && sqlVariantType == "System.Data.SqlTypes.SqlSingle") + // We need to specify the assembly name for SQL types in order to resolve the type correctly. + if (sqlVariantType.StartsWith("System.Data.SqlTypes.")) { - colType = typeof(SqlSingle); + sqlVariantType = sqlVariantType + ", System.Data.Common"; + } + colType = Type.GetType(sqlVariantType); + if (colType == null) + { + throw new ArgumentException(SR.QueryServiceUnsupportedSqlVariantType(sqlVariantType, column.ColumnName)); } } else { - colType = column.DataType; + Type type; + if (!sqlDBTypeMap.TryGetValue(column.SqlDbType, out type)) + { + type = typeof(SqlString); + } + colType = type; } // Use the right read function for the type to read the data from the file diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamWriter.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamWriter.cs index 522bdd55..3267b6c9 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamWriter.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamWriter.cs @@ -92,19 +92,19 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage {typeof(byte[]), val => WriteBytes((byte[]) val)}, {typeof(Guid), val => WriteGuid((Guid) val)}, - {typeof(SqlString), val => WriteNullable((SqlString) val, obj => WriteString((string) obj))}, - {typeof(SqlInt16), val => WriteNullable((SqlInt16) val, obj => WriteInt16((short) obj))}, - {typeof(SqlInt32), val => WriteNullable((SqlInt32) val, obj => WriteInt32((int) obj))}, - {typeof(SqlInt64), val => WriteNullable((SqlInt64) val, obj => WriteInt64((long) obj)) }, - {typeof(SqlByte), val => WriteNullable((SqlByte) val, obj => WriteByte((byte) obj)) }, - {typeof(SqlBoolean), val => WriteNullable((SqlBoolean) val, obj => WriteBoolean((bool) obj)) }, - {typeof(SqlDouble), val => WriteNullable((SqlDouble) val, obj => WriteDouble((double) obj)) }, - {typeof(SqlSingle), val => WriteNullable((SqlSingle) val, obj => WriteSingle((float) obj)) }, + {typeof(SqlString), val => WriteNullable((SqlString) val, obj => WriteString(((SqlString) obj).Value))}, + {typeof(SqlInt16), val => WriteNullable((SqlInt16) val, obj => WriteInt16(((SqlInt16) obj).Value))}, + {typeof(SqlInt32), val => WriteNullable((SqlInt32) val, obj => WriteInt32(((SqlInt32)obj).Value))}, + {typeof(SqlInt64), val => WriteNullable((SqlInt64) val, obj => WriteInt64(((SqlInt64) obj).Value)) }, + {typeof(SqlByte), val => WriteNullable((SqlByte) val, obj => WriteByte(((SqlByte) obj).Value)) }, + {typeof(SqlBoolean), val => WriteNullable((SqlBoolean) val, obj => WriteBoolean(((SqlBoolean) obj).Value)) }, + {typeof(SqlDouble), val => WriteNullable((SqlDouble) val, obj => WriteDouble(((SqlDouble) obj).Value)) }, + {typeof(SqlSingle), val => WriteNullable((SqlSingle) val, obj => WriteSingle(((SqlSingle) obj).Value)) }, {typeof(SqlDecimal), val => WriteNullable((SqlDecimal) val, obj => WriteSqlDecimal((SqlDecimal) obj)) }, - {typeof(SqlDateTime), val => WriteNullable((SqlDateTime) val, obj => WriteDateTime((DateTime) obj)) }, - {typeof(SqlBytes), val => WriteNullable((SqlBytes) val, obj => WriteBytes((byte[]) obj)) }, - {typeof(SqlBinary), val => WriteNullable((SqlBinary) val, obj => WriteBytes((byte[]) obj)) }, - {typeof(SqlGuid), val => WriteNullable((SqlGuid) val, obj => WriteGuid((Guid) obj)) }, + {typeof(SqlDateTime), val => WriteNullable((SqlDateTime) val, obj => WriteDateTime(((SqlDateTime) obj).Value)) }, + {typeof(SqlBytes), val => WriteNullable((SqlBytes) val, obj => WriteBytes(((SqlBytes) obj).Value)) }, + {typeof(SqlBinary), val => WriteNullable((SqlBinary) val, obj => WriteBytes(((SqlBinary) obj).Value)) }, + {typeof(SqlGuid), val => WriteNullable((SqlGuid) val, obj => WriteGuid(((SqlGuid) obj).Value)) }, {typeof(SqlMoney), val => WriteNullable((SqlMoney) val, obj => WriteMoney((SqlMoney) obj)) } }; } @@ -299,7 +299,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage internal int WriteBoolean(bool val) { byteBuffer[0] = 0x01; // length - byteBuffer[1] = (byte) (val ? 0x01 : 0x00); + byteBuffer[1] = (byte)(val ? 0x01 : 0x00); return FileUtilities.WriteWithLength(fileStream, byteBuffer, 2); } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/StorageDataReader.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/StorageDataReader.cs index 68742616..560e3209 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/StorageDataReader.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/StorageDataReader.cs @@ -98,7 +98,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage /// The value of the given column public object GetValue(int i) { - return sqlDataReader == null ? DbDataReader.GetValue(i) : sqlDataReader.GetValue(i); + return sqlDataReader == null ? DbDataReader.GetValue(i) : sqlDataReader.GetSqlValue(i); } /// @@ -113,7 +113,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage } else { - sqlDataReader.GetValues(values); + sqlDataReader.GetSqlValues(values); } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/QueryExecution/DataStorage/StorageDataReaderTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/QueryExecution/DataStorage/StorageDataReaderTests.cs index f5b35462..e65fa5e7 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/QueryExecution/DataStorage/StorageDataReaderTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/QueryExecution/DataStorage/StorageDataReaderTests.cs @@ -44,6 +44,20 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.QueryExecution.DataSt Assert.NotNull(bytes); } + /// + /// Validate GetBytesWithMaxCapacity + /// + [Test] + public void GetLongDecimalTest() + { + // SQL Server support up to 38 digits of decimal + var storageReader = GetTestStorageDataReader( + "SELECT 99999999999999999999999999999999999999"); + storageReader.DbDataReader.Read(); + var value = storageReader.GetValue(0); + Assert.AreEqual("99999999999999999999999999999999999999", value.ToString()); + } + /// /// Validate GetCharsWithMaxCapacity /// @@ -63,7 +77,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.QueryExecution.DataSt Assert.True(shortName.Length == 2); Assert.Throws(() => storageReader.GetBytesWithMaxCapacity(0, 0)); - Assert.Throws(() => storageReader.GetCharsWithMaxCapacity(0, 0)); + Assert.Throws(() => storageReader.GetCharsWithMaxCapacity(0, 0)); } /// @@ -94,10 +108,10 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.QueryExecution.DataSt writer.Write(output); Assert.True(writer.ToString().Equals(output)); writer.Write('.'); - Assert.True(writer.ToString().Equals(output + '.')); + Assert.True(writer.ToString().Equals(output + '.')); writer.Write(output); writer.Write('.'); Assert.True(writer.ToString().Equals(output + '.')); - } + } } } \ No newline at end of file diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/QueryExecution/DataTypeTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/QueryExecution/DataTypeTests.cs new file mode 100644 index 00000000..955be2ca --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/QueryExecution/DataTypeTests.cs @@ -0,0 +1,209 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; +using Microsoft.SqlTools.ServiceLayer.QueryExecution; +using Microsoft.SqlTools.ServiceLayer.SqlContext; +using Microsoft.SqlTools.ServiceLayer.Test.Common; +using NUnit.Framework; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.QueryExecution +{ + public class DataTypeTests + { + [Test] + public async Task BigIntTest() + { + await ExecuteAndVerifyResult("SELECT CAST(100 AS BIGINT)", "100"); + } + + [Test] + public async Task SqlVariantTest() + { + await ExecuteAndVerifyResult("DECLARE @ID sql_variant = 90;select @ID", "90"); + } + + [Test] + public async Task BinaryTest() + { + await ExecuteAndVerifyResult("SELECT CAST(100 AS BINARY)", "0x000000000000000000000000000000000000000000000000000000000064"); + } + + [Test] + public async Task BitTest() + { + await ExecuteAndVerifyResult("SELECT CAST(0 AS BIT)", "0"); + await ExecuteAndVerifyResult("SELECT CAST(1 AS BIT)", "1"); + } + + [Test] + public async Task CharTest() + { + await ExecuteAndVerifyResult("SELECT CAST('A' AS CHAR(1))", "A"); + } + + [Test] + public async Task DateTest() + { + await ExecuteAndVerifyResult("SELECT CAST('2020-01-01' AS DATE)", "2020-01-01"); + } + + [Test] + public async Task DateTimeTest() + { + await ExecuteAndVerifyResult("SELECT CAST('2020-01-01' AS DATETIME)", "2020-01-01 00:00:00.000"); + } + + [Test] + public async Task DateTime2Test() + { + await ExecuteAndVerifyResult("SELECT CAST('2020-01-01' AS DATETIME2)", "2020-01-01 00:00:00.0000000"); + } + + [Test] + public async Task DateTimeOffsetTest() + { + await ExecuteAndVerifyResult("SELECT CAST('2020-01-01' AS DATETIMEOFFSET)", "2020-01-01 00:00:00.0000000 +00:00"); + } + + [Test] + public async Task DecimalTest() + { + await ExecuteAndVerifyResult("SELECT CAST(99999999999999999999999999999999999999 AS DECIMAL(38))", "99999999999999999999999999999999999999"); + } + + [Test] + public async Task FloatTest() + { + await ExecuteAndVerifyResult("SELECT CAST(100.11 AS FLOAT)", "100.11"); + } + + [Test] + public async Task ImageTest() + { + await ExecuteAndVerifyResult("SELECT CAST('0' AS IMAGE)", "0x30"); + } + + [Test] + public async Task IntTest() + { + await ExecuteAndVerifyResult("SELECT CAST('100' AS INT)", "100"); + } + + [Test] + public async Task MoneyTest() + { + await ExecuteAndVerifyResult("SELECT CAST('100' AS MONEY)", "100.00"); + } + + [Test] + public async Task NCharTest() + { + await ExecuteAndVerifyResult("SELECT CAST(N'测' AS NCHAR(1))", "测"); + } + + [Test] + public async Task NTextTest() + { + await ExecuteAndVerifyResult("SELECT CAST(N'测试' AS NTEXT)", "测试"); + } + + [Test] + public async Task NVarCharTest() + { + await ExecuteAndVerifyResult("SELECT CAST(N'测试' AS NVARCHAR)", "测试"); + } + + [Test] + public async Task RealTest() + { + await ExecuteAndVerifyResult("SELECT CAST(100 AS REAL)", "100"); + } + + [Test] + public async Task SmallDateTimeTest() + { + await ExecuteAndVerifyResult("SELECT CAST('2021-01-01' AS SMALLDATETIME)", "2021-01-01 00:00:00"); + } + + [Test] + public async Task SmallIntTest() + { + await ExecuteAndVerifyResult("SELECT CAST(100 AS SMALLINT)", "100"); + } + + [Test] + public async Task SmallMoneyTest() + { + await ExecuteAndVerifyResult("SELECT CAST(100 AS SMALLMONEY)", "100.00"); + } + + [Test] + public async Task TextTest() + { + await ExecuteAndVerifyResult("SELECT CAST('abc' AS TEXT)", "abc"); + } + + [Test] + public async Task TimeTest() + { + await ExecuteAndVerifyResult("SELECT CAST('1:00:00.001' AS TIME)", "01:00:00.0010000"); + } + + [Test] + public async Task TimestampTest() + { + await ExecuteAndVerifyResult("SELECT CAST('A' AS TIMESTAMP)", "0x4100000000000000"); + } + + [Test] + public async Task TinyIntTest() + { + await ExecuteAndVerifyResult("SELECT CAST(1 AS TINYINT)", "1"); + } + + [Test] + public async Task UniqueIdentifierTest() + { + await ExecuteAndVerifyResult("SELECT CAST('fbf31d5b-bda2-4907-a50c-5458e95248ae' AS UNIQUEIDENTIFIER)", "fbf31d5b-bda2-4907-a50c-5458e95248ae"); + } + + [Test] + public async Task VarBinaryTest() + { + await ExecuteAndVerifyResult("SELECT CAST('ABCD' AS VARBINARY)", "0x41424344"); + } + + [Test] + public async Task VarCharTest() + { + await ExecuteAndVerifyResult("SELECT CAST('ABCD' AS VARCHAR)", "ABCD"); + } + + [Test] + public async Task XmlTest() + { + await ExecuteAndVerifyResult("SELECT CAST('1234' AS XML)", "1234"); + } + + private async Task ExecuteAndVerifyResult(string queryText, string expectedValue) + { + // Given a connection to a live database + var result = LiveConnectionHelper.InitLiveConnectionInfo(); + ConnectionInfo connInfo = result.ConnectionInfo; + var fileStreamFactory = MemoryFileSystem.GetFileStreamFactory(); + Query query = new Query(queryText, connInfo, new QueryExecutionSettings(), fileStreamFactory); + query.Execute(); + query.ExecutionTask.Wait(); + var subset = await query.GetSubset(0, 0, 0, 10); + Assert.AreEqual(1, subset.RowCount, "Row count does not match expected value."); + var actualValue = subset.Rows[0][0].DisplayValue; + Assert.AreEqual(expectedValue, actualValue); + } + } +} \ No newline at end of file