From 6b251bd24aa98a57dc5bb60d360300c0ad106792 Mon Sep 17 00:00:00 2001 From: Aasim Khan Date: Wed, 28 Jun 2023 16:17:20 -0700 Subject: [PATCH] Adding like filter and removing contains to OE filtering (#2105) * Adding startswith and like filter * Adding ends with * Adding tests * Remove null and not null * Fixing string * Update test/Microsoft.SqlTools.ServiceLayer.UnitTests/ObjectExplorer/NodeFilterTests.cs Co-authored-by: Charles Gagnon * Update test/Microsoft.SqlTools.ServiceLayer.UnitTests/ObjectExplorer/NodeFilterTests.cs Co-authored-by: Charles Gagnon * Adding back contains, starts with and ends with * Properly escaping chars for like based filters * Adding some comments regarding escape characters * Using generated regex * removing additional class * Adding extra auth type that was causing the tests to error out * Fixing regex * Adding integration tests * Fixing unit tests * Making fluent assertions --------- Co-authored-by: Charles Gagnon --- .../ObjectExplorer/Contracts/NodeInfo.cs | 4 + .../Nodes/NodePropertyFilter.cs | 46 ++- .../ObjectExplorer/ObjectExplorerUtils.cs | 29 +- .../ObjectExplorerServiceTests.cs | 115 +++++- .../TestServerIdentity.cs | 3 +- .../ObjectExplorer/NodeFilterTests.cs | 384 +++++++++++++++++- 6 files changed, 535 insertions(+), 46 deletions(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Contracts/NodeInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Contracts/NodeInfo.cs index 014a4d52..0ad04e10 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Contracts/NodeInfo.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Contracts/NodeInfo.cs @@ -157,6 +157,10 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Contracts NotBetween = 7, Contains = 8, NotContains = 9, + StartsWith = 10, + NotStartsWith = 11, + EndsWith = 12, + NotEndsWith = 13 } /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Nodes/NodePropertyFilter.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Nodes/NodePropertyFilter.cs index 1a5528f2..4e67c883 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Nodes/NodePropertyFilter.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Nodes/NodePropertyFilter.cs @@ -8,13 +8,16 @@ using System; using System.Collections.Generic; using System.Text; +using System.Text.RegularExpressions; +using Microsoft.SqlTools.ServiceLayer.Management; + namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes { /// /// Has information for filtering a SMO object by properties /// - public class NodePropertyFilter : INodeFilter + public partial class NodePropertyFilter : INodeFilter { /// /// Property name @@ -182,7 +185,25 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes object propertyValue = value; if (Type == typeof(string)) { - propertyValue = $"'{propertyValue}'"; + //Replacing quotes with double quotes + var escapedString = CUtils.EscapeStringSQuote(propertyValue.ToString()); + if (this.FilterType == FilterType.STARTSWITH || this.FilterType == FilterType.ENDSWITH) + { + escapedString = EscapeLikeURNRegex().Replace(escapedString, "[$0]"); + + if (this.FilterType == FilterType.STARTSWITH) + { + propertyValue = $"'{escapedString}%'"; + } + else if (this.FilterType == FilterType.ENDSWITH) + { + propertyValue = $"'%{escapedString}'"; + } + } + else + { + propertyValue = $"'{escapedString}'"; + } } else if (Type == typeof(Enum)) { @@ -211,15 +232,19 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes 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; + case FilterType.CONTAINS: + filterText = $"contains(@{Property}, {propertyValue})"; + break; + case FilterType.STARTSWITH: + case FilterType.ENDSWITH: + filterText = $"like(@{Property}, {propertyValue})"; + break; } } @@ -261,13 +286,17 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes return false; } } + + // For filters that use the LIKE operator, we need to escape the following characters: %, _, [, ^ + // we do this by wrapping them in square brackets eg: [%], [_], [[], [^] + [GeneratedRegexAttribute(@"%|_|\[|\^")] + public static partial Regex EscapeLikeURNRegex(); } public enum FilterType { EQUALS, DATETIME, - CONTAINS, FALSE, ISNULL, NOTEQUALS, @@ -276,6 +305,9 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes LESSTHANOREQUAL, GREATERTHANOREQUAL, BETWEEN, - NOTBETWEEN + NOTBETWEEN, + CONTAINS, + STARTSWITH, + ENDSWITH } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/ObjectExplorerUtils.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/ObjectExplorerUtils.cs index e1b7425a..1ec176ee 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/ObjectExplorerUtils.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/ObjectExplorerUtils.cs @@ -129,13 +129,6 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer case NodeFilterOperator.GreaterThanOrEquals: filterType = FilterType.GREATERTHANOREQUAL; break; - case NodeFilterOperator.Contains: - filterType = FilterType.CONTAINS; - break; - case NodeFilterOperator.NotContains: - filterType = FilterType.CONTAINS; - isNotFilter = true; - break; case NodeFilterOperator.Between: filterType = FilterType.BETWEEN; break; @@ -143,9 +136,29 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer filterType = FilterType.NOTBETWEEN; isNotFilter = true; break; + case NodeFilterOperator.Contains: + filterType = FilterType.CONTAINS; + break; + case NodeFilterOperator.NotContains: + filterType = FilterType.CONTAINS; + isNotFilter = true; + break; + case NodeFilterOperator.StartsWith: + filterType = FilterType.STARTSWITH; + break; + case NodeFilterOperator.NotStartsWith: + filterType = FilterType.STARTSWITH; + isNotFilter = true; + break; + case NodeFilterOperator.EndsWith: + filterType = FilterType.ENDSWITH; + break; + case NodeFilterOperator.NotEndsWith: + filterType = FilterType.ENDSWITH; + isNotFilter = true; + break; } - if (filter.Operator == NodeFilterOperator.Between || filter.Operator == NodeFilterOperator.NotBetween) { if (filterProperty.Type == NodeFilterPropertyDataType.Number) diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectExplorer/ObjectExplorerServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectExplorer/ObjectExplorerServiceTests.cs index ab909240..fa1c78ad 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectExplorer/ObjectExplorerServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectExplorer/ObjectExplorerServiceTests.cs @@ -267,25 +267,25 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectExplorer 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" + 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) + 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) + 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"); } @@ -293,6 +293,95 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectExplorer }); } + [Test] + public async Task VerifyOEFilters() + { + string query = @" + Create table ""table['^__']"" (id int); + Create table ""table['^__']2"" (id int); + Create table ""table['^%%2"" (id int); + Create table ""testTable"" (id int); + "; + + string databaseName = "#testDb#"; + await RunTest(databaseName, query, "Testdb", async (testDbName, session) => + { + var databaseNode = session.Root.ToNodeInfo(); + var databaseChildren = await _service.ExpandNode(session, databaseNode.NodePath); + Assert.True(databaseChildren.Nodes.Any(t => t.Label == SR.SchemaHierarchy_Tables), "Tables node should be found in database node"); + var tablesNode = databaseChildren.Nodes.First(t => t.Label == SR.SchemaHierarchy_Tables); + var NameProperty = tablesNode.FilterableProperties.First(t => t.Name == "Name"); + Assert.True(NameProperty != null, "Name property should be found in tables node"); + + // Testing contains operator + NodeFilter filter = new NodeFilter() + { + Name = NameProperty.Name, + Value = "table", + Operator = NodeFilterOperator.Contains + }; + var tablesChildren = await _service.ExpandNode(session, tablesNode.NodePath, true, null, new NodeFilter[] { filter }); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.testTable"), "testTable node should be found in tables node"); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.table['^__']"), "table['^__'] node should be found in tables node"); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.table['^__']2"), "table['^__']2 node should be found in tables node"); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.table['^%%2"), "table['^%%2 node should be found in tables node"); + + // Testing starts with operator + filter = new NodeFilter() + { + Name = NameProperty.Name, + Value = "table['^__']", + Operator = NodeFilterOperator.StartsWith + }; + tablesChildren = await _service.ExpandNode(session, tablesNode.NodePath, true, null, new NodeFilter[] { filter }); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.table['^__']"), "table['^__'] node should be found in tables node"); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.table['^__']2"), "table['^__']2 node should be found in tables node"); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.testTable") == false, "testTable node should not be found in tables node"); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.table['^%%2") == false, "table['^%%2 node should not be found in tables node"); + + // Testing starts with operator + filter = new NodeFilter() + { + Name = NameProperty.Name, + Value = "table['^%%2", + Operator = NodeFilterOperator.StartsWith + }; + tablesChildren = await _service.ExpandNode(session, tablesNode.NodePath, true, null, new NodeFilter[] { filter }); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.table['^%%2"), "table['^%%2 node should be found in tables node"); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.table['^__']") == false, "table['^__'] node should not be found in tables node"); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.table['^__']2") == false, "table['^__']2 node should not be found in tables node"); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.testTable") == false, "testTable node should not be found in tables node"); + + + // Testing ends with operator + filter = new NodeFilter() + { + Name = NameProperty.Name, + Value = "table['^__']", + Operator = NodeFilterOperator.EndsWith + }; + tablesChildren = await _service.ExpandNode(session, tablesNode.NodePath, true, null, new NodeFilter[] { filter }); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.table['^__']"), "table['^__'] node should be found in tables node"); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.table['^__']2") == false, "table['^__']2 node should not be found in tables node"); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.testTable") == false, "testTable node should not be found in tables node"); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.table['^%%2") == false, "table['^%%2 node should not be found in tables node"); + + // Testing equals operator + filter = new NodeFilter() + { + Name = NameProperty.Name, + Value = "table['^__']", + Operator = NodeFilterOperator.Equals + }; + tablesChildren = await _service.ExpandNode(session, tablesNode.NodePath, true, null, new NodeFilter[] { filter }); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.table['^__']"), "table['^__'] node should be found in tables node"); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.table['^__']2") == false, "table['^__']2 node should not be found in tables node"); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.testTable") == false, "testTable node should not be found in tables node"); + Assert.That(tablesChildren.Nodes.Any(t => t.Label == "dbo.table['^%%2") == false, "table['^%%2 node should not be found in tables node"); + + }); + } + private async Task VerifyRefresh(ObjectExplorerSession session, string tablePath, string tableName, bool deleted = true) { //Refresh Root diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestServerIdentity.cs b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestServerIdentity.cs index 0a1fce1d..9782878a 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestServerIdentity.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestServerIdentity.cs @@ -29,6 +29,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Common public enum AuthenticationType { Integrated, - SqlLogin + SqlLogin, + AzureMFA } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ObjectExplorer/NodeFilterTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ObjectExplorer/NodeFilterTests.cs index c81110a4..fbfd775c 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ObjectExplorer/NodeFilterTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ObjectExplorer/NodeFilterTests.cs @@ -7,11 +7,14 @@ using System; using System.Collections.Generic; +using Newtonsoft.Json; using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlTools.ServiceLayer.ObjectExplorer; using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes; using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel; +using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Contracts; using NUnit.Framework; +using Newtonsoft.Json.Linq; namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer { @@ -241,7 +244,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer }; string filterString = INodeFilter.GetPropertyFilter(filterList, typeof(SqlHistoryTableQuerier), ValidForFlag.All); - Assert.AreEqual("[(@CreateDate >= datetime('2021-01-01 00:00:00.000') and @CreateDate <= datetime('2021-01-01 23:59:59.999'))]", filterString, "Error parsing date filter with equals operator"); + Assert.That(filterString, Is.EqualTo("[(@CreateDate >= datetime('2021-01-01 00:00:00.000') and @CreateDate <= datetime('2021-01-01 23:59:59.999'))]"), "Error parsing date filter with equals operator"); // Testing date filter with less than @@ -259,7 +262,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer }; filterString = INodeFilter.GetPropertyFilter(filterList, typeof(SqlHistoryTableQuerier), ValidForFlag.All); - Assert.AreEqual("[(@CreateDate < datetime('2021-01-01 00:00:00.000'))]", filterString, "Error parsing date filter with less than operator"); + Assert.That(filterString, Is.EqualTo("[(@CreateDate < datetime('2021-01-01 00:00:00.000'))]"), "Error parsing date filter with less than operator"); // Testing date filter with greater than filterList = new List @@ -276,7 +279,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer }; filterString = INodeFilter.GetPropertyFilter(filterList, typeof(SqlHistoryTableQuerier), ValidForFlag.All); - Assert.AreEqual("[(@CreateDate > datetime('2021-01-01 23:59:59.999'))]", filterString, "Error parsing date filter with greater than operator"); + Assert.That(filterString, Is.EqualTo("[(@CreateDate > datetime('2021-01-01 23:59:59.999'))]"), "Error parsing date filter with greater than operator"); // Testing date filter with between filterList = new List @@ -293,7 +296,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer }; filterString = INodeFilter.GetPropertyFilter(filterList, typeof(SqlHistoryTableQuerier), ValidForFlag.All); - Assert.AreEqual("[(@CreateDate >= datetime('2021-01-01 00:00:00.000') and @CreateDate <= datetime('2021-01-02 23:59:59.999'))]", filterString, "Error parsing date filter with between operator"); + Assert.That(filterString, Is.EqualTo("[(@CreateDate >= datetime('2021-01-01 00:00:00.000') and @CreateDate <= datetime('2021-01-02 23:59:59.999'))]"), "Error parsing date filter with between operator"); // Testing date filter with not equals @@ -311,7 +314,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer }; filterString = INodeFilter.GetPropertyFilter(filterList, typeof(SqlHistoryTableQuerier), ValidForFlag.All); - Assert.AreEqual("[(not(@CreateDate >= datetime('2021-01-01 00:00:00.000') and @CreateDate <= datetime('2021-01-01 23:59:59.999')))]", filterString, "Error parsing date filter with not equals operator"); + Assert.That(filterString, Is.EqualTo("[(not(@CreateDate >= datetime('2021-01-01 00:00:00.000') and @CreateDate <= datetime('2021-01-01 23:59:59.999')))]"), "Error parsing date filter with not equals operator"); // Testing date filter with not between @@ -328,7 +331,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer } }; filterString = INodeFilter.GetPropertyFilter(filterList, typeof(SqlHistoryTableQuerier), ValidForFlag.All); - Assert.AreEqual("[(not(@CreateDate >= datetime('2021-01-01 00:00:00.000') and @CreateDate <= datetime('2021-01-02 23:59:59.999')))]", filterString, "Error parsing date filter with not between operator"); + Assert.That(filterString, Is.EqualTo("[(not(@CreateDate >= datetime('2021-01-01 00:00:00.000') and @CreateDate <= datetime('2021-01-02 23:59:59.999')))]"), "Error parsing date filter with not between operator"); // Testing date filter LessThanOrEquals filterList = new List @@ -344,7 +347,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer } }; filterString = INodeFilter.GetPropertyFilter(filterList, typeof(SqlHistoryTableQuerier), ValidForFlag.All); - Assert.AreEqual("[(@CreateDate <= datetime('2021-01-01 23:59:59.999'))]", filterString, "Error parsing date filter with LessThanOrEquals operator"); + Assert.That(filterString, Is.EqualTo("[(@CreateDate <= datetime('2021-01-01 23:59:59.999'))]"), "Error parsing date filter with LessThanOrEquals operator"); // Testing date filter GreaterThanOrEquals filterList = new List @@ -360,7 +363,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer } }; filterString = INodeFilter.GetPropertyFilter(filterList, typeof(SqlHistoryTableQuerier), ValidForFlag.All); - Assert.AreEqual("[(@CreateDate >= datetime('2021-01-01 00:00:00.000'))]", filterString, "Error parsing date filter with GreaterThanOrEquals operator"); + Assert.That(filterString, Is.EqualTo("[(@CreateDate >= datetime('2021-01-01 00:00:00.000'))]"), "Error parsing date filter with GreaterThanOrEquals operator"); // Testing date filter with invalid date @@ -438,7 +441,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer }; string filterString = INodeFilter.GetPropertyFilter(filterList, typeof(SqlHistoryTableQuerier), ValidForFlag.All); - Assert.AreEqual("[(@RowCount = 100)]", filterString, "Error parsing numeric filter with equals operator"); + Assert.That(filterString, Is.EqualTo("[(@RowCount = 100)]"), "Error parsing numeric filter with equals operator"); // Testing numeric filter with less than filterList = new List @@ -455,7 +458,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer }; filterString = INodeFilter.GetPropertyFilter(filterList, typeof(SqlHistoryTableQuerier), ValidForFlag.All); - Assert.AreEqual("[(@RowCount < 100)]", filterString, "Error parsing numeric filter with less than operator"); + Assert.That(filterString, Is.EqualTo("[(@RowCount < 100)]"), "Error parsing numeric filter with less than operator"); // Testing numeric filter with greater than filterList = new List @@ -472,7 +475,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer }; filterString = INodeFilter.GetPropertyFilter(filterList, typeof(SqlHistoryTableQuerier), ValidForFlag.All); - Assert.AreEqual("[(@RowCount > 100)]", filterString, "Error parsing numeric filter with greater than operator"); + Assert.That(filterString, Is.EqualTo("[(@RowCount > 100)]"), "Error parsing numeric filter with greater than operator"); // Testing numeric filter with between filterList = new List @@ -488,7 +491,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer } }; filterString = INodeFilter.GetPropertyFilter(filterList, typeof(SqlHistoryTableQuerier), ValidForFlag.All); - Assert.AreEqual("[(@RowCount >= 100 and @RowCount <= 200)]", filterString, "Error parsing numeric filter with between operator"); + Assert.That(filterString, Is.EqualTo("[(@RowCount >= 100 and @RowCount <= 200)]"), "Error parsing numeric filter with between operator"); // Testing numeric filter with not equals filterList = new List @@ -504,7 +507,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer } }; filterString = INodeFilter.GetPropertyFilter(filterList, typeof(SqlHistoryTableQuerier), ValidForFlag.All); - Assert.AreEqual("[(@RowCount != 100)]", filterString, "Error parsing numeric filter with not equals operator"); + Assert.That(filterString, Is.EqualTo("[(@RowCount != 100)]"), "Error parsing numeric filter with not equals operator"); // Testing numeric filter with not between filterList = new List @@ -520,7 +523,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer } }; filterString = INodeFilter.GetPropertyFilter(filterList, typeof(SqlHistoryTableQuerier), ValidForFlag.All); - Assert.AreEqual("[(not(@RowCount >= 100 and @RowCount <= 200))]", filterString, "Error parsing numeric filter with not between operator"); + Assert.That(filterString, Is.EqualTo("[(not(@RowCount >= 100 and @RowCount <= 200))]"), "Error parsing numeric filter with not between operator"); // Testing numeric filter LessThanOrEquals filterList = new List @@ -536,7 +539,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer } }; filterString = INodeFilter.GetPropertyFilter(filterList, typeof(SqlHistoryTableQuerier), ValidForFlag.All); - Assert.AreEqual("[(@RowCount <= 100)]", filterString, "Error parsing numeric filter with LessThanOrEquals operator"); + Assert.That(filterString, Is.EqualTo("[(@RowCount <= 100)]"), "Error parsing numeric filter with LessThanOrEquals operator"); // Testing numeric filter GreaterThanOrEquals filterList = new List @@ -552,7 +555,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer } }; filterString = INodeFilter.GetPropertyFilter(filterList, typeof(SqlHistoryTableQuerier), ValidForFlag.All); - Assert.AreEqual("[(@RowCount >= 100)]", filterString, "Error parsing numeric filter with GreaterThanOrEquals operator"); + Assert.That(filterString, Is.EqualTo("[(@RowCount >= 100)]"), filterString, "Error parsing numeric filter with GreaterThanOrEquals operator"); // Testing numeric filter with invalid value filterList = new List @@ -596,7 +599,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer } }; Assert.Throws(() => INodeFilter.GetPropertyFilter(filterList, typeof(SqlHistoryTableQuerier), ValidForFlag.All), "Error not thrown when a single value is passed for between operator"); - filterList = new List + filterList = new List { new NodePropertyFilter() { @@ -610,5 +613,352 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer }; Assert.Throws(() => INodeFilter.GetPropertyFilter(filterList, typeof(SqlHistoryTableQuerier), ValidForFlag.All), "Error not thrown when the array contains single value for between operator"); } + + [Test] + public void TestContainsFilter() + { + // Testing text filter with ends with operator + var filterList = new List + { + new NodePropertyFilter() + { + Property = "Name", + Type = typeof(string), + ValidFor = ValidForFlag.All, + Values = new List { "test" }, + FilterType = FilterType.CONTAINS, + IsDateTime = false + } + }; + var filterString = INodeFilter.GetPropertyFilter(filterList, typeof(SqlHistoryTableQuerier), ValidForFlag.All); + Assert.That(filterString, Is.EqualTo("[(contains(@Name, 'test'))]"), "Error parsing text filter with contains operator"); + } + + public static IEnumerable ConvertExpandNodeFilterToNodeFilterTestCases + { + get + { + // Test case for equals operator + yield return new TestCaseData( + new NodeFilter() + { + Name = "Name", + Operator = NodeFilterOperator.Equals, + Value = "test" + }, + new NodeFilterProperty() + { + Name = "Name", + Type = NodeFilterPropertyDataType.String + }, + new NodePropertyFilter() + { + Property = "Name", + Type = typeof(string), + Values = new List { "test" }, + IsNotFilter = false, + FilterType = FilterType.EQUALS, + IsDateTime = false + } + ); + + // Test case for not equals operator + yield return new TestCaseData( + new NodeFilter() + { + Name = "Name", + Operator = NodeFilterOperator.NotEquals, + Value = "test" + }, + new NodeFilterProperty() + { + Name = "Name", + Type = NodeFilterPropertyDataType.String + }, + new NodePropertyFilter() + { + Property = "Name", + Type = typeof(string), + Values = new List { "test" }, + IsNotFilter = true, + FilterType = FilterType.EQUALS, + IsDateTime = false + } + ); + + // Test case for like operator with string value + yield return new TestCaseData( + new NodeFilter() + { + Name = "Name", + Operator = NodeFilterOperator.Contains, + Value = "test" + }, + new NodeFilterProperty() + { + Name = "Name", + Type = NodeFilterPropertyDataType.String + }, + new NodePropertyFilter() + { + Property = "Name", + Type = typeof(string), + Values = new List { "test" }, + IsNotFilter = false, + FilterType = FilterType.CONTAINS, + IsDateTime = false + } + ); + + // Test case for not like operator with string value + yield return new TestCaseData( + new NodeFilter() + { + Name = "Name", + Operator = NodeFilterOperator.NotContains, + Value = "test" + }, + new NodeFilterProperty() + { + Name = "Name", + Type = NodeFilterPropertyDataType.String + }, + new NodePropertyFilter() + { + Property = "Name", + Type = typeof(string), + Values = new List { "test" }, + IsNotFilter = true, + FilterType = FilterType.CONTAINS, + IsDateTime = false + } + ); + + // Test case for date filter with equals operator + yield return new TestCaseData( + new NodeFilter() + { + Name = "Date", + Operator = NodeFilterOperator.Equals, + Value = "2020-01-01" + }, + new NodeFilterProperty() + { + Name = "Date", + Type = NodeFilterPropertyDataType.Date + }, + new NodePropertyFilter() + { + Property = "Date", + Type = typeof(string), + Values = new List { "2020-01-01" }, + IsNotFilter = false, + FilterType = FilterType.EQUALS, + IsDateTime = true + } + ); + + // test case for date with between operator + yield return new TestCaseData( + new NodeFilter() + { + Name = "Date", + Operator = NodeFilterOperator.Between, + Value = JArray.FromObject(new string[] { "2020-01-01", "2020-01-02" }) + }, + new NodeFilterProperty() + { + Name = "Date", + Type = NodeFilterPropertyDataType.Date + }, + new NodePropertyFilter() + { + Property = "Date", + Type = typeof(string), + Values = new List { new string[] { "2020-01-01", "2020-01-02" } }, + IsNotFilter = false, + FilterType = FilterType.BETWEEN, + IsDateTime = true + } + ); + + // test case for date with not between operator + yield return new TestCaseData( + new NodeFilter() + { + Name = "Date", + Operator = NodeFilterOperator.NotBetween, + Value = JArray.FromObject(new string[] { "2020-01-01", "2020-01-02" }) + }, + new NodeFilterProperty() + { + Name = "Date", + Type = NodeFilterPropertyDataType.Date + }, + new NodePropertyFilter() + { + Property = "Date", + Type = typeof(string), + Values = new List { new string[] { "2020-01-01", "2020-01-02" } }, + IsNotFilter = true, + FilterType = FilterType.NOTBETWEEN, + IsDateTime = true + } + ); + + // test case for date with starts with operator + yield return new TestCaseData( + new NodeFilter() + { + Name = "Name", + Operator = NodeFilterOperator.StartsWith, + Value = "test" + }, + new NodeFilterProperty() + { + Name = "Name", + Type = NodeFilterPropertyDataType.String + }, + new NodePropertyFilter() + { + Property = "Name", + Type = typeof(string), + Values = new List { "test" }, + IsNotFilter = false, + FilterType = FilterType.STARTSWITH, + IsDateTime = false + } + ); + + // test case for date with not starts with operator + yield return new TestCaseData( + new NodeFilter() + { + Name = "Name", + Operator = NodeFilterOperator.NotStartsWith, + Value = "test" + }, + new NodeFilterProperty() + { + Name = "Name", + Type = NodeFilterPropertyDataType.String + }, + new NodePropertyFilter() + { + Property = "Name", + Type = typeof(string), + Values = new List { "test" }, + IsNotFilter = true, + FilterType = FilterType.STARTSWITH, + IsDateTime = false + } + ); + + // test case for date with ends with operator + yield return new TestCaseData( + new NodeFilter() + { + Name = "Name", + Operator = NodeFilterOperator.EndsWith, + Value = "test" + }, + new NodeFilterProperty() + { + Name = "Name", + Type = NodeFilterPropertyDataType.String + }, + new NodePropertyFilter() + { + Property = "Name", + Type = typeof(string), + Values = new List { "test" }, + IsNotFilter = false, + FilterType = FilterType.ENDSWITH, + IsDateTime = false + } + ); + + // test case for date with not ends with operator + yield return new TestCaseData( + new NodeFilter() + { + Name = "Name", + Operator = NodeFilterOperator.NotEndsWith, + Value = "test" + }, + new NodeFilterProperty() + { + Name = "Name", + Type = NodeFilterPropertyDataType.String + }, + new NodePropertyFilter() + { + Property = "Name", + Type = typeof(string), + Values = new List { "test" }, + IsNotFilter = true, + FilterType = FilterType.ENDSWITH, + IsDateTime = false + } + ); + } + } + + [Test] + [TestCaseSource("ConvertExpandNodeFilterToNodeFilterTestCases")] + public void TestConvertExpandNodeFilterToNodeFilter(NodeFilter filter, NodeFilterProperty prop, INodeFilter expectedParsedFilter) + { + INodeFilter actualParsedFilter = ObjectExplorerUtils.ConvertExpandNodeFilterToNodeFilter(filter, prop); + Assert.That( + JsonConvert.SerializeObject(actualParsedFilter), + Is.EqualTo(JsonConvert.SerializeObject(expectedParsedFilter)), + $"Error parsing node filter '{prop.Name} ({prop.Type.ToString()})' with operator '{filter.Operator.ToString()}' and value '{filter.Value}'" + ); + } + + [Test] + [TestCase("test''test", "[(@Name = 'test''''test')]", "[(like(@Name, 'test''''test%'))]")] + [TestCase("test'test", "[(@Name = 'test''test')]", "[(like(@Name, 'test''test%'))]")] + [TestCase("test'test'test", "[(@Name = 'test''test''test')]", "[(like(@Name, 'test''test''test%'))]")] + [TestCase("test'[test]test", "[(@Name = 'test''[test]test')]", "[(like(@Name, 'test''[[]test]test%'))]")] + [TestCase("test^][%test", "[(@Name = 'test^][%test')]", "[(like(@Name, 'test[^]][[][%]test%'))]")] + [TestCase("test%test", "[(@Name = 'test%test')]", "[(like(@Name, 'test[%]test%'))]")] + [TestCase("test[test", "[(@Name = 'test[test')]", "[(like(@Name, 'test[[]test%'))]")] + [TestCase("test]test", "[(@Name = 'test]test')]", "[(like(@Name, 'test]test%'))]")] + [TestCase("test%%'%%test", "[(@Name = 'test%%''%%test')]", "[(like(@Name, 'test[%][%]''[%][%]test%'))]")] + public void TestFilterValuesWithSpecialCharacters(string input, string expectedFilterValue, string expectedLikeFilterValue) + { + var nonLikeFilter = new List + { + new NodePropertyFilter() + { + Property = "Name", + Type = typeof(string), + ValidFor = ValidForFlag.All, + Values = new List { input }, + FilterType = FilterType.EQUALS, + IsDateTime = false + } + }; + + var actualFilterValue = INodeFilter.GetPropertyFilter(nonLikeFilter, typeof(SqlHistoryTableQuerier), ValidForFlag.All); + Assert.That(actualFilterValue, Is.EqualTo(expectedFilterValue)); + + var likeFilter = new List + { + new NodePropertyFilter() + { + Property = "Name", + Type = typeof(string), + ValidFor = ValidForFlag.All, + Values = new List { input }, + FilterType = FilterType.STARTSWITH, + IsDateTime = false + } + }; + + var actualLikeFilterValue = INodeFilter.GetPropertyFilter(likeFilter, typeof(SqlHistoryTableQuerier), ValidForFlag.All); + Assert.That(actualLikeFilterValue, Is.EqualTo(expectedLikeFilterValue)); + } } }