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
AddSelectExpressionandAddWhereExpression - 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
- Colon parameters (
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
nullvalue - If a parameter exists with a
nullvalue, 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
InvalidOperationExceptionis 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 constructorQueryBreakdown(string selectClause, string fromClause)- Basic query with comment extractionQueryBreakdown(string selectClause, string fromClause, string whereClause)- With WHERE and comment extractionQueryBreakdown(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 textSelectClause.Comment- Associated comments
ISqlClause FromClause- FROM clause with commentsISqlExpressionClause 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 textstring IQueryBreakdown.WhereClause- Gets/sets WHERE clause textstring IQueryBreakdown.OrderByClause- Gets/sets ORDER BY clause textstring IQueryBreakdown.GroupByClause- Gets/sets GROUP BY clause textstring 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 SqlClausesDictionary<string, object> Parameters- Parameter dictionaryIEnumerable<IQueryParam> ParameterList- Parameter listList<string> SetupClauses- Setup statementsArrayList FinishClauses- Cleanup statementsbool IsUsingWithClause- Has WITH clausebool IsUsingFromClause- Has FROM clausebool IsUsingWhereClause- Has WHERE clausebool IsUsingOrderByClause- Has ORDER BYbool IsUsingGroupByClause- Has GROUP BYbool IsUsingHavingClause- Has HAVING
Core Methods
string GetSql(bool includeSetupFinish = true)- Generate SQL stringvirtual SqlClauses GetClauses()- Get a SqlClauses object containing the current clause propertiesvirtual 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 parametervoid AddParameter(IEnumerable<IQueryParam> params)- Add multiple parametersvoid SetParameterValue(string name, object value)- Update parameter valueobject GetParameterValue(string name)- Get parameter valuevoid 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 QueryBreakdownvirtual void AddWithClause(string name, string sql, bool isMicrosoftSql = true)- Add CTE from SQL stringvoid MergeWith(IQueryBreakdown query)- Merge another queryobject Clone()- Create deep clonestring ToString()- Get SQL string
Expression Methods
void AddSelectExpression(Expression expression, string? comment = null)- Add expression to SELECT clausevoid AddWhereExpression(Expression expression, string? comment = null, string operation = "AND")- Add expression to WHERE clause with AND/ORvirtual void AddGroupByExpression(Expression expression, string? comment = null)- Add expression to GROUP BY clausevirtual void AddOrderByExpression(Expression expression, string? comment = null)- Add expression to ORDER BY clausevirtual 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 objectsvirtual Expression? GetWhereExpression()- Parse WHERE clause into Expression objectvirtual Expression? GetHavingExpression()- Parse HAVING clause into Expression objectprotected 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 parsestatic 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 defaultvoid AddSelectExpression(Expression expression, string? comment, bool isMicrosoftSql = false)- Snowflake expression formattingvoid AddWhereExpression(Expression expression, string? comment, string operation, bool isMicrosoftSql = false)- Snowflake WHERE expressionsvoid AddGroupByExpression(Expression expression, string? comment, bool isMicrosoftSql = false)- Snowflake GROUP BY expressionsvoid AddOrderByExpression(Expression expression, string? comment, bool isMicrosoftSql = false)- Snowflake ORDER BY expressionsvoid AddHavingExpression(Expression expression, string? comment, string operation, bool isMicrosoftSql = false)- Snowflake HAVING expressionsoverride protected IStatementExpressionParser CreateExpressionParser()- Returns Snowflake-specific parserstatic new SnowflakeQueryBreakdown Parse(string sql, bool isMicrosoftSql = false)- Snowflake-aware parsingstatic 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
:parametersyntax in addition to@parameter - Handles Snowflake-specific keywords (LIMIT, QUALIFY)
- Default
isMicrosoftSql = falsefor parsing and expression methods - Uses
SnowflakeCommandVisitorfor expression SQL generation
Expression Classes
Core Expression Types
Expression- Base class for all expressionsGenericColumnExpression- Column reference with alias supportNumberLiteralExpression- Numeric literalsStringLiteralExpression- String literalsBooleanLiteralExpression- 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
IQueryBreakdownreference 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 utilitiesSqlUtils.Filters.cs- Filter generationSqlColumnHelpers.cs- Column and alias helpersSqlDataTypeHelpers.cs- Data type checksSqlFilterHelpers.cs- Filter value helpersSqlGuidHelpers.cs- GUID-related SQL operationsSqlPagingHelpers.cs- Query paginationSqlSchemaHelpers.cs- Schema informationSqlAggregationHelpers.cs- Aggregate function helpers
Other Utilities
ArrayUtils- Array and CSV conversion utilitiesGuidUtils- GUID encoding, validation, and parsingStringUtils- 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
Parametersdictionary 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
AddSelectExpressionandAddWhereExpressionfor type-safe query building - Leverage operator overloads (+, -, *, /, ==, !=, >, <, >=, <=) for readable code
- Wrap boolean literals in
BooleanLiteralExpressionfor 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
.Clauseand.Commentproperties 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: falsewhen generating CTEs to avoid recursion - Cache parsed queries when executing the same SQL multiple times
- Use
TryParseinstead ofParsein 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
isMicrosoftSqlparameter when working with Snowflake - Use
SnowflakeQueryBreakdownfor 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:
- Ensure all existing tests pass
- Add tests for new features
- Follow existing code style and naming conventions
- Update this README with new features and examples
- Document any breaking changes
Version History
Current Release
- Expression System: Type-safe query building with
AddSelectExpressionandAddWhereExpressionmethods - SqlClause Objects: Refactored to use
SqlClauseclass for better comment preservation - Template Method Pattern: Implemented in
CommandVisitorfor 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.