diff --git a/Packages.props b/Packages.props index cd4afd99..5b389bf4 100644 --- a/Packages.props +++ b/Packages.props @@ -21,7 +21,7 @@ - + diff --git a/bin/nuget/Microsoft.SqlServer.DacFx.Projects.161.8093.0-alpha.nupkg b/bin/nuget/Microsoft.SqlServer.DacFx.Projects.161.8093.0-alpha.nupkg deleted file mode 100644 index 9fef7734..00000000 Binary files a/bin/nuget/Microsoft.SqlServer.DacFx.Projects.161.8093.0-alpha.nupkg and /dev/null differ diff --git a/bin/nuget/Microsoft.SqlServer.DacFx.Projects.161.8416.0-alpha.nupkg b/bin/nuget/Microsoft.SqlServer.DacFx.Projects.161.8416.0-alpha.nupkg new file mode 100644 index 00000000..f2d96221 Binary files /dev/null and b/bin/nuget/Microsoft.SqlServer.DacFx.Projects.161.8416.0-alpha.nupkg differ diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/GetProjectProperties.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/GetProjectProperties.cs new file mode 100644 index 00000000..cb3033a8 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/GetProjectProperties.cs @@ -0,0 +1,58 @@ +// +// 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; + +#nullable disable + +namespace Microsoft.SqlTools.ServiceLayer.SqlProjects.Contracts +{ + /// + /// Get the cross-platform compatibility status for a project + /// + public class GetProjectPropertiesRequest + { + public static readonly RequestType Type = RequestType.Create("sqlProjects/getProjectProperties"); + } + + /// + /// Result containing project properties contained in the .sqlproj XML + /// + public class GetProjectPropertiesResult : ResultStatus + { + /// + /// GUID for the SQL project + /// + public string ProjectGuid { get; set; } + + /// + /// Build configuration, defaulted to Debug if not specified + /// + public string Configuration { get; set; } + + /// + /// Build platform, defaulted to AnyCPU if not specified + /// + public string Platform { get; set; } + + /// + /// Output path for build, defaulted to "bin/Debug" if not specified. + /// May be absolute or relative. + /// + public string OutputPath { get; set; } + + /// + /// Default collation for the project, defaulted to SQL_Latin1_General_CP1_CI_AS if not specified + /// + public string DefaultCollation { get; set; } + + /// + /// Source of the database schema, used in telemetry + /// + public string? DatabaseSource { get; set; } + + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/SetDatabaseSource.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/SetDatabaseSource.cs new file mode 100644 index 00000000..b11b1910 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/SetDatabaseSource.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +#nullable disable + +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.SqlProjects.Contracts +{ + /// + /// Parameters for setting the DatabaseSource property of a .sqlproj file + /// + public class SetDatabaseSourceParams : SqlProjectParams + { + /// + /// Source of the database schema, used in telemetry + /// + public string DatabaseSource { get; set; } + } + + /// + /// Set the DatabaseSource property of a .sqlproj file + /// + public class SetDatabaseSourceRequest + { + public static readonly RequestType Type = RequestType.Create("sqlProjects/setDatabaseSource"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/SqlProjectsService.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/SqlProjectsService.cs index 66a4a1a4..253a9fa9 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/SqlProjectsService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/SqlProjectsService.cs @@ -46,6 +46,8 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlProjects serviceHost.SetRequestHandler(CreateSqlProjectRequest.Type, HandleCreateSqlProjectRequest, isParallelProcessingSupported: true); serviceHost.SetRequestHandler(GetCrossPlatformCompatibilityRequest.Type, HandleGetCrossPlatformCompatibilityRequest, isParallelProcessingSupported: true); serviceHost.SetRequestHandler(UpdateProjectForCrossPlatformRequest.Type, HandleUpdateProjectForCrossPlatformRequest, isParallelProcessingSupported: false); + serviceHost.SetRequestHandler(GetProjectPropertiesRequest.Type, HandleGetProjectPropertiesRequest, isParallelProcessingSupported: true); + serviceHost.SetRequestHandler(SetDatabaseSourceRequest.Type, HandleSetDatabaseSourceRequest, isParallelProcessingSupported: false); // SQL object script functions serviceHost.SetRequestHandler(GetSqlObjectScriptsRequest.Type, HandleGetSqlObjectScriptsRequest, isParallelProcessingSupported: true); @@ -133,6 +135,31 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlProjects await RunWithErrorHandling(() => GetProject(requestParams.ProjectUri).UpdateForCrossPlatform(), requestContext); } + internal async Task HandleGetProjectPropertiesRequest(SqlProjectParams requestParams, RequestContext requestContext) + { + await RunWithErrorHandling(() => + { + SqlProjectProperties props = GetProject(requestParams.ProjectUri).Properties; + + return new GetProjectPropertiesResult() + { + Success = true, + ErrorMessage = null, + ProjectGuid = props.ProjectGuid, + Configuration = props.Configuration, + Platform = props.Platform, + OutputPath = props.OutputPath, + DefaultCollation = props.DefaultCollation, + DatabaseSource = props.DatabaseSource, + }; + }, requestContext); + } + + internal async Task HandleSetDatabaseSourceRequest(SetDatabaseSourceParams requestParams, RequestContext requestContext) + { + await RunWithErrorHandling(() => GetProject(requestParams.ProjectUri).Properties.DatabaseSource = requestParams.DatabaseSource, requestContext); + } + #endregion #region Script/folder functions @@ -310,7 +337,7 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlProjects #endregion - #region Database Reference functions + #region Database reference functions internal async Task HandleGetDatabaseReferencesRequest(SqlProjectParams requestParams, RequestContext requestContext) { @@ -459,7 +486,7 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlProjects { if (!Projects.ContainsKey(projectUri)) { - Projects[projectUri] = new SqlProject(projectUri); + Projects[projectUri] = SqlProject.OpenProject(projectUri); } return Projects[projectUri]; diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SqlProjects/SqlProjectsServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SqlProjects/SqlProjectsServiceTests.cs index 48b04201..b004e9d5 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SqlProjects/SqlProjectsServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SqlProjects/SqlProjectsServiceTests.cs @@ -161,7 +161,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.SqlProjects // Validate moving a SQL object script string movedScriptRelativePath = @"SubPath\MyRenamedTable.sql"; - string movedScriptAbsolutePath = Path.Join(Path.GetDirectoryName(projectUri), movedScriptRelativePath); + string movedScriptAbsolutePath = Path.Join(Path.GetDirectoryName(projectUri), FileUtils.NormalizePath(movedScriptRelativePath)); Directory.CreateDirectory(Path.GetDirectoryName(movedScriptAbsolutePath)!); requestMock = new(); @@ -254,7 +254,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.SqlProjects // Validate moving a None script string movedScriptRelativePath = @"SubPath\RenamedNoneIncludeFile.json"; - string movedScriptAbsolutePath = Path.Join(Path.GetDirectoryName(projectUri), movedScriptRelativePath); + string movedScriptAbsolutePath = Path.Join(Path.GetDirectoryName(projectUri), FileUtils.NormalizePath(movedScriptRelativePath)); Directory.CreateDirectory(Path.GetDirectoryName(movedScriptAbsolutePath)!); requestMock = new(); @@ -343,7 +343,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.SqlProjects // Validate moving a pre-deployment object script string movedScriptRelativePath = @"SubPath\RenamedPreDeploymentScript.sql"; - string movedScriptAbsolutePath = Path.Join(Path.GetDirectoryName(projectUri), movedScriptRelativePath); + string movedScriptAbsolutePath = Path.Join(Path.GetDirectoryName(projectUri), FileUtils.NormalizePath(movedScriptRelativePath)); Directory.CreateDirectory(Path.GetDirectoryName(movedScriptAbsolutePath)!); requestMock = new(); @@ -432,7 +432,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.SqlProjects // Validate moving a post-deployment object script string movedScriptRelativePath = @"SubPath\RenamedPostDeploymentScript.sql"; - string movedScriptAbsolutePath = Path.Join(Path.GetDirectoryName(projectUri), movedScriptRelativePath); + string movedScriptAbsolutePath = Path.Join(Path.GetDirectoryName(projectUri), FileUtils.NormalizePath(movedScriptRelativePath)); Directory.CreateDirectory(Path.GetDirectoryName(movedScriptAbsolutePath)!); requestMock = new(); @@ -823,6 +823,53 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.SqlProjects Assert.IsTrue(((GetCrossPlatformCompatibilityResult)getRequestMock.Result).IsCrossPlatformCompatible, "Input file should be cross-platform compatible after conversion"); } + [Test] + public async Task TestProjectProperties() + { + SqlProjectsService service = new(); + string projectUri = await service.CreateSqlProject(); + + MockRequest mock = new(); + + await service.HandleGetProjectPropertiesRequest(new SqlProjectParams() + { + ProjectUri = projectUri + }, mock.Object); + + mock.AssertSuccess(nameof(service.HandleGetProjectPropertiesRequest)); + + Assert.IsTrue(Guid.TryParse(mock.Result.ProjectGuid, out _), $"{mock.Result.ProjectGuid} should be set"); + Assert.AreEqual("AnyCPU", mock.Result.Platform); + Assert.AreEqual("Debug", mock.Result.Configuration); + Assert.AreEqual(@"bin\Debug\", mock.Result.OutputPath); // default value is normalized to Windows slashes + Assert.AreEqual("SQL_Latin1_General_CP1_CI_AS", mock.Result.DefaultCollation); + Assert.IsNull(mock.Result.DatabaseSource, nameof(mock.Result.DatabaseSource)); // validate DatabaseSource is null when the tag isn't present + + // Validate that DatabaseSource can be set when the tag doesn't exist + + MockRequest setSourceMock = new(); + await service.HandleSetDatabaseSourceRequest(new SetDatabaseSourceParams() + { + ProjectUri = projectUri, + DatabaseSource = "TestSource" + }, setSourceMock.Object); + + setSourceMock.AssertSuccess(nameof(service.HandleSetDatabaseSourceRequest)); + Assert.AreEqual("TestSource", service.Projects[projectUri].Properties.DatabaseSource); + + // Validate DatabaseSource is read when it has a value + + mock = new(); + + await service.HandleGetProjectPropertiesRequest(new SqlProjectParams() + { + ProjectUri = projectUri + }, mock.Object); + + mock.AssertSuccess(nameof(service.HandleGetProjectPropertiesRequest)); + Assert.AreEqual("TestSource", mock.Result.DatabaseSource); + } + #region Helpers private async Task<(SqlProjectsService Service, string ProjectUri, SqlCmdVariable DatabaseVar, SqlCmdVariable ServerVar)> SetUpDatabaseReferenceTest() diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test.Common/FileUtils.cs b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/FileUtils.cs index 22b58097..c3f21dd9 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test.Common/FileUtils.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/FileUtils.cs @@ -80,13 +80,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Common } /// - /// Normalizes Windows, Unix, and mixed paths to the same slash direction, specified by + /// Normalizes Windows, Unix, and mixed paths to the same slash direction, specified by . /// /// - /// Win32NT for \, Unix for / + /// Win32NT for \, Unix for /. If not set, path will be normalized to the current platform. /// - public static string NormalizePath(string path, PlatformID separatorType) + public static string NormalizePath(string path, PlatformID? separatorType = null) { + separatorType ??= Environment.OSVersion.Platform; + return separatorType switch { PlatformID.Win32NT => path.Contains('/')