Normalizing T-SQL text, part 2: using the TransactSql.ScriptDom classes


Happy New Year 2014 to all of you! With SQL Server 2014 set to release this year, I’m sure you are all excited about the months to come.

In my previous post I had reviewed the classic ways of obtaining ‘normalized’ text for ad-hoc SQL queries. Do take a minute to glance at that post in case you have not already done so. Both the methods described previously are dynamic - they need an active workload to operate upon. So if you have a static set of queries captured somewhere – such as a .SQL file or such, then we need an alternate method.

Algorithm

If you think about it, the core of normalizing these ad-hoc query text patterns is to identify literals and replace then with a generic / common value. Once the specific literal values are replaced with their generic ‘placeholders’ then it becomes a relatively easy task to identify commonalities.

To identify commonalities we propose to use a hashing algorithm, conceptually similar to the one used in the previous approaches. However, when computing hashes, there is another problem to consider: minor differences in whitespace / alphabet case of the query text will cause different hash values to be raised for essentially identical text.

ScriptDom Implementation

The first thing to consider is what kind of literals we would replace. In the ScriptDom class hierarchy, we have the following derived classes for the parent Literal class:

  • IntegerLiteral: whole numbers
  • NumericLiteral: decimal numbers such as 0.03
  • RealLiteral: numbers written with scientific notation such as 1E-02
  • MoneyLiteral: values prefixed with currency symbol such as $12
  • BinaryLiteral: such as 0xABCD1234
  • StringLiteral: such as ‘Hello World’
  • NullLiteral: the NULL value
  • DefaultLiteral: the DEFAULT keyword
  • MaxLiteral: the MAX specifier
  • OdbcLiteral: ODBC formatted literal such as { T 'blah' }
  • IdentifierLiteral: ‘special’ case when an identifier is used as a literal. I’ve never seen a real world example of this Smile

We need to keep this in mind when we write the visitor to traverse through the AST.

Visitor definition

Next, we need to setup our visitor. We will use the Visitor pattern to do this, and implement overridden methods to handle the various types of literals described above. And for each type of literal we will replace the value of the literal with a fixed, generic value. Here is an example for the real literal visitor method:

public override void ExplicitVisit(RealLiteral node)
{
    node.Value = "0.5E-2";
    base.ExplicitVisit(node);
}

Visitor invocation

For performance reasons we will call the visitor with the Parallel.ForEach loop which makes efficient use of multi-core CPUs:

Parallel.ForEach(
                 (frag as TSqlScript).Batches,
                 batch =>
                     {
                         myvisitor visit = new myvisitor();

                        batch.Accept(visit);

This way, each T-SQL batch in the original script is visited on a separate thread.

Regenerate the Script

Once the visitor does its job to ‘normalize’ the various literals encountered, the next step is to generate the script based on the tokens already obtained. That will take care of one of the 2 problems we spoke about – namely whitespace. We can do that using one of the many SqlScriptGenerator classes available (there is one for each compatibility level.) In the code snippet below, srcgen is one of the SqlScriptGenerator classes and script holds the output:

scrgen.GenerateScript(batch, out script);

Calculate the hash

Once the normalized script is re-generated from the SqlScriptGenerator class, it can then be run through a proper hash algorithm (in this sample we use SHA1) to calculate the hash value of the given script. Here is where we also handle the case sensitive / insensitive nature of the script:

  • For case insensitive cases, we simply convert the generated script to lower case before we compute the hash.
  • For case sensitive, we calculate the hash as-is on the generated script.

using (var hashProvider = new SHA1CryptoServiceProvider())
{
    if (caseSensitive)
    {
        hashValue = Convert.ToBase64String(hashProvider.ComputeHash(Encoding.Unicode.GetBytes(script)));
    }
    else
    {
        hashValue = Convert.ToBase64String(hashProvider.ComputeHash(Encoding.Unicode.GetBytes(script.ToLowerInvariant())));
    }
}

Track unique hashes

We can use a simple Dictionary class in .NET to track these, along with usage counts for each bucket. Each bucket also tracks an example of the batch (the original text itself.)

Sample output

The sample project when compiled and executed as below gives us the following output.

Command line

TSQLTextNormalizer.exe c:\temp\input.sql c:\temp\output.sql 110 false

Input

select * from ABC
where
i = 1
GO

select * from abC where i = 3
GO

Output

-- 2 times:
select * from ABC
where
i = 1
GO

That’s it! You can use this in many cases, limited only by your imagination Smile And more importantly I hope it showed you the power and flexibility of the ScriptDom classes.

Download the sample project here

Disclaimer

This Sample Code is provided for the purpose of illustration only and is not intended to be used in a production environment.  THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.  We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to reproduce and distribute the object code form of the Sample Code, provided that You agree: (i) to not use Our name, logo, or trademarks to market Your software product in which the Sample Code is embedded; (ii) to include a valid copyright notice on Your software product in which the Sample Code is embedded; and (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against any claims or lawsuits, including attorneys’ fees, that arise or result from the use or distribution of the Sample Code.

This posting is provided "AS IS" with no warranties, and confers no rights. Use of included script samples are subject to the terms specified at http://www.microsoft.com/info/cpyright.htm.

Comments (5)

  1. rrozema says:

    I’m trying to take the normalizing even further: I would like to reduce certain script parts to equivalent parts. But I don’t see how to use the Visitor pattern to make modifications in the AST-structure. For example: I would like to normalize “select 1 where not @a is null” into “select 1 where @a is not null”, i.e. replace each instance of “not is null” by ” is not null”. Can you please give me an example on how to replace all node sequences of “BooleanNotExpression -> BooleanIsNullExpression” by a node “BooleanIsNullExpression” with it’s IsNot property set to the !IsNot of the old BooleanIsNullExpression?

    1. Interesting question. I don’t have an example readily available, but I might just try to write one for you. Stay tuned!

      1. Hi rrozema. The example below should show you how to accomplish your requirement. The visitor pattern can still help to generalize this (in my sample below there is a hardcoded expression for whereClause, which can potentially be generalized by using Visitor pattern later):

        void Main()
        {
        var sqlQuery = @”select 1 where not @a is null”;

        var myParser = new TSql130Parser(true);

        using (var rdr = new StringReader(sqlQuery))
        {
        IList errors = null;
        var parseOutput = myParser.Parse(rdr, out errors);

        var whereClause = ((((TSqlScript) parseOutput).Batches[0].Statements[0] as SelectStatement).QueryExpression as QuerySpecification).WhereClause;
        var srchCond = whereClause.SearchCondition;
        if (srchCond is BooleanNotExpression)
        {
        if ((srchCond as BooleanNotExpression).Expression is BooleanIsNullExpression)
        {
        var reWritten = (srchCond as BooleanNotExpression).Expression as BooleanIsNullExpression;
        reWritten.IsNot = true;

        whereClause.SearchCondition = reWritten;

        var scriptGen = new Sql130ScriptGenerator();

        string batchScript;
        scriptGen.GenerateScript(parseOutput, out batchScript);

        batchScript.Dump();
        }
        }
        }
        }

        1. Richard Rozema says:

          Thank you, that is very helpful already. I’ll see where this gets me.

          I think I have to go through the tree, visiting all objects that have an Expression property and check to see if this Expression property’s value is a BooleanNotExpression which has an expression which is of type BooleanIsNullExpression. Each found instance I store in a list. After the Accept() returns, I replace all found expressions by the normalized version. Then generate the normalized statements. Is that the correct way to do it, or is there a smarter way?

          And further, do you know of documentation on how the visitor pattern traverses the AST tree? Plus what properties or tools we can and can not use from within the visitor code? f.e. is it safe to alter the Expression property’s value from within the visitor or is the

          1. Richard Rozema says:

            Hi Arvind,

            I’ve been playing with your example code and managed to add the “not is null” replacement plus a lot more. I have made several attempts on reducing parenthesis too, but failed to get something working correctly so far :-(. Yet, I thought I’d share my code to see if I can inspire you or other co-readers to add more normalisations. I’ve put the contents of my TSqlVisitor.cs below. It should be easy to copy-paste this over the contents in your demo project.

            using System;

            namespace TSQLTextNormalizer
            {

            using Microsoft.SqlServer.TransactSql.ScriptDom;
            using System.Reflection;

            class myvisitor : TSqlFragmentVisitor
            {
            public myvisitor()
            {
            }

            //public override void Visit(TSqlFragment node)
            //{
            // //Console.WriteLine(“{0}”, node.GetType().ToString());
            //
            // Because it is very hard to know for sure I’ve got an ExplicitVisit() method implemented to
            // cover all properties in all fragment types, I was trying to genericly locate all properties
            // of type BooleanExpression and ScalarExpression, and call the appropriate Normalize function
            // for them. I haven’t been able to get this working yet though.

            // Type ScalarExpressionType = typeof(ScalarExpression);
            // foreach (var property in node.GetType().
            // GetProperties(BindingFlags.Public | BindingFlags.Instance))
            // {
            // //Console.WriteLine(“{0}.{1} is a {2};”, node.GetType().ToString(), property.Name, property.PropertyType.ToString());
            // if (ScalarExpressionType.IsInstanceOfType(property.DeclaringType))
            // {
            // Console.WriteLine(“{0}.{1} is a ScalarExpression;”, node.GetType().ToString(), property.Name);
            // }
            // }

            // base.Visit(node);
            //}

            public override void ExplicitVisit(InPredicate node)
            {
            int numvalues = node.Values.Count;
            if (numvalues > 1)
            {
            for (int i = numvalues – 1; i > 0; i–)
            {
            if (node.Values[i] is Literal)
            {
            node.Values.RemoveAt(i);
            }
            }
            }

            base.ExplicitVisit(node);
            }

            public override void ExplicitVisit(WhereClause node)
            {
            node.SearchCondition = NormalizeBooleanExpression(node.SearchCondition);
            base.ExplicitVisit(node);
            }

            public override void ExplicitVisit(BooleanBinaryExpression node)
            {
            node.FirstExpression = NormalizeBooleanExpression(node.FirstExpression);
            node.SecondExpression = NormalizeBooleanExpression(node.SecondExpression);
            base.ExplicitVisit(node);
            }

            public override void ExplicitVisit(BooleanComparisonExpression node)
            {
            node.FirstExpression = NormalizeScalarExpression(node.FirstExpression);
            node.SecondExpression = NormalizeScalarExpression(node.SecondExpression);
            base.ExplicitVisit(node);
            }

            public override void ExplicitVisit(SearchedWhenClause node)
            {
            node.WhenExpression = NormalizeBooleanExpression(node.WhenExpression);
            base.ExplicitVisit(node);
            }

            public override void ExplicitVisit(BooleanParenthesisExpression node)
            {
            node.Expression = NormalizeBooleanExpression(node.Expression);
            base.ExplicitVisit(node);
            }

            public override void ExplicitVisit(QuerySpecification node)
            {
            base.ExplicitVisit(node);
            }

            public override void ExplicitVisit(AssignmentSetClause node)
            {
            node.NewValue = NormalizeScalarExpression(node.NewValue);
            base.ExplicitVisit(node);
            }

            public override void ExplicitVisit(FunctionCall node)
            {
            for(int i = 0; i 2)
            {
            node.SchemaObject.Identifiers.RemoveAt(0);
            }
            }
            base.ExplicitVisit(node);
            }

            public int PrecedenceLevel(TSqlFragment fragment)
            {
            if (fragment is BooleanBinaryExpression)
            {
            switch ((fragment as BooleanBinaryExpression).BinaryExpressionType)
            {
            case BooleanBinaryExpressionType.And:
            return 6;
            case BooleanBinaryExpressionType.Or:
            return 7;
            }
            }

            if (fragment is BooleanNotExpression)
            {
            return 5;
            }

            if (fragment is BooleanTernaryExpression)
            {
            switch ((fragment as BooleanTernaryExpression).TernaryExpressionType)
            {
            case BooleanTernaryExpressionType.Between:
            case BooleanTernaryExpressionType.NotBetween:
            return 7;
            }
            }

            if (fragment is BooleanComparisonExpression)
            {
            switch ((fragment as BooleanComparisonExpression).ComparisonType)
            {
            case BooleanComparisonType.Equals:
            case BooleanComparisonType.LessThan:
            case BooleanComparisonType.GreaterThanOrEqualTo:
            case BooleanComparisonType.LessThanOrEqualTo:
            case BooleanComparisonType.NotEqualToBrackets:
            case BooleanComparisonType.NotEqualToExclamation:
            case BooleanComparisonType.NotGreaterThan:
            case BooleanComparisonType.NotLessThan:
            return 4;
            }
            }

            if (fragment is UnaryExpression)
            {
            switch ((fragment as UnaryExpression).UnaryExpressionType)
            {
            case UnaryExpressionType.BitwiseNot:
            return 1;

            case UnaryExpressionType.Positive:
            case UnaryExpressionType.Negative:
            return 3;
            }
            }

            if (fragment is BinaryExpression)
            {
            switch ((fragment as BinaryExpression).BinaryExpressionType)
            {
            case BinaryExpressionType.Add:
            case BinaryExpressionType.Subtract:
            case BinaryExpressionType.BitwiseAnd:
            case BinaryExpressionType.BitwiseXor:
            case BinaryExpressionType.BitwiseOr:
            return 3;
            case BinaryExpressionType.Multiply:
            case BinaryExpressionType.Divide:
            case BinaryExpressionType.Modulo:
            return 2;
            }
            }

            if (fragment is SelectSetVariable)
            {
            return 8;
            }

            return 0;
            //throw new Exception(string.Format(“No precedence level known for \”{0}\” of type {1}.”, fragment.ToString(), fragment.GetType().Name));
            }

            public ScalarExpression NormalizeScalarExpression(ScalarExpression expression)
            {
            if (expression is ParenthesisExpression)
            {
            // Replace (expr) by expr.
            return NormalizeScalarExpression((expression as ParenthesisExpression).Expression);
            }

            if (expression is UnaryExpression)
            {
            UnaryExpression ue = expression as UnaryExpression;

            switch (ue.UnaryExpressionType)
            {
            case UnaryExpressionType.Positive:
            return NormalizeScalarExpression(ue.Expression);

            //case UnaryExpressionType.Negative:
            // if (ue.Expression is UnaryExpression && (ue.Expression as UnaryExpression).UnaryExpressionType == UnaryExpressionType.Negative)
            // {
            // return NormalizeScalarExpression((ue.Expression as UnaryExpression).Expression);
            // }

            // if (ue.Expression is BinaryExpression)
            // {
            // BinaryExpression be = ue.Expression as BinaryExpression;

            // switch (be.BinaryExpressionType)
            // {
            // case BinaryExpressionType.Add:
            // be.BinaryExpressionType = BinaryExpressionType.Subtract;
            // return NormalizeScalarExpression(be);

            // case BinaryExpressionType.Subtract:
            // be.BinaryExpressionType = BinaryExpressionType.Add;
            // return NormalizeScalarExpression(be);

            // case BinaryExpressionType.Multiply:
            // be.BinaryExpressionType = BinaryExpressionType.
            // }

            // //ue.Expression = be.FirstExpression;
            // //be.FirstExpression = ue;

            // //ue = new UnaryExpression();
            // //ue.UnaryExpressionType = UnaryExpressionType.Negative;
            // //ue.Expression = be.SecondExpression;
            // //be.SecondExpression = ue;

            // //return NormalizeScalarExpression(be);
            // }

            if (ue.Expression is ParenthesisExpression)
            {
            // Replace +(expr) by +expr, -(expr) by -expr and ~(expr) by ~expr.
            ue.Expression = (ue.Expression as ParenthesisExpression).Expression;
            return NormalizeScalarExpression(ue);
            }
            break;

            case UnaryExpressionType.BitwiseNot:
            break;
            }
            }

            //if (expression is BinaryExpression)
            //{
            // BinaryExpression be = expression as BinaryExpression;
            // if (be.FirstExpression is ParenthesisExpression)
            // {
            // ParenthesisExpression pe = be.FirstExpression as ParenthesisExpression;

            // }

            // //be.SecondExpression
            //}

            return expression;
            }

            public BooleanExpression NormalizeBooleanExpression(BooleanExpression expression)

            {
            if (expression is BooleanNotExpression)
            {
            // Replace not is null by is not null.
            if ((expression as BooleanNotExpression).Expression is BooleanIsNullExpression)
            {
            BooleanIsNullExpression bine = ((expression as BooleanNotExpression).Expression) as BooleanIsNullExpression;
            bine.IsNot = !bine.IsNot;
            return NormalizeBooleanExpression(bine);
            }

            if ((expression as BooleanNotExpression).Expression is BooleanNotExpression)
            {
            // Replace “not not expr” by “expr”.
            return NormalizeBooleanExpression(((expression as BooleanNotExpression).Expression as BooleanNotExpression).Expression);
            }

            if ((expression as BooleanNotExpression).Expression is BooleanComparisonExpression)
            {
            BooleanComparisonExpression bce = (expression as BooleanNotExpression).Expression as BooleanComparisonExpression;
            switch (bce.ComparisonType)
            {
            // Replace not a == b by a != b
            case BooleanComparisonType.Equals:
            bce.ComparisonType = BooleanComparisonType.NotEqualToExclamation;
            return NormalizeBooleanExpression(bce);

            // Replace not a b by a == b
            case BooleanComparisonType.NotEqualToBrackets:
            // Replace not a != b by a == b
            case BooleanComparisonType.NotEqualToExclamation:
            bce.ComparisonType = BooleanComparisonType.Equals;
            return NormalizeBooleanExpression(bce);

            // Replace not a > b by a = b by a < b
            case BooleanComparisonType.GreaterThanOrEqualTo:
            case BooleanComparisonType.NotLessThan:
            bce.ComparisonType = BooleanComparisonType.LessThan;
            return NormalizeBooleanExpression(bce);

            // Replace not a = b
            case BooleanComparisonType.LessThan:
            bce.ComparisonType = BooleanComparisonType.GreaterThanOrEqualTo;
            return NormalizeBooleanExpression(bce);

            // Replace not a b
            case BooleanComparisonType.LessThanOrEqualTo:
            case BooleanComparisonType.NotGreaterThan:
            bce.ComparisonType = BooleanComparisonType.GreaterThan;
            return NormalizeBooleanExpression(bce);
            }
            }

            if ((expression as BooleanNotExpression).Expression is BooleanBinaryExpression)
            {
            BooleanBinaryExpression bbe = (expression as BooleanNotExpression).Expression as BooleanBinaryExpression;

            switch (bbe.BinaryExpressionType)
            {
            // Replace “not expr and expr” by “not expr or not expr”
            case BooleanBinaryExpressionType.And:
            //BooleanParenthesisExpression bpe = new BooleanParenthesisExpression();

            //bpe.Expression = bbe;

            bbe.BinaryExpressionType = BooleanBinaryExpressionType.Or;

            BooleanNotExpression bne = expression as BooleanNotExpression;
            bne.Expression = bbe.FirstExpression;
            bbe.FirstExpression = bne;

            bne = new BooleanNotExpression();
            bne.Expression = bbe.SecondExpression;
            bbe.SecondExpression = bne;

            return NormalizeBooleanExpression(bbe);

            // Replace “not expr or expr” by “not expr and not expr”
            case BooleanBinaryExpressionType.Or:
            bbe.BinaryExpressionType = BooleanBinaryExpressionType.And;

            bne = expression as BooleanNotExpression;
            bne.Expression = bbe.FirstExpression;
            bbe.FirstExpression = bne;

            bne = new BooleanNotExpression();
            bne.Expression = bbe.SecondExpression;
            bbe.SecondExpression = bne;

            return NormalizeBooleanExpression(bbe);
            }
            }

            if ((expression as BooleanNotExpression).Expression is BooleanTernaryExpression)
            {
            // Replace “Not Between” by “Not between” and “Not Not between” by “Between”
            BooleanTernaryExpression bte = (expression as BooleanNotExpression).Expression as BooleanTernaryExpression;

            bte.TernaryExpressionType = bte.TernaryExpressionType == BooleanTernaryExpressionType.Between ? BooleanTernaryExpressionType.NotBetween : BooleanTernaryExpressionType.Between;

            return NormalizeBooleanExpression(bte);
            }

            if ((expression as BooleanNotExpression).Expression is BooleanParenthesisExpression)
            {
            // If Expression is either of NOT, AND, ALL, ANY, BETWEEN, IN, LIKE, OR, SOME or an assignment, we can remove the
            // parenthesis, as NOT’s precendece is not lower than that of the expression and left-to-right evaluation will make
            // sure the operators are properly processed.
            if (PrecedenceLevel(((expression as BooleanNotExpression).Expression as BooleanParenthesisExpression).Expression) < 5)
            {
            return NormalizeBooleanExpression(((expression as BooleanNotExpression).Expression as BooleanParenthesisExpression).Expression);
            }
            // BooleanParenthesisExpression bpe = (expression as BooleanNotExpression).Expression as BooleanParenthesisExpression;

            // // If Expression is either of NOT, AND, ALL, ANY, BETWEEN, IN, LIKE, OR, SOME or an assignment, we can remove the
            // // parenthesis, as NOT's precendece is not lower than that of the expression and left-to-right evaluation will make
            // // sure the operators are properly processed.

            // // Replace not(not expr) by expr
            // if (bpe.Expression is BooleanNotExpression)
            // return NormalizeBooleanExpression(bpe.Expression);

            // // Replace "not( expr and expr)" by not expr or not expr
            // if (bpe.Expression is BooleanBinaryExpression && (bpe.Expression as BooleanBinaryExpression).BinaryExpressionType == BooleanBinaryExpressionType.And)
            // {
            // BooleanBinaryExpression bbe = bpe.Expression as BooleanBinaryExpression;
            // bbe.BinaryExpressionType = BooleanBinaryExpressionType.Or;

            // // Re-use the existing "not" for the first expression to avoid memory fragmentation.
            // BooleanNotExpression bne = expression as BooleanNotExpression;
            // bne.Expression = bbe.FirstExpression;
            // bbe.FirstExpression = bne;

            // // For the 2nd expression we need to create a new "not".
            // bne = new BooleanNotExpression();
            // bne.Expression = bbe.SecondExpression;
            // bbe.SecondExpression = bne;

            // return NormalizeBooleanExpression(bbe);
            // }

            // // Replace "not( expr or expr)" by not expr and not expr
            // if (bpe.Expression is BooleanBinaryExpression && (bpe.Expression as BooleanBinaryExpression).BinaryExpressionType == BooleanBinaryExpressionType.And)
            // {
            // BooleanBinaryExpression bbe = bpe.Expression as BooleanBinaryExpression;

            // // Re-use the existing "not" for the first expression to avoid memory fragmentation.
            // BooleanNotExpression bne = expression as BooleanNotExpression;
            // bne.Expression = bbe.FirstExpression;
            // bbe.FirstExpression = bne;

            // // For the 2nd expression we need to create a new "not".
            // bne = new BooleanNotExpression();
            // bne.Expression = bbe.SecondExpression;
            // bbe.SecondExpression = bne;

            // return NormalizeBooleanExpression(bbe);
            // }

            }
            }

            if (expression is BooleanParenthesisExpression)
            {
            if ((expression as BooleanParenthesisExpression).Expression is BooleanParenthesisExpression)
            {
            // Replace ((expr)) by (expr).
            return NormalizeBooleanExpression((expression as BooleanParenthesisExpression).Expression);
            }
            }

            //else if (expression is BooleanBinaryExpression)
            //{
            // BooleanBinaryExpression bbe = expression as BooleanBinaryExpression;

            // if (bbe.FirstExpression is BooleanParenthesisExpression && PrecedenceLevel((bbe.FirstExpression as BooleanParenthesisExpression).Expression) <= PrecedenceLevel(bbe.SecondExpression))
            // {
            // bbe.FirstExpression = NormalizeBooleanExpression((bbe.FirstExpression as BooleanParenthesisExpression).Expression);
            // }
            //}
            //else if (expression is BooleanParenthesisExpression)
            //{
            // // Replace "(expr op expr)" by "expr op expr" if precendence level already provides correct evaluation order.
            // if ((expression as BooleanParenthesisExpression).Expression is BooleanBinaryExpression)
            // {
            // BooleanBinaryExpression bbe = (expression as BooleanParenthesisExpression).Expression as BooleanBinaryExpression;

            // if (PrecedenceLevel(bbe.FirstExpression) <= PrecedenceLevel(bbe.SecondExpression))
            // {
            // return NormalizeBooleanExpression((expression as BooleanParenthesisExpression).Expression);
            // }
            // }
            //}
            return expression;
            }
            }
            }

Skip to main content