Or Filtering on Object Explorer Nodes (#1608)

support for a more robust filtering system in the Object Explorer xml, allowing for or-ing filter properties together for use in URN querying for objects

Co-authored-by: Charles Gagnon <chgagnon@microsoft.com>
This commit is contained in:
Jordan Hays
2022-07-29 18:35:37 -04:00
committed by GitHub
parent 40df024dbc
commit 3294a52ad9
10 changed files with 1229 additions and 827 deletions

View File

@@ -36,7 +36,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes
/// <summary>
/// The list of filters that should be applied on the smo object list
/// </summary>
public abstract IEnumerable<NodeFilter> Filters { get; }
public abstract IEnumerable<INodeFilter> Filters { get; }
/// <summary>
/// The list of properties to be loaded with the object

View File

@@ -0,0 +1,57 @@
//
// 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.Collections.Generic;
//using System.Linq;
using System.Text;
namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes
{
/// <summary>
/// Has information for filtering a SMO object by properties
/// </summary>
public interface INodeFilter
{
/// <summary>
/// Returns true if the filter can be apply to the given type and Server type
/// </summary>
/// <param name="type">Type of the querier</param>
/// <param name="validForFlag">Server Type</param>
/// <returns></returns>
bool CanApplyFilter(Type type, ValidForFlag validForFlag);
/// <summary>
/// Creates a string from the filter property and values to be used in the Urn to query the SQL objects
/// Example of the output:[@ IsSystemObject = 0]
/// </summary>
/// <returns></returns>
string ToPropertyFilterString(Type type, ValidForFlag validForFlag);
/// <summary>
/// Creates a fully paramaterized property filter string for the URN query for SQL objects.
/// Example of the output:[@ IsSystemObject = 0]
/// </summary>
/// <returns></returns>
public static string GetPropertyFilter(IEnumerable<INodeFilter> filters, Type type, ValidForFlag validForFlag)
{
StringBuilder filter = new StringBuilder();
foreach (var value in filters)
{
string andPrefix = filter.Length == 0 ? string.Empty : " and ";
var filterString = value.ToPropertyFilterString(type, validForFlag);
if (filterString != string.Empty) {
filter.Append($"{andPrefix}{filterString}");
}
}
if (filter.Length != 0)
{
return "[" + filter.ToString() + "]";
}
return string.Empty;
}
}
}

View File

@@ -0,0 +1,58 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes
{
/// <summary>
/// Has information for filtering a SMO object by properties
/// </summary>
public class NodeOrFilter : INodeFilter
{
/// <summary>
/// Filter values
/// </summary>
public List<NodePropertyFilter> FilterList { get; set; }
/// <summary>
/// Returns true if any of the filters within the FilterList apply to the type and server type.
/// </summary>
/// <param name="type">Type of the querier</param>
/// <param name="validForFlag">Server Type</param>
public bool CanApplyFilter(Type type, ValidForFlag validForFlag) {
return this.FilterList.Exists(f => f.CanApplyFilter(type, validForFlag));
}
/// <summary>
/// Creates a string representation of a node "or" filter, which is combined in the INodeFilter interface to construct the filter used in the URN to query the SQL objects.
/// Example of the output: ((@TableTemporalType = 1) or (@LedgerTableType = 1))
/// </summary>
/// <returns></returns>
public string ToPropertyFilterString(Type type, ValidForFlag validForFlag)
{
StringBuilder filter = new StringBuilder();
foreach (var nodeFilter in FilterList)
{
string orPrefix = filter.Length == 0 ? string.Empty : " or ";
// For "or" filter, have to check each node as it's processed for whether it's valid.
var filterString = nodeFilter.ToPropertyFilterString(type, validForFlag);
if (filterString != string.Empty)
{
filter.Append($"{orPrefix}{filterString}");
}
}
if (filter.Length != 0)
{
return "(" + filter.ToString() + ")";
}
return string.Empty;
}
}
}

View File

@@ -5,14 +5,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes
{
/// <summary>
/// Has information for filtering a SMO object by properties
/// </summary>
public class NodeFilter
public class NodePropertyFilter : INodeFilter
{
/// <summary>
/// Property name
@@ -55,52 +55,40 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes
}
/// <summary>
/// Creates a string from the filter property and values to be used in the Urn to query the SQL objects
/// Example of the output:[@ IsSystemObject = 0]
/// Creates a string representation of a given node filter, which is combined in the INodeFilter interface to construct the filter used in the URN to query the SQL objects.
/// Example of the output: (@ IsSystemObject = 0)
/// </summary>
/// <returns></returns>
public string ToPropertyFilterString()
public string ToPropertyFilterString(Type type, ValidForFlag validForFlag)
{
string filter = "";
List<object> values = Values;
if (values != null)
// check first if the filter can be applied; if not just return empty string
if (!CanApplyFilter(type, validForFlag))
{
for (int i = 0; i < values.Count; i++)
return string.Empty;
}
StringBuilder filter = new StringBuilder();
foreach (var value in Values)
{
object propertyValue = value;
if (Type == typeof(string))
{
var value = values[i];
object propertyValue = value;
if (Type == typeof(string))
{
propertyValue = $"'{propertyValue}'";
}
if (Type == typeof(Enum))
{
propertyValue = (int)Convert.ChangeType(value, Type);
}
string orPrefix = i == 0 ? string.Empty : "or";
filter = $"{filter} {orPrefix} @{Property} = {propertyValue}";
propertyValue = $"'{propertyValue}'";
}
else if (Type == typeof(Enum))
{
propertyValue = (int)Convert.ChangeType(value, Type);
}
string orPrefix = filter.Length == 0 ? string.Empty : " or ";
filter.Append($"{orPrefix}@{Property} = {propertyValue}");
}
filter = $"({filter})";
return filter;
}
public static string ConcatProperties(IEnumerable<NodeFilter> filters)
{
string filter = "";
var list = filters.ToList();
for (int i = 0; i < list.Count; i++)
if (filter.Length != 0)
{
var value = list[i];
string andPrefix = i == 0 ? string.Empty : "and";
filter = $"{filter} {andPrefix} {value.ToPropertyFilterString()}";
return "(" + filter.ToString() + ")";
}
filter = $"[{filter}]";
return filter;
return string.Empty;
}
}
}

View File

@@ -104,7 +104,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
var smoProperties = this.SmoProperties.Where(p => ServerVersionHelper.IsValidFor(serverValidFor, p.ValidFor)).Select(x => x.Name);
if (!string.IsNullOrEmpty(name))
{
filters.Add(new NodeFilter
filters.Add(new NodePropertyFilter
{
Property = "Name",
Type = typeof(string),
@@ -118,7 +118,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
{
continue;
}
string propertyFilter = GetProperyFilter(filters, querier.GetType(), serverValidFor);
string propertyFilter = INodeFilter.GetPropertyFilter(filters, querier.GetType(), serverValidFor);
try
{
var smoObjectList = querier.Query(context, propertyFilter, refresh, smoProperties).ToList();
@@ -162,22 +162,6 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
return filterTheNode;
}
private string GetProperyFilter(IEnumerable<NodeFilter> filters, Type querierType, ValidForFlag validForFlag)
{
string filter = string.Empty;
if (filters != null)
{
var filtersToApply = filters.Where(f => f.CanApplyFilter(querierType, validForFlag)).ToList();
filter = string.Empty;
if (filtersToApply.Any())
{
filter = NodeFilter.ConcatProperties(filtersToApply);
}
}
return filter;
}
private bool IsCompatibleQuerier(SmoQuerier querier)
{
if (ChildQuerierTypes == null)
@@ -257,11 +241,11 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
}
}
public override IEnumerable<NodeFilter> Filters
public override IEnumerable<INodeFilter> Filters
{
get
{
return Enumerable.Empty<NodeFilter>();
return Enumerable.Empty<INodeFilter>();
}
}

View File

@@ -73,13 +73,13 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
var name = TreeNode.GetAttribute("Name");
WriteLine(string.Format(" internal sealed partial class {0} : SmoTreeNode", name));
WriteLine(" {");
WriteLine(string.Format(" public {0}() : base()", name));
WriteLine(" {");
WriteLine(" NodeValue = string.Empty;");
WriteLine(string.Format(" this.NodeType = \"{0}\";", name.Replace("TreeNode", string.Empty)));
WriteLine(string.Format(" this.NodeTypeId = NodeTypes.{0};", name.Replace("TreeNode", string.Empty)));
WriteLine(" OnInitialize();");
WriteLine(" }");
WriteLine(string.Format(" public {0}() : base()", name));
WriteLine(" {");
WriteLine(" NodeValue = string.Empty;");
WriteLine(string.Format(" this.NodeType = \"{0}\";", name.Replace("TreeNode", string.Empty)));
WriteLine(string.Format(" this.NodeTypeId = NodeTypes.{0};", name.Replace("TreeNode", string.Empty)));
WriteLine(" OnInitialize();");
WriteLine(" }");
WriteLine(" }");
WriteLine("");
}
@@ -116,86 +116,101 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
WriteLine(string.Format(" public override IEnumerable<string> ApplicableParents() {{ return new[] {{ \"{0}\" }}; }}", type));
List<XmlElement> children = GetChildren(xmlFile, type);
List<XmlElement> filters = GetNodeFilters(xmlFile, type);
List<XmlElement> smoProperties = GetNodeSmoProperties(xmlFile, type);
if (filters.Count > 0)
// Load and parse Filters node
// A <Filters> node is comprised of <Or> and <Filter> nodes
// - A <Filter> node defines the properties to construct a NodePropertyFilter object
// - An <Or> node defines a list of <Filter> nodes that are or'ed in the resulting URN Query
// <Filters> can have an arbitrary number of top-level <Or> and <Filter> nodes.
// All filters defined at the top-level are and'ed in the resulting URN Query.
// This is generated into the IEnumerable<INodeFilter> Filters object for a particular SMO object.
// Currently there is not support for nested <Or> nodes
XmlNode filtersNode = GetFiltersNode(xmlFile, type);
if (filtersNode != null)
{
// Get the children nodes for the Filters
XmlNodeList childNodes = filtersNode.ChildNodes;
if (childNodes.Count > 0)
{
// Write initial declarator for the filters object
WriteLine("");
WriteLine(" public override IEnumerable<NodeFilter> Filters");
WriteLine(" public override IEnumerable<INodeFilter> Filters");
WriteLine(" {");
WriteLine(" get");
WriteLine(" {");
WriteLine(" get");
WriteLine(" {");
WriteLine(" var filters = new List<INodeFilter>();");
WriteLine(" var filters = new List<NodeFilter>();");
foreach (var filter in filters)
// Parse each of the <Or> nodes in <Filters>
foreach (var orNode in filtersNode.SelectNodes("Or"))
{
var propertyName = filter.GetAttribute("Property");
var propertyType = filter.GetAttribute("Type");
var propertyValue = filter.GetAttribute("Value");
var validFor = filter.GetAttribute("ValidFor");
var typeToReverse = filter.GetAttribute("TypeToReverse");
XmlElement or = orNode as XmlElement;
if (or == null)
{
continue;
}
List<XmlElement> filterValues = GetNodeFilterValues(xmlFile, type, propertyName);
WriteLine(" filters.Add(new NodeFilter");
// Write initial declarator for the <Or> object, which is just a list of NodePropertyFilters
WriteLine(" filters.Add(new NodeOrFilter");
WriteLine(" {");
WriteLine(string.Format(" Property = \"{0}\",", propertyName));
WriteLine(string.Format(" Type = typeof({0}),", propertyType));
if (!string.IsNullOrWhiteSpace(typeToReverse))
{
WriteLine(string.Format(" TypeToReverse = typeof({0}Querier),", typeToReverse));
}
if (!string.IsNullOrWhiteSpace(validFor))
{
WriteLine(string.Format(" ValidFor = {0},", GetValidForFlags(validFor)));
}
if (propertyValue != null && (filterValues == null || filterValues.Count == 0))
{
WriteLine(string.Format(" Values = new List<object> {{ {0} }},", propertyValue));
}
if (filterValues != null && filterValues.Count > 0)
{
string filterValueType = "object";
if (propertyType == "Enum")
{
WriteLine(" FilterList = new List<NodePropertyFilter> {");
}
WriteLine(string.Format(" Values = new List<object>"));
WriteLine(string.Format(" {{"));
for(int i = 0; i < filterValues.Count; i++)
foreach(var orFilterNode in or.GetElementsByTagName("Filter"))
{
XmlElement orFilter = orFilterNode as XmlElement;
if (orFilter == null)
{
string separator = "";
if (i != filterValues.Count - 1)
{
separator = ",";
}
var filterValue = filterValues[i];
WriteLine(string.Format(" {{ {0} }}{1}", filterValue.InnerText, separator ));
continue;
}
WriteLine(string.Format(" }}"));
// Declaration of Filter node
WriteLine(" new NodePropertyFilter");
// Parse the elements in the <Filter> node into a string, and write
ParseAndWriteFilterNode(xmlFile, type, orFilter, true /*orFilter*/);
// Close out filter object definition
WriteLine(" },");
}
// Close out declaration of the NodeOrFilter
WriteLine(" }");
WriteLine(" });");
}
// Parse each of the top-level <Filter> nodes in <Filters>
foreach (var filterNode in filtersNode.SelectNodes("Filter"))
{
XmlElement filter = filterNode as XmlElement;
if (filter == null)
{
continue;
}
// Start declaration of Filter node
WriteLine(" filters.Add(new NodePropertyFilter");
// Parse the elements in the <Filter> node into a string, and write
ParseAndWriteFilterNode(xmlFile, type, filter);
// Close out filter object definition
WriteLine(" });");
}
// Close out declaration of the Filters object
WriteLine(" return filters;");
WriteLine(" }");
WriteLine(" }");
WriteLine(" }");
}
}
if (smoProperties.Count > 0)
{
WriteLine("");
WriteLine(" public override IEnumerable<NodeSmoProperty> SmoProperties");
WriteLine(" {");
WriteLine(" get");
WriteLine(" {");
WriteLine(" get");
WriteLine(" {");
WriteLine(" var properties = new List<NodeSmoProperty>();");
foreach (var smoPropertiy in smoProperties)
@@ -203,9 +218,6 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
var propertyName = smoPropertiy.GetAttribute("Name");
var validFor = smoPropertiy.GetAttribute("ValidFor");
WriteLine(" properties.Add(new NodeSmoProperty");
WriteLine(" {");
WriteLine(string.Format(" Name = \"{0}\",", propertyName));
@@ -215,17 +227,13 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
WriteLine(string.Format(" ValidFor = {0}", GetValidForFlags(validFor)));
}
WriteLine(" });");
}
WriteLine(" return properties;");
WriteLine(" }");
WriteLine(" }");
WriteLine(" }");
}
if (children.Count > 0)
{
WriteLine("");
@@ -276,11 +284,11 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
WriteLine("");
WriteLine(" internal override Type[] ChildQuerierTypes");
WriteLine(" {");
WriteLine(" get");
WriteLine(" {");
WriteLine(" get");
WriteLine(" {");
if (!string.IsNullOrWhiteSpace(ChildQuerierTypes))
{
Write(" return new [] {");
Write(" return new [] {");
foreach (var typeToRe in allTypes)
{
Write(string.Format(" typeof({0}Querier),", typeToRe));
@@ -289,9 +297,9 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
}
else
{
Write(" return new Type[0];");
WriteLine(" return new Type[0];");
}
WriteLine(" }");
WriteLine(" }");
WriteLine(" }");
WriteLine("");
@@ -464,14 +472,14 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
return retElements;
}
public static List<XmlElement> GetNodeFilters(string xmlFile, string parentName)
public static List<XmlElement> GetNodeOrFilters(string xmlFile, string parentName)
{
XmlElement nodeElement = GetNodeElement(xmlFile, parentName);
XmlDocument doc = new XmlDocument();
doc.Load(xmlFile);
List<XmlElement> retElements = new List<XmlElement>();
XmlNodeList nodeList = doc.SelectNodes(string.Format("/ServerExplorerTree/Node[@Name='{0}']/Filters/Filter", parentName));
XmlNodeList nodeList = doc.SelectNodes(string.Format("/ServerExplorerTree/Node[@Name='{0}']/Filters/Or/Filter", parentName));
foreach (var item in nodeList)
{
XmlElement itemAsElement = item as XmlElement;
@@ -483,6 +491,14 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
return retElements;
}
public static XmlNode GetFiltersNode(string xmlFile, string parentName)
{
XmlElement nodeElement = GetNodeElement(xmlFile, parentName);
XmlDocument doc = new XmlDocument();
doc.Load(xmlFile);
return doc.SelectSingleNode(string.Format("/ServerExplorerTree/Node[@Name='{0}']/Filters", parentName));
}
public static List<XmlElement> GetNodeSmoProperties(string xmlFile, string parentName)
{
@@ -503,14 +519,20 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
return retElements;
}
public static List<XmlElement> GetNodeFilterValues(string xmlFile, string parentName, string filterProperty)
public static List<XmlElement> GetNodeFilterValues(string xmlFile, string parentName, string filterProperty, bool orFilter = false)
{
XmlElement nodeElement = GetNodeElement(xmlFile, parentName);
XmlDocument doc = new XmlDocument();
doc.Load(xmlFile);
List<XmlElement> retElements = new List<XmlElement>();
XmlNodeList nodeList = doc.SelectNodes(string.Format("/ServerExplorerTree/Node[@Name='{0}']/Filters/Filter[@Property='{1}']/Value", parentName, filterProperty));
var xpath = string.Format(
"/ServerExplorerTree/Node[@Name='{0}']/Filters/{1}Filter[@Property='{2}']/Value",
parentName,
orFilter ? "Or/" : string.Empty,
filterProperty);
XmlNodeList nodeList = doc.SelectNodes(xpath);
foreach (var item in nodeList)
{
XmlElement itemAsElement = item as XmlElement;
@@ -521,4 +543,56 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
}
return retElements;
}
/// <summary>
/// Helper function to parse out and write the contents of a <Filter> node.
/// </summary>
/// <param name="xmlFile">Base xml doc</param>
/// <param name="parentName">name of the parent object in the xml</param>
/// <param name="filter">The filter to be parsed</param>
/// <param name="orFilter">Whether this filter is a sub-node of an <Or> node</param>
public void ParseAndWriteFilterNode(string xmlFile, string parentName, XmlElement filter, bool orFilter = false)
{
// <Or> Filters start at a larger base indentation than <Filter> nodes directly under <Filters>
var indent = orFilter ? " " : " ";
var propertyName = filter.GetAttribute("Property");
var propertyType = filter.GetAttribute("Type");
var propertyValue = filter.GetAttribute("Value");
var validFor = filter.GetAttribute("ValidFor");
var typeToReverse = filter.GetAttribute("TypeToReverse");
List<XmlElement> filterValues = GetNodeFilterValues(xmlFile, parentName, propertyName, orFilter);
// Write out the "meat" of the object definition
WriteLine(indent + "{");
WriteLine(indent + " Property = \"{0}\",", propertyName);
WriteLine(indent + " Type = typeof({0}),", propertyType);
if (!string.IsNullOrWhiteSpace(typeToReverse))
{
WriteLine(indent + " TypeToReverse = typeof({0}Querier),", typeToReverse);
}
if (!string.IsNullOrWhiteSpace(validFor))
{
WriteLine(indent + " ValidFor = {0},", GetValidForFlags(validFor));
}
if (propertyValue != null && (filterValues == null || filterValues.Count == 0))
{
WriteLine(indent + " Values = new List<object> {{ {0} }},", propertyValue);
}
if (filterValues != null && filterValues.Count > 0)
{
WriteLine(indent + " Values = new List<object>");
WriteLine(indent + " {");
for(int i = 0; i < filterValues.Count; i++)
{
string separator = (i != filterValues.Count - 1) ? "," : "";
var filterValue = filterValues[i];
WriteLine(indent + " {{ {0} }}{1}", filterValue.InnerText, separator);
}
WriteLine(indent + " }");
}
}
#>