left-icon

ScriptDOM Succinctly®
by Joseph D. Booth

Previous
Chapter

of
A
A
A

CHAPTER 7

Select Statement


The SELECT statement is certainly the most comprehensive and flexible data manipulation language statement in the SQL command set. The SELECT visitor class parses the SELECT statement, identifying tables, where expressions, columns, etc. The SELECT statement in SQL (particularly the WHERE expression) can be very complex, and ScriptDOM shines in helping parse it out.

We will use the SELECT statement to deal with the phone list problem from Chapter 1 (shown in Listing 26), but the SELECT statement processing is much more capable.

Code Listing 26: PhoneList

CREATE procedure [dbo].[PhoneList] (@whichparty varchar(12))

as

begin

     select * from dbo.voters

     where upper(party_affiliation) = @whichparty

end

Visitor method

The visitor method call to get SELECT statements is shown in Listing 27. The node has a base class of a QueryExpression, but we are going to get the child class, QuerySpecification, to get the necessary extra properties for handling the SELECT statement. This will be a fairly common theme in your Visitor classes: many properties are only exposed in the child class.

Code Listing 27: Visitor method for select statements

public override void ExplicitVisit(SelectStatement node)

{

    if (node.QueryExpression is QueryExpression)

    {

       QuerySpecification qe = node.QueryExpression as QuerySpecification;

    }

    base.ExplicitVisit(node);

}

Note: We need to cast the QueryExpression to a QuerySpecification to access the various properties. For example, the QueryExpression only has the ForClause property, the OffsetClause, and the OrderByClause. The QuerySpecification adds the FromClause, the GroupByClause, etc. It is derived from the QueryExpression class, but adds the additional properties needed to process a SQL SELECT statement.

The node will contain all the standard properties (FirstTokenIndex, FragmentLength, StartLine, etc.) However, Table 8 lists the properties unique to the SELECT statement, which are the properties we will likely use during our analysis method.

Table 8: Unique SELECT statement properties

Property

Type

Description

ComputeClause

Collection of Compute expressions

Although the property still exists, it has not been supported since SQL 2012. You should replace it with ROLLUP.

Into

Schema object name

The name of the table to which the results are sent. This can be null or possibly a fully qualified table name

OptimizerHints

Collection of optimizer hint options

Any optimizer hints found in the query (See the OPTION keyword for details)

·     Hint kind (Enum of the option)

·     Value (Setting for the option)

QueryExpression

Query specification

Details of tables, WHERE clauses, OrderBy, etc.

The QueryExpression property has the various information we need to parse the SELECT statement. If a particular clause, such as ORDER BY, is not present in the code, it will be null in the QueryExpression property. This is the workhorse property of the SELECT statement. Table 9 lists the key properties.

Table 9: Query expression properties

Property

Type

Description

ForClause

Collection of FOR settings (XML, JSON)

The Options property will vary depending on the type of FOR option. For example, the expression

FOR JSON PATH ROOT(‘voters’)

will return two JSON FOR clause options:

·     Path (no parameters)

·     Root (Parameter value of voters)

FromClause

From clause

Table references:

·     Named table reference

·     Qualified join

·     Null if no table specified

GroupByClause

Group by clause

A group-by option (none, rollup, cube) collection of expression-grouping specification objects.

HavingClause

Having clause

The search condition (Boolean comparison class) for the having expression is found in this object

OrderByClause

Order by clause

This class has a list of expressions making up the order clause; the expression can be functions, column names, or integer values

SelectElements

List

Each column type (*, column, etc.):

·     Select star expression

·     Select scalar expression

o     Column reference

o     Binary expression

TopRowFilter

Top row filter class

Contains the:

·     Expression (integer or function)

·     Percent Boolean

·     With ties Boolean

WhereClause

Where clause

The search condition class provides the details of the WHERE condition

A very simple code snippet might check to see if the TopRowFilter is specified (not null) while the OrderByClause is null. Such a condition could bring back random rows, which is probably not what is expected.

Determining tables

The FromClause property of the query expression contains the table references used by the query. For a simple query (one table), the TableReferences collection will contain a named table object with two key properties: the alias (null if not used) and the schema object, which provides the name of the table (property base identifier). A schema object has other properties to identify each element in a four-part table reference, shown in Table 10.

Table 10: Four-part table reference

Element

Example

Property

Server name

[JDB01]

ServerIdentifier

Database name

[BookExample]

DatabaseIdentifier

Schema name

[dbo]

SchemaIdentifier

Base identifier

[customer]

Table or view name

To make table naming easier, we can add the private method shown in Listing 28 to our Visitor class. It takes a schema object and returns a string table name (with as many components as found in the object)

Code Listing 28: Schema to TableName

   private string SchemaToTableName(SchemaObjectName schemaObject)

   {

       string tblName = "";

       if (schemaObject.ServerIdentifier != null)  {

           tblName = "["+ schemaObject.ServerIdentifier.Value+"].";

           }

       if (schemaObject.DatabaseIdentifier != null) {

           tblName += "[" + schemaObject.DatabaseIdentifier.Value + "].";

           }

       if (schemaObject.SchemaIdentifier!= null) {

           tblName += "[" + schemaObject.SchemaIdentifier.Value + "].";

           }

       tblName += "[" + schemaObject.BaseIdentifier.Value + "]";

       return tblName;

 }

Handling JOINS

When there are joins in the SELECT statement, the Table References collection will not be a named table object, but rather a qualified join object. This object contains properties to determine the join tables and the expression to join the tables or views.

Table 11 shows the properties of the qualified join object.

Table 11: Qualified Join class

Property

Type

Description

FirstTableReference

Named table reference

Details of the table or view

QualifiedJoinType

Enumeration

Full, inner, left, right

SearchCondition

Boolean comparison

Expression used to join tables

SecondTableReference

Named table reference

Details of the second table or view

If there are more than two tables being joined, the second table reference will be a qualified join object instead of a named table reference. Depending on the query complexity, this object structure can get quite deep.

To simplify the processing, we can create a nested object called JoinedTables and a list of joined tables in the Visitor class. Listing 29 shows the object and list property.

Code Listing 29: Joined tables

            public class JoinedTables

            {

                public string? LeftTable { get; set; }

                public string? RightTable { get; set; }

                public QualifiedJoinType? joinType { get; set; }

   public string? JoinExpression { get; set; }

                public BooleanComparisonExpression? joinOn { get; set; }

            }

            public List<JoinedTables> tables = new List<JoinedTables>();

            public string? BaseTable;

We can now add a function that will process the SELECT statement and return a list of joined tables found in the SELECT statement. It will also set the base table string to a qualified table name. Listing 30 is the code to iterate the FROM clause to create the joined list.

Code Listing 30: Populate joined tables

private void BuildJoinedTables(FromClause fromClause)

{

    tables.Clear();

    BaseTable = "";

    if (fromClause != null)

    {

       // If first reference is a table, rather than a join reference,

       // then only a single table

      if (fromClause.TableReferences[0] is NamedTableReference)

      {

         NamedTableReference tb =

                (NamedTableReference)fromClause.TableReferences[0];

         BaseTable = SchemaToTableName(tb.SchemaObject);

         return;

      }

      if (fromClause.TableReferences[0] is QualifiedJoin)

      {

       QualifiedJoin qn = (QualifiedJoin)fromClause.TableReferences[0];

       NamedTableReference tb1 = (NamedTableReference)qn.FirstTableReference;

       BaseTable = SchemaToTableName(tb1.SchemaObject);

       if (tb1.Alias != null)

       {

          BaseTable += " " + tb1.Alias.Value;

       }

       NamedTableReference tb2 = (NamedTableReference)qn.SecondTableReference;

       string SecondTable = SchemaToTableName(tb2.SchemaObject);

       if (tb2.Alias != null)

       {

          SecondTable += " " + tb2.Alias.Value;

       }

       JoinedTables jt = new JoinedTables();

           jt.LeftTable = BaseTable;

           jt.RightTable = SecondTable;

           jt.ExpObj = (BooleanComparisonExpression)qn.SearchCondition;

           jt.JoinExpression = SearchExpressionToText(jt.ExpObj);

           jt.joinType = qn.QualifiedJoinType;

           tables.Add(jt);

           return;

    }

  }

}

We also include a function to convert the Join expression to text to make it more readable. Listing 31 contains the SearchExpressionToText() function.

Code Listing 31: Search Expression to Text

private string SearchExpressionToText(BooleanComparisonExpression ExpObj)

{

    string ans = "";

    // Column reference, alias, and column name

    if(ExpObj.FirstExpression is ColumnReferenceExpression)

    {

        ColumnReferenceExpression? cr1 = ExpObj.FirstExpression as ColumnReferenceExpression;

        for(int x=0;x<cr1.MultiPartIdentifier.Count;x++)

        {

            if (x>0) {  ans += "."; }

            ans += cr1.MultiPartIdentifier[x].Value;                      

        }

        switch(ExpObj.ComparisonType)

        {

            case BooleanComparisonType.LessThan:

                ans += " < ";

                break;

            case BooleanComparisonType.GreaterThan:

                ans += " > ";

                break;

            case BooleanComparisonType.GreaterThanOrEqualTo:

                ans += " <= ";

                break;

            case BooleanComparisonType.LessThanOrEqualTo:

                ans += " <= ";

                break;

            default:

                ans += " = ";

                break;

        }

        ColumnReferenceExpression? cr2 = ExpObj.SecondExpression as

                            ColumnReferenceExpression;

        for (int x = 0; x < cr2.MultiPartIdentifier.Count; x++)

        {

            if (x > 0) { ans += "."; }

            ans += cr2.MultiPartIdentifier[x].Value;

        }

    }

    return ans;

}

WHERE clause

The primary property on the WHERE clause is the SearchCondition object, which returns the first and second expressions. For a simple WHERE (one condition), the object type is a BooleanComparisonExpression. In the WHERE filter (such as upper(party_affiliation) = @whichparty), the SearchCondition object will contain a first expression (in this case, a function call object) and a second expression (a variable reference). The comparison type (equals) shows how the two expressions are compared. Table 12 shows the mapping between the code and the object.

Table 12: Example WHERE parsing

SQL code

Object

Upper(party_affiliation)

Function call object, with a function name nested object

=

Comparison type (equals)

@whichparty

Variable reference, with a name field holding the variable name

The object types will vary, such as column reference or string literal. The comparison type will be an enumeration, such as equals, greater than, or less than.

When there are multiple expressions, the search condition changes a bit. It will be a type Boolean binary expression, with a binary expression type of AND/OR. The first expression could be a Boolean comparison expression, with options similar to Table 12. Similarly, the second expression can be a simple comparison.

Depending on the complexity of the WHERE clause, the object can get pretty deep. Table 13 shows a visual reference of the object structure for the following SQL clause:

Party affiliation=@whichParty AND gender=’F’

Table 13: Multiple condition WHERE example

Object

·     Search condition is a Boolean binary expression

·     Binary expression type: And

·     First expression: Boolean comparison expression

o     First expression: Column reference (party affiliation)

o     Comparison type: Equals

o     Second expression: Variable (@whichParty)

·     Second expression: Boolean comparison expression

o     First expression: Column reference (gender)

o     Comparison type: Equals

o     Second expression: String literal (F)

If a third condition were added, the second expression would become a Boolean binary expression object and would have first and second expression objects. The collection of expression properties will be as deep as needed to handle all of the conditions.

Tip: If you find the nesting getting pretty deep, it might be worth redesigning the query.

ORDER BY clause

The ORDER BY clause object only has one property of interest, the OrderByElements property, which is a collection of each expression in the ORDER BY clause. This list contains any number of ExpressionWithSortOrder objects.

Within the SortOrder objects, there are two interesting properties: The SortOrder is an enumerated value of ascending, descending, or not specified; the Expression property provides the details of the sort item.

Column reference expression

A column reference uses the multi-part identifier to return the column and possibly the table or alias name. You can loop through the property to determine the column for the OrderBy. Code Listing 32 shows the code to assemble the column reference.

Code Listing 32: Build Column reference

string ans = "";

             for(int x=0;x<cr1.MultiPartIdentifier.Count;x++)

                 {

                    if (x>0) {  ans += "."; }

                    ans += cr1.MultiPartIdentifier[x].Value;                      

                 }

Integer literal

SQL Server allows you to use numeric position when specifying sort order, although it is discouraged and may be unsupported in future SQL versions. If you use an integer sort column reference, the Value property will contain the numeric value.

Note: You can also sort on other literal values (dates, strings, etc.). While SQL supports this, it is not likely to provide any benefit to sort on a literal string.

GROUP BY clause

The GROUP BY clause object contains details about the grouping logic of the query. Table 14 shows the key properties of the class.

Table 14: Group By clause

Property

Type

Description

All

Boolean

The All option is noncompliant and only for backward compatibility

GroupByOption

Enumeration

Cube, None, Rollup

GroupingSpecifications

Collection

Collection of expression group specification objects for each value in the group by expression

Note: The All option is noncompliant for SQL Server and Azure.

Each ExpressionGroupSpecification object represents a column in the GROUP BY clause. The key property is the expression object, which provides details about the column.

Table 15: Expression class

Property

Type

Description

ColumnType

Enum

Regular or identity are the most common types

Multi-Part Identifier

Multi-part identifier

The Identifiers collection lists each column, and the Value property contains the column name

Having clause

The key property of the Having clause is a search condition (which is a Boolean comparison expression). This operates in the same manner as the search condition on the WHERE clause.

Example

Figure 10 shows a SELECT statement and the ScriptDOM objects that contain each clause.

SELECT statement

Figure 10: SELECT statement

Summary

The SELECT clause can get very complex with many options. ScriptDOM allows you to extract all components of the statement. When a clause does not exist in the statement, the class will be null in the QuerySpecification object.

You can look for many issues in the select statement node, such as using SELECT *, numeric ORDER BY expressions, and tables without schemas.

Scroll To Top
Disclaimer

DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.