// // 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.Collections.Generic; using System.Data; using System.Data.Common; using System.Data.SqlTypes; using System.Diagnostics; using Microsoft.SqlTools.Utility; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts { /// /// Wrapper around a DbColumn, which provides extra functionality, but can be used as a /// regular DbColumn /// public class DbColumnWrapper : DbColumn { #region Constants /// /// All types supported by the server, stored as a hash set to provide O(1) lookup /// private static readonly HashSet AllServerDataTypes = new HashSet { "bigint", "binary", "bit", "char", "datetime", "decimal", "float", "image", "int", "money", "nchar", "ntext", "nvarchar", "real", "uniqueidentifier", "smalldatetime", "smallint", "smallmoney", "text", "timestamp", "tinyint", "varbinary", "varchar", "sql_variant", "xml", "date", "time", "datetimeoffset", "datetime2" }; private const string SqlXmlDataTypeName = "xml"; private const string DbTypeXmlDataTypeName = "DBTYPE_XML"; private const string UnknownTypeName = "unknown"; #endregion /// /// Constructor for a DbColumnWrapper /// /// Most of this logic is taken from SSMS ColumnInfo class /// The column we're wrapping around public DbColumnWrapper(DbColumn column) { // Set all the fields for the base AllowDBNull = column.AllowDBNull; BaseCatalogName = column.BaseCatalogName; BaseColumnName = column.BaseColumnName; BaseSchemaName = column.BaseSchemaName; BaseServerName = column.BaseServerName; BaseTableName = column.BaseTableName; ColumnOrdinal = column.ColumnOrdinal; ColumnSize = column.ColumnSize; IsAliased = column.IsAliased; IsAutoIncrement = column.IsAutoIncrement; IsExpression = column.IsExpression; IsHidden = column.IsHidden; IsIdentity = column.IsIdentity; IsKey = column.IsKey; IsLong = column.IsLong; IsReadOnly = column.IsReadOnly; IsUnique = column.IsUnique; NumericPrecision = column.NumericPrecision; NumericScale = column.NumericScale; UdtAssemblyQualifiedName = column.UdtAssemblyQualifiedName; DataType = column.DataType; DataTypeName = column.DataTypeName.ToLowerInvariant(); DetermineSqlDbType(); AddNameAndDataFields(column.ColumnName); if (IsUdt) { // udtassemblyqualifiedname property is used to find if the datatype is of hierarchyid assembly type // Internally hiearchyid is sqlbinary so providerspecific type and type is changed to sqlbinarytype object assemblyQualifiedName = column.UdtAssemblyQualifiedName; const string hierarchyId = "MICROSOFT.SQLSERVER.TYPES.SQLHIERARCHYID"; if (assemblyQualifiedName != null && assemblyQualifiedName.ToString().StartsWith(hierarchyId, StringComparison.OrdinalIgnoreCase)) { DataType = typeof(SqlBinary); } else { DataType = typeof(byte[]); } } else { DataType = column.DataType; } } public DbColumnWrapper(ColumnInfo columnInfo) { DataTypeName = columnInfo.DataTypeName.ToLowerInvariant(); DetermineSqlDbType(); DataType = TypeConvertor.ToNetType(this.SqlDbType); if (DataType == typeof(String)) { this.ColumnSize = int.MaxValue; } AddNameAndDataFields(columnInfo.Name); } /// /// Default constructor, used for deserializing JSON RPC only /// public DbColumnWrapper() { } #region Properties /// /// Whether or not the column is bytes /// public bool IsBytes { get; private set; } /// /// Whether or not the column is a character type /// public bool IsChars { get; private set; } /// /// Whether or not the column is a SqlVariant type /// public bool IsSqlVariant { get; private set; } /// /// Whether or not the column is a user-defined type /// public bool IsUdt { get; private set; } /// /// Whether or not the column is XML /// public bool IsXml { get; set; } /// /// Whether or not the column is JSON /// public bool IsJson { get; set; } /// /// The SqlDbType of the column, for use in a SqlParameter /// public SqlDbType SqlDbType { get; private set; } /// /// Whther this is a HierarchyId column /// public bool IsHierarchyId { get; set; } /// /// Whether or not the column is an XML Reader type. /// /// /// Logic taken from SSDT determination of whether a column is a SQL XML type. It may not /// be possible to have XML readers from .NET Core SqlClient. /// public bool IsSqlXmlType => DataTypeName.Equals(SqlXmlDataTypeName, StringComparison.OrdinalIgnoreCase) || DataTypeName.Equals(DbTypeXmlDataTypeName, StringComparison.OrdinalIgnoreCase) || DataType == typeof(System.Xml.XmlReader); /// /// Whether or not the column is an unknown type /// /// /// Logic taken from SSDT determination of unknown columns. It may not even be possible to /// have "unknown" column types with the .NET Core SqlClient. /// public bool IsUnknownType => DataType == typeof(object) && DataTypeName.Equals(UnknownTypeName, StringComparison.OrdinalIgnoreCase); /// /// Whether or not the column can be updated, based on whether it's an auto increment /// column, is an XML reader column, and if it's read only. /// /// /// Logic taken from SSDT determination of updatable columns /// Special treatment for HierarchyId since we are using an Expression for HierarchyId column and expression column is readonly. /// public bool IsUpdatable => (!IsAutoIncrement.HasTrue() && !IsReadOnly.HasTrue() && !IsSqlXmlType) || IsHierarchyId; #endregion private void DetermineSqlDbType() { // Determine the SqlDbType SqlDbType type; if (Enum.TryParse(DataTypeName, true, out type)) { SqlDbType = type; } else { switch (DataTypeName) { case "numeric": SqlDbType = SqlDbType.Decimal; break; case "sql_variant": SqlDbType = SqlDbType.Variant; break; case "timestamp": SqlDbType = SqlDbType.VarBinary; break; case "sysname": SqlDbType = SqlDbType.NVarChar; break; default: SqlDbType = DataTypeName.EndsWith(".sys.hierarchyid") ? SqlDbType.Binary : SqlDbType.Udt; break; } } } private void AddNameAndDataFields(string columnName) { // We want the display name for the column to always exist ColumnName = string.IsNullOrEmpty(columnName) ? SR.QueryServiceColumnNull : columnName; switch (DataTypeName) { case "varchar": case "nvarchar": IsChars = true; Debug.Assert(ColumnSize.HasValue); if (ColumnSize.Value == int.MaxValue) { //For Yukon, special case nvarchar(max) with column name == "Microsoft SQL Server 2005 XML Showplan" - //assume it is an XML showplan. //Please note this field must be in sync with a similar field defined in QESQLBatch.cs. //This is not the best fix that we could do but we are trying to minimize code impact //at this point. Post Yukon we should review this code again and avoid //hard-coding special column name in multiple places. const string yukonXmlShowPlanColumn = "Microsoft SQL Server 2005 XML Showplan"; if (columnName == yukonXmlShowPlanColumn) { // Indicate that this is xml to apply the right size limit // Note we leave chars type as well to use the right retrieval mechanism. IsXml = true; } IsLong = true; } break; case "text": case "ntext": IsChars = true; IsLong = true; break; case "xml": IsXml = true; IsLong = true; break; case "binary": case "image": IsBytes = true; IsLong = true; break; case "varbinary": case "rowversion": IsBytes = true; Debug.Assert(ColumnSize.HasValue); if (ColumnSize.Value == int.MaxValue) { IsLong = true; } break; case "sql_variant": IsSqlVariant = true; break; default: if (!AllServerDataTypes.Contains(DataTypeName)) { // treat all UDT's as long/bytes data types to prevent the CLR from attempting // to load the UDT assembly into our process to call ToString() on the object. IsUdt = true; IsBytes = true; IsLong = true; } break; } } } /// /// Convert a base data type to another base data type /// public sealed class TypeConvertor { private static Dictionary _typeMap = new Dictionary(); static TypeConvertor() { _typeMap[SqlDbType.BigInt] = typeof(Int64); _typeMap[SqlDbType.Binary] = typeof(Byte); _typeMap[SqlDbType.Bit] = typeof(Boolean); _typeMap[SqlDbType.Char] = typeof(String); _typeMap[SqlDbType.DateTime] = typeof(DateTime); _typeMap[SqlDbType.Decimal] = typeof(Decimal); _typeMap[SqlDbType.Float] = typeof(Double); _typeMap[SqlDbType.Image] = typeof(Byte[]); _typeMap[SqlDbType.Int] = typeof(Int32); _typeMap[SqlDbType.Money] = typeof(Decimal); _typeMap[SqlDbType.NChar] = typeof(String); _typeMap[SqlDbType.NChar] = typeof(String); _typeMap[SqlDbType.NChar] = typeof(String); _typeMap[SqlDbType.NText] = typeof(String); _typeMap[SqlDbType.NVarChar] = typeof(String); _typeMap[SqlDbType.Real] = typeof(Single); _typeMap[SqlDbType.UniqueIdentifier] = typeof(Guid); _typeMap[SqlDbType.SmallDateTime] = typeof(DateTime); _typeMap[SqlDbType.SmallInt] = typeof(Int16); _typeMap[SqlDbType.SmallMoney] = typeof(Decimal); _typeMap[SqlDbType.Text] = typeof(String); _typeMap[SqlDbType.Timestamp] = typeof(Byte[]); _typeMap[SqlDbType.TinyInt] = typeof(Byte); _typeMap[SqlDbType.VarBinary] = typeof(Byte[]); _typeMap[SqlDbType.VarChar] = typeof(String); _typeMap[SqlDbType.Variant] = typeof(Object); // Note: treating as string _typeMap[SqlDbType.Xml] = typeof(String); _typeMap[SqlDbType.TinyInt] = typeof(Byte); _typeMap[SqlDbType.TinyInt] = typeof(Byte); _typeMap[SqlDbType.TinyInt] = typeof(Byte); _typeMap[SqlDbType.TinyInt] = typeof(Byte); } private TypeConvertor() { } /// /// Convert TSQL type to .Net data type /// /// /// public static Type ToNetType(SqlDbType sqlDbType) { Type netType; if (!_typeMap.TryGetValue(sqlDbType, out netType)) { netType = typeof(String); } return netType; } } }