diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs index c62f3bec..13e2fcb0 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs @@ -1269,6 +1269,14 @@ namespace Microsoft.SqlTools.ServiceLayer } } + public static string SchemaHierarchy_BuiltInSchema + { + get + { + return Keys.GetString(Keys.SchemaHierarchy_BuiltInSchema); + } + } + public static string SchemaHierarchy_Security { get @@ -10674,6 +10682,9 @@ namespace Microsoft.SqlTools.ServiceLayer public const string SchemaHierarchy_Schemas = "SchemaHierarchy_Schemas"; + public const string SchemaHierarchy_BuiltInSchema = "SchemaHierarchy_BuiltInSchema"; + + public const string SchemaHierarchy_Security = "SchemaHierarchy_Security"; diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx index 06cd6530..5892faaa 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx @@ -897,6 +897,10 @@ Schemas + + Built-in Schemas + + Security diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings index 3de42856..cc33a7f3 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings @@ -440,6 +440,8 @@ SchemaHierarchy_Rules = Rules SchemaHierarchy_Schemas = Schemas +SchemaHierarchy_BuiltInSchema = Built-in Schemas + SchemaHierarchy_Security = Security SchemaHierarchy_ServerObjects = Server Objects diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf index 44e32fc0..6ae5190a 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf @@ -6545,6 +6545,11 @@ The Query Processor estimates that implementing the following index could improv Sequence Number End + + Built-in Schemas + Built-in Schemas + + \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Nodes/NodePropertyFilter.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Nodes/NodePropertyFilter.cs index e8a37eb1..045d4e1a 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Nodes/NodePropertyFilter.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Nodes/NodePropertyFilter.cs @@ -41,6 +41,18 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes /// public Type TypeToReverse { get; set; } = default!; + /// + /// Indicates if the filter is a "not" filter. Eg (not(@IsSystemObject = 0)) + /// + public bool IsNotFilter { get; set; } = false; + + /// + /// Indicates the type of the filter. It can be EQUALS, DATETIME, FALSE or CONTAINS + /// More information can be found here: + /// https://learn.microsoft.com/en-us/sql/powershell/query-expressions-and-uniform-resource-names?view=sql-server-ver16#examples + /// + public FilterType FilterType { get; set; } = FilterType.EQUALS; + /// /// Returns true if the filter can be apply to the given type and Server type /// @@ -82,8 +94,35 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes propertyValue = (int)Convert.ChangeType(value, Type); } + string filterText = string.Empty; + switch (FilterType) + { + case FilterType.EQUALS: + filterText = $"@{Property} = {propertyValue}"; + break; + case FilterType.DATETIME: + filterText = $"@{Property} = datetime({propertyValue})"; + break; + case FilterType.CONTAINS: + filterText = $"contains(@{Property}, {propertyValue})"; + break; + case FilterType.FALSE: + filterText = $"@{Property} = false()"; + break; + case FilterType.ISNULL: + filterText = $"isnull(@{Property})"; + break; + } + string orPrefix = filter.Length == 0 ? string.Empty : " or "; - filter.Append($"{orPrefix}@{Property} = {propertyValue}"); + if (IsNotFilter) + { + filter.Append($"{orPrefix}not({filterText})"); + } + else + { + filter.Append($"{orPrefix}{filterText}"); + } } if (filter.Length != 0) @@ -93,4 +132,13 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes return string.Empty; } } + + public enum FilterType + { + EQUALS, + DATETIME, + CONTAINS, + FALSE, + ISNULL + } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Nodes/NodeTypes.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Nodes/NodeTypes.cs index 2f2f1038..d939f375 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Nodes/NodeTypes.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Nodes/NodeTypes.cs @@ -22,6 +22,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes Assemblies, AsymmetricKeys, BrokerPriorities, + BuiltInSchemas, Certificates, ColumnEncryptionKeys, ColumnMasterKeys, diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/SmoTreeNodes.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/SmoTreeNodes.cs index 9764058b..c7017279 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/SmoTreeNodes.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/SmoTreeNodes.cs @@ -680,6 +680,78 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel public override bool PutFoldersAfterNodes => true; public override IEnumerable ApplicableParents() { return new[] { nameof(NodeTypes.Database) }; } + public override IEnumerable Filters + { + get + { + var filters = new List(); + filters.Add(new NodePropertyFilter + { + Property = "Name", + Type = typeof(string), + IsNotFilter = true, + Values = new List { "db_accessadmin" }, + }); + filters.Add(new NodePropertyFilter + { + Property = "Name", + Type = typeof(string), + IsNotFilter = true, + Values = new List { "db_backupoperator" }, + }); + filters.Add(new NodePropertyFilter + { + Property = "Name", + Type = typeof(string), + IsNotFilter = true, + Values = new List { "db_datareader" }, + }); + filters.Add(new NodePropertyFilter + { + Property = "Name", + Type = typeof(string), + IsNotFilter = true, + Values = new List { "db_datawriter" }, + }); + filters.Add(new NodePropertyFilter + { + Property = "Name", + Type = typeof(string), + IsNotFilter = true, + Values = new List { "db_ddladmin" }, + }); + filters.Add(new NodePropertyFilter + { + Property = "Name", + Type = typeof(string), + IsNotFilter = true, + Values = new List { "db_denydatareader" }, + }); + filters.Add(new NodePropertyFilter + { + Property = "Name", + Type = typeof(string), + IsNotFilter = true, + Values = new List { "db_denydatawriter" }, + }); + filters.Add(new NodePropertyFilter + { + Property = "Name", + Type = typeof(string), + IsNotFilter = true, + Values = new List { "db_owner" }, + }); + filters.Add(new NodePropertyFilter + { + Property = "Name", + Type = typeof(string), + IsNotFilter = true, + Values = new List { "db_securityadmin" }, + }); + return filters; + } + } + protected override void OnExpandPopulateFolders(IList currentChildren, TreeNode parent) { if (!WorkspaceService.Instance.CurrentSettings.SqlTools.ObjectExplorer.GroupBySchema) @@ -708,6 +780,15 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel IsSystemObject = false, ValidFor = ValidForFlag.AllOnPrem|ValidForFlag.AzureV12, SortPriority = SmoTreeNode.NextSortPriority, + }); + } + if (WorkspaceService.Instance.CurrentSettings.SqlTools.ObjectExplorer.GroupBySchema) + { + currentChildren.Add(new FolderNode { + NodeValue = SR.SchemaHierarchy_BuiltInSchema, + NodeTypeId = NodeTypes.BuiltInSchemas, + IsSystemObject = false, + SortPriority = SmoTreeNode.NextSortPriority, }); } currentChildren.Add(new FolderNode { @@ -767,6 +848,96 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel } } + [Export(typeof(ChildFactory))] + [Shared] + internal partial class BuiltInSchemasChildFactory : SmoChildFactoryBase + { + public override IEnumerable ApplicableParents() { return new[] { nameof(NodeTypes.BuiltInSchemas) }; } + + public override IEnumerable Filters + { + get + { + var filters = new List(); + filters.Add(new NodeOrFilter + { + FilterList = new List { + new NodePropertyFilter + { + Property = "Name", + Type = typeof(string), + Values = new List { "db_accessadmin" }, + }, + new NodePropertyFilter + { + Property = "Name", + Type = typeof(string), + Values = new List { "db_backupoperator" }, + }, + new NodePropertyFilter + { + Property = "Name", + Type = typeof(string), + Values = new List { "db_datareader" }, + }, + new NodePropertyFilter + { + Property = "Name", + Type = typeof(string), + Values = new List { "db_datawriter" }, + }, + new NodePropertyFilter + { + Property = "Name", + Type = typeof(string), + Values = new List { "db_ddladmin" }, + }, + new NodePropertyFilter + { + Property = "Name", + Type = typeof(string), + Values = new List { "db_denydatareader" }, + }, + new NodePropertyFilter + { + Property = "Name", + Type = typeof(string), + Values = new List { "db_denydatawriter" }, + }, + new NodePropertyFilter + { + Property = "Name", + Type = typeof(string), + Values = new List { "db_owner" }, + }, + new NodePropertyFilter + { + Property = "Name", + Type = typeof(string), + Values = new List { "db_securityadmin" }, + }, + } + }); + return filters; + } + } + + internal override Type[] ChildQuerierTypes + { + get + { + return new [] { typeof(SqlSchemaQuerier), }; + } + } + + public override TreeNode CreateChild(TreeNode parent, object context) + { + var child = new ExpandableSchemaTreeNode(); + InitializeChild(parent, child, context); + return child; + } + } + [Export(typeof(ChildFactory))] [Shared] internal partial class ExpandableSchemaChildFactory : SmoChildFactoryBase diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/SmoTreeNodes.tt b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/SmoTreeNodes.tt index 7f41c0e8..b4f29f82 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/SmoTreeNodes.tt +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/SmoTreeNodes.tt @@ -621,6 +621,8 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel var propertyType = filter.GetAttribute("Type"); var propertyValue = filter.GetAttribute("Value"); var validFor = filter.GetAttribute("ValidFor"); + var filterType = filter.GetAttribute("FilterType"); + var isNotFiler = filter.GetAttribute("IsNotFilter"); var typeToReverse = filter.GetAttribute("TypeToReverse"); List filterValues = GetNodeFilterValues(xmlFile, parentName, propertyName, orFilter); @@ -637,9 +639,24 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel { WriteLine(indent + " ValidFor = {0},", GetValidForFlags(validFor)); } + if (!string.IsNullOrWhiteSpace(filterType)) + { + WriteLine(indent + " FilterType = FilterType.{0},", filterType.ToUpper()); + } + if (!string.IsNullOrWhiteSpace(isNotFiler)) + { + WriteLine(indent + " IsNotFilter = {0},", isNotFiler); + } if (propertyValue != null && (filterValues == null || filterValues.Count == 0)) { - WriteLine(indent + " Values = new List {{ {0} }},", propertyValue); + if (propertyType.Equals("string")) + { + WriteLine(indent + " Values = new List {{ \"{0}\" }},", propertyValue); + } + else + { + WriteLine(indent + " Values = new List {{ {0} }},", propertyValue); + } } if (filterValues != null && filterValues.Count > 0) { @@ -650,7 +667,14 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel { string separator = (i != filterValues.Count - 1) ? "," : ""; var filterValue = filterValues[i]; - WriteLine(indent + " {{ {0} }}{1}", filterValue.InnerText, separator); + if(propertyType.Equals("string")) + { + WriteLine(indent + " {{ \"{0}\" }}{1}", filterValue.InnerText, separator); + } + else + { + WriteLine(indent + " {{ {0} }}{1}", filterValue.InnerText, separator); + } } WriteLine(indent + " }"); diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/SmoTreeNodesDefinition.xml b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/SmoTreeNodesDefinition.xml index 7e31f73c..529cfa02 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/SmoTreeNodesDefinition.xml +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/SmoModel/SmoTreeNodesDefinition.xml @@ -64,9 +64,26 @@ + + + + + + + + + + + + + @@ -74,6 +91,22 @@ + + + + + + + + + + + + + + + + diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectExplorer/ObjectExplorerServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectExplorer/ObjectExplorerServiceTests.cs index 27767f01..a8a68f3e 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectExplorer/ObjectExplorerServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectExplorer/ObjectExplorerServiceTests.cs @@ -255,6 +255,46 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectExplorer }); } + [Test] + public async Task GroupBySchemaHidesLegacySchemas() + { + string query = @"Create schema t1 + GO + Create schema t2 + GO"; + string databaseName = "#testDb#"; + await RunTest(databaseName, query, "TestDb", async (testDbName, session) => + { + WorkspaceService.Instance.CurrentSettings.SqlTools.ObjectExplorer = new ObjectExplorerSettings() { GroupBySchema = true }; + var databaseNode = session.Root.ToNodeInfo(); + var databaseChildren = await _service.ExpandNode(session, databaseNode.NodePath); + Assert.True(databaseChildren.Nodes.Any(t => t.Label == "t1"), "Non legacy schema node t1 should be found in database node when group by schema is enabled"); + Assert.True(databaseChildren.Nodes.Any(t => t.Label == "t2"), "Non legacy schema node t2 should be found in database node when group by schema is enabled"); + string[] legacySchemas = new string[] + { + "db_accessadmin", + "db_backupoperator", + "db_datareader", + "db_datawriter", + "db_ddladmin", + "db_denydatareader", + "db_denydatawriter", + "db_owner", + "db_securityadmin" + }; + foreach(var nodes in databaseChildren.Nodes) + { + Assert.That(legacySchemas, Does.Not.Contain(nodes.Label), "Legacy schema node should not be found in database node when group by schema is enabled"); + } + var legacySchemasNode = databaseChildren.Nodes.First(t => t.Label == SR.SchemaHierarchy_BuiltInSchema); + var legacySchemasChildren = await _service.ExpandNode(session, legacySchemasNode.NodePath); + foreach(var nodes in legacySchemasChildren.Nodes) + { + Assert.That(legacySchemas, Does.Contain(nodes.Label), "Legacy schema nodes should be found in legacy schemas folder when group by schema is enabled"); + } + WorkspaceService.Instance.CurrentSettings.SqlTools.ObjectExplorer = new ObjectExplorerSettings() { GroupBySchema = false }; + }); + } private async Task VerifyRefresh(ObjectExplorerSession session, string tablePath, string tableName, bool deleted = true) {