diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/GenerateTSqlModelRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/GenerateTSqlModelRequest.cs new file mode 100644 index 00000000..f9d9e561 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/GenerateTSqlModelRequest.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.DacFx.Contracts +{ + /// + /// Parameters to generate a SQL model + /// + public class GenerateTSqlModelParams + { + /// + /// URI of the project file this model is for + /// + public string ProjectUri { get; set; } + + /// + /// The version of Sql Server to target + /// + public string ModelTargetVersion { get; set; } + + /// + /// Gets or sets the Sql script file paths. + /// + public string[] FilePaths { get; set; } + } + + /// + /// Defines the generate sql model request + /// + class GenerateTSqlModelRequest + { + public static readonly RequestType Type = + RequestType.Create("dacFx/generateTSqlModel"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/DacFxService.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/DacFxService.cs index 32784990..d3001555 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/DacFx/DacFxService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/DacFxService.cs @@ -11,6 +11,8 @@ using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.DacFx.Contracts; using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.TaskServices; +using Microsoft.SqlServer.Dac.Model; +using Microsoft.SqlTools.ServiceLayer.Utility; using DacTableDesigner = Microsoft.Data.Tools.Sql.DesignServices.TableDesigner.TableDesigner; namespace Microsoft.SqlTools.ServiceLayer.DacFx @@ -25,6 +27,11 @@ namespace Microsoft.SqlTools.ServiceLayer.DacFx private static readonly Lazy instance = new Lazy(() => new DacFxService()); private readonly Lazy> operations = new Lazy>(() => new ConcurrentDictionary()); + /// + /// that maps project uri to model + /// + public Lazy> projectModels = + new Lazy>(() => new ConcurrentDictionary()); /// /// Gets the singleton instance object @@ -50,6 +57,7 @@ namespace Microsoft.SqlTools.ServiceLayer.DacFx serviceHost.SetRequestHandler(ValidateStreamingJobRequest.Type, this.HandleValidateStreamingJobRequest); serviceHost.SetRequestHandler(GetDefaultPublishOptionsRequest.Type, this.HandleGetDefaultPublishOptionsRequest); serviceHost.SetRequestHandler(ParseTSqlScriptRequest.Type, this.HandleParseTSqlScriptRequest); + serviceHost.SetRequestHandler(GenerateTSqlModelRequest.Type, this.HandleGenerateTSqlModelRequest); } /// @@ -323,6 +331,26 @@ namespace Microsoft.SqlTools.ServiceLayer.DacFx } } + public async Task HandleGenerateTSqlModelRequest(GenerateTSqlModelParams requestParams, RequestContext requestContext) + { + try + { + GenerateTSqlModelOperation operation = new GenerateTSqlModelOperation(requestParams); + TSqlModel model = operation.GenerateTSqlModel(); + + projectModels.Value[operation.Parameters.ProjectUri] = model; + await requestContext.SendResult(new ResultStatus + { + Success = true, + ErrorMessage = null + }); + } + catch (Exception e) + { + await requestContext.SendError(e); + } + } + private void ExecuteOperation(DacFxOperation operation, DacFxParams parameters, string taskName, RequestContext requestContext) { Task.Run(async () => diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/GenerateTSqlModelOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/GenerateTSqlModelOperation.cs new file mode 100644 index 00000000..fd1e34d1 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/GenerateTSqlModelOperation.cs @@ -0,0 +1,53 @@ +// +// 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.Diagnostics; +using Microsoft.SqlTools.ServiceLayer.DacFx.Contracts; +using Microsoft.SqlTools.Utility; +using Microsoft.SqlServer.Dac.Model; + +namespace Microsoft.SqlTools.ServiceLayer.DacFx +{ + /// + /// Class to represent creating a dacfx model + /// + class GenerateTSqlModelOperation + { + public GenerateTSqlModelParams Parameters { get; } + + public GenerateTSqlModelOperation(GenerateTSqlModelParams parameters) + { + Validate.IsNotNull("parameters", parameters); + this.Parameters = parameters; + } + + /// + /// Generate model from sql files, if no sql files are passed in then it generates an empty model. + /// + public TSqlModel GenerateTSqlModel() + { + try + { + TSqlModelOptions options = new TSqlModelOptions(); + SqlServerVersion version = (SqlServerVersion)Enum.Parse(typeof(SqlServerVersion), Parameters.ModelTargetVersion); + + var model = new TSqlModel(version, options); + // read all sql files + foreach (string filePath in Parameters.FilePaths) + { + string fileContent = System.IO.File.ReadAllText(filePath); + model.AddOrUpdateObjects(fileContent, filePath, null); + } + return model; + } + catch (Exception ex) + { + Logger.Write(TraceEventType.Information, $"Failed to generate model. Error: {ex.Message}"); + throw; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DacFx/DacFxServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DacFx/DacFxServiceTests.cs index 6f7c201e..ab5cd971 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DacFx/DacFxServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DacFx/DacFxServiceTests.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using Microsoft.Data.SqlClient; using Microsoft.SqlServer.Dac; @@ -16,6 +17,8 @@ using Microsoft.SqlTools.ServiceLayer.DacFx.Contracts; using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; using Microsoft.SqlTools.ServiceLayer.TaskServices; using Microsoft.SqlTools.ServiceLayer.Test.Common; +using Microsoft.SqlTools.ServiceLayer.Utility; +using Microsoft.SqlServer.Dac.Model; using NUnit.Framework; using Moq; using System.Reflection; @@ -25,6 +28,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.DacFx public class DacFxServiceTests { private string publishProfileFolder = Path.Combine("..", "..", "..", "DacFx", "PublishProfiles"); + private string TSqlModelTestFolder = Path.Combine("..", "..", "..", "DacFx", "TSqlModels"); private const string SourceScript = @"CREATE TABLE [dbo].[table1] ( [ID] INT NOT NULL PRIMARY KEY, @@ -687,7 +691,7 @@ FROM MissingEdgeHubInputStream'"; DatabaseName = targetDb.DatabaseName, DeploymentOptions = new DeploymentOptions() { - ExcludeObjectTypes = new DeploymentOptionProperty( new[] { ObjectType.Views }) + ExcludeObjectTypes = new DeploymentOptionProperty(new[] { ObjectType.Views }) } }; @@ -926,5 +930,102 @@ Streaming query statement contains a reference to missing output stream 'Missing Assert.That(actualValue, Is.EqualTo(expectedValue), $"Actual Property from Service is not equal to default property for {optionRow.Key}"); } } + + /// + /// Verify the generate Tsql model operation + /// + [Test] + public void GenerateTSqlModelFromSqlFiles() + { + DacFxService service = new DacFxService(); + Directory.CreateDirectory(TSqlModelTestFolder); + string sqlTable1DefinitionPath = Path.Join(TSqlModelTestFolder, "table1.sql"); + string sqlTable2DefinitionPath = Path.Join(TSqlModelTestFolder, "table2.sql"); + const string table1 = @"CREATE TABLE [dbo].[table1] + ( + [ID] INT NOT NULL PRIMARY KEY, + )"; + const string table2 = @"CREATE TABLE [dbo].[table2] + ( + [ID] INT NOT NULL PRIMARY KEY, + )"; + // create sql file + File.WriteAllText(sqlTable1DefinitionPath, table1); + File.WriteAllText(sqlTable2DefinitionPath, table2); + + var generateTSqlScriptParams = new GenerateTSqlModelParams + { + ProjectUri = Path.Join(TSqlModelTestFolder, "test.sqlproj"), + ModelTargetVersion = "Sql160", + FilePaths = new[] { sqlTable1DefinitionPath, sqlTable2DefinitionPath } + }; + + GenerateTSqlModelOperation op = new GenerateTSqlModelOperation(generateTSqlScriptParams); + var model = op.GenerateTSqlModel(); + var objects = model.GetObjects(DacQueryScopes.UserDefined, ModelSchema.Table).ToList(); + + VerifyAndCleanup(sqlTable1DefinitionPath); + VerifyAndCleanup(sqlTable2DefinitionPath); + Directory.Delete(TSqlModelTestFolder); + + Assert.That(model.Version.ToString(), Is.EqualTo(generateTSqlScriptParams.ModelTargetVersion), $"Model version is not equal to {generateTSqlScriptParams.ModelTargetVersion}"); + Assert.That(objects, Is.Not.Empty); + + var tableNames = objects.Select(o => o.Name.ToString()).ToList(); + + Assert.That(tableNames.Count, Is.EqualTo(2), "Model was not populated correctly"); + CollectionAssert.AreEquivalent(tableNames, new[] { "[dbo].[table1]", "[dbo].[table2]" }, "Table names do not match"); + } + + /// + /// Verify the generate Tsql model operation, creates an empty model when files are empty + /// + [Test] + public void GenerateEmptyTSqlModel() + { + DacFxService service = new DacFxService(); + Directory.CreateDirectory(TSqlModelTestFolder); + + var generateTSqlScriptParams = new GenerateTSqlModelParams + { + ProjectUri = Path.Join(TSqlModelTestFolder, "test.sqlproj"), + ModelTargetVersion = "Sql160", + FilePaths = new string[] { } + }; + + GenerateTSqlModelOperation op = new GenerateTSqlModelOperation(generateTSqlScriptParams); + var model = op.GenerateTSqlModel(); + + // clean up + Directory.Delete(TSqlModelTestFolder); + + Assert.That(model.GetObjects(DacQueryScopes.UserDefined, ModelSchema.Table).ToList().Count, Is.EqualTo(0), "Model is not empty"); + Assert.That(model.Version.ToString(), Is.EqualTo(generateTSqlScriptParams.ModelTargetVersion), $"Model version is not equal to {generateTSqlScriptParams.ModelTargetVersion}"); + } + + /// + /// Verify the generate TSql Model handle + /// + [Test] + public async Task VerifyGenerateTSqlModelHandle() + { + DacFxService service = new DacFxService(); + Directory.CreateDirectory(TSqlModelTestFolder); + + var generateTSqlScriptParams = new GenerateTSqlModelParams + { + ProjectUri = Path.Join(TSqlModelTestFolder, "test.sqlproj"), + ModelTargetVersion = "Sql160", + FilePaths = new string[] { } + }; + + var requestContext = new Mock>(); + requestContext.Setup((RequestContext x) => x.SendResult(It.Is((result) => result.Success == true))).Returns(Task.FromResult(new object())); + await service.HandleGenerateTSqlModelRequest(generateTSqlScriptParams, requestContext.Object); + + Directory.Delete(TSqlModelTestFolder); + + Assert.True(service.projectModels.Value.Keys.Contains(generateTSqlScriptParams.ProjectUri), "Model was not stored under project uri"); + } } }