// ------------------------------------------------------------------------------------------------- // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // ------------------------------------------------------------------------------------------------- using System; using System.Collections.Generic; using System.Data; using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.SqlServer.TransactSql.ScriptDom; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Microsoft.Health.Extensions.BuildTimeCodeGenerator.Sql; /// /// The base class for SQL visitors /// public abstract class SqlVisitor : TSqlFragmentVisitor { private static readonly HashSet Keywords = new HashSet() { "TableName" }; /// /// These members will be added to the generated class. /// public List MembersToAdd { get; } = new List(); public abstract int ArtifactSortOder { get; } /// /// Converts a to a /// /// The type /// Whether the type is nullable /// The protected static TypeSyntax DataTypeReferenceToClrType(DataTypeReference dataType, bool nullable) { if (Enum.TryParse(dataType.Name.BaseIdentifier.Value, ignoreCase: true, out var sqlDbType)) { return SqlDbTypeToClrType(sqlDbType, nullable).ToTypeSyntax(useGlobalAlias: true); } // assumed to be a table type return TypeExtensions.CreateGenericTypeFromGenericTypeDefinition( typeof(IEnumerable<>).ToTypeSyntax(true), IdentifierName(GetRowStructNameForTableType(dataType.Name))); } /// /// Converts a to a . /// /// The type /// Whether the type is nullable /// The protected static Type SqlDbTypeToClrType(SqlDbType sqlDbType, bool nullable) { Type type = SqlDbTypeToClrType(sqlDbType); if (nullable && !type.IsClass) { type = typeof(Nullable<>).MakeGenericType(type); } return type; } /// /// Converts a to a . /// /// The type /// The protected static Type SqlDbTypeToClrType(SqlDbType sqlDbType) { switch (sqlDbType) { case SqlDbType.BigInt: return typeof(long); case SqlDbType.Binary: return typeof(byte[]); case SqlDbType.Bit: return typeof(bool); case SqlDbType.Char: break; case SqlDbType.Date: case SqlDbType.DateTime: case SqlDbType.DateTime2: case SqlDbType.SmallDateTime: case SqlDbType.Time: return typeof(DateTime); case SqlDbType.DateTimeOffset: return typeof(DateTimeOffset); case SqlDbType.Decimal: return typeof(decimal); case SqlDbType.Float: return typeof(double); case SqlDbType.Image: break; case SqlDbType.Int: return typeof(int); case SqlDbType.Money: break; case SqlDbType.NChar: case SqlDbType.NText: case SqlDbType.VarChar: case SqlDbType.NVarChar: case SqlDbType.Text: return typeof(string); case SqlDbType.Real: return typeof(double); case SqlDbType.SmallInt: return typeof(short); case SqlDbType.SmallMoney: break; case SqlDbType.Structured: return typeof(object); case SqlDbType.Timestamp: return typeof(byte[]); case SqlDbType.TinyInt: return typeof(byte); case SqlDbType.Udt: break; case SqlDbType.UniqueIdentifier: return typeof(Guid); case SqlDbType.VarBinary: return typeof(Stream); case SqlDbType.Variant: break; case SqlDbType.Xml: break; } throw new NotSupportedException(sqlDbType.ToString()); } /// /// Determines whether the given is nullable /// /// The column /// Whether the column is nullable protected static bool IsColumnNullable(ColumnDefinition column) { return column.Constraints.Any(c => c is NullableConstraintDefinition nc && nc.Nullable); } /// /// Creates an internal static readonly field for an instance of a class that has a parameterless constructor /// /// The class name /// The field name /// The field. protected static FieldDeclarationSyntax CreateStaticFieldForClass(string className, string fieldName) { return FieldDeclaration( VariableDeclaration(IdentifierName(className)) .AddVariables(VariableDeclarator(fieldName) .WithInitializer( EqualsValueClause( ObjectCreationExpression( IdentifierName(className)).AddArgumentListArguments())))) .AddModifiers(Token(SyntaxKind.InternalKeyword), Token(SyntaxKind.ReadOnlyKeyword), Token(SyntaxKind.StaticKeyword)); } /// /// Gets the arguments for SQL database types, like the length, precision, and scale. /// /// The column data type /// The column collation, if any /// The type arguments protected static IEnumerable GetDataTypeSpecificConstructorArguments(DataTypeReference dataType, Identifier collation) { if (dataType is ParameterizedDataTypeReference parameterizedDataType) { foreach (var parameter in parameterizedDataType.Parameters) { yield return Argument( LiteralExpression( SyntaxKind.NumericLiteralExpression, Literal(parameter.LiteralType == LiteralType.Max ? -1 : int.Parse(parameter.Value, CultureInfo.InvariantCulture)))); } } if (collation != null) { yield return Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(collation.Value))); } } /// /// Gets the name of the class that derives from TableValuedParameterDefiniition for a table type. /// /// The name of the table type /// The class name protected static string GetClassNameForTableValuedParameterDefinition(SchemaObjectName objectName) { return $"{GetTableTypeMemberName(objectName)}TableValuedParameterDefinition"; } /// /// Gets the name of the row struct for a table type. /// /// The name of the table type /// The struct name protected static string GetRowStructNameForTableType(SchemaObjectName objectName) { return $"{GetTableTypeMemberName(objectName)}Row"; } /// /// Strips away the underscore in a in a suffix for a table type name and replaces it with "V" /// For example, MyTableType_1 will return MyTableTypeV1 /// /// The table type name /// The name private static string GetTableTypeMemberName(SchemaObjectName objectName) { return Regex.Replace(objectName.BaseIdentifier.Value, @"_(\d+)$", "V$1"); } /// /// Strips away the version suffix for a member name. /// For example, "MyProcedure_1" will return "MyProcedure" /// /// The object name /// The name protected static string GetMemberNameWithoutVersionSuffix(SchemaObjectName objectName) { return Regex.Replace(objectName.BaseIdentifier.Value, @"_\d+$", string.Empty); } private static string GetColunmName(string sqlColumnName) { return Keywords.Contains(sqlColumnName) ? sqlColumnName + "Column" : sqlColumnName; } protected static MemberDeclarationSyntax CreatePropertyForTableColumn(ColumnDefinition column) { string normalizedSqlDbType = null; // Handle computed columns if (column.DataType == null && column.ComputedColumnExpression != null) { return FieldDeclaration( VariableDeclaration(SyntaxFactory.ParseTypeName("string")) .AddVariables(VariableDeclarator($"{column.ColumnIdentifier.Value}") .WithInitializer( EqualsValueClause( LiteralExpression( SyntaxKind.StringLiteralExpression, Literal(column.ColumnIdentifier.Value)))))) .AddModifiers(Token(SyntaxKind.InternalKeyword), Token(SyntaxKind.ConstKeyword)); } var sqlDbTypeToNormalize = column.DataType.Name.BaseIdentifier.Value; if (Enum.TryParse(sqlDbTypeToNormalize, true, out SqlDbType sqlDbType)) { normalizedSqlDbType = sqlDbType.ToString(); } else { if (string.Compare(sqlDbTypeToNormalize, "RowVersion", StringComparison.InvariantCultureIgnoreCase) == 0) { // The timestamp data type is a synonym for the rowversion data type. Its syntax is now deprecated in SQL. // We are translating rowversion (used in the SQL code) to timestamp here because the SqlDbType enum only supports timestamp. normalizedSqlDbType = SqlDbType.Timestamp.ToString(); } } IdentifierNameSyntax typeName = IdentifierName($"{(IsColumnNullable(column) ? "Nullable" : string.Empty)}{normalizedSqlDbType}Column"); return FieldDeclaration( VariableDeclaration(typeName) .AddVariables(VariableDeclarator(GetColunmName(column.ColumnIdentifier.Value)) .WithInitializer( EqualsValueClause( ObjectCreationExpression( typeName) .AddArgumentListArguments( Argument( LiteralExpression( SyntaxKind.StringLiteralExpression, Literal(column.ColumnIdentifier.Value)))) .AddArgumentListArguments(GetDataTypeSpecificConstructorArguments(column.DataType, column.Collation).ToArray()))))) .AddModifiers(Token(SyntaxKind.InternalKeyword), Token(SyntaxKind.ReadOnlyKeyword)); } }