// -------------------------------------------------------------------------------------------------
// 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));
}
}