diff --git a/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/AzureSqlDbHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/AzureSqlDbHelper.cs index ceff06d8..49b1dd2b 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/AzureSqlDbHelper.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/AzureSqlDbHelper.cs @@ -6,35 +6,94 @@ #nullable disable using System; -using System.Linq; using System.Collections.Generic; -using Microsoft.SqlServer.Management.Common; +using System.Diagnostics; +using System.Linq; using Microsoft.SqlTools.ServiceLayer.Management; -using SizeUnits = Microsoft.SqlTools.ServiceLayer.Management.DbSize.SizeUnits; +using static Microsoft.SqlTools.ServiceLayer.Management.DbSize; namespace Microsoft.SqlTools.ServiceLayer.Admin { public static class AzureSqlDbHelper { - - /// - /// Registry sub key for the AzureServiceObjectives overrides - /// - private const string AzureServiceObjectivesRegSubKey = @"AzureServiceObjectives"; - /// /// Contains the various editions available for an Azure Database + /// The implementation is opaque to consumers /// - /// ****IMPORTANT**** - If updating this enum make sure that the other logic in this class is updated as well - public enum AzureEdition + [DebuggerDisplay("{Name,nq}")] + public class AzureEdition { - Web = 0, - Business = 1, - Basic = 2, - Standard = 3, - Premium = 4, - DataWarehouse = 5, - PremiumRS = 6 + public static readonly AzureEdition Basic = new AzureEdition("Basic", "SR.BasicAzureEdition"); + public static readonly AzureEdition Standard = new AzureEdition("Standard", "SR.StandardAzureEdition"); + public static readonly AzureEdition Premium = new AzureEdition("Premium", "SR.PremiumAzureEdition"); + public static readonly AzureEdition DataWarehouse = new AzureEdition("DataWarehouse", "SR.DataWarehouseAzureEdition"); + public static readonly AzureEdition GeneralPurpose = new AzureEdition("GeneralPurpose", "SR.GeneralPurposeAzureEdition"); + public static readonly AzureEdition BusinessCritical = new AzureEdition("BusinessCritical", "SR.BusinessCriticalAzureEdition"); + + public static readonly AzureEdition Hyperscale = new AzureEdition("Hyperscale", "SR.HyperscaleAzureEdition"); + // Free does not offer DatabaseSize >=1GB, hence it's not "supported". + //public static readonly AzureEdition Free = new AzureEdition("Free", SR.FreeAzureEdition); + // Stretch and system do not seem to be applicable, so I'm commenting them out + //public static readonly AzureEdition Stretch = new AzureEdition("Stretch", SR.StretchAzureEdition); + //public static readonly AzureEdition System = new AzureEdition("System", SR.SystemAzureEdition); + + internal string Name { get; private set; } + internal string DisplayName { get; private set; } + + internal AzureEdition(string name, string displayName) + { + Name = name; + DisplayName = displayName; + } + + public override int GetHashCode() + { + return Name.GetHashCode(); + } + + public override bool Equals(object obj) + { + return obj is AzureEdition && ((AzureEdition)obj).Name.Equals(Name); + } + + public static bool operator ==(AzureEdition left, AzureEdition right) + { + return ReferenceEquals(left, right) || ((object)left != null && left.Equals(right)); + } + + public static bool operator !=(AzureEdition left, AzureEdition right) + { + return !(left == right); + } + + public override string ToString() + { + return Name; + } + } + + /// + /// Given a string, returns the matching AzureEdition instance. + /// + /// + /// + public static AzureEdition AzureEditionFromString(string edition) + { + var azureEdition = + AzureServiceObjectiveInfo.Keys.FirstOrDefault( + key => key.Name.ToLowerInvariant().Equals(edition.ToLowerInvariant())); + if (azureEdition != null) + { + return azureEdition; + } + if (edition.Contains('\'')) + { + throw new ArgumentException("ErrorInvalidEdition"); + } + // we don't know what it is but Azure lets you send any value you want + // including an empty string + return new AzureEdition(edition.ToLowerInvariant(), edition); + } /// @@ -45,47 +104,26 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin > { { - AzureEdition.Web, new KeyValuePair( - 1, //1GB - new[] - { - new DbSize(100, SizeUnits.MB), - new DbSize(1, SizeUnits.GB), //Default - new DbSize(5, SizeUnits.GB) - }) - }, - { - AzureEdition.Business, new KeyValuePair( - 0, //10GB - new[] - { - new DbSize(10, SizeUnits.GB), //Default - new DbSize(20, SizeUnits.GB), - new DbSize(30, SizeUnits.GB), - new DbSize(40, SizeUnits.GB), - new DbSize(50, SizeUnits.GB), - new DbSize(100, SizeUnits.GB), - new DbSize(150, SizeUnits.GB) - }) - }, - { - AzureEdition.Basic, new KeyValuePair( - 3, //2GB + AzureEdition.Basic, + new KeyValuePair( + 4, //2GB new[] { new DbSize(100, SizeUnits.MB), + new DbSize(250, SizeUnits.MB), new DbSize(500, SizeUnits.MB), new DbSize(1, SizeUnits.GB), - new DbSize(2, SizeUnits.GB) //Default + new DbSize(2, SizeUnits.GB), }) }, { AzureEdition.Standard, new KeyValuePair( - 13, //250GB + 14, //250GB new[] { new DbSize(100, SizeUnits.MB), + new DbSize(250, SizeUnits.MB), new DbSize(500, SizeUnits.MB), new DbSize(1, SizeUnits.GB), new DbSize(2, SizeUnits.GB), @@ -98,16 +136,22 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin new DbSize(100, SizeUnits.GB), new DbSize(150, SizeUnits.GB), new DbSize(200, SizeUnits.GB), - new DbSize(250, SizeUnits.GB) //Default + new DbSize(250, SizeUnits.GB), //Default + new DbSize(300, SizeUnits.GB), + new DbSize(400, SizeUnits.GB), + new DbSize(500, SizeUnits.GB), + new DbSize(750, SizeUnits.GB), + new DbSize(1024, SizeUnits.GB), }) }, { AzureEdition.Premium, new KeyValuePair( - 16, //500GB + 17, //500GB new[] { new DbSize(100, SizeUnits.MB), + new DbSize(250, SizeUnits.MB), new DbSize(500, SizeUnits.MB), new DbSize(1, SizeUnits.GB), new DbSize(2, SizeUnits.GB), @@ -124,6 +168,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin new DbSize(300, SizeUnits.GB), new DbSize(400, SizeUnits.GB), new DbSize(500, SizeUnits.GB), //Default + new DbSize(750, SizeUnits.GB), new DbSize(1024, SizeUnits.GB) //Following portal to display this as GB instead of 1TB }) }, @@ -143,23 +188,25 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin new DbSize(20480, SizeUnits.GB), new DbSize(30720, SizeUnits.GB), new DbSize(40960, SizeUnits.GB), - new DbSize(51200, SizeUnits.GB) + new DbSize(51200, SizeUnits.GB), + new DbSize(61440, SizeUnits.GB), + new DbSize(71680, SizeUnits.GB), + new DbSize(81920, SizeUnits.GB), + new DbSize(92160, SizeUnits.GB), + new DbSize(102400, SizeUnits.GB), + new DbSize(153600, SizeUnits.GB), + new DbSize(204800, SizeUnits.GB), + new DbSize(245760, SizeUnits.GB), + }) }, { - AzureEdition.PremiumRS, + AzureEdition.GeneralPurpose, new KeyValuePair( - 16, //500GB + 0, //32GB new[] { - new DbSize(100, SizeUnits.MB), - new DbSize(500, SizeUnits.MB), - new DbSize(1, SizeUnits.GB), - new DbSize(2, SizeUnits.GB), - new DbSize(5, SizeUnits.GB), - new DbSize(10, SizeUnits.GB), - new DbSize(20, SizeUnits.GB), - new DbSize(30, SizeUnits.GB), + new DbSize(32, SizeUnits.GB), new DbSize(40, SizeUnits.GB), new DbSize(50, SizeUnits.GB), new DbSize(100, SizeUnits.GB), @@ -168,34 +215,154 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin new DbSize(250, SizeUnits.GB), new DbSize(300, SizeUnits.GB), new DbSize(400, SizeUnits.GB), - new DbSize(500, SizeUnits.GB), //Default + new DbSize(500, SizeUnits.GB), + new DbSize(750, SizeUnits.GB), + new DbSize(1024, SizeUnits.GB), //Following portal to display this as GB instead of 1TB + new DbSize(1536, SizeUnits.GB), + new DbSize(3072, SizeUnits.GB), + new DbSize(4096, SizeUnits.GB), }) }, + { + AzureEdition.BusinessCritical, + new KeyValuePair( + 0, //32GB + new[] + { + new DbSize(32, SizeUnits.GB), + new DbSize(40, SizeUnits.GB), + new DbSize(50, SizeUnits.GB), + new DbSize(100, SizeUnits.GB), + new DbSize(150, SizeUnits.GB), + new DbSize(200, SizeUnits.GB), + new DbSize(250, SizeUnits.GB), + new DbSize(300, SizeUnits.GB), + new DbSize(400, SizeUnits.GB), + new DbSize(500, SizeUnits.GB), + new DbSize(750, SizeUnits.GB), + new DbSize(1024, SizeUnits.GB), //Following portal to display this as GB instead of 1TB + new DbSize(1536, SizeUnits.GB), + new DbSize(2048, SizeUnits.GB), + new DbSize(4096, SizeUnits.GB) + }) + }, + + { + AzureEdition.Hyperscale, + new KeyValuePair(0, new[] { new DbSize(0, SizeUnits.MB) }) + }, }; /// /// Maps Azure DB Editions to their corresponding Service Objective (Performance Level) options. These values are the default but - /// can be overridden by use of the ImportExportWizard registry key (see static initializer above). + /// can be overridden in the UI. /// /// The key is the index of the default value for the list /// + /// Try to keep this data structure (particularly the default values for each SLO) in sync with + /// the heuristic in TryGetAzureServiceLevelObjective() in %SDXROOT%\sql\ssms\core\sqlmanagerui\src\azureservicelevelobjectiveprovider.cs + /// private static readonly Dictionary> AzureServiceObjectiveInfo = new Dictionary > { - {AzureEdition.Basic, new KeyValuePair(0, new[] {"Basic"})}, - {AzureEdition.Standard, new KeyValuePair(2, new[] {"S0", "S1", "S2", "S3"})}, + {AzureEdition.Basic, new KeyValuePair(0, new string[] {"Basic"})}, + { + AzureEdition.Standard, + new KeyValuePair(0, new[] {"S0", "S1", "S2", "S3", "S4", "S6", "S7", "S9", "S12"}) + }, {AzureEdition.Premium, new KeyValuePair(0, new[] {"P1", "P2", "P4", "P6", "P11", "P15"})}, - {AzureEdition.PremiumRS, new KeyValuePair(0, new []{"PRS1", "PRS2", "PRS4", "PRS6"})}, - {AzureEdition.DataWarehouse, new KeyValuePair(3, new[] {"DW100", "DW200", "DW300", "DW400", "DW500", "DW600", "DW1000", "DW1200", "DW1500", "DW2000", "DW3000", "DW6000"})} + { + AzureEdition.DataWarehouse, + new KeyValuePair(3, + new[] + { + "DW100", "DW200", "DW300", "DW400", "DW500", "DW600", "DW1000", "DW1200", "DW1500", "DW2000", + "DW3000", "DW6000", "DW1000c","DW1500c","DW2000c", + "DW2500c","DW3000c","DW5000c","DW6000c","DW7500c", + "DW10000c","DW15000c","DW30000c" + }) + }, + { + // Added missing Vcore sku's + // Reference:https://docs.microsoft.com/en-us/azure/sql-database/sql-database-vcore-resource-limits-single-databases + AzureEdition.GeneralPurpose, + new KeyValuePair(6 /* Default = GP_Gen5_2 */, + new[] + { + "GP_Gen4_1", "GP_Gen4_2", "GP_Gen4_4", "GP_Gen4_8", "GP_Gen4_16","GP_Gen4_24", + "GP_Gen5_2","GP_Gen5_4","GP_Gen5_8","GP_Gen5_16","GP_Gen5_24","GP_Gen5_32","GP_Gen5_40","GP_Gen5_80" + + }) + }, + { + // Added missing Vcore sku's + // Reference:https://docs.microsoft.com/en-us/azure/sql-database/sql-database-vcore-resource-limits-single-databases + AzureEdition.BusinessCritical, + new KeyValuePair(6 /* Default = BC_Gen5_2 */, + new[] + { "BC_Gen4_1", "BC_Gen4_2", "BC_Gen4_4", "BC_Gen4_8", "BC_Gen4_16","BC_Gen4_24", + "BC_Gen5_2","BC_Gen5_4","BC_Gen5_8","BC_Gen5_16","BC_Gen5_24", "BC_Gen5_32", "BC_Gen5_40","BC_Gen5_80" + }) + }, + { + // HS_Gen5_2 is the default since, as of 2/25/2020, customers, unless on an allowed list, are already prevented from choosing Gen4. + AzureEdition.Hyperscale, + new KeyValuePair(11, new[] { + "HS_Gen4_1", "HS_Gen4_2", "HS_Gen4_3", "HS_Gen4_4", "HS_Gen4_5", "HS_Gen4_6", "HS_Gen4_7", "HS_Gen4_8", "HS_Gen4_9", "HS_Gen4_10", + "HS_Gen4_24", "HS_Gen5_2", "HS_Gen5_4", "HS_Gen5_6", "HS_Gen5_8", "HS_Gen5_10", "HS_Gen5_14", "HS_Gen5_16", "HS_Gen5_18", "HS_Gen5_20", + "HS_Gen5_24", "HS_Gen5_32", "HS_Gen5_40", "HS_Gen5_80" + }) + } }; - /// - /// Static initializer to read in the registry key values for the Service Objective mappings, which allows the user to override the defaults set for - /// the service objective list. We allow them to do this as a temporary measure so that if we change the service objectives in the future we - /// can tell people to use the registry key to use the new values until an updated SSMS can be released. - /// - static AzureSqlDbHelper() + //Supported BackupStorageRedundancy doc link:https://docs.microsoft.com/en-us/sql/t-sql/statements/create-database-transact-sql?view=azuresqldb-current&tabs=sqlpool + private static readonly Dictionary bsrAPIToUIValueMapping = new Dictionary() { + { "GRS", "Geo" }, + { "LRS", "Local" }, + { "ZRS", "Zone" } + }; + + //KeyValuePair contains the BackupStorageRedundancy values for all azure editions. + private static readonly KeyValuePair keyValuePair = new KeyValuePair(0, bsrAPIToUIValueMapping.Values.ToArray()); + private static readonly Dictionary> AzureBackupStorageRedundancy = new Dictionary + > + { + { + AzureEdition.Basic, keyValuePair + }, + { + AzureEdition.Standard, keyValuePair + }, + { + AzureEdition.Premium, keyValuePair + }, + { + AzureEdition.DataWarehouse, keyValuePair + }, + { + AzureEdition.GeneralPurpose, keyValuePair + }, + { + AzureEdition.BusinessCritical, keyValuePair + }, + { + AzureEdition.Hyperscale, keyValuePair + } + }; + + /// + /// Get the storageAccount Type string value from the dictionary backupStorageTypes. + /// + /// Current StorageAccountType + /// StorageAccountType string value for the current storageType + public static string GetStorageAccountTypeFromString(string storageAccountType) + { + if (bsrAPIToUIValueMapping.ContainsKey(storageAccountType)) + { + return bsrAPIToUIValueMapping[storageAccountType]; + } + return storageAccountType; } /// @@ -240,6 +407,25 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin return false; } + /// + /// Get the backupStorageRedundancy value for the given azure edition. + /// + /// Azure Edition + /// Supported BackupStorageRedundancy value + /// backupStorageRedundancy value for the given azure edition + public static bool TryGetBackupStorageRedundancy(AzureEdition edition, + out KeyValuePair backupStorageRedundancy) + { + if (AzureBackupStorageRedundancy.TryGetValue(edition, out backupStorageRedundancy)) + { + return true; + } + + backupStorageRedundancy = new KeyValuePair(-1, new string[0]); + + return false; + } + /// /// Gets the default database size for a specified Azure Edition /// @@ -270,7 +456,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin KeyValuePair pair; - if (AzureServiceObjectiveInfo.TryGetValue(edition, out pair)) + if (TryGetServiceObjectiveInfo(edition, out pair)) { //Bounds check since this value can be entered by users if (pair.Key >= 0 && pair.Key < pair.Value.Length) @@ -282,6 +468,24 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin return defaultServiceObjective; } + public static string GetDefaultBackupStorageRedundancy(AzureEdition edition) + { + string defaultBackupStorageRedundancy = ""; + + KeyValuePair pair; + + if (TryGetBackupStorageRedundancy(edition, out pair)) + { + //Bounds check since this value can be entered by users + if (pair.Key >= 0 && pair.Key < pair.Value.Length) + { + defaultBackupStorageRedundancy = pair.Value[pair.Key]; + } + } + + return defaultBackupStorageRedundancy; + } + /// /// Gets the localized Azure Edition display name /// @@ -289,82 +493,22 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin /// public static string GetAzureEditionDisplayName(AzureEdition edition) { - string result; - switch (edition) - { - //case AzureEdition.Business: - // result = SR.BusinessAzureEdition; - // break; - //case AzureEdition.Web: - // result = SR.WebAzureEdition; - // break; - //case AzureEdition.Basic: - // result = SR.BasicAzureEdition; - // break; - //case AzureEdition.Standard: - // result = SR.StandardAzureEdition; - // break; - //case AzureEdition.Premium: - // result = SR.PremiumAzureEdition; - // break; - //case AzureEdition.DataWarehouse: - // result = SR.DataWarehouseAzureEdition; - // break; - //case AzureEdition.PremiumRS: - // result = SR.PremiumRsAzureEdition; - // break; - default: - result = edition.ToString(); - break; - } - - return result; + return edition.DisplayName; } /// - /// Parses a display name back into its corresponding AzureEdition. + /// Parses a display name back into its corresponding AzureEdition. + /// If it doesn't match a known edition, returns one whose Name is a lowercase version of the + /// given displayName /// /// /// /// TRUE if the conversion succeeded, FALSE if it did not. public static bool TryGetAzureEditionFromDisplayName(string displayName, out AzureEdition edition) { - //if (string.Compare(displayName, SR.BusinessAzureEdition, CultureInfo.CurrentUICulture, CompareOptions.None) == 0) - //{ - // edition = AzureEdition.Business; - //} - //else if (string.Compare(displayName, SR.WebAzureEdition, CultureInfo.CurrentUICulture, CompareOptions.None) == 0) - //{ - // edition = AzureEdition.Web; - //} - //else if (string.Compare(displayName, SR.BasicAzureEdition, CultureInfo.CurrentUICulture, CompareOptions.None) == 0) - //{ - // edition = AzureEdition.Basic; - //} - //else if (string.Compare(displayName, SR.StandardAzureEdition, CultureInfo.CurrentUICulture, CompareOptions.None) == 0) - //{ - // edition = AzureEdition.Standard; - //} - //else if (string.Compare(displayName, SR.PremiumAzureEdition, CultureInfo.CurrentUICulture, CompareOptions.None) == 0) - //{ - // edition = AzureEdition.Premium; - //} - //else if (string.Compare(displayName, SR.DataWarehouseAzureEdition, CultureInfo.CurrentUICulture, CompareOptions.None) == 0) - //{ - // edition = AzureEdition.DataWarehouse; - //} - //else if (string.Compare(displayName, SR.PremiumRsAzureEdition, CultureInfo.CurrentUICulture, CompareOptions.None) == 0) - //{ - // edition = AzureEdition.PremiumRS; - //} - //else - { - //"Default" edition is standard - but since we're returning false the user shouldn't look at this anyways - edition = AzureEdition.Standard; - return false; - } - - // return true; + edition = AzureServiceObjectiveInfo.Keys.FirstOrDefault(key => key.DisplayName.Equals(displayName)) ?? + AzureEditionFromString(displayName); + return true; } /// @@ -374,23 +518,18 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin /// We do this so that the AzureEdition enum can have values such as NONE or DEFAULT added /// without requiring clients to explicitly filter out those values themselves each time. /// - public static IEnumerable GetValidAzureEditionOptions(ServerVersion version) + public static IEnumerable GetValidAzureEditionOptions(object unused) { - //Azure v12 and above doesn't have the Web and Business tiers - if (version.Major >= 12) - { - return new List() - { - AzureEdition.Basic, - AzureEdition.Standard, - AzureEdition.Premium, - AzureEdition.PremiumRS, - AzureEdition.DataWarehouse - }; - } - - //Default for now is to return all values since they're currently all valid - return Enum.GetValues(typeof(AzureEdition)).Cast(); + yield return AzureEdition.Basic; + yield return AzureEdition.Standard; + yield return AzureEdition.Premium; + yield return AzureEdition.DataWarehouse; + yield return AzureEdition.BusinessCritical; + yield return AzureEdition.GeneralPurpose; + //yield return AzureEdition.Free; + yield return AzureEdition.Hyperscale; + //yield return AzureEdition.Stretch; + //yield return AzureEdition.System; } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/CreateDatabaseObjects.cs b/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/CreateDatabaseObjects.cs index 58508b07..d894a272 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/CreateDatabaseObjects.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/CreateDatabaseObjects.cs @@ -6,16 +6,16 @@ #nullable disable using System; +using System.Collections.Generic; using System.ComponentModel; -using System.Resources; using System.Data; using System.IO; +using System.Resources; using Microsoft.SqlServer.Management.Common; -using Microsoft.SqlServer.Management.Smo; -using Smo = Microsoft.SqlServer.Management.Smo; using Microsoft.SqlServer.Management.Sdk.Sfc; -using System.Collections.Generic; +using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlTools.ServiceLayer.Management; +using Smo = Microsoft.SqlServer.Management.Smo; namespace Microsoft.SqlTools.ServiceLayer.Admin { @@ -56,6 +56,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin public string name; public bool isReadOnly; public bool isDefault; + public bool isAutogrowAllFiles; public FileGroupType fileGroupType = FileGroupType.RowsFileGroup; /// @@ -66,17 +67,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin this.name = String.Empty; this.isReadOnly = false; this.isDefault = false; + this.isAutogrowAllFiles = false; } /// /// Creates an instance of FilegroupData /// public FilegroupData(FileGroupType fileGroupType) + : this(name: String.Empty, isReadOnly: false, isDefault: false, fileGroupType: fileGroupType, isAutogrowAllFiles: false) { - this.name = String.Empty; - this.isReadOnly = false; - this.isDefault = false; - this.fileGroupType = fileGroupType; } /// @@ -87,10 +86,24 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin /// Default filegroup or not /// FileGroupType public FilegroupData(string name, bool isReadOnly, bool isDefault, FileGroupType fileGroupType) + : this(name, isReadOnly, isDefault, fileGroupType, isAutogrowAllFiles: false) + { + } + + /// + /// Initializes a new instance of the FilegroupData class. + /// + /// filegroup name + /// Readonly or not + /// Default filegroup or not + /// FileGroupType + /// Autogrow all files enabled or not + public FilegroupData(string name, bool isReadOnly, bool isDefault, FileGroupType fileGroupType, bool isAutogrowAllFiles) { this.name = name; this.isReadOnly = isReadOnly; this.isDefault = isDefault; + this.isAutogrowAllFiles = isAutogrowAllFiles; this.fileGroupType = fileGroupType; } @@ -99,11 +112,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin /// /// public FilegroupData(FilegroupData other) + : this(other.name, other.isReadOnly, other.isDefault, other.fileGroupType, other.isAutogrowAllFiles) { - this.name = other.name; - this.isReadOnly = other.isReadOnly; - this.isDefault = other.isDefault; - this.fileGroupType = other.fileGroupType; } /// @@ -135,6 +145,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin set { + System.Diagnostics.Debug.Assert(!this.Exists, "can't rename existing filegroups"); if (!this.Exists) { string oldname = this.currentState.name; @@ -178,6 +189,23 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin } } + /// + /// Whether the filegroup has AUTOGROW_ALL_FILES enabled + /// + public bool IsAutogrowAllFiles + { + get { return this.currentState.isAutogrowAllFiles; } + + set + { + if (this.currentState.isAutogrowAllFiles != value) + { + this.currentState.isAutogrowAllFiles = value; + this.parent.NotifyObservers(); + } + } + } + /// /// Whether the filegroup is of filestream type /// @@ -268,13 +296,14 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin /// /// public FilegroupPrototype(DatabasePrototype parent, FileGroupType filegroupType) + : this(parent: parent, + name: String.Empty, + isReadOnly: false, + isDefault: false, + filegroupType: filegroupType, + exists: false, + isAutogrowAllFiles: false) { - this.originalState = new FilegroupData(filegroupType); - this.currentState = this.originalState.Clone(); - this.parent = parent; - - this.filegroupExists = false; - this.removed = false; } /// @@ -288,8 +317,24 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin /// filegroup exists or not public FilegroupPrototype(DatabasePrototype parent, string name, bool isReadOnly, bool isDefault, FileGroupType filegroupType, bool exists) + : this(parent, name, isReadOnly, isDefault, filegroupType, exists, isAutogrowAllFiles: false) { - this.originalState = new FilegroupData(name, isReadOnly, isDefault, filegroupType); + } + + /// + /// Initializes a new instance of the FilegroupPrototype class. + /// + /// instance of DatabasePrototype + /// file group name + /// whether it is readonly or not + /// is default or not + /// filegrouptype + /// filegroup exists or not + /// is autogrow all files enabled or not + public FilegroupPrototype(DatabasePrototype parent, string name, bool isReadOnly, bool isDefault, + FileGroupType filegroupType, bool exists, bool isAutogrowAllFiles) + { + this.originalState = new FilegroupData(name, isReadOnly, isDefault, filegroupType, isAutogrowAllFiles); this.currentState = this.originalState.Clone(); this.parent = parent; @@ -321,7 +366,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin fg = db.FileGroups[this.Name]; } else - { + { fg = new FileGroup(db, this.Name, this.FileGroupType); db.FileGroups.Add(fg); } @@ -332,6 +377,13 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin filegroupChanged = true; } + if (fg.IsSupportedProperty("AutogrowAllFiles") && + (!this.Exists || (fg.AutogrowAllFiles != this.IsAutogrowAllFiles))) + { + fg.AutogrowAllFiles = this.IsAutogrowAllFiles; + filegroupChanged = true; + } + if (this.Exists && filegroupChanged) { fg.Alter(); @@ -352,7 +404,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin !this.Exists || this.Removed || (this.originalState.isDefault != this.currentState.isDefault) || - (this.originalState.isReadOnly != this.currentState.isReadOnly)); + (this.originalState.isReadOnly != this.currentState.isReadOnly) || + (this.originalState.isAutogrowAllFiles != this.currentState.isAutogrowAllFiles)); return result; } @@ -570,7 +623,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin if (FileGrowthType.Percent == file.GrowthType) { this.isGrowthInPercent = true; - this.growthInPercent = (int) file.Growth; + this.growthInPercent = (int)file.Growth; this.growthInKilobytes = 10240.0; // paranoia - make sure percent amount is greater than 1 @@ -642,7 +695,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin if (FileGrowthType.Percent == fileGrowthType) { this.isGrowthInPercent = true; - this.growthInPercent = (int) file.Growth; + this.growthInPercent = (int)file.Growth; this.growthInKilobytes = 10240.0; // paranoia - make sure percent amount is greater than 1 @@ -1020,9 +1073,16 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin set { + System.Diagnostics.Debug.Assert(!this.Exists, "Can't change the filegroup of an existing file."); + if ((FileType.Data == this.currentState.fileType || FileType.FileStream == this.currentState.fileType) && !this.Exists && (value != null)) { + if (this.IsPrimaryFile && (value != null)) + { + System.Diagnostics.Debug.Assert(value.Name == "PRIMARY", "Primary file must belong to primary filegroup"); + } + if (this.currentState.filegroup != null) { this.currentState.filegroup.OnFileGroupDeletedHandler -= @@ -1079,6 +1139,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin set { + if (value == true) + { + System.Diagnostics.Debug.Assert(FileGroup.Name == "PRIMARY", "Primary file must belong to primary filegroup"); + } + this.currentState.isPrimaryFile = value; this.database.NotifyObservers(); } @@ -1310,6 +1375,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin /// The database from which the file is to be removed private void RemoveFile(Database db) { + System.Diagnostics.Debug.Assert(this.Removed, "We're removing a file we arn't supposed to remove"); + if (this.Exists) { if (FileType.Log == this.DatabaseFileType) @@ -1404,7 +1471,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin if (this.Autogrowth.IsGrowthInPercent) { file.GrowthType = FileGrowthType.Percent; - file.Growth = (double) this.Autogrowth.GrowthInPercent; + file.Growth = (double)this.Autogrowth.GrowthInPercent; } else { @@ -1436,7 +1503,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin if (this.currentState.autogrowth.IsGrowthInPercent) { newFileGrowthType = FileGrowthType.Percent; - newGrowth = (double) this.currentState.autogrowth.GrowthInPercent; + newGrowth = (double)this.currentState.autogrowth.GrowthInPercent; } else { @@ -1455,7 +1522,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin if (this.originalState.autogrowth.IsGrowthInPercent) { originalFileGrowthType = FileGrowthType.Percent; - originalGrowth = (double) this.originalState.autogrowth.GrowthInPercent; + originalGrowth = (double)this.originalState.autogrowth.GrowthInPercent; } else { @@ -1628,7 +1695,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin if (this.Autogrowth.IsGrowthInPercent) { file.GrowthType = FileGrowthType.Percent; - file.Growth = (double) this.Autogrowth.GrowthInPercent; + file.Growth = (double)this.Autogrowth.GrowthInPercent; } else { @@ -1660,7 +1727,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin if (this.currentState.autogrowth.IsGrowthInPercent) { newFileGrowthType = FileGrowthType.Percent; - newGrowth = (double) this.currentState.autogrowth.GrowthInPercent; + newGrowth = (double)this.currentState.autogrowth.GrowthInPercent; } else { @@ -1679,7 +1746,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin if (this.originalState.autogrowth.IsGrowthInPercent) { originalFileGrowthType = FileGrowthType.Percent; - originalGrowth = (double) this.originalState.autogrowth.GrowthInPercent; + originalGrowth = (double)this.originalState.autogrowth.GrowthInPercent; } else { @@ -1764,12 +1831,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin } else { + System.Diagnostics.Debug.Assert(false, + "Client must set the ConnectionInfo property of the CDataContainer passed to the DatabasePrototype constructor"); + // $CONSIDER throwing an exception here. connectionInfo = context.ConnectionInfo; } // get default data file size request.Urn = "Server/Database[@Name='model']/FileGroup[@Name='PRIMARY']/File"; - request.Fields = new String[1] {"Size"}; + request.Fields = new String[1] { "Size" }; try { @@ -1781,8 +1851,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin defaultDataFileSize = DatabaseFilePrototype.RoundUpToNearestMegabyte(size); } - catch (Exception) + catch (Exception ex) { + System.Diagnostics.Trace.TraceError(ex.Message); // user doesn't have access to model so we set the default size // to be 5 MB defaultDataFileSize = 5120.0; @@ -1790,7 +1861,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin // get default log file size request.Urn = "Server/Database[@Name='model']/LogFile"; - request.Fields = new String[1] {"Size"}; + request.Fields = new String[1] { "Size" }; try { @@ -1799,8 +1870,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin defaultLogFileSize = DatabaseFilePrototype.RoundUpToNearestMegabyte(size); } - catch (Exception) + catch (Exception ex) { + System.Diagnostics.Trace.TraceError(ex.Message); + // user doesn't have access to model so we set the default size // to be 1MB defaultLogFileSize = 1024.0; @@ -1808,7 +1881,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin // get default data and log folders request.Urn = "Server/Setting"; - request.Fields = new String[] {"DefaultFile", "DefaultLog"}; + request.Fields = new String[] { "DefaultFile", "DefaultLog" }; try { @@ -1819,7 +1892,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin if (defaultDataFolder.Length == 0 || defaultLogFolder.Length == 0) { request.Urn = "Server/Information"; - request.Fields = new string[] {"MasterDBPath", "MasterDBLogPath"}; + request.Fields = new string[] { "MasterDBPath", "MasterDBLogPath" }; fileInfo = enumerator.Process(connectionInfo, request); if (defaultDataFolder.Length == 0) @@ -1858,8 +1931,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin rest); } } - catch (Exception) + catch (Exception ex) { + System.Diagnostics.Trace.TraceError(ex.Message); } } @@ -1899,7 +1973,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin if (defaultDataAutogrowth.IsGrowthInPercent) { - defaultDataAutogrowth.GrowthInPercent = (int) datafile.Growth; + defaultDataAutogrowth.GrowthInPercent = (int)datafile.Growth; defaultDataAutogrowth.GrowthInMegabytes = 10; } else @@ -1945,7 +2019,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin if (defaultLogAutogrowth.IsGrowthInPercent) { - defaultLogAutogrowth.GrowthInPercent = (int) logfile.Growth; + defaultLogAutogrowth.GrowthInPercent = (int)logfile.Growth; defaultLogAutogrowth.GrowthInMegabytes = 10; } else @@ -2001,7 +2075,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin /// /// private void OnFilegroupDeleted(object sender, FilegroupDeletedEventArgs e) - { + { + System.Diagnostics.Debug.Assert(this.FileGroup == sender, "received filegroup deleted notification from wrong filegroup"); e.DeletedFilegroup.OnFileGroupDeletedHandler -= new FileGroupDeletedEventHandler(OnFilegroupDeleted); // SQL Server deletes all the files in a filegroup when the filegroup is removed @@ -2021,7 +2096,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin /// The proposed file name to check private void CheckFileName(string fileName) { - char[] badFileCharacters = new char[] {'\\', '/', ':', '*', '?', '\"', '<', '>', '|'}; + char[] badFileCharacters = new char[] { '\\', '/', ':', '*', '?', '\"', '<', '>', '|' }; bool isAllWhitespace = (fileName.Trim(null).Length == 0); @@ -2043,6 +2118,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin else { int i = fileName.IndexOfAny(badFileCharacters); + System.Diagnostics.Debug.Assert(-1 < i, "unexpected error type"); message = String.Format(System.Globalization.CultureInfo.CurrentCulture, resourceManager.GetString("error_fileNameContainsIllegalCharacter"), fileName, fileName[i]); @@ -2059,7 +2135,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin /// The equivalent number of megabytes internal static int KilobytesToMegabytes(double kilobytes) { - return (int) Math.Ceiling(kilobytes/kilobytesPerMegabyte); + return (int)Math.Ceiling(kilobytes / kilobytesPerMegabyte); } /// @@ -2069,7 +2145,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin /// The equivalent number of kilobytes internal static double MegabytesToKilobytes(int megabytes) { - return (((double) megabytes)*kilobytesPerMegabyte); + return (((double)megabytes) * kilobytesPerMegabyte); } /// @@ -2080,8 +2156,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin /// The number of kb in the next larger mb internal static double RoundUpToNearestMegabyte(double kilobytes) { - double megabytes = Math.Ceiling(kilobytes/kilobytesPerMegabyte); - return (megabytes*kilobytesPerMegabyte); + double megabytes = Math.Ceiling(kilobytes / kilobytesPerMegabyte); + return (megabytes * kilobytesPerMegabyte); } /// @@ -2112,6 +2188,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin /// If logical name is empty, or physical name is invalid. private string MakeDiskFileName(string logicalName, string preferredPhysicalName, string suffix) { + System.Diagnostics.Debug.Assert(logicalName != null, "unexpected param - logical name cannot be null"); + System.Diagnostics.Debug.Assert(suffix != null, "unexpected param - suffix cannot be null. Pass String.Empty instead."); ResourceManager resourceManager = new ResourceManager("Microsoft.SqlTools.ServiceLayer.Localization.SR", typeof(DatabasePrototype).GetAssembly()); string filePath = String.Empty; // returned to the caller. @@ -2132,6 +2210,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin string message = String.Empty; message = resourceManager.GetString("error_emptyFileName"); + System.Diagnostics.Debug.Assert(message != null, "unexpected error string missing."); throw new InvalidOperationException(message); } @@ -2266,7 +2345,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin { ResourceManager resourceManager = new ResourceManager("Microsoft.SqlServer.Management.SqlManagerUI.CreateDatabaseStrings", - typeof (DatabaseAlreadyExistsException).GetAssembly()); + typeof(DatabaseAlreadyExistsException).Assembly); format = resourceManager.GetString("error.databaseAlreadyExists"); } @@ -2288,7 +2367,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin { ResourceManager manager = new ResourceManager("Microsoft.SqlServer.Management.SqlManagerUI.CreateDatabaseStrings", - typeof (DatabasePrototype).GetAssembly()); + typeof(DatabasePrototype).Assembly); List standardValues = null; TypeConverter.StandardValuesCollection result = null; @@ -2331,7 +2410,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin { ResourceManager manager = new ResourceManager("Microsoft.SqlServer.Management.SqlManagerUI.CreateDatabaseStrings", - typeof (DatabasePrototype90).GetAssembly()); + typeof(DatabasePrototype90).Assembly); List standardValues = new List(); TypeConverter.StandardValuesCollection result = null; @@ -2373,7 +2452,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin { ResourceManager manager = new ResourceManager("Microsoft.SqlServer.Management.SqlManagerUI.CreateDatabaseStrings", - typeof (DatabasePrototype80).GetAssembly()); + typeof(DatabasePrototype80).Assembly); List standardValues = new List(); TypeConverter.StandardValuesCollection result = null; @@ -2417,7 +2496,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin { ResourceManager manager = new ResourceManager("Microsoft.SqlServer.Management.SqlManagerUI.CreateDatabaseStrings", - typeof (DatabasePrototype80).GetAssembly()); + typeof(DatabasePrototype80).Assembly); List standardValues = new List(); TypeConverter.StandardValuesCollection result = null; @@ -2501,7 +2580,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin { ResourceManager manager = new ResourceManager("Microsoft.SqlServer.Management.SqlManagerUI.CreateDatabaseStrings", - typeof (DatabasePrototype).GetAssembly()); + typeof(DatabasePrototype).Assembly); List standardValues = new List(); TypeConverter.StandardValuesCollection result = null; @@ -2539,176 +2618,4 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin return true; } } - - ///// - ///// Helper class to provide standard values for populating drop down boxes on - ///// properties displayed in the Properties Grid - ///// - //internal class DynamicValuesConverter : StringConverter - //{ - // /// - // /// This method returns a list of dynamic values - // /// for various Properties in this class. - // /// - // /// - // /// List of Database Status Types - // public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) - // { - // var standardValues = new List(); - // StandardValuesCollection result = null; - - // //Handle ServiceLevelObjective values - // if (context.PropertyDescriptor != null && - // string.Compare(context.PropertyDescriptor.Name, "CurrentServiceLevelObjective", - // StringComparison.OrdinalIgnoreCase) == 0) - // { - // var designableObject = context.Instance as DesignableObject; - // if (designableObject != null) - // { - // var prototype = designableObject.ObjectDesigned as DatabasePrototypeAzure; - // if (prototype != null) - // { - // KeyValuePair pair; - // if (AzureSqlDbHelper.TryGetServiceObjectiveInfo(prototype.AzureEdition, out pair)) - // { - // standardValues.AddRange(pair.Value); - // } - // } - // } - // } - // //Handle AzureEditionDisplay values - // else if (context.PropertyDescriptor != null && - // string.Compare(context.PropertyDescriptor.Name, "AzureEditionDisplay", - // StringComparison.OrdinalIgnoreCase) == 0) - // { - // var designableObject = context.Instance as DesignableObject; - - // if (designableObject != null) - // { - // var prototype = designableObject.ObjectDesigned as DatabasePrototype; - // if (prototype != null) - // { - // foreach ( - // AzureEdition edition in - // AzureSqlDbHelper.GetValidAzureEditionOptions(prototype.ServerVersion)) - // { - // // We don't yet support creating DW with the UI - // if (prototype.Exists || edition != AzureEdition.DataWarehouse) - // { - // standardValues.Add(AzureSqlDbHelper.GetAzureEditionDisplayName(edition)); - // } - // } - // } - // else - // { - // STrace.Assert(false, - // "DesignableObject ObjectDesigned isn't a DatabasePrototype for AzureEditionDisplay StandardValues"); - // } - // } - // else - // { - // STrace.Assert(designableObject != null, - // "Context instance isn't a DesignableObject for AzureEditionDisplay StandardValues"); - // } - // } - // //Handle MaxSize values - // else if (context.PropertyDescriptor != null && - // string.Compare(context.PropertyDescriptor.Name, "MaxSize", StringComparison.OrdinalIgnoreCase) == 0) - // { - // var designableObject = context.Instance as DesignableObject; - // if (designableObject != null) - // { - - // var prototype = designableObject.ObjectDesigned as DatabasePrototypeAzure; - // if (prototype != null) - // { - // KeyValuePair pair; - // if (AzureSqlDbHelper.TryGetDatabaseSizeInfo(prototype.AzureEdition, out pair)) - // { - // standardValues.AddRange(pair.Value); - // } - // } - // } - - // } - - // if (standardValues.Count > 0) - // { - // result = new StandardValuesCollection(standardValues); - // } - - // return result; - // } - - // public override bool GetStandardValuesSupported(ITypeDescriptorContext context) - // { - // //Tells the grid that we'll support the values to display in a drop down - // return true; - // } - - // public override bool GetStandardValuesExclusive(ITypeDescriptorContext context) - // { - // //The values are exclusive (populated in a drop-down list instead of combo box) - // return true; - // } - //} - - ///// - ///// Helper class to provide standard values for populating drop down boxes on - ///// database scoped configuration properties displayed in the Properties Grid - ///// - //internal class DatabaseScopedConfigurationOnOffTypes : StringConverter - //{ - // /// - // /// This method returns a list of database scoped configuration on off values - // /// which will be populated as a drop down list. - // /// - // /// - // /// Database scoped configurations which will populate the drop down list. - // public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) - // { - // ResourceManager manager = - // new ResourceManager("Microsoft.SqlServer.Management.SqlManagerUI.CreateDatabaseStrings", - // typeof (DatabasePrototype).GetAssembly()); - // List standardValues = new List(); - // TypeConverter.StandardValuesCollection result = null; - - // if ( - // string.Compare(context.PropertyDescriptor.Name, "LegacyCardinalityEstimationDisplay", - // StringComparison.OrdinalIgnoreCase) == 0 || - // string.Compare(context.PropertyDescriptor.Name, "ParameterSniffingDisplay", - // StringComparison.OrdinalIgnoreCase) == 0 || - // string.Compare(context.PropertyDescriptor.Name, "QueryOptimizerHotfixesDisplay", - // StringComparison.OrdinalIgnoreCase) == 0) - // { - // standardValues.Add(manager.GetString("prototype.db.prop.databasescopedconfig.value.off")); - // standardValues.Add(manager.GetString("prototype.db.prop.databasescopedconfig.value.on")); - // } - // else - // { - // standardValues.Add(manager.GetString("prototype.db.prop.databasescopedconfig.value.off")); - // standardValues.Add(manager.GetString("prototype.db.prop.databasescopedconfig.value.on")); - // standardValues.Add(manager.GetString("prototype.db.prop.databasescopedconfig.value.primary")); - // } - - // if (standardValues.Count > 0) - // { - // result = new TypeConverter.StandardValuesCollection(standardValues); - // } - - // return result; - // } - - // public override bool GetStandardValuesSupported(ITypeDescriptorContext context) - // { - // //Tells the grid that we'll support the values to display in a drop down - // return true; - // } - - // public override bool GetStandardValuesExclusive(ITypeDescriptorContext context) - // { - // //The values are exclusive (populated in a drop-down list instead of combo box) - // return true; - // } - //} } \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype.cs b/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype.cs index e41a056e..04bc2db1 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype.cs @@ -6,18 +6,21 @@ #nullable disable using System; +using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Data; +using System.Diagnostics; +using System.Globalization; +using System.Linq; using System.Resources; +using Microsoft.Data.SqlClient; using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlServer.Management.Sdk.Sfc; -using Microsoft.Data.SqlClient; -using System.Collections.Generic; -using System.Diagnostics; -using AzureEdition = Microsoft.SqlTools.ServiceLayer.Admin.AzureSqlDbHelper.AzureEdition; using Microsoft.SqlTools.ServiceLayer.Management; +using Microsoft.SqlTools.ServiceLayer.Utility; +using AzureEdition = Microsoft.SqlTools.ServiceLayer.Admin.AzureSqlDbHelper.AzureEdition; namespace Microsoft.SqlTools.ServiceLayer.Admin { @@ -48,10 +51,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin public ContainmentType databaseContainmentType; public PageVerify pageVerify; public AzureEdition azureEdition; - public string azureEditionDisplayValue; public string configuredServiceLevelObjective; public string currentServiceLevelObjective; public DbSize maxSize; + public string backupStorageRedundancy; public bool closeCursorOnCommit; public bool isReadOnly; @@ -109,7 +112,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin public DatabaseScopedConfigurationOnOff queryOptimizerHotfixes; public DatabaseScopedConfigurationOnOff queryOptimizerHotfixesForSecondary; - + public bool isLedger; + /// /// Constructor for new databases using default data /// @@ -117,7 +121,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin /// This method is only called when the user doesn't have access to the model database /// public DatabaseData(CDataContainer context) - { + { + System.Diagnostics.Debug.Assert(context.Server != null, "SMO server not initialized"); + this.name = string.Empty; this.owner = string.Empty; this.restrictAccess = DatabaseUserAccess.Multiple; @@ -153,7 +159,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin this.filestreamDirectoryName = String.Empty; this.delayedDurability = DelayedDurability.Disabled; this.azureEdition = AzureEdition.Standard; - this.azureEditionDisplayValue = AzureEdition.Standard.ToString(); this.configuredServiceLevelObjective = String.Empty; this.currentServiceLevelObjective = String.Empty; this.maxSize = new DbSize(0, DbSize.SizeUnits.MB); @@ -165,17 +170,19 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin this.parameterSniffingForSecondary = DatabaseScopedConfigurationOnOff.Primary; this.queryOptimizerHotfixes = DatabaseScopedConfigurationOnOff.Off; this.queryOptimizerHotfixesForSecondary = DatabaseScopedConfigurationOnOff.Primary; + this.isLedger = false; //The following properties are introduced for contained databases. //In case of plain old databases, these values should reflect the server configuration values. this.defaultFulltextLanguageLcid = context.Server.Configuration.DefaultFullTextLanguage.ConfigValue; int defaultLanguagelangid = context.Server.Configuration.DefaultLanguage.ConfigValue; - this.defaultLanguageLcid = 1033; // LanguageUtils.GetLcidFromLangId(context.Server, defaultLanguagelangid); + this.defaultLanguageLcid = LanguageUtils.GetLcidFromLangId(context.Server, defaultLanguagelangid); this.nestedTriggersEnabled = context.Server.Configuration.NestedTriggers.ConfigValue == 1; this.transformNoiseWords = context.Server.Configuration.TransformNoiseWords.ConfigValue == 1; this.twoDigitYearCutoff = context.Server.Configuration.TwoDigitYearCutoff.ConfigValue; this.targetRecoveryTime = 0; + this.backupStorageRedundancy = string.Empty; ResourceManager manager = new ResourceManager("Microsoft.SqlTools.ServiceLayer.Localization.SR", typeof(DatabasePrototype).GetAssembly()); @@ -213,63 +220,27 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin this.fullTextIndexingEnabled = true; } - switch (context.SqlServerVersion) + var compatLevelInt = Enum.GetValues(typeof(CompatibilityLevel)).Cast().OrderBy(c => c).ToArray(); + // SqlServerVersion uses the Information.VersionMajor property which does get vbumped for Azure DB when it adds a new compat level + if (!compatLevelInt.Contains(context.SqlServerVersion * 10)) { - case 6: - - string errorMessage = manager.GetString("error_60compatibility"); - throw new InvalidOperationException(errorMessage); - - case 7: - - this.databaseCompatibilityLevel = CompatibilityLevel.Version70; - break; - - case 8: - - this.databaseCompatibilityLevel = CompatibilityLevel.Version80; - break; - - case 9: - - this.databaseCompatibilityLevel = CompatibilityLevel.Version90; - break; - - case 10: - - this.databaseCompatibilityLevel = CompatibilityLevel.Version100; - break; - - case 11: - - this.databaseCompatibilityLevel = CompatibilityLevel.Version110; - break; - - case 12: - - this.databaseCompatibilityLevel = CompatibilityLevel.Version120; - break; - - case 13: - this.databaseCompatibilityLevel = CompatibilityLevel.Version130; - break; - - case 14: - this.databaseCompatibilityLevel = CompatibilityLevel.Version140; - break; - - default: - this.databaseCompatibilityLevel = CompatibilityLevel.Version140; - break; + databaseCompatibilityLevel = (CompatibilityLevel)compatLevelInt[compatLevelInt.Length - 1]; + } + else + { + databaseCompatibilityLevel = (CompatibilityLevel)(context.SqlServerVersion * 10); } if (context.Server.ServerType == DatabaseEngineType.SqlAzureDatabase) - { //These properties are only available for Azure DBs - this.azureEdition = AzureEdition.Standard; - this.azureEditionDisplayValue = azureEdition.ToString(); - this.currentServiceLevelObjective = AzureSqlDbHelper.GetDefaultServiceObjective(this.azureEdition); - this.configuredServiceLevelObjective = AzureSqlDbHelper.GetDefaultServiceObjective(this.azureEdition); - this.maxSize = AzureSqlDbHelper.GetDatabaseDefaultSize(this.azureEdition); + { // These Properties have different expected defaults in Azure DBs + this.allowSnapshotIsolation = true; + this.isReadCommittedSnapshotOn = true; + this.encryptionEnabled = true; + //These properties are only available for Azure DBs + this.azureEdition = AzureEdition.GeneralPurpose; + this.currentServiceLevelObjective = this.configuredServiceLevelObjective = AzureSqlDbHelper.GetDefaultServiceObjective(this.azureEdition); + this.maxSize = AzureSqlDbHelper.GetDatabaseDefaultSize(this.azureEdition); + this.backupStorageRedundancy = AzureSqlDbHelper.GetDefaultBackupStorageRedundancy(this.azureEdition); } } @@ -280,7 +251,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin SELECT so.name as configured_slo_name, so2.name as current_slo_name FROM dbo.slo_database_objectives do INNER JOIN dbo.slo_service_objectives so ON do.configured_objective_id = so.objective_id - INNER JOIN dbo.slo_service_objectives so2 ON do.current_objective_id = so2.objective_id + INNER JOIN dbo.slo_service_objectives so2 ON do.current_objective_id = so2.objective_id WHERE do.database_id = @DbID "; @@ -300,18 +271,9 @@ WHERE do.database_id = @DbID isSystemDB = db.IsSystemObject; - ResourceManager manager = new ResourceManager("Microsoft.SqlTools.ServiceLayer.Localization.SR", typeof(DatabasePrototype).GetAssembly()); - - try - { - this.owner = db.Owner; - } - catch (Exception) - { - // TODO: fix the exception in SMO - this.owner = string.Empty; - } + ResourceManager manager = new ResourceManager("Microsoft.SqlServer.Management.SqlManagerUI.CreateDatabaseStrings", typeof(DatabasePrototype).Assembly); + this.owner = db.Owner; // Databases that are restored from other servers might not have valid owners. // If the logged in user is an administrator and the owner is not valid, show @@ -362,7 +324,7 @@ WHERE do.database_id = @DbID } else { - throw; + throw ex; } } @@ -375,7 +337,10 @@ WHERE do.database_id = @DbID { this.autoClose = db.DatabaseOptions.AutoClose; } - this.autoShrink = db.DatabaseOptions.AutoShrink; + if (db.IsSupportedProperty("AutoShrink")) + { + this.autoShrink = db.DatabaseOptions.AutoShrink; + } this.autoCreateStatistics = db.DatabaseOptions.AutoCreateStatistics; this.autoUpdateStatistics = db.DatabaseOptions.AutoUpdateStatistics; this.ansiNullDefault = db.DatabaseOptions.AnsiNullDefault; @@ -478,7 +443,7 @@ WHERE do.database_id = @DbID } this.varDecimalEnabled = - // db.IsVarDecimalStorageFormatSupported && + db.IsVarDecimalStorageFormatSupported && db.IsVarDecimalStorageFormatEnabled; // SQL Server 2008 options @@ -536,7 +501,8 @@ WHERE do.database_id = @DbID { string errorMessage = manager.GetString("error_60compatibility"); throw new InvalidOperationException(errorMessage); - } + } + this.databaseCompatibilityLevel = db.CompatibilityLevel; @@ -559,7 +525,7 @@ WHERE do.database_id = @DbID try { - if (db.IsSupportedProperty("IsMirroringEnabled") && db.IsMirroringEnabled) + if (db.IsMirroringEnabled) { this.mirrorSafetyLevel = db.MirroringSafetyLevel; this.witnessServer = db.MirroringWitness; @@ -574,7 +540,7 @@ WHERE do.database_id = @DbID } else { - throw; + throw ex; } } @@ -585,20 +551,25 @@ WHERE do.database_id = @DbID this.delayedDurability = db.DelayedDurability; } - //Only fill in the Azure properties when connected to an Azure server - if (context.Server.ServerType == DatabaseEngineType.SqlAzureDatabase - && context.Server.DatabaseEngineEdition != DatabaseEngineEdition.SqlOnDemand) + if (db.IsSupportedProperty("IsLedger")) { - this.azureEditionDisplayValue = db.AzureEdition; - AzureEdition edition; - if (Enum.TryParse(db.AzureEdition, true, out edition)) + this.isLedger = db.IsLedger; + } + + //Only fill in the Azure properties when connected to an Azure server + if (context.Server.ServerType == DatabaseEngineType.SqlAzureDatabase) + { + AzureEdition edition = AzureSqlDbHelper.AzureEditionFromString(db.AzureEdition); + if (edition != null) { this.azureEdition = edition; } else { - // Unknown Azure DB Edition so we can't set a value, leave as default Standard - // Note that this is likely a + //Unknown Azure DB Edition so we can't continue + System.Diagnostics.Debug.Fail(string.Format(CultureInfo.InvariantCulture, "Unknown Azure DB Edition {0}", + db.AzureEdition)); + throw new Exception("Error_UnknownAzureEdition"); } //Size is in MB, but if it's greater than a GB we want to display the size in GB @@ -611,9 +582,8 @@ WHERE do.database_id = @DbID { this.maxSize = new DbSize((int)db.Size, DbSize.SizeUnits.MB); } - this.GetServiceLevelObjectiveValues(context); - + this.backupStorageRedundancy = AzureSqlDbHelper.GetDefaultBackupStorageRedundancy(edition); } // Check if we support database scoped configurations on this server. Since these were all added at the same time, @@ -639,35 +609,11 @@ WHERE do.database_id = @DbID { Database db = context.Server.Databases[this.name]; - //For Azure v12 or later we can use SMO (the property doesn't exist prior to v12) - if (Utils.IsSql12OrLater(context.Server.Information.Version.Major)) - { - //Currently the only way to get the configured service level objective is to use the REST API. - //Since SSMS doesn't currently support that we'll leave it blank for now until support is - //added or T-SQL supports getting the configured SLO - this.configuredServiceLevelObjective = ""; - this.currentServiceLevelObjective = db.AzureServiceObjective; - } - else - { //If it's under v12 we need to query the master DB directly since that has the views containing the necessary information - using (var conn = new SqlConnection(context.Server.ConnectionContext.ConnectionString)) - { - using (var cmd = new SqlCommand(dbSloQuery, conn)) - { - cmd.Parameters.AddWithValue("@DbID", db.ID); - conn.Open(); - using (SqlDataReader reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - this.configuredServiceLevelObjective = reader["configured_slo_name"].ToString(); - this.currentServiceLevelObjective = reader["current_slo_name"].ToString(); - break; //Got our service level objective so we're done - } - } - } - } - } + //Currently the only way to get the configured service level objective is to use the REST API. + //Since SSMS doesn't currently support that we'll leave it blank for now until support is + //added or T-SQL supports getting the configured SLO + this.configuredServiceLevelObjective = ""; + this.currentServiceLevelObjective = db.AzureServiceObjective; } /// @@ -730,7 +676,6 @@ WHERE do.database_id = @DbID this.targetRecoveryTime = other.targetRecoveryTime; this.delayedDurability = other.delayedDurability; this.azureEdition = other.azureEdition; - this.azureEditionDisplayValue = other.azureEditionDisplayValue; this.configuredServiceLevelObjective = other.configuredServiceLevelObjective; this.currentServiceLevelObjective = other.currentServiceLevelObjective; this.legacyCardinalityEstimation = other.legacyCardinalityEstimation; @@ -742,6 +687,8 @@ WHERE do.database_id = @DbID this.queryOptimizerHotfixes = other.queryOptimizerHotfixes; this.queryOptimizerHotfixesForSecondary = other.queryOptimizerHotfixesForSecondary; this.maxSize = other.maxSize == null ? null : new DbSize(other.maxSize); + this.backupStorageRedundancy = other.backupStorageRedundancy; + this.isLedger = other.isLedger; } /// @@ -824,7 +771,9 @@ WHERE do.database_id = @DbID (this.queryOptimizerHotfixes == other.queryOptimizerHotfixes) && (this.queryOptimizerHotfixesForSecondary == other.queryOptimizerHotfixesForSecondary) && (this.queryStoreEnabled == other.queryStoreEnabled) && - (this.maxSize == other.maxSize); + (this.maxSize == other.maxSize) && + (this.backupStorageRedundancy == other.backupStorageRedundancy) && + (this.isLedger == other.isLedger); return result; } @@ -848,6 +797,7 @@ WHERE do.database_id = @DbID protected ServerVersion serverVersion; protected Microsoft.SqlServer.Management.Common.DatabaseEngineType databaseEngineType; private bool isFilestreamEnabled; + private bool isFileGroupAutogrowthEnabled; private event EventHandler observableChanged; private bool allowNotifications = true; @@ -864,11 +814,16 @@ WHERE do.database_id = @DbID /// /// Whether or not the UI should show File Groups /// + [Browsable(false)] public virtual bool HideFileSettings { - get { return false; } + get + { + return this.context.Server.DatabaseEngineEdition == DatabaseEngineEdition.SqlOnDemand; + } } + [Browsable(false)] public virtual bool AllowScripting { get { return true; } @@ -971,6 +926,7 @@ WHERE do.database_id = @DbID } set { + System.Diagnostics.Debug.Assert(this.IsCollationSupported, "changing collation is not supported on this server"); this.currentState.collation = value; this.NotifyObservers(); } @@ -1017,6 +973,8 @@ WHERE do.database_id = @DbID } + System.Diagnostics.Debug.Assert(result != null && result.Length != 0, "didn't get string for restrictAccess value"); + return result; } set @@ -1052,7 +1010,7 @@ WHERE do.database_id = @DbID { string result = null; ResourceManager manager = new ResourceManager("Microsoft.SqlTools.ServiceLayer.Localization.SR", typeof(DatabasePrototype).GetAssembly()); - + if ((this.currentState.databaseState & DatabaseStatus.Normal) != 0) { result = this.AppendState(result, manager.GetString("prototype_db_prop_databaseState_value_normal")); @@ -1173,13 +1131,15 @@ WHERE do.database_id = @DbID break; } + System.Diagnostics.Debug.Assert(result != null && result.Length != 0, "no string found for defaultCursor value"); + return result; } set { ResourceManager manager = new ResourceManager("Microsoft.SqlTools.ServiceLayer.Localization.SR", typeof(DatabasePrototype).GetAssembly()); - + if (value == manager.GetString("prototype_db_prop_defaultCursor_value_local")) { this.currentState.defaultCursor = DefaultCursor.Local; @@ -1593,6 +1553,7 @@ WHERE do.database_id = @DbID } set { + System.Diagnostics.Debug.Assert(this.Exists, "can not set safety level witness for new database"); currentState.mirrorSafetyLevel = value; this.NotifyObservers(); } @@ -1610,6 +1571,7 @@ WHERE do.database_id = @DbID } set { + System.Diagnostics.Debug.Assert(this.Exists, "can not set Mirror witness for new database"); currentState.witnessServer = value; this.NotifyObservers(); } @@ -1659,21 +1621,44 @@ WHERE do.database_id = @DbID } } + /// + /// Whether Filegroup autogrow all files feature is enabled or not. + /// + [Browsable(false)] + public bool IsFileGroupAutogrowthEnabled + { + get + { + return this.isFileGroupAutogrowthEnabled; + } + } + + /// + /// The name of the azure database edition + /// + [Browsable(false)] + public AzureEdition OriginalAzureEditionName + { + get + { + return this.originalState.azureEdition; + } + } #endregion private StringCollection GetDatabaseDefaultInitFields(Server server) { - StringCollection databaseDefaultInitFields; - if (context.IsNewObject) - { - databaseDefaultInitFields = server.GetPropertyNames(typeof(Database), server.DatabaseEngineEdition); - } - else - { - string databaseName = context.GetDocumentPropertyString("database"); - databaseDefaultInitFields = server.GetPropertyNames(typeof(Database), this.context.Server.Databases[databaseName].DatabaseEngineEdition); - } + StringCollection databaseDefaultInitFields; + if (context.IsNewObject) + { + databaseDefaultInitFields = server.GetPropertyNames(typeof(Database), server.DatabaseEngineEdition); + } + else + { + string databaseName = context.GetDocumentPropertyString("database"); + databaseDefaultInitFields = server.GetPropertyNames(typeof(Database), this.context.Server.Databases[databaseName].DatabaseEngineEdition); + } //AvailabilityGroupName throws exception for Contained Authentication //and at the same time is not required in the Database Properties UI. @@ -1727,6 +1712,8 @@ WHERE do.database_id = @DbID /// public DatabasePrototype(CDataContainer context) { + System.Diagnostics.Debug.Assert(context != null, "unexpected null server"); + this.context = context; this.serverVersion = context.Server.ConnectionContext.ServerVersion; this.databaseEngineType = context.Server.DatabaseEngineType; @@ -1736,7 +1723,8 @@ WHERE do.database_id = @DbID this.removedFilegroups = new List(); this.removedFiles = new List(); this.numberOfLogFiles = 0; - this.EditionToCreate = DatabaseEngineEdition.Unknown; + this.EditionToCreate = GetDefaultDatabaseEngineEdition(this.context.Server); + this.isFileGroupAutogrowthEnabled = FileGroupAutogrowthEnabled(this.context.Server); StringCollection databaseDefaultInitFields = this.GetDatabaseDefaultInitFields(this.context.Server); context.Server.SetDefaultInitFields(typeof(Database), databaseDefaultInitFields); @@ -1781,9 +1769,9 @@ WHERE do.database_id = @DbID this.originalState.name = String.Empty; this.currentState = this.originalState.Clone(); this.existingDatabase = false; - this.currentState = this.originalState.Clone(); + this.currentState = this.originalState.Clone(); //this value should set to false(it is true when it gets here due model db) - this.originalState.isSystemDB = false; + this.originalState.isSystemDB = false; } } @@ -1831,230 +1819,225 @@ WHERE do.database_id = @DbID /// The SMO database object that was created or modified public virtual Database ApplyChanges() { - Database db = null; + Database db = null; - if (this.ChangesExist()) - { - bool scripting = (SqlExecutionModes.CaptureSql == this.context.Server.ConnectionContext.SqlExecutionModes); - bool mustRollback = false; + if (this.ChangesExist()) + { + bool scripting = (SqlExecutionModes.CaptureSql == this.context.Server.ConnectionContext.SqlExecutionModes); + bool mustRollback = false; - db = this.GetDatabase(); + db = this.GetDatabase(); - // Other connections will need to be closed if the following is true - // 1) The database already exists, AND - // 2) We are not scripting, AND - // a) read-only state is changing, OR - // b) user-access is changing, OR - // c) date correlation optimization is changing + // Other connections will need to be closed if the following is true + // 1) The database already exists, AND + // 2) We are not scripting, AND + // a) read-only state is changing, OR + // b) user-access is changing, OR + // c) date correlation optimization is changing - // There are also additional properties we don't currently expose that also need - // to be changed when no one else is connected: + // There are also additional properties we don't currently expose that also need + // to be changed when no one else is connected: - // d) emergency, OR - // e) offline, (moving to offline - obviously not necessary to check when moving from offline) - // f) read committed snapshot + // d) emergency, OR + // e) offline, (moving to offline - obviously not necessary to check when moving from offline) + // f) read committed snapshot - if (this.Exists && !scripting && - ((this.currentState.isReadOnly != this.originalState.isReadOnly) || - (this.currentState.filestreamDirectoryName != this.originalState.filestreamDirectoryName) || - (this.currentState.filestreamNonTransactedAccess != this.originalState.filestreamNonTransactedAccess) || - (this.currentState.restrictAccess != this.originalState.restrictAccess) || - (this.currentState.dateCorrelationOptimization != this.originalState.dateCorrelationOptimization) || - (this.currentState.isReadCommittedSnapshotOn != this.originalState.isReadCommittedSnapshotOn))) - { + if (this.Exists && !scripting && + ((this.currentState.isReadOnly != this.originalState.isReadOnly) || + (this.currentState.filestreamDirectoryName != this.originalState.filestreamDirectoryName) || + (this.currentState.filestreamNonTransactedAccess != this.originalState.filestreamNonTransactedAccess) || + (this.currentState.restrictAccess != this.originalState.restrictAccess) || + (this.currentState.dateCorrelationOptimization != this.originalState.dateCorrelationOptimization) || + (this.currentState.isReadCommittedSnapshotOn != this.originalState.isReadCommittedSnapshotOn))) + { - // If the user lacks permissions to enumerate other connections (e.g. the user is not SA) - // assume there is a connection to close. This occasionally results in unnecessary - // prompts, but the database alter does succeed this way. If we assume no other connections, - // then we get errors when other connections do exist. - int numberOfOpenConnections = 1; + // If the user lacks permissions to enumerate other connections (e.g. the user is not SA) + // assume there is a connection to close. This occasionally results in unnecessary + // prompts, but the database alter does succeed this way. If we assume no other connections, + // then we get errors when other connections do exist. + int numberOfOpenConnections = 1; - try - { - numberOfOpenConnections = db.ActiveConnections; - } - catch (Exception) - { - // do nothing - the user doesn't have permission to check whether there are active connections - //STrace.LogExCatch(ex); - } + try + { + numberOfOpenConnections = db.ActiveConnections; + } + catch (Exception ex) + { + // do nothing - the user doesn't have permission to check whether there are active connections + System.Diagnostics.Trace.TraceError(ex.Message); + } - if (0 < numberOfOpenConnections) - { - // DialogResult result = (DialogResult)marshallingControl.Invoke(new SimplePrompt(this.PromptToCloseConnections)); - // if (result == DialogResult.No) - // { - // throw new OperationCanceledException(); - // } + if (0 < numberOfOpenConnections) + { mustRollback = true; - throw new OperationCanceledException(); - } - } + } + } - // create/alter filegroups - foreach (FilegroupPrototype filegroup in Filegroups) - { - filegroup.ApplyChanges(db); - } + // create/alter filegroups + foreach (FilegroupPrototype filegroup in Filegroups) + { + filegroup.ApplyChanges(db); + } - // create/alter files - foreach (DatabaseFilePrototype file in Files) - { - file.ApplyChanges(db); - } + // create/alter files + foreach (DatabaseFilePrototype file in Files) + { + file.ApplyChanges(db); + } - // set the database properties - this.SaveProperties(db); + // set the database properties + this.SaveProperties(db); - // alter the database to match the properties - if (!this.Exists) - { - // this is to prevent silent creation of db behind users back - // eg. the alter statements to set properties fail when filestream directory name is invalid bug #635273 - // but create database statement already succeeded + // alter the database to match the properties + if (!this.Exists) + { + // this is to prevent silent creation of db behind users back + // eg. the alter statements to set properties fail when filestream directory name is invalid bug #635273 + // but create database statement already succeeded - // if filestream directory name has been set by user validate it - if (!string.IsNullOrEmpty(this.FilestreamDirectoryName)) - { - // check is filestream directory name is valid - if (!FileNameHelper.IsValidFilename(this.FilestreamDirectoryName)) - { - string message = String.Format(System.Globalization.CultureInfo.InvariantCulture, + // if filestream directory name has been set by user validate it + if (!string.IsNullOrEmpty(this.FilestreamDirectoryName)) + { + // check is filestream directory name is valid + if (!FileNameHelper.IsValidFilename(this.FilestreamDirectoryName)) + { + string message = String.Format(System.Globalization.CultureInfo.InvariantCulture, SR.Error_InvalidDirectoryName, - this.FilestreamDirectoryName); - throw new ArgumentException(message); - } + this.FilestreamDirectoryName); + throw new ArgumentException(message); + } - int rowCount = 0; - try - { + int rowCount = 0; + try + { - //if filestream directory name already exists in this instance - string sqlFilestreamQuery = string.Format(System.Globalization.CultureInfo.InvariantCulture, - "SELECT * from sys.database_filestream_options WHERE directory_name = {0}", - SqlSmoObject.MakeSqlString(this.FilestreamDirectoryName)); - DataSet filestreamResults = this.context.ServerConnection.ExecuteWithResults(sqlFilestreamQuery); - rowCount = filestreamResults.Tables[0].Rows.Count; - } - catch - { - // lets not do anything if there is an exception while validating - // this is will prevent bugs in validation logic from preventing creation of valid databases - // if database settings are invalid create database tsql statement will fail anyways - } - if (rowCount != 0) - { - string message = String.Format(System.Globalization.CultureInfo.InvariantCulture, + //if filestream directory name already exists in this instance + string sqlFilestreamQuery = string.Format(SmoApplication.DefaultCulture, "SELECT * from sys.database_filestream_options WHERE directory_name = {0}", + SqlSmoObject.MakeSqlString(this.FilestreamDirectoryName)); + DataSet filestreamResults = this.context.ServerConnection.ExecuteWithResults(sqlFilestreamQuery); + rowCount = filestreamResults.Tables[0].Rows.Count; + } + catch + { + // lets not do anything if there is an exception while validating + // this is will prevent bugs in validation logic from preventing creation of valid databases + // if database settings are invalid create database tsql statement will fail anyways + } + if (rowCount != 0) + { + string message = String.Format(System.Globalization.CultureInfo.InvariantCulture, SR.Error_ExistingDirectoryName, - this.FilestreamDirectoryName, this.Name); - throw new ArgumentException(message); + this.FilestreamDirectoryName, this.Name); + throw new ArgumentException(message); - } - } + } + } - db.Create(); - } - else - { - TerminationClause termination = - mustRollback ? - TerminationClause.RollbackTransactionsImmediately : - TerminationClause.FailOnOpenTransactions; + db.Create(); + } + else + { + TerminationClause termination = + mustRollback ? + TerminationClause.RollbackTransactionsImmediately : + TerminationClause.FailOnOpenTransactions; - db.Alter(termination); - } + db.Alter(termination); + } - // have to explicitly set the default filegroup after the database has been created - foreach (FilegroupPrototype filegroup in Filegroups) - { - if (filegroup.IsDefault && !(filegroup.Exists && db.FileGroups[filegroup.Name].IsDefault)) - { - if ((filegroup.IsFileStream || filegroup.IsMemoryOptimized)) - { - db.SetDefaultFileStreamFileGroup(filegroup.Name); - } - else - { - db.SetDefaultFileGroup(filegroup.Name); - } - } - } + // have to explicitly set the default filegroup after the database has been created + // Also bug 97696 + foreach (FilegroupPrototype filegroup in Filegroups) + { + if (filegroup.IsDefault && !(filegroup.Exists && db.FileGroups[filegroup.Name].IsDefault)) + { + if ((filegroup.IsFileStream || filegroup.IsMemoryOptimized) && + Utils.IsKatmaiOrLater(db.ServerVersion.Major)) + { + db.SetDefaultFileStreamFileGroup(filegroup.Name); + } + else + { + db.SetDefaultFileGroup(filegroup.Name); + } + } + } - FilegroupPrototype fg = null; - // drop should happen after alter so that if we delete default filegroup it makes another default before deleting. - // drop removed files and filegroups for existing databases - if (this.Exists) - { - foreach (FilegroupPrototype filegroup in this.removedFilegroups) - { - // In case all filegroups are removed from filestream . memory optimized one default will remain and that has to be the last. - if ((filegroup.IsFileStream || filegroup.IsMemoryOptimized) && - db.FileGroups[filegroup.Name].IsDefault) - { - fg = filegroup; - } - else - { - filegroup.ApplyChanges(db); - } - } + FilegroupPrototype fg = null; + // drop should happen after alter so that if we delete default filegroup it makes another default before deleting. + // drop removed files and filegroups for existing databases + if (this.Exists) + { + foreach (FilegroupPrototype filegroup in this.removedFilegroups) + { + // In case all filegroups are removed from filestream . memory optimized one default will remain and that has to be the last. + if ((filegroup.IsFileStream || filegroup.IsMemoryOptimized) && + db.FileGroups[filegroup.Name].IsDefault) + { + fg = filegroup; + } + else + { + filegroup.ApplyChanges(db); + } + } - if (fg != null) - { - fg.ApplyChanges(db); - } + if (fg != null) + { + fg.ApplyChanges(db); + } - foreach (DatabaseFilePrototype file in this.removedFiles) - { - file.ApplyChanges(db); - } - } + foreach (DatabaseFilePrototype file in this.removedFiles) + { + file.ApplyChanges(db); + } + } - // SnapshotIsolation and Owner cannot be set during scripting time for a newly creating database - // and even in capture mode. Hence this check has been made - if (db.State == SqlSmoState.Existing) - { - if (this.originalState.allowSnapshotIsolation != this.currentState.allowSnapshotIsolation) - { - db.SetSnapshotIsolation(this.currentState.allowSnapshotIsolation); - } + // SnapshotIsolation and Owner cannot be set during scripting time for a newly creating database + // and even in capture mode. Hence this check has been made + if (db.State == SqlSmoState.Existing) + { + if (this.originalState.allowSnapshotIsolation != this.currentState.allowSnapshotIsolation) + { + db.SetSnapshotIsolation(this.currentState.allowSnapshotIsolation); + } - // Set the database owner. Note that setting owner is an "immediate" operation that - // has to happen after the database is created. There is a SMO limitation where SMO - // throws an exception if immediate operations such as SetOwner() are attempted on - // an object that doesn't exist on the server. + // Set the database owner. Note that setting owner is an "immediate" operation that + // has to happen after the database is created. There is a SMO limitation where SMO + // throws an exception if immediate operations such as SetOwner() are attempted on + // an object that doesn't exist on the server. - if ((this.Owner.Length != 0) && - (this.currentState.owner != this.originalState.owner)) - { - // - // bug 20000092 says the error message is confusing if this fails, so - // wrap this and throw a nicer error on failure. - // - try - { - db.SetOwner(this.Owner, false); - } - catch (Exception ex) - { - SqlException sqlException = CUtils.GetSqlException(ex); + if ((this.Owner.Length != 0) && + (this.currentState.owner != this.originalState.owner)) + { + // + // bug 20000092 says the error message is confusing if this fails, so + // wrap this and throw a nicer error on failure. + // + try + { + db.SetOwner(this.Owner, false); + } + catch (Exception ex) + { + SqlException sqlException = CUtils.GetSqlException(ex); - if ((null != sqlException) && CUtils.IsPermissionDeniedException(sqlException)) - { - - throw new Exception(SR.SetOwnerFailed(this.Owner) + ex.ToString()); - } - else - { - throw; - } - } - } - } - } + if ((null != sqlException) && CUtils.IsPermissionDeniedException(sqlException)) + { + System.Diagnostics.Trace.TraceError(ex.Message); + throw new Exception(SR.SetOwnerFailed(this.Owner) + ex.ToString()); + } + else + { + throw; + } + } + } + } + } - return db; + return db; } /// @@ -2064,7 +2047,7 @@ WHERE do.database_id = @DbID { Database db = this.GetDatabase(); - // db.QueryStoreOptions.PurgeQueryStoreData(); + db.QueryStoreOptions.PurgeQueryStoreData(); } /// @@ -2136,6 +2119,7 @@ WHERE do.database_id = @DbID if (filegroup.IsDefault) { FilegroupPrototype primary = this.filegroups[0]; + System.Diagnostics.Debug.Assert(primary.Name == "PRIMARY", "PRIMARY filegroup is not first in list"); primary.IsDefault = true; } @@ -2179,11 +2163,13 @@ WHERE do.database_id = @DbID { if (file.IsPrimaryFile) { + System.Diagnostics.Debug.Fail("unexpected removal of the primary data file"); throw new InvalidOperationException("unexpected removal of the primary data file"); } if ((1 == this.numberOfLogFiles) && (FileType.Log == file.DatabaseFileType)) { + System.Diagnostics.Debug.Fail("Unexpected removal of the last log file."); throw new InvalidOperationException("Unexpected removal of the last log file."); } @@ -2216,8 +2202,8 @@ WHERE do.database_id = @DbID this.removedFiles.Clear(); this.removedFilegroups.Clear(); - //Azure doesn't support files/filegroups so just exit early after clearing the current settings - if (this.context.Server.ServerType == DatabaseEngineType.SqlAzureDatabase) + //Azure and SqlOnDemand don't support files/filegroups so just exit early after clearing the current settings + if (this.context.Server.ServerType == DatabaseEngineType.SqlAzureDatabase || this.context.Server.DatabaseEngineEdition == DatabaseEngineEdition.SqlOnDemand) { return; } @@ -2225,12 +2211,13 @@ WHERE do.database_id = @DbID Database database = context.Server.Databases[this.Name]; foreach (FileGroup filegroup in database.FileGroups) { - FilegroupPrototype filegroupPrototype = new FilegroupPrototype(this, - filegroup.Name, - filegroup.ReadOnly, - filegroup.IsDefault, - filegroup.FileGroupType, - true); + FilegroupPrototype filegroupPrototype = new FilegroupPrototype(parent: this, + name: filegroup.Name, + isReadOnly: filegroup.ReadOnly, + isDefault: filegroup.IsDefault, + filegroupType: filegroup.FileGroupType, + exists: true, + isAutogrowAllFiles: filegroup.IsSupportedProperty("AutogrowAllFiles") ? filegroup.AutogrowAllFiles : false); this.Add(filegroupPrototype); @@ -2242,10 +2229,10 @@ WHERE do.database_id = @DbID this.Add(file); } } - catch (ExecutionFailureException) + catch (ExecutionFailureException ex) { // do nothing - + System.Diagnostics.Trace.TraceError(ex.Message); } } @@ -2279,18 +2266,16 @@ WHERE do.database_id = @DbID { try { - if (this.context.Server.DatabaseEngineEdition != DatabaseEngineEdition.SqlOnDemand) + foreach (LogFile logfile in database.LogFiles) { - foreach (LogFile logfile in database.LogFiles) - { - DatabaseFilePrototype logfilePrototype = new DatabaseFilePrototype(this, logfile); - this.Add(logfilePrototype); - } + DatabaseFilePrototype logfilePrototype = new DatabaseFilePrototype(this, logfile); + this.Add(logfilePrototype); } } - catch (ExecutionFailureException) + catch (ExecutionFailureException ex) { // do nothing + System.Diagnostics.Trace.TraceError(ex.Message); } } @@ -2331,9 +2316,12 @@ WHERE do.database_id = @DbID } } - if (!this.Exists || (db.DatabaseOptions.AutoShrink != this.AutoShrink)) + if (db.IsSupportedProperty("AutoShrink")) { - db.DatabaseOptions.AutoShrink = this.AutoShrink; + if (!this.Exists || (db.DatabaseOptions.AutoShrink != this.AutoShrink)) + { + db.DatabaseOptions.AutoShrink = this.AutoShrink; + } } if (!this.Exists || (db.DatabaseOptions.AutoCreateStatistics != this.AutoCreateStatistics)) @@ -2397,25 +2385,21 @@ WHERE do.database_id = @DbID } } - // $FUTURE: 6/25/2004-stevetw Consider moving mirroring property sets - // to a Yukon-specific subclass - if (db.IsSupportedProperty("IsMirroringEnabled")) - { - if (this.Exists && db.IsMirroringEnabled && (db.MirroringSafetyLevel != MirrorSafetyLevel)) - { - db.MirroringSafetyLevel = this.MirrorSafetyLevel; - } - if (this.Exists && db.IsMirroringEnabled && (string.Compare(db.MirroringWitness, this.MirrorWitness, StringComparison.OrdinalIgnoreCase) != 0)) + if (this.Exists && db.IsMirroringEnabled && (db.MirroringSafetyLevel != MirrorSafetyLevel)) + { + db.MirroringSafetyLevel = this.MirrorSafetyLevel; + } + + if (this.Exists && db.IsMirroringEnabled && (string.Compare(db.MirroringWitness, this.MirrorWitness, StringComparison.OrdinalIgnoreCase) != 0)) + { + if (this.MirrorWitness.Length == 0) // we want to remove it { - if (this.MirrorWitness.Length == 0) // we want to remove it - { - db.ChangeMirroringState(MirroringOption.RemoveWitness); - } - else - { - db.MirroringWitness = this.MirrorWitness; - } + db.ChangeMirroringState(MirroringOption.RemoveWitness); + } + else + { + db.MirroringWitness = this.MirrorWitness; } } @@ -2621,6 +2605,48 @@ WHERE do.database_id = @DbID return result; } + /// + /// Checks whether Filegroup autogrow all files feature is enabled on the server + /// + /// The server object to check against + /// True if the Filegroup autogrow all files is enabled on the server, false otherwise. + private bool FileGroupAutogrowthEnabled(Server svr) + { + bool result = false; + if (svr != null) + { + // Modifying Filegroup Autogrow property is only allowed for SQL 2016 and above, + // and is not permitted for Azure Synapse Analytics and Azure SQL DB. + if (svr.Information.Version.Major >= 13 + && svr.ServerType != DatabaseEngineType.SqlAzureDatabase) + { + result = true; + } + } + return result; + } + + /// + /// Set target database engine edition to Unknown (default behaviour). + /// For Managed Servers, set it explicitly to avoid + /// scripting unsupported stuff + /// /// + /// The server object to check against + /// Desired engine edition + private DatabaseEngineEdition GetDefaultDatabaseEngineEdition(Server svr) + { + if (svr != null && (svr.DatabaseEngineEdition == DatabaseEngineEdition.SqlManagedInstance || + svr.DatabaseEngineEdition == DatabaseEngineEdition.SqlOnDemand || + svr.DatabaseEngineEdition == DatabaseEngineEdition.SqlAzureArcManagedInstance)) + { + return svr.DatabaseEngineEdition; + } + else + { + return DatabaseEngineEdition.Unknown; + } + } + protected Database GetDatabase() { Database result = null; @@ -2629,6 +2655,8 @@ WHERE do.database_id = @DbID if (this.Exists) { result = this.context.Server.Databases[this.originalState.name]; + + System.Diagnostics.Debug.Assert(0 == String.Compare(this.originalState.name, this.currentState.name, StringComparison.Ordinal), "name of existing database has changed"); if (result == null) { throw new Exception("Object does not exist"); @@ -2636,9 +2664,9 @@ WHERE do.database_id = @DbID } else { - result = new Database(this.context.Server, this.Name, this.EditionToCreate); + result = new Database(this.context.Server, this.Name, this.EditionToCreate); } - + return result; } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype100.cs b/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype100.cs index 13b3936f..1a0f570e 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype100.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype100.cs @@ -273,6 +273,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin break; } + System.Diagnostics.Debug.Assert(result != null && result.Length != 0, "no string found for database scoped configuration value"); + return result; } @@ -291,11 +293,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin return DatabaseScopedConfigurationOnOff.Off; } else if (displayText == manager.GetString("prototype_db_prop_databasescopedconfig_value_on") || !forSecondary) - { + { return DatabaseScopedConfigurationOnOff.On; } else - { + { return DatabaseScopedConfigurationOnOff.Primary; } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype110.cs b/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype110.cs index 09d95aaa..768262d9 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype110.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype110.cs @@ -9,6 +9,8 @@ using System.ComponentModel; using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlServer.Management.Sdk.Sfc; using Microsoft.SqlTools.ServiceLayer.Management; +using Microsoft.SqlTools.ServiceLayer.Utility; +using Microsoft.SqlServer.Management.Common; namespace Microsoft.SqlTools.ServiceLayer.Admin { @@ -50,6 +52,20 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin } } + public LanguageChoice DefaultLanguage + { + get + { + return LanguageUtils.GetLanguageChoiceAlias(this.context.Server, + this.currentState.defaultLanguageLcid); + } + set + { + this.currentState.defaultLanguageLcid = value.lcid; + this.NotifyObservers(); + } + } + [Category("Category_ContainedDatabases"), DisplayNameAttribute("Property_NestedTriggersEnabled")] public bool NestedTriggersEnabled @@ -152,6 +168,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin db.DefaultFullTextLanguage.Lcid = this.DefaultFullTextLanguageLcid; } + if (!this.Exists || (db.DefaultLanguage.Lcid != this.DefaultLanguage.lcid)) + { + db.DefaultLanguage.Lcid = this.DefaultLanguage.lcid; + } + if (!this.Exists || (db.NestedTriggersEnabled != this.NestedTriggersEnabled)) { db.NestedTriggersEnabled = this.NestedTriggersEnabled; @@ -185,5 +206,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin } } } + + int Lcid + { + get { return this.DefaultLanguage.lcid; } + } + + ServerConnection Connection + { + get { return this.context.ServerConnection; } + } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype140.cs b/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype140.cs new file mode 100644 index 00000000..7b587f24 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype140.cs @@ -0,0 +1,77 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + + +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlTools.ServiceLayer.Management; +using System; +using System.ComponentModel; + +namespace Microsoft.SqlTools.ServiceLayer.Admin +{ + /// + /// Database properties for SqlServer 2017 + /// + internal class DatabasePrototype140 : DatabasePrototype110 + { + /// + /// Database properties for SqlServer 2017 class constructor + /// + public DatabasePrototype140(CDataContainer context) + : base(context) + { + } + + /// + /// Whether or not the UI should show File Groups + /// + public override bool HideFileSettings + { + get + { + return (this.context != null && this.context.Server != null && (this.context.Server.DatabaseEngineEdition == DatabaseEngineEdition.SqlManagedInstance || this.context.Server.DatabaseEngineEdition == DatabaseEngineEdition.SqlOnDemand)); + } + } + + /// + /// The recovery model for the database + /// + [Browsable(false)] + public override RecoveryModel RecoveryModel + { + get + { + if (this.context != null && + this.context.Server != null && + this.context.Server.DatabaseEngineEdition == DatabaseEngineEdition.SqlManagedInstance) + { + return RecoveryModel.Full; + } + else + { + return this.currentState.recoveryModel; + } + } + set + { + if (this.context != null && + this.context.Server != null && + this.context.Server.DatabaseEngineEdition == DatabaseEngineEdition.SqlManagedInstance && + value != RecoveryModel.Full) + { + System.Diagnostics.Debug.Assert(false, "Managed Instance supports only FULL recovery model!"); + throw new ArgumentException("Managed Instance supports only FULL recovery model!"); + } + else + { + base.RecoveryModel = value; + } + } + } + } +} + + diff --git a/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype160.cs b/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype160.cs new file mode 100644 index 00000000..276979ad --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype160.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 Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlTools.ServiceLayer.Management; +using System.ComponentModel; + +namespace Microsoft.SqlTools.ServiceLayer.Admin +{ + /// + /// Database properties for SqlServer 2022 + /// + internal class DatabasePrototype160 : DatabasePrototype140 + { + /// + /// Database properties for SqlServer 2022 class constructor + /// + public DatabasePrototype160(CDataContainer context) + : base(context) + { + } + + [Category("Category_Ledger"), + DisplayNameAttribute("Property_IsLedgerDatabase")] + public bool IsLedger + { + get { + return this.currentState.isLedger; + } + set + { + this.currentState.isLedger = value; + this.NotifyObservers(); + } + } + + protected override void SaveProperties(Database db) + { + base.SaveProperties(db); + + if (db.IsSupportedProperty("IsLedger")) + { + // Ledger can only be set on a new database, it is read-only after creation + if (!this.Exists) + { + db.IsLedger = this.IsLedger; + } + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype90.cs b/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype90.cs index a5647bef..6fffe1ad 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype90.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototype90.cs @@ -84,7 +84,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin this.currentState.pageVerify = PageVerify.None; } else - { + { this.currentState.pageVerify = PageVerify.TornPageDetection; } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototypeAzure.cs b/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototypeAzure.cs index 2d964e99..77146dd4 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototypeAzure.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Admin/Database/DatabasePrototypeAzure.cs @@ -11,28 +11,29 @@ using Microsoft.Data.SqlClient; using System.Globalization; using System.Linq; using Microsoft.SqlServer.Management.Common; -using Microsoft.SqlServer.Management.Sdk.Sfc; using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlTools.ServiceLayer.Management; using AzureEdition = Microsoft.SqlTools.ServiceLayer.Admin.AzureSqlDbHelper.AzureEdition; +using System; +using System.Data; namespace Microsoft.SqlTools.ServiceLayer.Admin { /// /// Database properties for SQL Azure DB. - /// Business/Web editions are up to compat level 100 now /// - [TypeConverter(typeof(DynamicValueTypeConverter))] - internal class DatabasePrototypeAzure : DatabasePrototype100 + internal class DatabasePrototypeAzure : DatabasePrototype160 { #region Constants public const string Category_Azure = "Category_Azure"; + public const string Category_Azure_BRS = "Category_Azure_BRS"; public const string Property_AzureMaxSize = "Property_AzureMaxSize"; public const string Property_AzureCurrentServiceLevelObjective = "Property_AzureCurrentServiceLevelObjective"; public const string Property_AzureConfiguredServiceLevelObjective = "Property_AzureConfiguredServiceLevelObjective"; public const string Property_AzureEdition = "Property_AzureEdition"; + public const string Property_AzureBackupStorageRedundancy = "Property_AzureBackupStorageRedundancy"; #endregion Constants public DatabasePrototypeAzure(CDataContainer context, DatabaseEngineEdition editionToCreate = DatabaseEngineEdition.SqlDatabase) @@ -42,9 +43,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin } #region Properties - - [Category(Category_Azure), - DisplayNameAttribute(Property_AzureMaxSize)] + public string MaxSize { get @@ -53,13 +52,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin } set { - this.currentState.maxSize = DbSize.ParseDbSize(value); + this.currentState.maxSize = string.IsNullOrEmpty(value) ? null : DbSize.ParseDbSize(value); this.NotifyObservers(); } } - [Category(Category_Azure), - DisplayNameAttribute(Property_AzureCurrentServiceLevelObjective)] public string CurrentServiceLevelObjective { get @@ -68,23 +65,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin } set { + if (value != null && value.Contains('\'')) + { + throw new ArgumentException("Error_InvalidServiceLevelObjective"); + } this.currentState.currentServiceLevelObjective = value; this.NotifyObservers(); } } - [Category(Category_Azure), - DisplayNameAttribute(Property_AzureConfiguredServiceLevelObjective)] - public string ConfiguredServiceLevelObjective - { - //This value is read only because it's changed by changing the current SLO, - //we just expose this to show if the DB is currently transitioning - get - { - return this.currentState.configuredServiceLevelObjective; - } - } - [Browsable(false)] public AzureEdition AzureEdition { @@ -94,35 +83,79 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin } } - [Category(Category_Azure), - DisplayNameAttribute(Property_AzureEdition)] //We have a separate property here so that the AzureEdition enum value is still exposed //(This property is for the name displayed in the drop down menu, which needs to be a string for casting purposes) public string AzureEditionDisplay { get { - return this.currentState.azureEditionDisplayValue; + return AzureSqlDbHelper.GetAzureEditionDisplayName(this.currentState.azureEdition); } - set - { - // TODO set from here should probably allow for the fact that System is a valid edition for - // actual system DBs. Not handling for now - AzureEdition edition; - if (AzureSqlDbHelper.TryGetAzureEditionFromDisplayName(value, out edition)) - { - if (edition == this.currentState.azureEdition) - { //No changes, return early since we don't need to do any of the changes below - return; - } + // set + // { + // AzureEdition edition; + // if (AzureSqlDbHelper.TryGetAzureEditionFromDisplayName(value, out edition)) + // { + // //Try to get the ServiceLevelObjective from the api,if not the default hardcoded service level objectives will be retrieved. + // string serverLevelObjective = AzureServiceLevelObjectiveProvider.TryGetAzureServiceLevelObjective(value, AzureServiceLocation); - this.currentState.azureEdition = edition; - this.currentState.azureEditionDisplayValue = value; - this.CurrentServiceLevelObjective = AzureSqlDbHelper.GetDefaultServiceObjective(edition); - this.MaxSize = AzureSqlDbHelper.GetDatabaseDefaultSize(edition).ToString(); - this.NotifyObservers(); - } - } + // if (!string.IsNullOrEmpty(serverLevelObjective)) + // { + // this.currentState.azureEdition = edition; + // this.currentState.currentServiceLevelObjective = serverLevelObjective; + // // Instead of creating db instance with default Edition, update EditionToCreate while selecting Edition from the UI. + // this.EditionToCreate = MapAzureEditionToDbEngineEdition(edition); + // string storageAccountType = AzureServiceLevelObjectiveProvider.TryGetAzureStorageType(value, AzureServiceLocation); + // if (!string.IsNullOrEmpty(storageAccountType)) + // { + // this.currentState.backupStorageRedundancy = storageAccountType; + // } + + // // Try to get the azure maxsize from the api,if not the default hardcoded maxsize will be retrieved. + // DbSize dbSize = AzureServiceLevelObjectiveProvider.TryGetAzureMaxSize(value, serverLevelObjective, AzureServiceLocation); + // if (!string.IsNullOrEmpty(dbSize.ToString())) + // { + // this.currentState.maxSize = new DbSize(dbSize.Size, dbSize.SizeUnit); + // } + // } + // else + // { + // if (edition == this.currentState.azureEdition) + // { //No changes, return early since we don't need to do any of the changes below + // return; + // } + + // this.currentState.azureEdition = edition; + // this.EditionToCreate = MapAzureEditionToDbEngineEdition(edition); + // this.CurrentServiceLevelObjective = AzureSqlDbHelper.GetDefaultServiceObjective(edition); + // this.BackupStorageRedundancy = AzureSqlDbHelper.GetDefaultBackupStorageRedundancy(edition); + // var defaultSize = AzureSqlDbHelper.GetDatabaseDefaultSize(edition); + + // this.MaxSize = defaultSize == null ? String.Empty : defaultSize.ToString(); + // } + // this.NotifyObservers(); + // } + // else + // { + // //Can't really do much if we fail to parse the display name so just leave it as is and log a message + // System.Diagnostics.Debug.Assert(false, + // string.Format(CultureInfo.InvariantCulture, + // "Failed to parse edition display name '{0}' back into AzureEdition", value)); + // } + // } + } + + /// + /// Mapping funtion to get the Database engine edition based on the selected AzureEdition value + /// + /// Selected dropdown Azure Edition value + /// Corresponding DatabaseEngineEdition value + private static DatabaseEngineEdition MapAzureEditionToDbEngineEdition(AzureEdition edition) + { + // As of now we only know for sure that AzureEdition.DataWarehouse maps to + // DatabaseEngineEdition.SqlDataWarehouse, for all others we keep the default value + // as before which was 'SqlDatabase' + return edition == AzureEdition.DataWarehouse ? DatabaseEngineEdition.SqlDataWarehouse : DatabaseEngineEdition.SqlDatabase; } public override IList Filegroups @@ -148,6 +181,22 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin get { return this.ServerVersion.Major > 11 && this.AzureEdition != AzureEdition.DataWarehouse; } } + // [Browsable(false)] + // public SubscriptionLocationKey AzureServiceLocation { get; set; } + + public string BackupStorageRedundancy + { + get + { + return this.currentState.backupStorageRedundancy; + } + set + { + this.currentState.backupStorageRedundancy = value; + this.NotifyObservers(); + } + } + #endregion Properties #region DatabasePrototype overrides @@ -159,69 +208,71 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin /// The SMO database object that was created or modified public override Database ApplyChanges() { - // For v12 Non-DW DBs lets use SMO - if (this.ServerVersion.Major >= 12 && this.AzureEdition != AzureEdition.DataWarehouse) - { - return base.ApplyChanges(); - } - - //Note : We purposely don't call base.ApplyChanges() here since SMO doesn't fully support Azure yet and so will throw - //an error if we try to modify the Database object directly - string alterDbPropertiesStatement = DatabasePrototypeAzure.CreateModifyAzureDbOptionsStatement(this.Name, this.AzureEdition, this.MaxSize, this.CurrentServiceLevelObjective); - if (this.AzureEdition == AzureEdition.DataWarehouse) - { - alterDbPropertiesStatement = DatabasePrototypeAzure.CreateModifySqlDwDbOptionsStatement(this.Name, this.MaxSize, this.CurrentServiceLevelObjective); - } - - string alterAzureDbRecursiveTriggersEnabledStatement = DatabasePrototypeAzure.CreateAzureDbSetRecursiveTriggersStatement(this.Name, this.RecursiveTriggers); - string alterAzureDbIsReadOnlyStatement = DatabasePrototypeAzure.CreateAzureDbSetIsReadOnlyStatement(this.Name, this.IsReadOnly); - - Database db = this.GetDatabase(); - - //Altering the DB needs to be done on the master DB - using (var conn = new SqlConnection(this.context.ServerConnection.GetDatabaseConnection("master").ConnectionString)) - { - using (var cmd = new SqlCommand()) + Database database = base.ApplyChanges(); + if (this.AzureEdition != AzureEdition.DataWarehouse) + { + // We don't need to alter BSR value if the user is just scripting or if the DB is not creating. + if (database != null && this.context.Server.ConnectionContext.SqlExecutionModes != SqlExecutionModes.CaptureSql) { - cmd.Connection = conn; - conn.Open(); - - //Only run the alter statements for modifications made. This is mostly to allow the non-Azure specific - //properties to be updated when a SLO change is in progress, but it also is beneficial to save trips to the - //server whenever we can (especially when Azure is concerned) - if (currentState.azureEdition != originalState.azureEdition || - currentState.currentServiceLevelObjective != originalState.currentServiceLevelObjective || - currentState.maxSize != originalState.maxSize) + string alterAzureDbBackupStorageRedundancy = DatabasePrototypeAzure.CreateModifySqlDBBackupStorageRedundancyStatement(this.Name, this.currentState.backupStorageRedundancy); + using (var conn = this.context.ServerConnection.GetDatabaseConnection(this.Name).SqlConnectionObject) { - cmd.CommandText = alterDbPropertiesStatement; - cmd.ExecuteNonQuery(); - } - - if (currentState.recursiveTriggers != originalState.recursiveTriggers) - { - cmd.CommandText = alterAzureDbRecursiveTriggersEnabledStatement; - cmd.ExecuteNonQuery(); - } - - if (currentState.isReadOnly != originalState.isReadOnly) - { - cmd.CommandText = alterAzureDbIsReadOnlyStatement; - cmd.ExecuteNonQuery(); + //While scripting the database, there is already an open connection. So, we are checking the state of the connection here. + if (conn != null && conn.State == ConnectionState.Closed) + { + conn.Open(); + using (var cmd = new SqlCommand { Connection = conn }) + { + cmd.CommandText = alterAzureDbBackupStorageRedundancy; + cmd.ExecuteNonQuery(); + } + } } } - } - //Because we didn't use SMO to do the alter we should refresh the DB object so it picks up the correct properties - db.Refresh(); + return database; + } - // For properties that are supported in Database.Alter(), call SaveProperties, and then alter the DB. - // - if (this.AzureEdition != AzureEdition.DataWarehouse) - { - this.SaveProperties(db); - db.Alter(TerminationClause.FailOnOpenTransactions); - } - return db; + string alterDbPropertiesStatement = DatabasePrototypeAzure.CreateModifySqlDwDbOptionsStatement(this.Name, this.MaxSize, this.CurrentServiceLevelObjective); + + string alterAzureDbRecursiveTriggersEnabledStatement = DatabasePrototypeAzure.CreateAzureDbSetRecursiveTriggersStatement(this.Name, this.RecursiveTriggers); + string alterAzureDbIsReadOnlyStatement = DatabasePrototypeAzure.CreateAzureDbSetIsReadOnlyStatement(this.Name, this.IsReadOnly); + + Database db = this.GetDatabase(); + + //Altering the DB needs to be done on the master DB + using (var conn = this.context.ServerConnection.GetDatabaseConnection("master").SqlConnectionObject) + { + var cmd = new SqlCommand { Connection = conn }; + conn.Open(); + + //Only run the alter statements for modifications made. This is mostly to allow the non-Azure specific + //properties to be updated when a SLO change is in progress, but it also is beneficial to save trips to the + //server whenever we can (especially when Azure is concerned) + if ((currentState.azureEdition != null && currentState.azureEdition != originalState.azureEdition) || + (!string.IsNullOrEmpty(currentState.currentServiceLevelObjective) && currentState.currentServiceLevelObjective != originalState.currentServiceLevelObjective) || + (currentState.maxSize != null && currentState.maxSize != originalState.maxSize)) + { + cmd.CommandText = alterDbPropertiesStatement; + cmd.ExecuteNonQuery(); + } + + if (currentState.recursiveTriggers != originalState.recursiveTriggers) + { + cmd.CommandText = alterAzureDbRecursiveTriggersEnabledStatement; + cmd.ExecuteNonQuery(); + } + + if (currentState.isReadOnly != originalState.isReadOnly) + { + cmd.CommandText = alterAzureDbIsReadOnlyStatement; + cmd.ExecuteNonQuery(); + } + } + + //Because we didn't use SMO to do the alter we should refresh the DB object so it picks up the correct properties + db.Refresh(); + return db; } #endregion DatabasePrototype overrides @@ -229,30 +280,28 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin protected override void SaveProperties(Database db) { base.SaveProperties(db); - if (this.ServerVersion.Major >= 12 && this.AzureEdition != AzureEdition.DataWarehouse) + + // treat null as defaults/unchanged + // SMO will only script changed values so if the user changes edition and size and SLO are empty the alter + // will change the db to the default size and slo for the new edition + // if the new combination of edition/size/slo is invalid the alter will fail + if (this.currentState.maxSize != null && (!this.Exists || (this.originalState.maxSize != this.currentState.maxSize))) { - if (!this.Exists || (this.originalState.maxSize != this.currentState.maxSize)) - { - db.MaxSizeInBytes = this.currentState.maxSize.SizeInBytes; - } - - if (!this.Exists || (this.originalState.azureEdition != this.currentState.azureEdition)) - { - db.AzureEdition = this.currentState.azureEdition.ToString(); - } - - if (!this.Exists || (this.originalState.currentServiceLevelObjective != this.currentState.currentServiceLevelObjective)) - { - db.AzureServiceObjective = this.currentState.currentServiceLevelObjective; - } + db.MaxSizeInBytes = this.currentState.maxSize.SizeInBytes; + } + + if (this.currentState.azureEdition != null && (!this.Exists || (this.originalState.azureEdition != this.currentState.azureEdition))) + { + db.AzureEdition = this.currentState.azureEdition.ToString(); + } + + if (!string.IsNullOrEmpty(this.currentState.currentServiceLevelObjective) && (!this.Exists || (this.originalState.currentServiceLevelObjective != this.currentState.currentServiceLevelObjective))) + { + db.AzureServiceObjective = this.currentState.currentServiceLevelObjective; } - } - private const string AlterDbStatementFormat = - @"ALTER DATABASE [{0}] {1}"; - - private const string ModifyAzureDbStatementFormat = @"MODIFY (EDITION = '{0}', MAXSIZE={1} {2})"; + private const string AlterDbStatementFormat = @"ALTER DATABASE [{0}] {1}"; private const string ModifySqlDwDbStatementFormat = @"MODIFY (MAXSIZE={0} {1})"; private const string AzureServiceLevelObjectiveOptionFormat = @"SERVICE_OBJECTIVE = '{0}'"; private const string SetReadOnlyOption = @"SET READ_ONLY"; @@ -260,6 +309,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin private const string SetRecursiveTriggersOptionFormat = @"SET RECURSIVE_TRIGGERS {0}"; private const string On = @"ON"; private const string Off = @"OFF"; + private const string ModifySqlDbBackupStorageRedundancy = @"MODIFY BACKUP_STORAGE_REDUNDANCY = '{0}'"; /// /// Creates an ALTER DATABASE statement to modify the Read-Only status of the target DB @@ -288,29 +338,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin recursiveTriggersEnabled ? DatabasePrototypeAzure.On : DatabasePrototypeAzure.Off)); } - /// - /// Creates an ALTER DATABASE statement to modify the Azure Database properties (Edition, MaxSize and Service Level Objective) - /// for the target database - /// - /// - /// - /// - /// - /// - protected static string CreateModifyAzureDbOptionsStatement(string dbName, AzureEdition edition, string maxSize, string serviceLevelObjective) - { - //We might not have a SLO since some editions don't support it - string sloOption = string.IsNullOrEmpty(serviceLevelObjective) ? - string.Empty : ", " + string.Format(CultureInfo.InvariantCulture, AzureServiceLevelObjectiveOptionFormat, serviceLevelObjective); - - return CreateAzureAlterDbStatement(dbName, - string.Format(CultureInfo.InvariantCulture, - ModifyAzureDbStatementFormat, - edition, - maxSize, - sloOption)); - } - /// /// Creates an ALTER DATABASE statement to modify the Azure DataWarehouse properties (MaxSize and Service Level Objective) /// for the target database @@ -332,6 +359,21 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin sloOption)); } + /// + /// Creates the ATLER DATABASE statement from the given backup storage redundancy option. + /// + /// + /// + /// + protected static string CreateModifySqlDBBackupStorageRedundancyStatement(string dbName, string option) + { + //Note: We allow user to select any one of the value from the UI for backupStorageRedundancy. So, we are inlining the value. + return CreateAzureAlterDbStatement(dbName, + string.Format(CultureInfo.InvariantCulture, + ModifySqlDbBackupStorageRedundancy, + option)); + } + /// /// Creates the ALTER DATABASE statement from the given op /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/Management/Common/DataContainer.cs b/src/Microsoft.SqlTools.ServiceLayer/Management/Common/DataContainer.cs index aa4ecf6b..b8464cde 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Management/Common/DataContainer.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Management/Common/DataContainer.cs @@ -3,11 +3,11 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -#nullable disable - using System; using System.Collections; +using System.Collections.Generic; using System.Data; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Security; @@ -30,7 +30,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management { SQL, OLAP, //This type is used only for non-express sku - SQLCE, + SQLCE, UNKNOWN } @@ -38,32 +38,31 @@ namespace Microsoft.SqlTools.ServiceLayer.Management #region Fields - private ServerConnection serverConnection; - private Server m_server = null; - protected XmlDocument m_doc = null; - private XmlDocument originalDocument = null; - private SqlOlapConnectionInfoBase connectionInfo = null; - private SqlConnectionInfoWithConnection sqlCiWithConnection; + private ServerConnection? serverConnection; + private Server? m_server; + protected XmlDocument? m_doc; + private XmlDocument? originalDocument; + private SqlOlapConnectionInfoBase? connectionInfo; + private SqlConnectionInfoWithConnection? sqlCiWithConnection; private bool ownConnection = true; - private IManagedConnection managedConnection; - protected string serverName; + private IManagedConnection? managedConnection; + protected string? serverName; //This member is used for non-express sku only - protected string olapServerName; + protected string? olapServerName; - protected string sqlceFilename; + protected string? sqlceFilename; private ServerType serverType = ServerType.UNKNOWN; - private Hashtable m_hashTable = null; + private Hashtable? m_hashTable; private string objectNameKey = "object-name-9524b5c1-e996-4119-a433-b5b947985566"; private string objectSchemaKey = "object-schema-ccaf2efe-8fa3-4f62-be79-62ef3cbe7390"; - private SqlSmoObject sqlDialogSubject = null; + private SqlSmoObject? sqlDialogSubject; private int sqlServerVersion = 0; - private int sqlServerEffectiveVersion = 0; #endregion @@ -73,7 +72,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management /// /// gets/sets XmlDocument with parameters /// - public XmlDocument Document + public XmlDocument? Document { get { @@ -85,8 +84,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management if (value != null) { - //this.originalDocument = (XmlDocument) value.Clone(); - this.originalDocument = value; + this.originalDocument = (XmlDocument)value.Clone(); } else { @@ -111,7 +109,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management /// /// gets/sets SMO server object /// - public Server Server + public Server? Server { get { @@ -127,26 +125,29 @@ namespace Microsoft.SqlTools.ServiceLayer.Management /// /// connection info that should be used by the dialogs /// - public SqlOlapConnectionInfoBase ConnectionInfo + public SqlOlapConnectionInfoBase? ConnectionInfo { get { - //// update the database name in the serverconnection object to set the correct database context when connected to Azure - //var conn = this.connectionInfo as SqlConnectionInfoWithConnection; + // update the database name in the serverconnection object to set the correct database context when connected to Azure + var conn = this.connectionInfo as SqlConnectionInfoWithConnection; - //if (conn != null && conn.ServerConnection.DatabaseEngineType == DatabaseEngineType.SqlAzureDatabase) - //{ - // if (this.RelevantDatabaseName != null) - // { - // IComparer dbNamesComparer = ServerConnection.ConnectionFactory.GetInstance(conn.ServerConnection).ServerComparer as IComparer; - // if (dbNamesComparer.Compare(this.RelevantDatabaseName, conn.DatabaseName) != 0) - // { - // ServerConnection serverConnection = conn.ServerConnection.GetDatabaseConnection(this.RelevantDatabaseName, true, conn.AccessToken); - // ((SqlConnectionInfoWithConnection)this.connectionInfo).ServerConnection = serverConnection; - // } - // } - //} - + // Don't update the database name if this is a Gen3 connection since Gen3 supports USE from the server connection. + if (conn != null && + conn.ServerConnection.DatabaseEngineType == DatabaseEngineType.SqlAzureDatabase && + !(conn.ServerConnection.DatabaseEngineEdition == DatabaseEngineEdition.SqlDataWarehouse && + conn.ServerConnection.ProductVersion.Major >= 12)) + { + if (this.RelevantDatabaseName != null) + { + IComparer dbNamesComparer = new ServerComparer(conn.ServerConnection, "master"); + if (dbNamesComparer.Compare(this.RelevantDatabaseName, conn.DatabaseName) != 0 && this.connectionInfo != null) + { + ServerConnection databaseConnection = conn.ServerConnection.GetDatabaseConnection(this.RelevantDatabaseName, true, conn.AccessToken); + ((SqlConnectionInfoWithConnection)this.connectionInfo).ServerConnection = databaseConnection; + } + } + } return this.connectionInfo; } } @@ -163,11 +164,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Management { if (this.serverType != ServerType.SQL) { + System.Diagnostics.Debug.Assert(false, "CDataContainer.ServerConnection can be used only for SQL connection"); + throw new InvalidOperationException(); } if (this.connectionInfo == null) { + System.Diagnostics.Debug.Assert(false, "CDataContainer.ServerConnection can be used only after ConnectionInfo property has been set"); + throw new InvalidOperationException(); } @@ -177,12 +182,13 @@ namespace Microsoft.SqlTools.ServiceLayer.Management } else { - SqlConnectionInfo sci = this.connectionInfo as SqlConnectionInfo; + SqlConnectionInfo? sci = this.connectionInfo as SqlConnectionInfo; + System.Diagnostics.Debug.Assert(sci != null, "CDataContainer.ServerConnection: connection info MUST be SqlConnectionInfo"); this.serverConnection = new ServerConnection(sci); } } - + System.Diagnostics.Debug.Assert(this.serverConnection != null); return this.serverConnection; } } @@ -191,7 +197,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management /// returns SMO server connection object constructed off the connectionInfo. /// This method cannot work until ConnectionInfo property has been set /// - public SqlConnectionInfoWithConnection SqlInfoWithConnection + public SqlConnectionInfoWithConnection? SqlInfoWithConnection { get { @@ -199,11 +205,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Management { if (this.serverType != ServerType.SQL) { + System.Diagnostics.Debug.Assert(false, "CDataContainer.ServerConnection can be used only for SQL connection"); + throw new InvalidOperationException(); } if (this.connectionInfo == null) { + System.Diagnostics.Debug.Assert(false, "CDataContainer.ServerConnection can be used only after ConnectionInfo property has been set"); + throw new InvalidOperationException(); } @@ -214,16 +224,18 @@ namespace Microsoft.SqlTools.ServiceLayer.Management } else { - SqlConnectionInfo sci = this.connectionInfo as SqlConnectionInfo; + SqlConnectionInfo? sci = this.connectionInfo as SqlConnectionInfo; + System.Diagnostics.Debug.Assert(sci != null, "CDataContainer.ServerConnection: connection info MUST be SqlConnectionInfo"); this.serverConnection = new ServerConnection(sci); } } + System.Diagnostics.Debug.Assert(this.serverConnection != null); return this.sqlCiWithConnection; } } - public string ServerName + public string? ServerName { get { @@ -247,7 +259,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management } } - public string SqlCeFileName + public string? SqlCeFileName { get { @@ -260,7 +272,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management } //This member is used for non-express sku only - public string OlapServerName + public string? OlapServerName { get { @@ -418,11 +430,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Management /// /// The SQL SMO object that is the subject of the dialog. /// - public SqlSmoObject SqlDialogSubject + public SqlSmoObject? SqlDialogSubject { get { - SqlSmoObject result = null; + SqlSmoObject? result; if (this.sqlDialogSubject != null) { @@ -430,7 +442,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management } else { - result = this.Server.GetSmoObject(this.ObjectUrn); + result = this.Server?.GetSmoObject(this.ObjectUrn); } return result; @@ -451,6 +463,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Management { bool result = false; + System.Diagnostics.Debug.Assert(this.Server != null, "SMO Server object is null!"); + System.Diagnostics.Debug.Assert(this.Server.ConnectionContext != null, "SMO Server Connection object is null!"); + if (this.Server != null && this.Server.ConnectionContext != null) { result = this.Server.ConnectionContext.IsInFixedServerRole(FixedServerRoles.SysAdmin); @@ -471,6 +486,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Management string result = String.Empty; string urnText = this.GetDocumentPropertyString("urn"); + System.Diagnostics.Debug.Assert(urnText.Length != 0, "couldn't get relevant URN"); + if (urnText.Length != 0) { Urn urn = new Urn(urnText); @@ -501,6 +518,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Management { this.sqlServerVersion = 9; + System.Diagnostics.Debug.Assert(this.ConnectionInfo != null, "ConnectionInfo is null!"); + System.Diagnostics.Debug.Assert(ServerType.SQL == this.ContainerServerType, "unexpected server type"); + if ((this.ConnectionInfo != null) && (ServerType.SQL == this.ContainerServerType)) { Enumerator enumerator = new Enumerator(); @@ -520,99 +540,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Management } - /// - /// The server version the database is emulating. If database compatibility level is - /// not relevant to the subject, then this just returns the actual server version. - /// - public int EffectiveSqlServerVersion - { - get - { - if (this.sqlServerEffectiveVersion == 0) - { - this.sqlServerEffectiveVersion = 9; - - if ((this.ConnectionInfo != null) && (ServerType.SQL == this.ContainerServerType)) - { - string databaseName = this.RelevantDatabaseName; - - if (databaseName.Length != 0) - { - Enumerator enumerator = new Enumerator(); - Urn urn = String.Format("Server/Database[@Name='{0}']", Urn.EscapeString(databaseName)); - string[] fields = new string[] { "CompatibilityLevel" }; - DataTable dataTable = enumerator.Process(this.ConnectionInfo, new Request(urn, fields)); - - if (dataTable.Rows.Count != 0) - { - - CompatibilityLevel level = (CompatibilityLevel)dataTable.Rows[0][0]; - - switch (level) - { - case CompatibilityLevel.Version60: - case CompatibilityLevel.Version65: - - this.sqlServerEffectiveVersion = 6; - break; - - case CompatibilityLevel.Version70: - - this.sqlServerEffectiveVersion = 7; - break; - - case CompatibilityLevel.Version80: - - this.sqlServerEffectiveVersion = 8; - break; - - case CompatibilityLevel.Version90: - - this.sqlServerEffectiveVersion = 9; - break; - case CompatibilityLevel.Version100: - - this.sqlServerEffectiveVersion = 10; - break; - case CompatibilityLevel.Version110: - - this.sqlServerEffectiveVersion = 11; - break; - case CompatibilityLevel.Version120: - - this.sqlServerEffectiveVersion = 12; - break; - - case CompatibilityLevel.Version130: - this.sqlServerEffectiveVersion = 13; - break; - - case CompatibilityLevel.Version140: - this.sqlServerEffectiveVersion = 14; - break; - - default: - - this.sqlServerEffectiveVersion = 14; - break; - } - } - else - { - this.sqlServerEffectiveVersion = this.SqlServerVersion; - } - } - else - { - this.sqlServerEffectiveVersion = this.SqlServerVersion; - } - } - } - - return this.sqlServerEffectiveVersion; - } - } - #endregion #region Constructors, finalizer @@ -628,9 +555,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Management /// connection info containing live connection public CDataContainer(object ciObj, bool ownConnection) { - SqlConnectionInfoWithConnection ci = (SqlConnectionInfoWithConnection)ciObj; + SqlConnectionInfoWithConnection ci = (SqlConnectionInfoWithConnection)ciObj; if (ci == null) { + System.Diagnostics.Debug.Assert(false, "CDataContainer.CDataContainer(SqlConnectionInfoWithConnection): specified connection info is null"); + throw new ArgumentNullException("ci"); } ApplyConnectionInfo(ci, ownConnection); @@ -645,9 +574,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Management /// connection info containing live connection public CDataContainer(ServerType serverType, object ciObj, bool ownConnection) { - SqlConnectionInfoWithConnection ci = (SqlConnectionInfoWithConnection)ciObj; + SqlConnectionInfoWithConnection ci = (SqlConnectionInfoWithConnection)ciObj; if (ci == null) { + System.Diagnostics.Debug.Assert(false, "CDataContainer.CDataContainer(SqlConnectionInfoWithConnection): specified connection info is null"); + throw new ArgumentNullException("ci"); } @@ -656,12 +587,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Management if (serverType == ServerType.SQL) { - //NOTE: ServerConnection property will constuct the object if needed - m_server = new Server(ServerConnection); - } + //NOTE: ServerConnection property will construct the object if needed + m_server = new Server(ServerConnection); + } else { - throw new ArgumentException(SR.UnknownServerType(serverType.ToString())); + throw new ArgumentException(SR.UnknownServerType(serverType.ToString())); } } @@ -674,7 +605,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management /// User name for not trusted connections /// Password for not trusted connections /// XML string with parameters - public CDataContainer(ServerType serverType, string serverName, bool trusted, string userName, SecureString password, string databaseName, string xmlParameters, string azureAccountToken = null) + public CDataContainer(ServerType serverType, string serverName, bool trusted, string userName, SecureString password, string databaseName, string xmlParameters, string? azureAccountToken = null) { this.serverType = serverType; this.serverName = serverName; @@ -710,6 +641,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Management /// XML string with parameters public CDataContainer(CDataContainer dataContainer, string xmlParameters) { + //BUGBUG - should we be reusing same SqlConnectionInfoWithConnection if it is available? + + System.Diagnostics.Debug.Assert(dataContainer.Server != null, "DataContainer.Server can not be null."); Server = dataContainer.Server; this.serverName = dataContainer.serverName; this.serverType = dataContainer.serverType; @@ -718,11 +652,16 @@ namespace Microsoft.SqlTools.ServiceLayer.Management this.sqlCiWithConnection = dataContainer.connectionInfo as SqlConnectionInfoWithConnection; if (this.sqlCiWithConnection != null) - { + { //we want to be notified if it is closed this.sqlCiWithConnection.ConnectionClosed += new EventHandler(OnSqlConnectionClosed); } + if (this.connectionInfo is SqlConnectionInfo) + { + System.Diagnostics.Debug.Assert(this.sqlCiWithConnection != null, "CDataContainer.ConnectionInfo setter: for SQL connection info you MUST use SqlConnectionInfoWithConnection derived class!"); + } + if (xmlParameters != null) { XmlDocument doc = GenerateXmlDocumentFromString(xmlParameters); @@ -764,20 +703,24 @@ namespace Microsoft.SqlTools.ServiceLayer.Management if (site != null) { - // see if service provider supports INodeInformation interface from the object explorer - // NOTE: we're trying to forcefully set connection information on the data container. - // If this code doesn't execute, then dc.Init call below will result in CDataContainer - // initializing its ConnectionInfo member with a new object contructed off the parameters - // in the XML doc [server name, user name etc] - IManagedConnection managedConnection = site.GetService(typeof(IManagedConnection)) as IManagedConnection; + Trace.TraceInformation("CDataContainer.Init has non-null IServiceProvider"); + //see if service provider supports IManagedConnection interface from the object explorer + + //NOTE: we're trying to forcefully set connection information on the data container. + //If this code doesn't execute, then dc.Init call below will result in CDataContainer + //initializing its ConnectionInfo member with a new object contructed off the parameters + //in the XML doc [server name, user name etc] + IManagedConnection? managedConnection = + site.GetService(typeof(IManagedConnection)) as IManagedConnection; if (managedConnection != null) { + Trace.TraceInformation("CDataContainer.Init has non-null IManagedConnection"); this.SetManagedConnection(managedConnection); } } this.Document = doc; - LoadData(); + LoadData(); // finish the initialization this.Init(doc); @@ -808,13 +751,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Management // NOTE: ServerConnection property will constuct the object if needed m_server ??= new Server(ServerConnection); - } - else if (this.serverType == ServerType.SQLCE) - { - // do nothing; originally we were only distinguishing between two - // types of servers (OLAP/SQL); as a result for SQLCE we were - // executing the same codepath as for OLAP server which was - // resulting in an exception; } } @@ -837,21 +773,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Management if (!bStatus || this.serverName.Length == 0) { + if (this.sqlCiWithConnection != null) { - bStatus = param.GetParam("database", ref this.sqlceFilename); - if (bStatus && !string.IsNullOrEmpty(this.sqlceFilename)) - { - this.serverType = ServerType.SQLCE; - } - else if (this.sqlCiWithConnection != null) - { - this.serverType = ServerType.SQL; - } - else - { - this.serverType = ServerType.UNKNOWN; - } - } + this.serverType = ServerType.SQL; + } + else + { + this.serverType = ServerType.UNKNOWN; + } + } else { @@ -877,10 +807,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Management } // Ensure there is no password in the XML document - string temp = string.Empty; + string? temp = string.Empty; if (param.GetParam("password", ref temp)) { - temp = null; + temp = null; + System.Diagnostics.Debug.Assert(false, "Plaintext password found in XML document! This must be fixed!"); + throw new SecurityException(); } @@ -899,7 +831,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Management /// /// internal void SetManagedConnection(IManagedConnection managedConnection) - { + { + System.Diagnostics.Debug.Assert(this.managedConnection == null, "CDataContainer.SetManagedConnection: overwriting the previous value"); this.managedConnection = managedConnection; ApplyConnectionInfo(managedConnection.Connection, true);//it will do some extra initialization @@ -912,7 +845,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management /// The property value public object GetDocumentPropertyValue(string propertyName) { - object result = null; + object? result = null; STParameters param = new STParameters(this.Document); param.GetBaseParam(propertyName, ref result); @@ -974,6 +907,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Management /// private void InitializeObjectNameAndSchema() { + System.Diagnostics.Debug.Assert(ServerType.SQL == this.serverType, "This method only valid for SQL Servers"); + string documentUrn = this.GetDocumentPropertyString("urn"); if (documentUrn.Length != 0) { @@ -1003,8 +938,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Management string userName, SecureString password, string databaseName, - string azureAccountToken) - { + string? azureAccountToken) + { + System.Diagnostics.Debug.Assert(this.serverType == ServerType.SQL, "GetTempSqlConnectionInfoWithConnection should only be called for SQL Server type"); + SqlConnectionInfoWithConnection tempCI = new SqlConnectionInfoWithConnection(serverName); tempCI.SingleConnection = false; tempCI.Pooled = false; @@ -1031,7 +968,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management /// /// /// - private void OnSqlConnectionClosed(object sender, EventArgs e) + private void OnSqlConnectionClosed(object? sender, EventArgs e) { } @@ -1042,7 +979,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Management /// private void ApplyConnectionInfo(SqlOlapConnectionInfoBase ci, bool ownConnection) { - + System.Diagnostics.Debug.Assert(this.connectionInfo == null, "CDataContainer.ApplyConnectionInfo: overwriting non-null connection info!"); + System.Diagnostics.Debug.Assert(ci != null, "CDataContainer.ApplyConnectionInfo: ci is null!"); + this.connectionInfo = ci; this.ownConnection = ownConnection; @@ -1050,12 +989,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Management this.sqlCiWithConnection = ci as SqlConnectionInfoWithConnection; if (this.sqlCiWithConnection != null) - { + { // we want to be notified if it is closed this.sqlCiWithConnection.ConnectionClosed += new EventHandler(OnSqlConnectionClosed); } } - + private static bool MustRethrow(Exception exception) { bool result = false; @@ -1132,7 +1071,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management /// MUST be called, as we'll be closing SQL connection inside this call /// private void Dispose(bool disposing) - { + { try { //take care of live SQL connection @@ -1175,42 +1114,56 @@ namespace Microsoft.SqlTools.ServiceLayer.Management this.managedConnection.Close(); } this.managedConnection = null; - } + } } catch (Exception) - { + { } } #endregion - + /// /// Create a data container object /// /// connection info /// flag indicating whether to create taskhelper for existing database or not internal static CDataContainer CreateDataContainer( - ConnectionInfo connInfo, + ConnectionInfo connInfo, bool databaseExists = false, - XmlDocument containerDoc = null) + XmlDocument? containerDoc = null) { containerDoc ??= CreateDataContainerDocument(connInfo, databaseExists); var serverConnection = ConnectionService.OpenServerConnection(connInfo, "DataContainer"); - var connectionInfoWithConnection = new SqlConnectionInfoWithConnection(); connectionInfoWithConnection.ServerConnection = serverConnection; + + return CreateDataContainer(connectionInfoWithConnection, containerDoc); + } + + /// + /// Create a data container object + /// + /// connection info + /// flag indicating whether to create taskhelper for existing database or not + internal static CDataContainer CreateDataContainer( + SqlConnectionInfoWithConnection connectionInfoWithConnection, + XmlDocument containerDoc) + { CDataContainer dataContainer = new CDataContainer(ServerType.SQL, connectionInfoWithConnection, true); dataContainer.Init(containerDoc); - return dataContainer; } - internal static System.Security.SecureString BuildSecureStringFromPassword(string password) { + internal static System.Security.SecureString BuildSecureStringFromPassword(string password) + { var passwordSecureString = new System.Security.SecureString(); - if (password != null) { - foreach (char c in password) { + if (password != null) + { + foreach (char c in password) + { passwordSecureString.AppendChar(c); } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Management/Common/DataContainerXmlGenerator.cs b/src/Microsoft.SqlTools.ServiceLayer/Management/Common/DataContainerXmlGenerator.cs new file mode 100644 index 00000000..77410d91 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Management/Common/DataContainerXmlGenerator.cs @@ -0,0 +1,900 @@ +// +// 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 System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.IO; +using System.Text; +using System.Xml; +using Microsoft.Data.SqlClient; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Management +{ + + + public class ActionContext + { + #region private members + /// + /// Name of the object + /// + string name; + /// + /// connection to the server + /// + private ServerConnection connection; + /// + /// Connection context + /// + private string contextUrn; + /// + /// Parent node in the tree + /// + //private INodeInformation parent; + /// + /// Weak reference to the tree node this is paired with + /// + WeakReference NavigableItemReference; + /// + /// Property handlers + /// + //private IList propertyHandlers; + /// + /// Property bag + /// + NameObjectCollection properties; + /// + /// Object to lock on when we are modifying public state + /// + private object itemStateLock = new object(); + /// + /// Cached UrnPath + /// + private string urnPath; + #endregion + + #region constructors + + public ActionContext(ServerConnection connection, string name, string contextUrn) + { + if (connection == null) + { + throw new ArgumentNullException("connection"); + } + if (contextUrn == null) + { + throw new ArgumentNullException("context"); + } + if (name == null) + { + throw new ArgumentNullException("name"); + } + this.connection = connection; + this.contextUrn = contextUrn; + this.name = name; + + properties = new NameObjectCollection(); + //propertyHandlers = null; + NavigableItemReference = null; + } + #endregion + + #region INodeInformation implementation + public ServerConnection Connection + { + get + { + return this.connection; + } + set + { + lock (this.itemStateLock) + { + this.connection = value; + } + } + } + public string ContextUrn + { + get + { + return this.contextUrn; + } + set + { + lock (this.itemStateLock) + { + this.contextUrn = value; + } + } + } + + public string NavigationContext + { + get + { + return GetNavigationContext(this); + } + } + + public string UrnPath + { + get + { + this.urnPath ??= ActionContext.BuildUrnPath(this.NavigationContext); + return this.urnPath; + } + } + + public string InvariantName + { + get + { + string name = this["UniqueName"] as string; + + if (!string.IsNullOrEmpty(name)) + return name; + + StringBuilder uniqueName = new StringBuilder(); + + foreach (string urnValue in GetUrnPropertyValues()) + { + if (uniqueName.Length > 0) + uniqueName.Append("."); + + uniqueName.Append(urnValue); + } + + return (uniqueName.Length > 0) ? uniqueName.ToString() : new Urn(ContextUrn).Type; + } + } + + /// + /// property bag for this node + /// + public object this[string name] => properties[name]; + + public object CreateObjectInstance() + { + return CreateObjectInstance(this.ContextUrn, this.Connection); + } + + #endregion + + #region ISfcPropertyProvider implementation + + public NameObjectCollection GetPropertySet() + { + return this.properties; + } + + #endregion + + #region NodeName helper + public string Name + { + get + { + return this.name; + } + set + { + lock (this.itemStateLock) + { + this.name = value; + } + } + } + #endregion + + #region property bag support + + public NameObjectCollection Properties + { + get + { + return this.properties; + } + set + { + lock (this.itemStateLock) + { + this.properties = value; + } + } + } + + #endregion + + #region helpers + + public static string GetNavigationContext(ActionContext source) + { + string context = source.ContextUrn; + // see if this is a folder + string name = source["UniqueName"] as string; + if (name == null || name.Length == 0) + { + name = source.Name; + } + string queryHint = source["QueryHint"] as string; + if (queryHint == null || queryHint.Length == 0) + { + context = string.Format( + System.Globalization.CultureInfo.InvariantCulture + , "{0}/Folder[@Name='{1}']" + , source.ContextUrn + , Urn.EscapeString(name)); + } + else + { + context = string.Format( + System.Globalization.CultureInfo.InvariantCulture + , "{0}/Folder[@Name='{1}' and @Type='{2}']" + , source.ContextUrn + , Urn.EscapeString(name) + , Urn.EscapeString(queryHint)); + + } + return context; + } + + /// + /// Get the values of the keys in the current objects Urn + /// e.g. For Table[@Name='Foo' and @Schema='Bar'] return Foo and Bar + /// + /// + private IEnumerable GetUrnPropertyValues() + { + Urn urn = new Urn(ContextUrn); + Enumerator enumerator = new Enumerator(); + RequestObjectInfo request = new RequestObjectInfo(urn, RequestObjectInfo.Flags.UrnProperties); + + ObjectInfo info = enumerator.Process(connection, request); + + if (info == null || info.UrnProperties == null) + yield break; + + // Special order for Schema and Name + if (properties.Contains("Schema")) + yield return urn.GetAttribute("Schema"); + + if (properties.Contains("Name")) + yield return urn.GetAttribute("Name"); + + foreach (ObjectProperty obj in info.UrnProperties) + { + if (obj.Name.Equals("Name", StringComparison.OrdinalIgnoreCase) || obj.Name.Equals("Schema", StringComparison.OrdinalIgnoreCase)) + continue; + yield return urn.GetAttribute(obj.Name); + } + } + + public static string BuildUrnPath(string urn) + { + StringBuilder urnPathBuilder = new StringBuilder(urn != null ? urn.Length : 0); + + string folderName = string.Empty; + bool replaceLeafValueInQuery = false; + + if (!string.IsNullOrEmpty(urn)) + { + Urn urnObject = new Urn(urn); + + while (urnObject != null) + { + string objectType = urnObject.Type; + + if (string.CompareOrdinal(objectType, "Folder") == 0) + { + folderName = urnObject.GetAttribute("Name").Replace(" ", ""); + if (folderName != null) + { + objectType = string.Format("{0}Folder", folderName); + } + } + + // Build the path + if (urnPathBuilder.Length > 0) + { + urnPathBuilder.Insert(0, '/'); + } + + if (objectType.Length > 0) + { + urnPathBuilder.Insert(0, objectType); + } + + // Remove one element from the urn + urnObject = urnObject.Parent; + } + + // Build the query + if (replaceLeafValueInQuery) + { + // This is another special case for DTS urns. + // When we want to request data for an individual package + // we need to use a special urn with Leaf="2" attribute, + // replacing the Leaf='1' that comes from OE. + urnObject = new Urn(urn.Replace("@Leaf='1'", "@Leaf='2'")); + } + else + { + urnObject = new Urn(urn); + } + } + + return urnPathBuilder.ToString(); + } + + public static object CreateObjectInstance(string urn, ServerConnection serverConnection) + { + if (string.IsNullOrEmpty(urn)) + { + return null; + } + + try + { + SfcObjectQuery oq = null; + Urn urnObject = new Microsoft.SqlServer.Management.Sdk.Sfc.Urn(urn); + + // i have to find domain from Urn. + // DomainInstanceName thrown NotImplemented Exception + // so, i have to walk Urn tree to the top + Urn current = urnObject; + while (current.Parent != null) + { + current = current.Parent; + } + string domainName = current.Type; + + if (domainName == "Server") + { + oq = new SfcObjectQuery(new Microsoft.SqlServer.Management.Smo.Server(serverConnection)); + } + else + { + SqlConnection connection = serverConnection.SqlConnectionObject; + if (connection == null) + { + return null; + } + + // no need to check return value - this method will throw, if domain is incorrect + SfcDomainInfo ddi = Microsoft.SqlServer.Management.Sdk.Sfc.SfcRegistration.Domains[domainName]; + + ISfcDomain domain = (ISfcDomain)Activator.CreateInstance(ddi.RootType, new SqlStoreConnection(connection)); + + oq = new SfcObjectQuery(domain); + } + + foreach (object obj in oq.ExecuteIterator(new SfcQueryExpression(urn), null, null)) + { + return obj; + } + } + catch (Exception ex) + { + System.Diagnostics.Trace.TraceError(ex.Message); + return null; + } + + return null; + } + + #endregion + } + + public class DataContainerXmlGenerator + { + #region private members + /// + /// additional xml to be passed to the dialog + /// + protected string rawXml = string.Empty; + /// + /// do not pass this type information to the dialog. + /// e.g. New Database menu item on an existing database should not pass the database name through, + /// so we set itemType as Database. + /// + protected string? itemType = string.Empty; + /// + /// Additional query to perform and pass the results to the dialog. + /// + protected string? invokeMultiChildQueryXPath = null; + + private ActionContext context; + /// + /// The node in the hierarchy that owns this + /// + public virtual ActionContext Context + { + get { return context; } + set { context = value; } + } + + private string mode; + /// + /// mode + /// + /// + /// "new" "properties" + /// + public string Mode + { + get { return mode; } + set { mode = value; } + } + + #endregion + + #region construction + /// + /// + /// + public DataContainerXmlGenerator(ActionContext context, string mode = "new") + { + this.context = context; + this.mode = mode; + } + + #endregion + + #region IObjectBuilder implementation + /// + /// + /// + /// + /// + public void AddProperty(string name, object value) + { + // RAWXML is xml that is added to the document we're passing to the dialog with no additional + // processing + if (string.Compare(name, "rawxml", StringComparison.OrdinalIgnoreCase) == 0) + { + this.rawXml += value.ToString(); + } + // ITEMTYPE is for new menu items where we do not want to pass in the information for this type + // e.g. New Database menu item on an existing database should not pass the database name through, + // so we set ITEMTYPE as Database. + else if (string.Compare(name, "itemtype", StringComparison.OrdinalIgnoreCase) == 0) + { + this.itemType = value.ToString(); + } + // Allows us to query below the current level in the enumerator and pass the results through to + // the dialog. Usefull for Do xyz on all for menu's on folders. + else if (string.Compare(name, "multichildqueryxpath", StringComparison.OrdinalIgnoreCase) == 0) + { + this.invokeMultiChildQueryXPath = value.ToString(); + } + + + } + #endregion + + #region xml + #region xml document generation + /// + /// Generate an XmlDocument that contains all of the context needed to launch a dialog + /// + /// XmlDocument + public virtual XmlDocument GenerateXmlDocument() + { + MemoryStream memoryStream = new MemoryStream(); + // build the xml + XmlTextWriter xmlWriter = new XmlTextWriter(memoryStream, Encoding.UTF8); + + // write out the document headers + StartXmlDocument(xmlWriter); + // write xml specific to each connection type + GenerateConnectionXml(xmlWriter); + // generate the xml specific to the item we are being launched against + GenerateItemContext(xmlWriter); + // write out any of out properties to the document + WritePropertiesToXml(xmlWriter); + // close the document headers + EndXmlDocument(xmlWriter); + + // make sure everything is commited + xmlWriter.Flush(); + + // Resets the stream to the beginning + memoryStream.Seek(0, SeekOrigin.Begin); + + // done composing the XML string, now build the document + XmlDocument doc = new XmlDocument(); + + // don't lose leading or trailing whitespace + doc.PreserveWhitespace = true; + + // directly create the document from the memoryStream. + // We do this because using an xmlreader in between would an extra + // overhead and it also messes up the new line characters in the original + // stream (converts all \r to \n).-anchals + doc.Load(memoryStream); + + return doc; + } + #endregion + + #region document start/end + /// + /// Write the starting elements needed by the dialog framework + /// + /// XmlWriter that these elements will be written to + protected virtual void StartXmlDocument(XmlWriter xmlWriter) + { + XmlGeneratorHelper.StartXmlDocument(xmlWriter); + } + /// + /// Close the elements needed by the dialog framework + /// + /// XmlWriter that these elements will be written to + protected virtual void EndXmlDocument(XmlWriter xmlWriter) + { + System.Diagnostics.Debug.Assert(xmlWriter != null, "xmlWriter should never be null."); + + // close params + xmlWriter.WriteEndElement(); + // close formdescription + xmlWriter.WriteEndElement(); + } + #endregion + + #region server specific generation + /// + /// Generate the XML that will allow the dialog to connect to the server + /// + /// XmlWriter that these elements will be written to + protected virtual void GenerateConnectionXml(XmlWriter xmlWriter) + { + XmlGeneratorHelper.GenerateConnectionXml(xmlWriter, this.Context); + } + /// + /// Generate SQL Server specific connection information + /// + /// XmlWriter that these elements will be written to + protected virtual void GenerateSqlConnectionXml(XmlWriter xmlWriter) + { + XmlGeneratorHelper.GenerateSqlConnectionXml(xmlWriter, this.Context); + } + + #endregion + + #region item context generation + /// + /// Generate context specific to the node this menu item is being launched against. + /// + /// XmlWriter that these elements will be written to + protected virtual void GenerateItemContext(XmlWriter xmlWriter) + { + System.Diagnostics.Debug.Assert(xmlWriter != null, "xmlWriter should never be null."); + + // There are two ways we can add context information. + // The first is just off of the node we were launched against. We will break the urn down + // into it's individual components. And pass them to the dialog. + // The second is by performing a query relative to the node we were launched against + // and adding any urns that are returned. No other process will be performed on the urn + + // see if we are invoking on single, or multiple items + if (InvokeOnSingleItemOnly()) + { + // no query, just an individual item + GenerateIndividualItemContext(xmlWriter); + } + else + { + GenerateMultiItemContext(xmlWriter); + } + } + /// + /// Generate the context for an individual item. + /// While Generating the context we will break down the Urn to it's individual elements + /// and pass each Type attribute in individually. + /// + /// XmlWriter that these elements will be written to + protected virtual void GenerateIndividualItemContext(XmlWriter xmlWriter) + { + XmlGeneratorHelper.GenerateIndividualItemContext(xmlWriter, itemType, this.Context); + } + + /// + /// Generate Context for multiple items. + /// + /// XmlWriter that these elements will be written to + protected virtual void GenerateMultiItemContext(XmlWriter xmlWriter) + { + // there will be a query performed + GenerateItemContextFromQuery(xmlWriter); + } + + /// + /// Generate Context with the results of a Query. We will just pass in the multiple + /// Urn's if any that are the results of the query. + /// + /// XmlWriter that these elements will be written to + protected virtual void GenerateItemContextFromQuery(XmlWriter xmlWriter) + { + System.Diagnostics.Debug.Assert(xmlWriter != null, "xmlWriter should never be null."); + + // generate the request + Request request = new Request(); + // only need urn + request.Fields = new string[] { "Urn" }; + request.Urn = new Urn(this.Context.ContextUrn + "/" + this.invokeMultiChildQueryXPath); + + DataTable dt; + + // run the query + Enumerator enumerator = new Enumerator(); + EnumResult result = enumerator.Process(this.Context.Connection, request); + + if (result.Type == ResultType.DataTable) + { + dt = result; + } + else + { + dt = ((DataSet)result).Tables[0]; + } + + //TODO: Consider throwing if there are no results. + // Write the results to the XML document + foreach (DataRow row in dt.Rows) + { + WriteUrnInformation(xmlWriter, row[0].ToString()); + } + + } + /// + /// Writes a Urn to the XML. If this is an Olap connection we will also write out + /// the Olap Path, which is the AMO equivelent of a Urn. + /// + /// XmlWriter that these elements will be written to + /// Urn to be written + protected virtual void WriteUrnInformation(XmlWriter xmlWriter, string? urn) + { + XmlGeneratorHelper.WriteUrnInformation(xmlWriter, urn, this.Context); + } + /// + /// Get the list of Urn attributes for this item. + /// + /// Urn to be checked + /// string array of Urn attribute names. This can be zero length but will not be null + protected virtual string[] GetUrnAttributes(Urn urn) + { + string[]? urnAttributes = null; + + if (urn.XPathExpression != null && urn.XPathExpression.Length > 0) + { + int index = urn.XPathExpression.Length - 1; + if (index > 0) + { + System.Collections.SortedList list = urn.XPathExpression[index].FixedProperties; + System.Collections.ICollection keys = list.Keys; + + urnAttributes = new string[keys.Count]; + + int i = 0; + foreach (object o in keys) + { + string? key = o.ToString(); + if (key != null) + { + urnAttributes[i++] = key; + } + } + } + } + return urnAttributes != null ? urnAttributes : new string[0]; + } + #endregion + + #region write properties + /// + /// Write properties set for this menu item. These can be set to pass different information + /// to the dialog independently of the node type. + /// + /// XmlWriter that these elements will be written to + protected virtual void WritePropertiesToXml(XmlWriter xmlWriter) + { + System.Diagnostics.Debug.Assert(xmlWriter != null, "xmlWriter should never be null."); + + // mode could indicate properties or new + if (Mode != null && Mode.Length > 0) + { + xmlWriter.WriteElementString("mode", Mode); + } + // raw xml to be passed to the dialog. + // mostly used to control instance awareness. + if (rawXml != null && rawXml.Length > 0) + { + xmlWriter.WriteRaw(rawXml); + } + // mostly used to restrict the context for new item off of an item of that type + // some dialogs require this is passed in so they know what item type they are + // supposed to be creating. + if (this.itemType.Length > 0) + { + xmlWriter.WriteElementString("itemtype", this.itemType); + } + } + #endregion + #endregion + + #region protected helpers + /// + /// Inidicates whether the source is a single or multiple items. + /// + /// + protected virtual bool InvokeOnSingleItemOnly() + { + return (this.invokeMultiChildQueryXPath == null || this.invokeMultiChildQueryXPath.Length == 0); + } + #endregion + } + + /// + /// provides helper methods to generate LaunchForm XML and launch certain wizards and dialogs + /// + public static class XmlGeneratorHelper + { + /// + /// Write the starting elements needed by the dialog framework + /// + /// XmlWriter that these elements will be written to + public static void StartXmlDocument(XmlWriter xmlWriter) + { + System.Diagnostics.Debug.Assert(xmlWriter != null, "xmlWriter should never be null."); + + xmlWriter.WriteStartElement("formdescription"); + xmlWriter.WriteStartElement("params"); + } + + /// + /// Writes a Urn to the XML. If this is an Olap connection we will also write out + /// the Olap Path, which is the AMO equivelent of a Urn. + /// + /// XmlWriter that these elements will be written to + /// Urn to be written + public static void WriteUrnInformation(XmlWriter xmlWriter, string urn, ActionContext context) + { + System.Diagnostics.Debug.Assert(xmlWriter != null, "xmlWriter should never be null."); + + // write the Urn + xmlWriter.WriteElementString("urn", urn); + } + + /// + /// Generate the XML that will allow the dialog to connect to the server + /// + /// XmlWriter that these elements will be written to + public static void GenerateConnectionXml(XmlWriter xmlWriter, ActionContext context) + { + System.Diagnostics.Debug.Assert(xmlWriter != null, "xmlWriter should never be null."); + + // framework also needs to know the type + string serverType = string.Empty; + + // Generate Connection specific XML. + if (context.Connection is ServerConnection) + { + GenerateSqlConnectionXml(xmlWriter, context); + serverType = "sql"; + } + else + { + System.Diagnostics.Debug.Assert(false, "Warning: Connection type is unknown."); + } + + System.Diagnostics.Debug.Assert(serverType.Length > 0, "serverType has not been defined"); + xmlWriter.WriteElementString("servertype", serverType); + } + + /// + /// Generate SQL Server specific connection information + /// + /// XmlWriter that these elements will be written to + public static void GenerateSqlConnectionXml(XmlWriter xmlWriter, ActionContext context) + { + System.Diagnostics.Debug.Assert(xmlWriter != null, "xmlWriter should never be null."); + + // write the server name + xmlWriter.WriteElementString("servername", context.Connection.ServerInstance); + } + + /// + /// Generate the context for an individual item. + /// While Generating the context we will break down the Urn to it's individual elements + /// and pass each Type attribute in individually. + /// + /// XmlWriter that these elements will be written to + public static void GenerateIndividualItemContext(XmlWriter xmlWriter, string itemType, ActionContext context) + { + System.Diagnostics.Debug.Assert(xmlWriter != null, "xmlWriter should never be null."); + System.Diagnostics.Debug.Assert(context.ContextUrn != null, "No context available."); + + Urn urn = new Urn(context.ContextUrn); + + foreach (KeyValuePair item in ExtractUrnPart(itemType, urn)) + { + xmlWriter.WriteElementString(item.Key, item.Value); + } + + // if we are filtering out the information for this level (e.g. new database on a database should not + // pass in the information relating to the selected database. We need to make sure that the Urn we pass + // in is trimmed as well. + Urn sourceUrn = new Urn(context.ContextUrn); + if (itemType != null + && itemType.Length > 0 + && sourceUrn.Type == itemType) + { + sourceUrn = sourceUrn.Parent; + } + + // as well as breaking everything down we will write the Urn directly + // into the XML. Some dialogs will use the individual items, some will + // use the Urn. + WriteUrnInformation(xmlWriter, sourceUrn, context); + } + + public static IEnumerable> ExtractUrnPart(string itemType, Urn urn) + { + // break the urn up into individual xml elements, and add each item + // so Database[@Name='foo']/User[@Name='bar'] + // will become + // foo + // bar + // Note: We don't care about server. It is taken care of elsewhere. + // The dialogs need every item to be converted to lower case or they will not + // be able to retrieve the information. + do + { + // server information has already gone in, and is server type specific + // don't get it from the urn + if (urn.Parent != null) + { + // get the attributes for this part of the Urn. For Olap this is ID, for + // everything else it is usually Name, although Schema may also be used for SQL + string[] urnAttributes = UrnUtils.GetUrnAttributes(urn); + + // make sure we are not supposed to skip this type. The skip allows us to bring up a "new" + // dialog on an item of that type without passing in context. + // e.g. New Database... on AdventureWorks should not pass in AdventureWorks + if (string.Compare(urn.Type, itemType, StringComparison.OrdinalIgnoreCase) != 0) + { + for (int i = 0; i < urnAttributes.Length; ++i) + { + // Some Urn attributes require special handling. Don't ask me why + string thisUrnAttribute = urnAttributes[i].ToLower(CultureInfo.InvariantCulture); + string elementName; + switch (thisUrnAttribute) + { + case "schema": + case "categoryid": + elementName = thisUrnAttribute; + break; + default: + elementName = urn.Type.ToLower(CultureInfo.InvariantCulture); // I think it's always the same as thisUrnAttribute but I'm not sure + break; + } + yield return new KeyValuePair(elementName, urn.GetAttribute(urnAttributes[i])); + } + } + } + urn = urn.Parent; + } + while (urn != null); + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Management/Common/ManagementActionBase.cs b/src/Microsoft.SqlTools.ServiceLayer/Management/Common/ManagementActionBase.cs index 3eeeaa04..95bb73ef 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Management/Common/ManagementActionBase.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Management/Common/ManagementActionBase.cs @@ -207,7 +207,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management /// protected virtual bool DoPreProcessExecution(RunType runType, out ExecutionMode executionResult) { - //ask the framework to do normal execution by calling OnRunNOw methods + //ask the framework to do normal execution by calling OnRunNow methods //of the views one by one executionResult = ExecutionMode.Success; return true; diff --git a/src/Microsoft.SqlTools.ServiceLayer/Management/Common/UrnToDataPathConverter.cs b/src/Microsoft.SqlTools.ServiceLayer/Management/Common/UrnToDataPathConverter.cs new file mode 100644 index 00000000..6afbc5b8 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Management/Common/UrnToDataPathConverter.cs @@ -0,0 +1,135 @@ +// +// 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.IO; +using System.Text; +using System.Xml; + +using Microsoft.SqlServer.Management.Sdk.Sfc; + +namespace Microsoft.SqlTools.ServiceLayer.Management +{ + /// + /// Provides helper functions for converting urn enumerator urns + /// to olap path equivilent. + /// +#if DEBUG || EXPOSE_MANAGED_INTERNALS + public +#else + internal +#endif + class UrnDataPathConverter + { + // static only members + private UrnDataPathConverter() + { + } + + /// + /// Convert a Urn to and olap compatible datapath string + /// + /// Source urn + /// string that Xml that can be used as an olap path + /// + /// Node types are + /// ServerID + /// DatabaseID + /// CubeID + /// DimensionID + /// MeasureGroupID + /// PartitionID + /// MiningStructureID + /// MininingModelID + /// + /// These currently map mostly to enuerator types with the addition of ID + /// + /// string is of the format ObjectID�.ObjectID + /// + /// + public static string ConvertUrnToDataPath(Urn urn) + { + String element = String.Empty; + if(urn == null) + { + throw new ArgumentNullException("urn"); + } + + StringWriter stringWriter = new StringWriter(); + XmlTextWriter xmlWriter = new XmlTextWriter(stringWriter); + + ConvertUrnToDataPath(urn, xmlWriter); + + xmlWriter.Flush(); + xmlWriter.Close(); + + return stringWriter.ToString(); + } + /// + /// Datapath conversion helper. Does the conversion using XmlWriter and recursion. + /// + /// Urn to be converted + /// XmlWriter that the results will be written to. + private static void ConvertUrnToDataPath(Urn urn, XmlWriter xmlWriter) + { + if(urn == null) + { + throw new ArgumentNullException("urn"); + } + if(xmlWriter == null) + { + throw new ArgumentNullException("xmlWriter"); + } + + // preserve the order so do the parent first + Urn parent = urn.Parent; + if(parent != null) + { + ConvertUrnToDataPath(parent, xmlWriter); + } + + String tag = urn.Type; + + // don't put server into the olap path. + if(tag != "OlapServer") + { + xmlWriter.WriteElementString(tag + "ID", urn.GetAttribute("ID")); + } + } + /// + /// Convert an xml body string that is compatible with a string representation + /// (i.e. deal with < > &) + /// + /// source + /// string that can be used as the body for xml stored in a string + public static string TokenizeXml(string source) + { + System.Diagnostics.Debug.Assert(false, "do not use this function. See bugs 322423 and 115450 in SQLBU Defect tracking"); + + if(null == source) return String.Empty; + + StringBuilder sb = new StringBuilder(); + foreach(char c in source) + { + switch(c) + { + case '<': + sb.Append("<"); + break; + case '>': + sb.Append(">"); + break; + case '&': + sb.Append("&"); + break; + default: + sb.Append(c); + break; + } + } + return sb.ToString(); + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Management/Common/UrnUtils.cs b/src/Microsoft.SqlTools.ServiceLayer/Management/Common/UrnUtils.cs new file mode 100644 index 00000000..ebd575e6 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Management/Common/UrnUtils.cs @@ -0,0 +1,46 @@ +// +// 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 Microsoft.SqlServer.Management.Sdk.Sfc; + +namespace Microsoft.SqlTools.ServiceLayer.Management +{ + internal class UrnUtils + { + private UrnUtils () { } + + /// + /// Get the list of Urn attributes for this item. + /// + /// Urn to be checked + /// String array of Urn attribute names. This can be zero length but will not be null + public static string[] GetUrnAttributes(Urn urn) + { + String[]? urnAttributes = null; + + if(urn.XPathExpression != null && urn.XPathExpression.Length > 0) + { + int index = urn.XPathExpression.Length - 1; + if(index >= 0) + { + System.Collections.SortedList list = urn.XPathExpression[index].FixedProperties; + System.Collections.ICollection keys = list.Keys; + + urnAttributes = new String[keys.Count]; + + int i = 0; + foreach(object o in keys) + { + string? key = o.ToString(); + if (key != null) + urnAttributes[i++] = key; + } + } + } + return urnAttributes != null ? urnAttributes : new String[0]; + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/UserInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/UserInfo.cs index 158787af..5ab74266 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/UserInfo.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/UserInfo.cs @@ -58,7 +58,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts /// public class UserInfo { - DatabaseUserType? Type { get; set; } + public DatabaseUserType? Type { get; set; } + + public string UserName { get; set; } public string LoginName { get; set; } @@ -72,9 +74,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts public bool isAAD { get; set; } - public ExtendedProperty[] ExtendedProperties { get; set; } + public ExtendedProperty[]? ExtendedProperties { get; set; } - public SecurablePermissions[] SecurablePermissions { get; set; } + public SecurablePermissions[]? SecurablePermissions { get; set; } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/UserRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/UserRequest.cs new file mode 100644 index 00000000..99332e72 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/UserRequest.cs @@ -0,0 +1,66 @@ +// +// 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; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts +{ + /// + /// Create User parameters + /// + public class CreateUserParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + + public UserInfo User { get; set; } + } + + /// + /// Create User result + /// + public class CreateUserResult : ResultStatus + { + public UserInfo User { get; set; } + } + + + /// + /// Create User request type + /// + public class CreateUserRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("objectmanagement/createuser"); + } + + /// + /// Delete User params + /// + public class DeleteUserParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + + public string UserName { get; set; } + } + + /// + /// Delete User request type + /// + public class DeleteUserRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("objectmanagement/deleteuser"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/SecurityService.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/SecurityService.cs index a5572fef..b0bd7c33 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Security/SecurityService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Security/SecurityService.cs @@ -9,8 +9,8 @@ using System; using System.Collections; using System.Collections.Specialized; using System.Data; -using System.Security; using System.Threading.Tasks; +using System.Xml; using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Dmf; using Microsoft.SqlServer.Management.Sdk.Sfc; @@ -31,7 +31,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security { private bool disposed; - private ConnectionService connectionService = null; + private ConnectionService connectionService; private static readonly Lazy instance = new Lazy(() => new SecurityService()); @@ -93,6 +93,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Security // Login request handlers this.ServiceHost.SetRequestHandler(CreateLoginRequest.Type, HandleCreateLoginRequest, true); this.ServiceHost.SetRequestHandler(DeleteLoginRequest.Type, HandleDeleteLoginRequest, true); + + // User request handlers + this.ServiceHost.SetRequestHandler(CreateUserRequest.Type, HandleCreateUserRequest, true); } @@ -170,112 +173,73 @@ namespace Microsoft.SqlTools.ServiceLayer.Security #region "User Handlers" - private UserPrototype InitUserNew(CDataContainer dataContainer) + internal Task> ConfigureUser( + string ownerUri, + UserInfo user, + ConfigAction configAction, + RunType runType) { - // this.DataContainer = context; - // this.parentDbUrn = new Urn(this.DataContainer.ParentUrn); - // this.objectUrn = new Urn(this.DataContainer.ObjectUrn); - ExhaustiveUserTypes currentUserType; - UserPrototypeFactory userPrototypeFactory = UserPrototypeFactory.GetInstance(dataContainer); - - if (dataContainer.IsNewObject) + return Task>.Run(() => { - if (IsParentDatabaseContained(dataContainer.ParentUrn, dataContainer)) + try { - currentUserType = ExhaustiveUserTypes.SqlUserWithPassword; - } - else - { - currentUserType = ExhaustiveUserTypes.LoginMappedUser; - } - } - else - { - currentUserType = this.GetCurrentUserTypeForExistingUser( - dataContainer.Server.GetSmoObject(dataContainer.ObjectUrn) as User); - } - - UserPrototype currentUserPrototype = userPrototypeFactory.GetUserPrototype(currentUserType); - return currentUserPrototype; - } - - private ExhaustiveUserTypes GetCurrentUserTypeForExistingUser(User user) - { - switch (user.UserType) - { - case UserType.SqlUser: - if (user.IsSupportedProperty("AuthenticationType")) + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection(ownerUri, out connInfo); + if (connInfo == null) { - if (user.AuthenticationType == AuthenticationType.Windows) - { - return ExhaustiveUserTypes.WindowsUser; - } - else if (user.AuthenticationType == AuthenticationType.Database) - { - return ExhaustiveUserTypes.SqlUserWithPassword; - } + throw new ArgumentException("Invalid connection URI '{0}'", ownerUri); } - return ExhaustiveUserTypes.LoginMappedUser; - - case UserType.NoLogin: - return ExhaustiveUserTypes.SqlUserWithoutLogin; - - case UserType.Certificate: - return ExhaustiveUserTypes.CertificateMappedUser; - - case UserType.AsymmetricKey: - return ExhaustiveUserTypes.AsymmetricKeyMappedUser; - - default: - return ExhaustiveUserTypes.Unknown; - } + var serverConnection = ConnectionService.OpenServerConnection(connInfo, "DataContainer"); + var connectionInfoWithConnection = new SqlConnectionInfoWithConnection(); + connectionInfoWithConnection.ServerConnection = serverConnection; + + string urn = string.Format(System.Globalization.CultureInfo.InvariantCulture, + "Server/Database[@Name='{0}']", + Urn.EscapeString(serverConnection.DatabaseName)); + + ActionContext context = new ActionContext(serverConnection, "new_user", urn); + DataContainerXmlGenerator containerXml = new DataContainerXmlGenerator(context); + containerXml.AddProperty("itemtype", "User"); + + XmlDocument xmlDoc = containerXml.GenerateXmlDocument(); + bool objectExists = configAction != ConfigAction.Create; + CDataContainer dataContainer = CDataContainer.CreateDataContainer(connectionInfoWithConnection, xmlDoc); + + using (var actions = new UserActions(dataContainer, user, configAction)) + { + var executionHandler = new ExecutonHandler(actions); + executionHandler.RunNow(runType, this); + } + + return new Tuple(true, string.Empty); + } + catch (Exception ex) + { + return new Tuple(false, ex.ToString()); + } + }); } - private bool IsParentDatabaseContained(Urn parentDbUrn, CDataContainer dataContainer) + /// + /// Handle request to create a user + /// + internal async Task HandleCreateUserRequest(CreateUserParams parameters, RequestContext requestContext) { - string parentDbName = parentDbUrn.GetNameForType("Database"); - Database parentDatabase = dataContainer.Server.Databases[parentDbName]; + var result = await ConfigureUser(parameters.OwnerUri, + parameters.User, + ConfigAction.Create, + RunType.RunNow); - if (parentDatabase.IsSupportedProperty("ContainmentType") - && parentDatabase.ContainmentType == ContainmentType.Partial) + await requestContext.SendResult(new CreateUserResult() { - return true; - } - - return false; + User = parameters.User, + Success = result.Item1, + ErrorMessage = result.Item2 + }); } - private void GetUserTypeOptions(CDataContainer dataContainer) - { - if (SqlMgmtUtils.IsSql11OrLater(dataContainer.Server.ServerVersion) - && IsParentDatabaseContained(dataContainer.ParentUrn, dataContainer)) - { - // this.userTypeComboBox.Items.AddRange( - // new string[]{ - // UserSR.SqlUserWithPasswordUserTypeText - // } - //); - } - if (SqlMgmtUtils.IsYukonOrAbove(dataContainer.Server)) - { - // this.userTypeComboBox.Items.AddRange( - // new string[]{ - // UserSR.AsymmetricKeyUserTypeText, - // UserSR.CertificateUserTypeText, - // UserSR.WithoutLoginSqlUserTypeText, - // UserSR.WindowsUserTypeText - // } - // ); - } - // this.userTypeComboBox.Items.AddRange( - // new string[]{ - // UserSR.LoginMappedSqlUserTypeText - // } - // ); - } - private void GetDefaultLanguageOptions(CDataContainer dataContainer) { // this.defaultLanguageComboBox.Items.Clear(); @@ -298,65 +262,54 @@ namespace Microsoft.SqlTools.ServiceLayer.Security } } - private SecureString GetReadOnlySecureString(string secret) - { - SecureString ss = new SecureString(); - foreach (char c in secret.ToCharArray()) - { - ss.AppendChar(c); - } - ss.MakeReadOnly(); + // code needs to be ported into the useraction class + // public void UserMemberships_OnRunNow(object sender, CDataContainer dataContainer) + // { + // UserPrototype currentPrototype = UserPrototypeFactory.GetInstance(dataContainer).CurrentPrototype; - return ss; - } + // //In case the UserGeneral/OwnedSchemas pages are loaded, + // //those will takes care of applying membership changes also. + // //Hence, we only need to apply changes in this method when those are not loaded. + // if (!currentPrototype.IsRoleMembershipChangesApplied) + // { + // //base.OnRunNow(sender); - public void UserMemberships_OnRunNow(object sender, CDataContainer dataContainer) - { - UserPrototype currentPrototype = UserPrototypeFactory.GetInstance(dataContainer).CurrentPrototype; + // User user = currentPrototype.ApplyChanges(); - //In case the UserGeneral/OwnedSchemas pages are loaded, - //those will takes care of applying membership changes also. - //Hence, we only need to apply changes in this method when those are not loaded. - if (!currentPrototype.IsRoleMembershipChangesApplied) - { - //base.OnRunNow(sender); + // //this.ExecutionMode = ExecutionMode.Success; + // dataContainer.ObjectName = currentPrototype.Name; + // dataContainer.SqlDialogSubject = user; + // } - User user = currentPrototype.ApplyChanges(); + // //setting back to original after changes are applied + // currentPrototype.IsRoleMembershipChangesApplied = false; + // } - //this.ExecutionMode = ExecutionMode.Success; - dataContainer.ObjectName = currentPrototype.Name; - dataContainer.SqlDialogSubject = user; - } + // /// + // /// implementation of OnPanelRunNow + // /// + // /// + // public void UserOwnedSchemas_OnRunNow(object sender, CDataContainer dataContainer) + // { + // UserPrototype currentPrototype = UserPrototypeFactory.GetInstance(dataContainer).CurrentPrototype; - //setting back to original after changes are applied - currentPrototype.IsRoleMembershipChangesApplied = false; - } + // //In case the UserGeneral/Membership pages are loaded, + // //those will takes care of applying schema ownership changes also. + // //Hence, we only need to apply changes in this method when those are not loaded. + // if (!currentPrototype.IsSchemaOwnershipChangesApplied) + // { + // //base.OnRunNow(sender); - /// - /// implementation of OnPanelRunNow - /// - /// - public void UserOwnedSchemas_OnRunNow(object sender, CDataContainer dataContainer) - { - UserPrototype currentPrototype = UserPrototypeFactory.GetInstance(dataContainer).CurrentPrototype; + // User user = currentPrototype.ApplyChanges(); - //In case the UserGeneral/Membership pages are loaded, - //those will takes care of applying schema ownership changes also. - //Hence, we only need to apply changes in this method when those are not loaded. - if (!currentPrototype.IsSchemaOwnershipChangesApplied) - { - //base.OnRunNow(sender); + // //this.ExecutionMode = ExecutionMode.Success; + // dataContainer.ObjectName = currentPrototype.Name; + // dataContainer.SqlDialogSubject = user; + // } - User user = currentPrototype.ApplyChanges(); - - //this.ExecutionMode = ExecutionMode.Success; - dataContainer.ObjectName = currentPrototype.Name; - dataContainer.SqlDialogSubject = user; - } - - //setting back to original after changes are applied - currentPrototype.IsSchemaOwnershipChangesApplied = false; - } + // //setting back to original after changes are applied + // currentPrototype.IsSchemaOwnershipChangesApplied = false; + // } // how to populate defaults from prototype, will delete once refactored // private void InitializeValuesInUiControls() @@ -855,7 +808,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security Database database = dataContainer.Server.Databases[databaseName]; System.Diagnostics.Debug.Assert(database!= null, "database is null"); - DatabaseRole role = null; + DatabaseRole role; if (isPropertiesMode == true) // in properties mode -> alter role { @@ -888,7 +841,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security private void DbRole_LoadSchemas(string databaseName, string dbroleName, ServerConnection serverConnection) { bool isPropertiesMode = false; - HybridDictionary schemaOwnership = null; + HybridDictionary schemaOwnership; schemaOwnership = new HybridDictionary(); Enumerator en = new Enumerator(); diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/UserActions.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/UserActions.cs new file mode 100644 index 00000000..5f8f18f9 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Security/UserActions.cs @@ -0,0 +1,144 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlTools.ServiceLayer.Management; +using Microsoft.SqlTools.ServiceLayer.Security.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.Security +{ + internal class UserActions : ManagementActionBase + { +#region Variables + //private UserPrototypeData userData; + private UserPrototype userPrototype; + private UserInfo user; + private ConfigAction configAction; +#endregion + +#region Constructors / Dispose + /// + /// required when loading from Object Explorer context + /// + /// + public UserActions( + CDataContainer context, + UserInfo user, + ConfigAction configAction) + { + this.DataContainer = context; + this.user = user; + this.configAction = configAction; + + this.userPrototype = InitUserNew(context, user); + } + + // /// + // /// Clean up any resources being used. + // /// + // protected override void Dispose(bool disposing) + // { + // base.Dispose(disposing); + // } + +#endregion + + /// + /// called on background thread by the framework to execute the action + /// + /// + public override void OnRunNow(object sender) + { + if (this.configAction == ConfigAction.Drop) + { + // if (this.credentialData.Credential != null) + // { + // this.credentialData.Credential.DropIfExists(); + // } + } + else + { + this.userPrototype.ApplyChanges(); + } + } + + private UserPrototype InitUserNew(CDataContainer dataContainer, UserInfo user) + { + // this.DataContainer = context; + // this.parentDbUrn = new Urn(this.DataContainer.ParentUrn); + // this.objectUrn = new Urn(this.DataContainer.ObjectUrn); + ExhaustiveUserTypes currentUserType; + UserPrototypeFactory userPrototypeFactory = UserPrototypeFactory.GetInstance(dataContainer, user); + + if (dataContainer.IsNewObject) + { + if (IsParentDatabaseContained(dataContainer.ParentUrn, dataContainer)) + { + currentUserType = ExhaustiveUserTypes.SqlUserWithPassword; + } + else + { + currentUserType = ExhaustiveUserTypes.LoginMappedUser; + } + } + else + { + currentUserType = this.GetCurrentUserTypeForExistingUser( + dataContainer.Server.GetSmoObject(dataContainer.ObjectUrn) as User); + } + + UserPrototype currentUserPrototype = userPrototypeFactory.GetUserPrototype(currentUserType); + return currentUserPrototype; + } + + private ExhaustiveUserTypes GetCurrentUserTypeForExistingUser(User user) + { + switch (user.UserType) + { + case UserType.SqlUser: + if (user.IsSupportedProperty("AuthenticationType")) + { + if (user.AuthenticationType == AuthenticationType.Windows) + { + return ExhaustiveUserTypes.WindowsUser; + } + else if (user.AuthenticationType == AuthenticationType.Database) + { + return ExhaustiveUserTypes.SqlUserWithPassword; + } + } + + return ExhaustiveUserTypes.LoginMappedUser; + + case UserType.NoLogin: + return ExhaustiveUserTypes.SqlUserWithoutLogin; + + case UserType.Certificate: + return ExhaustiveUserTypes.CertificateMappedUser; + + case UserType.AsymmetricKey: + return ExhaustiveUserTypes.AsymmetricKeyMappedUser; + + default: + return ExhaustiveUserTypes.Unknown; + } + } + + private bool IsParentDatabaseContained(Urn parentDbUrn, CDataContainer dataContainer) + { + string parentDbName = parentDbUrn.GetNameForType("Database"); + Database parentDatabase = dataContainer.Server.Databases[parentDbName]; + + if (parentDatabase.IsSupportedProperty("ContainmentType") + && parentDatabase.ContainmentType == ContainmentType.Partial) + { + return true; + } + + return false; + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/UserData.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/UserData.cs index ec840add..d09f577c 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Security/UserData.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Security/UserData.cs @@ -5,11 +5,13 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Security; using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlServer.Management.Sdk.Sfc; using Microsoft.SqlTools.ServiceLayer.Management; -using System.Linq; +using Microsoft.SqlTools.ServiceLayer.Security.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; namespace Microsoft.SqlTools.ServiceLayer.Security { @@ -101,7 +103,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security this.isMember = new Dictionary(); } - public UserPrototypeData(CDataContainer context) + public UserPrototypeData(CDataContainer context, UserInfo userInfo) { this.isSchemaOwned = new Dictionary(); this.isMember = new Dictionary(); @@ -110,10 +112,17 @@ namespace Microsoft.SqlTools.ServiceLayer.Security { this.LoadUserData(context); } + else + { + this.name = userInfo.UserName; + this.mappedLoginName = userInfo.LoginName; + this.defaultSchemaName = userInfo.DefaultSchema; + this.password = DatabaseUtils.GetReadOnlySecureString(userInfo.Password); + } this.LoadRoleMembership(context); - this.LoadSchemaData(context); + this.LoadSchemaData(context); } public UserPrototypeData Clone() @@ -465,7 +474,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security var comparer = this.parent.GetStringComparer(); if (comparer.Compare(dbRole.Name, "public") != 0) { - this.roleNames.Add(dbRole.Name); + roleNames.Add(dbRole.Name); } } return roleNames; @@ -483,7 +492,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security foreach (Schema sch in this.parent.Schemas) { - this.schemaNames.Add(sch.Name); + schemaNames.Add(sch.Name); } return schemaNames; } @@ -539,7 +548,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security { enumerator.Reset(); - String? nullString = null; + string? nullString = null; while (enumerator.MoveNext()) { @@ -597,15 +606,13 @@ namespace Microsoft.SqlTools.ServiceLayer.Security } if ((this.currentState.userType == UserType.Certificate) - &&(!this.Exists || (user.Certificate != this.currentState.certificateName)) - ) + &&(!this.Exists || (user.Certificate != this.currentState.certificateName))) { user.Certificate = this.currentState.certificateName; } if ((this.currentState.userType == UserType.AsymmetricKey) - && (!this.Exists || (user.AsymmetricKey != this.currentState.asymmetricKeyName)) - ) + && (!this.Exists || (user.AsymmetricKey != this.currentState.asymmetricKeyName))) { user.AsymmetricKey = this.currentState.asymmetricKeyName; } @@ -621,7 +628,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security result = this.parent.Users[this.originalState.name]; result?.Refresh(); - System.Diagnostics.Debug.Assert(0 == String.Compare(this.originalState.name, this.currentState.name, StringComparison.Ordinal), "name of existing user has changed"); + System.Diagnostics.Debug.Assert(0 == string.Compare(this.originalState.name, this.currentState.name, StringComparison.Ordinal), "name of existing user has changed"); if (result == null) { throw new Exception(); @@ -756,7 +763,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security { //Default Schema was not supported before Denali for windows group. User user = this.GetUser(); - if (this.Exists && user.LoginType == LoginType.WindowsGroup) + if (this.Exists && user.LoginType == Microsoft.SqlServer.Management.Smo.LoginType.WindowsGroup) { return SqlMgmtUtils.IsSql11OrLater(this.context.Server.ConnectionContext.ServerVersion); } @@ -993,15 +1000,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Security } } - private UserPrototypeFactory(CDataContainer context) + private UserPrototypeFactory(CDataContainer context, UserInfo user) { this.context = context; - this.originalData = new UserPrototypeData(this.context); + this.originalData = new UserPrototypeData(this.context, user); this.currentData = this.originalData.Clone(); } - public static UserPrototypeFactory GetInstance(CDataContainer context) + public static UserPrototypeFactory GetInstance(CDataContainer context, UserInfo user) { if (singletonInstance != null && singletonInstance.context != context) @@ -1009,7 +1016,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security singletonInstance = null; } - singletonInstance ??= new UserPrototypeFactory(context); + singletonInstance ??= new UserPrototypeFactory(context, user); return singletonInstance; } @@ -1076,87 +1083,4 @@ namespace Microsoft.SqlTools.ServiceLayer.Security CertificateMappedUser, AsymmetricKeyMappedUser }; - - internal class LanguageUtils - { - /// - /// Gets alias for a language name. - /// - /// - /// - /// Returns string.Empty in case it doesn't find a matching languageName on the server - public static string GetLanguageAliasFromName(Server connectedServer, - string languageName) - { - string languageAlias = string.Empty; - - SetLanguageDefaultInitFieldsForDefaultLanguages(connectedServer); - - foreach (Language lang in connectedServer.Languages) - { - if (lang.Name == languageName) - { - languageAlias = lang.Alias; - break; - } - } - - return languageAlias; - } - - /// - /// Gets name for a language alias. - /// - /// - /// - /// Returns string.Empty in case it doesn't find a matching languageAlias on the server - public static string GetLanguageNameFromAlias(Server connectedServer, - string languageAlias) - { - string languageName = string.Empty; - - SetLanguageDefaultInitFieldsForDefaultLanguages(connectedServer); - - foreach (Language lang in connectedServer.Languages) - { - if (lang.Alias == languageAlias) - { - languageName = lang.Name; - break; - } - } - - return languageName; - } - - /// - /// Sets exhaustive fields required for displaying and working with default languages in server, - /// database and user dialogs as default init fields so that queries are not sent again and again. - /// - /// server on which languages will be enumerated - public static void SetLanguageDefaultInitFieldsForDefaultLanguages(Server connectedServer) - { - string[] fieldsNeeded = new string[] { "Alias", "Name", "LocaleID", "LangID" }; - connectedServer.SetDefaultInitFields(typeof(Language), fieldsNeeded); - } - } - - internal class ObjectNoLongerExistsException : Exception - { - private static string ExceptionMessage - { - get - { - return "Object no longer exists"; - } - } - - public ObjectNoLongerExistsException() - : base(ExceptionMessage) - { - // - // TODO: Add constructor logic here - // - } - } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Utility/DatabaseUtils.cs b/src/Microsoft.SqlTools.ServiceLayer/Utility/DatabaseUtils.cs index a7bacd45..7e6d2895 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Utility/DatabaseUtils.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Utility/DatabaseUtils.cs @@ -9,6 +9,7 @@ using Microsoft.SqlTools.ServiceLayer.Management; using System; using System.Collections.Generic; using System.IO; +using System.Security; namespace Microsoft.SqlTools.ServiceLayer.Utility { @@ -67,5 +68,17 @@ namespace Microsoft.SqlTools.ServiceLayer.Utility parameters.Add($"{paramName}", contentBytes); return $"@{paramName}"; } + + public static SecureString GetReadOnlySecureString(string secret) + { + SecureString ss = new SecureString(); + foreach (char c in secret.ToCharArray()) + { + ss.AppendChar(c); + } + ss.MakeReadOnly(); + + return ss; + } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Utility/LanguageUtils.cs b/src/Microsoft.SqlTools.ServiceLayer/Utility/LanguageUtils.cs new file mode 100644 index 00000000..8560c1fc --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Utility/LanguageUtils.cs @@ -0,0 +1,185 @@ +// +// 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 Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Smo; + +namespace Microsoft.SqlTools.ServiceLayer.Utility +{ + /// + /// Summary description for CUtils. + /// + internal class LanguageUtils + { + /// + /// Gets alias for a language name. + /// + /// + /// + /// Returns string.Empty in case it doesn't find a matching languageName on the server + public static string GetLanguageAliasFromName(Server connectedServer, + string languageName) + { + string languageAlias = string.Empty; + + SetLanguageDefaultInitFieldsForDefaultLanguages(connectedServer); + + foreach (Language lang in connectedServer.Languages) + { + if (lang.Name == languageName) + { + languageAlias = lang.Alias; + break; + } + } + + return languageAlias; + } + + /// + /// Gets name for a language alias. + /// + /// + /// + /// Returns string.Empty in case it doesn't find a matching languageAlias on the server + public static string GetLanguageNameFromAlias(Server connectedServer, + string languageAlias) + { + string languageName = string.Empty; + + SetLanguageDefaultInitFieldsForDefaultLanguages(connectedServer); + + foreach (Language lang in connectedServer.Languages) + { + if (lang.Alias == languageAlias) + { + languageName = lang.Name; + break; + } + } + + return languageName; + } + + /// + /// Gets lcid for a languageId. + /// + /// + /// + /// Throws exception in case it doesn't find a matching languageId on the server + public static int GetLcidFromLangId(Server connectedServer, + int langId) + { + int lcid = -1; //Unacceptable Lcid. + + SetLanguageDefaultInitFieldsForDefaultLanguages(connectedServer); + + foreach (Language lang in connectedServer.Languages) + { + if (lang.LangID == langId) + { + lcid = lang.LocaleID; + break; + } + } + + if (lcid == -1) //Ideally this will never happen. + { + throw new ArgumentOutOfRangeException("langId", "This language id is not present in sys.syslanguages catalog."); + } + + return lcid; + } + + /// + /// Gets languageId for a lcid. + /// + /// + /// + /// Throws exception in case it doesn't find a matching lcid on the server + public static int GetLangIdFromLcid(Server connectedServer, + int lcid) + { + int langId = -1; //Unacceptable LangId. + + SetLanguageDefaultInitFieldsForDefaultLanguages(connectedServer); + + foreach (Language lang in connectedServer.Languages) + { + if (lang.LocaleID == lcid) + { + langId = lang.LangID; + break; + } + } + + if (langId == -1) //Ideally this will never happen. + { + throw new ArgumentOutOfRangeException("lcid", "This locale id is not present in sys.syslanguages catalog."); + } + + return langId; + } + + /// + /// returns a language choice alias for that language + /// + /// + /// + public static LanguageChoice GetLanguageChoiceAlias(Server connectedServer, + int lcid) + { + SetLanguageDefaultInitFieldsForDefaultLanguages(connectedServer); + + foreach (Language smoL in connectedServer.Languages) + { + if (smoL.LocaleID == lcid) + { + string alias = smoL.Alias; + return new LanguageChoice(alias, lcid); + } + } + return new LanguageChoice(String.Empty, lcid); + } + + /// + /// Sets exhaustive fields required for displaying and working with default languages in server, + /// database and user dialogs as default init fields so that queries are not sent again and again. + /// + /// server on which languages will be enumerated + public static void SetLanguageDefaultInitFieldsForDefaultLanguages(Server connectedServer) + { + string[] fieldsNeeded = new string[] { "Alias", "Name", "LocaleID", "LangID" }; + connectedServer.SetDefaultInitFields(typeof(Language), fieldsNeeded); + } + } + + #region interface - ILanguageLcidWithConnectionInfo - used by property editors to talk with data object + interface ILanguageLcidWithConnectionInfo + { + int Lcid { get; } + ServerConnection Connection { get; } + } + #endregion + + #region class - LanguageChoice + internal class LanguageChoice + { + public string alias; + public System.Int32 lcid; + public LanguageChoice(string alias, System.Int32 lcid) + { + this.alias = alias; + this.lcid = lcid; + } + + public override string ToString() + { + return alias; + } + } + #endregion +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Utility/NameObjectCollection.cs b/src/Microsoft.SqlTools.ServiceLayer/Utility/NameObjectCollection.cs new file mode 100644 index 00000000..050430a3 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Utility/NameObjectCollection.cs @@ -0,0 +1,562 @@ +// +// 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 System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Text; + +using Microsoft.SqlServer.Management.Sdk.Sfc; + +namespace Microsoft.SqlTools.ServiceLayer.Utility +{ + /// + /// Provides a sorted collection of associated String keys and Object values that can be accessed either with the key or with the index. + /// + public class NameObjectCollection + { + #region struct NameValuePair + + [DebuggerDisplay("{Name}:{Value}")] + internal struct NameValuePair + { + private string name; + private object value; + + internal NameValuePair(string name) + { + this.name = name; + this.value = null; + } + + internal NameValuePair(string name, object value) + { + this.name = name; + this.value = ConvertValue(value); + } + + internal string Name + { + get { return this.name; } + } + + internal object Value + { + get { return this.value; } + set { this.value = ConvertValue(value); } + } + + public override bool Equals(object obj) + { + if (obj is NameValuePair) + { + return Equals((NameValuePair)obj); + } + return false; + } + + public bool Equals(NameValuePair other) + { + return Name == other.Name; + } + + public override int GetHashCode() + { + return this.name.GetHashCode(); + } + + public static bool operator ==(NameValuePair p1, NameValuePair p2) + { + return p1.Equals(p2); + } + + public static bool operator !=(NameValuePair p1, NameValuePair p2) + { + return !p1.Equals(p2); + } + + private static object ConvertValue(object value) + { + // Depending of whether values come from a DataTable or + // a data reader, some property values can be either enum values or integers. + // For example the value of LoginType can be either SqlLogin or 1. + // Enums cause a problems because they gets converted to a string rather than an + // integer value in OE expression evaluation code. + // Since originally all the values came from data tables and the rest of OE code expects + // integers we are going to conver any enum valus to integers here + if (value != null && value.GetType().IsEnum) + { + value = Convert.ToInt32(value); + } + + return value; + } + } + #endregion + + #region Property + + internal class Property : ISfcProperty + { + private NameValuePair pair; + + internal Property(NameValuePair pair) + { + this.pair = pair; + } + + #region ISfcProperty implementation + /// + /// Name of property + /// + public string Name + { + get { return pair.Name; } + } + + /// + /// Type of property + /// + public Type Type + { + get { return pair.Value.GetType(); } + } + + /// + /// Check whether the value is enabled or not + /// + public bool Enabled + { + get { return true; } + } + + /// + /// Value of property + /// + public object Value + { + get { return pair.Value; } + set { throw new NotSupportedException(); } + } + + /// + /// Indicates whether the property is required to persist the current state of the object + /// + public bool Required + { + get { return false; } + } + + /// + /// Indicates that Consumer should be theat this property as read-only + /// + public bool Writable + { + get { return false; } + } + + /// + /// Indicates whether the property value has been changed. + /// + public bool Dirty + { + get { return false; } + } + + /// + /// Indicates whether the properties data has been read, and is null + /// + public bool IsNull + { + get { return pair.Value == null || pair.Value is DBNull; } + } + + /// + /// Aggregated list of custom attributes associated with property + /// + public AttributeCollection Attributes + { + get { return null; } + } + + #endregion + } + + #endregion + + private List pairs; + + /// + /// Initializes a new instance of the NameObjectCollection class that is empty. + /// + public NameObjectCollection() + { + this.pairs = new List(); + } + + /// + /// Initializes a new instance of the NameObjectCollection class that is empty and has the specified initial capacity. + /// + /// The approximate number of entries that the NameObjectCollection instance can initially contain. + public NameObjectCollection(int capacity) + { + this.pairs = new List(capacity); + } + + /// + /// Adds an entry with the specified key and value into the NameObjectCollection instance. + /// + /// The String key of the entry to add. The key can be null. + /// The Object value of the entry to add. The value can be null. + public void Add(string name, object value) + { + this.pairs.Add(new NameValuePair(name, value)); + } + + /// + /// Removes all entries from the NameObjectCollection instance. + /// + public void Clear() + { + this.pairs.Clear(); + } + + /// + /// Gets the value of the specified entry from the NameObjectCollection instance. C# indexer + /// + public object this[int index] + { + get + { + return Get(index); + } + set + { + Set(index, value); + } + } + + /// + /// Gets the value of the specified entry from the NameObjectCollection instance. C# indexer + /// + public object this[string name] + { + get + { + return Get(name); + } + set + { + Set(name, value); + } + } + + /// + /// Gets the value of the specified entry from the NameObjectCollection instance. + /// + /// Gets the value of the entry at the specified index of the NameObjectCollection instance. + /// An Object that represents the value of the first entry with the specified key, if found; otherwise null + public object Get(int index) + { + return this.pairs[index].Value; + } + + /// + /// Gets the value of the first entry with the specified key from the NameObjectCollection instance. + /// + /// The String key of the entry to get. The key can be a null reference (Nothing in Visual Basic). + /// An Object that represents the value of the first entry with the specified key, if found; otherwise null + public object Get(string name) + { + int index = IndexOf(name); + return index >= 0 ? this.pairs[index].Value : null; + } + + /// + /// Returns a String array that contains all the keys in the NameObjectCollection instance. + /// + /// A String array that contains all the keys in the NameObjectCollection instance. + public string[] GetAllKeys() + { + string[] keys = new string[this.pairs.Count]; + + for (int i = 0; i < this.pairs.Count; i++) + { + keys[i] = this.pairs[i].Name; + } + + return keys; + } + + /// + /// Returns an array that contains all the values in the NameObjectCollection instance. + /// + /// An Object array that contains all the values in the NameObjectCollection instance. + public object[] GetAllValues() + { + object[] values = new object[this.pairs.Count]; + + for (int i = 0; i < this.pairs.Count; i++) + { + values[i] = this.pairs[i].Value; + } + + return values; + } + + /// + /// Gets the key of the entry at the specified index of the NameObjectCollection instance. + /// + /// The zero-based index of the key to get. + /// A String that represents the key of the entry at the specified index. + public string GetKey(int index) + { + return this.pairs[index].Name; + } + + /// + /// Gets a value indicating whether the NameObjectCollection instance contains entries whose keys are not null. + /// + /// true if the NameObjectCollection instance contains entries whose keys are not a null reference (Nothing in Visual Basic); otherwise, false. + public bool HasKeys() + { + return this.pairs.Count > 0; + } + + /// + /// Removes the entries with the specified key from the NameObjectCollection instance. + /// + /// + public void Remove(string name) + { + RemoveAt(IndexOf(name)); + } + + /// + /// Removes the entry at the specified index of the NameObjectCollection instance. + /// + /// The zero-based index of the entry to remove. + public void RemoveAt(int index) + { + this.pairs.RemoveAt(index); + } + + /// + /// Sets the value of the entry at the specified index of the NameObjectCollection instance. + /// + /// The zero-based index of the entry to set. + /// The Object that represents the new value of the entry to set. The value can be null. + public void Set(int index, object value) + { + NameValuePair pair = this.pairs[index]; + pair.Value = value; + this.pairs[index] = pair; + } + + /// + /// Sets the value of the first entry with the specified key in the NameObjectCollection instance, if found; otherwise, adds an entry with the specified key and value into the NameObjectCollection instance. + /// + /// The String key of the entry to set. The key can be null. + /// The Object that represents the new value of the entry to set. The value can be null. + public void Set(string name, object value) + { + int index = IndexOf(name); + if (index >= 0) + { + Set(index, value); + } + else + { + Add(name, value); + } + } + + /// + /// Copies elements of this collection to an Array starting at a particular array index + /// + /// The one-dimensional Array that is the destination of the elements copied from NameObjectCollection. The Array must have zero-based indexing. + /// The zero-based index in array at which copying begins. + public void CopyTo(object[] array, int index) + { + GetAllValues().CopyTo(array, index); + } + + /// + /// Gets internal index of NameValuePair + /// + /// + /// + private int IndexOf(string name) + { + // This version does simple iteration. It relies on GetHashCode for optimization + NameValuePair pair = new NameValuePair(name); + for (int i = 0; i < this.pairs.Count; i++) + { + if (this.pairs[i].Equals(pair)) + { + return i; + } + } + return -1; + } + + #region ISfcPropertySet implementation + + /// + /// Checks if the property with specified name exists + /// + /// property name + /// true if succeeded + public bool Contains(string propertyName) + { + return IndexOf(propertyName) >= 0; + } + + /// + /// Checks if the property with specified metadata exists + /// + /// Property + /// true if succeeded + public bool Contains(ISfcProperty property) + { + return Contains(property.Name); + } + + /// + /// Checks if the property with specified name and type exists + /// + /// property type + /// property name + /// true if succeeded + public bool Contains(string name) + { + int index = IndexOf(name); + return index >= 0 && this.pairs[index].Value != null && this.pairs[index].Value.GetType() == typeof(T); + } + + /// + /// Attempts to get property value from provider + /// + /// property type + /// name name + /// property value + /// true if succeeded + public bool TryGetPropertyValue(string name, out T value) + { + value = default(T); + int index = IndexOf(name); + + if (index >= 0) + { + value = (T)this.pairs[index].Value; + return true; + } + return false; + } + + /// + /// Attempts to get property value from provider + /// + /// property name + /// property value + /// true if succeeded + public bool TryGetPropertyValue(string name, out object value) + { + value = null; + int index = IndexOf(name); + + if (index >= 0) + { + value = this.pairs[index].Value; + return true; + } + return false; + } + + /// + /// Attempts to get property metadata + /// + /// property name + /// propetty information + /// + public bool TryGetProperty(string name, out ISfcProperty property) + { + property = null; + int index = IndexOf(name); + + if (index >= 0) + { + property = new Property(this.pairs[index]); + return true; + } + return false; + } + + /// + /// Enumerates all properties + /// + /// + public IEnumerable EnumProperties() + { + foreach (NameValuePair pair in this.pairs) + { + yield return new Property(pair); + } + } + + #endregion + + #region ICollection implementation + + public void CopyTo(Array array, int index) + { + Array.Copy(this.pairs.ToArray(), array, index); + } + + public IEnumerator GetEnumerator() + { + // Existing code expects this to enumerate property names + return GetAllKeys().GetEnumerator(); + } + + public int Count + { + get { return this.pairs.Count; } + } + + public bool IsSynchronized + { + get { return false; } + } + + public object SyncRoot + { + get { return null; } + } + + + #endregion + + public override string ToString() + { + StringBuilder textBuilder = new StringBuilder(); + + foreach (NameValuePair pair in this.pairs) + { + if (textBuilder.Length > 0) + { + textBuilder.Append(", "); + } + textBuilder.AppendFormat("{0}={1}", pair.Name, pair.Value); + } + + return textBuilder.ToString(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/SecurityTestUtils.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/SecurityTestUtils.cs index d87ef3e0..ce7c6ef1 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/SecurityTestUtils.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/SecurityTestUtils.cs @@ -47,6 +47,23 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Security }; } + internal static UserInfo GetTestUserInfo(string loginName) + { + return new UserInfo() + { + Type = DatabaseUserType.UserWithLogin, + UserName = "TestUserName_" + new Random().NextInt64(10000000,90000000).ToString(), + LoginName = loginName, + Password = "placeholder", + DefaultSchema = "dbo", + OwnedSchemas = new string[] { "dbo" }, + isEnabled = false, + isAAD = false, + ExtendedProperties = null, + SecurablePermissions = null + }; + } + internal static CredentialInfo GetTestCredentialInfo() { return new CredentialInfo() diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/UserTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/UserTests.cs new file mode 100644 index 00000000..2ec90be0 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/UserTests.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading.Tasks; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; +using Microsoft.SqlTools.ServiceLayer.Security; +using Microsoft.SqlTools.ServiceLayer.Security.Contracts; +using Microsoft.SqlTools.ServiceLayer.Test.Common; +using Moq; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Security +{ + /// + /// Tests for the User management component + /// + public class UserTests + { + /// + /// Test the basic Create User method handler + /// + // [Test] + public async Task TestHandleCreateUserRequest() + { + using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile()) + { + // setup + var connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", queryTempFile.FilePath); + var loginParams = new CreateLoginParams + { + OwnerUri = connectionResult.ConnectionInfo.OwnerUri, + Login = SecurityTestUtils.GetTestLoginInfo() + }; + + var createLoginContext = new Mock>(); + createLoginContext.Setup(x => x.SendResult(It.IsAny())) + .Returns(Task.FromResult(new object())); + + // call the create login method + SecurityService service = new SecurityService(); + await service.HandleCreateLoginRequest(loginParams, createLoginContext.Object); + + // verify the result + createLoginContext.Verify(x => x.SendResult(It.Is + (p => p.Success && p.Login.LoginName != string.Empty))); + + + var userParams = new CreateUserParams + { + OwnerUri = connectionResult.ConnectionInfo.OwnerUri, + User = SecurityTestUtils.GetTestUserInfo(loginParams.Login.LoginName) + }; + + var createUserContext = new Mock>(); + createUserContext.Setup(x => x.SendResult(It.IsAny())) + .Returns(Task.FromResult(new object())); + + // call the create login method + await service.HandleCreateUserRequest(userParams, createUserContext.Object); + + // verify the result + createUserContext.Verify(x => x.SendResult(It.Is + (p => p.Success && p.User.UserName != string.Empty))); + } + } + } +}