// -------------------------------------------------------------------------------------------------
// 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.Linq;
using Microsoft.CodeAnalysis;
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;
///
/// Visits a SQL AST and creates a class for each CREATE TABLE, CREATE VIEW, and CREATE INDEX statement.
///
internal class CreateTableVisitor : SqlVisitor
{
public override int ArtifactSortOder => 0;
public override void Visit(CreateTableStatement node)
{
string tableName = node.SchemaObjectName.BaseIdentifier.Value;
// If its a temp table, ignore creating an object for it.
if (!tableName.StartsWith('#'))
{
string schemaQualifiedTableName = $"{node.SchemaObjectName.SchemaIdentifier.Value}.{tableName}";
string className = GetClassNameForTable(tableName);
ClassDeclarationSyntax classDeclarationSyntax =
CreateSkeletalClass(className, schemaQualifiedTableName)
.AddMembers(node.Definition.ColumnDefinitions.Select(CreatePropertyForTableColumn).ToArray());
FieldDeclarationSyntax field = CreateStaticFieldForClass(className, tableName);
MembersToAdd.Add(field.AddSortingKey(this, tableName));
MembersToAdd.Add(classDeclarationSyntax.AddSortingKey(this, tableName));
}
base.Visit(node);
}
private static ClassDeclarationSyntax CreateSkeletalClass(string className, string schemaQualifiedTableName)
{
return ClassDeclaration(className)
.WithModifiers(TokenList(Token(SyntaxKind.InternalKeyword)))
.WithBaseList(
BaseList(
SingletonSeparatedList(
SimpleBaseType(
IdentifierName("Table")))))
.AddMembers(
ConstructorDeclaration(
Identifier(className))
.WithModifiers(
TokenList(
Token(SyntaxKind.InternalKeyword)))
.WithInitializer(
ConstructorInitializer(
SyntaxKind.BaseConstructorInitializer,
ArgumentList(
SingletonSeparatedList(
Argument(
LiteralExpression(
SyntaxKind.StringLiteralExpression,
Literal(schemaQualifiedTableName)))))))
.WithBody(Block()));
}
public override void Visit(CreateViewStatement node)
{
// skip type creation for view. Views can have complicated columns like selects and c# types are not needed.
base.Visit(node);
}
private MemberDeclarationSyntax CreatePropertyForViewColumn(SelectElement selectElement, List<(string name, string alias)> tablesInScope)
{
if (selectElement is not SelectScalarExpression exp)
{
// notably SELECT * is not supported
throw new NotSupportedException($"Select element {selectElement.GetType().Name} is not supported");
}
if (exp.Expression is not ColumnReferenceExpression columnReference)
{
throw new NotSupportedException($"{exp.Expression.GetType().Name} is not supported.");
}
if (columnReference.MultiPartIdentifier.Count != 2)
{
throw new NotSupportedException("Please qualify column references with the table's alias");
}
string tableAliasName = columnReference.MultiPartIdentifier[0].Value;
string tableColumnName = columnReference.MultiPartIdentifier[1].Value;
string tableName = tablesInScope.Where(t => t.alias == tableAliasName).Select(t => t.name).FirstOrDefault() ?? throw new InvalidOperationException($"Unable to resolve table alias '{tableAliasName}'.");
string classNameForTable = GetClassNameForTable(tableName);
// find the class we generated for the table
var tableDeclaration = (ClassDeclarationSyntax)MembersToAdd.FirstOrDefault(m => m is ClassDeclarationSyntax c && c.Identifier.ValueText == classNameForTable)
?? throw new InvalidOperationException($"Table '{classNameForTable}' was not found");
// find the field we generated for the column
var columnDeclaration = (FieldDeclarationSyntax)tableDeclaration.Members.FirstOrDefault(m => m is FieldDeclarationSyntax fd && fd.Declaration.Variables[0].Identifier.ValueText == tableColumnName)
?? throw new InvalidOperationException($"Unable to find member for column '{tableColumnName}'");
if (exp.ColumnName == null)
{
// we can reuse the same declaration for the view.
return columnDeclaration;
}
// In this scenario, we have a SELECT t.MyCol AS Abc. We need to change the column from "MyCol" to "Abc"
return columnDeclaration.ReplaceNodes(
columnDeclaration.DescendantNodes(),
(original, updated) => original switch
{
VariableDeclaratorSyntax v when v.Identifier.ValueText == tableColumnName => ((VariableDeclaratorSyntax)updated).WithIdentifier(Identifier(exp.ColumnName.Value)),
LiteralExpressionSyntax l when l.Token.ValueText == tableColumnName => ((LiteralExpressionSyntax)updated).Update(Literal(exp.ColumnName.Value)),
_ => updated,
});
}
public override void Visit(CreateIndexStatement node)
{
string indexName = node.Name.Value;
// If its a temp table, ignore creating an object for it.
if (!node.OnName.BaseIdentifier.Value.StartsWith('#'))
{
var indexClassName = IdentifierName("Index");
FieldDeclarationSyntax indexNameField =
FieldDeclaration(
VariableDeclaration(indexClassName)
.AddVariables(
VariableDeclarator(Identifier(indexName))
.WithInitializer(
EqualsValueClause(
ObjectCreationExpression(indexClassName).AddArgumentListArguments(
Argument(
LiteralExpression(
SyntaxKind.StringLiteralExpression,
Literal(indexName))))))))
.AddModifiers(Token(SyntaxKind.InternalKeyword), Token(SyntaxKind.ReadOnlyKeyword));
string tableClassName = GetClassNameForTable(node.OnName.BaseIdentifier.Value);
string viewClassName = GetClassNameForView(node.OnName.BaseIdentifier.Value);
var memberIndex = MembersToAdd.FindIndex(m => m is ClassDeclarationSyntax c && (c.Identifier.ValueText == tableClassName || c.Identifier.ValueText == viewClassName));
if (memberIndex < 0)
{
throw new InvalidOperationException($"Index '{node.Name.Value}' is declared on an unrecognized type '{node.OnName.BaseIdentifier.Value}'");
}
MembersToAdd[memberIndex] = ((ClassDeclarationSyntax)MembersToAdd[memberIndex]).AddMembers(indexNameField);
}
base.Visit(node);
}
private static string GetClassNameForTable(string tableName) => $"{tableName}Table";
private static string GetClassNameForView(string viewName) => $"{viewName}View";
}