Files
sql-utilities/docs/SqlUtilities.Core.md

50 KiB

Strata.SqlTools

A comprehensive SQL query parsing and generation library for Microsoft T-SQL and Snowflake SQL, providing robust decomposition and reconstruction of SELECT queries with support for Common Table Expressions (CTEs), expression trees, and type-safe query building.

Overview

The Strata.SqlTools namespace provides powerful classes for working with SQL queries programmatically:

  • QueryBreakdown: Parse and generate Microsoft T-SQL SELECT queries with expression-based query building
  • SnowflakeQueryBreakdown: Parse and generate Snowflake SQL SELECT queries with dialect-specific features
  • Expression System: Type-safe expression trees for building SQL queries programmatically
  • Statement Parsing: Token-based SQL parsing with support for both SQL Server and Snowflake syntax
  • Visitor Pattern: Extensible SQL generation with dialect-specific formatting

Both QueryBreakdown classes support:

  • Full SQL clause decomposition (SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY)
  • Common Table Expressions (WITH clause) with multiple CTEs
  • Parameter extraction and management (@parameter for T-SQL, :parameter or @parameter for Snowflake)
  • Query merging and composition
  • Expression-based query building with AddSelectExpression and AddWhereExpression
  • Setup and finish clauses for complex scripts
  • Deep cloning and serialization
  • Comment preservation during parsing

Features

Core Capabilities

  • Parse SQL SELECT statements into structured components
  • Generate properly formatted SQL from components
  • Support for nested subqueries and complex expressions
  • Multiple CTE support with ordered list management
  • Parameter extraction and value binding
  • Query merging and composition
  • WHERE clause builder with AND/OR operators
  • Round-trip parsing and generation

Dialect Support

  • T-SQL: Microsoft SQL Server syntax with @parameters and 5-space indentation
  • Snowflake: Snowflake-specific features including:
    • Colon parameters (:param)
    • Double colon casting (::VARCHAR)
    • VARIANT/JSON data types
    • LIMIT clause
    • QUALIFY clause
    • Time Travel (AT)
    • 4-space indentation

Architecture

Project Structure

The library is organized into focused folders for maintainability and clarity:

graph TD
    A[Strata.SqlTools] --> B[SqlServer/]
    A --> C[Snowflake/]
    A --> D[Classes/]
    A --> E[Enums/SQL/]
    A --> F[Utilities/]
    A --> G[Extensions/]
    A --> H[Expressions/]
    A --> I[Interfaces/]
    A --> J[QueryBuilders/]
    A --> K[Breakdowns/]

    B --> B1[StatementParser]
    B --> B2[StatementReader]
    B --> B3[StatementExpressionParser]
    B --> B4[CommandVisitor]
    B --> B5[QueryBreakdown]
    B --> B6[SqlClause]

    C --> C1[StatementParser]
    C --> C2[StatementReader]
    C --> C3[StatementExpressionParser]
    C --> C4[CommandVisitor]
    C --> C5[QueryBreakdown]

    D --> D1[Token]
    D --> D2[SqlClause]
    D --> D3[SqlClauses]
    D --> D4[SqlFilter]
    D --> D5[QueryParam]

    E --> E1[TokenType]
    E --> E2[SqlDataType]
    E --> E3[FilterOperation]
    E --> E4[AggregateFunction]

    F --> F1[SqlUtils]
    F --> F2[ArrayUtils]
    F --> F3[GuidUtils]
    F --> F4[StringUtils]
    F --> F5[SqlUtils.Filters]
    F --> F6[SqlColumnHelpers]
    F --> F7[SqlDataTypeHelpers]

    H --> H1[Expression]
    H --> H2[Literals/]
    H --> H3[Functions/]
    H --> H4[Operators/]

Key Components

1. Statement Parsing

The library uses a multi-stage parsing approach:

graph LR
    A[SQL String] --> B[StatementReader]
    B --> C[Tokens]
    C --> D[StatementExpressionParser]
    D --> E[Expression Tree]
    E --> F[QueryBreakdown]

    style B fill:#e1f5ff
    style D fill:#e1f5ff
    style F fill:#ffe1e1
  • StatementReader: Tokenizes SQL into individual elements (identifiers, operators, literals)
  • StatementExpressionParser: Converts tokens into expression trees
  • StatementParser: Extracts SQL clauses with comment preservation
  • QueryBreakdown: High-level query representation with all clauses

2. Visitor Pattern for SQL Generation

SQL generation uses the Template Method pattern for dialect flexibility:

classDiagram
    class CommandVisitor {
        +Visit(Expression) string
        #FormatIdentifier(string) string
        #FormatParameterName(string) string
        #FormatBooleanLiteral(bool) string
        #FormatStringLiteral(string) string
        #FormatCaseInsensitiveLike() string
    }

    class SqlServerCommandVisitor {
        #FormatIdentifier("[", "]")
        #FormatParameterName("@")
        #FormatBooleanLiteral("1"/"0")
    }

    class SnowflakeCommandVisitor {
        #FormatIdentifier(no brackets)
        #FormatParameterName(":")
        #FormatBooleanLiteral("TRUE"/"FALSE")
    }

    CommandVisitor <|-- SqlServerCommandVisitor
    CommandVisitor <|-- SnowflakeCommandVisitor

82% Code Reduction: The Template Method pattern reduced Snowflake-specific code from 280 lines to 50 lines by extracting common visitor logic into the base class.

3. Expression System

Type-safe expression building with operator overloads:

graph TD
    A[Expression] --> B[GenericColumnExpression]
    A --> C[LiteralExpression]
    A --> D[OperatorExpression]
    A --> E[FunctionExpression]

    C --> C1[NumberLiteralExpression]
    C --> C2[StringLiteralExpression]
    C --> C3[BooleanLiteralExpression]

    D --> D1[ComparisonOperatorExpression]
    D --> D2[ArithmeticOperatorExpression]
    D --> D3[LogicalOperatorExpression]

    E --> E1[AggregateFunctionExpression]
    E --> E2[ScalarFunctionExpression]

    style A fill:#ffe1e1
    style B fill:#e1ffe1
    style C fill:#e1ffe1
    style D fill:#e1ffe1
    style E fill:#e1ffe1

4. Data Flow

Complete query building workflow:

sequenceDiagram
    participant User
    participant QueryBreakdown
    participant Expression
    participant CommandVisitor
    participant StatementParser

    User->>QueryBreakdown: AddSelectExpression(expr)
    QueryBreakdown->>CommandVisitor: Visit(expr)
    CommandVisitor-->>QueryBreakdown: SQL string
    QueryBreakdown->>StatementParser: ExtractSqlComments()
    StatementParser-->>QueryBreakdown: SqlClause

    User->>QueryBreakdown: GetSql()
    QueryBreakdown->>QueryBreakdown: Build SELECT clause
    QueryBreakdown->>QueryBreakdown: Build FROM clause
    QueryBreakdown->>QueryBreakdown: Build WHERE clause
    QueryBreakdown-->>User: Complete SQL

Namespace Organization

Namespace Purpose Key Classes
Strata.SqlTools.Breakdowns.SqlServer SQL Server-specific implementations QueryBreakdown, CommandVisitor
Strata.SqlTools.Breakdowns.Snowflake Snowflake-specific implementations QueryBreakdown, CommandVisitor
Strata.SqlTools.Statements.SqlServer SQL Server statement parsing StatementParser, StatementReader, StatementExpressionParser
Strata.SqlTools.Statements.Snowflake Snowflake statement parsing StatementParser, StatementReader, StatementExpressionParser
Strata.SqlTools.Classes Core data structures SqlClause, SqlClauses, Token
Strata.SqlTools.Enums.SQL SQL-related enumerations TokenType, SqlDataType, FilterOperation
Strata.SqlTools.Utilities Helper utilities SqlUtils, ArrayUtils, GuidUtils, StringUtils
Strata.SqlTools.Extensions Extension methods StringBuilderEx
Strata.SqlTools.Expressions Expression tree components Expression, literal/operator/function types
Strata.SqlTools.Interfaces Contracts and abstractions IQueryBreakdown, IStatementReader

Installation

Reference the Strata.Base assembly in your project to access the Strata.SqlTools namespace.

using Strata.SqlTools;

Usage

Basic Query Construction (T-SQL)

// Simple query
var query = new QueryBreakdown("ID, Name, Email", "Users");
query.WhereClause.Clause = "Active = 1";
query.OrderByClause.Clause = "Name ASC";

string sql = query.GetSql();
// Output:
// SELECT
//      ID, Name, Email
// FROM
//      Users
// WHERE
//      Active = 1
// ORDER BY
//      Name ASC

Adding Parameters

var query = new QueryBreakdown("*", "Users");
query.WhereClause.Clause = "UserID = @UserId AND Status = @Status";

// Add parameters
query.AddParameter("UserId", 123);
query.AddParameter("Status", "Active");

// Set parameter values
query.SetParameterValue("UserId", 456);

// Get parameter value
var userId = query.GetParameterValue("UserId"); // Returns 456

Automatic Parameter Extraction

When using AddWhereClause, parameters are automatically extracted and added to the Parameters dictionary:

var query = new QueryBreakdown("*", "Users");

// Parameters are automatically extracted and added
query.AddWhereClause("UserID = @UserId AND Status = @Status");

// Parameters dictionary now contains @UserId and @Status (both initially null)
Assert.That(query.Parameters, Does.ContainKey("@UserId"));
Assert.That(query.Parameters, Does.ContainKey("@Status"));

// Set parameter values
query.SetParameterValue("UserId", 123);
query.SetParameterValue("Status", "Active");

// Adding additional WHERE conditions automatically extracts new parameters
query.AddWhereClause("CreatedDate > @StartDate", "and");
Assert.That(query.Parameters, Does.ContainKey("@StartDate"));

Smart Parameter Management:

  • If a parameter doesn't exist, it's created with a null value
  • If a parameter exists with a null value, setting a value updates it
  • If a parameter exists with a non-null value, it's preserved (not overwritten by repeated AddWhereClause calls)
  • If you attempt to set a parameter to a different type than its existing value, an InvalidOperationException is thrown
// Example: Type-safe parameter management
var query = new QueryBreakdown("*", "Orders");

query.SetParameterValue("@OrderId", 123); // Set as int
query.AddWhereClause("OrderID = @OrderId"); // Preserves existing value (123)

// This would throw InvalidOperationException:
// query.Parameters["@OrderId"] = "string_value"; // Different type!

Snowflake Parameter Support:

Snowflake QueryBreakdown supports both :parameter and @parameter syntax:

// Snowflake with colon parameters
var query = new Snowflake.QueryBreakdown("*", "USERS");
query.AddWhereClause("USER_ID = :UserId AND STATUS = :Status", false);

// Parameters are extracted with colon prefix
Assert.That(query.Parameters, Does.ContainKey(":UserId"));
Assert.That(query.Parameters, Does.ContainKey(":Status"));

Using Constructors

// Constructor with SELECT and FROM
var query1 = new QueryBreakdown("ID, Name", "Users");

// Constructor with SELECT, FROM, and WHERE
var query2 = new QueryBreakdown("ID, Name", "Users", "Active = 1");

// Constructor with all common clauses
var query3 = new QueryBreakdown("ID, Name", "Users", "Active = 1", "Name ASC");

Building WHERE Clauses Dynamically

var query = new QueryBreakdown("*", "Orders");

// Add conditions with AND (default)
query.AddWhereClause("Status = 'Open'");
query.AddWhereClause("Amount > 100");

// Add condition with OR
query.AddWhereClause("Priority = 'High'", "or");

// Result: WHERE Status = 'Open' and Amount > 100 or Priority = 'High'

// Parameters are automatically extracted from WHERE clauses
query.AddWhereClause("CustomerID = @CustomerId");
query.AddWhereClause("Region = @Region", "and");

// Set the parameter values
query.SetParameterValue("@CustomerId", 456);
query.SetParameterValue("@Region", "West");

// Result: WHERE Status = 'Open' and Amount > 100 or Priority = 'High' and CustomerID = @CustomerId and Region = @Region

Expression-Based Query Building

Build queries type-safely using expression trees with operator overloads:

using Strata.SqlTools.Expressions;
using Strata.SqlTools.Expressions.Literals;

var query = new QueryBreakdown();
query.FromClause.Clause = "Products";

// Create column expressions
var productId = new GenericColumnExpression(1, "ProductID", "ID");
var price = new GenericColumnExpression(2, "Price", "Price");
var category = new GenericColumnExpression(3, "Category", "Category");

// Add SELECT expressions with operator overloads
query.AddSelectExpression(productId);  // ProductID AS ID
query.AddSelectExpression(price * 1.1, "PriceWithTax");  // Price * 1.1 AS PriceWithTax
query.AddSelectExpression(price + 10, "PriceWithFee");   // Price + 10 AS PriceWithFee

// Add WHERE expressions with comparison operators
query.AddWhereExpression(price > 100);  // Price > 100
query.AddWhereExpression(category == "Electronics", null, "AND");  // AND Category = 'Electronics'

string sql = query.GetSql();
// SELECT
//      ProductID AS ID, (Price * 1.1) AS PriceWithTax, (Price + 10) AS PriceWithFee
// FROM
//      Products
// WHERE
//      Price > 100 AND Category = 'Electronics'

Advanced Expression Operations

// Complex arithmetic expressions
var revenue = new GenericColumnExpression(1, "Quantity", "Quantity");
var unitPrice = new GenericColumnExpression(2, "UnitPrice", "UnitPrice");
var discount = new GenericColumnExpression(3, "Discount", "Discount");

var totalRevenue = (revenue * unitPrice) * (1 - discount);
query.AddSelectExpression(totalRevenue, "TotalRevenue");

// Multiple comparison operators
var minPrice = new GenericColumnExpression(4, "MinPrice", "MinPrice");
var maxPrice = new GenericColumnExpression(5, "MaxPrice", "MaxPrice");

query.AddWhereExpression(unitPrice >= minPrice, null, "AND");
query.AddWhereExpression(unitPrice <= maxPrice, null, "AND");

// Boolean literal comparisons
var isActive = new GenericColumnExpression(6, "IsActive", "IsActive");
query.AddWhereExpression(isActive == new BooleanLiteralExpression(true), null, "AND");

// Result: WHERE UnitPrice >= MinPrice AND UnitPrice <= MaxPrice AND IsActive = 1

Expression Comments

Add SQL comments to generated expressions for better readability:

var salesAmount = new GenericColumnExpression(1, "SalesAmount", "Amount");

// Add expression with inline comment
query.AddSelectExpression(
    salesAmount * 1.08,
    "-- Calculate amount including 8% tax");

// Add WHERE expression with comment
query.AddWhereExpression(
    salesAmount > 1000,
    "-- Filter for high-value transactions",
    "AND");

// Generated SQL includes comments:
// SELECT
//      (SalesAmount * 1.08) AS AmountWithTax -- Calculate amount including 8% tax
// FROM ...
// WHERE
//      SalesAmount > 1000 -- Filter for high-value transactions

Adding Expressions to GROUP BY, ORDER BY, and HAVING Clauses

Build GROUP BY, ORDER BY, and HAVING clauses using expressions:

using Strata.SqlTools.Expressions;

var query = new QueryBreakdown();
query.FromClause.Clause = "Sales";

// Create column expressions
var region = new GenericColumnExpression(1, "Region", "Region");
var salesAmount = new GenericColumnExpression(2, "SalesAmount", "Amount");
var year = new GenericColumnExpression(3, "Year", "Year");

// Add SELECT with aggregation
query.AddSelectExpression(region);
query.AddSelectExpression(year);
query.AddSelectExpression(new SumExpression(salesAmount), "TotalSales");

// Add GROUP BY expressions
query.AddGroupByExpression(region);
query.AddGroupByExpression(year);

// Add HAVING expression with aggregation filter
var totalSalesExpr = new SumExpression(salesAmount);
query.AddHavingExpression(totalSalesExpr > 10000);

// Add ORDER BY expressions
query.AddOrderByExpression(new SumExpression(salesAmount), "DESC");

string sql = query.GetSql();
// SELECT
//      Region, Year, SUM(Amount) AS TotalSales
// FROM
//      Sales
// GROUP BY
//      Region, Year
// HAVING
//      SUM(Amount) > 10000
// ORDER BY
//      SUM(Amount) DESC

Common Table Expressions (CTEs)

// Create main query
var mainQuery = new QueryBreakdown(
    "p.ProductName, ps.TotalQuantity, ps.TotalRevenue",
    "Products p INNER JOIN ProductSummary ps ON p.ProductID = ps.ProductID");

// Create CTE using QueryBreakdown object
var cte = new QueryBreakdown(
    "ProductID, SUM(Quantity) AS TotalQuantity, SUM(Price * Quantity) AS TotalRevenue",
    "OrderDetails")
{
    GroupByClause = "ProductID",
    HavingClause = "SUM(Quantity) > 10"
};

// Add CTE to main query
mainQuery.AddWithClause("ProductSummary", cte);

string sql = mainQuery.GetSql();
// Output:
// WITH
//      ProductSummary AS (
//           SELECT
//                ProductID, SUM(Quantity) AS TotalQuantity, SUM(Price * Quantity) AS TotalRevenue
//           FROM
//                OrderDetails
//           GROUP BY
//                ProductID
//           HAVING
//                SUM(Quantity) > 10
//      )
// SELECT
//      p.ProductName, ps.TotalQuantity, ps.TotalRevenue
// FROM
//      Products p INNER JOIN ProductSummary ps ON p.ProductID = ps.ProductID

Multiple CTEs

var mainQuery = new QueryBreakdown("*", "FinalData");

// Add multiple CTEs in order
var cte1 = new QueryBreakdown("OrderID, CustomerID, OrderDate", "Orders")
{
    WhereClause = "OrderDate >= '2024-01-01'"
};

var cte2 = new QueryBreakdown("CustomerID, COUNT(*) AS OrderCount", "RecentOrders")
{
    GroupByClause = "CustomerID"
};

var cte3 = new QueryBreakdown("CustomerID, Name, OrderCount",
    "Customers c INNER JOIN CustomerOrders co ON c.CustomerID = co.CustomerID");

mainQuery.AddWithClause("RecentOrders", cte1);
mainQuery.AddWithClause("CustomerOrders", cte2);
mainQuery.AddWithClause("FinalData", cte3);

// CTEs are generated in the order they were added with proper comma separation
string sql = mainQuery.GetSql();

Accessing WITH Clause Objects

var mainQuery = new QueryBreakdown("*", "Products");
var cte = new QueryBreakdown("ProductID, COUNT(*) AS OrderCount", "Orders");
((IQueryBreakdown)cte).GroupByClause = "ProductID";

mainQuery.AddWithClause("ProductOrders", cte);

// Access WithClause objects via the WithClauses collection
IWithClause withClause = mainQuery.WithClauses[0];

Console.WriteLine(withClause.TableName);     // "ProductOrders"
Console.WriteLine(withClause.Query);          // The IQueryBreakdown object
Console.WriteLine(withClause.Sql);            // Parsed SqlClauses structure (if available)
Console.WriteLine(withClause.Comment);        // Any associated SQL comments

// Query property provides access to parameters
if (withClause.Query != null)
{
    var parameters = withClause.Query.Parameters;
    // Work with CTE parameters
}

Adding CTE from SQL String

var mainQuery = new QueryBreakdown("*", "Products");

// Parse SQL string and add as CTE
string cteSql = "SELECT ProductID, COUNT(*) AS OrderCount FROM Orders GROUP BY ProductID";
mainQuery.AddWithClause("ProductOrders", cteSql, isMicrosoftSql: true);

// The SQL is parsed into a QueryBreakdown and added to the CTE list

Parsing Existing SQL

string sql = @"
    SELECT
        u.UserID, u.Name, u.Email,
        (SELECT COUNT(*) FROM Orders o WHERE o.UserID = u.UserID) AS OrderCount
    FROM Users u
    WHERE u.Active = 1
    ORDER BY u.Name";

// Parse the SQL
var query = QueryBreakdown.Parse(sql);

// Access components
Console.WriteLine(query.SelectClause);
Console.WriteLine(query.FromClause);
Console.WriteLine(query.WhereClause);
Console.WriteLine(query.OrderByClause);

// Modify and regenerate
query.WhereClause += " AND u.CreatedDate > '2024-01-01'";
string modifiedSql = query.GetSql();

Safe Parsing with TryParse

string sql = "SELECT * FROM Users WHERE UserID = @UserId";

if (QueryBreakdown.TryParse(sql, out var query, out var error))
{
    Console.WriteLine("Parsed successfully!");
    Console.WriteLine($"Parameters found: {query.Parameters.Count}");
}
else
{
    Console.WriteLine($"Parse failed: {error}");
}

Merging Queries

var baseQuery = new QueryBreakdown("u.ID, u.Name", "Users u");
baseQuery.WhereClause.Clause = "u.Active = 1";

var joinQuery = new QueryBreakdown();
joinQuery.FromClause.Clause = "INNER JOIN Orders o ON u.ID = o.UserID";
joinQuery.WhereClause.Clause = "o.Status = 'Completed'";

// Merge queries
baseQuery.MergeWith(joinQuery);

// Result: FROM Users u INNER JOIN Orders o ON u.ID = o.UserID
//         WHERE u.Active = 1 AND o.Status = 'Completed'

Cloning Queries

var original = new QueryBreakdown("*", "Users")
{
    WhereClause = "Active = 1",
    OrderByClause = "Name ASC"
};
original.AddParameter("Status", "Active");

// Create deep clone
var clone = (QueryBreakdown)original.Clone();

// Modify clone without affecting original
clone.WhereClause = "Active = 0";

Setup and Finish Clauses

var query = new QueryBreakdown("*", "#TempUsers");

// Add setup clauses (executed before main query)
query.SetupClauses.Add("CREATE TABLE #TempUsers (ID INT, Name VARCHAR(100))");
query.SetupClauses.Add("INSERT INTO #TempUsers VALUES (1, 'John'), (2, 'Jane')");

// Add finish clauses (cleanup after main query)
query.FinishClauses.Add("DROP TABLE #TempUsers");

// Generate complete script
string script = query.GetSql(includeSetupFinish: true);

// Or generate only the SELECT without setup/finish
string selectOnly = query.GetSql(includeSetupFinish: false);

Snowflake SQL Usage

Basic Snowflake Query

var query = new SnowflakeQueryBreakdown("ID, NAME, EMAIL", "USERS");
query.WhereClause.Clause = "ACTIVE = 1";
query.OrderByClause.Clause = "NAME ASC LIMIT 100";

string sql = query.GetSql();
// Output uses 4-space Snowflake indentation:
// SELECT
//     ID, NAME, EMAIL
// FROM
//     USERS
// WHERE
//     ACTIVE = 1
// ORDER BY
//     NAME ASC LIMIT 100

Snowflake Parameters (Colon Syntax)

string sql = "SELECT * FROM USERS WHERE USER_ID = :userId AND STATUS = :status";

var query = SnowflakeQueryBreakdown.Parse(sql, isMicrosoftSql: false);

// Snowflake parameters extracted with colon prefix
Console.WriteLine(query.Parameters.ContainsKey(":userId"));  // true
Console.WriteLine(query.Parameters.ContainsKey(":status"));  // true

// Set parameter values
query.SetParameterValue("userId", 123);
query.SetParameterValue("status", "ACTIVE");

Snowflake Expression-Based Query Building

Snowflake supports the same expression API with dialect-specific formatting:

using Strata.SqlTools.Expressions;
using Strata.SqlTools.Breakdowns.Snowflake;

var query = new QueryBreakdown();
query.FromClause.Clause = "SALES";

var region = new GenericColumnExpression(1, "REGION", "REGION");
var salesAmount = new GenericColumnExpression(2, "SALES_AMOUNT", "AMOUNT");
var year = new GenericColumnExpression(3, "YEAR", "YEAR");

// Add SELECT expressions with Snowflake-specific SQL generation
query.AddSelectExpression(region, null, isMicrosoftSql: false);
query.AddSelectExpression(year, null, isMicrosoftSql: false);
query.AddSelectExpression(new SumExpression(salesAmount), null, isMicrosoftSql: false);

// Add GROUP BY expressions with dialect control
query.AddGroupByExpression(region, null, isMicrosoftSql: false);
query.AddGroupByExpression(year, null, isMicrosoftSql: false);

// Add HAVING expression
var totalSales = new SumExpression(salesAmount);
query.AddHavingExpression(totalSales > 10000, null, "AND", isMicrosoftSql: false);

// Add ORDER BY expression
query.AddOrderByExpression(totalSales, "DESC", isMicrosoftSql: false);

// Generates Snowflake-formatted SQL:
// SELECT
//     REGION, YEAR, SUM(AMOUNT)
// FROM
//     SALES
// GROUP BY
//     REGION, YEAR
// HAVING
//     SUM(AMOUNT) > 10000
// ORDER BY
//     SUM(AMOUNT) DESC

Snowflake Type Casting (Double Colon)

var query = new SnowflakeQueryBreakdown(
    "ID::VARCHAR, AMOUNT::DECIMAL(10,2), CREATE_DATE::TIMESTAMP",
    "ORDERS");

string sql = query.GetSql();
// Preserves Snowflake :: casting syntax

Snowflake JSON/VARIANT Data

var query = new SnowflakeQueryBreakdown(
    "JSON_DATA:name::STRING AS NAME, JSON_DATA:age::NUMBER AS AGE",
    "USER_JSON");

string sql = query.GetSql();
// Handles Snowflake JSON path notation

Snowflake CTEs

var mainQuery = new SnowflakeQueryBreakdown(
    "R.REGION_NAME, S.TOTAL_SALES",
    "REGIONS R INNER JOIN SALES_SUMMARY S ON R.REGION_ID = S.REGION_ID");

var salesCte = new SnowflakeQueryBreakdown(
    "REGION_ID, SUM(AMOUNT::DECIMAL(18,2)) AS TOTAL_SALES",
    "SALES")
{
    WhereClause = "SALE_DATE >= :startDate",
    GroupByClause = "REGION_ID"
};

mainQuery.AddWithClause("SALES_SUMMARY", salesCte);

// Uses Snowflake 4-space indentation for CTEs
string sql = mainQuery.GetSql();

Snowflake Window Functions with QUALIFY

string sql = @"
    SELECT
        ID, NAME, DEPARTMENT, SALARY,
        ROW_NUMBER() OVER (PARTITION BY DEPARTMENT ORDER BY SALARY DESC) AS RN
    FROM EMPLOYEES
    QUALIFY ROW_NUMBER() OVER (PARTITION BY DEPARTMENT ORDER BY SALARY DESC) = 1";

var query = SnowflakeQueryBreakdown.Parse(sql, isMicrosoftSql: false);

// QUALIFY clause parsed correctly
Console.WriteLine(query.SelectClause); // Contains window function

Parsing Snowflake vs T-SQL

string sql = "SELECT ProductID, COUNT(*) AS OrderCount FROM Orders GROUP BY ProductID";

// Parse as Snowflake SQL (default)
var snowflakeQuery = SnowflakeQueryBreakdown.Parse(sql, isMicrosoftSql: false);

// Parse as T-SQL (delegates to base QueryBreakdown)
var tsqlQuery = SnowflakeQueryBreakdown.Parse(sql, isMicrosoftSql: true);

// Both work, but generate different formatting
string snowflakeSql = snowflakeQuery.GetSql(); // 4-space indent
string tsqlSql = tsqlQuery.GetSql(); // 5-space indent

Real-World Snowflake Example

// Complex analytical query with multiple CTEs
var salesCte = new SnowflakeQueryBreakdown(
    "REGION_ID, PRODUCT_ID, SUM(AMOUNT::DECIMAL(18,2)) AS TOTAL_SALES",
    "SALES_DATA")
{
    WhereClause = "SALE_DATE >= :startDate AND SALE_DATE < :endDate",
    GroupByClause = "REGION_ID, PRODUCT_ID"
};

var topProductsCte = new SnowflakeQueryBreakdown(
    "REGION_ID, PRODUCT_ID, TOTAL_SALES, ROW_NUMBER() OVER (PARTITION BY REGION_ID ORDER BY TOTAL_SALES DESC) AS RN",
    "SALES_SUMMARY");

var mainQuery = new SnowflakeQueryBreakdown(
    "R.REGION_NAME, P.PRODUCT_NAME, T.TOTAL_SALES",
    "TOP_PRODUCTS T INNER JOIN REGIONS R ON T.REGION_ID = R.ID INNER JOIN PRODUCTS P ON T.PRODUCT_ID = P.ID")
{
    WhereClause = "T.RN <= 10",
    OrderByClause = "R.REGION_NAME, T.TOTAL_SALES DESC LIMIT 100"
};

mainQuery.AddWithClause("SALES_SUMMARY", salesCte);
mainQuery.AddWithClause("TOP_PRODUCTS", topProductsCte);
mainQuery.AddParameter("startDate", "2024-01-01");
mainQuery.AddParameter("endDate", "2024-12-31");

string sql = mainQuery.GetSql();
// Generates properly formatted Snowflake SQL with:
// - Multiple CTEs
// - Window functions
// - Parameters
// - LIMIT clause

API Reference

QueryBreakdown Class

Constructors

  • QueryBreakdown() - Empty constructor
  • QueryBreakdown(string selectClause, string fromClause) - Basic query with comment extraction
  • QueryBreakdown(string selectClause, string fromClause, string whereClause) - With WHERE and comment extraction
  • QueryBreakdown(string selectClause, string fromClause, string whereClause, string orderByClause) - Full constructor with comment extraction

Properties

Clause Properties Each clause property contains both the SQL text and any associated comments:

  • ISqlExpressionClause SelectClause - SELECT clause with comments (can parse into Expression objects)
    • SelectClause.Clause - SELECT clause SQL text
    • SelectClause.Comment - Associated comments
  • ISqlClause FromClause - FROM clause with comments
  • ISqlExpressionClause WhereClause - WHERE clause with comments (can parse into Expression objects)
  • ISqlExpressionClause GroupByClause - GROUP BY clause with comments (can parse into Expression objects)
  • ISqlExpressionClause HavingClause - HAVING clause with comments (can parse into Expression objects)
  • ISqlExpressionClause OrderByClause - ORDER BY clause with comments (can parse into Expression objects)

String Interface Properties (via IQueryBreakdown) For backward compatibility, explicit interface implementation provides string accessors:

  • string IQueryBreakdown.SelectClause - Gets/sets SELECT clause text (auto-updates SqlClause)
  • string IQueryBreakdown.FromClause - Gets/sets FROM clause text
  • string IQueryBreakdown.WhereClause - Gets/sets WHERE clause text
  • string IQueryBreakdown.OrderByClause - Gets/sets ORDER BY clause text
  • string IQueryBreakdown.GroupByClause - Gets/sets GROUP BY clause text
  • string IQueryBreakdown.HavingClause - Gets/sets HAVING clause text

Other Properties

  • IReadOnlyList<IWithClause> WithClauses - Ordered list of WITH clauses (CTEs), each containing TableName, Query, and optional parsed SqlClauses
  • Dictionary<string, object> Parameters - Parameter dictionary
  • IEnumerable<IQueryParam> ParameterList - Parameter list
  • List<string> SetupClauses - Setup statements
  • ArrayList FinishClauses - Cleanup statements
  • bool IsUsingWithClause - Has WITH clause
  • bool IsUsingFromClause - Has FROM clause
  • bool IsUsingWhereClause - Has WHERE clause
  • bool IsUsingOrderByClause - Has ORDER BY
  • bool IsUsingGroupByClause - Has GROUP BY
  • bool IsUsingHavingClause - Has HAVING

Core Methods

  • string GetSql(bool includeSetupFinish = true) - Generate SQL string
  • virtual SqlClauses GetClauses() - Get a SqlClauses object containing the current clause properties
  • virtual void ApplyClauses(SqlClauses? clauses) - Apply SQL clauses from a SqlClauses object (only non-null clauses are applied)
  • void AddParameter(string name, object value) - Add single parameter
  • void AddParameter(IEnumerable<IQueryParam> params) - Add multiple parameters
  • void SetParameterValue(string name, object value) - Update parameter value
  • object GetParameterValue(string name) - Get parameter value
  • void AddWhereClause(string sql) - Add WHERE condition with AND (automatically extracts parameters)
  • void AddWhereClause(string sql, string operation) - Add WHERE with AND/OR (automatically extracts parameters)
  • void AddWithClause(string name, IQueryBreakdown query) - Add CTE from QueryBreakdown
  • virtual void AddWithClause(string name, string sql, bool isMicrosoftSql = true) - Add CTE from SQL string
  • void MergeWith(IQueryBreakdown query) - Merge another query
  • object Clone() - Create deep clone
  • string ToString() - Get SQL string

Expression Methods

  • void AddSelectExpression(Expression expression, string? comment = null) - Add expression to SELECT clause
  • void AddWhereExpression(Expression expression, string? comment = null, string operation = "AND") - Add expression to WHERE clause with AND/OR
  • virtual void AddGroupByExpression(Expression expression, string? comment = null) - Add expression to GROUP BY clause
  • virtual void AddOrderByExpression(Expression expression, string? comment = null) - Add expression to ORDER BY clause
  • virtual void AddHavingExpression(Expression expression, string? comment = null, string operation = "AND") - Add expression to HAVING clause with AND/OR

Expression Parsing Methods

  • virtual IEnumerable<Expression> GetSelectExpressions() - Parse SELECT clause into Expression objects
  • virtual Expression? GetWhereExpression() - Parse WHERE clause into Expression object
  • virtual Expression? GetHavingExpression() - Parse HAVING clause into Expression object
  • protected virtual IStatementExpressionParser CreateExpressionParser() - Factory method for creating dialect-specific parsers

Static Parsing Methods

  • static QueryBreakdown Parse(string sql) - Parse SQL (throws on error)
  • static bool TryParse(string sql, out QueryBreakdown result) - Safe parse
  • static bool TryParse(string sql, out QueryBreakdown result, out string error) - Safe parse with error

SnowflakeQueryBreakdown Class

Inherits all members from QueryBreakdown with Snowflake-specific overrides.

Additional/Overridden Methods

  • override string GetSql(bool includeSetupFinish = true) - Snowflake formatting (4-space indent)
  • override void AddWithClause(string name, string sql, bool isMicrosoftSql = false) - Snowflake parsing by default
  • void AddSelectExpression(Expression expression, string? comment, bool isMicrosoftSql = false) - Snowflake expression formatting
  • void AddWhereExpression(Expression expression, string? comment, string operation, bool isMicrosoftSql = false) - Snowflake WHERE expressions
  • void AddGroupByExpression(Expression expression, string? comment, bool isMicrosoftSql = false) - Snowflake GROUP BY expressions
  • void AddOrderByExpression(Expression expression, string? comment, bool isMicrosoftSql = false) - Snowflake ORDER BY expressions
  • void AddHavingExpression(Expression expression, string? comment, string operation, bool isMicrosoftSql = false) - Snowflake HAVING expressions
  • override protected IStatementExpressionParser CreateExpressionParser() - Returns Snowflake-specific parser
  • static new SnowflakeQueryBreakdown Parse(string sql, bool isMicrosoftSql = false) - Snowflake-aware parsing
  • static bool TryParse(string sql, out SnowflakeQueryBreakdown result, bool isMicrosoftSql = false)
  • static bool TryParse(string sql, out SnowflakeQueryBreakdown result, out string error, bool isMicrosoftSql = false)

Key Differences from Base Class

  • 4-space indentation instead of 5-space
  • Supports :parameter syntax in addition to @parameter
  • Handles Snowflake-specific keywords (LIMIT, QUALIFY)
  • Default isMicrosoftSql = false for parsing and expression methods
  • Uses SnowflakeCommandVisitor for expression SQL generation

Expression Classes

Core Expression Types

  • Expression - Base class for all expressions
  • GenericColumnExpression - Column reference with alias support
  • NumberLiteralExpression - Numeric literals
  • StringLiteralExpression - String literals
  • BooleanLiteralExpression - Boolean literals (true/false)
  • ComparisonOperatorExpression - Comparison operators (==, !=, >, <, >=, <=)
  • ArithmeticOperatorExpression - Arithmetic operators (+, -, *, /)

Expression Operator Overloads

// Arithmetic
Expression + Expression  // Addition
Expression - Expression  // Subtraction
Expression * Expression  // Multiplication
Expression / Expression  // Division

// Comparison
Expression == Expression  // Equality
Expression != Expression  // Inequality
Expression > Expression   // Greater than
Expression < Expression   // Less than
Expression >= Expression  // Greater than or equal
Expression <= Expression  // Less than or equal

SqlClause Class

Represents a SQL clause with optional comments:

public class SqlClause : ISqlClause
{
    public string? Clause { get; set; }    // SQL clause text
    public string? Comment { get; set; }   // Associated comments
}

WithClause Class

Represents a WITH clause (Common Table Expression) with structured query information:

public class WithClause : SqlClause, IWithClause
{
    public string TableName { get; set; }              // CTE table name
    public SqlClauses? Sql { get; set; }               // Parsed query clauses
    public IQueryBreakdown? Query { get; set; }        // Full query breakdown with parameters
}

The WithClause extends SqlClause to provide:

  • TableName: The name identifier for the CTE
  • Sql: Parsed structure of the CTE query (SELECT, FROM, WHERE, etc.)
  • Query: Full IQueryBreakdown reference providing access to parameters and query generation
  • Clause (inherited): The raw SQL text of the CTE for round-trip parsing
  • Comment (inherited): Any SQL comments associated with the CTE

This structure enables QueryBreakdown to manage CTEs with proper parameter handling and nested query support.

SqlClauses Class

Helper structure for holding parsed SQL clauses:

public sealed class SqlClauses
{
    public ISqlExpressionClause? SelectClause { get; set; }
    public ISqlClause? FromClause { get; set; }
    public ISqlExpressionClause? WhereClause { get; set; }
    public ISqlExpressionClause? GroupByClause { get; set; }
    public ISqlExpressionClause? HavingClause { get; set; }
    public ISqlExpressionClause? OrderByClause { get; set; }

    public SqlClauses Copy()  // Creates a copy with same clause references
}

Utility Classes

SqlUtils (Partial Class)

Static utility methods split across multiple files in Utilities/ folder:

  • SqlUtils.cs - Core SQL utilities
  • SqlUtils.Filters.cs - Filter generation
  • SqlColumnHelpers.cs - Column and alias helpers
  • SqlDataTypeHelpers.cs - Data type checks
  • SqlFilterHelpers.cs - Filter value helpers
  • SqlGuidHelpers.cs - GUID-related SQL operations
  • SqlPagingHelpers.cs - Query pagination
  • SqlSchemaHelpers.cs - Schema information
  • SqlAggregationHelpers.cs - Aggregate function helpers

Other Utilities

  • ArrayUtils - Array and CSV conversion utilities
  • GuidUtils - GUID encoding, validation, and parsing
  • StringUtils - String hashing and manipulation

Testing

Comprehensive NUnit tests are available in the Strata.SqlTools.Tests folder:

  • QueryBreakdownTests.cs - 100+ tests covering:

    • Constructor variations
    • Property accessors (SqlClause objects)
    • Parameter management
    • WHERE clause building
    • Expression-based query building (AddSelectExpression, AddWhereExpression)
    • CTE operations
    • Parsing (simple and complex)
    • Merging and cloning
    • SQL injection patterns
    • Whitespace handling
    • Special characters
    • Nested queries
    • Comment preservation
    • Edge cases
  • SnowflakeQueryBreakdownTests.cs - 80+ tests covering:

    • Snowflake-specific syntax
    • Double colon casting
    • VARIANT/JSON data types
    • FLATTEN function
    • Time Travel
    • QUALIFY clause
    • Colon parameters
    • Expression-based building with Snowflake formatting
    • Indentation differences (4-space vs 5-space)
    • Dual-syntax support (isMicrosoftSql parameter)
    • Real-world scenarios
  • StatementReaderTests.cs - Tokenization tests for both SQL Server and Snowflake

  • StatementExpressionParserTests.cs - Expression parsing and tree building

  • CommentTests.cs - SQL comment extraction and preservation

Run tests:

dotnet test Strata.SqlTools.Tests

Best Practices

Security

  • Always use parameters for user input to prevent SQL injection
  • Use the Parameters dictionary for value binding
  • Never concatenate user input directly into SQL strings
  • Prefer expression-based building over string concatenation
// ✅ GOOD - Using parameters
var query = new QueryBreakdown("*", "Users");
query.WhereClause.Clause = "Email = @Email";
query.AddParameter("Email", userInput);

// ✅ BETTER - Using expressions (type-safe)
var email = new GenericColumnExpression(1, "Email", "Email");
query.AddWhereExpression(email == userInput);

// ❌ BAD - SQL injection risk
query.WhereClause.Clause = $"Email = '{userInput}'";

Code Quality

Expression-Based Building

  • Use AddSelectExpression and AddWhereExpression for type-safe query building
  • Leverage operator overloads (+, -, *, /, ==, !=, >, <, >=, <=) for readable code
  • Wrap boolean literals in BooleanLiteralExpression for proper dialect support
  • Use expressions for complex calculations to ensure proper parenthesization
// Type-safe with automatic SQL generation
var revenue = quantity * unitPrice * (1 - discount);
query.AddSelectExpression(revenue, "NetRevenue");

// vs. error-prone string building
query.SelectClause.Clause = "Quantity * UnitPrice * (1 - Discount) AS NetRevenue";

Comment Preservation

  • Access .Clause and .Comment properties of SqlClause objects separately
  • Use comment parameters in expression methods for self-documenting SQL
  • Comments are preserved during parsing and reconstruction
// Access clause and comment separately
Console.WriteLine(query.SelectClause.Clause);   // SQL text
Console.WriteLine(query.SelectClause.Comment);  // Associated comments

// Add expressions with documentation
query.AddSelectExpression(
    salesAmount * 1.08,
    "TotalWithTax",
    "-- Includes 8% sales tax");

Performance

  • Use includeSetupFinish: false when generating CTEs to avoid recursion
  • Cache parsed queries when executing the same SQL multiple times
  • Use TryParse instead of Parse in production to handle errors gracefully
  • Expression trees are evaluated once during SQL generation (no runtime overhead)

Maintainability

  • Use descriptive CTE names that explain their purpose
  • Add comments to complex expressions using the comment parameter
  • Break large queries into multiple CTEs for readability
  • Use the query builder and expressions instead of string concatenation
  • Leverage the visitor pattern for dialect-specific customization

Dialect Handling

  • Always specify isMicrosoftSql parameter when working with Snowflake
  • Use SnowflakeQueryBreakdown for Snowflake-specific features
  • Test with both dialects if building cross-platform queries
  • Expression methods automatically handle dialect differences
// Snowflake with explicit dialect control
var snowflakeQuery = new SnowflakeQueryBreakdown();
snowflakeQuery.AddSelectExpression(
    productId,
    null,
    isMicrosoftSql: false);  // Uses Snowflake formatting

// SQL Server uses default
var sqlServerQuery = new QueryBreakdown();
sqlServerQuery.AddSelectExpression(productId);  // Uses T-SQL formatting

Error Handling

if (QueryBreakdown.TryParse(sql, out var query, out var error))
{
    // Use query
    string result = query.GetSql();
}
else
{
    // Log error and handle gracefully
    _logger.LogError($"Failed to parse SQL: {error}");
    throw new InvalidOperationException($"Invalid SQL: {error}");
}

Limitations

Parsing Limitations

  • Comments: SQL comments are extracted and preserved but may cause issues in some complex scenarios (e.g., comments inside string literals)
  • Dollar-quoted strings: Snowflake $$ syntax has limited support
  • PIVOT/UNPIVOT: Complex pivoting operations may not parse correctly
  • Stored procedures: Only SELECT statements are supported, not stored procedure definitions
  • Some Snowflake features: Advanced features like MATCH_RECOGNIZE have limited support

Expression System Limitations

  • Aggregate functions: Some complex aggregates may require string-based building
  • Window functions: Complex OVER clauses should use string-based WHERE/SELECT clauses
  • Type inference: Expression system doesn't validate SQL types at compile time
  • Subqueries: Nested subqueries in expressions not fully supported

Workarounds

For unsupported scenarios, use direct string assignment:

// Complex window function - use string assignment
query.SelectClause.Clause = @"
    ProductID,
    ROW_NUMBER() OVER (PARTITION BY Category ORDER BY Price DESC) AS RowNum";

// Simple expressions - use expression API
var price = new GenericColumnExpression(1, "Price", "Price");
query.AddWhereExpression(price > 100);

Design Patterns

The library employs several design patterns for extensibility and maintainability:

Template Method Pattern

The CommandVisitor class uses the Template Method pattern to allow dialect-specific SQL formatting:

graph TD
    A[CommandVisitor.Visit] --> B{Expression Type?}
    B -->|Column| C[FormatIdentifier]
    B -->|Parameter| D[FormatParameterName]
    B -->|Boolean| E[FormatBooleanLiteral]
    B -->|String| F[FormatStringLiteral]

    C --> G[Subclass Override]
    D --> G
    E --> G
    F --> G

    G --> H[SQL String]

    style A fill:#ffe1e1
    style G fill:#e1ffe1

Benefits:

  • 82% code reduction in Snowflake implementation (280 lines → 50 lines)
  • Easy to add new SQL dialects
  • Centralized common logic
  • Override only what differs between dialects

Visitor Pattern

Expression trees use the Visitor pattern for SQL generation:

// Expression tree
var expr = (quantity * unitPrice) * (1 - discount);

// Visitor converts to SQL
var visitor = new CommandVisitor();
string sql = expr.Accept(visitor);  // "((Quantity * UnitPrice) * (1 - Discount))"

Builder Pattern

QueryBreakdown acts as a builder for complex SQL queries:

var query = new QueryBreakdown()
    .WithSelect("ID, Name")
    .WithFrom("Users")
    .WithWhere("Active = 1");

// Fluent API for query construction
query.AddSelectExpression(column1);
query.AddWhereExpression(column2 > 100);

Explicit Interface Implementation

Maintains backward compatibility while evolving to SqlClause objects:

// New: Direct SqlClause access
query.SelectClause.Clause = "ID, Name";
query.SelectClause.Comment = "-- User columns";

// Old: String interface (still works)
((IQueryBreakdown)query).SelectClause = "ID, Name";

Credits

This library is developed and maintained by the Strata Decision Technology team.

Contributing

When contributing to this library:

  1. Ensure all existing tests pass
  2. Add tests for new features
  3. Follow existing code style and naming conventions
  4. Update this README with new features and examples
  5. Document any breaking changes

Version History

Current Release

  • Expression System: Type-safe query building with AddSelectExpression and AddWhereExpression methods
  • SqlClause Objects: Refactored to use SqlClause class for better comment preservation
  • Template Method Pattern: Implemented in CommandVisitor for 82% code reduction in Snowflake variant
  • Improved Architecture: Reorganized into focused folders (Utilities, Classes, Enums, Extensions)
  • Comment Preservation: SQL comments extracted and preserved during parsing and reconstruction
  • Dual Syntax Support: Snowflake constructors and expression methods support both SQL Server and Snowflake parsing
  • Comprehensive Testing: 200+ NUnit tests covering expressions, dialects, and edge cases

Previous Releases

  • Full CTE support with ordered list storage
  • Comprehensive test coverage for both SQL Server and Snowflake
  • Added Snowflake SQL support with dialect-specific features
  • Added parameter extraction and management
  • Added query merging and composition
  • Initial release with basic SELECT parsing and generation

Migration Guide

Upgrading to SqlClause Objects

If you were directly assigning to clause properties:

// Old approach (still works via explicit interface)
query.SelectClause.Clause = "ID, Name";

// New approach (recommended)
query.SelectClause.Clause = "ID, Name";
query.SelectClause.Comment = "-- Primary columns";

// Or use expressions (preferred)
var id = new GenericColumnExpression(1, "ID", "ID");
var name = new GenericColumnExpression(2, "Name", "Name");
query.AddSelectExpression(id);
query.AddSelectExpression(name);

Adopting Expression-Based Building

For new code, prefer expressions over strings:

// String-based (old)
query.SelectClause.Clause = "Price * Quantity AS Revenue";
query.WhereClause.Clause = "Price > 100 AND Category = 'Electronics'";

// Expression-based (new - type-safe)
var price = new GenericColumnExpression(1, "Price", "Price");
var quantity = new GenericColumnExpression(2, "Quantity", "Quantity");
var category = new GenericColumnExpression(3, "Category", "Category");

query.AddSelectExpression(price * quantity, "Revenue");
query.AddWhereExpression(price > 100);
query.AddWhereExpression(category == "Electronics", null, "AND");

Support

For questions or issues:

  • Check the test files for additional usage examples
  • Review the inline XML documentation
  • Refer to the Architecture section for design patterns and component relationships
  • Contact the Strata development team

Folder Structure

Strata.SqlTools/
├── Breakdowns/           # Query breakdown implementations
│   └── InsertSqlBreakdown.cs
├── Classes/              # Core data structures
│   ├── SqlClause.cs     # SQL clause with comments
│   ├── SqlClauses.cs    # Container for all clauses
│   ├── Token.cs         # Tokenization result
│   ├── QueryParam.cs
│   ├── SqlFilter.cs
│   └── ...
├── Enums/SQL/           # SQL-related enumerations
│   ├── TokenType.cs     # Lexical token types
│   ├── SqlDataType.cs
│   ├── FilterOperation.cs
│   ├── AggregateFunction.cs
│   └── ...
├── Exceptions/          # Custom exception types
├── Expressions/         # Expression tree components
│   ├── Expression.cs    # Base expression class
│   ├── Literals/        # Literal expressions
│   ├── Functions/       # Function expressions
│   └── Operators/       # Operator expressions
├── Extensions/          # Extension methods
│   └── StringBuilderEx.cs
├── Interfaces/          # Contracts and abstractions
│   ├── IQueryBreakdown.cs
│   ├── IStatementReader.cs
│   └── ...
├── QueryBuilders/       # Query builder implementations
├── Snowflake/          # Snowflake-specific implementations
│   ├── CommandVisitor.cs          # Snowflake SQL generation
│   ├── QueryBreakdown.cs          # Snowflake query representation
│   ├── StatementExpressionParser.cs
│   ├── StatementParser.cs         # Snowflake SQL parsing
│   └── StatementReader.cs         # Snowflake tokenization
├── SqlServer/          # SQL Server-specific implementations
│   ├── CommandVisitor.cs          # T-SQL generation (base)
│   ├── QueryBreakdown.cs          # SQL Server query representation
│   ├── StatementExpressionParser.cs
│   ├── StatementParser.cs         # T-SQL parsing (base)
│   └── StatementReader.cs         # T-SQL tokenization (base)
└── Utilities/          # Helper utilities (partial SqlUtils class)
    ├── ArrayUtils.cs
    ├── GuidUtils.cs
    ├── StringUtils.cs
    ├── SqlUtils.cs                # Core SQL utilities
    ├── SqlUtils.Filters.cs        # Filter generation
    ├── SqlAggregationHelpers.cs   # Aggregate functions
    ├── SqlColumnHelpers.cs        # Column operations
    ├── SqlDataTypeHelpers.cs      # Type checking
    ├── SqlFilterHelpers.cs        # Filter helpers
    ├── SqlGuidHelpers.cs          # GUID operations
    ├── SqlPagingHelpers.cs        # Pagination
    └── SqlSchemaHelpers.cs        # Schema info

Key File Relationships

graph LR
    A[QueryBreakdown] --> B[StatementParser]
    A --> C[CommandVisitor]
    B --> D[SqlClause/SqlClauses]
    C --> E[Expression]
    F[StatementReader] --> G[Token]
    G --> H[TokenType]
    F --> I[StatementExpressionParser]
    I --> E

    style A fill:#ffe1e1
    style D fill:#e1f5ff
    style E fill:#e1ffe1
    style H fill:#ffffcc

License

Copyright © Strata Decision Technology. All rights reserved.